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;
|
||||
}
|
||||
|
||||
.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,
|
||||
.group-head-toggle input {
|
||||
.group-head-toggle input,
|
||||
.collection-visibility-toggle input {
|
||||
width: 2.42rem;
|
||||
height: 1.32rem;
|
||||
margin: 0;
|
||||
|
|
@ -1001,7 +1030,8 @@ body[data-file-picker-active="true"] .editor {
|
|||
}
|
||||
|
||||
.boolean-field input::before,
|
||||
.group-head-toggle input::before {
|
||||
.group-head-toggle input::before,
|
||||
.collection-visibility-toggle input::before {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
aspect-ratio: 1;
|
||||
|
|
@ -1012,12 +1042,14 @@ body[data-file-picker-active="true"] .editor {
|
|||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
transform: translateX(1.1rem);
|
||||
}
|
||||
|
|
@ -1098,6 +1130,10 @@ body[data-file-picker-active="true"] .editor {
|
|||
opacity: 0.16;
|
||||
}
|
||||
|
||||
.collection-item-card.is-hidden {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.collection-summary {
|
||||
min-height: 3rem;
|
||||
cursor: pointer;
|
||||
|
|
@ -1110,6 +1146,8 @@ body[data-file-picker-active="true"] .editor {
|
|||
}
|
||||
|
||||
.collection-summary-title {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.82rem;
|
||||
|
|
@ -1124,6 +1162,45 @@ body[data-file-picker-active="true"] .editor {
|
|||
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-icon-btn {
|
||||
display: grid;
|
||||
|
|
@ -1589,6 +1666,33 @@ textarea {
|
|||
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) {
|
||||
body {
|
||||
overflow: auto;
|
||||
|
|
|
|||
205
admin/admin.js
205
admin/admin.js
|
|
@ -9,8 +9,12 @@ const state = {
|
|||
filePickerActive: false,
|
||||
mediaUploadActive: false,
|
||||
filePickerResetTimer: null,
|
||||
deleteConfirmResolve: null,
|
||||
};
|
||||
|
||||
const SECTIONS_REGION_ID = "sections";
|
||||
const SECTIONS_REGION_LABEL = "Секции";
|
||||
|
||||
const el = {
|
||||
regions: document.querySelector("#regions"),
|
||||
addBlock: document.querySelector("#add-block"),
|
||||
|
|
@ -40,6 +44,11 @@ const el = {
|
|||
templateList: document.querySelector("#template-list"),
|
||||
templateModalClose: document.querySelector("#template-modal-close"),
|
||||
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 = {
|
||||
|
|
@ -423,6 +432,32 @@ function ensureStaticElements() {
|
|||
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() {
|
||||
return state.selected?.kind === "static";
|
||||
}
|
||||
|
|
@ -559,8 +594,7 @@ function readFileAsDataUrl(file) {
|
|||
}
|
||||
|
||||
function labelForRegion(region) {
|
||||
if (region.id === "beforeCViz") return "Главная";
|
||||
if (region.id === "cViz") return "Секции";
|
||||
if (region.id === SECTIONS_REGION_ID || region.id === "beforeCViz" || region.id === "cViz") return SECTIONS_REGION_LABEL;
|
||||
return region.label || region.id;
|
||||
}
|
||||
|
||||
|
|
@ -855,14 +889,13 @@ function renderRegions() {
|
|||
}
|
||||
|
||||
function blockTemplates() {
|
||||
return state.templates.map((template) => {
|
||||
const regionIndex = state.page.regions.findIndex((region) => region.id === template.regionId);
|
||||
const region = state.page.regions[regionIndex] || state.page.regions[0];
|
||||
const regionIndex = sectionsRegionIndex();
|
||||
|
||||
return state.templates.map((template) => {
|
||||
return {
|
||||
...template,
|
||||
regionIndex: regionIndex >= 0 ? regionIndex : 0,
|
||||
regionLabel: template.regionLabel || labelForRegion(region),
|
||||
regionIndex,
|
||||
regionLabel: SECTIONS_REGION_LABEL,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -907,6 +940,29 @@ function closeTemplateModal() {
|
|||
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) {
|
||||
const template = blockTemplates().find((candidate) => candidate.key === templateKey);
|
||||
if (!template) return;
|
||||
|
|
@ -1142,18 +1198,20 @@ async function uploadMediaFile({ block, editableField, hiddenInput, urlInput, fi
|
|||
fileName.textContent = "Сохраняем в assets...";
|
||||
|
||||
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", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
fileName: file.name,
|
||||
mimeType: file.type || "application/octet-stream",
|
||||
bucket: uploadBucketForField(block, editableField),
|
||||
dataUrl,
|
||||
}),
|
||||
body: formData,
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = await response.json().catch(() => ({
|
||||
ok: false,
|
||||
error: `Сервер вернул ${response.status}`,
|
||||
}));
|
||||
|
||||
if (!response.ok || !payload.ok) {
|
||||
throw new Error(payload.error || "Файл не удалось сохранить");
|
||||
|
|
@ -1500,6 +1558,54 @@ function renderRangeField({ block, editableField, grid }) {
|
|||
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 }) {
|
||||
const label = document.createElement("label");
|
||||
const input = document.createElement("input");
|
||||
|
|
@ -1532,6 +1638,11 @@ function renderCollectionScalarField({ block, field, grid }) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (field.kind === "number") {
|
||||
renderNumberField({ block, editableField: field, grid });
|
||||
return;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
|
|
@ -1666,8 +1777,14 @@ function renderCollectionField({ block, editableField, grid }) {
|
|||
const dragHandle = document.createElement("button");
|
||||
const body = 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.classList.toggle("is-hidden", item?.enabled === false);
|
||||
if (index === 0 && items.length <= 8) details.open = true;
|
||||
summary.className = "collection-summary";
|
||||
summaryTitle.className = "collection-summary-title";
|
||||
|
|
@ -1685,6 +1802,29 @@ function renderCollectionField({ block, editableField, grid }) {
|
|||
dragHandle.addEventListener("pointerdown", (event) => startCollectionPointerDrag(event, details, items, index, rerender));
|
||||
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;
|
||||
|
|
@ -1702,7 +1842,9 @@ function renderCollectionField({ block, editableField, grid }) {
|
|||
items.splice(index + 1, 0, prepareCollectionItemForInsert(items, item));
|
||||
rerender("Элемент коллекции продублирован локально.");
|
||||
}],
|
||||
["⌫", "Удалить", () => {
|
||||
["⌫", "Удалить", async () => {
|
||||
const confirmed = await requestDeleteConfirmation();
|
||||
if (!confirmed) return;
|
||||
items.splice(index, 1);
|
||||
rerender("Элемент коллекции удалён локально.");
|
||||
}],
|
||||
|
|
@ -1716,7 +1858,7 @@ function renderCollectionField({ block, editableField, grid }) {
|
|||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handler();
|
||||
Promise.resolve(handler()).catch((error) => setStatus(`Ошибка действия коллекции: ${error.message}`));
|
||||
});
|
||||
controls.append(button);
|
||||
});
|
||||
|
|
@ -1802,6 +1944,11 @@ function renderContentFields(block) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (editableField.kind === "number") {
|
||||
renderNumberField({ block, editableField, grid });
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = document.createElement("label");
|
||||
const labelRow = document.createElement("span");
|
||||
const labelText = document.createElement("span");
|
||||
|
|
@ -1886,6 +2033,7 @@ async function loadPage() {
|
|||
state.page = await pageResponse.json();
|
||||
state.templates = await templatesResponse.json();
|
||||
ensureStaticElements();
|
||||
ensureSectionsRegion();
|
||||
state.selected = { kind: "static" };
|
||||
renderAll();
|
||||
setDirty(false);
|
||||
|
|
@ -1979,11 +2127,14 @@ function pasteAfterSelected() {
|
|||
markDirty(`Блок «${pasted.adminLabel || pasted.id}» вставлен локально.`);
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
async function deleteSelected() {
|
||||
if (isStaticSelection()) return;
|
||||
const region = activeRegion();
|
||||
if (!region || state.selected.blockIndex == null) return;
|
||||
|
||||
const confirmed = await requestDeleteConfirmation();
|
||||
if (!confirmed) return;
|
||||
|
||||
region.blocks.splice(state.selected.blockIndex, 1);
|
||||
state.selected.blockIndex = Math.min(state.selected.blockIndex, region.blocks.length - 1);
|
||||
if (state.selected.blockIndex < 0) state.selected = { kind: "static" };
|
||||
|
|
@ -2010,8 +2161,20 @@ el.templateModalCancel.addEventListener("click", closeTemplateModal);
|
|||
el.templateModal.addEventListener("click", (event) => {
|
||||
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) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
|
@ -2023,7 +2186,7 @@ el.moveDown.addEventListener("click", () => moveSelected(1));
|
|||
el.duplicate.addEventListener("click", duplicateSelected);
|
||||
el.copy.addEventListener("click", copySelected);
|
||||
el.pasteAfter.addEventListener("click", pasteAfterSelected);
|
||||
el.deleteBlock.addEventListener("click", deleteSelected);
|
||||
el.deleteBlock.addEventListener("click", () => deleteSelected().catch((error) => setStatus(`Ошибка удаления: ${error.message}`)));
|
||||
|
||||
window.addEventListener("error", (event) => {
|
||||
recoverEditorSurface();
|
||||
|
|
|
|||
|
|
@ -190,6 +190,21 @@
|
|||
</footer>
|
||||
</article>
|
||||
</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>
|
||||
</body>
|
||||
</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)}
|
||||
.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}
|
||||
.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 MAX_SCROLL_SPEED = 2;
|
||||
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() {
|
||||
return window.matchMedia?.("(pointer: fine)").matches ?? true;
|
||||
|
|
@ -227,14 +231,106 @@
|
|||
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") {
|
||||
document.addEventListener("DOMContentLoaded", bindInteractiveScroll, { once: true });
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => {
|
||||
hydrateMediaHandles();
|
||||
bindInteractiveScroll();
|
||||
bindAdaptiveVideos();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
} else {
|
||||
hydrateMediaHandles();
|
||||
bindInteractiveScroll();
|
||||
bindAdaptiveVideos();
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
hydrateMediaHandles();
|
||||
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",
|
||||
"regionLabel": "Главная",
|
||||
"title": "Первый экран и заявка",
|
||||
"description": "hero-waitlist.html",
|
||||
"title": "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||
"description": "theses-request-demo.html",
|
||||
"block": {
|
||||
"region": "beforeCViz",
|
||||
"id": "hero-waitlist",
|
||||
"type": "heroWaitlist",
|
||||
"template": "hero-waitlist.html",
|
||||
"adminLabel": "Первый экран и заявка",
|
||||
"template": "theses-request-demo.html",
|
||||
"adminLabel": "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||
"anchor": "hero",
|
||||
"content": {
|
||||
"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",
|
||||
"regionLabel": "Главная",
|
||||
"title": "Демо и базовая логика",
|
||||
"description": "feature-video.html",
|
||||
"title": "PROJECT VIDEO - FIRST",
|
||||
"description": "project-video-first.html",
|
||||
"block": {
|
||||
"region": "beforeCViz",
|
||||
"id": "feature-video",
|
||||
"type": "featureVideo",
|
||||
"template": "feature-video.html",
|
||||
"adminLabel": "Демо и базовая логика",
|
||||
"template": "project-video-first.html",
|
||||
"adminLabel": "PROJECT VIDEO - FIRST",
|
||||
"anchor": "features",
|
||||
"content": {
|
||||
"windowTitle": "демо-видео.mp4",
|
||||
|
|
@ -336,7 +464,8 @@
|
|||
"eyebrow": "Деплой в один клик",
|
||||
"bodyHtml": "<span class=\"darker-70\">Нажмите и публикуйте</span> кастомный код, написанный ИИ, чтобы он стал доступен всем посетителям сайта."
|
||||
}
|
||||
]
|
||||
],
|
||||
"videoHeightPx": 620
|
||||
},
|
||||
"editableFields": [
|
||||
{
|
||||
|
|
@ -353,6 +482,18 @@
|
|||
"renderAs": "attr",
|
||||
"group": "Видео-окно"
|
||||
},
|
||||
{
|
||||
"path": "content.videoHeightPx",
|
||||
"label": "Высота видео",
|
||||
"kind": "number",
|
||||
"renderAs": "number",
|
||||
"group": "Видео-окно",
|
||||
"min": 240,
|
||||
"max": 900,
|
||||
"step": 1,
|
||||
"suffix": "px",
|
||||
"defaultValue": 620
|
||||
},
|
||||
{
|
||||
"path": "content.heading.muted",
|
||||
"label": "Заголовок: приглушённая строка",
|
||||
|
|
@ -435,17 +576,17 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"key": "cViz:componentLibrary:component-library.html",
|
||||
"key": "cViz:componentLibrary:project-video-detail.html",
|
||||
"regionId": "cViz",
|
||||
"regionLabel": "Секции",
|
||||
"title": "Компонентная библиотека и MCP",
|
||||
"description": "component-library.html",
|
||||
"title": "PROJECT VIDEO - DETAIL",
|
||||
"description": "project-video-detail.html",
|
||||
"block": {
|
||||
"region": "cViz",
|
||||
"id": "component-library",
|
||||
"type": "componentLibrary",
|
||||
"template": "component-library.html",
|
||||
"adminLabel": "Компонентная библиотека и MCP",
|
||||
"template": "project-video-detail.html",
|
||||
"adminLabel": "PROJECT VIDEO - DETAIL",
|
||||
"anchor": null,
|
||||
"content": {
|
||||
"heading": {
|
||||
|
|
@ -457,69 +598,45 @@
|
|||
"sliderItems": [
|
||||
{
|
||||
"videoSrc": "./assets/media/various-modes.mp4",
|
||||
"title": "Переключение режимов ИИ"
|
||||
"title": "Переключение режимов ИИ",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/project-flow.mp4",
|
||||
"title": "Создание нового проекта"
|
||||
"title": "Создание нового проекта",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/ai-dock-mode.mp4",
|
||||
"title": "ИИ-док"
|
||||
"title": "ИИ-док",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/account.mp4",
|
||||
"title": "Аккаунт"
|
||||
"title": "Аккаунт",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/code-edits.mp4",
|
||||
"title": "Редактирование кода с ИИ"
|
||||
"title": "Редактирование кода с ИИ",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/dev-server.mp4",
|
||||
"title": "Dev-сервер с ИИ"
|
||||
"title": "Dev-сервер с ИИ",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"videoSrc": "./assets/media/webflow-mcp.mp4",
|
||||
"title": "Webflow MCP"
|
||||
"title": "Webflow MCP",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"speed": 0.55
|
||||
}
|
||||
|
|
@ -565,420 +682,30 @@
|
|||
"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": "Слайдер"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"path": "content.sliderItems",
|
||||
"label": "Слайды",
|
||||
"kind": "collection",
|
||||
"renderAs": "collection",
|
||||
"group": "Слайдер",
|
||||
"itemTemplate": {
|
||||
"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.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": "Слайдер"
|
||||
"title": "Новый слайд"
|
||||
},
|
||||
"itemFields": [
|
||||
{
|
||||
"path": "videoSrc",
|
||||
"label": "Видео / картинка",
|
||||
"kind": "media",
|
||||
"renderAs": "attr"
|
||||
},
|
||||
{
|
||||
"path": "title",
|
||||
"label": "Подпись",
|
||||
"kind": "text",
|
||||
"renderAs": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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");
|
||||
}
|
||||
|
||||
async function readBody(req) {
|
||||
async function readBodyBuffer(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
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) {
|
||||
|
|
@ -146,6 +150,29 @@ function buildStoredFileName(fileName, mimeType) {
|
|||
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) {
|
||||
if (!isUploadPayload(payload)) {
|
||||
throw new Error("Некорректный payload загрузки");
|
||||
|
|
@ -158,21 +185,99 @@ async function saveUploadedAsset(payload) {
|
|||
}
|
||||
|
||||
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 directory = join(uploadRoot, ...bucket.split("/"));
|
||||
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, storedName), fileBuffer);
|
||||
return saveUploadedAssetBuffer({
|
||||
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 {
|
||||
ok: true,
|
||||
url: `./assets/uploads/${bucket}/${storedName}`,
|
||||
fileName: storedName,
|
||||
originalFileName: payload.fileName,
|
||||
mimeType,
|
||||
bucket,
|
||||
fileName: fields.fileName || filePart.fileName,
|
||||
mimeType: fields.mimeType || filePart.mimeType,
|
||||
bucket: fields.bucket || "media",
|
||||
fileBuffer: filePart.fileBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -244,8 +349,9 @@ const server = createServer(async (req, res) => {
|
|||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
|
||||
const body = await readBody(req);
|
||||
const result = await saveUploadedAsset(JSON.parse(body));
|
||||
const result = isMultipartRequest(req)
|
||||
? await saveUploadedAssetBuffer(parseMultipartUpload(req, await readBodyBuffer(req)))
|
||||
: await saveUploadedAsset(JSON.parse(await readBody(req)));
|
||||
sendJson(res, 200, result);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ const blocks = [
|
|||
region: "beforeCViz",
|
||||
id: "hero-waitlist",
|
||||
type: "staticHtml",
|
||||
template: "hero-waitlist.html",
|
||||
adminLabel: "Первый экран и заявка",
|
||||
template: "theses-request-demo.html",
|
||||
adminLabel: "ТЕЗИСЫ + ЗАПРОС ДЕМО",
|
||||
anchor: "hero",
|
||||
html: html.slice(heroStart, featuresStart),
|
||||
},
|
||||
|
|
@ -72,8 +72,8 @@ const blocks = [
|
|||
region: "beforeCViz",
|
||||
id: "feature-video",
|
||||
type: "staticHtml",
|
||||
template: "feature-video.html",
|
||||
adminLabel: "Демо и базовая логика",
|
||||
template: "project-video-first.html",
|
||||
adminLabel: "PROJECT VIDEO - FIRST",
|
||||
anchor: "features",
|
||||
html: html.slice(featuresStart, cVizStart),
|
||||
},
|
||||
|
|
@ -81,8 +81,8 @@ const blocks = [
|
|||
region: "cViz",
|
||||
id: "component-library",
|
||||
type: "staticHtml",
|
||||
template: "component-library.html",
|
||||
adminLabel: "Компонентная библиотека и MCP",
|
||||
template: "project-video-detail.html",
|
||||
adminLabel: "PROJECT VIDEO - DETAIL",
|
||||
anchor: null,
|
||||
html: productSections[0],
|
||||
},
|
||||
|
|
@ -90,8 +90,8 @@ const blocks = [
|
|||
region: "cViz",
|
||||
id: "ai-modes",
|
||||
type: "staticHtml",
|
||||
template: "ai-modes.html",
|
||||
adminLabel: "Гибкие режимы ИИ",
|
||||
template: "project-composition.html",
|
||||
adminLabel: "СОСТАВ ПРОЕКТА",
|
||||
anchor: null,
|
||||
html: productSections[1],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const blockRoot = join(templateRoot, "blocks");
|
|||
const REGION_TOKEN = {
|
||||
beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
|
||||
cViz: "{{NODEDC_BLOCKS_CVIZ}}",
|
||||
sections: "{{NODEDC_BLOCKS_SECTIONS}}",
|
||||
};
|
||||
|
||||
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
||||
|
|
@ -176,14 +177,48 @@ async function renderRegion(region) {
|
|||
return blocks.join("");
|
||||
}
|
||||
|
||||
let output = renderTemplate(shell, { id: page.slug, ...page });
|
||||
for (const region of page.regions ?? []) {
|
||||
const token = REGION_TOKEN[region.id];
|
||||
if (!token) {
|
||||
throw new Error(`Unknown region: ${region.id}`);
|
||||
async function renderSections() {
|
||||
const renderCounts = new Map();
|
||||
const output = [];
|
||||
let cVizRun = [];
|
||||
|
||||
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)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue