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

View File

@ -919,6 +919,54 @@ body[data-file-picker-active="true"] .editor {
white-space: nowrap; 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 { .collection-field {
display: grid; display: grid;
grid-column: 1 / -1; grid-column: 1 / -1;

View File

@ -80,8 +80,32 @@ const STATIC_FALLBACK = {
}, },
], ],
dock: { dock: {
appTarget: "app-data", items: [
trashAlt: "Белая иконка корзины.", {
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", demoFileLabel: "демо-видео.txt",
}, },
assets: { assets: {
@ -126,8 +150,33 @@ const STATIC_FALLBACK = {
field("body", "Текст", "textarea", "text", "Уведомления"), field("body", "Текст", "textarea", "text", "Уведомления"),
], ],
), ),
field("content.dock.appTarget", "Dock: ID приложения", "text", "text", "Dock"), collection(
field("content.dock.trashAlt", "Dock: alt корзины", "text", "text", "Dock"), "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.dock.demoFileLabel", "Dock: подпись демо-файла", "text", "text", "Dock"),
field("content.assets.webglModelSrc", "WebGL-модель логотипа", "media", "attr", "Модель и логотипы"), field("content.assets.webglModelSrc", "WebGL-модель логотипа", "media", "attr", "Модель и логотипы"),
field("content.assets.logoPng", "PNG-логотип", "media", "attr", "Модель и логотипы"), field("content.assets.logoPng", "PNG-логотип", "media", "attr", "Модель и логотипы"),
@ -143,11 +192,11 @@ const GROUP_COPY = {
"Шапка: логотип": "Статичный знак верхней mac-панели: файл логотипа, alt и брендовая подпись.", "Шапка: логотип": "Статичный знак верхней mac-панели: файл логотипа, alt и брендовая подпись.",
"Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.", "Шапка: ссылки": "Пункты навигации верхней mac-панели. Каждый пункт можно добавить, удалить и переставить.",
Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.", Уведомления: "Карточки уведомлений справа сверху. Порядок в админке соответствует порядку на сайте.",
Dock: "Нижняя системная панель: связь кнопки приложения, корзина и подписи файлов.", Dock: "Нижняя системная панель: приложения, корзина, ссылки и подписи файлов.",
"Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.", "Модель и логотипы": "Базовые ассеты статичного слоя: GLB-модель, PNG/SVG-логотипы и alt-тексты.",
Заголовок: "Главный заголовок и вводный текст выбранной секции.", Заголовок: "Главный заголовок и вводный текст выбранной секции.",
"Тезисы первого экрана": "Четыре смысловых тезиса вокруг формы раннего доступа.", "Тезисы первого экрана": "Четыре смысловых тезиса вокруг формы раннего доступа.",
"Карточка раннего доступа": "Текст внутри центрального окна приложения.", "Карточка доступа": "Логотипы, тексты и видимость центрального окна приложения.",
Форма: "Плейсхолдеры, кнопки, success/failure состояния формы.", Форма: "Плейсхолдеры, кнопки, success/failure состояния формы.",
"Юридический блок": "Политики, права и нижний текст формы.", "Юридический блок": "Политики, права и нижний текст формы.",
"Видео-окно": "Окно демо-видео и подключенный медиа-файл.", "Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
@ -270,7 +319,7 @@ function inferFieldGroup(path) {
if (path.startsWith("content.dock.")) return "Dock"; if (path.startsWith("content.dock.")) return "Dock";
if (path.startsWith("content.assets.")) return "Модель и логотипы"; if (path.startsWith("content.assets.")) return "Модель и логотипы";
if (path.startsWith("content.cards.")) 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.startsWith("content.form.")) return "Форма";
if (path === "content.legalHtml") return "Юридический блок"; if (path === "content.legalHtml") return "Юридический блок";
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно"; if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
@ -289,6 +338,63 @@ function inferFieldGroup(path) {
return "Контент"; 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() { function ensureStaticElements() {
if (!state.page.staticElements) { if (!state.page.staticElements) {
state.page.staticElements = clone(STATIC_FALLBACK); state.page.staticElements = clone(STATIC_FALLBACK);
@ -307,8 +413,9 @@ function ensureStaticElements() {
state.page.staticElements.content.notifications.forEach((notification, index) => { state.page.staticElements.content.notifications.forEach((notification, index) => {
notification.eyebrowClass ||= STATIC_FALLBACK.content.notifications[index]?.eyebrowClass || "red-txt gray"; 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.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); state.page.staticElements.editableFields = clone(STATIC_FALLBACK.editableFields);
} }
@ -1286,7 +1393,71 @@ function prepareCollectionItemForInsert(items, item) {
return nextItem; 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 }) { function renderCollectionScalarField({ block, field, grid }) {
if (field.kind === "boolean") {
renderBooleanField({ block, editableField: field, grid });
return;
}
const label = document.createElement("label"); const label = document.createElement("label");
const labelRow = document.createElement("span"); const labelRow = document.createElement("span");
const labelText = document.createElement("span"); const labelText = document.createElement("span");
@ -1317,9 +1488,7 @@ function renderCollectionScalarField({ block, field, grid }) {
input.addEventListener("input", () => { input.addEventListener("input", () => {
const active = activeBlock(); const active = activeBlock();
if (!active) return; if (!active) return;
setByPath(active, field.path, input.value); syncFieldValue(active, field.path, input);
el.json.value = JSON.stringify(active, null, 2);
markDirty();
}); });
label.append(labelRow, input, path); label.append(labelRow, input, path);
@ -1534,7 +1703,11 @@ function renderContentFields(block) {
head.append(titleWrap); head.append(titleWrap);
grid.className = "group-grid"; 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") { if (editableField.kind === "collection") {
renderCollectionField({ block, editableField, grid }); renderCollectionField({ block, editableField, grid });
continue; continue;
@ -1545,6 +1718,11 @@ function renderContentFields(block) {
continue; continue;
} }
if (editableField.kind === "boolean") {
renderBooleanField({ block, editableField, grid });
continue;
}
const label = document.createElement("label"); const label = document.createElement("label");
const labelRow = document.createElement("span"); const labelRow = document.createElement("span");
const labelText = document.createElement("span"); const labelText = document.createElement("span");
@ -1575,9 +1753,7 @@ function renderContentFields(block) {
input.addEventListener("input", () => { input.addEventListener("input", () => {
const active = activeBlock(); const active = activeBlock();
if (!active) return; if (!active) return;
setByPath(active, editableField.path, input.value); syncFieldValue(active, editableField.path, input);
el.json.value = JSON.stringify(active, null, 2);
markDirty();
}); });
label.append(labelRow, input, path); label.append(labelRow, input, path);
@ -1594,7 +1770,7 @@ function syncContentFields() {
if (!block) return; if (!block) return;
for (const input of el.contentFields.querySelectorAll("[data-path]")) { 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);
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -35,13 +35,6 @@
"bodyHtml": "<span class=\"darker-70\">Деплой в один клик,</span> непрерывный пайплайн, мгновенный откат и быстрая <span class=\"darker-70\">доставка через CDN.</span>" "bodyHtml": "<span class=\"darker-70\">Деплой в один клик,</span> непрерывный пайплайн, мгновенный откат и быстрая <span class=\"darker-70\">доставка через CDN.</span>"
} }
], ],
"app": {
"iconAlt": "Иконка NODE.DC",
"logoAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
},
"form": { "form": {
"messagePlaceholder": "Сообщение (необязательно)", "messagePlaceholder": "Сообщение (необязательно)",
"namePlaceholder": "Ваше имя*", "namePlaceholder": "Ваше имя*",
@ -56,7 +49,17 @@
"failureHeading": "Не получилось", "failureHeading": "Не получилось",
"failureTextHtml": "Что-то пошло не так. <br/>Попробуйте ещё раз чуть позже.<br/>" "failureTextHtml": "Что-то пошло не так. <br/>Попробуйте ещё раз чуть позже.<br/>"
}, },
"legalHtml": "<a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика конфиденциальности</a> • <a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика cookie</a><br/>© — Все права защищены<br/>2026" "legalHtml": "<a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика конфиденциальности</a> • <a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика cookie</a><br/>© — Все права защищены<br/>2026",
"accessCard": {
"enabled": true,
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
}
}, },
"editableFields": [ "editableFields": [
{ {
@ -144,39 +147,61 @@
"group": "Тезисы первого экрана" "group": "Тезисы первого экрана"
}, },
{ {
"path": "content.app.iconAlt", "path": "content.accessCard.enabled",
"label": "App: alt иконки", "label": "Карточка доступа: отображение",
"kind": "text", "kind": "boolean",
"renderAs": "boolean",
"group": "Карточка доступа",
"headerToggle": true
},
{
"path": "content.accessCard.iconSrc",
"label": "Карточка доступа: иконка",
"kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.logoAlt", "path": "content.accessCard.wordmarkSrc",
"label": "App: alt логотипа", "label": "Карточка доступа: wordmark",
"kind": "text", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.release", "path": "content.accessCard.iconAlt",
"label": "App: релиз", "label": "Карточка доступа: alt иконки",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.heading", "path": "content.accessCard.wordmarkAlt",
"label": "App: заголовок", "label": "Карточка доступа: alt логотипа",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.description", "path": "content.accessCard.release",
"label": "App: описание", "label": "Карточка доступа: релиз",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
},
{
"path": "content.accessCard.heading",
"label": "Карточка доступа: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.description",
"label": "Карточка доступа: описание",
"kind": "textarea",
"renderAs": "text",
"group": "Карточка доступа"
}, },
{ {
"path": "content.form.messagePlaceholder", "path": "content.form.messagePlaceholder",
@ -2042,15 +2067,6 @@
"anchor": "waitlist", "anchor": "waitlist",
"enabled": true, "enabled": true,
"content": { "content": {
"app": {
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
},
"form": { "form": {
"messagePlaceholder": "Сообщение (необязательно)", "messagePlaceholder": "Сообщение (необязательно)",
"namePlaceholder": "Ваше имя*", "namePlaceholder": "Ваше имя*",
@ -2303,57 +2319,75 @@
"iconSrcset": "./assets/webflow/images/69a9fb87bc56142371745926_txt-file-p-500.avif 500w, ./assets/webflow/images/69a9fb87bc56142371745926_txt-file.avif 1024w", "iconSrcset": "./assets/webflow/images/69a9fb87bc56142371745926_txt-file-p-500.avif 500w, ./assets/webflow/images/69a9fb87bc56142371745926_txt-file.avif 1024w",
"iconAlt": "Иконка TXT-файла" "iconAlt": "Иконка TXT-файла"
} }
] ],
"accessCard": {
"enabled": true,
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
}
}, },
"editableFields": [ "editableFields": [
{ {
"path": "content.app.iconSrc", "path": "content.accessCard.enabled",
"label": "Иконка приложения", "label": "Карточка доступа: отображение",
"kind": "boolean",
"renderAs": "boolean",
"group": "Карточка доступа",
"headerToggle": true
},
{
"path": "content.accessCard.iconSrc",
"label": "Карточка доступа: иконка",
"kind": "media", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.iconAlt", "path": "content.accessCard.wordmarkSrc",
"label": "Иконка: alt", "label": "Карточка доступа: wordmark",
"kind": "text",
"renderAs": "attr",
"group": "Карточка раннего доступа"
},
{
"path": "content.app.wordmarkSrc",
"label": "Wordmark",
"kind": "media", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.wordmarkAlt", "path": "content.accessCard.iconAlt",
"label": "Wordmark: alt", "label": "Карточка доступа: alt иконки",
"kind": "text",
"renderAs": "attr",
"group": "Карточка раннего доступа"
},
{
"path": "content.app.release",
"label": "Релиз",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.heading", "path": "content.accessCard.wordmarkAlt",
"label": "Заголовок", "label": "Карточка доступа: alt логотипа",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.description", "path": "content.accessCard.release",
"label": "Описание", "label": "Карточка доступа: релиз",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.heading",
"label": "Карточка доступа: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.description",
"label": "Карточка доступа: описание",
"kind": "textarea", "kind": "textarea",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.form.messagePlaceholder", "path": "content.form.messagePlaceholder",

View File

@ -57,15 +57,39 @@
} }
], ],
"dock": { "dock": {
"appTarget": "app-data", "demoFileLabel": "демо-видео.txt",
"trashAlt": "Белая иконка корзины.", "items": [
"demoFileLabel": "демо-видео.txt" {
"enabled": true,
"title": "NODE.DC",
"href": "#hero",
"linkTarget": "",
"targetId": "app-data",
"iconSrc": "./assets/uploads/images/static-elements/dclogo_white_alpha-mqus5if2.png",
"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
}
]
}, },
"assets": { "assets": {
"webglModelSrc": "./assets/custom/icon.glb", "webglModelSrc": "./assets/custom/icon.glb",
"logoPng": "./assets/custom/logo.png", "logoPng": "./assets/custom/logo.png",
"logoPngAlt": "Логотип NODE.DC", "logoPngAlt": "Логотип NODE.DC",
"appIconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg", "appIconSrc": "./assets/uploads/images/static-elements/dclogo_white_alpha-mqus5if2.png",
"appIconAlt": "Иконка NODE.DC", "appIconAlt": "Иконка NODE.DC",
"appWordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc", "appWordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"appWordmarkAlt": "Логотип NODE.DC" "appWordmarkAlt": "Логотип NODE.DC"
@ -172,19 +196,82 @@
] ]
}, },
{ {
"path": "content.dock.appTarget", "path": "content.dock.items",
"label": "Dock: ID приложения", "label": "Иконки dock-панели",
"kind": "collection",
"renderAs": "collection",
"group": "Dock",
"itemTemplate": {
"enabled": true,
"title": "Новое приложение",
"href": "#hero",
"linkTarget": "",
"targetId": "",
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка приложения",
"className": "dock-icon ssscript",
"iconClass": "logo",
"dividerBefore": false
},
"itemFields": [
{
"path": "enabled",
"label": "Отображение",
"kind": "boolean",
"renderAs": "boolean",
"group": "Dock"
},
{
"path": "title",
"label": "Название",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Dock" "group": "Dock"
}, },
{ {
"path": "content.dock.trashAlt", "path": "href",
"label": "Dock: alt корзины", "label": "Ссылка / адрес",
"kind": "url",
"renderAs": "attr",
"group": "Dock"
},
{
"path": "linkTarget",
"label": "Target ссылки",
"kind": "text",
"renderAs": "attr",
"group": "Dock"
},
{
"path": "targetId",
"label": "ID окна / приложения",
"kind": "text",
"renderAs": "attr",
"group": "Dock"
},
{
"path": "iconSrc",
"label": "Иконка",
"kind": "media",
"renderAs": "attr",
"group": "Dock"
},
{
"path": "iconAlt",
"label": "Alt иконки",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Dock" "group": "Dock"
}, },
{
"path": "dividerBefore",
"label": "Разделитель слева",
"kind": "boolean",
"renderAs": "boolean",
"group": "Dock"
}
]
},
{ {
"path": "content.dock.demoFileLabel", "path": "content.dock.demoFileLabel",
"label": "Dock: подпись демо-файла", "label": "Dock: подпись демо-файла",
@ -283,13 +370,6 @@
"bodyHtml": "<span class=\"darker-70\">Деплой в один клик,</span> непрерывный пайплайн, мгновенный откат и быстрая <span class=\"darker-70\">доставка через CDN.</span>" "bodyHtml": "<span class=\"darker-70\">Деплой в один клик,</span> непрерывный пайплайн, мгновенный откат и быстрая <span class=\"darker-70\">доставка через CDN.</span>"
} }
], ],
"app": {
"iconAlt": "Иконка NODE.DC",
"logoAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
},
"form": { "form": {
"messagePlaceholder": "Сообщение (необязательно)", "messagePlaceholder": "Сообщение (необязательно)",
"namePlaceholder": "Ваше имя*", "namePlaceholder": "Ваше имя*",
@ -304,7 +384,17 @@
"failureHeading": "Не получилось", "failureHeading": "Не получилось",
"failureTextHtml": "Что-то пошло не так. <br/>Попробуйте ещё раз чуть позже.<br/>" "failureTextHtml": "Что-то пошло не так. <br/>Попробуйте ещё раз чуть позже.<br/>"
}, },
"legalHtml": "<a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика конфиденциальности</a> • <a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика cookie</a><br/>© — Все права защищены<br/>2026" "legalHtml": "<a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика конфиденциальности</a> • <a href=\"https://docs.google.com/document/d/1vME2w8ZB88HoH-JyvkkiL7exrcMfWhkERfFRlnIeZFA/edit?usp=sharing\" target=\"_blank\">Политика cookie</a><br/>© — Все права защищены<br/>2026",
"accessCard": {
"enabled": true,
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
}
}, },
"editableFields": [ "editableFields": [
{ {
@ -392,39 +482,61 @@
"group": "Тезисы первого экрана" "group": "Тезисы первого экрана"
}, },
{ {
"path": "content.app.iconAlt", "path": "content.accessCard.enabled",
"label": "App: alt иконки", "label": "Карточка доступа: отображение",
"kind": "text", "kind": "boolean",
"renderAs": "boolean",
"group": "Карточка доступа",
"headerToggle": true
},
{
"path": "content.accessCard.iconSrc",
"label": "Карточка доступа: иконка",
"kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.logoAlt", "path": "content.accessCard.wordmarkSrc",
"label": "App: alt логотипа", "label": "Карточка доступа: wordmark",
"kind": "text", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.release", "path": "content.accessCard.iconAlt",
"label": "App: релиз", "label": "Карточка доступа: alt иконки",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.heading", "path": "content.accessCard.wordmarkAlt",
"label": "App: заголовок", "label": "Карточка доступа: alt логотипа",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.description", "path": "content.accessCard.release",
"label": "App: описание", "label": "Карточка доступа: релиз",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
},
{
"path": "content.accessCard.heading",
"label": "Карточка доступа: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.description",
"label": "Карточка доступа: описание",
"kind": "textarea",
"renderAs": "text",
"group": "Карточка доступа"
}, },
{ {
"path": "content.form.messagePlaceholder", "path": "content.form.messagePlaceholder",
@ -2241,15 +2353,6 @@
"anchor": "waitlist", "anchor": "waitlist",
"enabled": true, "enabled": true,
"content": { "content": {
"app": {
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
},
"form": { "form": {
"messagePlaceholder": "Сообщение (необязательно)", "messagePlaceholder": "Сообщение (необязательно)",
"namePlaceholder": "Ваше имя*", "namePlaceholder": "Ваше имя*",
@ -2502,57 +2605,75 @@
"iconSrcset": "./assets/webflow/images/69a9fb87bc56142371745926_txt-file-p-500.avif 500w, ./assets/webflow/images/69a9fb87bc56142371745926_txt-file.avif 1024w", "iconSrcset": "./assets/webflow/images/69a9fb87bc56142371745926_txt-file-p-500.avif 500w, ./assets/webflow/images/69a9fb87bc56142371745926_txt-file.avif 1024w",
"iconAlt": "Иконка TXT-файла" "iconAlt": "Иконка TXT-файла"
} }
] ],
"accessCard": {
"enabled": true,
"iconSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg",
"iconAlt": "Иконка NODE.DC",
"wordmarkSrc": "./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc",
"wordmarkAlt": "Логотип NODE.DC",
"release": "Бета-релиз: июль 2026",
"heading": "Ранний доступ",
"description": "Кастомный код, MCP и ИИ-агенты для Webflow в одном рабочем контуре."
}
}, },
"editableFields": [ "editableFields": [
{ {
"path": "content.app.iconSrc", "path": "content.accessCard.enabled",
"label": "Иконка приложения", "label": "Карточка доступа: отображение",
"kind": "boolean",
"renderAs": "boolean",
"group": "Карточка доступа",
"headerToggle": true
},
{
"path": "content.accessCard.iconSrc",
"label": "Карточка доступа: иконка",
"kind": "media", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.iconAlt", "path": "content.accessCard.wordmarkSrc",
"label": "Иконка: alt", "label": "Карточка доступа: wordmark",
"kind": "text",
"renderAs": "attr",
"group": "Карточка раннего доступа"
},
{
"path": "content.app.wordmarkSrc",
"label": "Wordmark",
"kind": "media", "kind": "media",
"renderAs": "attr", "renderAs": "attr",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.wordmarkAlt", "path": "content.accessCard.iconAlt",
"label": "Wordmark: alt", "label": "Карточка доступа: alt иконки",
"kind": "text",
"renderAs": "attr",
"group": "Карточка раннего доступа"
},
{
"path": "content.app.release",
"label": "Релиз",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.heading", "path": "content.accessCard.wordmarkAlt",
"label": "Заголовок", "label": "Карточка доступа: alt логотипа",
"kind": "text", "kind": "text",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.app.description", "path": "content.accessCard.release",
"label": "Описание", "label": "Карточка доступа: релиз",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.heading",
"label": "Карточка доступа: заголовок",
"kind": "text",
"renderAs": "text",
"group": "Карточка доступа"
},
{
"path": "content.accessCard.description",
"label": "Карточка доступа: описание",
"kind": "textarea", "kind": "textarea",
"renderAs": "text", "renderAs": "text",
"group": "Карточка раннего доступа" "group": "Карточка доступа"
}, },
{ {
"path": "content.form.messagePlaceholder", "path": "content.form.messagePlaceholder",

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<header id="hero" class="s h-screen"><div class="s-cont shorter"><div class="h-full between"><hgroup id="w-node-_42aec6af-ef19-ac2b-f604-ecf49ea735c6-43601640" data-module="hg" data-group="hero" class="hgroup main"><h1 class="main-h"><span class="darker-70">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h1><div class="h-txt">{{html:content.introHtml}}</div></hgroup><hgroup id="w-node-ddd50e0c-bdcb-417b-7dc2-7db828a06f46-43601640" data-module="hg" data-group="hero" class="to-left"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.0.muted}}</span><span>{{text:content.cards.0.main}}</span></h2><div class="h-txt">{{html:content.cards.0.bodyHtml}}</div></hgroup></div><div class="h-full between lower"><hgroup data-module="hg" data-group="hero"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.1.muted}}</span><span>{{text:content.cards.1.main}}</span></h2><div class="h-txt">{{html:content.cards.1.bodyHtml}}</div></hgroup><hgroup id="w-node-f82ddc17-0624-f069-9963-dda808a65750-43601640" data-module="hg" data-group="hero" class="to-right"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.2.muted}}</span><span>{{text:content.cards.2.main}}</span></h2><div class="h-txt">{{html:content.cards.2.bodyHtml}}</div></hgroup></div></div><div class="app-w"><div id="app-data" data-module="app" class="app-item data"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" arial-label="закрыть окно" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div><div class="circle opaque"></div></div></div><div class="flex-v"><div class="flex-v _w-full"><img src="./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg" loading="lazy" data-module="mouse-follow" alt="{{attr:content.app.iconAlt}}" class="logo app"/><div class="logo-lockup-w"><img src="./assets/webflow/images/69ad96a28b06b7552a339137_logtype.svg?v=node-dc" loading="eager" alt="{{attr:content.app.logoAlt}}" class="logo-size"/><div class="notice-txt sm">{{text:content.app.release}}</div></div></div><h3 data-module="text-wave" class="data-he">{{text:content.app.heading}}</h3><h2 class="data-he h2">{{text:content.app.description}}</h2><div class="form-element"><div data-module="form" class="form-w w-form"><form id="wf-form-sign-up" name="wf-form-sign-up" data-name="sign-up" method="get" data-form="" class="form" data-wf-page-id="69b2dc3d81980aaf43601640" data-wf-element-id="4c5736fb-5001-9c71-eaa8-239119d387ce"><textarea placeholder="{{attr:content.form.messagePlaceholder}}" maxlength="5000" id="Message" name="Message" data-name="Message" class="form-field taller w-input"></textarea><input class="form-field w-input" maxlength="256" name="name" data-name="Имя" placeholder="{{attr:content.form.namePlaceholder}}" type="text" id="Name" required=""/><div class="form-item"><input class="form-field w-input" maxlength="256" name="Email" data-name="Email" placeholder="{{attr:content.form.emailPlaceholder}}" type="email" id="Email" required=""/><button data-module="btn" class="form-btn"><div data-module="alpha" class="btn-bg"><div class="clip-12"><div>{{text:content.form.submitLabel}}</div></div></div></button></div></form><div data-success="" class="form-success"><h2 class="data-he sm">{{text:content.form.successHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.successTextHtml}}</h2><a data-module="btn" href="beta-apply.html" class="form-btn static w-inline-block"><div class="btn-bg gray"><div class="clip-12"><div>{{text:content.form.betaCtaLabel}}</div></div></div></a><h2 class="data-he h2 xs">{{html:content.form.founderIntroHtml}}</h2><a data-module="btn" href="founders.html" class="form-btn static w-inline-block"><div class="btn-bg"><div class="clip-12"><div>{{html:content.form.founderCtaHtml}}</div></div></div></a><link rel="prefetch" href="founders.html"/><div data-url="https://web.ssscript.app/api/founders-spots-remaining" data-module="founders-spots" class="pricing-sm darker-70 is--right">{{text:content.form.spotsLabel}}</div></div></div><div data-failure="" class="form-fail"><h2 class="data-he sm">{{text:content.form.failureHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.failureTextHtml}}</h2></div></div><div class="notice-txt">{{html:content.legalHtml}}</div></div></div></div></header> <header id="hero" class="s h-screen"><div class="s-cont shorter"><div class="h-full between"><hgroup id="w-node-_42aec6af-ef19-ac2b-f604-ecf49ea735c6-43601640" data-module="hg" data-group="hero" class="hgroup main"><h1 class="main-h"><span class="darker-70">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h1><div class="h-txt">{{html:content.introHtml}}</div></hgroup><hgroup id="w-node-ddd50e0c-bdcb-417b-7dc2-7db828a06f46-43601640" data-module="hg" data-group="hero" class="to-left"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.0.muted}}</span><span>{{text:content.cards.0.main}}</span></h2><div class="h-txt">{{html:content.cards.0.bodyHtml}}</div></hgroup></div><div class="h-full between lower"><hgroup data-module="hg" data-group="hero"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.1.muted}}</span><span>{{text:content.cards.1.main}}</span></h2><div class="h-txt">{{html:content.cards.1.bodyHtml}}</div></hgroup><hgroup id="w-node-f82ddc17-0624-f069-9963-dda808a65750-43601640" data-module="hg" data-group="hero" class="to-right"><h2 class="main-h sm"><span class="darker-70">{{text:content.cards.2.muted}}</span><span>{{text:content.cards.2.main}}</span></h2><div class="h-txt">{{html:content.cards.2.bodyHtml}}</div></hgroup></div></div>{{#if content.accessCard.enabled}}<div class="app-w"><div id="app-data" data-module="app" class="app-item data"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" arial-label="закрыть окно" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div><div class="circle opaque"></div></div></div><div class="flex-v"><div class="flex-v _w-full"><img src="{{attr:content.accessCard.iconSrc}}" loading="lazy" data-module="mouse-follow" alt="{{attr:content.accessCard.iconAlt}}" class="logo app"/><div class="logo-lockup-w"><img src="{{attr:content.accessCard.wordmarkSrc}}" loading="eager" alt="{{attr:content.accessCard.wordmarkAlt}}" class="logo-size"/><div class="notice-txt sm">{{text:content.accessCard.release}}</div></div></div><h3 data-module="text-wave" class="data-he">{{text:content.accessCard.heading}}</h3><h2 class="data-he h2">{{text:content.accessCard.description}}</h2><div class="form-element"><div data-module="form" class="form-w w-form"><form id="wf-form-sign-up" name="wf-form-sign-up" data-name="sign-up" method="get" data-form="" class="form" data-wf-page-id="69b2dc3d81980aaf43601640" data-wf-element-id="4c5736fb-5001-9c71-eaa8-239119d387ce"><textarea placeholder="{{attr:content.form.messagePlaceholder}}" maxlength="5000" id="Message" name="Message" data-name="Message" class="form-field taller w-input"></textarea><input class="form-field w-input" maxlength="256" name="name" data-name="Имя" placeholder="{{attr:content.form.namePlaceholder}}" type="text" id="Name" required=""/><div class="form-item"><input class="form-field w-input" maxlength="256" name="Email" data-name="Email" placeholder="{{attr:content.form.emailPlaceholder}}" type="email" id="Email" required=""/><button data-module="btn" class="form-btn"><div data-module="alpha" class="btn-bg"><div class="clip-12"><div>{{text:content.form.submitLabel}}</div></div></div></button></div></form><div data-success="" class="form-success"><h2 class="data-he sm">{{text:content.form.successHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.successTextHtml}}</h2><a data-module="btn" href="beta-apply.html" class="form-btn static w-inline-block"><div class="btn-bg gray"><div class="clip-12"><div>{{text:content.form.betaCtaLabel}}</div></div></div></a><h2 class="data-he h2 xs">{{html:content.form.founderIntroHtml}}</h2><a data-module="btn" href="founders.html" class="form-btn static w-inline-block"><div class="btn-bg"><div class="clip-12"><div>{{html:content.form.founderCtaHtml}}</div></div></div></a><link rel="prefetch" href="founders.html"/><div data-url="https://web.ssscript.app/api/founders-spots-remaining" data-module="founders-spots" class="pricing-sm darker-70 is--right">{{text:content.form.spotsLabel}}</div></div></div><div data-failure="" class="form-fail"><h2 class="data-he sm">{{text:content.form.failureHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.failureTextHtml}}</h2></div></div><div class="notice-txt">{{html:content.legalHtml}}</div></div></div></div>{{/if}}</header>

View File

@ -1 +1 @@
<section id="{{attr:anchor}}" class="s h-screen"><div class="app-w"><div id="app-data" data-module="app" class="app-item data"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div><div class="circle opaque"></div></div></div><div class="flex-v"><div class="flex-v _w-full"><img src="{{attr:content.app.iconSrc}}" loading="eager" data-module="mouse-follow" alt="{{attr:content.app.iconAlt}}" class="logo app"/><div class="logo-lockup-w"><img src="{{attr:content.app.wordmarkSrc}}" loading="eager" alt="{{attr:content.app.wordmarkAlt}}" class="logo-size"/><div class="notice-txt sm">{{text:content.app.release}}</div></div></div><h1 data-module="text-wave" class="data-he">{{text:content.app.heading}}</h1><h2 class="data-he h2">{{text:content.app.description}}</h2><div class="form-element"><div data-module="form" class="form-w w-form"><form id="wf-form-sign-up" name="wf-form-sign-up" data-name="sign-up" method="get" data-form="" class="form" data-wf-page-id="69b2dc3d81980aaf43601640" data-wf-element-id="3167f865-849c-497b-197f-50d61e82127f"><textarea placeholder="{{attr:content.form.messagePlaceholder}}" maxlength="5000" id="Message" name="Message" data-name="Message" class="form-field taller w-input"></textarea><input class="form-field w-input" maxlength="256" name="name" data-name="Имя" placeholder="{{attr:content.form.namePlaceholder}}" type="text" id="Name" required=""/><div class="form-item"><input class="form-field w-input" maxlength="256" name="Email" data-name="Email" placeholder="{{attr:content.form.emailPlaceholder}}" type="email" id="Email" required=""/><button data-module="btn" class="form-btn"><div data-module="alpha" class="btn-bg"><div class="clip-12"><div>{{text:content.form.submitLabel}}</div></div></div></button></div></form><div data-success="" class="form-success"><h2 class="data-he sm">{{text:content.form.successHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.successTextHtml}}</h2><a data-module="btn" href="{{attr:content.form.betaHref}}" class="form-btn static w-inline-block"><div data-module="alpha" class="btn-bg gray"><div class="clip-12"><div>{{text:content.form.betaCtaLabel}}</div></div></div></a></div></div></div><div class="notice-txt">{{html:content.legalHtml}}</div></div></div></div><div class="waitlist-coll"><div class="folder-w txts w-dyn-list"><div role="list" class="folder-w txts-windows w-dyn-items">{{#each content.files}}<div role="listitem" class="app-item-dot w-dyn-item"><div id="{{attr:id}}" data-module="app" data-start="closed" class="app-item txt"><div data-draggable-area="" class="app-head"><div class="app-head-trigs"><button data-close="" class="circle red"></button><div class="circle yellow"></div><div class="circle green"></div></div><div class="app-name-w"><img src="{{attr:iconSrc}}" loading="lazy" sizes="(max-width: 1024px) 100vw, 1024px" srcset="{{attr:iconSrcset}}" alt="{{attr:iconAlt}}" class="app-icon"/><div class="app-name-txt">{{text:name}}</div></div></div><div class="text-editable"><div data-module="editable-text" class="txt-text-block editable">{{html:bodyHtml}}</div></div></div></div>{{/each}}</div></div><div class="collection-list-wrapper-3 w-dyn-list"><div data-form="duplicate" role="list" class="folders txts w-dyn-items">{{#each content.files}}<div role="listitem" class="collection-item-2 w-dyn-item"><div tabindex="0" data-target="{{attr:id}}" data-module="apple-icon" class="folder"><img class="image" src="{{attr:iconSrc}}" alt="{{attr:iconAlt}}" sizes="(max-width: 1024px) 100vw, 1024px" data-icon="" loading="eager" srcset="{{attr:iconSrcset}}"/><div data-txt="" class="text-ho"><div class="folder-title">{{text:name}}</div><div class="folder-title lower">.txt</div></div></div></div>{{/each}}</div></div></div></section> <section id="{{attr:anchor}}" class="s h-screen">{{#if content.accessCard.enabled}}<div class="app-w"><div id="app-data" data-module="app" class="app-item data"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div><div class="circle opaque"></div></div></div><div class="flex-v"><div class="flex-v _w-full"><img src="{{attr:content.accessCard.iconSrc}}" loading="eager" data-module="mouse-follow" alt="{{attr:content.accessCard.iconAlt}}" class="logo app"/><div class="logo-lockup-w"><img src="{{attr:content.accessCard.wordmarkSrc}}" loading="eager" alt="{{attr:content.accessCard.wordmarkAlt}}" class="logo-size"/><div class="notice-txt sm">{{text:content.accessCard.release}}</div></div></div><h1 data-module="text-wave" class="data-he">{{text:content.accessCard.heading}}</h1><h2 class="data-he h2">{{text:content.accessCard.description}}</h2><div class="form-element"><div data-module="form" class="form-w w-form"><form id="wf-form-sign-up" name="wf-form-sign-up" data-name="sign-up" method="get" data-form="" class="form" data-wf-page-id="69b2dc3d81980aaf43601640" data-wf-element-id="3167f865-849c-497b-197f-50d61e82127f"><textarea placeholder="{{attr:content.form.messagePlaceholder}}" maxlength="5000" id="Message" name="Message" data-name="Message" class="form-field taller w-input"></textarea><input class="form-field w-input" maxlength="256" name="name" data-name="Имя" placeholder="{{attr:content.form.namePlaceholder}}" type="text" id="Name" required=""/><div class="form-item"><input class="form-field w-input" maxlength="256" name="Email" data-name="Email" placeholder="{{attr:content.form.emailPlaceholder}}" type="email" id="Email" required=""/><button data-module="btn" class="form-btn"><div data-module="alpha" class="btn-bg"><div class="clip-12"><div>{{text:content.form.submitLabel}}</div></div></div></button></div></form><div data-success="" class="form-success"><h2 class="data-he sm">{{text:content.form.successHeading}}</h2><h2 class="data-he h2 xs">{{html:content.form.successTextHtml}}</h2><a data-module="btn" href="{{attr:content.form.betaHref}}" class="form-btn static w-inline-block"><div data-module="alpha" class="btn-bg gray"><div class="clip-12"><div>{{text:content.form.betaCtaLabel}}</div></div></div></a></div></div></div><div class="notice-txt">{{html:content.legalHtml}}</div></div></div></div>{{/if}}<div class="waitlist-coll"><div class="folder-w txts w-dyn-list"><div role="list" class="folder-w txts-windows w-dyn-items">{{#each content.files}}<div role="listitem" class="app-item-dot w-dyn-item"><div id="{{attr:id}}" data-module="app" data-start="closed" class="app-item txt"><div data-draggable-area="" class="app-head"><div class="app-head-trigs"><button data-close="" class="circle red"></button><div class="circle yellow"></div><div class="circle green"></div></div><div class="app-name-w"><img src="{{attr:iconSrc}}" loading="lazy" sizes="(max-width: 1024px) 100vw, 1024px" srcset="{{attr:iconSrcset}}" alt="{{attr:iconAlt}}" class="app-icon"/><div class="app-name-txt">{{text:name}}</div></div></div><div class="text-editable"><div data-module="editable-text" class="txt-text-block editable">{{html:bodyHtml}}</div></div></div></div>{{/each}}</div></div><div class="collection-list-wrapper-3 w-dyn-list"><div data-form="duplicate" role="list" class="folders txts w-dyn-items">{{#each content.files}}<div role="listitem" class="collection-item-2 w-dyn-item"><div tabindex="0" data-target="{{attr:id}}" data-module="apple-icon" class="folder"><img class="image" src="{{attr:iconSrc}}" alt="{{attr:iconAlt}}" sizes="(max-width: 1024px) 100vw, 1024px" data-icon="" loading="eager" srcset="{{attr:iconSrcset}}"/><div data-txt="" class="text-ho"><div class="folder-title">{{text:name}}</div><div class="folder-title lower">.txt</div></div></div></div>{{/each}}</div></div></div></section>

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,11 @@ function getByPath(source, path) {
return path.split(".").reduce((value, key) => value?.[key], source); return path.split(".").reduce((value, key) => value?.[key], source);
} }
function isTruthy(value) {
if (Array.isArray(value)) return value.length > 0;
return Boolean(value);
}
function escapeHtml(value) { function escapeHtml(value) {
return String(value) return String(value)
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@ -83,6 +88,24 @@ function renderTemplateTokens(html, block) {
}); });
} }
function renderIfBlocks(html, block) {
let rendered = html;
let previous = null;
while (rendered !== previous) {
previous = rendered;
rendered = rendered.replace(
/\{\{#if\s+([a-zA-Z0-9_.-]+)\s*\}\}((?:(?!\{\{#if|\{\{\/if\}\})[\s\S])*)\{\{\/if\}\}/g,
(_match, path, innerHtml) => {
const value = getByPath(block, path);
return isTruthy(value) ? renderTemplate(innerHtml, block) : "";
},
);
}
return rendered;
}
function renderEachBlocks(html, block) { function renderEachBlocks(html, block) {
return html.replace(/\{\{#each\s+([a-zA-Z0-9_.-]+)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, path, innerHtml) => { return html.replace(/\{\{#each\s+([a-zA-Z0-9_.-]+)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, path, innerHtml) => {
const items = getByPath(block, path); const items = getByPath(block, path);
@ -91,10 +114,14 @@ function renderEachBlocks(html, block) {
throw new Error(`Each token "${path}" in block "${block.id}" must point to an array`); throw new Error(`Each token "${path}" in block "${block.id}" must point to an array`);
} }
return items.map((item) => renderTemplateTokens(innerHtml, item ?? {})).join(""); return items.map((item) => renderTemplate(innerHtml, item ?? {})).join("");
}); });
} }
function renderTemplate(html, block) {
return renderTemplateTokens(renderIfBlocks(renderEachBlocks(html, block), block), block);
}
function scopeDuplicateIds(html, scope) { function scopeDuplicateIds(html, scope) {
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]); const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
if (!ids.length) { if (!ids.length) {
@ -124,7 +151,7 @@ async function renderBlock(block, renderCounts) {
const renderCount = renderCounts.get(templateName) ?? 0; const renderCount = renderCounts.get(templateName) ?? 0;
renderCounts.set(templateName, renderCount + 1); renderCounts.set(templateName, renderCount + 1);
const html = renderTemplateTokens(renderEachBlocks(await readTemplate(templateName), block), block); const html = renderTemplate(await readTemplate(templateName), block);
if (renderCount === 0 && block.scopeIds !== true) { if (renderCount === 0 && block.scopeIds !== true) {
return html; return html;
@ -144,7 +171,7 @@ async function renderRegion(region) {
return blocks.join(""); return blocks.join("");
} }
let output = renderTemplateTokens(renderEachBlocks(shell, { id: page.slug, ...page }), { id: page.slug, ...page }); let output = renderTemplate(shell, { 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) {