Refine admin static collections and drag sorting
This commit is contained in:
parent
e4bfd93a67
commit
7230e8a319
105
admin/admin.css
105
admin/admin.css
|
|
@ -28,6 +28,15 @@
|
||||||
--surface-strong: rgba(8, 8, 11, 0.9);
|
--surface-strong: rgba(8, 8, 11, 0.9);
|
||||||
--surface-soft: rgba(255, 255, 255, 0.08);
|
--surface-soft: rgba(255, 255, 255, 0.08);
|
||||||
--field: rgba(255, 255, 255, 0.06);
|
--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-primary: #f7f8f4;
|
||||||
--text-secondary: rgba(247, 248, 244, 0.72);
|
--text-secondary: rgba(247, 248, 244, 0.72);
|
||||||
--text-muted: rgba(247, 248, 244, 0.48);
|
--text-muted: rgba(247, 248, 244, 0.48);
|
||||||
|
|
@ -333,7 +342,9 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-block-button {
|
.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;
|
font-weight: 520;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,13 +495,48 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-btn.dragging {
|
.block-btn.dragging {
|
||||||
opacity: 0.46;
|
opacity: 0.16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorting-source {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-btn.drag-over {
|
.block-btn.drag-over {
|
||||||
background: rgba(255, 255, 255, 0.16);
|
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 {
|
.block-copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -896,13 +942,18 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection-add-btn {
|
.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);
|
border-radius: var(--launcher-radius-circle);
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
color: rgba(8, 8, 10, 0.96);
|
color: rgba(8, 8, 10, 0.96);
|
||||||
padding: 0 0.95rem;
|
padding: 0;
|
||||||
font-size: 0.76rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection-list {
|
.collection-list {
|
||||||
|
|
@ -914,12 +965,20 @@ p {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--launcher-radius-control);
|
border-radius: var(--launcher-radius-control);
|
||||||
background: rgba(255, 255, 255, 0.045);
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
opacity 160ms ease,
|
||||||
|
transform 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection-item-card[open] {
|
.collection-item-card[open] {
|
||||||
background: rgba(255, 255, 255, 0.065);
|
background: rgba(255, 255, 255, 0.065);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collection-item-card.dragging {
|
||||||
|
opacity: 0.16;
|
||||||
|
}
|
||||||
|
|
||||||
.collection-summary {
|
.collection-summary {
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -946,6 +1005,7 @@ p {
|
||||||
gap: 0.22rem;
|
gap: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collection-drag-handle,
|
||||||
.collection-icon-btn {
|
.collection-icon-btn {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 2.2rem;
|
width: 2.2rem;
|
||||||
|
|
@ -957,6 +1017,18 @@ p {
|
||||||
font-size: 0.84rem;
|
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 {
|
.collection-icon-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.14);
|
background: rgba(255, 255, 255, 0.14);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
@ -1210,10 +1282,10 @@ textarea {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgba(0, 0, 0, 0.34);
|
background: rgba(0, 0, 0, 0.38);
|
||||||
padding: 1.4rem;
|
padding: 1.4rem;
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(22px) saturate(1.08);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(22px) saturate(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-modal {
|
.template-modal {
|
||||||
|
|
@ -1222,20 +1294,23 @@ textarea {
|
||||||
width: min(58rem, calc(100vw - 2.8rem));
|
width: min(58rem, calc(100vw - 2.8rem));
|
||||||
max-height: min(46rem, calc(100vh - 2.8rem));
|
max-height: min(46rem, calc(100vh - 2.8rem));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--nodedc-glass-outline);
|
||||||
border-radius: var(--launcher-radius-modal);
|
border-radius: var(--launcher-radius-modal);
|
||||||
background:
|
background: var(--nodedc-glass-panel-surface-strong);
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.02)),
|
box-shadow:
|
||||||
rgba(8, 8, 11, 0.8);
|
var(--nodedc-glass-panel-shadow),
|
||||||
box-shadow: 0 44px 130px rgba(0, 0, 0, 0.62);
|
0 46px 140px rgba(0, 0, 0, 0.62);
|
||||||
backdrop-filter: blur(40px) saturate(1.12);
|
backdrop-filter: var(--nodedc-glass-filter);
|
||||||
-webkit-backdrop-filter: blur(40px) saturate(1.12);
|
-webkit-backdrop-filter: var(--nodedc-glass-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-modal::before {
|
.template-modal::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: inherit;
|
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: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
283
admin/admin.js
283
admin/admin.js
|
|
@ -46,6 +46,7 @@ const STATIC_FALLBACK = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
content: {
|
content: {
|
||||||
navigation: {
|
navigation: {
|
||||||
|
logoSrc: "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
|
||||||
logoAlt: "главная",
|
logoAlt: "главная",
|
||||||
brandLabel: "NODE.DC",
|
brandLabel: "NODE.DC",
|
||||||
links: [
|
links: [
|
||||||
|
|
@ -58,16 +59,19 @@ const STATIC_FALLBACK = {
|
||||||
notifications: [
|
notifications: [
|
||||||
{
|
{
|
||||||
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> — ИЮЛЬ 2026',
|
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> — ИЮЛЬ 2026',
|
||||||
|
eyebrowClass: "red-txt gray",
|
||||||
title: "Открытая бета",
|
title: "Открытая бета",
|
||||||
body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
|
body: "Открываем доступ всем. Будем собирать и отлаживать вместе.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
|
eyebrowHtml: '<span class="darker-70">ДАЛЕЕ</span> —<span class="darker-70"> </span>ИЮЛЬ 2026',
|
||||||
|
eyebrowClass: "red-txt gray",
|
||||||
title: "Альфа для Founder-доступа",
|
title: "Альфа для Founder-доступа",
|
||||||
body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
|
body: "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
eyebrowHtml: "СЕЙЧАС",
|
eyebrowHtml: "СЕЙЧАС",
|
||||||
|
eyebrowClass: "red-txt",
|
||||||
title: "Инвайты в закрытую бету уже отправлены.",
|
title: "Инвайты в закрытую бету уже отправлены.",
|
||||||
body: "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
|
body: "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
|
||||||
},
|
},
|
||||||
|
|
@ -88,26 +92,37 @@ const STATIC_FALLBACK = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editableFields: [
|
editableFields: [
|
||||||
field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка"),
|
field("content.navigation.logoSrc", "Логотип в шапке", "media", "attr", "Шапка: логотип"),
|
||||||
field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка"),
|
field("content.navigation.logoAlt", "Логотип в шапке: alt", "text", "text", "Шапка: логотип"),
|
||||||
field("content.navigation.links.0.label", "Ссылка 1: текст", "text", "text", "Шапка"),
|
field("content.navigation.brandLabel", "Бренд в шапке", "text", "text", "Шапка: логотип"),
|
||||||
field("content.navigation.links.0.href", "Ссылка 1: адрес", "url", "attr", "Шапка"),
|
collection(
|
||||||
field("content.navigation.links.1.label", "Ссылка 2: текст", "text", "text", "Шапка"),
|
"content.navigation.links",
|
||||||
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", "Шапка"),
|
{ label: "Новая ссылка", href: "index.html#", target: "" },
|
||||||
field("content.navigation.links.2.target", "Ссылка 3: target", "text", "attr", "Шапка"),
|
[
|
||||||
field("content.navigation.links.3.label", "Ссылка 4: текст", "text", "text", "Шапка"),
|
field("label", "Текст кнопки", "text", "text", "Шапка: ссылки"),
|
||||||
field("content.navigation.links.3.href", "Ссылка 4: адрес", "url", "attr", "Шапка"),
|
field("href", "Адрес", "url", "attr", "Шапка: ссылки"),
|
||||||
field("content.notifications.0.eyebrowHtml", "Уведомление 1: дата/статус", "html", "html", "Уведомления"),
|
field("target", "Target", "text", "attr", "Шапка: ссылки"),
|
||||||
field("content.notifications.0.title", "Уведомление 1: заголовок", "text", "text", "Уведомления"),
|
],
|
||||||
field("content.notifications.0.body", "Уведомление 1: текст", "textarea", "text", "Уведомления"),
|
),
|
||||||
field("content.notifications.1.eyebrowHtml", "Уведомление 2: дата/статус", "html", "html", "Уведомления"),
|
collection(
|
||||||
field("content.notifications.1.title", "Уведомление 2: заголовок", "text", "text", "Уведомления"),
|
"content.notifications",
|
||||||
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", "Уведомления"),
|
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.appTarget", "Dock: ID приложения", "text", "text", "Dock"),
|
||||||
field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"),
|
field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"),
|
||||||
field("content.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
|
field("content.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
|
||||||
|
|
@ -122,8 +137,9 @@ const STATIC_FALLBACK = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const GROUP_COPY = {
|
const GROUP_COPY = {
|
||||||
Шапка: "Навигация верхней mac-панели: бренд, ссылки и целевые адреса.",
|
"Шапка: логотип": "Статичный знак верхней mac-панели: файл логотипа, alt и брендовая подпись.",
|
||||||
Уведомления: "Карточки уведомлений справа сверху. Порядок соответствует текущему stack в shell.",
|
"Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.",
|
||||||
|
Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.",
|
||||||
Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
|
Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.",
|
||||||
"Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
|
"Модель и логотипы": "Базовые ассеты статичного слоя: 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 };
|
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) {
|
function setStatus(message) {
|
||||||
el.status.textContent = message;
|
el.status.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +260,16 @@ function ensureStaticElements() {
|
||||||
state.page.staticElements.adminLabel ||= STATIC_FALLBACK.adminLabel;
|
state.page.staticElements.adminLabel ||= STATIC_FALLBACK.adminLabel;
|
||||||
state.page.staticElements.enabled = true;
|
state.page.staticElements.enabled = true;
|
||||||
state.page.staticElements.content ||= clone(STATIC_FALLBACK.content);
|
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() {
|
function isStaticSelection() {
|
||||||
|
|
@ -377,23 +414,81 @@ function blockMeta(block) {
|
||||||
function cleanupDragState() {
|
function cleanupDragState() {
|
||||||
state.drag = null;
|
state.drag = null;
|
||||||
state.dragTarget = null;
|
state.dragTarget = null;
|
||||||
el.regions.querySelectorAll(".dragging, .drag-over").forEach((node) => {
|
document.querySelectorAll(".dragging, .drag-over, .sorting-source, .sorting-active").forEach((node) => {
|
||||||
node.classList.remove("dragging", "drag-over");
|
node.classList.remove("dragging", "drag-over", "sorting-source", "sorting-active");
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll(".sortable-ghost, .sortable-placeholder").forEach((node) => node.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragContextFromPoint(event) {
|
function createSortableGhost(source, event) {
|
||||||
const target = document.elementFromPoint(event.clientX, event.clientY);
|
const rect = source.getBoundingClientRect();
|
||||||
const button = target?.closest?.(".block-btn.has-drag-handle");
|
const ghost = source.cloneNode(true);
|
||||||
if (!button || !el.regions.contains(button)) return null;
|
|
||||||
|
|
||||||
return {
|
ghost.classList.add("sortable-ghost");
|
||||||
node: button,
|
ghost.style.width = `${rect.width}px`;
|
||||||
context: {
|
ghost.style.height = `${rect.height}px`;
|
||||||
regionIndex: Number(button.dataset.regionIndex),
|
ghost.dataset.dragOffsetX = String(event.clientX - rect.left);
|
||||||
blockIndex: Number(button.dataset.blockIndex),
|
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) {
|
function startPointerDrag(event, button, dragContext) {
|
||||||
|
|
@ -401,18 +496,27 @@ function startPointerDrag(event, button, dragContext) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const list = button.closest(".block-list");
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
const placeholder = createSortablePlaceholder(button);
|
||||||
|
const ghost = createSortableGhost(button, event);
|
||||||
|
|
||||||
state.drag = dragContext;
|
state.drag = dragContext;
|
||||||
state.dragTarget = 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 onMove = (moveEvent) => {
|
||||||
const target = dragContextFromPoint(moveEvent);
|
moveSortableGhost(ghost, moveEvent);
|
||||||
el.regions.querySelectorAll(".drag-over").forEach((node) => node.classList.remove("drag-over"));
|
movePlaceholderByPoint({
|
||||||
|
event: moveEvent,
|
||||||
if (!target || target.context.regionIndex !== dragContext.regionIndex) return;
|
list,
|
||||||
|
source: button,
|
||||||
state.dragTarget = target.context;
|
placeholder,
|
||||||
target.node.classList.add("drag-over");
|
selector: ".block-btn.has-drag-handle",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = () => {
|
const onFinish = () => {
|
||||||
|
|
@ -420,12 +524,13 @@ function startPointerDrag(event, button, dragContext) {
|
||||||
document.removeEventListener("pointerup", onFinish);
|
document.removeEventListener("pointerup", onFinish);
|
||||||
document.removeEventListener("pointercancel", onCancel);
|
document.removeEventListener("pointercancel", onCancel);
|
||||||
|
|
||||||
if (state.drag && state.dragTarget) {
|
const nextIndex = sortablePlaceholderIndex({
|
||||||
reorderBlockWithinRegion(state.drag, state.dragTarget);
|
list,
|
||||||
return;
|
placeholder,
|
||||||
}
|
source: button,
|
||||||
|
selector: ".block-btn.has-drag-handle",
|
||||||
cleanupDragState();
|
});
|
||||||
|
moveBlockWithinRegionToIndex(dragContext.regionIndex, dragContext.blockIndex, nextIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
|
@ -493,7 +598,7 @@ function renderBlockButton({ list, block, meta, active, disabled, onClick, dragC
|
||||||
if (canDrag) {
|
if (canDrag) {
|
||||||
const handle = document.createElement("span");
|
const handle = document.createElement("span");
|
||||||
handle.className = "block-drag-handle";
|
handle.className = "block-drag-handle";
|
||||||
handle.draggable = true;
|
handle.draggable = false;
|
||||||
handle.title = "Перетащить блок";
|
handle.title = "Перетащить блок";
|
||||||
handle.setAttribute("aria-label", "Перетащить блок");
|
handle.setAttribute("aria-label", "Перетащить блок");
|
||||||
handle.textContent = "⋮⋮";
|
handle.textContent = "⋮⋮";
|
||||||
|
|
@ -683,6 +788,7 @@ function renderEditor() {
|
||||||
|
|
||||||
el.empty.classList.add("hidden");
|
el.empty.classList.add("hidden");
|
||||||
el.form.classList.remove("hidden");
|
el.form.classList.remove("hidden");
|
||||||
|
el.form.dataset.static = String(isStatic);
|
||||||
el.selectedRegion.textContent = `NODE.DC / ${region.label || region.id}`;
|
el.selectedRegion.textContent = `NODE.DC / ${region.label || region.id}`;
|
||||||
el.selectedTitle.textContent = block.adminLabel || block.id;
|
el.selectedTitle.textContent = block.adminLabel || block.id;
|
||||||
el.id.value = block.id || "";
|
el.id.value = block.id || "";
|
||||||
|
|
@ -693,6 +799,7 @@ function renderEditor() {
|
||||||
el.id.readOnly = isStatic;
|
el.id.readOnly = isStatic;
|
||||||
el.anchor.disabled = isStatic;
|
el.anchor.disabled = isStatic;
|
||||||
el.enabled.disabled = isStatic;
|
el.enabled.disabled = isStatic;
|
||||||
|
el.enabled.closest(".toggle-row").hidden = isStatic;
|
||||||
setStructuralControlsDisabled(isStatic);
|
setStructuralControlsDisabled(isStatic);
|
||||||
renderContentFields(block);
|
renderContentFields(block);
|
||||||
el.json.value = JSON.stringify(block, null, 2);
|
el.json.value = JSON.stringify(block, null, 2);
|
||||||
|
|
@ -1024,6 +1131,64 @@ function renderCollectionScalarField({ block, field, grid }) {
|
||||||
grid.append(label);
|
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 }) {
|
function renderCollectionField({ block, editableField, grid }) {
|
||||||
const items = collectionItems(block, editableField);
|
const items = collectionItems(block, editableField);
|
||||||
const container = document.createElement("section");
|
const container = document.createElement("section");
|
||||||
|
|
@ -1044,7 +1209,9 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
title.textContent = editableField.label || editableField.path;
|
title.textContent = editableField.label || editableField.path;
|
||||||
addButton.className = "collection-add-btn";
|
addButton.className = "collection-add-btn";
|
||||||
addButton.type = "button";
|
addButton.type = "button";
|
||||||
addButton.textContent = "Добавить";
|
addButton.title = `Добавить элемент в «${editableField.label || editableField.path}»`;
|
||||||
|
addButton.setAttribute("aria-label", addButton.title);
|
||||||
|
addButton.textContent = "+";
|
||||||
addButton.addEventListener("click", () => {
|
addButton.addEventListener("click", () => {
|
||||||
items.push(prepareCollectionItemForInsert(items, collectionItemTemplate(editableField)));
|
items.push(prepareCollectionItemForInsert(items, collectionItemTemplate(editableField)));
|
||||||
rerender(`Добавлен элемент в «${editableField.label || editableField.path}».`);
|
rerender(`Добавлен элемент в «${editableField.label || editableField.path}».`);
|
||||||
|
|
@ -1058,6 +1225,7 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
const summary = document.createElement("summary");
|
const summary = document.createElement("summary");
|
||||||
const summaryTitle = document.createElement("span");
|
const summaryTitle = document.createElement("span");
|
||||||
const controls = document.createElement("span");
|
const controls = document.createElement("span");
|
||||||
|
const dragHandle = document.createElement("button");
|
||||||
const body = document.createElement("div");
|
const body = document.createElement("div");
|
||||||
const itemGrid = document.createElement("div");
|
const itemGrid = document.createElement("div");
|
||||||
|
|
||||||
|
|
@ -1067,6 +1235,17 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
summaryTitle.className = "collection-summary-title";
|
summaryTitle.className = "collection-summary-title";
|
||||||
summaryTitle.textContent = collectionItemTitle(item, index);
|
summaryTitle.textContent = collectionItemTitle(item, index);
|
||||||
controls.className = "collection-item-actions";
|
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);
|
||||||
|
|
||||||
[
|
[
|
||||||
["↑", "Поднять", () => {
|
["↑", "Поднять", () => {
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,27 @@
|
||||||
"href": "index.html#pricing-wrap",
|
"href": "index.html#pricing-wrap",
|
||||||
"target": ""
|
"target": ""
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg"
|
||||||
},
|
},
|
||||||
"notifications": [
|
"notifications": [
|
||||||
{
|
{
|
||||||
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> — ИЮЛЬ 2026",
|
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> — ИЮЛЬ 2026",
|
||||||
"title": "Открытая бета",
|
"title": "Открытая бета",
|
||||||
"body": "Открываем доступ всем. Будем собирать и отлаживать вместе."
|
"body": "Открываем доступ всем. Будем собирать и отлаживать вместе.",
|
||||||
|
"eyebrowClass": "red-txt gray"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> —<span class=\"darker-70\"> </span>ИЮЛЬ 2026",
|
"eyebrowHtml": "<span class=\"darker-70\">ДАЛЕЕ</span> —<span class=\"darker-70\"> </span>ИЮЛЬ 2026",
|
||||||
"title": "Альфа для Founder-доступа",
|
"title": "Альфа для Founder-доступа",
|
||||||
"body": "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа."
|
"body": "На второй фазе тестирования бета открыта для всех подписчиков Founder-тарифа.",
|
||||||
|
"eyebrowClass": "red-txt gray"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"eyebrowHtml": "СЕЙЧАС",
|
"eyebrowHtml": "СЕЙЧАС",
|
||||||
"title": "Инвайты в закрытую бету уже отправлены.",
|
"title": "Инвайты в закрытую бету уже отправлены.",
|
||||||
"body": "Сейчас тестируем базовую функциональность и ИИ-интеграцию. "
|
"body": "Сейчас тестируем базовую функциональность и ИИ-интеграцию. ",
|
||||||
|
"eyebrowClass": "red-txt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dock": {
|
"dock": {
|
||||||
|
|
@ -68,145 +72,104 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editableFields": [
|
"editableFields": [
|
||||||
|
{
|
||||||
|
"path": "content.navigation.logoSrc",
|
||||||
|
"label": "Логотип в шапке",
|
||||||
|
"kind": "media",
|
||||||
|
"renderAs": "attr",
|
||||||
|
"group": "Шапка: логотип"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "content.navigation.logoAlt",
|
"path": "content.navigation.logoAlt",
|
||||||
"label": "Логотип в шапке: alt",
|
"label": "Логотип в шапке: alt",
|
||||||
"kind": "text",
|
"kind": "text",
|
||||||
"renderAs": "text",
|
"renderAs": "text",
|
||||||
"group": "Шапка"
|
"group": "Шапка: логотип"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "content.navigation.brandLabel",
|
"path": "content.navigation.brandLabel",
|
||||||
"label": "Бренд в шапке",
|
"label": "Бренд в шапке",
|
||||||
"kind": "text",
|
"kind": "text",
|
||||||
"renderAs": "text",
|
"renderAs": "text",
|
||||||
"group": "Шапка"
|
"group": "Шапка: логотип"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "content.navigation.links.0.label",
|
"path": "content.navigation.links",
|
||||||
"label": "Ссылка 1: текст",
|
"label": "Кнопки шапки",
|
||||||
"kind": "text",
|
"kind": "collection",
|
||||||
"renderAs": "text",
|
"renderAs": "collection",
|
||||||
"group": "Шапка"
|
"group": "Шапка: ссылки",
|
||||||
|
"itemTemplate": {
|
||||||
|
"label": "Новая ссылка",
|
||||||
|
"href": "index.html#",
|
||||||
|
"target": ""
|
||||||
|
},
|
||||||
|
"itemFields": [
|
||||||
|
{
|
||||||
|
"path": "label",
|
||||||
|
"label": "Текст кнопки",
|
||||||
|
"kind": "text",
|
||||||
|
"renderAs": "text",
|
||||||
|
"group": "Шапка: ссылки"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "href",
|
||||||
|
"label": "Адрес",
|
||||||
|
"kind": "url",
|
||||||
|
"renderAs": "attr",
|
||||||
|
"group": "Шапка: ссылки"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "target",
|
||||||
|
"label": "Target",
|
||||||
|
"kind": "text",
|
||||||
|
"renderAs": "attr",
|
||||||
|
"group": "Шапка: ссылки"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "content.navigation.links.0.href",
|
"path": "content.notifications",
|
||||||
"label": "Ссылка 1: адрес",
|
"label": "Уведомления",
|
||||||
"kind": "url",
|
"kind": "collection",
|
||||||
"renderAs": "attr",
|
"renderAs": "collection",
|
||||||
"group": "Шапка"
|
"group": "Уведомления",
|
||||||
},
|
"itemTemplate": {
|
||||||
{
|
"eyebrowHtml": "СЕЙЧАС",
|
||||||
"path": "content.navigation.links.1.label",
|
"eyebrowClass": "red-txt gray",
|
||||||
"label": "Ссылка 2: текст",
|
"title": "Новое уведомление",
|
||||||
"kind": "text",
|
"body": "Текст уведомления."
|
||||||
"renderAs": "text",
|
},
|
||||||
"group": "Шапка"
|
"itemFields": [
|
||||||
},
|
{
|
||||||
{
|
"path": "eyebrowHtml",
|
||||||
"path": "content.navigation.links.1.href",
|
"label": "Дата / статус",
|
||||||
"label": "Ссылка 2: адрес",
|
"kind": "html",
|
||||||
"kind": "url",
|
"renderAs": "html",
|
||||||
"renderAs": "attr",
|
"group": "Уведомления"
|
||||||
"group": "Шапка"
|
},
|
||||||
},
|
{
|
||||||
{
|
"path": "eyebrowClass",
|
||||||
"path": "content.navigation.links.2.label",
|
"label": "CSS-класс статуса",
|
||||||
"label": "Ссылка 3: текст",
|
"kind": "text",
|
||||||
"kind": "text",
|
"renderAs": "attr",
|
||||||
"renderAs": "text",
|
"group": "Уведомления"
|
||||||
"group": "Шапка"
|
},
|
||||||
},
|
{
|
||||||
{
|
"path": "title",
|
||||||
"path": "content.navigation.links.2.href",
|
"label": "Заголовок",
|
||||||
"label": "Ссылка 3: адрес",
|
"kind": "text",
|
||||||
"kind": "url",
|
"renderAs": "text",
|
||||||
"renderAs": "attr",
|
"group": "Уведомления"
|
||||||
"group": "Шапка"
|
},
|
||||||
},
|
{
|
||||||
{
|
"path": "body",
|
||||||
"path": "content.navigation.links.2.target",
|
"label": "Текст",
|
||||||
"label": "Ссылка 3: target",
|
"kind": "textarea",
|
||||||
"kind": "text",
|
"renderAs": "text",
|
||||||
"renderAs": "attr",
|
"group": "Уведомления"
|
||||||
"group": "Шапка"
|
}
|
||||||
},
|
]
|
||||||
{
|
|
||||||
"path": "content.navigation.links.3.label",
|
|
||||||
"label": "Ссылка 4: текст",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Шапка"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.navigation.links.3.href",
|
|
||||||
"label": "Ссылка 4: адрес",
|
|
||||||
"kind": "url",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Шапка"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.notifications.0.eyebrowHtml",
|
|
||||||
"label": "Уведомление 1: дата/статус",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "Уведомления"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.notifications.0.title",
|
|
||||||
"label": "Уведомление 1: заголовок",
|
|
||||||
"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: текст",
|
|
||||||
"kind": "textarea",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Уведомления"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "content.dock.appTarget",
|
"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
|
|
@ -144,7 +144,7 @@ async function renderRegion(region) {
|
||||||
return blocks.join("");
|
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 ?? []) {
|
for (const region of page.regions ?? []) {
|
||||||
const token = REGION_TOKEN[region.id];
|
const token = REGION_TOKEN[region.id];
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue