Refine admin dock and access card editing

This commit is contained in:
dcconstructions
2026-06-26 14:19:14 +03:00
parent 8472611ef3
commit 25e5afedf2
10 changed files with 573 additions and 167 deletions
+48
View File
@@ -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
View File
@@ -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);
}
}