Refine admin static collections and drag sorting

This commit is contained in:
dcconstructions 2026-06-26 12:11:07 +03:00
parent e4bfd93a67
commit 7230e8a319
6 changed files with 415 additions and 198 deletions

View File

@ -28,6 +28,15 @@
--surface-strong: rgba(8, 8, 11, 0.9);
--surface-soft: rgba(255, 255, 255, 0.08);
--field: rgba(255, 255, 255, 0.06);
--nodedc-glass-panel-bg: rgba(28, 28, 30, 0.72);
--nodedc-glass-panel-bg-strong: rgba(28, 28, 30, 0.84);
--nodedc-glass-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.055) 0%, rgba(255, 255, 255, 0.014) 100%),
var(--nodedc-glass-panel-bg);
--nodedc-glass-panel-surface-strong: linear-gradient(180deg, rgba(255, 255, 255, 0.068) 0%, rgba(255, 255, 255, 0.018) 100%),
var(--nodedc-glass-panel-bg-strong);
--nodedc-glass-outline: rgba(255, 255, 255, 0.09);
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.075), 0 22px 72px rgba(0, 0, 0, 0.42);
--nodedc-glass-filter: blur(30px) saturate(1.26) brightness(1.05);
--text-primary: #f7f8f4;
--text-secondary: rgba(247, 248, 244, 0.72);
--text-muted: rgba(247, 248, 244, 0.48);
@ -333,7 +342,9 @@ p {
}
.add-block-button {
font-size: 1.55rem;
width: calc(var(--admin-control-ring) * 0.85);
height: calc(var(--admin-control-ring) * 0.85);
font-size: 1.32rem;
font-weight: 520;
}
@ -484,13 +495,48 @@ p {
}
.block-btn.dragging {
opacity: 0.46;
opacity: 0.16;
}
.sorting-source {
display: none !important;
}
.block-btn.drag-over {
background: rgba(255, 255, 255, 0.16);
}
.block-list.sorting-active .block-btn {
transition:
background 160ms ease,
color 160ms ease,
opacity 160ms ease,
transform 180ms ease,
box-shadow 160ms ease;
}
.sortable-placeholder {
border: 1px dashed rgba(255, 255, 255, 0.32);
border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.07);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 12px 34px rgba(0, 0, 0, 0.22);
}
.collection-list .sortable-placeholder {
border-radius: var(--launcher-radius-control);
}
.sortable-ghost {
position: fixed;
z-index: 300;
pointer-events: none;
opacity: 0.94;
transform: scale(1.015);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
}
.block-copy {
display: grid;
min-width: 0;
@ -896,13 +942,18 @@ p {
}
.collection-add-btn {
min-height: 2.4rem;
display: grid;
width: 2.05rem;
height: 2.05rem;
min-height: 2.05rem;
place-items: center;
border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.9);
color: rgba(8, 8, 10, 0.96);
padding: 0 0.95rem;
font-size: 0.76rem;
padding: 0;
font-size: 1.05rem;
font-weight: 850;
line-height: 1;
}
.collection-list {
@ -914,12 +965,20 @@ p {
overflow: hidden;
border-radius: var(--launcher-radius-control);
background: rgba(255, 255, 255, 0.045);
transition:
background 160ms ease,
opacity 160ms ease,
transform 180ms ease;
}
.collection-item-card[open] {
background: rgba(255, 255, 255, 0.065);
}
.collection-item-card.dragging {
opacity: 0.16;
}
.collection-summary {
min-height: 3rem;
cursor: pointer;
@ -946,6 +1005,7 @@ p {
gap: 0.22rem;
}
.collection-drag-handle,
.collection-icon-btn {
display: grid;
width: 2.2rem;
@ -957,6 +1017,18 @@ p {
font-size: 0.84rem;
}
.collection-drag-handle {
letter-spacing: -0.12em;
cursor: grab;
touch-action: none;
user-select: none;
}
.collection-drag-handle:active {
cursor: grabbing;
}
.collection-drag-handle:hover,
.collection-icon-btn:hover {
background: rgba(255, 255, 255, 0.14);
color: var(--text-primary);
@ -1210,10 +1282,10 @@ textarea {
inset: 0;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.34);
background: rgba(0, 0, 0, 0.38);
padding: 1.4rem;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(22px) saturate(1.08);
-webkit-backdrop-filter: blur(22px) saturate(1.08);
}
.template-modal {
@ -1222,20 +1294,23 @@ textarea {
width: min(58rem, calc(100vw - 2.8rem));
max-height: min(46rem, calc(100vh - 2.8rem));
overflow: hidden;
border: 1px solid var(--nodedc-glass-outline);
border-radius: var(--launcher-radius-modal);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.02)),
rgba(8, 8, 11, 0.8);
box-shadow: 0 44px 130px rgba(0, 0, 0, 0.62);
backdrop-filter: blur(40px) saturate(1.12);
-webkit-backdrop-filter: blur(40px) saturate(1.12);
background: var(--nodedc-glass-panel-surface-strong);
box-shadow:
var(--nodedc-glass-panel-shadow),
0 46px 140px rgba(0, 0, 0, 0.62);
backdrop-filter: var(--nodedc-glass-filter);
-webkit-backdrop-filter: var(--nodedc-glass-filter);
}
.template-modal::before {
position: absolute;
inset: 0;
border-radius: inherit;
background: rgba(255, 255, 255, 0.025);
background:
radial-gradient(circle at 22% 0%, rgba(255, 255, 255, 0.14), transparent 32%),
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.01));
content: "";
pointer-events: none;
}

View File

@ -46,6 +46,7 @@ const STATIC_FALLBACK = {
enabled: true,
content: {
navigation: {
logoSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
logoAlt: "главная",
brandLabel: "NODE.DC",
links: [
@ -58,16 +59,19 @@ const STATIC_FALLBACK = {
notifications: [
{
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> — ИЮЛЬ 2026',
eyebrowClass: "red-txt gray",
title: "Открытая бета",
body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
},
{
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
eyebrowClass: "red-txt gray",
title: "Альфа для Founder-доступа",
body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
},
{
eyebrowHtml: "СЕЙЧАС",
eyebrowClass: "red-txt",
title: "Инвайты в закрытую бету уже отправлены.",
body: "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
},
@ -88,26 +92,37 @@ const STATIC_FALLBACK = {
},
},
editableFields: [
field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка"),
field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка"),
field("content.navigation.links.0.label", "Ссылка 1: текст", "text", "text", "Шапка"),
field("content.navigation.links.0.href", "Ссылка 1: адрес", "url", "attr", "Шапка"),
field("content.navigation.links.1.label", "Ссылка 2: текст", "text", "text", "Шапка"),
field("content.navigation.links.1.href", "Ссылка 2: адрес", "url", "attr", "Шапка"),
field("content.navigation.links.2.label", "Ссылка 3: текст", "text", "text", "Шапка"),
field("content.navigation.links.2.href", "Ссылка 3: адрес", "url", "attr", "Шапка"),
field("content.navigation.links.2.target", "Ссылка 3: target", "text", "attr", "Шапка"),
field("content.navigation.links.3.label", "Ссылка 4: текст", "text", "text", "Шапка"),
field("content.navigation.links.3.href", "Ссылка 4: адрес", "url", "attr", "Шапка"),
field("content.notifications.0.eyebrowHtml", "Уведомление 1: дата/статус", "html", "html", "Уведомления"),
field("content.notifications.0.title", "Уведомление 1: заголовок", "text", "text", "Уведомления"),
field("content.notifications.0.body", "Уведомление 1: текст", "textarea", "text", "Уведомления"),
field("content.notifications.1.eyebrowHtml", "Уведомление 2: дата/статус", "html", "html", "Уведомления"),
field("content.notifications.1.title", "Уведомление 2: заголовок", "text", "text", "Уведомления"),
field("content.notifications.1.body", "Уведомление 2: текст", "textarea", "text", "Уведомления"),
field("content.notifications.2.eyebrowHtml", "Уведомление 3: дата/статус", "html", "html", "Уведомления"),
field("content.notifications.2.title", "Уведомление 3: заголовок", "text", "text", "Уведомления"),
field("content.notifications.2.body", "Уведомление 3: текст", "textarea", "text", "Уведомления"),
field("content.navigation.logoSrc", "Логотип в шапке", "media", "attr", "Шапка: логотип"),
field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка: логотип"),
field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка: логотип"),
collection(
"content.navigation.links",
"Кнопки шапки",
"Шапка: ссылки",
{ label: "Новая ссылка", href: "index.html#", target: "" },
[
field("label", "Текст кнопки", "text", "text", "Шапка: ссылки"),
field("href", "Адрес", "url", "attr", "Шапка: ссылки"),
field("target", "Target", "text", "attr", "Шапка: ссылки"),
],
),
collection(
"content.notifications",
"Уведомления",
"Уведомления",
{
eyebrowHtml: "СЕЙЧАС",
eyebrowClass: "red-txt gray",
title: "Новое уведомление",
body: "Текст уведомления.",
},
[
field("eyebrowHtml", "Дата / статус", "html", "html", "Уведомления"),
field("eyebrowClass", "CSS-класс статуса", "text", "attr", "Уведомления"),
field("title", "Заголовок", "text", "text", "Уведомления"),
field("body", "Текст", "textarea", "text", "Уведомления"),
],
),
field("content.dock.appTarget", "Dock: ID приложения", "text", "text", "Dock"),
field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"),
field("content.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
@ -122,8 +137,9 @@ const STATIC_FALLBACK = {
};
const GROUP_COPY = {
Шапка: "Навигация верхней mac-панели: бренд, ссылки и целевые адреса.",
Уведомления: "Карточки уведомлений справа сверху. Порядок соответствует текущему stack в shell.",
"Шапка: логотип": "Статичный знак верхней mac-панели: файл логотипа, alt и брендовая подпись.",
"Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.",
Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.",
Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
"Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
Заголовок: "Главный заголовок и вводный текст выбранной секции.",
@ -152,6 +168,18 @@ function field(path, label, kind = "text", renderAs = kind === "html" ? "html" :
return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options };
}
function collection(path, label, group, itemTemplate, itemFields) {
return {
path,
label,
kind: "collection",
renderAs: "collection",
group,
itemTemplate,
itemFields,
};
}
function setStatus(message) {
el.status.textContent = message;
}
@ -232,7 +260,16 @@ function ensureStaticElements() {
state.page.staticElements.adminLabel ||= STATIC_FALLBACK.adminLabel;
state.page.staticElements.enabled = true;
state.page.staticElements.content ||= clone(STATIC_FALLBACK.content);
state.page.staticElements.editableFields ||= clone(STATIC_FALLBACK.editableFields);
state.page.staticElements.content.navigation ||= clone(STATIC_FALLBACK.content.navigation);
state.page.staticElements.content.navigation.logoSrc ||= STATIC_FALLBACK.content.navigation.logoSrc;
state.page.staticElements.content.navigation.links ||= clone(STATIC_FALLBACK.content.navigation.links);
state.page.staticElements.content.notifications ||= clone(STATIC_FALLBACK.content.notifications);
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.editableFields = clone(STATIC_FALLBACK.editableFields);
}
function isStaticSelection() {
@ -377,23 +414,81 @@ function blockMeta(block) {
function cleanupDragState() {
state.drag = null;
state.dragTarget = null;
el.regions.querySelectorAll(".dragging, .drag-over").forEach((node) => {
node.classList.remove("dragging", "drag-over");
document.querySelectorAll(".dragging, .drag-over, .sorting-source, .sorting-active").forEach((node) => {
node.classList.remove("dragging", "drag-over", "sorting-source", "sorting-active");
});
document.querySelectorAll(".sortable-ghost, .sortable-placeholder").forEach((node) => node.remove());
}
function dragContextFromPoint(event) {
const target = document.elementFromPoint(event.clientX, event.clientY);
const button = target?.closest?.(".block-btn.has-drag-handle");
if (!button || !el.regions.contains(button)) return null;
function createSortableGhost(source, event) {
const rect = source.getBoundingClientRect();
const ghost = source.cloneNode(true);
return {
node: button,
context: {
regionIndex: Number(button.dataset.regionIndex),
blockIndex: Number(button.dataset.blockIndex),
},
};
ghost.classList.add("sortable-ghost");
ghost.style.width = `${rect.width}px`;
ghost.style.height = `${rect.height}px`;
ghost.dataset.dragOffsetX = String(event.clientX - rect.left);
ghost.dataset.dragOffsetY = String(event.clientY - rect.top);
document.body.append(ghost);
moveSortableGhost(ghost, event);
return ghost;
}
function moveSortableGhost(ghost, event) {
ghost.style.left = `${event.clientX - Number(ghost.dataset.dragOffsetX || 0)}px`;
ghost.style.top = `${event.clientY - Number(ghost.dataset.dragOffsetY || 0)}px`;
}
function createSortablePlaceholder(source) {
const rect = source.getBoundingClientRect();
const placeholder = document.createElement("div");
placeholder.className = "sortable-placeholder";
placeholder.style.height = `${rect.height}px`;
return placeholder;
}
function pointInsideRect(event, rect) {
return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
}
function movePlaceholderByPoint({ event, list, source, placeholder, selector }) {
const rect = list.getBoundingClientRect();
if (!pointInsideRect(event, rect)) return;
const candidates = [...list.querySelectorAll(selector)].filter((node) => node !== source && node !== placeholder);
for (const candidate of candidates) {
const candidateRect = candidate.getBoundingClientRect();
if (event.clientY < candidateRect.top + candidateRect.height / 2) {
list.insertBefore(placeholder, candidate);
return;
}
}
list.append(placeholder);
}
function sortablePlaceholderIndex({ list, placeholder, source, selector }) {
return [...list.querySelectorAll(`${selector}, .sortable-placeholder`)]
.filter((node) => node === placeholder || node !== source)
.indexOf(placeholder);
}
function moveBlockWithinRegionToIndex(regionIndex, fromIndex, toIndex) {
const region = state.page.regions[regionIndex];
if (!region || fromIndex === toIndex || toIndex < 0) {
cleanupDragState();
return;
}
const [block] = region.blocks.splice(fromIndex, 1);
region.blocks.splice(Math.min(toIndex, region.blocks.length), 0, block);
state.selected = { kind: "block", regionIndex, blockIndex: Math.min(toIndex, region.blocks.length - 1) };
cleanupDragState();
renderAll();
markDirty("Порядок блоков изменён локально. Нажмите «Сохранить модель» или «Обновить index.html».");
}
function startPointerDrag(event, button, dragContext) {
@ -401,18 +496,27 @@ function startPointerDrag(event, button, dragContext) {
event.preventDefault();
event.stopPropagation();
const list = button.closest(".block-list");
if (!list) return;
const placeholder = createSortablePlaceholder(button);
const ghost = createSortableGhost(button, event);
state.drag = dragContext;
state.dragTarget = dragContext;
button.classList.add("dragging");
button.classList.add("dragging", "sorting-source");
list.classList.add("sorting-active");
button.after(placeholder);
const onMove = (moveEvent) => {
const target = dragContextFromPoint(moveEvent);
el.regions.querySelectorAll(".drag-over").forEach((node) => node.classList.remove("drag-over"));
if (!target || target.context.regionIndex !== dragContext.regionIndex) return;
state.dragTarget = target.context;
target.node.classList.add("drag-over");
moveSortableGhost(ghost, moveEvent);
movePlaceholderByPoint({
event: moveEvent,
list,
source: button,
placeholder,
selector: ".block-btn.has-drag-handle",
});
};
const onFinish = () => {
@ -420,12 +524,13 @@ function startPointerDrag(event, button, dragContext) {
document.removeEventListener("pointerup", onFinish);
document.removeEventListener("pointercancel", onCancel);
if (state.drag && state.dragTarget) {
reorderBlockWithinRegion(state.drag, state.dragTarget);
return;
}
cleanupDragState();
const nextIndex = sortablePlaceholderIndex({
list,
placeholder,
source: button,
selector: ".block-btn.has-drag-handle",
});
moveBlockWithinRegionToIndex(dragContext.regionIndex, dragContext.blockIndex, nextIndex);
};
const onCancel = () => {
@ -493,7 +598,7 @@ function renderBlockButton({ list, block, meta, active, disabled, onClick, dragC
if (canDrag) {
const handle = document.createElement("span");
handle.className = "block-drag-handle";
handle.draggable = true;
handle.draggable = false;
handle.title = "Перетащить блок";
handle.setAttribute("aria-label", "Перетащить блок");
handle.textContent = "⋮⋮";
@ -683,6 +788,7 @@ function renderEditor() {
el.empty.classList.add("hidden");
el.form.classList.remove("hidden");
el.form.dataset.static = String(isStatic);
el.selectedRegion.textContent = `NODE.DC / ${region.label || region.id}`;
el.selectedTitle.textContent = block.adminLabel || block.id;
el.id.value = block.id || "";
@ -693,6 +799,7 @@ function renderEditor() {
el.id.readOnly = isStatic;
el.anchor.disabled = isStatic;
el.enabled.disabled = isStatic;
el.enabled.closest(".toggle-row").hidden = isStatic;
setStructuralControlsDisabled(isStatic);
renderContentFields(block);
el.json.value = JSON.stringify(block, null, 2);
@ -1024,6 +1131,64 @@ function renderCollectionScalarField({ block, field, grid }) {
grid.append(label);
}
function startCollectionPointerDrag(event, card, items, index, rerender) {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
const list = card.closest(".collection-list");
if (!list) return;
const placeholder = createSortablePlaceholder(card);
const ghost = createSortableGhost(card, event);
card.classList.add("dragging", "sorting-source");
list.classList.add("sorting-active");
card.after(placeholder);
const onMove = (moveEvent) => {
moveSortableGhost(ghost, moveEvent);
movePlaceholderByPoint({
event: moveEvent,
list,
source: card,
placeholder,
selector: ".collection-item-card",
});
};
const onFinish = () => {
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onFinish);
document.removeEventListener("pointercancel", onCancel);
const nextIndex = sortablePlaceholderIndex({
list,
placeholder,
source: card,
selector: ".collection-item-card",
});
cleanupDragState();
if (nextIndex < 0 || nextIndex === index) return;
const [moved] = items.splice(index, 1);
items.splice(Math.min(nextIndex, items.length), 0, moved);
rerender("Порядок элементов коллекции изменён локально.");
};
const onCancel = () => {
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onFinish);
document.removeEventListener("pointercancel", onCancel);
cleanupDragState();
};
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onFinish);
document.addEventListener("pointercancel", onCancel);
}
function renderCollectionField({ block, editableField, grid }) {
const items = collectionItems(block, editableField);
const container = document.createElement("section");
@ -1044,7 +1209,9 @@ function renderCollectionField({ block, editableField, grid }) {
title.textContent = editableField.label || editableField.path;
addButton.className = "collection-add-btn";
addButton.type = "button";
addButton.textContent = "Добавить";
addButton.title = `Добавить элемент в «${editableField.label || editableField.path}»`;
addButton.setAttribute("aria-label", addButton.title);
addButton.textContent = "+";
addButton.addEventListener("click", () => {
items.push(prepareCollectionItemForInsert(items, collectionItemTemplate(editableField)));
rerender(`Добавлен элемент в «${editableField.label || editableField.path}».`);
@ -1058,6 +1225,7 @@ function renderCollectionField({ block, editableField, grid }) {
const summary = document.createElement("summary");
const summaryTitle = document.createElement("span");
const controls = document.createElement("span");
const dragHandle = document.createElement("button");
const body = document.createElement("div");
const itemGrid = document.createElement("div");
@ -1067,6 +1235,17 @@ function renderCollectionField({ block, editableField, grid }) {
summaryTitle.className = "collection-summary-title";
summaryTitle.textContent = collectionItemTitle(item, index);
controls.className = "collection-item-actions";
dragHandle.className = "collection-drag-handle";
dragHandle.type = "button";
dragHandle.title = "Перетащить элемент";
dragHandle.setAttribute("aria-label", "Перетащить элемент");
dragHandle.textContent = "⋮⋮";
dragHandle.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
});
dragHandle.addEventListener("pointerdown", (event) => startCollectionPointerDrag(event, details, items, index, rerender));
controls.append(dragHandle);
[
["↑", "Поднять", () => {

View File

@ -33,23 +33,27 @@
"href": "index.html#pricing-wrap",
"target": ""
}
]
],
"logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg"
},
"notifications": [
{
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> — ИЮЛЬ 2026",
"title": "Открытая бета",
"body": "Открываем доступ всем. Будем собирать и отлаживать вместе."
"body": "Открываем доступ всем. Будем собирать и отлаживать вместе.",
"eyebrowClass": "red-txt gray"
},
{
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> —<span class=\"darker-70\"> </span>ИЮЛЬ 2026",
"title": "Альфа для Founder-доступа",
"body": "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа."
"body": "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
"eyebrowClass": "red-txt gray"
},
{
"eyebrowHtml": "СЕЙЧАС",
"title": "Инвайты в закрытую бету уже отправлены.",
"body": "Сейчас тестируем базовую функциональность и ИИ-интеграцию. "
"body": "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
"eyebrowClass": "red-txt"
}
],
"dock": {
@ -68,145 +72,104 @@
}
},
"editableFields": [
{
"path": "content.navigation.logoSrc",
"label": "Логотип в шапке",
"kind": "media",
"renderAs": "attr",
"group": "Шапка: логотип"
},
{
"path": "content.navigation.logoAlt",
"label": "Логотип в шапке: alt",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
"group": "Шапка: логотип"
},
{
"path": "content.navigation.brandLabel",
"label": "Бренд в шапке",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
"group": "Шапка: логотип"
},
{
"path": "content.navigation.links.0.label",
"label": "Ссылка 1: текст",
"path": "content.navigation.links",
"label": "Кнопки шапки",
"kind": "collection",
"renderAs": "collection",
"group": "Шапка: ссылки",
"itemTemplate": {
"label": "Новая ссылка",
"href": "index.html#",
"target": ""
},
"itemFields": [
{
"path": "label",
"label": "Текст кнопки",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
"group": "Шапка: ссылки"
},
{
"path": "content.navigation.links.0.href",
"label": "Ссылка 1: адрес",
"path": "href",
"label": "Адрес",
"kind": "url",
"renderAs": "attr",
"group": "Шапка"
"group": "Шапка: ссылки"
},
{
"path": "content.navigation.links.1.label",
"label": "Ссылка 2: текст",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
},
{
"path": "content.navigation.links.1.href",
"label": "Ссылка 2: адрес",
"kind": "url",
"renderAs": "attr",
"group": "Шапка"
},
{
"path": "content.navigation.links.2.label",
"label": "Ссылка 3: текст",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
},
{
"path": "content.navigation.links.2.href",
"label": "Ссылка 3: адрес",
"kind": "url",
"renderAs": "attr",
"group": "Шапка"
},
{
"path": "content.navigation.links.2.target",
"label": "Ссылка 3: target",
"path": "target",
"label": "Target",
"kind": "text",
"renderAs": "attr",
"group": "Шапка"
"group": "Шапка: ссылки"
}
]
},
{
"path": "content.navigation.links.3.label",
"label": "Ссылка 4: текст",
"kind": "text",
"renderAs": "text",
"group": "Шапка"
"path": "content.notifications",
"label": "Уведомления",
"kind": "collection",
"renderAs": "collection",
"group": "Уведомления",
"itemTemplate": {
"eyebrowHtml": "СЕЙЧАС",
"eyebrowClass": "red-txt gray",
"title": "Новое уведомление",
"body": "Текст уведомления."
},
"itemFields": [
{
"path": "content.navigation.links.3.href",
"label": "Ссылка 4: адрес",
"kind": "url",
"renderAs": "attr",
"group": "Шапка"
},
{
"path": "content.notifications.0.eyebrowHtml",
"label": "Уведомление 1: дата/статус",
"path": "eyebrowHtml",
"label": "Дата / статус",
"kind": "html",
"renderAs": "html",
"group": "Уведомления"
},
{
"path": "content.notifications.0.title",
"label": "Уведомление 1: заголовок",
"path": "eyebrowClass",
"label": "CSS-класс статуса",
"kind": "text",
"renderAs": "attr",
"group": "Уведомления"
},
{
"path": "title",
"label": "Заголовок",
"kind": "text",
"renderAs": "text",
"group": "Уведомления"
},
{
"path": "content.notifications.0.body",
"label": "Уведомление 1: текст",
"kind": "textarea",
"renderAs": "text",
"group": "Уведомления"
},
{
"path": "content.notifications.1.eyebrowHtml",
"label": "Уведомление 2: дата/статус",
"kind": "html",
"renderAs": "html",
"group": "Уведомления"
},
{
"path": "content.notifications.1.title",
"label": "Уведомление 2: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Уведомления"
},
{
"path": "content.notifications.1.body",
"label": "Уведомление 2: текст",
"kind": "textarea",
"renderAs": "text",
"group": "Уведомления"
},
{
"path": "content.notifications.2.eyebrowHtml",
"label": "Уведомление 3: дата/статус",
"kind": "html",
"renderAs": "html",
"group": "Уведомления"
},
{
"path": "content.notifications.2.title",
"label": "Уведомление 3: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Уведомления"
},
{
"path": "content.notifications.2.body",
"label": "Уведомление 3: текст",
"path": "body",
"label": "Текст",
"kind": "textarea",
"renderAs": "text",
"group": "Уведомления"
}
]
},
{
"path": "content.dock.appTarget",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -144,7 +144,7 @@ async function renderRegion(region) {
return blocks.join("");
}
let output = renderTemplateTokens(shell, { id: page.slug, ...page });
let output = renderTemplateTokens(renderEachBlocks(shell, { id: page.slug, ...page }), { id: page.slug, ...page });
for (const region of page.regions ?? []) {
const token = REGION_TOKEN[region.id];
if (!token) {