Add admin scroll controls and site interaction fixes
This commit is contained in:
@@ -923,6 +923,72 @@ body[data-file-picker-active="true"] .editor {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.range-field {
|
||||
gap: 0.46rem;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
display: flex;
|
||||
min-height: 2.72rem;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
border-radius: var(--launcher-radius-control);
|
||||
background: var(--field);
|
||||
padding: 0 0.86rem;
|
||||
}
|
||||
|
||||
.range-control input[type="range"] {
|
||||
height: 1.5rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
accent-color: #f7f8f4;
|
||||
}
|
||||
|
||||
.range-control input[type="range"]::-webkit-slider-runnable-track {
|
||||
height: 0.32rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.range-control input[type="range"]::-webkit-slider-thumb {
|
||||
width: 1.08rem;
|
||||
height: 1.08rem;
|
||||
margin-top: -0.38rem;
|
||||
appearance: none;
|
||||
border-radius: 50%;
|
||||
background: #f7f8f4;
|
||||
box-shadow: 0 0 0 0.28rem rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.range-control input[type="range"]::-moz-range-track {
|
||||
height: 0.32rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.range-control input[type="range"]::-moz-range-thumb {
|
||||
width: 1.08rem;
|
||||
height: 1.08rem;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #f7f8f4;
|
||||
box-shadow: 0 0 0 0.28rem rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.range-value {
|
||||
min-width: 3.4rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 850;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.boolean-field input,
|
||||
.group-head-toggle input {
|
||||
width: 2.42rem;
|
||||
|
||||
+82
-3
@@ -202,9 +202,12 @@ const GROUP_COPY = {
|
||||
"Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
|
||||
Пункты: "Текстовые пункты внутри секции.",
|
||||
Слайдер: "Видео-превью и подписи карточек внутри горизонтального слайдера.",
|
||||
Скролл: "Скорость локальной прокрутки внутри блока: меньше значение — спокойнее шаг, больше — быстрее.",
|
||||
"Один клик": "Подблок про быстрое добавление и настройку компонентов.",
|
||||
MCP: "Подблок про MCP-сервер и контекст Webflow.",
|
||||
FAQ: "Список вопросов и тексты ответов в mail-интерфейсе.",
|
||||
"Окно FAQ": "Название окна FAQ и служебные подписи mail-интерфейса.",
|
||||
Отправитель: "Профиль отправителя в FAQ: аватар, имя и подписи письма.",
|
||||
FAQ: "Вопросы и ответы mail-интерфейса. Каждый пункт можно добавить, удалить и переставить.",
|
||||
CTA: "Финальный призыв к действию: заголовок, пояснение, кнопка и ссылка.",
|
||||
Тарифы: "Верхняя часть тарифной таблицы: заголовки, планы, цены, кнопки и примечания.",
|
||||
"Строки тарифов": "Повторяемые строки тарифной таблицы и значения по каждому плану.",
|
||||
@@ -324,6 +327,7 @@ function inferFieldGroup(path) {
|
||||
if (path === "content.legalHtml") return "Юридический блок";
|
||||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||||
if (path.startsWith("content.features.")) return "Пункты";
|
||||
if (path.startsWith("content.scroll.")) return "Скролл";
|
||||
if (path.startsWith("content.sliderItems.")) return "Слайдер";
|
||||
if (path.startsWith("content.oneClick.")) return "Один клик";
|
||||
if (path.startsWith("content.mcp.")) return "MCP";
|
||||
@@ -1393,8 +1397,19 @@ function prepareCollectionItemForInsert(items, item) {
|
||||
return nextItem;
|
||||
}
|
||||
|
||||
function fieldInputValue(input) {
|
||||
if (input.type === "checkbox") return input.checked;
|
||||
|
||||
if (input.dataset.valueType === "number" || input.type === "range" || input.type === "number") {
|
||||
const value = Number.parseFloat(input.value);
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
return input.value;
|
||||
}
|
||||
|
||||
function syncFieldValue(block, path, input) {
|
||||
setByPath(block, path, input.type === "checkbox" ? input.checked : input.value);
|
||||
setByPath(block, path, fieldInputValue(input));
|
||||
el.json.value = JSON.stringify(block, null, 2);
|
||||
markDirty();
|
||||
}
|
||||
@@ -1431,6 +1446,60 @@ function renderBooleanField({ block, editableField, grid }) {
|
||||
grid.append(label);
|
||||
}
|
||||
|
||||
function formatRangeValue(value, editableField) {
|
||||
const step = Number.parseFloat(editableField.step ?? 1);
|
||||
const precision = Number.isFinite(step) && step > 0 && step < 1 ? String(step).split(".")[1]?.length || 0 : 0;
|
||||
return `${Number(value).toFixed(precision)}${editableField.suffix || ""}`;
|
||||
}
|
||||
|
||||
function renderRangeField({ block, editableField, grid }) {
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
const kind = document.createElement("span");
|
||||
const path = document.createElement("span");
|
||||
const control = document.createElement("span");
|
||||
const input = document.createElement("input");
|
||||
const output = document.createElement("span");
|
||||
const min = Number.parseFloat(editableField.min ?? 0);
|
||||
const max = Number.parseFloat(editableField.max ?? 1);
|
||||
const step = Number.parseFloat(editableField.step ?? 0.01);
|
||||
const rawValue = Number.parseFloat(getByPath(block, editableField.path));
|
||||
const value = Number.isFinite(rawValue) ? rawValue : min;
|
||||
|
||||
label.className = "field-control range-field wide";
|
||||
labelRow.className = "field-label-row";
|
||||
labelText.className = "field-label-text";
|
||||
labelText.textContent = editableField.label || editableField.path;
|
||||
kind.className = "field-kind";
|
||||
kind.textContent = editableField.kind || "range";
|
||||
path.className = "field-path";
|
||||
path.textContent = editableField.path;
|
||||
control.className = "range-control";
|
||||
output.className = "range-value";
|
||||
labelRow.append(labelText, kind);
|
||||
|
||||
input.dataset.path = editableField.path;
|
||||
input.dataset.valueType = "number";
|
||||
input.type = "range";
|
||||
input.min = Number.isFinite(min) ? String(min) : "0";
|
||||
input.max = Number.isFinite(max) ? String(max) : "1";
|
||||
input.step = Number.isFinite(step) ? String(step) : "0.01";
|
||||
input.value = String(value);
|
||||
output.textContent = formatRangeValue(value, editableField);
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
output.textContent = formatRangeValue(input.value, editableField);
|
||||
syncFieldValue(active, editableField.path, input);
|
||||
});
|
||||
|
||||
control.append(input, output);
|
||||
label.append(labelRow, control, path);
|
||||
grid.append(label);
|
||||
}
|
||||
|
||||
function renderHeaderBooleanToggle({ block, editableField, head }) {
|
||||
const label = document.createElement("label");
|
||||
const input = document.createElement("input");
|
||||
@@ -1458,6 +1527,11 @@ function renderCollectionScalarField({ block, field, grid }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.kind === "range") {
|
||||
renderRangeField({ block, editableField: field, grid });
|
||||
return;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
@@ -1723,6 +1797,11 @@ function renderContentFields(block) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (editableField.kind === "range") {
|
||||
renderRangeField({ block, editableField, grid });
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
@@ -1770,7 +1849,7 @@ function syncContentFields() {
|
||||
if (!block) return;
|
||||
|
||||
for (const input of el.contentFields.querySelectorAll("[data-path]")) {
|
||||
setByPath(block, input.dataset.path, input.type === "checkbox" ? input.checked : input.value);
|
||||
setByPath(block, input.dataset.path, fieldInputValue(input));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user