Update promo admin media blocks
This commit is contained in:
parent
fb1531f7c5
commit
82cdab07f2
112
admin/admin.css
112
admin/admin.css
|
|
@ -989,8 +989,37 @@ body[data-file-picker-active="true"] .editor {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.number-control {
|
||||||
|
display: flex;
|
||||||
|
min-height: 2.72rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.52rem;
|
||||||
|
border-radius: var(--launcher-radius-control);
|
||||||
|
background: var(--field);
|
||||||
|
padding: 0 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-control input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 820;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-suffix {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
.boolean-field input,
|
.boolean-field input,
|
||||||
.group-head-toggle input {
|
.group-head-toggle input,
|
||||||
|
.collection-visibility-toggle input {
|
||||||
width: 2.42rem;
|
width: 2.42rem;
|
||||||
height: 1.32rem;
|
height: 1.32rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -1001,7 +1030,8 @@ body[data-file-picker-active="true"] .editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
.boolean-field input::before,
|
.boolean-field input::before,
|
||||||
.group-head-toggle input::before {
|
.group-head-toggle input::before,
|
||||||
|
.collection-visibility-toggle input::before {
|
||||||
display: block;
|
display: block;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|
@ -1012,12 +1042,14 @@ body[data-file-picker-active="true"] .editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
.boolean-field input:checked,
|
.boolean-field input:checked,
|
||||||
.group-head-toggle input:checked {
|
.group-head-toggle input:checked,
|
||||||
|
.collection-visibility-toggle input:checked {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boolean-field input:checked::before,
|
.boolean-field input:checked::before,
|
||||||
.group-head-toggle input:checked::before {
|
.group-head-toggle input:checked::before,
|
||||||
|
.collection-visibility-toggle input:checked::before {
|
||||||
background: #050506;
|
background: #050506;
|
||||||
transform: translateX(1.1rem);
|
transform: translateX(1.1rem);
|
||||||
}
|
}
|
||||||
|
|
@ -1098,6 +1130,10 @@ body[data-file-picker-active="true"] .editor {
|
||||||
opacity: 0.16;
|
opacity: 0.16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collection-item-card.is-hidden {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
.collection-summary {
|
.collection-summary {
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -1110,6 +1146,8 @@ body[data-file-picker-active="true"] .editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection-summary-title {
|
.collection-summary-title {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|
@ -1124,6 +1162,45 @@ body[data-file-picker-active="true"] .editor {
|
||||||
gap: 0.22rem;
|
gap: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2.2rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
padding: 0 0.52rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle span {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 850;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle input {
|
||||||
|
width: 2rem;
|
||||||
|
height: 1.12rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0.13rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle input::before {
|
||||||
|
width: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-visibility-toggle input:checked::before {
|
||||||
|
transform: translateX(0.88rem);
|
||||||
|
}
|
||||||
|
|
||||||
.collection-drag-handle,
|
.collection-drag-handle,
|
||||||
.collection-icon-btn {
|
.collection-icon-btn {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -1589,6 +1666,33 @@ textarea {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delete-modal-layer {
|
||||||
|
z-index: 240;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal {
|
||||||
|
width: min(30rem, calc(100vw - 2.8rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-head {
|
||||||
|
padding-right: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-foot {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-confirm-btn {
|
||||||
|
min-width: 4.4rem;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: rgba(8, 8, 10, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-confirm-btn:hover {
|
||||||
|
background: #fff;
|
||||||
|
color: rgba(8, 8, 10, 1);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
body {
|
body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
|
||||||
205
admin/admin.js
205
admin/admin.js
|
|
@ -9,8 +9,12 @@ const state = {
|
||||||
filePickerActive: false,
|
filePickerActive: false,
|
||||||
mediaUploadActive: false,
|
mediaUploadActive: false,
|
||||||
filePickerResetTimer: null,
|
filePickerResetTimer: null,
|
||||||
|
deleteConfirmResolve: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SECTIONS_REGION_ID = "sections";
|
||||||
|
const SECTIONS_REGION_LABEL = "Секции";
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
regions: document.querySelector("#regions"),
|
regions: document.querySelector("#regions"),
|
||||||
addBlock: document.querySelector("#add-block"),
|
addBlock: document.querySelector("#add-block"),
|
||||||
|
|
@ -40,6 +44,11 @@ const el = {
|
||||||
templateList: document.querySelector("#template-list"),
|
templateList: document.querySelector("#template-list"),
|
||||||
templateModalClose: document.querySelector("#template-modal-close"),
|
templateModalClose: document.querySelector("#template-modal-close"),
|
||||||
templateModalCancel: document.querySelector("#template-modal-cancel"),
|
templateModalCancel: document.querySelector("#template-modal-cancel"),
|
||||||
|
deleteModal: document.querySelector("#delete-modal"),
|
||||||
|
deleteModalTitle: document.querySelector("#delete-modal-title"),
|
||||||
|
deleteModalBody: document.querySelector("#delete-modal-body"),
|
||||||
|
deleteModalCancel: document.querySelector("#delete-modal-cancel"),
|
||||||
|
deleteModalConfirm: document.querySelector("#delete-modal-confirm"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATIC_FALLBACK = {
|
const STATIC_FALLBACK = {
|
||||||
|
|
@ -423,6 +432,32 @@ function ensureStaticElements() {
|
||||||
state.page.staticElements.editableFields = clone(STATIC_FALLBACK.editableFields);
|
state.page.staticElements.editableFields = clone(STATIC_FALLBACK.editableFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureSectionsRegion() {
|
||||||
|
const regions = Array.isArray(state.page.regions) ? state.page.regions : [];
|
||||||
|
const blocks = regions.flatMap((region) =>
|
||||||
|
(region.blocks ?? []).map((block) => {
|
||||||
|
block.region ||= region.id;
|
||||||
|
return block;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.page.regions = [
|
||||||
|
{
|
||||||
|
id: SECTIONS_REGION_ID,
|
||||||
|
label: SECTIONS_REGION_LABEL,
|
||||||
|
description: "Единый порядок секций главной страницы.",
|
||||||
|
blocks,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionsRegionIndex() {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
state.page.regions.findIndex((region) => region.id === SECTIONS_REGION_ID),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isStaticSelection() {
|
function isStaticSelection() {
|
||||||
return state.selected?.kind === "static";
|
return state.selected?.kind === "static";
|
||||||
}
|
}
|
||||||
|
|
@ -559,8 +594,7 @@ function readFileAsDataUrl(file) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelForRegion(region) {
|
function labelForRegion(region) {
|
||||||
if (region.id === "beforeCViz") return "Главная";
|
if (region.id === SECTIONS_REGION_ID || region.id === "beforeCViz" || region.id === "cViz") return SECTIONS_REGION_LABEL;
|
||||||
if (region.id === "cViz") return "Секции";
|
|
||||||
return region.label || region.id;
|
return region.label || region.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -855,14 +889,13 @@ function renderRegions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockTemplates() {
|
function blockTemplates() {
|
||||||
return state.templates.map((template) => {
|
const regionIndex = sectionsRegionIndex();
|
||||||
const regionIndex = state.page.regions.findIndex((region) => region.id === template.regionId);
|
|
||||||
const region = state.page.regions[regionIndex] || state.page.regions[0];
|
|
||||||
|
|
||||||
|
return state.templates.map((template) => {
|
||||||
return {
|
return {
|
||||||
...template,
|
...template,
|
||||||
regionIndex: regionIndex >= 0 ? regionIndex : 0,
|
regionIndex,
|
||||||
regionLabel: template.regionLabel || labelForRegion(region),
|
regionLabel: SECTIONS_REGION_LABEL,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -907,6 +940,29 @@ function closeTemplateModal() {
|
||||||
el.addBlock.focus();
|
el.addBlock.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal(confirmed = false) {
|
||||||
|
const resolve = state.deleteConfirmResolve;
|
||||||
|
|
||||||
|
state.deleteConfirmResolve = null;
|
||||||
|
el.deleteModal.classList.add("hidden");
|
||||||
|
if (resolve) resolve(confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDeleteConfirmation({
|
||||||
|
title = "Подтвердите действие",
|
||||||
|
body = "Вы действительно подтвердить удаление элемента?",
|
||||||
|
} = {}) {
|
||||||
|
if (state.deleteConfirmResolve) closeDeleteModal(false);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
state.deleteConfirmResolve = resolve;
|
||||||
|
el.deleteModalTitle.textContent = title;
|
||||||
|
el.deleteModalBody.textContent = body;
|
||||||
|
el.deleteModal.classList.remove("hidden");
|
||||||
|
el.deleteModalConfirm.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addBlockFromTemplate(templateKey) {
|
function addBlockFromTemplate(templateKey) {
|
||||||
const template = blockTemplates().find((candidate) => candidate.key === templateKey);
|
const template = blockTemplates().find((candidate) => candidate.key === templateKey);
|
||||||
if (!template) return;
|
if (!template) return;
|
||||||
|
|
@ -1142,18 +1198,20 @@ async function uploadMediaFile({ block, editableField, hiddenInput, urlInput, fi
|
||||||
fileName.textContent = "Сохраняем в assets...";
|
fileName.textContent = "Сохраняем в assets...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataUrl = await readFileAsDataUrl(file);
|
const formData = new FormData();
|
||||||
|
formData.append("file", file, file.name);
|
||||||
|
formData.append("fileName", file.name);
|
||||||
|
formData.append("mimeType", file.type || "application/octet-stream");
|
||||||
|
formData.append("bucket", uploadBucketForField(block, editableField));
|
||||||
|
|
||||||
const response = await fetch("/api/upload/asset", {
|
const response = await fetch("/api/upload/asset", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
body: formData,
|
||||||
body: JSON.stringify({
|
|
||||||
fileName: file.name,
|
|
||||||
mimeType: file.type || "application/octet-stream",
|
|
||||||
bucket: uploadBucketForField(block, editableField),
|
|
||||||
dataUrl,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const payload = await response.json();
|
const payload = await response.json().catch(() => ({
|
||||||
|
ok: false,
|
||||||
|
error: `Сервер вернул ${response.status}`,
|
||||||
|
}));
|
||||||
|
|
||||||
if (!response.ok || !payload.ok) {
|
if (!response.ok || !payload.ok) {
|
||||||
throw new Error(payload.error || "Файл не удалось сохранить");
|
throw new Error(payload.error || "Файл не удалось сохранить");
|
||||||
|
|
@ -1500,6 +1558,54 @@ function renderRangeField({ block, editableField, grid }) {
|
||||||
grid.append(label);
|
grid.append(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderNumberField({ 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 control = document.createElement("span");
|
||||||
|
const input = document.createElement("input");
|
||||||
|
const suffix = document.createElement("span");
|
||||||
|
const rawValue = Number.parseFloat(getByPath(block, editableField.path));
|
||||||
|
const fallback = Number.parseFloat(editableField.defaultValue ?? editableField.min ?? 0);
|
||||||
|
const value = Number.isFinite(rawValue) ? rawValue : fallback;
|
||||||
|
|
||||||
|
label.className = "field-control number-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 || "number";
|
||||||
|
path.className = "field-path";
|
||||||
|
path.textContent = editableField.path;
|
||||||
|
control.className = "number-control";
|
||||||
|
suffix.className = "number-suffix";
|
||||||
|
suffix.textContent = editableField.suffix || "";
|
||||||
|
labelRow.append(labelText, kind);
|
||||||
|
|
||||||
|
input.dataset.path = editableField.path;
|
||||||
|
input.dataset.valueType = "number";
|
||||||
|
input.type = "number";
|
||||||
|
input.value = Number.isFinite(value) ? String(value) : "0";
|
||||||
|
input.autocomplete = "off";
|
||||||
|
|
||||||
|
if (editableField.min != null) input.min = String(editableField.min);
|
||||||
|
if (editableField.max != null) input.max = String(editableField.max);
|
||||||
|
if (editableField.step != null) input.step = String(editableField.step);
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
const active = activeBlock();
|
||||||
|
if (!active) return;
|
||||||
|
syncFieldValue(active, editableField.path, input);
|
||||||
|
});
|
||||||
|
|
||||||
|
control.append(input);
|
||||||
|
if (suffix.textContent) control.append(suffix);
|
||||||
|
label.append(labelRow, control, path);
|
||||||
|
grid.append(label);
|
||||||
|
}
|
||||||
|
|
||||||
function renderHeaderBooleanToggle({ block, editableField, head }) {
|
function renderHeaderBooleanToggle({ block, editableField, head }) {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
|
|
@ -1532,6 +1638,11 @@ function renderCollectionScalarField({ block, field, grid }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.kind === "number") {
|
||||||
|
renderNumberField({ 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");
|
||||||
|
|
@ -1666,8 +1777,14 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
const dragHandle = document.createElement("button");
|
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");
|
||||||
|
const hasVisibilityToggle =
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
(Object.prototype.hasOwnProperty.call(item, "enabled") ||
|
||||||
|
Object.prototype.hasOwnProperty.call(editableField.itemTemplate ?? {}, "enabled"));
|
||||||
|
|
||||||
details.className = "collection-item-card";
|
details.className = "collection-item-card";
|
||||||
|
details.classList.toggle("is-hidden", item?.enabled === false);
|
||||||
if (index === 0 && items.length <= 8) details.open = true;
|
if (index === 0 && items.length <= 8) details.open = true;
|
||||||
summary.className = "collection-summary";
|
summary.className = "collection-summary";
|
||||||
summaryTitle.className = "collection-summary-title";
|
summaryTitle.className = "collection-summary-title";
|
||||||
|
|
@ -1685,6 +1802,29 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
dragHandle.addEventListener("pointerdown", (event) => startCollectionPointerDrag(event, details, items, index, rerender));
|
dragHandle.addEventListener("pointerdown", (event) => startCollectionPointerDrag(event, details, items, index, rerender));
|
||||||
controls.append(dragHandle);
|
controls.append(dragHandle);
|
||||||
|
|
||||||
|
if (hasVisibilityToggle) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(item, "enabled")) item.enabled = true;
|
||||||
|
|
||||||
|
const visibilityToggle = document.createElement("span");
|
||||||
|
const visibilityInput = document.createElement("input");
|
||||||
|
const visibilityText = document.createElement("span");
|
||||||
|
|
||||||
|
visibilityToggle.className = "collection-visibility-toggle";
|
||||||
|
visibilityToggle.title = "Скрыть/показать";
|
||||||
|
visibilityToggle.setAttribute("aria-label", "Скрыть/показать");
|
||||||
|
visibilityInput.type = "checkbox";
|
||||||
|
visibilityInput.checked = item.enabled !== false;
|
||||||
|
visibilityText.textContent = "Показать";
|
||||||
|
visibilityToggle.addEventListener("click", (event) => event.stopPropagation());
|
||||||
|
visibilityInput.addEventListener("click", (event) => event.stopPropagation());
|
||||||
|
visibilityInput.addEventListener("change", () => {
|
||||||
|
item.enabled = visibilityInput.checked;
|
||||||
|
rerender(visibilityInput.checked ? "Элемент коллекции показан локально." : "Элемент коллекции скрыт локально.");
|
||||||
|
});
|
||||||
|
visibilityToggle.append(visibilityInput, visibilityText);
|
||||||
|
controls.append(visibilityToggle);
|
||||||
|
}
|
||||||
|
|
||||||
[
|
[
|
||||||
["↑", "Поднять", () => {
|
["↑", "Поднять", () => {
|
||||||
if (index === 0) return;
|
if (index === 0) return;
|
||||||
|
|
@ -1702,7 +1842,9 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
items.splice(index + 1, 0, prepareCollectionItemForInsert(items, item));
|
items.splice(index + 1, 0, prepareCollectionItemForInsert(items, item));
|
||||||
rerender("Элемент коллекции продублирован локально.");
|
rerender("Элемент коллекции продублирован локально.");
|
||||||
}],
|
}],
|
||||||
["⌫", "Удалить", () => {
|
["⌫", "Удалить", async () => {
|
||||||
|
const confirmed = await requestDeleteConfirmation();
|
||||||
|
if (!confirmed) return;
|
||||||
items.splice(index, 1);
|
items.splice(index, 1);
|
||||||
rerender("Элемент коллекции удалён локально.");
|
rerender("Элемент коллекции удалён локально.");
|
||||||
}],
|
}],
|
||||||
|
|
@ -1716,7 +1858,7 @@ function renderCollectionField({ block, editableField, grid }) {
|
||||||
button.addEventListener("click", (event) => {
|
button.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handler();
|
Promise.resolve(handler()).catch((error) => setStatus(`Ошибка действия коллекции: ${error.message}`));
|
||||||
});
|
});
|
||||||
controls.append(button);
|
controls.append(button);
|
||||||
});
|
});
|
||||||
|
|
@ -1802,6 +1944,11 @@ function renderContentFields(block) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editableField.kind === "number") {
|
||||||
|
renderNumberField({ 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");
|
||||||
|
|
@ -1886,6 +2033,7 @@ async function loadPage() {
|
||||||
state.page = await pageResponse.json();
|
state.page = await pageResponse.json();
|
||||||
state.templates = await templatesResponse.json();
|
state.templates = await templatesResponse.json();
|
||||||
ensureStaticElements();
|
ensureStaticElements();
|
||||||
|
ensureSectionsRegion();
|
||||||
state.selected = { kind: "static" };
|
state.selected = { kind: "static" };
|
||||||
renderAll();
|
renderAll();
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
|
|
@ -1979,11 +2127,14 @@ function pasteAfterSelected() {
|
||||||
markDirty(`Блок «${pasted.adminLabel || pasted.id}» вставлен локально.`);
|
markDirty(`Блок «${pasted.adminLabel || pasted.id}» вставлен локально.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelected() {
|
async function deleteSelected() {
|
||||||
if (isStaticSelection()) return;
|
if (isStaticSelection()) return;
|
||||||
const region = activeRegion();
|
const region = activeRegion();
|
||||||
if (!region || state.selected.blockIndex == null) return;
|
if (!region || state.selected.blockIndex == null) return;
|
||||||
|
|
||||||
|
const confirmed = await requestDeleteConfirmation();
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
region.blocks.splice(state.selected.blockIndex, 1);
|
region.blocks.splice(state.selected.blockIndex, 1);
|
||||||
state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1);
|
state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1);
|
||||||
if (state.selected.blockIndex < 0) state.selected = { kind: "static" };
|
if (state.selected.blockIndex < 0) state.selected = { kind: "static" };
|
||||||
|
|
@ -2010,8 +2161,20 @@ el.templateModalCancel.addEventListener("click", closeTemplateModal);
|
||||||
el.templateModal.addEventListener("click", (event) => {
|
el.templateModal.addEventListener("click", (event) => {
|
||||||
if (event.target === el.templateModal) closeTemplateModal();
|
if (event.target === el.templateModal) closeTemplateModal();
|
||||||
});
|
});
|
||||||
|
el.deleteModalCancel.addEventListener("click", () => closeDeleteModal(false));
|
||||||
|
el.deleteModalConfirm.addEventListener("click", () => closeDeleteModal(true));
|
||||||
|
el.deleteModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === el.deleteModal) closeDeleteModal(false);
|
||||||
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape" && !el.templateModal.classList.contains("hidden")) {
|
if (event.key !== "Escape") return;
|
||||||
|
|
||||||
|
if (!el.deleteModal.classList.contains("hidden")) {
|
||||||
|
closeDeleteModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!el.templateModal.classList.contains("hidden")) {
|
||||||
closeTemplateModal();
|
closeTemplateModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -2023,7 +2186,7 @@ el.moveDown.addEventListener("click", () => moveSelected(1));
|
||||||
el.duplicate.addEventListener("click", duplicateSelected);
|
el.duplicate.addEventListener("click", duplicateSelected);
|
||||||
el.copy.addEventListener("click", copySelected);
|
el.copy.addEventListener("click", copySelected);
|
||||||
el.pasteAfter.addEventListener("click", pasteAfterSelected);
|
el.pasteAfter.addEventListener("click", pasteAfterSelected);
|
||||||
el.deleteBlock.addEventListener("click", deleteSelected);
|
el.deleteBlock.addEventListener("click", () => deleteSelected().catch((error) => setStatus(`Ошибка удаления: ${error.message}`)));
|
||||||
|
|
||||||
window.addEventListener("error", (event) => {
|
window.addEventListener("error", (event) => {
|
||||||
recoverEditorSurface();
|
recoverEditorSurface();
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,21 @@
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="delete-modal" class="template-modal-layer delete-modal-layer hidden" role="dialog" aria-modal="true" aria-labelledby="delete-modal-title" aria-describedby="delete-modal-body">
|
||||||
|
<article class="template-modal delete-modal">
|
||||||
|
<header class="template-modal-head delete-modal-head">
|
||||||
|
<div>
|
||||||
|
<div class="section-kicker">Удаление</div>
|
||||||
|
<h2 id="delete-modal-title">Подтвердите действие</h2>
|
||||||
|
<p id="delete-modal-body">Вы действительно подтвердить удаление элемента?</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<footer class="template-modal-foot delete-modal-foot">
|
||||||
|
<button id="delete-modal-cancel" class="ghost-btn" type="button">Отмена</button>
|
||||||
|
<button id="delete-modal-confirm" class="danger-btn delete-confirm-btn" type="button">Да</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
<script src="./admin.js" type="module"></script>
|
<script src="./admin.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}img{pointer-events:none}.text-scrollable{scrollbar-width:none;-ms-overflow-style:none}.text-scrollable::-webkit-scrollbar{display:none}.circle .x-icon{opacity:0}.circle:hover .x-icon{opacity:1}[data-css=fade-tb]{mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);-webkit-mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);mask-size:100% 100%;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat}.current .mail-item{background-color:#3d3d3dc9}[data-module=""]{outline:10px solid red}[data-module=btn]>div>div>div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=btn]:hover>div>div>div>div{transform:translateY(-1.3em)}[data-module=link]{transition:opacity .1s ease-in-out}[data-module=link] div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=link]:hover{opacity:.6!important}[data-module=link]:hover div>div{transform:translateY(-.65em)}[data-module=apple-icon]:focus [data-txt]{position:relative}[data-module=apple-icon]:focus [data-txt]:before{content:"";position:absolute;z-index:-1;background-color:#0059d0;border-radius:.3em;width:120%;height:120%;top:-10%;left:-10%}[data-module=notification-w]{--notification-height:98%}[data-module=notification-w]>div:nth-child(2){transform:translateY(calc(-1*var(--notification-height)));z-index:2}[data-module=notification-w]>div:nth-child(3){transform:translateY(calc(-2*var(--notification-height)));z-index:3}[data-module=notification-w]:hover{--notification-height:98%}[data-module=notification-w]:hover>div:nth-child(2){z-index:2;transform:translateY(0)}[data-module=notification-w]:hover>div:nth-child(3){z-index:3;transform:translateY(0)}[data-module=notification]{transition:transform .3s cubic-bezier(.22,1,.36,1)}
|
*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}img{pointer-events:none}.text-scrollable{scrollbar-width:none;-ms-overflow-style:none}.text-scrollable::-webkit-scrollbar{display:none}.circle .x-icon{opacity:0}.circle:hover .x-icon{opacity:1}[data-css=fade-tb]{mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);-webkit-mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);mask-size:100% 100%;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat}.current .mail-item{background-color:#3d3d3dc9}[data-module=""]{outline:10px solid red}[data-module=btn]>div>div>div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=btn]:hover>div>div>div>div{transform:translateY(-1.3em)}[data-module=link]{transition:opacity .1s ease-in-out}[data-module=link] div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=link]:hover{opacity:.6!important}[data-module=link]:hover div>div{transform:translateY(-.65em)}[data-module=apple-icon]:focus [data-txt]{position:relative}[data-module=apple-icon]:focus [data-txt]:before{content:"";position:absolute;z-index:-1;background-color:#0059d0;border-radius:.3em;width:120%;height:120%;top:-10%;left:-10%}[data-module=notification-w]{--notification-height:98%}[data-module=notification-w]>div:nth-child(2){transform:translateY(calc(-1*var(--notification-height)));z-index:2}[data-module=notification-w]>div:nth-child(3){transform:translateY(calc(-2*var(--notification-height)));z-index:3}[data-module=notification-w]:hover{--notification-height:98%}[data-module=notification-w]:hover>div:nth-child(2){z-index:2;transform:translateY(0)}[data-module=notification-w]:hover>div:nth-child(3){z-index:3;transform:translateY(0)}[data-module=notification]{transition:transform .3s cubic-bezier(.22,1,.36,1)}
|
||||||
|
.app-item.video[data-adaptive-video-window]{--nodedc-video-source-aspect:1.777778;--nodedc-video-height:620px;--nodedc-video-max-height:72svh;--nodedc-video-max-width:70vw;--nodedc-video-render-height:min(var(--nodedc-video-height),var(--nodedc-video-max-height));aspect-ratio:auto;width:min(var(--nodedc-video-max-width),calc(var(--nodedc-video-render-height)*var(--nodedc-video-source-aspect)));height:auto}.app-item.video[data-adaptive-video-window] .video-size[data-adaptive-video]{aspect-ratio:var(--nodedc-video-source-aspect);display:block;width:100%;height:auto;max-height:var(--nodedc-video-render-height)}.app-item.video[data-adaptive-video-window] .video-size[data-adaptive-video]>video.video{display:block;width:100%;height:100%;object-fit:cover}.video-screen [data-media-handle]{display:block;width:100%;height:100%}.video-screen [data-media-handle]>img,.video-screen [data-media-handle]>video{display:block;width:100%;height:100%;object-fit:cover}@media screen and (max-width:767px){.app-item.video[data-adaptive-video-window]{--nodedc-video-max-height:58svh;--nodedc-video-max-width:95vw}}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}img{pointer-events:none}.text-scrollable{scrollbar-width:none;-ms-overflow-style:none}.text-scrollable::-webkit-scrollbar{display:none}.circle .x-icon{opacity:0}.circle:hover .x-icon{opacity:1}[data-css=fade-tb]{mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);-webkit-mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);mask-size:100% 100%;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat}.current .mail-item{background-color:#3d3d3dc9}[data-module=""]{outline:10px solid red}[data-module=btn]>div>div>div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=btn]:hover>div>div>div>div{transform:translateY(-1.3em)}[data-module=link]{transition:opacity .1s ease-in-out}[data-module=link] div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=link]:hover{opacity:.6!important}[data-module=link]:hover div>div{transform:translateY(-.65em)}[data-module=apple-icon]:focus [data-txt]{position:relative}[data-module=apple-icon]:focus [data-txt]:before{content:"";position:absolute;z-index:-1;background-color:#0059d0;border-radius:.3em;width:120%;height:120%;top:-10%;left:-10%}[data-module=notification-w]{--notification-height:98%}[data-module=notification-w]>div:nth-child(2){transform:translateY(calc(-1*var(--notification-height)));z-index:2}[data-module=notification-w]>div:nth-child(3){transform:translateY(calc(-2*var(--notification-height)));z-index:3}[data-module=notification-w]:hover{--notification-height:98%}[data-module=notification-w]:hover>div:nth-child(2){z-index:2;transform:translateY(0)}[data-module=notification-w]:hover>div:nth-child(3){z-index:3;transform:translateY(0)}[data-module=notification]{transition:transform .3s cubic-bezier(.22,1,.36,1)}[data-start=closed]{pointer-events:none;visibility:hidden}[data-module=app]:not([data-start=closed]){transform:scale(0)translateY(200%)}[data-module=folder-wrap]>*{transform:scale(0)}[data-module=list] a{opacity:0}[data-success],[data-failure]{display:none}[data-module=alpha]{opacity:0}
|
*{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}img{pointer-events:none}.text-scrollable{scrollbar-width:none;-ms-overflow-style:none}.text-scrollable::-webkit-scrollbar{display:none}.circle .x-icon{opacity:0}.circle:hover .x-icon{opacity:1}[data-css=fade-tb]{mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);-webkit-mask-image:linear-gradient(#0000 0%,#000 15% 85%,#0000 100%);mask-size:100% 100%;mask-repeat:no-repeat;-webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat}.current .mail-item{background-color:#3d3d3dc9}[data-module=""]{outline:10px solid red}[data-module=btn]>div>div>div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=btn]:hover>div>div>div>div{transform:translateY(-1.3em)}[data-module=link]{transition:opacity .1s ease-in-out}[data-module=link] div>div{text-shadow:0 1.3em #fff;transition:transform .3s ease-in-out}[data-module=link]:hover{opacity:.6!important}[data-module=link]:hover div>div{transform:translateY(-.65em)}[data-module=apple-icon]:focus [data-txt]{position:relative}[data-module=apple-icon]:focus [data-txt]:before{content:"";position:absolute;z-index:-1;background-color:#0059d0;border-radius:.3em;width:120%;height:120%;top:-10%;left:-10%}[data-module=notification-w]{--notification-height:98%}[data-module=notification-w]>div:nth-child(2){transform:translateY(calc(-1*var(--notification-height)));z-index:2}[data-module=notification-w]>div:nth-child(3){transform:translateY(calc(-2*var(--notification-height)));z-index:3}[data-module=notification-w]:hover{--notification-height:98%}[data-module=notification-w]:hover>div:nth-child(2){z-index:2;transform:translateY(0)}[data-module=notification-w]:hover>div:nth-child(3){z-index:3;transform:translateY(0)}[data-module=notification]{transition:transform .3s cubic-bezier(.22,1,.36,1)}[data-start=closed]{pointer-events:none;visibility:hidden}[data-module=app]:not([data-start=closed]){transform:scale(0)translateY(200%)}[data-module=folder-wrap]>*{transform:scale(0)}[data-module=list] a{opacity:0}[data-success],[data-failure]{display:none}[data-module=alpha]{opacity:0}
|
||||||
|
.app-item.video[data-adaptive-video-window]{--nodedc-video-source-aspect:1.777778;--nodedc-video-height:620px;--nodedc-video-max-height:72svh;--nodedc-video-max-width:70vw;--nodedc-video-render-height:min(var(--nodedc-video-height),var(--nodedc-video-max-height));aspect-ratio:auto;width:min(var(--nodedc-video-max-width),calc(var(--nodedc-video-render-height)*var(--nodedc-video-source-aspect)));height:auto}.app-item.video[data-adaptive-video-window] .video-size[data-adaptive-video]{aspect-ratio:var(--nodedc-video-source-aspect);display:block;width:100%;height:auto;max-height:var(--nodedc-video-render-height)}.app-item.video[data-adaptive-video-window] .video-size[data-adaptive-video]>video.video{display:block;width:100%;height:100%;object-fit:cover}.video-screen [data-media-handle]{display:block;width:100%;height:100%}.video-screen [data-media-handle]>img,.video-screen [data-media-handle]>video{display:block;width:100%;height:100%;object-fit:cover}@media screen and (max-width:767px){.app-item.video[data-adaptive-video-window]{--nodedc-video-max-height:58svh;--nodedc-video-max-width:95vw}}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
const MIN_SCROLL_SPEED = 0.2;
|
const MIN_SCROLL_SPEED = 0.2;
|
||||||
const MAX_SCROLL_SPEED = 2;
|
const MAX_SCROLL_SPEED = 2;
|
||||||
const DEFAULT_SCROLL_SPEED = 0.55;
|
const DEFAULT_SCROLL_SPEED = 0.55;
|
||||||
|
const ADAPTIVE_VIDEO_BOUND_FLAG = "__nodedcAdaptiveVideoBound";
|
||||||
|
const MEDIA_HANDLE_BOUND_FLAG = "__nodedcMediaHandleBound";
|
||||||
|
const IMAGE_EXTENSIONS = new Set(["avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]);
|
||||||
|
const VIDEO_EXTENSIONS = new Set(["m4v", "mov", "mp4", "webm"]);
|
||||||
|
|
||||||
function isDesktopPointer() {
|
function isDesktopPointer() {
|
||||||
return window.matchMedia?.("(pointer: fine)").matches ?? true;
|
return window.matchMedia?.("(pointer: fine)").matches ?? true;
|
||||||
|
|
@ -227,14 +231,106 @@
|
||||||
document.querySelectorAll('[data-module="mail-list"]').forEach(bindMailListWheel);
|
document.querySelectorAll('[data-module="mail-list"]').forEach(bindMailListWheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mediaExtension(src) {
|
||||||
|
const cleanSrc = String(src || "").split(/[?#]/)[0];
|
||||||
|
const match = /\.([a-z0-9]+)$/i.exec(cleanSrc);
|
||||||
|
return match ? match[1].toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMediaElement(src) {
|
||||||
|
const extension = mediaExtension(src);
|
||||||
|
|
||||||
|
if (IMAGE_EXTENSIONS.has(extension)) {
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.className = "video-el";
|
||||||
|
image.src = src;
|
||||||
|
image.alt = "";
|
||||||
|
image.loading = "lazy";
|
||||||
|
image.decoding = "async";
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VIDEO_EXTENSIONS.has(extension)) return null;
|
||||||
|
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.className = "video-el";
|
||||||
|
video.dataset.module = "video-handle";
|
||||||
|
video.src = src;
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.preload = "none";
|
||||||
|
video.setAttribute("playsinline", "true");
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateMediaHandle(handle) {
|
||||||
|
if (!handle || handle[MEDIA_HANDLE_BOUND_FLAG]) return;
|
||||||
|
|
||||||
|
const src = handle.dataset.mediaSrc;
|
||||||
|
const media = createMediaElement(src);
|
||||||
|
|
||||||
|
if (!media) return;
|
||||||
|
handle[MEDIA_HANDLE_BOUND_FLAG] = true;
|
||||||
|
handle.textContent = "";
|
||||||
|
handle.append(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateMediaHandles() {
|
||||||
|
document.querySelectorAll("[data-media-handle]").forEach(hydrateMediaHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAdaptiveVideoAspect(video) {
|
||||||
|
const surface = video.closest("[data-adaptive-video]");
|
||||||
|
if (!surface || !video.videoWidth || !video.videoHeight) return;
|
||||||
|
|
||||||
|
const windowEl = video.closest("[data-adaptive-video-window]");
|
||||||
|
const sourceAspect = video.videoWidth / video.videoHeight;
|
||||||
|
const targets = windowEl ? [surface, windowEl] : [surface];
|
||||||
|
|
||||||
|
targets.forEach((target) => {
|
||||||
|
target.style.setProperty("--nodedc-video-source-aspect", sourceAspect.toFixed(6));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindAdaptiveVideo(video) {
|
||||||
|
if (!video || video[ADAPTIVE_VIDEO_BOUND_FLAG]) return;
|
||||||
|
|
||||||
|
video[ADAPTIVE_VIDEO_BOUND_FLAG] = true;
|
||||||
|
video.addEventListener("loadedmetadata", () => applyAdaptiveVideoAspect(video));
|
||||||
|
applyAdaptiveVideoAspect(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindAdaptiveVideos() {
|
||||||
|
document.querySelectorAll("[data-adaptive-video] video").forEach(bindAdaptiveVideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateMediaHandles();
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", bindInteractiveScroll, { once: true });
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
() => {
|
||||||
|
hydrateMediaHandles();
|
||||||
|
bindInteractiveScroll();
|
||||||
|
bindAdaptiveVideos();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
hydrateMediaHandles();
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
|
bindAdaptiveVideos();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
hydrateMediaHandles();
|
||||||
bindInteractiveScroll();
|
bindInteractiveScroll();
|
||||||
window.setTimeout(bindInteractiveScroll, 500);
|
bindAdaptiveVideos();
|
||||||
|
window.setTimeout(() => {
|
||||||
|
hydrateMediaHandles();
|
||||||
|
bindInteractiveScroll();
|
||||||
|
bindAdaptiveVideos();
|
||||||
|
}, 500);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 544 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,16 +1,16 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "beforeCViz:heroWaitlist:hero-waitlist.html",
|
"key": "beforeCViz:heroWaitlist:theses-request-demo.html",
|
||||||
"regionId": "beforeCViz",
|
"regionId": "beforeCViz",
|
||||||
"regionLabel": "Главная",
|
"regionLabel": "Главная",
|
||||||
"title": "Первый экран и заявка",
|
"title": "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||||
"description": "hero-waitlist.html",
|
"description": "theses-request-demo.html",
|
||||||
"block": {
|
"block": {
|
||||||
"region": "beforeCViz",
|
"region": "beforeCViz",
|
||||||
"id": "hero-waitlist",
|
"id": "hero-waitlist",
|
||||||
"type": "heroWaitlist",
|
"type": "heroWaitlist",
|
||||||
"template": "hero-waitlist.html",
|
"template": "theses-request-demo.html",
|
||||||
"adminLabel": "Первый экран и заявка",
|
"adminLabel": "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||||
"anchor": "hero",
|
"anchor": "hero",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|
@ -299,17 +299,145 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "beforeCViz:featureVideo:feature-video.html",
|
"key": "cViz:aiModes:project-composition.html",
|
||||||
|
"regionId": "cViz",
|
||||||
|
"regionLabel": "Секции",
|
||||||
|
"title": "СОСТАВ ПРОЕКТА",
|
||||||
|
"description": "project-composition.html",
|
||||||
|
"block": {
|
||||||
|
"region": "cViz",
|
||||||
|
"id": "ai-modes",
|
||||||
|
"type": "aiModes",
|
||||||
|
"template": "project-composition.html",
|
||||||
|
"adminLabel": "СОСТАВ ПРОЕКТА",
|
||||||
|
"anchor": null,
|
||||||
|
"content": {
|
||||||
|
"heading": {
|
||||||
|
"muted": "Гибкие",
|
||||||
|
"main": "режимы ИИ."
|
||||||
|
},
|
||||||
|
"introHtml": "Разные <span class=\"darker-70\">режимы</span> и <span class=\"darker-70\">модели</span> для разных задач.",
|
||||||
|
"sliderItems": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/various-modes.mp4",
|
||||||
|
"title": "Переключение режимов ИИ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/project-flow.mp4",
|
||||||
|
"title": "Создание нового проекта"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/ai-dock-mode.mp4",
|
||||||
|
"title": "ИИ-док"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/account.mp4",
|
||||||
|
"title": "Аккаунт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/code-edits.mp4",
|
||||||
|
"title": "Редактирование кода с ИИ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/dev-server.mp4",
|
||||||
|
"title": "Dev-сервер с ИИ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/webflow-mcp.mp4",
|
||||||
|
"title": "Webflow MCP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/publish-flow.mp4",
|
||||||
|
"title": "Публикация"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scroll": {
|
||||||
|
"speed": 0.55
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editableFields": [
|
||||||
|
{
|
||||||
|
"path": "content.scroll.speed",
|
||||||
|
"label": "Скорость скролла",
|
||||||
|
"kind": "range",
|
||||||
|
"renderAs": "number",
|
||||||
|
"group": "Скролл",
|
||||||
|
"min": 0.2,
|
||||||
|
"max": 2,
|
||||||
|
"step": 0.05,
|
||||||
|
"suffix": "x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "content.heading.muted",
|
||||||
|
"label": "Заголовок: приглушённая строка",
|
||||||
|
"kind": "text",
|
||||||
|
"renderAs": "text",
|
||||||
|
"group": "Заголовок"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "content.heading.main",
|
||||||
|
"label": "Заголовок: основная строка",
|
||||||
|
"kind": "text",
|
||||||
|
"renderAs": "text",
|
||||||
|
"group": "Заголовок"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "content.introHtml",
|
||||||
|
"label": "Вводный текст",
|
||||||
|
"kind": "html",
|
||||||
|
"renderAs": "html",
|
||||||
|
"group": "Заголовок"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "content.sliderItems",
|
||||||
|
"label": "Слайды",
|
||||||
|
"kind": "collection",
|
||||||
|
"renderAs": "collection",
|
||||||
|
"group": "Слайдер",
|
||||||
|
"itemTemplate": {
|
||||||
|
"enabled": true,
|
||||||
|
"videoSrc": "./assets/media/publish-flow.mp4",
|
||||||
|
"title": "Новый слайд"
|
||||||
|
},
|
||||||
|
"itemFields": [
|
||||||
|
{
|
||||||
|
"path": "videoSrc",
|
||||||
|
"label": "Видео",
|
||||||
|
"kind": "media",
|
||||||
|
"renderAs": "attr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "title",
|
||||||
|
"label": "Подпись",
|
||||||
|
"kind": "text",
|
||||||
|
"renderAs": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "beforeCViz:featureVideo:project-video-first.html",
|
||||||
"regionId": "beforeCViz",
|
"regionId": "beforeCViz",
|
||||||
"regionLabel": "Главная",
|
"regionLabel": "Главная",
|
||||||
"title": "Демо и базовая логика",
|
"title": "PROJECT VIDEO - FIRST",
|
||||||
"description": "feature-video.html",
|
"description": "project-video-first.html",
|
||||||
"block": {
|
"block": {
|
||||||
"region": "beforeCViz",
|
"region": "beforeCViz",
|
||||||
"id": "feature-video",
|
"id": "feature-video",
|
||||||
"type": "featureVideo",
|
"type": "featureVideo",
|
||||||
"template": "feature-video.html",
|
"template": "project-video-first.html",
|
||||||
"adminLabel": "Демо и базовая логика",
|
"adminLabel": "PROJECT VIDEO - FIRST",
|
||||||
"anchor": "features",
|
"anchor": "features",
|
||||||
"content": {
|
"content": {
|
||||||
"windowTitle": "демо-видео.mp4",
|
"windowTitle": "демо-видео.mp4",
|
||||||
|
|
@ -336,7 +464,8 @@
|
||||||
"eyebrow": "Деплой в один клик",
|
"eyebrow": "Деплой в один клик",
|
||||||
"bodyHtml": "<span class=\"darker-70\">Нажмите и публикуйте</span> кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта."
|
"bodyHtml": "<span class=\"darker-70\">Нажмите и публикуйте</span> кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"videoHeightPx": 620
|
||||||
},
|
},
|
||||||
"editableFields": [
|
"editableFields": [
|
||||||
{
|
{
|
||||||
|
|
@ -353,6 +482,18 @@
|
||||||
"renderAs": "attr",
|
"renderAs": "attr",
|
||||||
"group": "Видео-окно"
|
"group": "Видео-окно"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "content.videoHeightPx",
|
||||||
|
"label": "Высота видео",
|
||||||
|
"kind": "number",
|
||||||
|
"renderAs": "number",
|
||||||
|
"group": "Видео-окно",
|
||||||
|
"min": 240,
|
||||||
|
"max": 900,
|
||||||
|
"step": 1,
|
||||||
|
"suffix": "px",
|
||||||
|
"defaultValue": 620
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "content.heading.muted",
|
"path": "content.heading.muted",
|
||||||
"label": "Заголовок: приглушённая строка",
|
"label": "Заголовок: приглушённая строка",
|
||||||
|
|
@ -435,17 +576,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "cViz:componentLibrary:component-library.html",
|
"key": "cViz:componentLibrary:project-video-detail.html",
|
||||||
"regionId": "cViz",
|
"regionId": "cViz",
|
||||||
"regionLabel": "Секции",
|
"regionLabel": "Секции",
|
||||||
"title": "Компонентная библиотека и MCP",
|
"title": "PROJECT VIDEO - DETAIL",
|
||||||
"description": "component-library.html",
|
"description": "project-video-detail.html",
|
||||||
"block": {
|
"block": {
|
||||||
"region": "cViz",
|
"region": "cViz",
|
||||||
"id": "component-library",
|
"id": "component-library",
|
||||||
"type": "componentLibrary",
|
"type": "componentLibrary",
|
||||||
"template": "component-library.html",
|
"template": "project-video-detail.html",
|
||||||
"adminLabel": "Компонентная библиотека и MCP",
|
"adminLabel": "PROJECT VIDEO - DETAIL",
|
||||||
"anchor": null,
|
"anchor": null,
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|
@ -457,69 +598,45 @@
|
||||||
"sliderItems": [
|
"sliderItems": [
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/various-modes.mp4",
|
"videoSrc": "./assets/media/various-modes.mp4",
|
||||||
"title": "Переключение режимов ИИ"
|
"title": "Переключение режимов ИИ",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/project-flow.mp4",
|
"videoSrc": "./assets/media/project-flow.mp4",
|
||||||
"title": "Создание нового проекта"
|
"title": "Создание нового проекта",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/ai-dock-mode.mp4",
|
"videoSrc": "./assets/media/ai-dock-mode.mp4",
|
||||||
"title": "ИИ-док"
|
"title": "ИИ-док",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/account.mp4",
|
"videoSrc": "./assets/media/account.mp4",
|
||||||
"title": "Аккаунт"
|
"title": "Аккаунт",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/code-edits.mp4",
|
"videoSrc": "./assets/media/code-edits.mp4",
|
||||||
"title": "Редактирование кода с ИИ"
|
"title": "Редактирование кода с ИИ",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/dev-server.mp4",
|
"videoSrc": "./assets/media/dev-server.mp4",
|
||||||
"title": "Dev-сервер с ИИ"
|
"title": "Dev-сервер с ИИ",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/webflow-mcp.mp4",
|
"videoSrc": "./assets/media/webflow-mcp.mp4",
|
||||||
"title": "Webflow MCP"
|
"title": "Webflow MCP",
|
||||||
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"videoSrc": "./assets/media/publish-flow.mp4",
|
"videoSrc": "./assets/media/publish-flow.mp4",
|
||||||
"title": "Публикация"
|
"title": "Публикация",
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oneClick": {
|
|
||||||
"heading": {
|
|
||||||
"muted": "Один",
|
|
||||||
"middle": "клик",
|
|
||||||
"main": "добавления."
|
|
||||||
},
|
|
||||||
"introHtml": "С помощью <span class=\"darker-70\">расширения NODE.DC для Webflow.</span> ",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"eyebrow": "Понимание компонентов",
|
|
||||||
"bodyHtml": "Используйте <span class=\"darker-70\">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"eyebrow": "Быстрый старт",
|
|
||||||
"bodyHtml": "Используйте быстрые <span class=\"darker-70\">Bun</span> сборки и автообновление для вашего <span class=\"darker-70\">кастомного кода.</span>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mcp": {
|
|
||||||
"heading": {
|
|
||||||
"muted": "Получите",
|
|
||||||
"middle": "более сильный",
|
|
||||||
"main": "MCP."
|
|
||||||
},
|
|
||||||
"introHtml": "Полностью переработанный и более быстрый MCP-сервер. ",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"eyebrow": "Понимание Webflow",
|
|
||||||
"bodyHtml": "Используйте <span class=\"darker-70\">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"scroll": {
|
"scroll": {
|
||||||
"speed": 0.55
|
"speed": 0.55
|
||||||
}
|
}
|
||||||
|
|
@ -565,420 +682,30 @@
|
||||||
"group": "Заголовок"
|
"group": "Заголовок"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "content.sliderItems.0.videoSrc",
|
"path": "content.sliderItems",
|
||||||
"label": "Слайд 1: видео",
|
"label": "Слайды",
|
||||||
"kind": "media",
|
"kind": "collection",
|
||||||
"renderAs": "attr",
|
"renderAs": "collection",
|
||||||
"group": "Слайдер"
|
"group": "Слайдер",
|
||||||
},
|
"itemTemplate": {
|
||||||
{
|
"enabled": true,
|
||||||
"path": "content.sliderItems.0.title",
|
|
||||||
"label": "Слайд 1: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.1.videoSrc",
|
|
||||||
"label": "Слайд 2: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.1.title",
|
|
||||||
"label": "Слайд 2: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.2.videoSrc",
|
|
||||||
"label": "Слайд 3: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.2.title",
|
|
||||||
"label": "Слайд 3: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.3.videoSrc",
|
|
||||||
"label": "Слайд 4: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.3.title",
|
|
||||||
"label": "Слайд 4: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.4.videoSrc",
|
|
||||||
"label": "Слайд 5: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.4.title",
|
|
||||||
"label": "Слайд 5: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.5.videoSrc",
|
|
||||||
"label": "Слайд 6: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.5.title",
|
|
||||||
"label": "Слайд 6: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.6.videoSrc",
|
|
||||||
"label": "Слайд 7: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.6.title",
|
|
||||||
"label": "Слайд 7: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.7.videoSrc",
|
|
||||||
"label": "Слайд 8: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.7.title",
|
|
||||||
"label": "Слайд 8: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.heading.muted",
|
|
||||||
"label": "Один клик: приглушённая строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.heading.middle",
|
|
||||||
"label": "Один клик: средняя строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.heading.main",
|
|
||||||
"label": "Один клик: основная строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.introHtml",
|
|
||||||
"label": "Один клик: вводный текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.items.0.eyebrow",
|
|
||||||
"label": "Один клик: пункт 1, подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.items.0.bodyHtml",
|
|
||||||
"label": "Один клик: пункт 1, текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.items.1.eyebrow",
|
|
||||||
"label": "Один клик: пункт 2, подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.oneClick.items.1.bodyHtml",
|
|
||||||
"label": "Один клик: пункт 2, текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "Один клик"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.heading.muted",
|
|
||||||
"label": "MCP: приглушённая строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.heading.middle",
|
|
||||||
"label": "MCP: средняя строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.heading.main",
|
|
||||||
"label": "MCP: основная строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.introHtml",
|
|
||||||
"label": "MCP: вводный текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.items.0.eyebrow",
|
|
||||||
"label": "MCP: пункт 1, подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.mcp.items.0.bodyHtml",
|
|
||||||
"label": "MCP: пункт 1, текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "MCP"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "cViz:aiModes:ai-modes.html",
|
|
||||||
"regionId": "cViz",
|
|
||||||
"regionLabel": "Секции",
|
|
||||||
"title": "Гибкие режимы ИИ",
|
|
||||||
"description": "ai-modes.html",
|
|
||||||
"block": {
|
|
||||||
"region": "cViz",
|
|
||||||
"id": "ai-modes",
|
|
||||||
"type": "aiModes",
|
|
||||||
"template": "ai-modes.html",
|
|
||||||
"adminLabel": "Гибкие режимы ИИ",
|
|
||||||
"anchor": null,
|
|
||||||
"content": {
|
|
||||||
"heading": {
|
|
||||||
"muted": "Гибкие",
|
|
||||||
"main": "режимы ИИ."
|
|
||||||
},
|
|
||||||
"introHtml": "Разные <span class=\"darker-70\">режимы</span> и <span class=\"darker-70\">модели</span> для разных задач.",
|
|
||||||
"sliderItems": [
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/various-modes.mp4",
|
|
||||||
"title": "Переключение режимов ИИ"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/project-flow.mp4",
|
|
||||||
"title": "Создание нового проекта"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/ai-dock-mode.mp4",
|
|
||||||
"title": "ИИ-док"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/account.mp4",
|
|
||||||
"title": "Аккаунт"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/code-edits.mp4",
|
|
||||||
"title": "Редактирование кода с ИИ"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/dev-server.mp4",
|
|
||||||
"title": "Dev-сервер с ИИ"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/webflow-mcp.mp4",
|
|
||||||
"title": "Webflow MCP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"videoSrc": "./assets/media/publish-flow.mp4",
|
"videoSrc": "./assets/media/publish-flow.mp4",
|
||||||
"title": "Публикация"
|
"title": "Новый слайд"
|
||||||
}
|
},
|
||||||
],
|
"itemFields": [
|
||||||
"scroll": {
|
{
|
||||||
"speed": 0.55
|
"path": "videoSrc",
|
||||||
}
|
"label": "Видео / картинка",
|
||||||
},
|
"kind": "media",
|
||||||
"editableFields": [
|
"renderAs": "attr"
|
||||||
{
|
},
|
||||||
"path": "content.scroll.speed",
|
{
|
||||||
"label": "Скорость скролла",
|
"path": "title",
|
||||||
"kind": "range",
|
"label": "Подпись",
|
||||||
"renderAs": "number",
|
"kind": "text",
|
||||||
"group": "Скролл",
|
"renderAs": "text"
|
||||||
"min": 0.2,
|
}
|
||||||
"max": 2,
|
]
|
||||||
"step": 0.05,
|
|
||||||
"suffix": "x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.heading.muted",
|
|
||||||
"label": "Заголовок: приглушённая строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Заголовок"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.heading.main",
|
|
||||||
"label": "Заголовок: основная строка",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Заголовок"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.introHtml",
|
|
||||||
"label": "Вводный текст",
|
|
||||||
"kind": "html",
|
|
||||||
"renderAs": "html",
|
|
||||||
"group": "Заголовок"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.0.videoSrc",
|
|
||||||
"label": "Слайд 1: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.0.title",
|
|
||||||
"label": "Слайд 1: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.1.videoSrc",
|
|
||||||
"label": "Слайд 2: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.1.title",
|
|
||||||
"label": "Слайд 2: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.2.videoSrc",
|
|
||||||
"label": "Слайд 3: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.2.title",
|
|
||||||
"label": "Слайд 3: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.3.videoSrc",
|
|
||||||
"label": "Слайд 4: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.3.title",
|
|
||||||
"label": "Слайд 4: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.4.videoSrc",
|
|
||||||
"label": "Слайд 5: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.4.title",
|
|
||||||
"label": "Слайд 5: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.5.videoSrc",
|
|
||||||
"label": "Слайд 6: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.5.title",
|
|
||||||
"label": "Слайд 6: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.6.videoSrc",
|
|
||||||
"label": "Слайд 7: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.6.title",
|
|
||||||
"label": "Слайд 7: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.7.videoSrc",
|
|
||||||
"label": "Слайд 8: видео",
|
|
||||||
"kind": "media",
|
|
||||||
"renderAs": "attr",
|
|
||||||
"group": "Слайдер"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "content.sliderItems.7.title",
|
|
||||||
"label": "Слайд 8: подпись",
|
|
||||||
"kind": "text",
|
|
||||||
"renderAs": "text",
|
|
||||||
"group": "Слайдер"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
304
index.html
304
index.html
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
<section class="s"><div class="c _w-60"><hgroup data-module="hg" class="ho"><h2 class="main-h h2"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div></hgroup></div><div class="c full"><div class="w-dyn-list"><div data-module="slider" data-scroll-speed="{{attr:content.scroll.speed}}" role="list" class="slider w-dyn-items"><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.0.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.0.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.1.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.1.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.2.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.2.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.3.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.3.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.4.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.4.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.5.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.5.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.6.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.6.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.7.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.7.title}}</h3></hgroup></div></div></div></div><div class="webgl-target-w"><div data-scale="1, 0.6" data-module="webgl-target" class="webgl-target"></div></div></section>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<section class="s"><div class="c _w-60"><hgroup data-module="hg" class="ho flip"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span class="darker-70">{{text:content.heading.middle}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div></hgroup></div><div class="c full"><div class="w-dyn-list"><div data-module="slider" data-scroll-speed="{{attr:content.scroll.speed}}" role="list" class="slider w-dyn-items"><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.0.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.0.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.1.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.1.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.2.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.2.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.3.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.3.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.4.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.4.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.5.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.5.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.6.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.6.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.7.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.7.title}}</h3></hgroup></div></div></div></div><div class="gap-v"><div class="c _w-60"><hgroup data-module="hg" class="ho"><h2 class="main-h h2"><span class="darker-40">{{text:content.oneClick.heading.muted}}</span><span class="darker-70">{{text:content.oneClick.heading.middle}}</span><span>{{text:content.oneClick.heading.main}}</span></h2><div class="txt-w"><div class="h-txt">{{html:content.oneClick.introHtml}}</div><div class="hg-par darker-40">{{text:content.oneClick.items.0.eyebrow}}</div><div class="h-txt">{{html:content.oneClick.items.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.oneClick.items.1.eyebrow}}</div><div class="h-txt">{{html:content.oneClick.items.1.bodyHtml}}</div></div></hgroup></div><div class="c _w-60"><hgroup data-module="hg" class="ho flip"><h2 class="main-h h2"><span class="darker-40">{{text:content.mcp.heading.muted}}</span><span class="darker-70">{{text:content.mcp.heading.middle}}</span><span>{{text:content.mcp.heading.main}}</span></h2><div class="txt-w"><div class="h-txt">{{html:content.mcp.introHtml}}</div><div class="hg-par darker-40">{{text:content.mcp.items.0.eyebrow}}</div><div class="h-txt">{{html:content.mcp.items.0.bodyHtml}}</div></div></hgroup></div></div></section>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<section id="features" class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video"><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><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size"><video src="{{attr:content.videoSrc}}" autoplay="true" muted="" loop="" playsinline="true" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<section class="s">
|
||||||
|
<div class="c _w-60">
|
||||||
|
<hgroup data-module="hg" class="ho">
|
||||||
|
<h2 class="main-h h2">
|
||||||
|
<span class="darker-40">{{text:content.heading.muted}}</span>
|
||||||
|
<span>{{text:content.heading.main}}</span>
|
||||||
|
</h2>
|
||||||
|
<div class="h-txt">{{html:content.introHtml}}</div>
|
||||||
|
</hgroup>
|
||||||
|
</div>
|
||||||
|
<div class="c full">
|
||||||
|
<div class="w-dyn-list">
|
||||||
|
<div data-module="slider" data-scroll-speed="{{attr:content.scroll.speed}}" role="list" class="slider w-dyn-items">
|
||||||
|
{{#each content.sliderItems}}
|
||||||
|
{{#if enabled}}
|
||||||
|
<div role="listitem" class="slide-w w-dyn-item">
|
||||||
|
<div class="video-cut">
|
||||||
|
<div class="video-screen">
|
||||||
|
<video src="{{attr:videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hgroup class="slide-he-w">
|
||||||
|
<h3 data-title="" class="slide-he">{{text:title}}</h3>
|
||||||
|
</hgroup>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="webgl-target-w">
|
||||||
|
<div data-scale="1, 0.6" data-module="webgl-target" class="webgl-target"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<section class="s">
|
||||||
|
<div class="c _w-60">
|
||||||
|
<hgroup data-module="hg" class="ho flip">
|
||||||
|
<h2 class="main-h h1">
|
||||||
|
<span class="darker-40">{{text:content.heading.muted}}</span>
|
||||||
|
<span class="darker-70">{{text:content.heading.middle}}</span>
|
||||||
|
<span>{{text:content.heading.main}}</span>
|
||||||
|
</h2>
|
||||||
|
<div class="h-txt">{{html:content.introHtml}}</div>
|
||||||
|
</hgroup>
|
||||||
|
</div>
|
||||||
|
<div class="c full">
|
||||||
|
<div class="w-dyn-list">
|
||||||
|
<div data-module="slider" data-scroll-speed="{{attr:content.scroll.speed}}" role="list" class="slider w-dyn-items">
|
||||||
|
{{#each content.sliderItems}}
|
||||||
|
{{#if enabled}}
|
||||||
|
<div role="listitem" class="slide-w w-dyn-item">
|
||||||
|
<div class="video-cut">
|
||||||
|
<div class="video-screen">
|
||||||
|
<div data-media-handle="" data-media-src="{{attr:videoSrc}}" class="video-el"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hgroup class="slide-he-w">
|
||||||
|
<h3 data-title="" class="slide-he">{{text:title}}</h3>
|
||||||
|
</hgroup>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<section id="features" class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video" data-adaptive-video-window style="--nodedc-video-height: {{attr:content.videoHeightPx}}px;"><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><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size" data-adaptive-video><video src="{{attr:content.videoSrc}}" autoplay="true" muted="" loop="" playsinline="true" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -48,12 +48,16 @@ function sendJson(res, status, payload) {
|
||||||
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readBody(req) {
|
async function readBodyBuffer(req) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for await (const chunk of req) {
|
for await (const chunk of req) {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
return Buffer.concat(chunks).toString("utf8");
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBody(req) {
|
||||||
|
return (await readBodyBuffer(req)).toString("utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
function runNodeScript(scriptName) {
|
function runNodeScript(scriptName) {
|
||||||
|
|
@ -146,6 +150,29 @@ function buildStoredFileName(fileName, mimeType) {
|
||||||
return `${safeBase}-${Date.now().toString(36)}${extension}`;
|
return `${safeBase}-${Date.now().toString(36)}${extension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveUploadedAssetBuffer({ fileName, mimeType, bucket, fileBuffer }) {
|
||||||
|
if (typeof fileName !== "string" || !fileName || !Buffer.isBuffer(fileBuffer)) {
|
||||||
|
throw new Error("Некорректный файл загрузки");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMimeType = mimeType || "application/octet-stream";
|
||||||
|
const resolvedBucket = sanitizeUploadBucket(bucket || "media");
|
||||||
|
const storedName = buildStoredFileName(fileName, resolvedMimeType);
|
||||||
|
const directory = join(uploadRoot, ...resolvedBucket.split("/"));
|
||||||
|
|
||||||
|
await mkdir(directory, { recursive: true });
|
||||||
|
await writeFile(join(directory, storedName), fileBuffer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
url: `./assets/uploads/${resolvedBucket}/${storedName}`,
|
||||||
|
fileName: storedName,
|
||||||
|
originalFileName: fileName,
|
||||||
|
mimeType: resolvedMimeType,
|
||||||
|
bucket: resolvedBucket,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function saveUploadedAsset(payload) {
|
async function saveUploadedAsset(payload) {
|
||||||
if (!isUploadPayload(payload)) {
|
if (!isUploadPayload(payload)) {
|
||||||
throw new Error("Некорректный payload загрузки");
|
throw new Error("Некорректный payload загрузки");
|
||||||
|
|
@ -158,21 +185,99 @@ async function saveUploadedAsset(payload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
|
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
|
||||||
const bucket = sanitizeUploadBucket(payload.bucket || "media");
|
|
||||||
const storedName = buildStoredFileName(payload.fileName, mimeType);
|
|
||||||
const fileBuffer = Buffer.from(match[2], "base64");
|
const fileBuffer = Buffer.from(match[2], "base64");
|
||||||
const directory = join(uploadRoot, ...bucket.split("/"));
|
|
||||||
|
|
||||||
await mkdir(directory, { recursive: true });
|
return saveUploadedAssetBuffer({
|
||||||
await writeFile(join(directory, storedName), fileBuffer);
|
fileName: payload.fileName,
|
||||||
|
mimeType,
|
||||||
|
bucket: payload.bucket || "media",
|
||||||
|
fileBuffer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultipartRequest(req) {
|
||||||
|
return String(req.headers["content-type"] || "").toLowerCase().startsWith("multipart/form-data");
|
||||||
|
}
|
||||||
|
|
||||||
|
function multipartBoundary(req) {
|
||||||
|
const contentType = String(req.headers["content-type"] || "");
|
||||||
|
const match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
|
||||||
|
return match?.[1] || match?.[2] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaderParams(value) {
|
||||||
|
const params = {};
|
||||||
|
const parts = String(value || "").split(";");
|
||||||
|
|
||||||
|
for (const part of parts.slice(1)) {
|
||||||
|
const [rawKey, ...rawValueParts] = part.trim().split("=");
|
||||||
|
if (!rawKey || !rawValueParts.length) continue;
|
||||||
|
const rawValue = rawValueParts.join("=").trim();
|
||||||
|
params[rawKey.toLowerCase()] = rawValue.replace(/^"|"$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMultipartUpload(req, bodyBuffer) {
|
||||||
|
const boundary = multipartBoundary(req);
|
||||||
|
if (!boundary) {
|
||||||
|
throw new Error("Не найден multipart boundary");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = {};
|
||||||
|
let filePart = null;
|
||||||
|
const body = bodyBuffer.toString("latin1");
|
||||||
|
const delimiter = `--${boundary}`;
|
||||||
|
|
||||||
|
for (const rawPart of body.split(delimiter)) {
|
||||||
|
let part = rawPart;
|
||||||
|
if (!part || part === "--" || part === "--\r\n") continue;
|
||||||
|
if (part.startsWith("\r\n")) part = part.slice(2);
|
||||||
|
if (part.endsWith("--")) part = part.slice(0, -2);
|
||||||
|
if (part.endsWith("\r\n")) part = part.slice(0, -2);
|
||||||
|
|
||||||
|
const headerEnd = part.indexOf("\r\n\r\n");
|
||||||
|
if (headerEnd < 0) continue;
|
||||||
|
|
||||||
|
const headerLines = part.slice(0, headerEnd).split("\r\n");
|
||||||
|
const headers = Object.fromEntries(
|
||||||
|
headerLines
|
||||||
|
.map((line) => {
|
||||||
|
const separator = line.indexOf(":");
|
||||||
|
if (separator < 0) return null;
|
||||||
|
return [line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim()];
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
const disposition = headers["content-disposition"];
|
||||||
|
const params = parseHeaderParams(disposition);
|
||||||
|
const fieldName = params.name;
|
||||||
|
const value = part.slice(headerEnd + 4);
|
||||||
|
|
||||||
|
if (!fieldName) continue;
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(params, "filename")) {
|
||||||
|
filePart = {
|
||||||
|
fileName: params.filename || fields.fileName || "asset.bin",
|
||||||
|
mimeType: headers["content-type"] || fields.mimeType || "application/octet-stream",
|
||||||
|
fileBuffer: Buffer.from(value, "latin1"),
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[fieldName] = Buffer.from(value, "latin1").toString("utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePart) {
|
||||||
|
throw new Error("Файл не найден в multipart payload");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
fileName: fields.fileName || filePart.fileName,
|
||||||
url: `./assets/uploads/${bucket}/${storedName}`,
|
mimeType: fields.mimeType || filePart.mimeType,
|
||||||
fileName: storedName,
|
bucket: fields.bucket || "media",
|
||||||
originalFileName: payload.fileName,
|
fileBuffer: filePart.fileBuffer,
|
||||||
mimeType,
|
|
||||||
bucket,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,8 +349,9 @@ const server = createServer(async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
|
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
|
||||||
const body = await readBody(req);
|
const result = isMultipartRequest(req)
|
||||||
const result = await saveUploadedAsset(JSON.parse(body));
|
? await saveUploadedAssetBuffer(parseMultipartUpload(req, await readBodyBuffer(req)))
|
||||||
|
: await saveUploadedAsset(JSON.parse(await readBody(req)));
|
||||||
sendJson(res, 200, result);
|
sendJson(res, 200, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ const blocks = [
|
||||||
region: "beforeCViz",
|
region: "beforeCViz",
|
||||||
id: "hero-waitlist",
|
id: "hero-waitlist",
|
||||||
type: "staticHtml",
|
type: "staticHtml",
|
||||||
template: "hero-waitlist.html",
|
template: "theses-request-demo.html",
|
||||||
adminLabel: "Первый экран и заявка",
|
adminLabel: "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||||
anchor: "hero",
|
anchor: "hero",
|
||||||
html: html.slice(heroStart, featuresStart),
|
html: html.slice(heroStart, featuresStart),
|
||||||
},
|
},
|
||||||
|
|
@ -72,8 +72,8 @@ const blocks = [
|
||||||
region: "beforeCViz",
|
region: "beforeCViz",
|
||||||
id: "feature-video",
|
id: "feature-video",
|
||||||
type: "staticHtml",
|
type: "staticHtml",
|
||||||
template: "feature-video.html",
|
template: "project-video-first.html",
|
||||||
adminLabel: "Демо и базовая логика",
|
adminLabel: "PROJECT VIDEO - FIRST",
|
||||||
anchor: "features",
|
anchor: "features",
|
||||||
html: html.slice(featuresStart, cVizStart),
|
html: html.slice(featuresStart, cVizStart),
|
||||||
},
|
},
|
||||||
|
|
@ -81,8 +81,8 @@ const blocks = [
|
||||||
region: "cViz",
|
region: "cViz",
|
||||||
id: "component-library",
|
id: "component-library",
|
||||||
type: "staticHtml",
|
type: "staticHtml",
|
||||||
template: "component-library.html",
|
template: "project-video-detail.html",
|
||||||
adminLabel: "Компонентная библиотека и MCP",
|
adminLabel: "PROJECT VIDEO - DETAIL",
|
||||||
anchor: null,
|
anchor: null,
|
||||||
html: productSections[0],
|
html: productSections[0],
|
||||||
},
|
},
|
||||||
|
|
@ -90,8 +90,8 @@ const blocks = [
|
||||||
region: "cViz",
|
region: "cViz",
|
||||||
id: "ai-modes",
|
id: "ai-modes",
|
||||||
type: "staticHtml",
|
type: "staticHtml",
|
||||||
template: "ai-modes.html",
|
template: "project-composition.html",
|
||||||
adminLabel: "Гибкие режимы ИИ",
|
adminLabel: "СОСТАВ ПРОЕКТА",
|
||||||
anchor: null,
|
anchor: null,
|
||||||
html: productSections[1],
|
html: productSections[1],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const blockRoot = join(templateRoot, "blocks");
|
||||||
const REGION_TOKEN = {
|
const REGION_TOKEN = {
|
||||||
beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
|
beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
|
||||||
cViz: "{{NODEDC_BLOCKS_CVIZ}}",
|
cViz: "{{NODEDC_BLOCKS_CVIZ}}",
|
||||||
|
sections: "{{NODEDC_BLOCKS_SECTIONS}}",
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
||||||
|
|
@ -176,14 +177,48 @@ async function renderRegion(region) {
|
||||||
return blocks.join("");
|
return blocks.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = renderTemplate(shell, { id: page.slug, ...page });
|
async function renderSections() {
|
||||||
for (const region of page.regions ?? []) {
|
const renderCounts = new Map();
|
||||||
const token = REGION_TOKEN[region.id];
|
const output = [];
|
||||||
if (!token) {
|
let cVizRun = [];
|
||||||
throw new Error(`Unknown region: ${region.id}`);
|
|
||||||
|
const flushCVizRun = () => {
|
||||||
|
if (!cVizRun.length) return;
|
||||||
|
output.push(`<div class="c-viz">${cVizRun.join("")}</div>`);
|
||||||
|
cVizRun = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const region of page.regions ?? []) {
|
||||||
|
for (const block of region.blocks ?? []) {
|
||||||
|
const html = await renderBlock(block, renderCounts);
|
||||||
|
if (!html) continue;
|
||||||
|
|
||||||
|
if (block.region === "cViz") {
|
||||||
|
cVizRun.push(html);
|
||||||
|
} else {
|
||||||
|
flushCVizRun();
|
||||||
|
output.push(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output = output.replace(token, await renderRegion(region));
|
flushCVizRun();
|
||||||
|
return output.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = renderTemplate(shell, { id: page.slug, ...page });
|
||||||
|
|
||||||
|
if (output.includes(REGION_TOKEN.sections)) {
|
||||||
|
output = output.replace(REGION_TOKEN.sections, await renderSections());
|
||||||
|
} else {
|
||||||
|
for (const region of page.regions ?? []) {
|
||||||
|
const token = REGION_TOKEN[region.id];
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(`Unknown region: ${region.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output.replace(token, await renderRegion(region));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const token of Object.values(REGION_TOKEN)) {
|
for (const token of Object.values(REGION_TOKEN)) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue