Refine admin dock and access card editing
This commit is contained in:
@@ -919,6 +919,54 @@ body[data-file-picker-active="true"] .editor {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.boolean-field {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.boolean-field input,
|
||||
.group-head-toggle input {
|
||||
width: 2.42rem;
|
||||
height: 1.32rem;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
padding: 0.16rem;
|
||||
}
|
||||
|
||||
.boolean-field input::before,
|
||||
.group-head-toggle input::before {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
content: "";
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.boolean-field input:checked,
|
||||
.group-head-toggle input:checked {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.boolean-field input:checked::before,
|
||||
.group-head-toggle input:checked::before {
|
||||
background: #050506;
|
||||
transform: translateX(1.1rem);
|
||||
}
|
||||
|
||||
.group-head-toggle {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 0.52rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 850;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collection-field {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
|
||||
+192
-16
@@ -80,8 +80,32 @@ const STATIC_FALLBACK = {
|
||||
},
|
||||
],
|
||||
dock: {
|
||||
appTarget: "app-data",
|
||||
trashAlt: "Белая иконка корзины.",
|
||||
items: [
|
||||
{
|
||||
enabled: true,
|
||||
title: "NODE.DC",
|
||||
href: "#hero",
|
||||
linkTarget: "",
|
||||
targetId: "app-data",
|
||||
iconSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
|
||||
iconAlt: "Иконка NODE.DC",
|
||||
className: "dock-icon ssscript",
|
||||
iconClass: "logo",
|
||||
dividerBefore: false,
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
title: "Корзина",
|
||||
href: "",
|
||||
linkTarget: "",
|
||||
targetId: "",
|
||||
iconSrc: "./assets/webflow/images/69a9ae818a76c773dc3cf7bc_trash.avif",
|
||||
iconAlt: "Белая иконка корзины.",
|
||||
className: "dock-icon no-bg",
|
||||
iconClass: "dock-app-img trash",
|
||||
dividerBefore: true,
|
||||
},
|
||||
],
|
||||
demoFileLabel: "демо-видео.txt",
|
||||
},
|
||||
assets: {
|
||||
@@ -126,8 +150,33 @@ const STATIC_FALLBACK = {
|
||||
field("body", "Текст", "textarea", "text", "Уведомления"),
|
||||
],
|
||||
),
|
||||
field("content.dock.appTarget", "Dock: ID приложения", "text", "text", "Dock"),
|
||||
field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"),
|
||||
collection(
|
||||
"content.dock.items",
|
||||
"Иконки dock-панели",
|
||||
"Dock",
|
||||
{
|
||||
enabled: true,
|
||||
title: "Новое приложение",
|
||||
href: "#hero",
|
||||
linkTarget: "",
|
||||
targetId: "",
|
||||
iconSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
|
||||
iconAlt: "Иконка приложения",
|
||||
className: "dock-icon ssscript",
|
||||
iconClass: "logo",
|
||||
dividerBefore: false,
|
||||
},
|
||||
[
|
||||
field("enabled", "Отображение", "boolean", "boolean", "Dock"),
|
||||
field("title", "Название", "text", "text", "Dock"),
|
||||
field("href", "Ссылка / адрес", "url", "attr", "Dock"),
|
||||
field("linkTarget", "Target ссылки", "text", "attr", "Dock"),
|
||||
field("targetId", "ID окна / приложения", "text", "attr", "Dock"),
|
||||
field("iconSrc", "Иконка", "media", "attr", "Dock"),
|
||||
field("iconAlt", "Alt иконки", "text", "text", "Dock"),
|
||||
field("dividerBefore", "Разделитель слева", "boolean", "boolean", "Dock"),
|
||||
],
|
||||
),
|
||||
field("content.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
|
||||
field("content.assets.webglModelSrc", "WebGL-модель логотипа", "media", "attr", "Модель и логотипы"),
|
||||
field("content.assets.logoPng", "PNG-логотип", "media", "attr", "Модель и логотипы"),
|
||||
@@ -143,11 +192,11 @@ const GROUP_COPY = {
|
||||
"Шапка: логотип": "Статичный знак верхней mac-панели: файл логотипа, alt и брендовая подпись.",
|
||||
"Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.",
|
||||
Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.",
|
||||
Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
|
||||
Dock: "Нижняя системная панель: приложения, корзина, ссылки и подписи файлов.",
|
||||
"Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
|
||||
Заголовок: "Главный заголовок и вводный текст выбранной секции.",
|
||||
"Тезисы первого экрана": "Четыре смысловых тезиса вокруг формы раннего доступа.",
|
||||
"Карточка раннего доступа": "Текст внутри центрального окна приложения.",
|
||||
"Карточка доступа": "Логотипы, тексты и видимость центрального окна приложения.",
|
||||
Форма: "Плейсхолдеры, кнопки, success/failure состояния формы.",
|
||||
"Юридический блок": "Политики, права и нижний текст формы.",
|
||||
"Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
|
||||
@@ -270,7 +319,7 @@ function inferFieldGroup(path) {
|
||||
if (path.startsWith("content.dock.")) return "Dock";
|
||||
if (path.startsWith("content.assets.")) return "Модель и логотипы";
|
||||
if (path.startsWith("content.cards.")) return "Тезисы первого экрана";
|
||||
if (path.startsWith("content.app.")) return "Карточка раннего доступа";
|
||||
if (path.startsWith("content.app.") || path.startsWith("content.accessCard.")) return "Карточка доступа";
|
||||
if (path.startsWith("content.form.")) return "Форма";
|
||||
if (path === "content.legalHtml") return "Юридический блок";
|
||||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||||
@@ -289,6 +338,63 @@ function inferFieldGroup(path) {
|
||||
return "Контент";
|
||||
}
|
||||
|
||||
function createDefaultDockItems(dock = {}, assets = {}) {
|
||||
return [
|
||||
{
|
||||
enabled: true,
|
||||
title: "NODE.DC",
|
||||
href: "#hero",
|
||||
linkTarget: "",
|
||||
targetId: dock.appTarget || "app-data",
|
||||
iconSrc: assets.appIconSrc || STATIC_FALLBACK.content.assets.appIconSrc,
|
||||
iconAlt: assets.appIconAlt || STATIC_FALLBACK.content.assets.appIconAlt,
|
||||
className: "dock-icon ssscript",
|
||||
iconClass: "logo",
|
||||
dividerBefore: false,
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
title: "Корзина",
|
||||
href: "",
|
||||
linkTarget: "",
|
||||
targetId: "",
|
||||
iconSrc: "./assets/webflow/images/69a9ae818a76c773dc3cf7bc_trash.avif",
|
||||
iconAlt: dock.trashAlt || "Белая иконка корзины.",
|
||||
className: "dock-icon no-bg",
|
||||
iconClass: "dock-app-img trash",
|
||||
dividerBefore: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeDock(dock, assets = {}) {
|
||||
const fallbackItems = createDefaultDockItems(dock, assets);
|
||||
|
||||
if (!Array.isArray(dock.items)) {
|
||||
dock.items = fallbackItems;
|
||||
} else {
|
||||
dock.items = dock.items.map((item, index) => {
|
||||
const fallback = fallbackItems[index] || fallbackItems[0];
|
||||
return {
|
||||
enabled: item.enabled !== false,
|
||||
title: item.title || fallback.title || `Элемент ${index + 1}`,
|
||||
href: item.href ?? fallback.href ?? "",
|
||||
linkTarget: item.linkTarget ?? fallback.linkTarget ?? "",
|
||||
targetId: item.targetId ?? fallback.targetId ?? "",
|
||||
iconSrc: item.iconSrc || fallback.iconSrc,
|
||||
iconAlt: item.iconAlt || item.alt || fallback.iconAlt,
|
||||
className: item.className || fallback.className || "dock-icon ssscript",
|
||||
iconClass: item.iconClass || fallback.iconClass || "logo",
|
||||
dividerBefore: Boolean(item.dividerBefore),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
dock.demoFileLabel ||= STATIC_FALLBACK.content.dock.demoFileLabel;
|
||||
delete dock.appTarget;
|
||||
delete dock.trashAlt;
|
||||
}
|
||||
|
||||
function ensureStaticElements() {
|
||||
if (!state.page.staticElements) {
|
||||
state.page.staticElements = clone(STATIC_FALLBACK);
|
||||
@@ -307,8 +413,9 @@ function ensureStaticElements() {
|
||||
state.page.staticElements.content.notifications.forEach((notification, index) => {
|
||||
notification.eyebrowClass ||= STATIC_FALLBACK.content.notifications[index]?.eyebrowClass || "red-txt gray";
|
||||
});
|
||||
state.page.staticElements.content.dock ||= clone(STATIC_FALLBACK.content.dock);
|
||||
state.page.staticElements.content.assets ||= clone(STATIC_FALLBACK.content.assets);
|
||||
state.page.staticElements.content.dock ||= clone(STATIC_FALLBACK.content.dock);
|
||||
normalizeDock(state.page.staticElements.content.dock, state.page.staticElements.content.assets);
|
||||
state.page.staticElements.editableFields = clone(STATIC_FALLBACK.editableFields);
|
||||
}
|
||||
|
||||
@@ -1286,7 +1393,71 @@ function prepareCollectionItemForInsert(items, item) {
|
||||
return nextItem;
|
||||
}
|
||||
|
||||
function syncFieldValue(block, path, input) {
|
||||
setByPath(block, path, input.type === "checkbox" ? input.checked : input.value);
|
||||
el.json.value = JSON.stringify(block, null, 2);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function renderBooleanField({ 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 input = document.createElement("input");
|
||||
|
||||
label.className = "field-control boolean-field";
|
||||
labelRow.className = "field-label-row";
|
||||
labelText.className = "field-label-text";
|
||||
labelText.textContent = editableField.label || editableField.path;
|
||||
kind.className = "field-kind";
|
||||
kind.textContent = editableField.kind || "boolean";
|
||||
path.className = "field-path";
|
||||
path.textContent = editableField.path;
|
||||
labelRow.append(labelText, kind);
|
||||
|
||||
input.dataset.path = editableField.path;
|
||||
input.type = "checkbox";
|
||||
input.checked = Boolean(getByPath(block, editableField.path));
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
syncFieldValue(active, editableField.path, input);
|
||||
});
|
||||
|
||||
label.append(labelRow, input, path);
|
||||
grid.append(label);
|
||||
}
|
||||
|
||||
function renderHeaderBooleanToggle({ block, editableField, head }) {
|
||||
const label = document.createElement("label");
|
||||
const input = document.createElement("input");
|
||||
const text = document.createElement("span");
|
||||
|
||||
label.className = "group-head-toggle";
|
||||
input.dataset.path = editableField.path;
|
||||
input.type = "checkbox";
|
||||
input.checked = Boolean(getByPath(block, editableField.path));
|
||||
text.textContent = "Отображение";
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
syncFieldValue(active, editableField.path, input);
|
||||
});
|
||||
|
||||
label.append(input, text);
|
||||
head.append(label);
|
||||
}
|
||||
|
||||
function renderCollectionScalarField({ block, field, grid }) {
|
||||
if (field.kind === "boolean") {
|
||||
renderBooleanField({ block, editableField: field, grid });
|
||||
return;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
@@ -1317,9 +1488,7 @@ function renderCollectionScalarField({ block, field, grid }) {
|
||||
input.addEventListener("input", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
setByPath(active, field.path, input.value);
|
||||
el.json.value = JSON.stringify(active, null, 2);
|
||||
markDirty();
|
||||
syncFieldValue(active, field.path, input);
|
||||
});
|
||||
|
||||
label.append(labelRow, input, path);
|
||||
@@ -1534,7 +1703,11 @@ function renderContentFields(block) {
|
||||
head.append(titleWrap);
|
||||
grid.className = "group-grid";
|
||||
|
||||
for (const editableField of fields) {
|
||||
const headerFields = fields.filter((field) => field.kind === "boolean" && field.headerToggle);
|
||||
const bodyFields = fields.filter((field) => !headerFields.includes(field));
|
||||
headerFields.forEach((editableField) => renderHeaderBooleanToggle({ block, editableField, head }));
|
||||
|
||||
for (const editableField of bodyFields) {
|
||||
if (editableField.kind === "collection") {
|
||||
renderCollectionField({ block, editableField, grid });
|
||||
continue;
|
||||
@@ -1545,6 +1718,11 @@ function renderContentFields(block) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (editableField.kind === "boolean") {
|
||||
renderBooleanField({ block, editableField, grid });
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
@@ -1575,9 +1753,7 @@ function renderContentFields(block) {
|
||||
input.addEventListener("input", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
setByPath(active, editableField.path, input.value);
|
||||
el.json.value = JSON.stringify(active, null, 2);
|
||||
markDirty();
|
||||
syncFieldValue(active, editableField.path, input);
|
||||
});
|
||||
|
||||
label.append(labelRow, input, path);
|
||||
@@ -1594,7 +1770,7 @@ function syncContentFields() {
|
||||
if (!block) return;
|
||||
|
||||
for (const input of el.contentFields.querySelectorAll("[data-path]")) {
|
||||
setByPath(block, input.dataset.path, input.value);
|
||||
setByPath(block, input.dataset.path, input.type === "checkbox" ? input.checked : input.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user