diff --git a/_astro/InstrumentSans-Bold.DWGdGw8L.woff2 b/_astro/InstrumentSans-Bold.DWGdGw8L.woff2 new file mode 100644 index 0000000..e4e387f Binary files /dev/null and b/_astro/InstrumentSans-Bold.DWGdGw8L.woff2 differ diff --git a/_astro/InstrumentSans-BoldItalic.BOjDmDe7.woff2 b/_astro/InstrumentSans-BoldItalic.BOjDmDe7.woff2 new file mode 100644 index 0000000..1d8b388 Binary files /dev/null and b/_astro/InstrumentSans-BoldItalic.BOjDmDe7.woff2 differ diff --git a/_astro/InstrumentSans-Italic.FeNa2EJX.woff2 b/_astro/InstrumentSans-Italic.FeNa2EJX.woff2 new file mode 100644 index 0000000..ef2e884 Binary files /dev/null and b/_astro/InstrumentSans-Italic.FeNa2EJX.woff2 differ diff --git a/_astro/InstrumentSans-Medium.B2xOBd5A.woff2 b/_astro/InstrumentSans-Medium.B2xOBd5A.woff2 new file mode 100644 index 0000000..026309a Binary files /dev/null and b/_astro/InstrumentSans-Medium.B2xOBd5A.woff2 differ diff --git a/_astro/InstrumentSans-MediumItalic.c03vUl6A.woff2 b/_astro/InstrumentSans-MediumItalic.c03vUl6A.woff2 new file mode 100644 index 0000000..b5b16ce Binary files /dev/null and b/_astro/InstrumentSans-MediumItalic.c03vUl6A.woff2 differ diff --git a/_astro/InstrumentSans-Regular.JnvgzjHK.woff2 b/_astro/InstrumentSans-Regular.JnvgzjHK.woff2 new file mode 100644 index 0000000..fd72c25 Binary files /dev/null and b/_astro/InstrumentSans-Regular.JnvgzjHK.woff2 differ diff --git a/_astro/InstrumentSans-SemiBold.wfsLgfkq.woff2 b/_astro/InstrumentSans-SemiBold.wfsLgfkq.woff2 new file mode 100644 index 0000000..8073ca4 Binary files /dev/null and b/_astro/InstrumentSans-SemiBold.wfsLgfkq.woff2 differ diff --git a/_astro/InstrumentSans-SemiBoldItalic.DRnKVj3E.woff2 b/_astro/InstrumentSans-SemiBoldItalic.DRnKVj3E.woff2 new file mode 100644 index 0000000..b3ba828 Binary files /dev/null and b/_astro/InstrumentSans-SemiBoldItalic.DRnKVj3E.woff2 differ diff --git a/admin/admin.css b/admin/admin.css index e152db4..f4baa1e 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -802,6 +802,15 @@ body[data-admin-mode="seo"] .json-details { display: none; } +body[data-admin-mode="knowledge"] #block-form, +body[data-admin-mode="knowledge"] #empty-state { + display: none !important; +} + +body:not([data-admin-mode="knowledge"]) #knowledge-form { + display: none !important; +} + .hidden { display: none !important; } @@ -1669,6 +1678,221 @@ select { line-height: 1.45; } +.knowledge-editor { + display: grid; + min-height: 0; + overflow: auto; + gap: 1rem; + padding: 0 0.2rem 5rem; + scrollbar-color: rgba(255, 255, 255, 0.25) transparent; +} + +.knowledge-tree { + display: grid; + gap: 0.28rem; + padding-inline: 0.35rem; +} + +.knowledge-tree-item, +.knowledge-tree-children { + display: grid; + gap: 0.22rem; +} + +.knowledge-tree-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + min-height: 3.1rem; + align-items: center; + gap: 0.28rem; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.035); + padding: 0.24rem 0.34rem 0.24rem calc(0.34rem + (var(--depth, 0) * 0.82rem)); + opacity: 0.86; +} + +.knowledge-tree-row[data-active="true"] { + background: rgba(255, 255, 255, 0.14); + opacity: 1; +} + +.knowledge-tree-row[data-disabled="true"] { + opacity: 0.42; +} + +.knowledge-tree-select { + display: grid; + grid-template-columns: 2.2rem minmax(0, 1fr); + min-width: 0; + min-height: 2.55rem; + align-items: center; + gap: 0.5rem; + border-radius: var(--launcher-radius-circle); + background: transparent; + color: var(--text-secondary); + padding: 0; + text-align: left; +} + +.knowledge-tree-icon { + display: grid; + width: 2.2rem; + height: 2.2rem; + place-items: center; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.055); + color: var(--text-muted); + font-size: 0.72rem; + font-weight: 850; +} + +.knowledge-tree-copy { + display: grid; + min-width: 0; + gap: 0.1rem; +} + +.knowledge-tree-title, +.knowledge-tree-meta { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.knowledge-tree-title { + color: var(--text-primary); + font-size: 0.82rem; + font-weight: 850; +} + +.knowledge-tree-meta { + color: var(--text-muted); + font-size: 0.68rem; + font-weight: 720; +} + +.knowledge-tree-actions { + display: inline-flex; + gap: 0.18rem; + opacity: 0; + transition: opacity 160ms ease; +} + +.knowledge-tree-row:hover .knowledge-tree-actions, +.knowledge-tree-row[data-active="true"] .knowledge-tree-actions { + opacity: 1; +} + +.knowledge-tree-action { + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.07); + color: var(--text-secondary); + padding: 0; + font-size: 0.82rem; +} + +.knowledge-tree-action:hover { + background: rgba(255, 255, 255, 0.14); + color: var(--text-primary); +} + +.knowledge-tree-action .trash-can-icon { + width: 0.92rem; + height: 0.92rem; +} + +.knowledge-empty-tree { + margin-inline: calc(var(--admin-nav-pad) + 0.35rem); +} + +.knowledge-rich-panel { + gap: 0.8rem; +} + +.knowledge-rich-toolbar { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.knowledge-rich-toolbar button { + min-height: 2.15rem; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.075); + color: var(--text-secondary); + padding: 0 0.72rem; + font-size: 0.72rem; + font-weight: 850; +} + +.knowledge-rich-toolbar button:hover { + background: rgba(255, 255, 255, 0.14); + color: var(--text-primary); +} + +.knowledge-rich-editor { + min-height: 28rem; + overflow: auto; + border-radius: var(--launcher-radius-card); + background: rgba(255, 255, 255, 0.04); + color: var(--text-secondary); + padding: 1.1rem; + font-size: 1rem; + line-height: 1.55; + outline: none; +} + +.knowledge-rich-editor:focus { + background: rgba(255, 255, 255, 0.06); +} + +.knowledge-rich-editor h2, +.knowledge-rich-editor h3, +.knowledge-rich-editor strong { + color: var(--text-primary); +} + +.knowledge-rich-editor h2 { + margin: 1.4rem 0 0.6rem; + font-size: 1.85rem; + line-height: 1.1; +} + +.knowledge-rich-editor h3 { + margin: 1.2rem 0 0.55rem; + font-size: 1.25rem; +} + +.knowledge-rich-editor p { + margin: 0 0 1rem; +} + +.knowledge-rich-editor img { + display: block; + max-width: 100%; + height: auto; + border-radius: 0.65rem; + margin: 1rem 0; +} + +.knowledge-rich-editor .knowledge-button { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + color: rgba(8, 8, 10, 0.96); + padding: 0 1.25rem; + font-size: 0.88rem; + font-weight: 850; + text-decoration: none; +} + .status { min-height: 1.25rem; margin: 0.8rem 0.35rem 0; @@ -2469,18 +2693,57 @@ select { width: min(28rem, calc(100vw - 2.8rem)); } +.knowledge-button-modal-layer { + z-index: 300; +} + +.knowledge-button-modal { + width: min(34rem, calc(100vw - 2.8rem)); +} + .media-action-head { padding-right: 1.25rem; } +.knowledge-button-head { + padding-right: 4.5rem; +} + .media-action-body { padding: 0 1.25rem 1.25rem; } +.knowledge-button-body { + display: grid; + gap: 0.9rem; + padding: 0 1.25rem 1.25rem; +} + +.knowledge-button-hint, +.knowledge-button-error { + margin: 0; + font-size: 0.78rem; + font-weight: 650; + line-height: 1.4; +} + +.knowledge-button-hint { + color: var(--text-muted); +} + +.knowledge-button-error { + min-height: 1.1rem; + color: rgba(255, 123, 108, 0.95); +} + .media-action-foot { justify-content: flex-end; } +.knowledge-button-foot { + justify-content: flex-end; +} + .media-modal-foot { justify-content: space-between; } diff --git a/admin/admin.js b/admin/admin.js index 8c41ab7..f894c0d 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -1,4 +1,5 @@ const ADMIN_LAYOUT_STORAGE_KEY = "nodedc-admin-layout-v1"; +const KNOWLEDGE_ROOT_ID = "__knowledge-root__"; function readAdminLayoutState() { try { @@ -35,12 +36,17 @@ function readLastMediaDirectory() { } function readAdminMode() { - return readAdminLayoutState().mode === "seo" ? "seo" : "content"; + const mode = readAdminLayoutState().mode; + return mode === "seo" || mode === "knowledge" ? mode : "content"; } const state = { mode: readAdminMode(), page: null, + knowledge: null, + knowledgeSelectedId: null, + knowledgeSavedSelection: null, + knowledgeOpenIds: new Set(readAdminLayoutState().knowledgeOpenIds || []), templates: [], selected: null, clipboard: null, @@ -52,6 +58,7 @@ const state = { filePickerResetTimer: null, deleteConfirmResolve: null, mediaActionResolve: null, + knowledgeButtonResolve: null, collectionOpenState: readCollectionOpenState(), mediaLibrary: { columns: [], @@ -140,6 +147,7 @@ const BLOCK_SEO_FIELDS = [ const el = { modeContent: document.querySelector("#admin-mode-content"), modeSeo: document.querySelector("#admin-mode-seo"), + modeKnowledge: document.querySelector("#admin-mode-knowledge"), sidebarTitle: document.querySelector("#sidebar-title"), workspaceRole: document.querySelector("#workspace-role"), editorHeading: document.querySelector("#editor-heading"), @@ -155,12 +163,14 @@ const el = { status: document.querySelector("#status"), empty: document.querySelector("#empty-state"), form: document.querySelector("#block-form"), + knowledgeForm: document.querySelector("#knowledge-form"), selectedRegion: document.querySelector("#selected-region"), selectedTitle: document.querySelector("#selected-title"), id: document.querySelector("#block-id"), label: document.querySelector("#block-label"), template: document.querySelector("#block-template"), anchor: document.querySelector("#block-anchor"), + targetOptions: document.querySelector("#block-target-options"), enabled: document.querySelector("#block-enabled"), contentFields: document.querySelector("#content-fields"), json: document.querySelector("#block-json"), @@ -210,6 +220,15 @@ const el = { mediaActionInput: document.querySelector("#media-action-input"), mediaActionCancel: document.querySelector("#media-action-cancel"), mediaActionConfirm: document.querySelector("#media-action-confirm"), + knowledgeButtonModal: document.querySelector("#knowledge-button-modal"), + knowledgeButtonClose: document.querySelector("#knowledge-button-close"), + knowledgeButtonName: document.querySelector("#knowledge-button-name"), + knowledgeButtonLink: document.querySelector("#knowledge-button-link"), + knowledgeButtonTarget: document.querySelector("#knowledge-button-target"), + knowledgeButtonTargetOptions: document.querySelector("#knowledge-button-target-options"), + knowledgeButtonError: document.querySelector("#knowledge-button-error"), + knowledgeButtonCancel: document.querySelector("#knowledge-button-cancel"), + knowledgeButtonConfirm: document.querySelector("#knowledge-button-confirm"), }; const STATIC_FALLBACK = { @@ -223,7 +242,7 @@ const STATIC_FALLBACK = { logoAlt: "главная", brandLabel: "NODE.DC", links: [ - { enabled: true, label: "О продукте", href: "", target: "index.html#features", linkMode: "target", linkTarget: "" }, + { enabled: true, label: "О продукте", href: "", target: "index.html#platform-composition", linkMode: "target", linkTarget: "" }, { enabled: true, label: "Лист ожидания", href: "", target: "index.html#hero", linkMode: "target", linkTarget: "" }, { enabled: true, @@ -233,7 +252,7 @@ const STATIC_FALLBACK = { linkMode: "href", linkTarget: "_blank", }, - { enabled: true, label: "Тарифы", href: "", target: "index.html#pricing-wrap", linkMode: "target", linkTarget: "" }, + { enabled: true, label: "Тарифы", href: "", target: "index.html#pricing-intro", linkMode: "target", linkTarget: "" }, ], }, notifications: [ @@ -496,6 +515,7 @@ function setFilePickerActive(active) { function recoverEditorSurface() { if (!activeBlock()) return; + el.knowledgeForm.classList.add("hidden"); el.empty.classList.add("hidden"); el.form.classList.remove("hidden"); } @@ -513,6 +533,15 @@ function clone(value) { return JSON.parse(JSON.stringify(value)); } +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + function getByPath(source, path) { return path.split(".").reduce((value, key) => value?.[key], source); } @@ -565,9 +594,14 @@ function ensureSeoModel() { } function setAdminMode(nextMode) { - const mode = nextMode === "seo" ? "seo" : "content"; + const mode = nextMode === "seo" || nextMode === "knowledge" ? nextMode : "content"; state.mode = mode; updateAdminLayoutState({ mode }); + if (mode === "knowledge" && !state.knowledge) { + loadKnowledge().catch((error) => setStatus(error.message)); + updateAdminModeSurface(); + return; + } if (state.page) { renderAll(); } else { @@ -577,19 +611,37 @@ function setAdminMode(nextMode) { function updateAdminModeSurface() { const isSeo = state.mode === "seo"; + const isKnowledge = state.mode === "knowledge"; document.body.dataset.adminMode = state.mode; - if (el.modeContent) el.modeContent.dataset.active = String(!isSeo); + if (el.modeContent) el.modeContent.dataset.active = String(!isSeo && !isKnowledge); if (el.modeSeo) el.modeSeo.dataset.active = String(isSeo); - if (el.sidebarTitle) el.sidebarTitle.textContent = isSeo ? "SEO" : "Администрирование"; - if (el.workspaceRole) el.workspaceRole.textContent = isSeo ? "SEO-режим сайта" : "Администратор сайта"; - if (el.editorHeading) el.editorHeading.textContent = isSeo ? "SEO сайта" : "Настройки сайта"; - if (el.panelKicker) el.panelKicker.textContent = isSeo ? "SEO" : "Контент"; - if (el.panelTitle) el.panelTitle.textContent = isSeo ? "Семантика и сниппеты" : "Редактируемые поля"; + if (el.modeKnowledge) el.modeKnowledge.dataset.active = String(isKnowledge); + if (el.sidebarTitle) el.sidebarTitle.textContent = isKnowledge ? "База знаний" : isSeo ? "SEO" : "Администрирование"; + if (el.workspaceRole) { + el.workspaceRole.textContent = isKnowledge ? "Редактор материалов" : isSeo ? "SEO-режим сайта" : "Администратор сайта"; + } + if (el.editorHeading) el.editorHeading.textContent = isKnowledge ? "База знаний" : isSeo ? "SEO сайта" : "Настройки сайта"; + if (el.panelKicker) el.panelKicker.textContent = isKnowledge ? "Knowledge" : isSeo ? "SEO" : "Контент"; + if (el.panelTitle) el.panelTitle.textContent = isKnowledge ? "Дерево и статья" : isSeo ? "Семантика и сниппеты" : "Редактируемые поля"; if (el.panelNote) { - el.panelNote.textContent = isSeo - ? "Поля готовят страницу и секции к семантической настройке и дальнейшей автоматизации." - : "Поля сгруппированы по смысловым подблокам верстки."; + el.panelNote.textContent = isKnowledge + ? "Материалы генерируются в статические SEO-страницы раздела /knowledge/." + : isSeo + ? "Поля готовят страницу и секции к семантической настройке и дальнейшей автоматизации." + : "Поля сгруппированы по смысловым подблокам верстки."; + } + if (el.addBlock) { + el.addBlock.title = isKnowledge ? "Добавить раздел в корень базы знаний" : "Добавить блок"; + el.addBlock.setAttribute("aria-label", el.addBlock.title); + } + if (el.save) { + el.save.textContent = isKnowledge ? "Сохранить базу" : "Сохранить модель"; + el.save.title = isKnowledge ? "Записать дерево в content/knowledge.json" : "Записать текущую модель в content/pages/home.json"; + } + if (el.render) { + el.render.textContent = isKnowledge ? "Обновить /knowledge/" : "Обновить index.html"; + el.render.title = isKnowledge ? "Сохранить дерево и пересобрать публичный раздел /knowledge/" : "Сохранить модель и пересобрать публичный index.html"; } } @@ -815,6 +867,127 @@ function isExternalHref(value) { return /^(https?:)?\/\//i.test(String(value || "")) || /^(mailto:|tel:)/i.test(String(value || "")); } +function normalizeLandingTargetCandidate(value) { + const raw = String(value || "").trim(); + if (!raw || /^javascript:/i.test(raw) || isExternalHref(raw)) return ""; + + let target = raw; + if (target.includes("#")) { + target = target.slice(target.indexOf("#") + 1); + } + + target = target + .replace(/^\/+/, "") + .replace(/^index\.html#?/i, "") + .replace(/^#+/, "") + .trim(); + + if (!target || target === "/" || target === "index.html" || target.startsWith("knowledge/") || isWindowTarget(target)) { + return ""; + } + + return target; +} + +function landingBlockTargets() { + const targets = []; + + for (const region of state.page?.regions || []) { + for (const block of region.blocks || []) { + if (block.enabled === false) continue; + const target = normalizeLandingTargetCandidate(block.anchor); + if (!target) continue; + targets.push({ + block, + id: block.id, + label: block.adminLabel || block.id, + target, + }); + } + } + + return targets.sort((left, right) => left.target.localeCompare(right.target, "ru")); +} + +function collectLandingButtonTargets() { + return landingBlockTargets().map((item) => item.target); +} + +function findLandingTargetByInput(value) { + const input = String(value || "").trim(); + const normalized = normalizeLandingTargetCandidate(input); + const lowerInput = input.toLowerCase(); + const lowerTarget = normalized.toLowerCase(); + + return ( + landingBlockTargets().find((item) => item.target.toLowerCase() === lowerTarget) || + landingBlockTargets().find((item) => item.id.toLowerCase() === lowerInput) || + landingBlockTargets().find((item) => item.label.toLowerCase() === lowerInput) || + null + ); +} + +function uniqueLandingTarget(base, exceptBlock = null) { + const usedTargets = new Set( + landingBlockTargets() + .filter((item) => item.block !== exceptBlock) + .map((item) => item.target), + ); + const root = safeBucketSegment(normalizeLandingTargetCandidate(base) || base || "section"); + let candidate = root; + let index = 2; + + while (usedTargets.has(candidate)) { + candidate = `${root}-${index}`; + index += 1; + } + + return candidate; +} + +function landingTargetDuplicates() { + const seen = new Map(); + const duplicates = []; + + for (const item of landingBlockTargets()) { + if (seen.has(item.target)) { + duplicates.push({ target: item.target, first: seen.get(item.target), second: item }); + continue; + } + seen.set(item.target, item); + } + + return duplicates; +} + +function assertUniqueLandingTargets() { + const duplicates = landingTargetDuplicates(); + if (!duplicates.length) return; + + const details = duplicates + .map(({ target, first, second }) => `${target}: ${first.label} / ${second.label}`) + .join("; "); + throw new Error(`Target должен быть уникальным для каждого блока. Дубли: ${details}`); +} + +function renderLandingTargetOptions() { + if (!el.targetOptions) return; + + el.targetOptions.innerHTML = ""; + for (const { target, label } of landingBlockTargets()) { + const option = document.createElement("option"); + option.value = target; + option.label = label; + el.targetOptions.append(option); + } +} + +function knowledgeTargetPromptText() { + const targets = landingBlockTargets(); + const hint = targets.length ? `\n\nДоступные блоки:\n${targets.map((item) => `${item.target} — ${item.label}`).join("\n")}` : ""; + return `Target или ID блока лендинга. Если пусто, откроется первый экран.${hint}`; +} + function normalizeChoiceLink(item, fallback = {}) { if (!item || typeof item !== "object") return; @@ -1586,6 +1759,9 @@ function addBlockFromTemplate(templateKey) { block.id = uniqueBlockId(block.id); block.enabled = true; + if (block.anchor) { + block.anchor = uniqueLandingTarget(block.anchor || block.adminLabel || block.id, block); + } block.scopeIds = true; block.seo = { ...BLOCK_SEO_DEFAULTS, @@ -1606,6 +1782,7 @@ function setStructuralControlsDisabled(disabled) { function renderEditorError(error) { console.error(error); + el.knowledgeForm.classList.add("hidden"); el.empty.classList.add("hidden"); el.form.classList.remove("hidden"); el.contentFields.innerHTML = ""; @@ -1619,6 +1796,7 @@ function renderEditorError(error) { function renderEditor() { updateAdminModeSurface(); + el.knowledgeForm.classList.add("hidden"); const region = activeRegion(); const block = activeBlock(); @@ -1630,6 +1808,7 @@ function renderEditor() { el.form.classList.add("hidden"); el.selectedRegion.textContent = "NODE.DC / Выберите блок"; el.selectedTitle.textContent = isSeo ? "SEO-слой" : "Контентный слой"; + renderLandingTargetOptions(); return; } @@ -1645,6 +1824,9 @@ function renderEditor() { el.label.value = block.adminLabel || ""; el.template.value = block.template || block.type || ""; el.anchor.value = block.anchor || ""; + el.anchor.placeholder = "hub-video / engine-video / applied-modules"; + el.anchor.title = "Публичный Target конкретного блока: используется как якорь лендинга и как ссылка из Knowledge."; + renderLandingTargetOptions(); el.enabled.checked = block.enabled !== false; el.id.readOnly = isStatic; el.anchor.disabled = isStatic; @@ -1667,6 +1849,10 @@ function renderEditor() { function renderAll() { try { + if (state.mode === "knowledge") { + renderKnowledgeAll(); + return; + } renderRegions(); renderEditor(); } catch (error) { @@ -1684,8 +1870,14 @@ function syncBlockFromFields() { if (!isStaticSelection()) { block.id = el.id.value.trim(); - block.anchor = el.anchor.value.trim() || null; + block.anchor = normalizeLandingTargetCandidate(el.anchor.value) || null; + el.anchor.value = block.anchor || ""; block.enabled = el.enabled.checked; + renderLandingTargetOptions(); + const duplicate = landingTargetDuplicates().find((item) => item.second.block === block || item.first.block === block); + if (duplicate) { + setStatus(`Target «${duplicate.target}» уже используется в другом блоке. Переименуйте один из target-ов перед сохранением.`); + } } el.json.value = JSON.stringify(block, null, 2); @@ -2932,7 +3124,8 @@ async function toggleMediaTrash() { } async function openMediaLibrary(context) { - const currentValuePath = pathFromUrl(context.hiddenInput.value || ""); + const currentValue = context.hiddenInput?.value || ""; + const currentValuePath = pathFromUrl(currentValue); const suggestedPath = `assets/uploads/${uploadBucketForField(context.block, context.editableField)}`; const lastPath = state.mediaLibrary.lastPath || readLastMediaDirectory(); const startPath = lastPath || (currentValuePath ? parentMediaPath(currentValuePath) : suggestedPath); @@ -2940,7 +3133,7 @@ async function openMediaLibrary(context) { state.mediaLibrary.context = context; state.mediaLibrary.trashMode = false; state.mediaLibrary.previousPath = ""; - state.mediaLibrary.selectedUrl = context.hiddenInput.value || ""; + state.mediaLibrary.selectedUrl = currentValue; state.mediaLibrary.selectedEntry = null; state.mediaLibrary.selectedPath = startPath; state.mediaLibrary.query = ""; @@ -2954,7 +3147,7 @@ async function openMediaLibrary(context) { refreshMediaSiteSize().catch((error) => setStatus(`Ошибка размера сайта: ${error.message}`)); try { - await loadMediaColumns(startPath, { selectedUrl: context.hiddenInput.value || "" }); + await loadMediaColumns(startPath, { selectedUrl: currentValue }); } catch (error) { setStatus(`Ошибка медиатеки: ${error.message}`); renderMediaLibrary(); @@ -2982,12 +3175,16 @@ function chooseSelectedMediaFile() { return; } - updateMediaFieldValue({ - ...context, - value: file.url, - }); + if (typeof context.onSelect === "function") { + context.onSelect(file.url, file); + } else { + updateMediaFieldValue({ + ...context, + value: file.url, + }); + context.setSource("file"); + } rememberMediaDirectory(file.directory || parentMediaPath(pathFromUrl(file.url))); - context.setSource("file"); closeMediaLibrary(); markDirty(`Выбран файл: ${file.url}. Нажмите «Сохранить модель», чтобы записать путь.`); } @@ -4081,6 +4278,808 @@ function syncBlockFromJson() { } } +function knowledgeTree() { + if (!state.knowledge) return []; + if (!Array.isArray(state.knowledge.tree)) state.knowledge.tree = []; + return state.knowledge.tree; +} + +function knowledgeWalk(nodes = knowledgeTree(), parent = null, callback = () => {}) { + nodes.forEach((node, index) => { + callback(node, parent, nodes, index); + knowledgeWalk(Array.isArray(node.children) ? node.children : [], node, callback); + }); +} + +function allKnowledgeIds() { + const ids = new Set(); + knowledgeWalk(knowledgeTree(), null, (node) => { + if (node.id) ids.add(node.id); + }); + return ids; +} + +function findKnowledgeNode(id) { + let found = null; + let foundParent = null; + let foundSiblings = null; + let foundIndex = -1; + + knowledgeWalk(knowledgeTree(), null, (node, parent, siblings, index) => { + if (found || node.id !== id) return; + found = node; + foundParent = parent; + foundSiblings = siblings; + foundIndex = index; + }); + + return found ? { node: found, parent: foundParent, siblings: foundSiblings, index: foundIndex } : null; +} + +function isKnowledgeRootId(id) { + return id === KNOWLEDGE_ROOT_ID; +} + +function isKnowledgeRootSelected() { + return isKnowledgeRootId(state.knowledgeSelectedId); +} + +function knowledgeRootTitle() { + return state.knowledge?.title || state.knowledge?.settings?.navTitle || "Knowledge"; +} + +function selectedKnowledgeNode() { + if (isKnowledgeRootSelected()) return state.knowledge; + return findKnowledgeNode(state.knowledgeSelectedId)?.node || null; +} + +function knowledgeSlug(value, fallback = "material") { + const map = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "c", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya", + }; + const safe = String(value || "") + .toLowerCase() + .replace(/[а-яё]/g, (char) => map[char] || "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + + return safe || fallback; +} + +function uniqueKnowledgeValue(values, base) { + const cleanBase = base || "material"; + if (!values.has(cleanBase)) return cleanBase; + let index = 2; + while (values.has(`${cleanBase}-${index}`)) index += 1; + return `${cleanBase}-${index}`; +} + +function createKnowledgeNode(title = "Новый материал", siblings = []) { + const ids = allKnowledgeIds(); + const siblingSlugs = new Set(siblings.map((node) => node.slug).filter(Boolean)); + const slug = uniqueKnowledgeValue(siblingSlugs, knowledgeSlug(title)); + return { + id: uniqueKnowledgeValue(ids, slug), + title, + slug, + enabled: true, + useContent: true, + seo: { + title, + description: "", + }, + bodyHtml: "

Новый материал базы знаний.

", + children: [], + }; +} + +function ensureKnowledgeSelection() { + if (!state.knowledge) return; + if (isKnowledgeRootSelected()) return; + const existing = state.knowledgeSelectedId ? findKnowledgeNode(state.knowledgeSelectedId) : null; + if (existing) return; + state.knowledgeSelectedId = KNOWLEDGE_ROOT_ID; +} + +function selectKnowledgeNode(id) { + state.knowledgeSelectedId = id; + updateAdminLayoutState({ knowledgeSelectedId: id }); + renderKnowledgeAll(); +} + +function persistKnowledgeOpenState() { + updateAdminLayoutState({ knowledgeOpenIds: [...state.knowledgeOpenIds] }); +} + +function setKnowledgeNodeOpen(id, open) { + if (!id) return; + if (open) state.knowledgeOpenIds.add(id); + else state.knowledgeOpenIds.delete(id); + persistKnowledgeOpenState(); +} + +function addKnowledgeNode(parentId = null) { + const parent = parentId ? findKnowledgeNode(parentId)?.node : null; + const siblings = parent ? (parent.children ||= []) : knowledgeTree(); + const node = createKnowledgeNode(parent ? "Новый подраздел" : "Новый раздел", siblings); + siblings.push(node); + if (parent) setKnowledgeNodeOpen(parent.id, true); + state.knowledgeSelectedId = node.id; + renderKnowledgeAll(); + markDirty("Материал добавлен в дерево базы знаний."); +} + +function moveKnowledgeNode(id, delta) { + const match = findKnowledgeNode(id); + if (!match) return; + const nextIndex = match.index + delta; + if (nextIndex < 0 || nextIndex >= match.siblings.length) return; + const [node] = match.siblings.splice(match.index, 1); + match.siblings.splice(nextIndex, 0, node); + renderKnowledgeAll(); + markDirty("Порядок дерева базы знаний изменён."); +} + +async function deleteKnowledgeNode(id) { + const match = findKnowledgeNode(id); + if (!match) return; + const confirmed = await requestDeleteConfirmation({ + title: "Удалить материал?", + body: `«${match.node.title || match.node.id}» и все вложенные элементы будут удалены из дерева.`, + }); + if (!confirmed) return; + match.siblings.splice(match.index, 1); + if (state.knowledgeSelectedId === id) { + state.knowledgeSelectedId = match.siblings[Math.min(match.index, match.siblings.length - 1)]?.id || match.parent?.id || KNOWLEDGE_ROOT_ID; + } + renderKnowledgeAll(); + markDirty("Материал удалён из дерева базы знаний."); +} + +function renderKnowledgeTreeItem(node, depth = 0) { + const item = document.createElement("div"); + const row = document.createElement("div"); + const select = document.createElement("button"); + const icon = document.createElement("span"); + const copy = document.createElement("span"); + const title = document.createElement("span"); + const meta = document.createElement("span"); + const actions = document.createElement("span"); + const children = Array.isArray(node.children) ? node.children : []; + const hasChildren = children.length > 0; + const isOpen = state.knowledgeOpenIds.has(node.id) || node.id === state.knowledgeSelectedId || children.some((child) => child.id === state.knowledgeSelectedId); + + item.className = "knowledge-tree-item"; + item.style.setProperty("--depth", String(depth)); + row.className = "knowledge-tree-row"; + row.dataset.active = String(node.id === state.knowledgeSelectedId); + row.dataset.disabled = String(node.enabled === false); + select.className = "knowledge-tree-select"; + select.type = "button"; + icon.className = "knowledge-tree-icon"; + icon.textContent = hasChildren ? (isOpen ? "▾" : "›") : node.useContent === false ? "G" : "A"; + copy.className = "knowledge-tree-copy"; + title.className = "knowledge-tree-title"; + title.textContent = node.title || node.id; + meta.className = "knowledge-tree-meta"; + meta.textContent = node.useContent === false ? "группа" : hasChildren ? "страница + ветка" : "статья"; + copy.append(title, meta); + select.append(icon, copy); + select.addEventListener("click", () => { + if (hasChildren) setKnowledgeNodeOpen(node.id, !isOpen); + selectKnowledgeNode(node.id); + }); + + actions.className = "knowledge-tree-actions"; + [ + ["+", "Добавить дочерний материал", () => addKnowledgeNode(node.id)], + ["↑", "Поднять", () => moveKnowledgeNode(node.id, -1)], + ["↓", "Опустить", () => moveKnowledgeNode(node.id, 1)], + [FINDER_TRASH_ICON_SVG, "Удалить", () => deleteKnowledgeNode(node.id)], + ].forEach(([symbol, titleText, handler]) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "knowledge-tree-action"; + button.title = titleText; + button.setAttribute("aria-label", titleText); + if (titleText === "Удалить") button.innerHTML = symbol; + else button.textContent = symbol; + button.addEventListener("click", (event) => { + event.stopPropagation(); + Promise.resolve(handler()).catch((error) => setStatus(`Ошибка дерева: ${error.message}`)); + }); + actions.append(button); + }); + + row.append(select, actions); + item.append(row); + + if (hasChildren && isOpen) { + const childList = document.createElement("div"); + childList.className = "knowledge-tree-children"; + children.forEach((child) => childList.append(renderKnowledgeTreeItem(child, depth + 1))); + item.append(childList); + } + + return item; +} + +function renderKnowledgeRootItem() { + const item = document.createElement("div"); + const row = document.createElement("div"); + const select = document.createElement("button"); + const icon = document.createElement("span"); + const copy = document.createElement("span"); + const title = document.createElement("span"); + const meta = document.createElement("span"); + const actions = document.createElement("span"); + const add = document.createElement("button"); + + item.className = "knowledge-tree-item knowledge-root-item"; + item.style.setProperty("--depth", "0"); + row.className = "knowledge-tree-row"; + row.dataset.active = String(isKnowledgeRootSelected()); + row.dataset.disabled = "false"; + select.className = "knowledge-tree-select"; + select.type = "button"; + icon.className = "knowledge-tree-icon"; + icon.textContent = "K"; + copy.className = "knowledge-tree-copy"; + title.className = "knowledge-tree-title"; + title.textContent = knowledgeRootTitle(); + meta.className = "knowledge-tree-meta"; + meta.textContent = "/knowledge/ главная"; + copy.append(title, meta); + select.append(icon, copy); + select.addEventListener("click", () => selectKnowledgeNode(KNOWLEDGE_ROOT_ID)); + + actions.className = "knowledge-tree-actions"; + add.type = "button"; + add.className = "knowledge-tree-action"; + add.title = "Добавить раздел в корень"; + add.setAttribute("aria-label", "Добавить раздел в корень"); + add.textContent = "+"; + add.addEventListener("click", (event) => { + event.stopPropagation(); + addKnowledgeNode(null); + }); + actions.append(add); + + row.append(select, actions); + item.append(row); + return item; +} + +function renderKnowledgeRegions() { + el.regions.innerHTML = ""; + const group = document.createElement("section"); + const title = document.createElement("h2"); + const root = document.createElement("div"); + + group.className = "region-group knowledge-region"; + title.className = "region-title"; + title.textContent = "База знаний"; + root.className = "knowledge-tree"; + root.append(renderKnowledgeRootItem()); + + if (!knowledgeTree().length) { + const empty = document.createElement("div"); + empty.className = "empty-note knowledge-empty-tree"; + empty.textContent = "Дерево пустое. Нажмите плюс сверху, чтобы добавить первый раздел."; + root.append(empty); + } else { + knowledgeTree().forEach((node) => root.append(renderKnowledgeTreeItem(node))); + } + + group.append(title, root); + el.regions.append(group); +} + +function updateKnowledgeNodeField(node, path, value, message = "Материал базы знаний обновлён.") { + setByPath(node, path, value); + markDirty(message); +} + +function createKnowledgeInput({ node, path, label, kind = "text", wide = false, placeholder = "" }) { + const field = document.createElement("label"); + const labelRow = document.createElement("span"); + const labelText = document.createElement("span"); + const kindLabel = document.createElement("span"); + const input = kind === "textarea" ? document.createElement("textarea") : document.createElement("input"); + const pathLabel = document.createElement("span"); + + field.className = `field-control${wide || kind === "textarea" ? " wide" : ""}`; + labelRow.className = "field-label-row"; + labelText.className = "field-label-text"; + labelText.textContent = label; + kindLabel.className = "field-kind"; + kindLabel.textContent = kind; + input.value = getByPath(node, path) ?? ""; + input.placeholder = placeholder; + input.dataset.knowledgePath = path; + if (input.tagName === "INPUT") { + input.type = kind === "url" ? "url" : "text"; + input.autocomplete = "off"; + } + input.addEventListener("input", () => { + updateKnowledgeNodeField(node, path, input.value); + if (path === "title" || path === "settings.navTitle") renderKnowledgeRegions(); + }); + pathLabel.className = "field-path"; + pathLabel.textContent = path; + labelRow.append(labelText, kindLabel); + field.append(labelRow, input, pathLabel); + return field; +} + +function createKnowledgeCheckbox({ node, path, label, checkedDefault = true }) { + const field = document.createElement("label"); + const labelRow = document.createElement("span"); + const labelText = document.createElement("span"); + const kindLabel = document.createElement("span"); + const input = document.createElement("input"); + const pathLabel = document.createElement("span"); + + field.className = "field-control boolean-field"; + labelRow.className = "field-label-row"; + labelText.className = "field-label-text"; + labelText.textContent = label; + kindLabel.className = "field-kind"; + kindLabel.textContent = "boolean"; + input.type = "checkbox"; + const value = getByPath(node, path); + input.checked = value == null ? checkedDefault : value !== false; + input.addEventListener("change", () => { + updateKnowledgeNodeField(node, path, input.checked); + renderKnowledgeEditor(); + renderKnowledgeRegions(); + }); + pathLabel.className = "field-path"; + pathLabel.textContent = path; + labelRow.append(labelText, kindLabel); + field.append(labelRow, input, pathLabel); + return field; +} + +function knowledgeSelectionNodeInsideEditor(editor, node) { + if (!editor || !node) return false; + const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; + return Boolean(element && editor.contains(element)); +} + +function saveKnowledgeEditorSelection() { + const selection = window.getSelection(); + if (!selection?.rangeCount) return; + const editor = el.knowledgeForm.querySelector("[data-knowledge-richtext]"); + if (!knowledgeSelectionNodeInsideEditor(editor, selection.anchorNode) || !knowledgeSelectionNodeInsideEditor(editor, selection.focusNode)) return; + state.knowledgeSavedSelection = selection.getRangeAt(0).cloneRange(); +} + +function restoreKnowledgeEditorSelection(editor) { + editor.focus(); + let range = state.knowledgeSavedSelection; + if (!range || !knowledgeSelectionNodeInsideEditor(editor, range.commonAncestorContainer)) { + range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); + state.knowledgeSavedSelection = range.cloneRange(); + } + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + return range; +} + +function syncKnowledgeRichText(node, editor) { + setByPath(node, editor.dataset.knowledgeBodyPath || "bodyHtml", editor.innerHTML.trim()); + markDirty("Текст статьи обновлён."); +} + +function execKnowledgeCommand(editor, node, command, value = null) { + restoreKnowledgeEditorSelection(editor); + document.execCommand(command, false, value); + syncKnowledgeRichText(node, editor); +} + +function insertKnowledgeHtml(editor, node, html) { + restoreKnowledgeEditorSelection(editor); + document.execCommand("insertHTML", false, html); + saveKnowledgeEditorSelection(); + syncKnowledgeRichText(node, editor); +} + +function escapeKnowledgeShortcodeAttr(value) { + return String(value ?? "") + .replace(/\s+/g, " ") + .trim() + .replaceAll("&", "&") + .replaceAll('"', """) + .replaceAll("[", "(") + .replaceAll("]", ")"); +} + +function normalizeKnowledgeLandingTarget(value) { + return String(value || "") + .trim() + .replace(/^#+/, "") + .replace(/^index\.html#/i, "") + .replace(/^\/?#/, ""); +} + +function normalizeKnowledgeButtonLink(value) { + return (String(value ?? "").trim() || "/").replace(/^\/+(https?:\/\/)/i, "$1").replace(/^\/+((?:mailto|tel):)/i, "$1"); +} + +function knowledgeButtonShortcode({ name, link, target, blockId }) { + const safeName = escapeKnowledgeShortcodeAttr(name || "Посмотреть"); + const safeLink = escapeKnowledgeShortcodeAttr(normalizeKnowledgeButtonLink(link)); + const safeTarget = normalizeKnowledgeLandingTarget(target); + const safeBlockId = String(blockId || "").trim(); + const blockPart = safeBlockId ? ` block="${escapeKnowledgeShortcodeAttr(safeBlockId)}"` : ""; + const targetPart = safeTarget ? ` target="${escapeKnowledgeShortcodeAttr(safeTarget)}"` : ""; + return `[button name="${safeName}" link="${safeLink}"${blockPart}${targetPart}]`; +} + +function renderKnowledgeButtonTargetOptions() { + if (!el.knowledgeButtonTargetOptions) return; + + el.knowledgeButtonTargetOptions.innerHTML = ""; + const added = new Set(); + + for (const { id, label, target } of landingBlockTargets()) { + const targetKey = target.toLowerCase(); + if (!added.has(targetKey)) { + const option = document.createElement("option"); + option.value = target; + option.label = `${label} / ${id}`; + el.knowledgeButtonTargetOptions.append(option); + added.add(targetKey); + } + + const idKey = String(id || "").toLowerCase(); + if (id && !added.has(idKey)) { + const option = document.createElement("option"); + option.value = id; + option.label = `${label} / block id`; + el.knowledgeButtonTargetOptions.append(option); + added.add(idKey); + } + } +} + +function setKnowledgeButtonError(message = "") { + if (el.knowledgeButtonError) el.knowledgeButtonError.textContent = message; +} + +function closeKnowledgeButtonModal(value = null) { + const resolve = state.knowledgeButtonResolve; + + state.knowledgeButtonResolve = null; + el.knowledgeButtonModal.classList.add("hidden"); + setKnowledgeButtonError(""); + if (resolve) resolve(value); +} + +function confirmKnowledgeButtonModal() { + const name = el.knowledgeButtonName.value.trim(); + if (!name) { + setKnowledgeButtonError("Название кнопки обязательно."); + el.knowledgeButtonName.focus(); + return; + } + + const link = normalizeKnowledgeButtonLink(el.knowledgeButtonLink.value); + el.knowledgeButtonLink.value = link; + closeKnowledgeButtonModal({ + name, + link, + targetInput: el.knowledgeButtonTarget.value.trim(), + }); +} + +function requestKnowledgeButtonInput({ name = "Посмотреть демо", link = "/", target = "" } = {}) { + if (state.knowledgeButtonResolve) closeKnowledgeButtonModal(null); + + return new Promise((resolve) => { + state.knowledgeButtonResolve = resolve; + renderKnowledgeButtonTargetOptions(); + setKnowledgeButtonError(""); + el.knowledgeButtonName.value = name; + el.knowledgeButtonLink.value = normalizeKnowledgeButtonLink(link); + el.knowledgeButtonTarget.value = normalizeKnowledgeLandingTarget(target); + el.knowledgeButtonModal.classList.remove("hidden"); + el.knowledgeButtonName.focus(); + el.knowledgeButtonName.select(); + }); +} + +function openKnowledgeMediaPicker(editor, node) { + saveKnowledgeEditorSelection(); + const fakeInput = { value: "" }; + const fakeText = { textContent: "", title: "" }; + const fakePreview = document.createElement("div"); + const fakeHint = document.createElement("span"); + const fakeError = document.createElement("span"); + const fakeUrlInput = { value: "" }; + openMediaLibrary({ + block: { id: node.id || "knowledge" }, + editableField: { path: "content.bodyHtml", label: "Изображение статьи" }, + hiddenInput: fakeInput, + urlInput: fakeUrlInput, + fileName: fakeText, + preview: fakePreview, + hint: fakeHint, + error: fakeError, + setSource() {}, + onSelect(url) { + const safeUrl = String(url || ""); + if (!safeUrl) return; + insertKnowledgeHtml(editor, node, ``); + }, + }).catch((error) => setStatus(`Ошибка медиатеки: ${error.message}`)); +} + +function renderKnowledgeToolbar(editor, node) { + const toolbar = document.createElement("div"); + toolbar.className = "knowledge-rich-toolbar"; + const buttons = [ + ["P", "Абзац", () => execKnowledgeCommand(editor, node, "formatBlock", "P")], + ["H2", "Заголовок H2", () => execKnowledgeCommand(editor, node, "formatBlock", "H2")], + ["H3", "Заголовок H3", () => execKnowledgeCommand(editor, node, "formatBlock", "H3")], + ["B", "Жирный", () => execKnowledgeCommand(editor, node, "bold")], + ["I", "Курсив", () => execKnowledgeCommand(editor, node, "italic")], + ["UL", "Список", () => execKnowledgeCommand(editor, node, "insertUnorderedList")], + ["OL", "Нумерованный список", () => execKnowledgeCommand(editor, node, "insertOrderedList")], + ["Link", "Ссылка", () => { + const href = window.prompt("Адрес ссылки", "/knowledge/"); + if (!href) return; + execKnowledgeCommand(editor, node, "createLink", href); + }], + ["Button", "Кнопка", async () => { + saveKnowledgeEditorSelection(); + const values = await requestKnowledgeButtonInput(); + if (!values) return; + const targetInput = values.targetInput; + const landingTarget = findLandingTargetByInput(targetInput); + const target = landingTarget ? landingTarget.target : targetInput; + const blockId = landingTarget?.id || ""; + insertKnowledgeHtml( + editor, + node, + `

${escapeHtml(knowledgeButtonShortcode({ name: values.name, link: values.link, target, blockId }))}

`, + ); + }], + ["Image", "Изображение", () => openKnowledgeMediaPicker(editor, node)], + ]; + + buttons.forEach(([label, title, action]) => { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = label; + button.title = title; + button.addEventListener("mousedown", (event) => event.preventDefault()); + button.addEventListener("click", action); + toolbar.append(button); + }); + + return toolbar; +} + +function renderKnowledgeEditor() { + const isRoot = isKnowledgeRootSelected(); + const node = selectedKnowledgeNode(); + el.empty.classList.add("hidden"); + el.form.classList.add("hidden"); + el.knowledgeForm.classList.remove("hidden"); + el.knowledgeForm.innerHTML = ""; + + if (!state.knowledge) { + el.knowledgeForm.innerHTML = '
Загружаем базу знаний...
'; + return; + } + + if (!node) { + const empty = document.createElement("section"); + empty.className = "content-panel"; + empty.innerHTML = "

Дерево базы знаний пустое

Нажмите плюс в левом верхнем углу, чтобы создать первый раздел.

"; + el.knowledgeForm.append(empty); + return; + } + + state.knowledge.settings ||= {}; + const bodyPath = isRoot ? "introHtml" : "bodyHtml"; + const match = isRoot ? null : findKnowledgeNode(node.id); + const parentTitle = match?.parent?.title || state.knowledge.title || "База знаний"; + el.selectedRegion.textContent = isRoot + ? `NODE.DC / ${knowledgeRootTitle()} / /knowledge/` + : `NODE.DC / ${state.knowledge.title || "База знаний"} / ${parentTitle}`; + el.selectedTitle.textContent = isRoot ? knowledgeRootTitle() : node.title || "Материал"; + + const metaPanel = document.createElement("section"); + const metaHead = document.createElement("div"); + const metaTitle = document.createElement("h3"); + const metaKicker = document.createElement("div"); + const grid = document.createElement("div"); + metaPanel.className = "content-panel"; + metaHead.className = "panel-head"; + metaKicker.className = "section-kicker"; + metaKicker.textContent = isRoot ? "Корневая страница" : "Узел дерева"; + metaTitle.textContent = isRoot ? "Настройки /knowledge/" : "Параметры материала"; + metaHead.append(Object.assign(document.createElement("div"), { innerHTML: `${metaKicker.outerHTML}

${metaTitle.textContent}

` })); + grid.className = "group-grid"; + if (isRoot) { + grid.append( + createKnowledgeInput({ node, path: "title", label: "H1 / название в sidebar", wide: false }), + createKnowledgeInput({ node, path: "settings.navTitle", label: "Название в шапке", wide: false }), + createKnowledgeInput({ node, path: "settings.searchPlaceholder", label: "Placeholder поиска", wide: false }), + createKnowledgeInput({ node, path: "settings.homeHref", label: "Ссылка логотипа", kind: "url", wide: false }), + createKnowledgeInput({ node, path: "settings.themeDefault", label: "Тема по умолчанию", wide: false }), + createKnowledgeCheckbox({ node, path: "settings.downloadEnabled", label: "Показывать Download", checkedDefault: false }), + ); + } else { + grid.append( + createKnowledgeInput({ node, path: "title", label: "Название", wide: false }), + createKnowledgeInput({ node, path: "slug", label: "Slug", wide: false }), + createKnowledgeCheckbox({ node, path: "enabled", label: "Публиковать" }), + createKnowledgeCheckbox({ node, path: "useContent", label: "Использовать как информационную страницу" }), + ); + } + metaPanel.append(metaHead, grid); + + const seoPanel = document.createElement("section"); + const seoHead = document.createElement("div"); + const seoGrid = document.createElement("div"); + seoPanel.className = "content-panel"; + seoHead.className = "panel-head"; + seoHead.innerHTML = `
SEO

${isRoot ? "Сниппет раздела" : "Сниппет страницы"}

Поля попадут в title, description, Open Graph и sitemap.`; + seoGrid.className = "group-grid"; + if (isRoot) { + seoGrid.append( + createKnowledgeInput({ node, path: "settings.description", label: "SEO description / описание", kind: "textarea", wide: true }), + ); + } else { + node.seo ||= {}; + seoGrid.append( + createKnowledgeInput({ node, path: "seo.title", label: "SEO title", wide: true }), + createKnowledgeInput({ node, path: "seo.description", label: "SEO description", kind: "textarea", wide: true }), + ); + } + seoPanel.append(seoHead, seoGrid); + + const contentPanel = document.createElement("section"); + const contentHead = document.createElement("div"); + const editor = document.createElement("div"); + contentPanel.className = "content-panel knowledge-rich-panel"; + contentHead.className = "panel-head"; + contentHead.innerHTML = `
Контент

${isRoot ? "Текст главной страницы" : "Текст статьи"}

H2/H3 формируют правый список “На этой странице”.`; + editor.className = "knowledge-rich-editor"; + editor.contentEditable = isRoot || node.useContent !== false ? "true" : "false"; + editor.dataset.knowledgeRichtext = "true"; + editor.dataset.knowledgeBodyPath = bodyPath; + editor.innerHTML = getByPath(node, bodyPath) || "

"; + editor.addEventListener("focusout", () => saveKnowledgeEditorSelection()); + editor.addEventListener("keyup", () => saveKnowledgeEditorSelection()); + editor.addEventListener("mouseup", () => saveKnowledgeEditorSelection()); + editor.addEventListener("input", () => syncKnowledgeRichText(node, editor)); + contentPanel.append(contentHead, renderKnowledgeToolbar(editor, node), editor); + if (!isRoot && node.useContent === false) { + const disabledNote = document.createElement("p"); + disabledNote.className = "group-desc"; + disabledNote.textContent = "Контент сохранится в модели, но публичная страница для этого узла не будет генерироваться, пока флаг выключен."; + contentPanel.append(disabledNote); + } + + const jsonDetails = document.createElement("details"); + jsonDetails.className = "json-details knowledge-json"; + jsonDetails.innerHTML = `${isRoot ? "JSON всей базы знаний" : "JSON выбранного материала"}`; + const jsonField = document.createElement("label"); + const json = document.createElement("textarea"); + jsonField.className = "json-field"; + json.value = JSON.stringify(node, null, 2); + json.addEventListener("blur", () => { + try { + const parsed = JSON.parse(json.value); + if (isRoot) { + state.knowledge = parsed; + state.knowledgeSelectedId = KNOWLEDGE_ROOT_ID; + } else { + const match = findKnowledgeNode(node.id); + if (!match) return; + match.siblings[match.index] = parsed; + state.knowledgeSelectedId = parsed.id || node.id; + } + renderKnowledgeAll(); + markDirty(isRoot ? "JSON базы знаний применён локально." : "JSON материала применён локально."); + } catch (error) { + setStatus(`Ошибка JSON ${isRoot ? "базы знаний" : "материала"}: ${error.message}`); + } + }); + jsonField.innerHTML = "Техническое представление"; + jsonField.append(json); + jsonDetails.append(jsonField); + + el.knowledgeForm.append(metaPanel, seoPanel, contentPanel, jsonDetails); +} + +function renderKnowledgeAll() { + updateAdminModeSurface(); + ensureKnowledgeSelection(); + renderKnowledgeRegions(); + renderKnowledgeEditor(); +} + +async function loadKnowledge() { + const response = await fetch("/api/knowledge"); + if (!response.ok) throw new Error(await response.text()); + state.knowledge = await response.json(); + state.knowledgeSelectedId = readAdminLayoutState().knowledgeSelectedId || state.knowledgeSelectedId || null; + ensureKnowledgeSelection(); + renderAll(); + setDirty(false); + setStatus("Модель knowledge.json загружена."); +} + +async function persistKnowledge() { + const response = await fetch("/api/knowledge", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(state.knowledge), + }); + + if (!response.ok) throw new Error(await response.text()); +} + +async function saveKnowledge() { + await persistKnowledge(); + setDirty(false); + setStatus("База знаний сохранена в content/knowledge.json."); +} + +async function renderKnowledge() { + await persistKnowledge(); + const response = await fetch("/api/render/knowledge", { method: "POST" }); + const payload = await response.json(); + + if (!response.ok || !payload.ok) { + throw new Error(payload.error || "Render failed"); + } + + setDirty(false); + setStatus(payload.stdout ? `База знаний сохранена. ${payload.stdout}` : "База знаний сохранена и пересобрана."); +} + async function loadPage() { const [pageResponse, templatesResponse] = await Promise.all([ fetch("/api/page/home"), @@ -4099,10 +5098,15 @@ async function loadPage() { renderAll(); setDirty(false); setStatus("Модель home.json загружена."); + + if (state.mode === "knowledge" && !state.knowledge) { + await loadKnowledge(); + } } async function persistPage() { if (activeBlock()) syncBlockFromFields(); + assertUniqueLandingTargets(); const response = await fetch("/api/page/home", { method: "PUT", @@ -4157,6 +5161,9 @@ function duplicateSelected() { const duplicate = clone(block); duplicate.id = uniqueId(block.id); duplicate.adminLabel = `${block.adminLabel || block.id} — копия`; + if (duplicate.anchor) { + duplicate.anchor = uniqueLandingTarget(duplicate.anchor, duplicate); + } duplicate.scopeIds = true; region.blocks.splice(state.selected.blockIndex + 1, 0, duplicate); state.selected.blockIndex += 1; @@ -4181,6 +5188,9 @@ function pasteAfterSelected() { const pasted = clone(state.clipboard); pasted.id = uniqueId(pasted.id); pasted.adminLabel = `${pasted.adminLabel || pasted.id} — вставка`; + if (pasted.anchor) { + pasted.anchor = uniqueLandingTarget(pasted.anchor, pasted); + } pasted.scopeIds = true; region.blocks.splice(state.selected.blockIndex + 1, 0, pasted); state.selected.blockIndex += 1; @@ -4210,8 +5220,10 @@ for (const input of [el.id, el.label, el.anchor, el.enabled]) { el.modeContent?.addEventListener("click", () => setAdminMode("content")); el.modeSeo?.addEventListener("click", () => setAdminMode("seo")); +el.modeKnowledge?.addEventListener("click", () => setAdminMode("knowledge")); el.json.addEventListener("blur", () => { + if (state.mode === "knowledge") return; if (state.filePickerActive || state.mediaUploadActive) { recoverEditorSurface(); return; @@ -4219,7 +5231,14 @@ el.json.addEventListener("blur", () => { syncBlockFromJson(); }); -el.addBlock.addEventListener("click", openTemplateModal); +el.addBlock.addEventListener("click", () => { + if (state.mode === "knowledge") { + addKnowledgeNode(null); + return; + } + + openTemplateModal(); +}); el.templateModalClose.addEventListener("click", closeTemplateModal); el.templateModalCancel.addEventListener("click", closeTemplateModal); el.templateModal.addEventListener("click", (event) => { @@ -4235,6 +5254,15 @@ el.mediaActionConfirm.addEventListener("click", () => closeMediaActionModal(el.m el.mediaActionModal.addEventListener("click", (event) => { if (event.target === el.mediaActionModal) closeMediaActionModal(null); }); +el.knowledgeButtonClose.addEventListener("click", () => closeKnowledgeButtonModal(null)); +el.knowledgeButtonCancel.addEventListener("click", () => closeKnowledgeButtonModal(null)); +el.knowledgeButtonConfirm.addEventListener("click", confirmKnowledgeButtonModal); +el.knowledgeButtonLink.addEventListener("blur", () => { + el.knowledgeButtonLink.value = normalizeKnowledgeButtonLink(el.knowledgeButtonLink.value); +}); +el.knowledgeButtonModal.addEventListener("click", (event) => { + if (event.target === el.knowledgeButtonModal) closeKnowledgeButtonModal(null); +}); el.mediaModalClose.addEventListener("click", closeMediaLibrary); el.mediaModalCancel.addEventListener("click", closeMediaLibrary); el.mediaModal.addEventListener("click", (event) => { @@ -4284,6 +5312,10 @@ document.addEventListener("contextmenu", (event) => { if (el.mediaModal.contains(event.target)) return; closeMediaContextMenu(); }); +document.addEventListener("selectionchange", () => { + if (state.mode !== "knowledge") return; + saveKnowledgeEditorSelection(); +}); document.addEventListener("keydown", (event) => { if (!el.deleteModal.classList.contains("hidden")) { if (event.key === "Enter") { @@ -4309,6 +5341,22 @@ document.addEventListener("keydown", (event) => { return; } + if (!el.knowledgeButtonModal.classList.contains("hidden")) { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + confirmKnowledgeButtonModal(); + } + if (event.key === "Enter" && event.target !== el.knowledgeButtonTarget) { + event.preventDefault(); + confirmKnowledgeButtonModal(); + } + if (event.key === "Escape") { + event.preventDefault(); + closeKnowledgeButtonModal(null); + } + return; + } + if (!el.mediaModal.classList.contains("hidden")) { if (!el.mediaContextMenu.classList.contains("hidden") && event.key === "Escape") { event.preventDefault(); @@ -4361,9 +5409,18 @@ document.addEventListener("keydown", (event) => { closeTemplateModal(); } }); -el.reload.addEventListener("click", () => loadPage().catch((error) => setStatus(error.message))); -el.save.addEventListener("click", () => savePage().catch((error) => setStatus(error.message))); -el.render.addEventListener("click", () => renderHome().catch((error) => setStatus(error.message))); +el.reload.addEventListener("click", () => { + const action = state.mode === "knowledge" ? loadKnowledge : loadPage; + action().catch((error) => setStatus(error.message)); +}); +el.save.addEventListener("click", () => { + const action = state.mode === "knowledge" ? saveKnowledge : savePage; + action().catch((error) => setStatus(error.message)); +}); +el.render.addEventListener("click", () => { + const action = state.mode === "knowledge" ? renderKnowledge : renderHome; + action().catch((error) => setStatus(error.message)); +}); el.moveUp.addEventListener("click", () => moveSelected(-1)); el.moveDown.addEventListener("click", () => moveSelected(1)); el.duplicate.addEventListener("click", duplicateSelected); diff --git a/admin/index.html b/admin/index.html index fc31835..dfefc99 100644 --- a/admin/index.html +++ b/admin/index.html @@ -27,6 +27,9 @@ +
@@ -113,8 +116,9 @@
@@ -174,6 +178,8 @@ + +

       
     
@@ -300,6 +306,39 @@
         
       
     
+    
     
   
 
diff --git a/assets/custom/css/knowledge.css b/assets/custom/css/knowledge.css
new file mode 100644
index 0000000..e1903d2
--- /dev/null
+++ b/assets/custom/css/knowledge.css
@@ -0,0 +1,120 @@
+.site-title-logo:where(.astro-idrpryed) img {
+  display: block;
+  width: auto;
+  height: 100%;
+  object-fit: contain;
+}
+
+.knowledge-search-popover {
+  position: absolute;
+  top: calc(100% + 0.5rem);
+  left: 50%;
+  z-index: calc(var(--sl-z-index-navbar) + 1);
+  width: min(28rem, calc(100vw - 2rem));
+  max-height: calc(100vh - 7rem);
+  overflow: auto;
+  transform: translateX(-50%);
+  border: 1px solid var(--sl-color-gray-5);
+  border-radius: 0.5rem;
+  background: var(--sl-color-gray-6);
+  box-shadow: var(--sl-shadow-lg);
+  padding: 0.75rem;
+}
+
+.knowledge-search-popover[hidden] {
+  display: none;
+}
+
+.knowledge-download-placeholder {
+  visibility: hidden;
+  pointer-events: none;
+}
+
+.knowledge-download-placeholder:where(.astro-sle7k2hz) {
+  min-width: 8.25rem;
+}
+
+.knowledge-search-input {
+  display: block;
+  width: 100%;
+  border: 1px solid var(--sl-color-gray-5);
+  border-radius: 0.5rem;
+  background: var(--sl-color-black);
+  color: var(--sl-color-white);
+  font: inherit;
+  padding: 0.75rem 0.875rem;
+  outline-offset: 2px;
+}
+
+.knowledge-search-results {
+  display: grid;
+  gap: 0.25rem;
+  margin-top: 0.5rem;
+}
+
+.knowledge-search-result {
+  display: grid;
+  gap: 0.2rem;
+  border-radius: 0.35rem;
+  color: var(--sl-color-gray-2);
+  padding: 0.65rem 0.7rem;
+  text-decoration: none;
+}
+
+.knowledge-search-result:hover {
+  background: var(--sl-color-gray-5);
+  color: var(--sl-color-white);
+}
+
+.knowledge-search-result strong {
+  color: var(--sl-color-white);
+  font-size: var(--sl-text-sm);
+  font-weight: 600;
+}
+
+.knowledge-search-result span {
+  color: var(--sl-color-gray-3);
+  font-size: var(--sl-text-xs);
+  line-height: 1.35;
+}
+
+summary:where(.astro-j63poqv6) .knowledge-summary-link {
+  display: inline;
+  margin: 0;
+  padding: 0;
+  color: inherit;
+  text-decoration: none;
+}
+
+summary:where(.astro-j63poqv6)[aria-current="page"] .knowledge-summary-link,
+summary:where(.astro-j63poqv6)[aria-current="page"] .large,
+summary:where(.astro-j63poqv6)[aria-current="page"] .caret {
+  color: var(--sl-color-text-invert);
+}
+
+.sl-markdown-content .knowledge-button:not(:where(.not-content *)) {
+  display: inline-flex;
+  align-items: center;
+  border: 1px solid var(--sl-color-text-accent);
+  border-radius: 999rem;
+  background: var(--sl-color-text-accent);
+  color: var(--sl-color-black);
+  font-size: var(--sl-text-base);
+  font-weight: 600;
+  line-height: 1.1875;
+  padding: 0.9375rem 1.25rem;
+  text-decoration: none;
+}
+
+.sl-markdown-content .knowledge-button:hover:not(:where(.not-content *)) {
+  color: var(--sl-color-black);
+  filter: brightness(1.06);
+}
+
+.knowledge-card-list {
+  padding-inline-start: 1.25rem;
+}
+
+.knowledge-card-list li + li {
+  margin-top: 0.35rem;
+}
diff --git a/assets/vendor/ssscript-docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js b/assets/vendor/ssscript-docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js
new file mode 100644
index 0000000..b2fd297
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js
@@ -0,0 +1 @@
+import{S as r}from"./TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js";class c extends r{set current(e){super.current=e;const t=this.querySelector(".display-current");t&&(t.textContent=e.textContent)}constructor(){super();const e=this.querySelector("details");if(!e)return;const t=()=>{e.open=!1};e.querySelectorAll("a").forEach(s=>{s.addEventListener("click",t)}),window.addEventListener("click",s=>{e.contains(s.target)||t()}),window.addEventListener("keydown",s=>{if(s.key==="Escape"&&e.open){const o=e.contains(document.activeElement);if(t(),o){const n=e.querySelector("summary");n&&n.focus()}}})}}customElements.define("mobile-starlight-toc",c);
diff --git a/assets/vendor/ssscript-docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js b/assets/vendor/ssscript-docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js
new file mode 100644
index 0000000..53de038
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js
@@ -0,0 +1 @@
+const m="_top";class H extends HTMLElement{constructor(){super(),this._current=this.querySelector('a[aria-current="true"]'),this.minH=parseInt(this.dataset.minH||"2",10),this.maxH=parseInt(this.dataset.maxH||"3",10),this.tocHeadingSelector=`h1#${m},:where(${[...Array.from({length:1+this.maxH-this.minH}).map((e,n)=>`h${this.minH+n}`)].join()})[id]`,this.onIdle=e=>(window.requestIdleCallback||(n=>setTimeout(n,1)))(e),this.init=()=>{const e=[...this.querySelectorAll("a")],n=t=>t.matches(this.tocHeadingSelector),i=t=>{if(!t)return null;const h=t;for(;t;){if(t.matches(".sl-markdown-content, main > *"))return document.getElementById(m);if(n(t))return t;const s=t.querySelector(this.tocHeadingSelector);if(s)return s;for(t=t.previousElementSibling;t?.lastElementChild;)t=t.lastElementChild;const o=i(t);if(o)return o}return i(h.parentElement)},c=t=>{for(const{isIntersecting:h,target:s}of t){if(!h)continue;const o=i(s);if(!o)continue;const l=e.find(g=>g.hash==="#"+encodeURIComponent(o.id));if(l){this.current=l;break}}},a=document.querySelectorAll([`main :where(${this.tocHeadingSelector})`,`main :where(${this.tocHeadingSelector}, .sl-heading-wrapper) ~ *:not(:has(${this.tocHeadingSelector}))`,`main .sl-markdown-content > *:not(:has(${this.tocHeadingSelector}))`,`main > *:not(:has(${this.tocHeadingSelector}))`].join());let r;const u=()=>{r||(r=new IntersectionObserver(c,{rootMargin:this.getRootMargin()}),a.forEach(t=>r.observe(t)))};u();let d;window.addEventListener("resize",()=>{r&&(r.disconnect(),r=void 0),clearTimeout(d),d=setTimeout(()=>this.onIdle(u),200)})},this.onIdle(()=>this.init())}set current(e){e!==this._current&&(this._current&&this._current.removeAttribute("aria-current"),e.setAttribute("aria-current","true"),this._current=e)}getRootMargin(){const e=document.querySelector("header")?.getBoundingClientRect().height||0,n=this.querySelector("summary")?.getBoundingClientRect().height||0,i=e+n+32,c=i+53,a=document.documentElement.clientHeight;return`-${i}px 0% ${c-a}px`}}customElements.define("starlight-toc",H);export{H as S};
diff --git a/assets/vendor/ssscript-docs/_astro/index.Bh6uiXgK.css b/assets/vendor/ssscript-docs/_astro/index.Bh6uiXgK.css
new file mode 100644
index 0000000..1d2f9e8
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/index.Bh6uiXgK.css
@@ -0,0 +1 @@
+/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer base{*,:after,:before{border:0 solid}html,:host{font-family:var(--font-sans)}code,kbd,samp,pre{font-family:var(--font-mono)}}@layer starlight,theme,components;@layer utilities{:root{--sl-font:var(--font-sans);--sl-font-mono:var(--font-mono);--sl-color-white:var(--color-white);--sl-color-gray-1:var(--color-gray-200);--sl-color-gray-2:var(--color-gray-300);--sl-color-gray-3:var(--color-gray-400);--sl-color-gray-4:var(--color-gray-600);--sl-color-gray-5:var(--color-gray-700);--sl-color-gray-6:var(--color-gray-800);--sl-color-black:var(--color-gray-900);--sl-color-accent-low:var(--color-accent-950,var(--color-accent-900,#1e1b4b));--sl-color-accent:var(--color-accent-600,#4f46e5);--sl-color-accent-high:var(--color-accent-200,#c7d2fe)}:root[data-theme=light]{--sl-color-white:var(--color-gray-900);--sl-color-gray-1:var(--color-gray-800);--sl-color-gray-2:var(--color-gray-700);--sl-color-gray-3:var(--color-gray-500);--sl-color-gray-4:var(--color-gray-400);--sl-color-gray-5:var(--color-gray-300);--sl-color-gray-6:var(--color-gray-200);--sl-color-gray-7:var(--color-gray-100);--sl-color-black:var(--color-white);--sl-color-accent-low:var(--color-accent-200,#c7d2fe);--sl-color-accent:var(--color-accent-600,#4f46e5);--sl-color-accent-high:var(--color-accent-900,#312e81)}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.relative{position:relative}.static{position:static}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.col-span-full{grid-column:1/-1}.-m-1{margin:calc(var(--spacing) * -1)}.m-0{margin:calc(var(--spacing) * 0)}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-full{height:100%}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-shrink{flex-shrink:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-\(--sl-nav-gap\){gap:var(--sl-nav-gap)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-\[8px\]{border-radius:8px}.rounded-\[12px\]{border-radius:12px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-amber-400\/35{border-color:#fcbb0059}@supports (color:color-mix(in lab,red,red)){.border-amber-400\/35{border-color:color-mix(in oklab,var(--color-amber-400) 35%,transparent)}}.border-amber-400\/40{border-color:#fcbb0066}@supports (color:color-mix(in lab,red,red)){.border-amber-400\/40{border-color:color-mix(in oklab,var(--color-amber-400) 40%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500) 10%,transparent)}}.p-1{padding:calc(var(--spacing) * 1)}.p-3{padding:calc(var(--spacing) * 3)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-\[0\.5rem\]{padding-inline:.5rem}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-\[0\.3rem\]{padding-block:.3rem}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-\[0\.4rem\]{padding-left:.4rem}.text-center{text-align:center}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-\[10px\]{font-size:10px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.whitespace-nowrap{white-space:nowrap}.text-amber-100{color:var(--color-amber-100)}.text-amber-100\/90{color:#fef3c6e6}@supports (color:color-mix(in lab,red,red)){.text-amber-100\/90{color:color-mix(in oklab,var(--color-amber-100) 90%,transparent)}}.text-amber-100\/95{color:#fef3c6f2}@supports (color:color-mix(in lab,red,red)){.text-amber-100\/95{color:color-mix(in oklab,var(--color-amber-100) 95%,transparent)}}.text-amber-200{color:var(--color-amber-200)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[color\,filter\]{transition-property:color,filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}@media not all and (min-width:48rem){.max-md\:flex{display:flex}}@media(min-width:48rem){.md\:items-start{align-items:flex-start}.md\:justify-start{justify-content:flex-start}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:px-5{padding-inline:calc(var(--spacing) * 5)}.md\:py-\[0\.9375rem\]{padding-block:.9375rem}.md\:text-left{text-align:left}}@media print{.print\:hidden{display:none}}}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-Regular.JnvgzjHK.woff2)format("woff2");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-Italic.FeNa2EJX.woff2)format("woff2");font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-Medium.B2xOBd5A.woff2)format("woff2");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-MediumItalic.c03vUl6A.woff2)format("woff2");font-weight:500;font-style:italic;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-SemiBold.wfsLgfkq.woff2)format("woff2");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-SemiBoldItalic.DRnKVj3E.woff2)format("woff2");font-weight:600;font-style:italic;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-Bold.DWGdGw8L.woff2)format("woff2");font-weight:700;font-style:normal;font-display:swap}@font-face{font-family:Instrument Sans;src:url(/_astro/InstrumentSans-BoldItalic.BOjDmDe7.woff2)format("woff2");font-weight:700;font-style:italic;font-display:swap}:root,:host{--color-accent-200:var(--color-indigo-200);--color-accent-600:var(--color-indigo-600);--color-accent-900:var(--color-indigo-900);--color-accent-950:var(--color-indigo-950);--color-gray-100:var(--color-zinc-100);--color-gray-200:var(--color-zinc-200);--color-gray-300:var(--color-zinc-300);--color-gray-400:var(--color-zinc-400);--color-gray-500:var(--color-zinc-500);--color-gray-600:var(--color-zinc-600);--color-gray-700:var(--color-zinc-700);--color-gray-800:var(--color-zinc-800);--color-gray-900:var(--color-zinc-900);--font-sans:"Instrument Sans", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, monospace;--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--radius-lg:.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}:root{--sl-color-black:#0a0a0a;--sl-color-gray-6:#141414;--sl-color-gray-5:#161616;--sl-color-gray-4:#2d2d2d;--sl-color-gray-3:#525252;--sl-color-gray-2:#b4b4b4;--sl-color-gray-1:#ebebeb;--sl-color-white:#fff;--docs-bg-gradient:linear-gradient(to bottom, #141414 0%, #0a0a0a 100%);--sl-color-accent-low:#2d2d2d;--sl-color-accent:#c4c4c4;--sl-color-accent-high:#ebebeb;--sl-color-text:var(--sl-color-gray-2);--sl-color-text-accent:var(--sl-color-accent-high);--sl-color-text-invert:var(--sl-color-accent-low);--sl-color-bg:var(--sl-color-black);--sl-color-bg-nav:var(--sl-color-gray-6);--sl-color-bg-sidebar:var(--sl-color-gray-6);--sl-color-bg-inline-code:var(--sl-color-gray-5);--sl-color-bg-accent:var(--sl-color-accent-high);--sl-color-hairline-light:var(--sl-color-gray-5);--sl-color-hairline:var(--sl-color-gray-6);--sl-color-hairline-shade:var(--sl-color-black);--sl-color-backdrop-overlay:#0a0a0aa8;--sl-line-height-headings:1.25;--sl-line-height-body:1.45}:root[data-theme=light],[data-theme=light]{--sl-color-white:#0f0f0f;--sl-color-gray-1:#181818;--sl-color-gray-2:#2d2d2d;--sl-color-gray-3:#525252;--sl-color-gray-4:#737373;--sl-color-gray-5:#a3a3a3;--sl-color-gray-6:#d4d4d4;--sl-color-gray-7:#ebebeb;--sl-color-black:#fff;--docs-bg-gradient:linear-gradient(to bottom, #fafafa 0%, #ebebeb 100%);--sl-color-accent-high:#2d2d2d;--sl-color-accent:#525252;--sl-color-accent-low:#e5e5e5;--sl-color-text-accent:var(--sl-color-accent-high);--sl-color-text-invert:var(--sl-color-black);--sl-color-bg-nav:var(--sl-color-gray-7);--sl-color-bg-sidebar:var(--sl-color-bg);--sl-color-bg-inline-code:var(--sl-color-gray-6);--sl-color-bg-accent:var(--sl-color-accent);--sl-color-hairline-light:var(--sl-color-gray-6);--sl-color-hairline-shade:var(--sl-color-gray-6);--sl-color-backdrop-overlay:#0006}body{background:var(--docs-bg-gradient);background-attachment:fixed;min-height:100vh}@layer starlight.core{.page,[data-sl-sidebar],[data-sl-main],.sl-sidebar-pane,.main-frame{background:0 0!important}site-search dialog{box-sizing:border-box;width:90%!important;max-width:40rem!important;height:max-content!important;min-height:15rem!important;max-height:calc(100% - 8rem)!important;margin:4rem auto auto!important;position:fixed!important;inset:0!important}}.sl-markdown-content{line-height:var(--sl-line-height-body)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@layer starlight.base,starlight.reset,starlight.core,starlight.content,starlight.components,starlight.utils;@layer starlight.base{:root,::backdrop{--sl-color-white: hsl(0, 0%, 100%);--sl-color-gray-1: hsl(224, 20%, 94%);--sl-color-gray-2: hsl(224, 6%, 77%);--sl-color-gray-3: hsl(224, 6%, 56%);--sl-color-gray-4: hsl(224, 7%, 36%);--sl-color-gray-5: hsl(224, 10%, 23%);--sl-color-gray-6: hsl(224, 14%, 16%);--sl-color-black: hsl(224, 10%, 10%);--sl-hue-orange: 41;--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);--sl-hue-green: 101;--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);--sl-hue-blue: 234;--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);--sl-hue-purple: 281;--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);--sl-hue-red: 339;--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);--sl-color-accent-low: hsl(224, 54%, 20%);--sl-color-accent: hsl(224, 100%, 60%);--sl-color-accent-high: hsl(224, 100%, 85%);--sl-color-text: var(--sl-color-gray-2);--sl-color-text-accent: var(--sl-color-accent-high);--sl-color-text-invert: var(--sl-color-accent-low);--sl-color-bg: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-6);--sl-color-bg-sidebar: var(--sl-color-gray-6);--sl-color-bg-inline-code: var(--sl-color-gray-5);--sl-color-bg-accent: var(--sl-color-accent-high);--sl-color-hairline-light: var(--sl-color-gray-5);--sl-color-hairline: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-black);--sl-color-backdrop-overlay: hsla(223, 13%, 10%, .66);--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, .12), 0px 2px 1px hsla(0, 0%, 0%, .24);--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, .08), 0px 5px 2px hsla(0, 0%, 0%, .08), 0px 3px 2px hsla(0, 0%, 0%, .12), 0px 1px 1px hsla(0, 0%, 0%, .15);--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, .03), 0px 16px 6px hsla(0, 0%, 0%, .1), 0px 9px 5px hsla(223, 13%, 10%, .33), 0px 4px 4px hsla(0, 0%, 0%, .75), 0px 4px 2px hsla(0, 0%, 0%, .25);--sl-text-2xs: .75rem;--sl-text-xs: .8125rem;--sl-text-sm: .875rem;--sl-text-base: 1rem;--sl-text-lg: 1.125rem;--sl-text-xl: 1.25rem;--sl-text-2xl: 1.5rem;--sl-text-3xl: 1.8125rem;--sl-text-4xl: 2.1875rem;--sl-text-5xl: 2.625rem;--sl-text-6xl: 4rem;--sl-text-body: var(--sl-text-base);--sl-text-body-sm: var(--sl-text-xs);--sl-text-code: var(--sl-text-sm);--sl-text-code-sm: var(--sl-text-xs);--sl-text-h1: var(--sl-text-4xl);--sl-text-h2: var(--sl-text-3xl);--sl-text-h3: var(--sl-text-2xl);--sl-text-h4: var(--sl-text-xl);--sl-text-h5: var(--sl-text-lg);--sl-line-height: 1.75;--sl-line-height-headings: 1.2;--sl-font-system: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);--__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)), var(--sl-font-system-mono);--sl-nav-height: 3.5rem;--sl-nav-pad-x: 1rem;--sl-nav-pad-y: .75rem;--sl-mobile-toc-height: 3rem;--sl-sidebar-width: 18.75rem;--sl-sidebar-pad-x: 1rem;--sl-content-width: 45rem;--sl-content-pad-x: 1rem;--sl-content-gap-y: 1rem;--sl-main-pad: 0 0 3vh 0;--sl-menu-button-size: 2rem;--sl-nav-gap: var(--sl-content-pad-x);--sl-outline-offset-inside: -.1875rem;--sl-z-index-toc: 4;--sl-z-index-menu: 5;--sl-z-index-navbar: 10;--sl-z-index-skiplink: 20}:root[data-theme=light],[data-theme=light] ::backdrop{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);--sl-color-accent-high: hsl(234, 80%, 30%);--sl-color-accent: hsl(234, 90%, 60%);--sl-color-accent-low: hsl(234, 88%, 90%);--sl-color-text-accent: var(--sl-color-accent);--sl-color-text-invert: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-7);--sl-color-bg-sidebar: var(--sl-color-bg);--sl-color-bg-inline-code: var(--sl-color-gray-6);--sl-color-bg-accent: var(--sl-color-accent);--sl-color-hairline-light: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-gray-6);--sl-color-backdrop-overlay: hsla(225, 9%, 36%, .66);--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, .06), 0px 2px 1px hsla(0, 0%, 0%, .06);--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, .03), 0px 5px 2px hsla(0, 0%, 0%, .03), 0px 3px 2px hsla(0, 0%, 0%, .06), 0px 1px 1px hsla(0, 0%, 0%, .06);--sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, .01), 0px 16px 6px hsla(0, 0%, 0%, .03), 0px 9px 5px hsla(223, 13%, 10%, .08), 0px 4px 4px hsla(0, 0%, 0%, .16), 0px 4px 2px hsla(0, 0%, 0%, .04)}@media(min-width:50em){:root{--sl-nav-height: 4rem;--sl-nav-pad-x: 1.5rem;--sl-text-h1: var(--sl-text-5xl);--sl-text-h2: var(--sl-text-4xl);--sl-text-h3: var(--sl-text-3xl);--sl-text-h4: var(--sl-text-2xl)}}@media(min-width:72rem){:root{--sl-content-pad-x: 1.5rem;--sl-mobile-toc-height: 0rem}}}@layer starlight.reset{*,*:before,*:after{box-sizing:border-box}*{margin:0}html{color-scheme:dark;accent-color:var(--sl-color-accent);-webkit-text-size-adjust:100%;text-size-adjust:100%}html[data-theme=light]{color-scheme:light}body{font-family:var(--__sl-font);line-height:var(--sl-line-height);-webkit-font-smoothing:antialiased;color:var(--sl-color-text);background-color:var(--sl-color-bg)}input,button,textarea,select{font:inherit}p,h1,h2,h3,h4,h5,h6,code{overflow-wrap:break-word}code{font-family:var(--__sl-font-mono)}}@layer starlight.components{.starlight-aside{padding:1rem;border-inline-start:.25rem solid var(--sl-color-asides-border);color:var(--sl-color-white)}.starlight-aside--note{--sl-color-asides-text-accent: var(--sl-color-blue-high);--sl-color-asides-border: var(--sl-color-blue);background-color:var(--sl-color-blue-low)}.starlight-aside--tip{--sl-color-asides-text-accent: var(--sl-color-purple-high);--sl-color-asides-border: var(--sl-color-purple);background-color:var(--sl-color-purple-low)}.starlight-aside--caution{--sl-color-asides-text-accent: var(--sl-color-orange-high);--sl-color-asides-border: var(--sl-color-orange);background-color:var(--sl-color-orange-low)}.starlight-aside--danger{--sl-color-asides-text-accent: var(--sl-color-red-high);--sl-color-asides-border: var(--sl-color-red);background-color:var(--sl-color-red-low)}.starlight-aside__title{display:flex;gap:.5rem;align-items:center;font-size:var(--sl-text-h5);font-weight:600;line-height:var(--sl-line-height-headings);color:var(--sl-color-asides-text-accent)}.starlight-aside__icon{font-size:1.333em;width:1em;height:1em}.starlight-aside__title+.starlight-aside__content{margin-top:.5rem}.starlight-aside__content a{color:var(--sl-color-asides-text-accent)}}@layer starlight.utils{.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.sl-hidden{display:none}.sl-flex{display:flex}.sl-block{display:block}@media(min-width:50rem){.md\:sl-hidden{display:none}.md\:sl-flex{display:flex}.md\:sl-block{display:block}}@media(min-width:72rem){.lg\:sl-hidden{display:none}.lg\:sl-flex{display:flex}.lg\:sl-block{display:block}}[data-theme=light] .light\:sl-hidden,[data-theme=dark] .dark\:sl-hidden{display:none}[dir=rtl] .rtl\:flip:not(:where([dir=rtl] [dir=ltr] *)){transform:scaleX(-1)}}@layer starlight.content{.sl-markdown-content .sl-heading-wrapper{--sl-anchor-icon-size: .8275em;--sl-anchor-icon-gap: .25em;--sl-anchor-icon-space: calc(var(--sl-anchor-icon-size) + var(--sl-anchor-icon-gap));line-height:var(--sl-line-height-headings)}.sl-markdown-content :not(h1,h2,h3,h4,h5,h6,.sl-heading-wrapper)+:is(.sl-heading-wrapper){margin-top:1.5em}.sl-markdown-content .sl-heading-wrapper.level-h1{font-size:var(--sl-text-h1)}.sl-markdown-content .sl-heading-wrapper.level-h2{font-size:var(--sl-text-h2)}.sl-markdown-content .sl-heading-wrapper.level-h3{font-size:var(--sl-text-h3)}.sl-markdown-content .sl-heading-wrapper.level-h4{font-size:var(--sl-text-h4)}.sl-markdown-content .sl-heading-wrapper.level-h5{font-size:var(--sl-text-h5)}.sl-markdown-content .sl-heading-wrapper.level-h6{font-size:var(--sl-text-h6)}.sl-markdown-content .sl-heading-wrapper>:first-child{display:inline;padding-inline-end:var(--sl-anchor-icon-space)}.sl-markdown-content .sl-anchor-link{position:relative;margin-inline-start:calc(-1 * var(--sl-anchor-icon-size));-webkit-user-select:none;user-select:none;display:inline-flex}.sl-markdown-content .sl-anchor-link:after{content:"";position:absolute;inset:-.25rem -.5rem}.sl-markdown-content .sl-anchor-icon>svg{display:inline;width:var(--sl-anchor-icon-size);vertical-align:top;transform:translateY(calc((var(--sl-line-height-headings) * 1em - var(--sl-anchor-icon-size)) / 2))}@media(hover:hover){.sl-markdown-content .sl-anchor-link{opacity:0}.sl-markdown-content .sl-anchor-link:focus,.sl-markdown-content .sl-heading-wrapper:hover .sl-anchor-link{opacity:1}}}@layer starlight.core{.sl-banner:where(.astro-2xxawopd){--__sl-banner-text: var(--sl-color-banner-text, var(--sl-color-text-invert));padding:var(--sl-nav-pad-y) var(--sl-nav-pad-x);background-color:var(--sl-color-banner-bg, var(--sl-color-bg-accent));color:var(--__sl-banner-text);line-height:var(--sl-line-height-headings);text-align:center;text-wrap:balance;box-shadow:var(--sl-shadow-sm)}.sl-banner:where(.astro-2xxawopd) a{color:var(--__sl-banner-text)}}@layer starlight.core{.content-panel:where(.astro-hxjpthjh){padding:1.5rem var(--sl-content-pad-x)}.content-panel:where(.astro-hxjpthjh)+.content-panel:where(.astro-hxjpthjh){border-top:1px solid var(--sl-color-hairline)}.sl-container:where(.astro-hxjpthjh){max-width:var(--sl-content-width)}.sl-container:where(.astro-hxjpthjh)>*+*{margin-top:1.5rem}@media(min-width:72rem){.sl-container:where(.astro-hxjpthjh){margin-inline:var(--sl-content-margin-inline, auto)}}}@layer starlight.components{svg:where(.astro-fr7meb3z){color:var(--sl-icon-color);font-size:var(--sl-icon-size, 1em);width:1em;height:1em}}@layer starlight.core{p:where(.astro-hykfrv54){border:1px solid var(--sl-color-orange);padding:.75em 1em;background-color:var(--sl-color-orange-low);color:var(--sl-color-orange-high);width:max-content;max-width:100%;align-items:center;gap:.75em;font-size:var(--sl-text-body-sm);line-height:var(--sl-line-height-headings)}}@layer starlight.core{a:where(.astro-uawllorr){gap:.5rem;align-items:center;text-decoration:none;color:var(--sl-color-gray-3)}a:where(.astro-uawllorr):hover{color:var(--sl-color-white)}}@layer starlight.core{.pagination-links:where(.astro-7ghqze2o){display:grid;grid-template-columns:repeat(auto-fit,minmax(min(18rem,100%),1fr));gap:1rem}a:where(.astro-7ghqze2o){display:flex;align-items:center;justify-content:flex-start;gap:.5rem;width:100%;flex-basis:calc(50% - .5rem);flex-grow:1;border:1px solid var(--sl-color-gray-5);border-radius:.5rem;padding:1rem;text-decoration:none;color:var(--sl-color-gray-2);box-shadow:var(--sl-shadow-md);overflow-wrap:anywhere}:where(.astro-7ghqze2o)[rel=next]{justify-content:end;text-align:end;flex-direction:row-reverse}a:where(.astro-7ghqze2o):hover{border-color:var(--sl-color-gray-2)}.link-title:where(.astro-7ghqze2o){color:var(--sl-color-white);font-size:var(--sl-text-2xl);line-height:var(--sl-line-height-headings)}svg:where(.astro-7ghqze2o){flex-shrink:0}}@layer starlight.components{:root{--sl-badge-default-border: var(--sl-color-accent);--sl-badge-default-bg: var(--sl-color-accent-low);--sl-badge-default-text: #fff;--sl-badge-note-border: var(--sl-color-blue);--sl-badge-note-bg: var(--sl-color-blue-low);--sl-badge-note-text: #fff;--sl-badge-danger-border: var(--sl-color-red);--sl-badge-danger-bg: var(--sl-color-red-low);--sl-badge-danger-text: #fff;--sl-badge-success-border: var(--sl-color-green);--sl-badge-success-bg: var(--sl-color-green-low);--sl-badge-success-text: #fff;--sl-badge-caution-border: var(--sl-color-orange);--sl-badge-caution-bg: var(--sl-color-orange-low);--sl-badge-caution-text: #fff;--sl-badge-tip-border: var(--sl-color-purple);--sl-badge-tip-bg: var(--sl-color-purple-low);--sl-badge-tip-text: #fff}[data-theme=light]:root{--sl-badge-default-bg: var(--sl-color-accent-high);--sl-badge-note-bg: var(--sl-color-blue-high);--sl-badge-danger-bg: var(--sl-color-red-high);--sl-badge-success-bg: var(--sl-color-green-high);--sl-badge-caution-bg: var(--sl-color-orange-high);--sl-badge-tip-bg: var(--sl-color-purple-high)}.sl-badge:where(.astro-px754twf){display:inline-block;border:1px solid var(--sl-color-border-badge);border-radius:.25rem;font-family:var(--sl-font-system-mono);line-height:normal;color:var(--sl-color-text-badge);background-color:var(--sl-color-bg-badge);overflow-wrap:anywhere}.sidebar-content .sl-badge:where(.astro-px754twf){line-height:1;font-size:var(--sl-text-xs);padding:.125rem .375rem}.sidebar-content a[aria-current=page]>.sl-badge:where(.astro-px754twf){--sl-color-bg-badge: transparent;--sl-color-border-badge: currentColor;color:inherit}.default:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-default-bg);--sl-color-border-badge: var(--sl-badge-default-border);--sl-color-text-badge: var(--sl-badge-default-text)}.note:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-note-bg);--sl-color-border-badge: var(--sl-badge-note-border);--sl-color-text-badge: var(--sl-badge-note-text)}.danger:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-danger-bg);--sl-color-border-badge: var(--sl-badge-danger-border);--sl-color-text-badge: var(--sl-badge-danger-text)}.success:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-success-bg);--sl-color-border-badge: var(--sl-badge-success-border);--sl-color-text-badge: var(--sl-badge-success-text)}.tip:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-tip-bg);--sl-color-border-badge: var(--sl-badge-tip-border);--sl-color-text-badge: var(--sl-badge-tip-text)}.caution:where(.astro-px754twf){--sl-color-bg-badge: var(--sl-badge-caution-bg);--sl-color-border-badge: var(--sl-badge-caution-border);--sl-color-text-badge: var(--sl-badge-caution-text)}.small:where(.astro-px754twf){font-size:var(--sl-text-xs);padding:.125rem .25rem}.medium:where(.astro-px754twf){font-size:var(--sl-text-sm);padding:.175rem .35rem}.large:where(.astro-px754twf){font-size:var(--sl-text-base);padding:.225rem .45rem}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6) .sl-badge:where(.astro-px754twf){vertical-align:middle}}@layer starlight.components{.sl-steps{--bullet-size: calc(var(--sl-line-height) * 1rem);--bullet-margin: .375rem;list-style:none;counter-reset:steps-counter var(--sl-steps-start, 0);padding-inline-start:0}.sl-steps>li{counter-increment:steps-counter;position:relative;padding-inline-start:calc(var(--bullet-size) + 1rem);padding-bottom:1px;min-height:calc(var(--bullet-size) + var(--bullet-margin))}.sl-steps>li+li{margin-top:0}.sl-steps>li:before{content:counter(steps-counter);position:absolute;top:0;inset-inline-start:0;width:var(--bullet-size);height:var(--bullet-size);line-height:var(--bullet-size);font-size:var(--sl-text-xs);font-weight:600;text-align:center;color:var(--sl-color-white);background-color:var(--sl-color-gray-6);border-radius:99rem;box-shadow:inset 0 0 0 1px var(--sl-color-gray-5)}.sl-steps>li:after{--guide-width: 1px;content:"";position:absolute;top:calc(var(--bullet-size) + var(--bullet-margin));bottom:var(--bullet-margin);inset-inline-start:calc((var(--bullet-size) - var(--guide-width)) / 2);width:var(--guide-width);background-color:var(--sl-color-hairline-light)}}@layer starlight.content{.sl-steps>li>:first-child{--lh: calc(1em * var(--sl-line-height));--shift-y: calc(.5 * (var(--bullet-size) - var(--lh)));transform:translateY(var(--shift-y));margin-bottom:var(--shift-y)}.sl-steps>li>:first-child:where(h1,h2,h3,h4,h5,h6){--lh: calc(1em * var(--sl-line-height-headings))}@supports (--prop: 1lh){.sl-steps>li>:first-child{--lh: 1lh}}}@layer starlight.core{footer:where(.astro-vcuyfr7s){flex-direction:column;gap:1.5rem}.meta:where(.astro-vcuyfr7s){gap:.75rem 3rem;justify-content:space-between;flex-wrap:wrap;margin-top:3rem;font-size:var(--sl-text-sm);color:var(--sl-color-gray-3)}.meta:where(.astro-vcuyfr7s)>p:only-child{margin-inline-start:auto}.kudos:where(.astro-vcuyfr7s){align-items:center;gap:.5em;margin:1.5rem auto;font-size:var(--sl-text-xs);text-decoration:none;color:var(--sl-color-gray-3)}.kudos:where(.astro-vcuyfr7s):hover{color:var(--sl-color-white)}}@layer starlight.components{.kudos:where(.astro-vcuyfr7s) svg{color:var(--sl-color-orange)}}@layer starlight.core{label:where(.astro-47nlhtql){--sl-label-icon-size: .875rem;--sl-caret-size: 1.25rem;--sl-inline-padding: .5rem;position:relative;display:flex;align-items:center;gap:.25rem;color:var(--sl-color-gray-1)}label:where(.astro-47nlhtql):hover{color:var(--sl-color-gray-2)}.icon:where(.astro-47nlhtql){position:absolute;top:50%;transform:translateY(-50%);pointer-events:none}select:where(.astro-47nlhtql){border:0;padding-block:.625rem;padding-inline:calc(var(--sl-label-icon-size) + var(--sl-inline-padding) + .25rem) calc(var(--sl-caret-size) + var(--sl-inline-padding) + .25rem);margin-inline:calc(var(--sl-inline-padding) * -1);width:calc(var(--sl-select-width) + var(--sl-inline-padding) * 2);background-color:transparent;text-overflow:ellipsis;color:inherit;cursor:pointer;appearance:none}option:where(.astro-47nlhtql){background-color:var(--sl-color-bg-nav);color:var(--sl-color-gray-1)}@media(min-width:50rem){select:where(.astro-47nlhtql){font-size:var(--sl-text-sm)}}}@layer starlight.components{.label-icon:where(.astro-47nlhtql){font-size:var(--sl-label-icon-size);inset-inline-start:0}.caret:where(.astro-47nlhtql){font-size:var(--sl-caret-size);inset-inline-end:0}}@layer starlight.core{.pagefind-ui__result.svelte-j9e30.svelte-j9e30{list-style-type:none;display:flex;align-items:flex-start;gap:min(calc(40px * var(--pagefind-ui-scale)),3%);padding:calc(30px * var(--pagefind-ui-scale)) 0 calc(40px * var(--pagefind-ui-scale));border-top:solid var(--pagefind-ui-border-width) var(--pagefind-ui-border)}.pagefind-ui__result.svelte-j9e30.svelte-j9e30:last-of-type{border-bottom:solid var(--pagefind-ui-border-width) var(--pagefind-ui-border)}.pagefind-ui__result-thumb.svelte-j9e30.svelte-j9e30{width:min(30%,calc((30% - (100px * var(--pagefind-ui-scale))) * 100000));max-width:calc(120px * var(--pagefind-ui-scale));margin-top:calc(10px * var(--pagefind-ui-scale));aspect-ratio:var(--pagefind-ui-image-box-ratio);position:relative}.pagefind-ui__result-image.svelte-j9e30.svelte-j9e30{display:block;position:absolute;left:50%;transform:translate(-50%);font-size:0;width:auto;height:auto;max-width:100%;max-height:100%;border-radius:var(--pagefind-ui-image-border-radius)}.pagefind-ui__result-inner.svelte-j9e30.svelte-j9e30{flex:1;display:flex;flex-direction:column;align-items:flex-start;margin-top:calc(10px * var(--pagefind-ui-scale))}.pagefind-ui__result-title.svelte-j9e30.svelte-j9e30{display:inline-block;font-weight:700;font-size:calc(21px * var(--pagefind-ui-scale));margin-top:0;margin-bottom:0}.pagefind-ui__result-title.svelte-j9e30 .pagefind-ui__result-link.svelte-j9e30{color:var(--pagefind-ui-text);text-decoration:none}.pagefind-ui__result-title.svelte-j9e30 .pagefind-ui__result-link.svelte-j9e30:hover{text-decoration:underline}.pagefind-ui__result-excerpt.svelte-j9e30.svelte-j9e30{display:inline-block;font-weight:400;font-size:calc(16px * var(--pagefind-ui-scale));margin-top:calc(4px * var(--pagefind-ui-scale));margin-bottom:0;min-width:calc(250px * var(--pagefind-ui-scale))}.pagefind-ui__loading.svelte-j9e30.svelte-j9e30{color:var(--pagefind-ui-text);background-color:var(--pagefind-ui-text);border-radius:var(--pagefind-ui-border-radius);opacity:.1;pointer-events:none}.pagefind-ui__result-tags.svelte-j9e30.svelte-j9e30{list-style-type:none;padding:0;display:flex;gap:calc(20px * var(--pagefind-ui-scale));flex-wrap:wrap;margin-top:calc(20px * var(--pagefind-ui-scale))}.pagefind-ui__result-tag.svelte-j9e30.svelte-j9e30{padding:calc(4px * var(--pagefind-ui-scale)) calc(8px * var(--pagefind-ui-scale));font-size:calc(14px * var(--pagefind-ui-scale));border-radius:var(--pagefind-ui-border-radius);background-color:var(--pagefind-ui-tag)}.pagefind-ui__result.svelte-4xnkmf.svelte-4xnkmf{list-style-type:none;display:flex;align-items:flex-start;gap:min(calc(40px * var(--pagefind-ui-scale)),3%);padding:calc(30px * var(--pagefind-ui-scale)) 0 calc(40px * var(--pagefind-ui-scale));border-top:solid var(--pagefind-ui-border-width) var(--pagefind-ui-border)}.pagefind-ui__result.svelte-4xnkmf.svelte-4xnkmf:last-of-type{border-bottom:solid var(--pagefind-ui-border-width) var(--pagefind-ui-border)}.pagefind-ui__result-nested.svelte-4xnkmf.svelte-4xnkmf{display:flex;flex-direction:column;padding-left:calc(20px * var(--pagefind-ui-scale))}.pagefind-ui__result-nested.svelte-4xnkmf.svelte-4xnkmf:first-of-type{padding-top:calc(10px * var(--pagefind-ui-scale))}.pagefind-ui__result-nested.svelte-4xnkmf .pagefind-ui__result-link.svelte-4xnkmf{font-size:.9em;position:relative}.pagefind-ui__result-nested.svelte-4xnkmf .pagefind-ui__result-link.svelte-4xnkmf:before{content:"⤷ ";position:absolute;top:0;right:calc(100% + .1em)}.pagefind-ui__result-thumb.svelte-4xnkmf.svelte-4xnkmf{width:min(30%,calc((30% - (100px * var(--pagefind-ui-scale))) * 100000));max-width:calc(120px * var(--pagefind-ui-scale));margin-top:calc(10px * var(--pagefind-ui-scale));aspect-ratio:var(--pagefind-ui-image-box-ratio);position:relative}.pagefind-ui__result-image.svelte-4xnkmf.svelte-4xnkmf{display:block;position:absolute;left:50%;transform:translate(-50%);font-size:0;width:auto;height:auto;max-width:100%;max-height:100%;border-radius:var(--pagefind-ui-image-border-radius)}.pagefind-ui__result-inner.svelte-4xnkmf.svelte-4xnkmf{flex:1;display:flex;flex-direction:column;align-items:flex-start;margin-top:calc(10px * var(--pagefind-ui-scale))}.pagefind-ui__result-title.svelte-4xnkmf.svelte-4xnkmf{display:inline-block;font-weight:700;font-size:calc(21px * var(--pagefind-ui-scale));margin-top:0;margin-bottom:0}.pagefind-ui__result-title.svelte-4xnkmf .pagefind-ui__result-link.svelte-4xnkmf{color:var(--pagefind-ui-text);text-decoration:none}.pagefind-ui__result-title.svelte-4xnkmf .pagefind-ui__result-link.svelte-4xnkmf:hover{text-decoration:underline}.pagefind-ui__result-excerpt.svelte-4xnkmf.svelte-4xnkmf{display:inline-block;font-weight:400;font-size:calc(16px * var(--pagefind-ui-scale));margin-top:calc(4px * var(--pagefind-ui-scale));margin-bottom:0;min-width:calc(250px * var(--pagefind-ui-scale))}.pagefind-ui__loading.svelte-4xnkmf.svelte-4xnkmf{color:var(--pagefind-ui-text);background-color:var(--pagefind-ui-text);border-radius:var(--pagefind-ui-border-radius);opacity:.1;pointer-events:none}.pagefind-ui__result-tags.svelte-4xnkmf.svelte-4xnkmf{list-style-type:none;padding:0;display:flex;gap:calc(20px * var(--pagefind-ui-scale));flex-wrap:wrap;margin-top:calc(20px * var(--pagefind-ui-scale))}.pagefind-ui__result-tag.svelte-4xnkmf.svelte-4xnkmf{padding:calc(4px * var(--pagefind-ui-scale)) calc(8px * var(--pagefind-ui-scale));font-size:calc(14px * var(--pagefind-ui-scale));border-radius:var(--pagefind-ui-border-radius);background-color:var(--pagefind-ui-tag)}legend.svelte-1v2r7ls.svelte-1v2r7ls{position:absolute;clip:rect(0 0 0 0)}.pagefind-ui__filter-panel.svelte-1v2r7ls.svelte-1v2r7ls{min-width:min(calc(260px * var(--pagefind-ui-scale)),100%);flex:1;display:flex;flex-direction:column;margin-top:calc(20px * var(--pagefind-ui-scale))}.pagefind-ui__filter-group.svelte-1v2r7ls.svelte-1v2r7ls{border:0;padding:0}.pagefind-ui__filter-block.svelte-1v2r7ls.svelte-1v2r7ls{padding:0;display:block;border-bottom:solid calc(2px * var(--pagefind-ui-scale)) var(--pagefind-ui-border);padding:calc(20px * var(--pagefind-ui-scale)) 0}.pagefind-ui__filter-name.svelte-1v2r7ls.svelte-1v2r7ls{font-size:calc(16px * var(--pagefind-ui-scale));position:relative;display:flex;align-items:center;list-style:none;font-weight:700;cursor:pointer;height:calc(24px * var(--pagefind-ui-scale))}.pagefind-ui__filter-name.svelte-1v2r7ls.svelte-1v2r7ls::-webkit-details-marker{display:none}.pagefind-ui__filter-name.svelte-1v2r7ls.svelte-1v2r7ls:after{position:absolute;content:"";right:calc(6px * var(--pagefind-ui-scale));top:50%;width:calc(8px * var(--pagefind-ui-scale));height:calc(8px * var(--pagefind-ui-scale));border:solid calc(2px * var(--pagefind-ui-scale)) currentColor;border-right:0;border-top:0;transform:translateY(-70%) rotate(-45deg)}.pagefind-ui__filter-block[open].svelte-1v2r7ls .pagefind-ui__filter-name.svelte-1v2r7ls:after{transform:translateY(-70%) rotate(-225deg)}.pagefind-ui__filter-group.svelte-1v2r7ls.svelte-1v2r7ls{display:flex;flex-direction:column;gap:calc(20px * var(--pagefind-ui-scale));padding-top:calc(30px * var(--pagefind-ui-scale))}.pagefind-ui__filter-value.svelte-1v2r7ls.svelte-1v2r7ls{position:relative;display:flex;align-items:center;gap:calc(8px * var(--pagefind-ui-scale))}.pagefind-ui__filter-value.svelte-1v2r7ls.svelte-1v2r7ls:before{position:absolute;content:"";top:50%;left:calc(8px * var(--pagefind-ui-scale));width:0px;height:0px;border:solid 1px #fff;opacity:0;transform:translate(calc(4.5px * var(--pagefind-ui-scale) * -1),calc(.8px * var(--pagefind-ui-scale))) skew(-5deg) rotate(-45deg);transform-origin:top left;border-top:0;border-right:0;pointer-events:none}.pagefind-ui__filter-value.pagefind-ui__filter-value--checked.svelte-1v2r7ls.svelte-1v2r7ls:before{opacity:1;width:calc(9px * var(--pagefind-ui-scale));height:calc(4px * var(--pagefind-ui-scale));transition:width .1s ease-out .1s,height .1s ease-in}.pagefind-ui__filter-checkbox.svelte-1v2r7ls.svelte-1v2r7ls{margin:0;width:calc(16px * var(--pagefind-ui-scale));height:calc(16px * var(--pagefind-ui-scale));border:solid 1px var(--pagefind-ui-border);appearance:none;-webkit-appearance:none;border-radius:calc(var(--pagefind-ui-border-radius) / 2);background-color:var(--pagefind-ui-background);cursor:pointer}.pagefind-ui__filter-checkbox.svelte-1v2r7ls.svelte-1v2r7ls:checked{background-color:var(--pagefind-ui-primary);border:solid 1px var(--pagefind-ui-primary)}.pagefind-ui__filter-label.svelte-1v2r7ls.svelte-1v2r7ls{cursor:pointer;font-size:calc(16px * var(--pagefind-ui-scale));font-weight:400}.pagefind-ui--reset *:where(:not(html,iframe,canvas,img,svg,video):not(svg *,symbol *)){all:unset;display:revert;outline:revert}.pagefind-ui--reset *,.pagefind-ui--reset *:before,.pagefind-ui--reset *:after{box-sizing:border-box}.pagefind-ui--reset a,.pagefind-ui--reset button{cursor:revert}.pagefind-ui--reset ol,.pagefind-ui--reset ul,.pagefind-ui--reset menu{list-style:none}.pagefind-ui--reset img{max-width:100%}.pagefind-ui--reset table{border-collapse:collapse}.pagefind-ui--reset input,.pagefind-ui--reset textarea{-webkit-user-select:auto}.pagefind-ui--reset textarea{white-space:revert}.pagefind-ui--reset meter{-webkit-appearance:revert;appearance:revert}.pagefind-ui--reset ::placeholder{color:unset}.pagefind-ui--reset :where([hidden]){display:none}.pagefind-ui--reset :where([contenteditable]:not([contenteditable=false])){-moz-user-modify:read-write;-webkit-user-modify:read-write;overflow-wrap:break-word;-webkit-line-break:after-white-space;-webkit-user-select:auto}.pagefind-ui--reset :where([draggable=true]){-webkit-user-drag:element}.pagefind-ui--reset mark{all:revert}:root{--pagefind-ui-scale:.8;--pagefind-ui-primary:#393939;--pagefind-ui-text:#393939;--pagefind-ui-background:#ffffff;--pagefind-ui-border:#eeeeee;--pagefind-ui-tag:#eeeeee;--pagefind-ui-border-width:2px;--pagefind-ui-border-radius:8px;--pagefind-ui-image-border-radius:8px;--pagefind-ui-image-box-ratio:3 / 2;--pagefind-ui-font:system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif}.pagefind-ui.svelte-e9gkc3{width:100%;color:var(--pagefind-ui-text);font-family:var(--pagefind-ui-font)}.pagefind-ui__hidden.svelte-e9gkc3{display:none!important}.pagefind-ui__suppressed.svelte-e9gkc3{opacity:0;pointer-events:none}.pagefind-ui__form.svelte-e9gkc3{position:relative}.pagefind-ui__form.svelte-e9gkc3:before{background-color:var(--pagefind-ui-text);width:calc(18px * var(--pagefind-ui-scale));height:calc(18px * var(--pagefind-ui-scale));top:calc(23px * var(--pagefind-ui-scale));left:calc(20px * var(--pagefind-ui-scale));content:"";position:absolute;display:block;opacity:.7;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");mask-image:url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.7549 11.255H11.9649L11.6849 10.985C12.6649 9.845 13.2549 8.365 13.2549 6.755C13.2549 3.165 10.3449 0.255005 6.75488 0.255005C3.16488 0.255005 0.254883 3.165 0.254883 6.755C0.254883 10.345 3.16488 13.255 6.75488 13.255C8.36488 13.255 9.84488 12.665 10.9849 11.685L11.2549 11.965V12.755L16.2549 17.745L17.7449 16.255L12.7549 11.255ZM6.75488 11.255C4.26488 11.255 2.25488 9.245 2.25488 6.755C2.25488 4.26501 4.26488 2.255 6.75488 2.255C9.24488 2.255 11.2549 4.26501 11.2549 6.755C11.2549 9.245 9.24488 11.255 6.75488 11.255Z' fill='%23000000'/%3E%3C/svg%3E%0A");-webkit-mask-size:100%;mask-size:100%;z-index:9;pointer-events:none}.pagefind-ui__search-input.svelte-e9gkc3{height:calc(64px * var(--pagefind-ui-scale));padding:0 calc(70px * var(--pagefind-ui-scale)) 0 calc(54px * var(--pagefind-ui-scale));background-color:var(--pagefind-ui-background);border:var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);border-radius:var(--pagefind-ui-border-radius);font-size:calc(21px * var(--pagefind-ui-scale));position:relative;appearance:none;-webkit-appearance:none;display:flex;width:100%;box-sizing:border-box;font-weight:700}.pagefind-ui__search-input.svelte-e9gkc3::placeholder{opacity:.2}.pagefind-ui__search-clear.svelte-e9gkc3{position:absolute;top:calc(3px * var(--pagefind-ui-scale));right:calc(3px * var(--pagefind-ui-scale));height:calc(58px * var(--pagefind-ui-scale));padding:0 calc(15px * var(--pagefind-ui-scale)) 0 calc(2px * var(--pagefind-ui-scale));color:var(--pagefind-ui-text);font-size:calc(14px * var(--pagefind-ui-scale));cursor:pointer;background-color:var(--pagefind-ui-background);border-radius:var(--pagefind-ui-border-radius)}.pagefind-ui__drawer.svelte-e9gkc3{gap:calc(60px * var(--pagefind-ui-scale));display:flex;flex-direction:row;flex-wrap:wrap}.pagefind-ui__results-area.svelte-e9gkc3{min-width:min(calc(400px * var(--pagefind-ui-scale)),100%);flex:1000;margin-top:calc(20px * var(--pagefind-ui-scale))}.pagefind-ui__results.svelte-e9gkc3{padding:0}.pagefind-ui__message.svelte-e9gkc3{box-sizing:content-box;font-size:calc(16px * var(--pagefind-ui-scale));height:calc(24px * var(--pagefind-ui-scale));padding:calc(20px * var(--pagefind-ui-scale)) 0;display:flex;align-items:center;font-weight:700;margin-top:0}.pagefind-ui__button.svelte-e9gkc3{margin-top:calc(40px * var(--pagefind-ui-scale));border:var(--pagefind-ui-border-width) solid var(--pagefind-ui-border);border-radius:var(--pagefind-ui-border-radius);height:calc(48px * var(--pagefind-ui-scale));padding:0 calc(12px * var(--pagefind-ui-scale));font-size:calc(16px * var(--pagefind-ui-scale));color:var(--pagefind-ui-primary);background:var(--pagefind-ui-background);width:100%;text-align:center;font-weight:700;cursor:pointer}.pagefind-ui__button.svelte-e9gkc3:hover{border-color:var(--pagefind-ui-primary);color:var(--pagefind-ui-primary);background:var(--pagefind-ui-background)}}@layer starlight.core{[data-search-modal-open]{overflow:hidden}#starlight__search{--sl-search-result-spacing: calc(1.25rem * var(--pagefind-ui-scale));--sl-search-result-pad-inline-start: calc(3.75rem * var(--pagefind-ui-scale));--sl-search-result-pad-inline-end: calc(1.25rem * var(--pagefind-ui-scale));--sl-search-result-pad-block: calc(.9375rem * var(--pagefind-ui-scale));--sl-search-result-nested-pad-block: calc(.625rem * var(--pagefind-ui-scale));--sl-search-corners: calc(.3125rem * var(--pagefind-ui-scale));--sl-search-page-icon-size: calc(1.875rem * var(--pagefind-ui-scale));--sl-search-page-icon-inline-start: calc( (var(--sl-search-result-pad-inline-start) - var(--sl-search-page-icon-size)) / 2 );--sl-search-tree-diagram-size: calc(2.5rem * var(--pagefind-ui-scale));--sl-search-tree-diagram-inline-start: calc( (var(--sl-search-result-pad-inline-start) - var(--sl-search-tree-diagram-size)) / 2 )}#starlight__search .pagefind-ui__form:before{--pagefind-ui-text: var(--sl-color-gray-1);opacity:1}#starlight__search .pagefind-ui__search-input{color:var(--sl-color-white);font-weight:400;width:calc(100% - var(--sl-search-cancel-space))}#starlight__search input:focus{--pagefind-ui-border: var(--sl-color-accent)}#starlight__search .pagefind-ui__search-clear{inset-inline-end:var(--sl-search-cancel-space);width:calc(60px * var(--pagefind-ui-scale));padding:0;background-color:transparent;overflow:hidden}#starlight__search .pagefind-ui__search-clear:focus{outline:1px solid var(--sl-color-accent)}#starlight__search .pagefind-ui__search-clear:before{content:"";-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E") center / 50% no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E") center / 50% no-repeat;background-color:var(--sl-color-text-accent);display:block;width:100%;height:100%}#starlight__search .pagefind-ui__results>*+*{margin-top:var(--sl-search-result-spacing)}#starlight__search .pagefind-ui__result{border:0;padding:0}#starlight__search .pagefind-ui__result-nested{position:relative;padding:var(--sl-search-result-nested-pad-block) var(--sl-search-result-pad-inline-end);padding-inline-start:var(--sl-search-result-pad-inline-start)}#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)),#starlight__search .pagefind-ui__result-nested{position:relative;background-color:var(--sl-color-black)}#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):hover,#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,#starlight__search .pagefind-ui__result-nested:hover,#starlight__search .pagefind-ui__result-nested:focus-within{outline:1px solid var(--sl-color-accent-high)}#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,#starlight__search .pagefind-ui__result-nested:focus-within{background-color:var(--sl-color-accent-low)}#starlight__search .pagefind-ui__result-thumb,#starlight__search .pagefind-ui__result-inner{margin-top:0}#starlight__search .pagefind-ui__result-inner>:first-child{border-radius:var(--sl-search-corners) var(--sl-search-corners) 0 0}#starlight__search .pagefind-ui__result-inner>:last-child{border-radius:0 0 var(--sl-search-corners) var(--sl-search-corners)}#starlight__search .pagefind-ui__result-inner>.pagefind-ui__result-title{padding:var(--sl-search-result-pad-block) var(--sl-search-result-pad-inline-end);padding-inline-start:var(--sl-search-result-pad-inline-start)}#starlight__search .pagefind-ui__result-inner>.pagefind-ui__result-title:before{content:"";position:absolute;inset-block:0;inset-inline-start:var(--sl-search-page-icon-inline-start);width:var(--sl-search-page-icon-size);background:var(--sl-color-gray-3);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E") center no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E") center no-repeat}#starlight__search .pagefind-ui__result-inner{align-items:stretch;gap:1px}#starlight__search .pagefind-ui__result-link{position:unset;--pagefind-ui-text: var(--sl-color-white);font-weight:600}#starlight__search .pagefind-ui__result-link:hover{text-decoration:none}#starlight__search .pagefind-ui__result-nested .pagefind-ui__result-link:before{content:unset}#starlight__search .pagefind-ui__result-nested:before{content:"";position:absolute;inset-block:0;inset-inline-start:var(--sl-search-tree-diagram-inline-start);width:var(--sl-search-tree-diagram-size);background:var(--sl-color-gray-4);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E") 0% 0% / 100% no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E") 0% 0% / 100% no-repeat}#starlight__search .pagefind-ui__result-nested:last-of-type:before{-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E")}[dir=rtl] .pagefind-ui__result-title:before,[dir=rtl] .pagefind-ui__result-nested:before{transform:scaleX(-1)}#starlight__search .pagefind-ui__result-link:after{content:"";position:absolute;inset:0}#starlight__search .pagefind-ui__result-excerpt{font-size:calc(1rem * var(--pagefind-ui-scale));overflow-wrap:anywhere}#starlight__search mark{color:var(--sl-color-gray-2);background-color:transparent;font-weight:600}#starlight__search .pagefind-ui__filter-value:before{border-color:var(--sl-color-text-invert)}#starlight__search .pagefind-ui__result-tags{background-color:var(--sl-color-black);margin-top:0;padding:var(--sl-search-result-nested-pad-block) var(--sl-search-result-pad-inline-end)}}@layer starlight.core{site-search:where(.astro-2f6du2pg){display:contents}button:where(.astro-2f6du2pg)[data-open-modal]{display:flex;align-items:center;gap:.5rem;border:0;background-color:transparent;color:var(--sl-color-gray-1);cursor:pointer;height:2.5rem;font-size:var(--sl-text-xl)}@media(min-width:50rem){button:where(.astro-2f6du2pg)[data-open-modal]{border:1px solid var(--sl-color-gray-5);border-radius:.5rem;padding-inline-start:.75rem;padding-inline-end:.5rem;background-color:var(--sl-color-black);color:var(--sl-color-gray-2);font-size:var(--sl-text-sm);width:100%;max-width:22rem}button:where(.astro-2f6du2pg)[data-open-modal]:hover{border-color:var(--sl-color-gray-2);color:var(--sl-color-white)}button:where(.astro-2f6du2pg)[data-open-modal]>:where(.astro-2f6du2pg):last-child{margin-inline-start:auto}}button:where(.astro-2f6du2pg)>kbd:where(.astro-2f6du2pg){border-radius:.25rem;font-size:var(--sl-text-2xs);gap:.25em;padding-inline:.375rem;background-color:var(--sl-color-gray-6)}kbd:where(.astro-2f6du2pg){font-family:var(--__sl-font)}dialog:where(.astro-2f6du2pg){margin:0;background-color:var(--sl-color-gray-6);border:1px solid var(--sl-color-gray-5);width:100%;max-width:100%;height:100%;max-height:100%;box-shadow:var(--sl-shadow-lg)}dialog:where(.astro-2f6du2pg)[open]{display:flex}dialog:where(.astro-2f6du2pg)::backdrop{background-color:var(--sl-color-backdrop-overlay);-webkit-backdrop-filter:blur(.25rem);backdrop-filter:blur(.25rem)}.dialog-frame:where(.astro-2f6du2pg){position:relative;overflow:auto;flex-direction:column;flex-grow:1;gap:1rem;padding:1rem}button:where(.astro-2f6du2pg)[data-close-modal]{position:absolute;z-index:1;align-items:center;align-self:flex-end;height:calc(64px * var(--pagefind-ui-scale));padding:.25rem;border:0;background:transparent;cursor:pointer;color:var(--sl-color-text-accent)}#starlight__search:where(.astro-2f6du2pg){--pagefind-ui-primary: var(--sl-color-text);--pagefind-ui-text: var(--sl-color-gray-2);--pagefind-ui-font: var(--__sl-font);--pagefind-ui-background: var(--sl-color-black);--pagefind-ui-border: var(--sl-color-gray-5);--pagefind-ui-border-width: 1px;--pagefind-ui-tag: var(--sl-color-gray-5);--sl-search-cancel-space: 5rem}:root[data-theme=light] #starlight__search:where(.astro-2f6du2pg){--pagefind-ui-tag: var(--sl-color-gray-6)}@media(min-width:50rem){#starlight__search:where(.astro-2f6du2pg){--sl-search-cancel-space: 0px}dialog:where(.astro-2f6du2pg){margin:4rem auto auto;border-radius:.5rem;width:90%;max-width:40rem;height:max-content;min-height:15rem;max-height:calc(100% - 8rem)}.dialog-frame:where(.astro-2f6du2pg){padding:1.5rem}}}@layer starlight.core{a:where(.astro-zjlfhtpr){color:var(--sl-color-text-accent);padding:.5em;margin:-.5em}a:where(.astro-zjlfhtpr):hover{opacity:.66}}@layer starlight.core{.download-cta:where(.astro-sle7k2hz){color:var(--sl-color-accent-high);background:var(--bg-b-t, linear-gradient(180deg, #1b1b1b 0%, #000 100%));border:1px solid var(--sl-color-hairline-light)}.download-cta:where(.astro-sle7k2hz):hover{color:var(--sl-color-text-invert);filter:brightness(1.15)}.download-cta:where(.astro-sle7k2hz) span:where(.astro-sle7k2hz):first-of-type svg{width:100%;height:100%;display:block}}@layer starlight.core{.site-title:where(.astro-idrpryed){font-size:var(--sl-text-h4);color:var(--sl-color-text-accent)}.site-title-logo:where(.astro-idrpryed){color:var(--sl-color-gray-1);height:calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y))}.site-title-logo:where(.astro-idrpryed) svg{height:100%;width:auto;display:block}}@layer starlight.core{.social-icons:where(.astro-3ef6ksr2):after{content:"";height:2rem;border-inline-end:1px solid var(--sl-color-gray-5)}@media(min-width:50rem){:root[data-has-sidebar]{--__sidebar-pad: calc(2 * var(--sl-nav-pad-x))}:root:not([data-has-toc]){--__toc-width: 0rem}.header-grid:where(.astro-3ef6ksr2){--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));--__main-column-fr: calc( ( 100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) - (2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) - var(--sl-content-width) ) / 2 );display:grid;grid-template-columns:minmax(calc(var(--__sidebar-width) + max(0rem,var(--__main-column-fr) - var(--sl-nav-gap))),auto) minmax(16rem,1fr) auto;align-content:center}.search-wrapper:where(.astro-3ef6ksr2){min-width:0}.search-wrapper:where(.astro-3ef6ksr2) button[data-open-modal]{height:3rem;font-size:var(--sl-text-base);max-width:28rem;padding-inline-start:1rem;padding-inline-end:.75rem}}}@layer starlight.components{.sl-link-button:where(.astro-5eunqzkt){align-items:center;border:1px solid transparent;border-radius:999rem;display:inline-flex;font-size:var(--sl-text-sm);gap:.5em;line-height:1.1875;outline-offset:.25rem;padding:.4375rem 1.125rem;text-decoration:none}.sl-link-button:where(.astro-5eunqzkt).primary{background:var(--sl-color-text-accent);border-color:var(--sl-color-text-accent);color:var(--sl-color-black)}.sl-link-button:where(.astro-5eunqzkt).primary:hover{color:var(--sl-color-black)}.sl-link-button:where(.astro-5eunqzkt).secondary{border-color:inherit;color:var(--sl-color-white)}.sl-link-button:where(.astro-5eunqzkt).minimal{color:var(--sl-color-white);padding-inline:0}.icon:where(.astro-5eunqzkt){display:inline-flex;width:1.25rem;height:1.25rem;flex-shrink:0}.icon:where(.astro-5eunqzkt) svg{width:100%;height:100%;display:block}@media(min-width:50rem){.sl-link-button:where(.astro-5eunqzkt){font-size:var(--sl-text-base);padding:.9375rem 1.25rem}}}@layer starlight.core{.hero-image:where(.astro-bbe6dxrz){object-fit:contain;width:min(47%,13.5rem);height:auto;margin-inline:auto}.copy:where(.astro-bbe6dxrz)>:where(.astro-bbe6dxrz){max-width:50ch}.hero-title:where(.astro-bbe6dxrz){font-size:clamp(var(--sl-text-3xl),calc(.25rem + 5vw),var(--sl-text-6xl));line-height:var(--sl-line-height-headings);font-weight:600;color:var(--sl-color-white)}.tagline:where(.astro-bbe6dxrz){font-size:clamp(var(--sl-text-base),calc(.0625rem + 2vw),var(--sl-text-xl));color:var(--sl-color-gray-2);max-width:50ch}.platform-icons:where(.astro-bbe6dxrz){display:inline-flex;align-items:center;gap:.5rem;color:var(--sl-color-gray-2)}.platform-icon:where(.astro-bbe6dxrz){display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem}.platform-icon:where(.astro-bbe6dxrz) svg{width:100%;height:100%;display:block}.platform-icon-windows:where(.astro-bbe6dxrz){opacity:.2}@media(min-width:50rem){.hero-grid:where(.astro-bbe6dxrz){grid-template-columns:7fr 4fr;gap:3%;padding-block:clamp(1.25rem,calc(.5rem + 4vmin),3rem)}.hero-image:where(.astro-bbe6dxrz){order:2;width:min(67%,17rem)}}}@layer starlight.content{.sl-markdown-content :not(a,strong,em,del,span,input,code,br)+:not(a,strong,em,del,span,input,code,br,:where(.not-content *)){margin-top:var(--sl-content-gap-y)}.sl-markdown-content :not(a,strong,em,del,span,input,code,br)+:is(astro-island,astro-slot)>:not(a,strong,em,del,span,input,code,br,:where(.not-content *)):first-child{margin-top:var(--sl-content-gap-y)}.sl-markdown-content :not(h1,h2,h3,h4,h5,h6)+:is(h1,h2,h3,h4,h5,h6):not(:where(.not-content *)){margin-top:1.5em}.sl-markdown-content li+li:not(:where(.not-content *)),.sl-markdown-content dt+dt:not(:where(.not-content *)),.sl-markdown-content dt+dd:not(:where(.not-content *)),.sl-markdown-content dd+dd:not(:where(.not-content *)){margin-top:.25rem}.sl-markdown-content li:not(:where(.not-content *)){overflow-wrap:anywhere}.sl-markdown-content li>:is(:last-child:not(li,ul,ol,a,strong,em,del,span,input,code,br,script,:where(.not-content *)),:not(script):has(~script:last-child):not(:has(~:not(script)))){margin-bottom:1.25rem}.sl-markdown-content dt:not(:where(.not-content *)){font-weight:700}.sl-markdown-content dd:not(:where(.not-content *)){padding-inline-start:1rem}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6):not(:where(.not-content *)){color:var(--sl-color-white);line-height:var(--sl-line-height-headings);font-weight:600}.sl-markdown-content :is(img,picture,video,canvas,svg,iframe):not(:where(.not-content *)){display:block;max-width:100%;height:auto}.sl-markdown-content h1:not(:where(.not-content *)){font-size:var(--sl-text-h1)}.sl-markdown-content h2:not(:where(.not-content *)){font-size:var(--sl-text-h2)}.sl-markdown-content h3:not(:where(.not-content *)){font-size:var(--sl-text-h3)}.sl-markdown-content h4:not(:where(.not-content *)){font-size:var(--sl-text-h4)}.sl-markdown-content h5:not(:where(.not-content *)){font-size:var(--sl-text-h5)}.sl-markdown-content h6:not(:where(.not-content *)){font-size:var(--sl-text-h6)}.sl-markdown-content a:not(:where(.not-content *)){color:var(--sl-color-text-accent)}.sl-markdown-content a:hover:not(:where(.not-content *)){color:var(--sl-color-white)}.sl-markdown-content code:not(:where(.not-content *)){background-color:var(--sl-color-bg-inline-code);margin-block:-.125rem;padding:.125rem .375rem;font-size:var(--sl-text-code-sm)}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6) code{font-size:inherit}.sl-markdown-content pre:not(:where(.not-content *)){border:1px solid var(--sl-color-gray-5);padding:.75rem 1rem;font-size:var(--sl-text-code);tab-size:2}.sl-markdown-content pre code:not(:where(.not-content *)){all:unset;font-family:var(--__sl-font-mono)}.sl-markdown-content blockquote:not(:where(.not-content *)){border-inline-start:1px solid var(--sl-color-gray-5);padding-inline-start:1rem}.sl-markdown-content table:not(:where(.not-content *)){display:block;overflow:auto;border-spacing:0}.sl-markdown-content :is(th,td):not(:where(.not-content *)){border-bottom:1px solid var(--sl-color-gray-5);padding:.5rem 1rem;vertical-align:baseline}.sl-markdown-content :is(th:first-child,td:first-child):not(:where(.not-content *)){padding-inline-start:0}.sl-markdown-content :is(th:last-child,td:last-child):not(:where(.not-content *)){padding-inline-end:0}.sl-markdown-content th:not(:where(.not-content *)){color:var(--sl-color-white);font-weight:600}.sl-markdown-content th:not([align]):not(:where(.not-content *)){text-align:start}.sl-markdown-content .starlight-aside :is(th,td,hr,blockquote):not(:where(.not-content *)){border-color:var(--sl-color-gray-4)}@supports (border-color: color-mix(in srgb,var(--sl-color-asides-text-accent) 30%,transparent)){.sl-markdown-content .starlight-aside :is(th,td,hr,blockquote):not(:where(.not-content *)){border-color:color-mix(in srgb,var(--sl-color-asides-text-accent) 30%,transparent)}}@supports (border-color: color-mix(in srgb,var(--sl-color-asides-text-accent) 12%,transparent)){.sl-markdown-content .starlight-aside code:not(:where(.not-content *)){background-color:color-mix(in srgb,var(--sl-color-asides-text-accent) 12%,transparent)}}.sl-markdown-content hr:not(:where(.not-content *)){border:0;border-bottom:1px solid var(--sl-color-hairline)}.sl-markdown-content details:not(:where(.not-content *)){--sl-details-border-color: var(--sl-color-gray-5);--sl-details-border-color--hover: var(--sl-color-text-accent);border-inline-start:2px solid var(--sl-details-border-color);padding-inline-start:1rem}.sl-markdown-content details:not([open]):hover:not(:where(.not-content *)),.sl-markdown-content details:has(>summary:hover):not(:where(.not-content *)){border-color:var(--sl-details-border-color--hover)}.sl-markdown-content summary:not(:where(.not-content *)){color:var(--sl-color-white);cursor:pointer;display:block;font-weight:600;margin-inline-start:-.5rem;padding-inline-start:.5rem}.sl-markdown-content details[open]>summary:not(:where(.not-content *)){margin-bottom:1rem}.sl-markdown-content summary:not(:where(.not-content *))::marker,.sl-markdown-content summary:not(:where(.not-content *))::-webkit-details-marker{display:none}.sl-markdown-content summary:not(:where(.not-content *)):before{--sl-details-marker-size: 1.25rem;background-color:currentColor;content:"";display:inline-block;height:var(--sl-details-marker-size);width:var(--sl-details-marker-size);margin-inline:calc((var(--sl-details-marker-size) / 4) * -1) .25rem;vertical-align:middle;-webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}@media(prefers-reduced-motion:no-preference){.sl-markdown-content summary:not(:where(.not-content *)):before{transition:transform .2s ease-in-out}}.sl-markdown-content details[open]>summary:not(:where(.not-content *)):before{transform:rotate(90deg)}[dir=rtl] .sl-markdown-content summary:not(:where(.not-content *)):before,.sl-markdown-content [dir=rtl] summary:not(:where(.not-content *)):before{transform:rotate(180deg)}.sl-markdown-content summary:not(:where(.not-content *)) p:only-child{display:inline}.sl-markdown-content .starlight-aside details:not(:where(.not-content *)){--sl-details-border-color: var(--sl-color-asides-border);--sl-details-border-color--hover: var(--sl-color-asides-text-accent)}}@layer starlight.core{[data-mobile-menu-expanded]{overflow:hidden}@media(min-width:50rem){[data-mobile-menu-expanded]{overflow:auto}}}@layer starlight.core{button:where(.astro-yrbfzory){position:fixed;top:calc((var(--sl-nav-height) - var(--sl-menu-button-size)) / 2);inset-inline-end:var(--sl-nav-pad-x);z-index:var(--sl-z-index-navbar);border:0;border-radius:50%;width:var(--sl-menu-button-size);height:var(--sl-menu-button-size);padding:.5rem;background-color:var(--sl-color-white);color:var(--sl-color-black);box-shadow:var(--sl-shadow-md);cursor:pointer}:where(.astro-yrbfzory)[aria-expanded=true] button:where(.astro-yrbfzory){background-color:var(--sl-color-gray-2);box-shadow:none}:where(.astro-yrbfzory)[aria-expanded=true] button:where(.astro-yrbfzory) .open-menu:where(.astro-yrbfzory){display:none}:where(.astro-yrbfzory):not([aria-expanded=true]) button:where(.astro-yrbfzory) .close-menu:where(.astro-yrbfzory){display:none}[data-theme=light] button:where(.astro-yrbfzory){background-color:var(--sl-color-black);color:var(--sl-color-white)}[data-theme=light] :where(.astro-yrbfzory)[aria-expanded=true] button:where(.astro-yrbfzory){background-color:var(--sl-color-gray-5)}}@layer starlight.core{.page:where(.astro-iyrf6vv4){flex-direction:column;min-height:100vh}.header:where(.astro-iyrf6vv4){z-index:var(--sl-z-index-navbar);position:fixed;inset-inline-start:0;inset-block-start:0;width:100%;height:var(--sl-nav-height);border-bottom:1px solid var(--sl-color-hairline-shade);padding:var(--sl-nav-pad-y) var(--sl-nav-pad-x);padding-inline-end:var(--sl-nav-pad-x);background-color:var(--sl-color-bg-nav)}[data-has-sidebar] .header:where(.astro-iyrf6vv4){padding-inline-end:calc(var(--sl-nav-gap) + var(--sl-nav-pad-x) + var(--sl-menu-button-size))}.sidebar-pane:where(.astro-iyrf6vv4){visibility:var(--sl-sidebar-visibility, hidden);position:fixed;z-index:var(--sl-z-index-menu);inset-block:var(--sl-nav-height) 0;inset-inline-start:0;width:100%;background-color:var(--sl-color-black);overflow-y:auto}[aria-expanded=true]~.sidebar-pane:where(.astro-iyrf6vv4){--sl-sidebar-visibility: visible}.sidebar-content:where(.astro-iyrf6vv4){height:100%;min-height:max-content;padding:1rem var(--sl-sidebar-pad-x) 0;flex-direction:column;gap:1rem}@media(min-width:50rem){.sidebar-content:where(.astro-iyrf6vv4):after{content:"";padding-bottom:1px}}.main-frame:where(.astro-iyrf6vv4){padding-top:calc(var(--sl-nav-height) + var(--sl-mobile-toc-height));padding-inline-start:var(--sl-content-inline-start)}@media(min-width:50rem){[data-has-sidebar] .header:where(.astro-iyrf6vv4){padding-inline-end:var(--sl-nav-pad-x)}.sidebar-pane:where(.astro-iyrf6vv4){--sl-sidebar-visibility: visible;width:var(--sl-sidebar-width);background-color:var(--sl-color-bg-sidebar);border-inline-end:1px solid var(--sl-color-hairline-shade)}}}@layer starlight.core{ul:where(.astro-uehw6aht){padding:0;list-style:none}a:where(.astro-uehw6aht){--pad-inline: .5rem;display:block;border-radius:.25rem;padding-block:.25rem;padding-inline:calc(1rem * var(--depth) + var(--pad-inline)) var(--pad-inline);line-height:1.25}a:where(.astro-uehw6aht)[aria-current=true]{color:var(--sl-color-text-accent)}.isMobile:where(.astro-uehw6aht) a:where(.astro-uehw6aht){--pad-inline: 1rem;display:flex;justify-content:space-between;gap:var(--pad-inline);border-top:1px solid var(--sl-color-gray-6);border-radius:0;padding-block:.5rem;color:var(--sl-color-text);font-size:var(--sl-text-sm);text-decoration:none;outline-offset:var(--sl-outline-offset-inside)}.isMobile:where(.astro-uehw6aht):first-child>li:where(.astro-uehw6aht):first-child>a:where(.astro-uehw6aht){border-top:0}.isMobile:where(.astro-uehw6aht) a:where(.astro-uehw6aht)[aria-current=true],.isMobile:where(.astro-uehw6aht) a:where(.astro-uehw6aht)[aria-current=true]:hover,.isMobile:where(.astro-uehw6aht) a:where(.astro-uehw6aht)[aria-current=true]:focus{color:var(--sl-color-white);background-color:unset}.isMobile:where(.astro-uehw6aht) a:where(.astro-uehw6aht)[aria-current=true]:after{content:"";width:1rem;background-color:var(--sl-color-text-accent);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==);mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;flex-shrink:0}}@layer starlight.core{nav:where(.astro-2zbszx3p){position:fixed;z-index:var(--sl-z-index-toc);top:calc(var(--sl-nav-height) - 1px);inset-inline:0;border-top:1px solid var(--sl-color-gray-5);background-color:var(--sl-color-bg-nav)}@media(min-width:50rem){nav:where(.astro-2zbszx3p){inset-inline-start:var(--sl-content-inline-start, 0)}}summary:where(.astro-2zbszx3p){gap:.5rem;align-items:center;height:var(--sl-mobile-toc-height);border-bottom:1px solid var(--sl-color-hairline-shade);padding:.5rem 1rem;font-size:var(--sl-text-xs);outline-offset:var(--sl-outline-offset-inside)}summary:where(.astro-2zbszx3p)::marker,summary:where(.astro-2zbszx3p)::-webkit-details-marker{display:none}.toggle:where(.astro-2zbszx3p){flex-shrink:0;gap:1rem;align-items:center;justify-content:space-between;border:1px solid var(--sl-color-gray-5);border-radius:.5rem;padding-block:.5rem;padding-inline-start:.75rem;padding-inline-end:.5rem;line-height:1;background-color:var(--sl-color-black);user-select:none;cursor:pointer}details:where(.astro-2zbszx3p)[open] .toggle:where(.astro-2zbszx3p){color:var(--sl-color-white);border-color:var(--sl-color-accent)}details:where(.astro-2zbszx3p) .toggle:where(.astro-2zbszx3p):hover{color:var(--sl-color-white);border-color:var(--sl-color-gray-2)}[dir=rtl] .caret:where(.astro-2zbszx3p){transform:rotate(180deg)}details:where(.astro-2zbszx3p)[open] .caret:where(.astro-2zbszx3p){transform:rotate(90deg)}.display-current:where(.astro-2zbszx3p){white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:var(--sl-color-white)}.dropdown:where(.astro-2zbszx3p){--border-top: 1px;margin-top:calc(-1 * var(--border-top));border:var(--border-top) solid var(--sl-color-gray-6);border-top-color:var(--sl-color-hairline-shade);max-height:calc(85vh - var(--sl-nav-height) - var(--sl-mobile-toc-height));overflow-y:auto;background-color:var(--sl-color-black);box-shadow:var(--sl-shadow-md);overscroll-behavior:contain}}@layer starlight.core{.right-sidebar-panel:where(.astro-auw6kndz){padding:1rem var(--sl-sidebar-pad-x)}.sl-container:where(.astro-auw6kndz){width:calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x))}.right-sidebar-panel:where(.astro-auw6kndz) h2{color:var(--sl-color-white);font-size:var(--sl-text-h5);font-weight:600;line-height:var(--sl-line-height-headings);margin-bottom:.5rem}.right-sidebar-panel:where(.astro-auw6kndz) :where(a){display:block;font-size:var(--sl-text-xs);text-decoration:none;color:var(--sl-color-gray-3);overflow-wrap:anywhere}.right-sidebar-panel:where(.astro-auw6kndz) :where(a):hover{color:var(--sl-color-white)}@media(min-width:72rem){.sl-container:where(.astro-auw6kndz){max-width:calc(((100vw - var(--sl-sidebar-width) - 2 * var(--sl-content-pad-x) - 2 * var(--sl-sidebar-pad-x)) * .25))}}}@layer starlight.core{h1:where(.astro-gldynaio){margin-top:1rem;font-size:var(--sl-text-h1);line-height:var(--sl-line-height-headings);font-weight:600;color:var(--sl-color-white)}}@layer starlight.core{.social-icons:where(.astro-22fp5ct5){display:flex;margin-inline-end:auto;gap:1rem;align-items:center;padding-block:1rem}.social-icons:where(.astro-22fp5ct5):empty{display:none}.mobile-preferences:where(.astro-22fp5ct5){justify-content:space-between;flex-wrap:wrap;border-top:1px solid var(--sl-color-gray-6);column-gap:1rem;padding:.5rem 0}}@layer starlight.core{sl-sidebar-state-persist:where(.astro-fouifdpt){display:contents}}@layer starlight.core{ul:where(.astro-j63poqv6){--sl-sidebar-item-padding-inline: .5rem;list-style:none;padding:0}li:where(.astro-j63poqv6){overflow-wrap:anywhere}ul:where(.astro-j63poqv6) ul:where(.astro-j63poqv6) li:where(.astro-j63poqv6){margin-inline-start:var(--sl-sidebar-item-padding-inline);border-inline-start:1px solid var(--sl-color-hairline-light);padding-inline-start:var(--sl-sidebar-item-padding-inline)}.large:where(.astro-j63poqv6){font-size:var(--sl-text-lg);font-weight:600;color:var(--sl-color-white)}.top-level:where(.astro-j63poqv6)>li:where(.astro-j63poqv6)+li:where(.astro-j63poqv6){margin-top:.75rem}summary:where(.astro-j63poqv6){display:flex;align-items:center;justify-content:space-between;padding:.2em var(--sl-sidebar-item-padding-inline);line-height:1.4;cursor:pointer;user-select:none}summary:where(.astro-j63poqv6)::marker,summary:where(.astro-j63poqv6)::-webkit-details-marker{display:none}.caret:where(.astro-j63poqv6){transition:transform .2s ease-in-out;flex-shrink:0}[dir=rtl] .caret:where(.astro-j63poqv6){transform:rotate(180deg)}:where(.astro-j63poqv6)[open]>summary:where(.astro-j63poqv6) .caret:where(.astro-j63poqv6){transform:rotate(90deg)}a:where(.astro-j63poqv6){display:block;border-radius:.25rem;text-decoration:none;color:var(--sl-color-gray-2);padding:.3em var(--sl-sidebar-item-padding-inline);line-height:1.4}a:where(.astro-j63poqv6):hover,a:where(.astro-j63poqv6):focus{color:var(--sl-color-white)}:where(.astro-j63poqv6)[aria-current=page],:where(.astro-j63poqv6)[aria-current=page]:hover,:where(.astro-j63poqv6)[aria-current=page]:focus{font-weight:600;color:var(--sl-color-text-invert);background-color:var(--sl-color-text-accent)}a:where(.astro-j63poqv6)>:where(.astro-j63poqv6):not(:last-child),.group-label:where(.astro-j63poqv6)>:where(.astro-j63poqv6):not(:last-child){margin-inline-end:.25em}@media(min-width:50rem){.top-level:where(.astro-j63poqv6)>li:where(.astro-j63poqv6)+li:where(.astro-j63poqv6){margin-top:.5rem}.large:where(.astro-j63poqv6){font-size:var(--sl-text-base)}a:where(.astro-j63poqv6){font-size:var(--sl-text-sm)}}}@layer starlight.core{a:where(.astro-yr4ykvqi){clip:rect(0,0,0,0);position:fixed;top:.75rem;inset-inline-start:.75rem}a:where(.astro-yr4ykvqi):focus{clip:unset;z-index:var(--sl-z-index-skiplink);display:block;padding:.5rem 1rem;text-decoration:none;color:var(--sl-color-text-invert);background-color:var(--sl-color-text-accent);box-shadow:var(--sl-shadow-lg)}}@layer starlight.core{.main-pane:where(.astro-6ptv7odt){isolation:isolate}@media(min-width:72rem){.right-sidebar-container:where(.astro-6ptv7odt){order:2;position:relative;width:calc(var(--sl-sidebar-width) + (100% - var(--sl-content-width) - var(--sl-sidebar-width)) / 2)}.right-sidebar:where(.astro-6ptv7odt){position:fixed;top:0;border-inline-start:1px solid var(--sl-color-hairline);padding-top:var(--sl-nav-height);width:100%;height:100vh;overflow-y:auto;scrollbar-width:none}.main-pane:where(.astro-6ptv7odt){width:100%}[data-has-sidebar][data-has-toc] .main-pane:where(.astro-6ptv7odt){--sl-content-margin-inline: auto 0;order:1;width:calc(var(--sl-content-width) + (100% - var(--sl-content-width) - var(--sl-sidebar-width)) / 2)}}}html:not([data-has-toc]){--sl-mobile-toc-height: 0rem}html:not([data-has-sidebar]){--sl-content-width: 67.5rem}html{scroll-padding-top:calc(1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height))}main:where(.astro-ept3ui5y){padding:var(--sl-main-pad)}@media(min-width:50em){:where(.astro-ept3ui5y)[data-has-sidebar]{--sl-content-inline-start: var(--sl-sidebar-width)}}@media(min-width:72em){html{scroll-padding-top:calc(1.5rem + var(--sl-nav-height))}}
diff --git a/assets/vendor/ssscript-docs/_astro/logo-app-bg.CaK6FTqF_Z1HYzg2.svg b/assets/vendor/ssscript-docs/_astro/logo-app-bg.CaK6FTqF_Z1HYzg2.svg
new file mode 100644
index 0000000..c93759c
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/logo-app-bg.CaK6FTqF_Z1HYzg2.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/vendor/ssscript-docs/_astro/page.B1D-nYk3.js b/assets/vendor/ssscript-docs/_astro/page.B1D-nYk3.js
new file mode 100644
index 0000000..bb3de36
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/page.B1D-nYk3.js
@@ -0,0 +1 @@
+const v={},d=new Set,s=new WeakSet;let f=!0,h,l=!1;function g(e){l||(l=!0,f??=!1,h??="hover",p(),w(),y(),S())}function p(){for(const e of["touchstart","mousedown"])document.addEventListener(e,t=>{a(t.target,"tap")&&c(t.target.href,{ignoreSlowConnection:!0})},{passive:!0})}function w(){let e;document.body.addEventListener("focusin",n=>{a(n.target,"hover")&&t(n)},{passive:!0}),document.body.addEventListener("focusout",o,{passive:!0}),u(()=>{for(const n of document.getElementsByTagName("a"))s.has(n)||a(n,"hover")&&(s.add(n),n.addEventListener("mouseenter",t,{passive:!0}),n.addEventListener("mouseleave",o,{passive:!0}))});function t(n){const r=n.target.href;e&&clearTimeout(e),e=setTimeout(()=>{c(r)},80)}function o(){e&&(clearTimeout(e),e=0)}}function y(){let e;u(()=>{for(const t of document.getElementsByTagName("a"))s.has(t)||a(t,"viewport")&&(s.add(t),e??=L(),e.observe(t))})}function L(){const e=new WeakMap;return new IntersectionObserver((t,o)=>{for(const n of t){const r=n.target,i=e.get(r);n.isIntersecting?(i&&clearTimeout(i),e.set(r,setTimeout(()=>{o.unobserve(r),e.delete(r),c(r.href)},300))):i&&(clearTimeout(i),e.delete(r))}})}function S(){u(()=>{for(const e of document.getElementsByTagName("a"))a(e,"load")&&c(e.href)})}function c(e,t){e=e.replace(/#.*/,"");const o=t?.ignoreSlowConnection??!1;if(E(e,o))if(d.add(e),document.createElement("link").relList?.supports?.("prefetch")&&t?.with!=="fetch"){const n=document.createElement("link");n.rel="prefetch",n.setAttribute("href",e),document.head.append(n)}else{const n=new Headers;for(const[r,i]of Object.entries(v))n.set(r,i);fetch(e,{priority:"low",headers:n})}}function E(e,t){if(!navigator.onLine||!t&&m())return!1;try{const o=new URL(e,location.href);return location.origin===o.origin&&(location.pathname!==o.pathname||location.search!==o.search)&&!d.has(e)}catch{}return!1}function a(e,t){if(e?.tagName!=="A")return!1;const o=e.dataset.astroPrefetch;return o==="false"?!1:t==="tap"&&(o!=null||f)&&m()?!0:o==null&&f||o===""?t===h:o===t}function m(){if("connection"in navigator){const e=navigator.connection;return e.saveData||/2g/.test(e.effectiveType)}return!1}function u(e){e();let t=!1;document.addEventListener("astro:page-load",()=>{if(!t){t=!0;return}e()})}g();
diff --git a/assets/vendor/ssscript-docs/_astro/print.DNXP8c50.css b/assets/vendor/ssscript-docs/_astro/print.DNXP8c50.css
new file mode 100644
index 0000000..f721e68
--- /dev/null
+++ b/assets/vendor/ssscript-docs/_astro/print.DNXP8c50.css
@@ -0,0 +1 @@
+@media print{:root{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);--sl-color-accent-high: hsl(234, 80%, 30%);--sl-color-accent: hsl(234, 90%, 60%);--sl-color-accent-low: hsl(234, 88%, 90%);--sl-color-text-accent: var(--sl-color-accent);--sl-color-text-invert: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-7);--sl-color-bg-sidebar: var(--sl-color-bg);--sl-color-bg-inline-code: var(--sl-color-gray-6);--sl-color-bg-accent: var(--sl-color-accent);--sl-color-hairline-light: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-gray-6);--sl-color-backdrop-overlay: hsla(225, 9%, 36%, .66);--sl-shadow-sm: none;--sl-shadow-md: none;--sl-shadow-lg: none}.print\:hidden{display:none}.print\:flex{display:flex}.print\:block{display:block}main{padding-bottom:0}main>.content-panel{padding-block-start:0}.content-panel+.content-panel{border:0}.page>header{position:relative}.page>.main-frame{padding-top:0;padding-inline-start:0}.main-pane{--sl-sidebar-width: 0px;--sl-content-width: 100%}.sl-banner{--sl-color-banner-text: var(--sl-color-white);background-color:transparent}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6){break-after:avoid}.sl-markdown-content :is(p,li){orphans:2;widows:2}.sl-markdown-content pre{overflow-x:hidden;white-space:pre-wrap}.sl-markdown-content .expressive-code,.sl-markdown-content figure,.sl-markdown-content pre{break-inside:avoid}.expressive-code .frame.is-terminal .header:before{box-shadow:inset 99rem 99rem var(--sl-color-gray-5)}.expressive-code .frame.has-title:not(.is-terminal) .header{background:transparent;border-bottom:1px solid var(--sl-color-gray-6)}.expressive-code .frame.has-title:not(.is-terminal) .title{background:transparent}.expressive-code .frame.has-title:not(.is-terminal) .title:after{border-top:0}.expressive-code .copy{display:none}.sl-markdown-content code:not(:where(.not-content *)){background-color:transparent;padding:0;margin-block:unset;font-size:.9375em}.sl-badge{background:transparent;color:var(--sl-color-white)}starlight-file-tree{break-inside:avoid}starlight-file-tree .highlight{outline:3px solid var(--sl-color-accent-low);color:var(--sl-color-text);background-color:transparent}.starlight-aside{break-inside:avoid}.sl-link-button.primary{background:transparent;border-color:var(--sl-color-white);color:var(--sl-color-white)}starlight-tabs{break-inside:avoid}.sl-steps>li:after{box-shadow:inset 99rem 99rem var(--sl-color-hairline-light)}}
diff --git a/assets/vendor/ssscript-docs/favicon.svg b/assets/vendor/ssscript-docs/favicon.svg
new file mode 100644
index 0000000..1e1c79e
--- /dev/null
+++ b/assets/vendor/ssscript-docs/favicon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/content/knowledge.json b/content/knowledge.json
new file mode 100644
index 0000000..577ebd2
--- /dev/null
+++ b/content/knowledge.json
@@ -0,0 +1,201 @@
+{
+  "schemaVersion": 1,
+  "slug": "knowledge",
+  "title": "Knowledge",
+  "basePath": "knowledge",
+  "settings": {
+    "description": "Материалы о NODE.DC, агентных модулях, интеграциях, цифровых двойниках и рабочих контурах платформы.",
+    "searchPlaceholder": "Поиск",
+    "themeDefault": "dark",
+    "navTitle": "Knowledge",
+    "downloadEnabled": false,
+    "homeHref": "/"
+  },
+  "introHtml": "

Knowledge - библиотека материалов NODE.DC о платформе, агентных контурах, интеграциях и прикладных модулях. Здесь собраны не инструкции ради инструкций, а объяснения: как устроены слои системы, где появляются данные, как они попадают в задачи и какие сценарии можно собирать поверх общей инфраструктуры.

\n

Что здесь лежит

\n

Раздел помогает быстро разобраться в NODE.DC как в единой операционной платформе: от Hub, Engine и Ops до тендерного агента, интеграции с 1С, цифровых двойников, беспилотников и SEO-модуля.

\n

Как использовать

\n

Материалы можно читать как обзор архитектуры. 

\n

[button name=\"Перейти к модулям на лендинге\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]

", + "tree": [ + { + "id": "platform-overview", + "title": "NODE.DC", + "slug": "platform", + "enabled": true, + "useContent": true, + "seo": { + "title": "NODE.DC | NODE.DC Knowledge", + "description": "Обзор платформы NODE.DC: данные, процессы, агенты, Hub, Engine, Ops и прикладные контуры предприятия." + }, + "bodyHtml": "\n

NODE.DC - операционная платформа, которая связывает распределённые объекты, данные и действия в едином контуре управления. Она подключает физические источники, цифровые сервисы, бизнес-системы и прикладные модули, превращая разрозненные сигналы в воспроизводимые потоки событий.

\n

Смысл платформы

\n

В основе NODE.DC лежит модель объектов: сущности, паспорта, связи, состояния, события и допустимые действия. Эта структура даёт общую картину того, что происходит в компании, где это происходит, с чем связано и какие следующие действия доступны.

\n

Три базовых слоя

\n\n

Агентный интеллектуальный слой

\n

Поверх данных работает AIS - агентный слой платформы. Он анализирует историю и текущее состояние, выявляет отклонения, формирует объяснения, предлагает действия и запускает сценарии строго в рамках прав доступа и контекста.

\n

Где применяется

\n

NODE.DC полезен там, где важно быстро перейти от наблюдения к действию: в закупках, проектном офисе, инженерных данных, производстве, строительстве, цифровых двойниках, робототехнике и интеграциях с учётными системами.

\n

[button name=\"Посмотреть платформу на лендинге\" link=\"/\" block=\"ai-modes\" target=\"platform-composition\"]

\n", + "children": [ + { + "id": "hub", + "title": "Hub", + "slug": "hub", + "enabled": true, + "useContent": true, + "seo": { + "title": "Hub | NODE.DC Knowledge", + "description": "NODE.DC Hub: единая точка входа, каталог приложений, роли, группы, доступы и рабочие пространства." + }, + "bodyHtml": "

\"\"

NODE.DC Hub - единая точка доступа к среде NODE.DC. Он показывает пользователю доступные приложения, рабочие пространства и сервисы платформы с учётом роли, группы и настроек конкретного контура.

\n

Что делает Hub

\n\n

Enterprise-сценарий

\n

В корпоративном контуре администратор может назначать типовые конфигурации доступа для подразделений и отдельно расширять права конкретным пользователям. За счёт этого платформа не превращается в общий склад кнопок: каждый видит только те инструменты, которые относятся к его работе.

\n

Связь с другими слоями

\n

Из Hub пользователь переходит в Engine, Ops и прикладные приложения. Все переходы сохраняют контекст прав доступа, поэтому один и тот же модуль может выглядеть по-разному для руководителя проекта, инженера, закупщика или внешнего подрядчика.

[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video\" target=\"hub-video\"]

", + "children": [] + }, + { + "id": "engine", + "title": "Engine", + "slug": "engine", + "enabled": true, + "useContent": true, + "seo": { + "title": "Engine | NODE.DC Knowledge", + "description": "NODE.DC Engine: нодовый слой для сборки процессов, интеграций, интерфейсов и агентных сценариев." + }, + "bodyHtml": "

NODE.DC Engine - рабочий слой для проектирования и запуска процессов внутри платформы. Здесь создаются интеграции сервисов, обработчики данных, диспетчерские панели, сценарии роботизации и внутренние автоматизации для бизнеса.

\n

Нодовый редактор

\n

Процессы собираются в узловом редакторе. Узлы описывают источники данных, преобразования, действия, визуализацию, проверки, внешние API и пользовательские интерфейсы. Такой подход позволяет быстро добавлять новые сценарии без переписывания всей системы.

\n

Роль ИИ-агентов

\n

Сценарий можно собрать вручную или с помощью агента: пользователь описывает задачу, а агент предлагает архитектуру решения, добавляет необходимые узлы и готовит схему для подключения к интерфейсу.

\n

Что можно собирать

\n\n

Передача в Ops

\n

Результат работы Engine не остаётся схемой в редакторе. Он передаётся в операционный слой: создаёт задачи, обновляет статусы, прикладывает отчёты, запускает согласования или отдаёт данные в другой модуль.

[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video-copy\" target=\"engine-video\"]


", + "children": [] + }, + { + "id": "ops", + "title": "Ops", + "slug": "ops", + "enabled": true, + "useContent": true, + "seo": { + "title": "Ops | NODE.DC Knowledge", + "description": "NODE.DC Ops: операционный слой для задач, статусов, отчётов, инженерных данных и результатов агентов." + }, + "bodyHtml": "

NODE.DC Ops - операционный слой платформы. В него сводятся проекты, процессы, задачи, инженерные данные, статусы, отчёты, согласования и результаты работы прикладных контуров.

\n

Единая рабочая среда

\n

Ops объединяет людей, агентов и цифровые контуры предприятия. Задачи можно ставить, уточнять, декомпозировать, передавать между подразделениями, контролировать по этапам и связывать с результатами из других слоёв NODE.DC.

\n

Ассистент внутри процесса

\n

Сквозной ассистент работает не как справочная кнопка, а как интерфейс к рабочему контексту: задачам, проектным материалам, кодовой базе, инженерным данным, отчётам и действиям агентов. При наличии прав он может готовить материалы, создавать подзадачи и инициировать изменения в сервисах.

\n

Форматы представления

\n\n

Зачем это нужно

\n

Ops делает работу наблюдаемой: каждое действие, решение и отклонение получает место в общем контуре, а не теряется в чатах, таблицах и разрозненных файлах.

[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video-copy-copy\" target=\"ops-video\"]

", + "children": [] + } + ] + }, + { + "id": "agent-context", + "title": "Агентный контур", + "slug": "agent-context", + "enabled": true, + "useContent": true, + "seo": { + "title": "Агентный контур | NODE.DC Knowledge", + "description": "Как в NODE.DC устроена агентная автоматизация: контекст, права, задачи, данные и передача результатов между модулями." + }, + "bodyHtml": "\n

Агентный контур NODE.DC - рабочая среда для сборки, запуска и контроля цифровых агентов внутри единой операционной платформы. Здесь агентная автоматизация не является набором отдельных скриптов: она встроена в процессы, роли, данные и задачи компании.

\n

Что передаётся агентам

\n

Агентам можно отдавать повторяющиеся и трудоёмкие операции: поиск, отбор, классификацию, разбор документов, подготовку отчётов, сверку данных и запуск следующих шагов. Человек остаётся владельцем решения, а агент снимает рутину и ускоряет подготовку материалов.

\n

Контекст и права

\n

Агент работает только в доступном ему контексте. Он видит задачи, документы, модели, сервисы и историю действий в рамках настроенных прав. Это важно для enterprise-сценариев, где один и тот же процесс может касаться разных подразделений и уровней доступа.

\n

Передача результата

\n\n

Почему это инфраструктура

\n

Главная ценность агентного слоя в том, что он поднимает автоматизацию с уровня отдельного инструмента до уровня рабочей инфраструктуры. События, причины, решения и действия остаются в одном контуре и могут быть проверены позже.

\n", + "children": [] + }, + { + "id": "applied-modules", + "title": "Прикладные модули", + "slug": "modules", + "enabled": true, + "useContent": true, + "seo": { + "title": "Прикладные модули | NODE.DC Knowledge", + "description": "Прикладные модули NODE.DC: отраслевые и корпоративные контуры поверх общей платформенной инфраструктуры." + }, + "bodyHtml": "\n

Прикладные модули NODE.DC закрывают конкретные функции предприятия: закупки, тендеры, 1С, производство, строительство, цифровые двойники, беспилотники, SEO и внутренние инструменты разработки.

\n

Задача прикладного слоя

\n

Смысл модулей - адаптировать платформу под специфику компании. В одном контуре можно связать учётные системы, роли, задачи, файлы, инженерные данные, агентов, отчёты и интерфейсы без разрыва между приложениями.

\n

Что можно добавить

\n\n

Единый контекст

\n

Модуль не живёт отдельно. Он подключается к ролям, данным, задачам и сквозному ассистенту NODE.DC. Поэтому результат работы тендерного агента может попасть в Ops, данные из 1С могут участвовать в закупочной проверке, а инженерная модель может быть связана с задачей, отчётом и согласованием.

\n

[button name=\"Смотреть прикладные модули\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]

\n", + "children": [ + { + "id": "tender-agent", + "title": "Тендерный агент", + "slug": "tender-agent", + "enabled": true, + "useContent": true, + "seo": { + "title": "Тендерный агент | NODE.DC Knowledge", + "description": "Как тендерный агент NODE.DC помогает искать закупки, анализировать требования, риски, документы и передавать задачи в смежные контуры." + }, + "bodyHtml": "\n

Тендерный агент NODE.DC закрывает рабочий контур тендерного отдела. В одном интерфейсе объединяются поиск и агрегация закупок, первичный отбор, AI-анализ конкретных тендеров и передача материалов в смежные модули.

\n

Что делает агент

\n\n

Поведенческий поиск

\n

Система учитывает действия специалиста: какие закупки он открывает, какие отклоняет, по каким признакам принимает решение. На этой базе формируются устойчивые паттерны отбора, а релевантные тендеры находятся точнее.

\n

Роль человека

\n

Специалист тендерного отдела остаётся оператором решения. Агент сокращает ручной перебор, готовит анализ и подсвечивает риски, но финальное решение, подача и коммуникация остаются под контролем команды.

\n

Связь с платформой

\n

Материалы тендера могут стать задачами в Ops, данными для юридического анализа, входом для 1С-интеграции или основанием для закупочной цепочки.

\n

[button name=\"Посмотреть демо на лендинге\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]

\n", + "children": [] + }, + { + "id": "1c-integration", + "title": "Интеграция с 1С", + "slug": "1c-integration", + "enabled": true, + "useContent": true, + "seo": { + "title": "Интеграция с 1С | NODE.DC Knowledge", + "description": "Как NODE.DC связывает 1С с задачами, агентами, закупками, документами, складами и операционными процессами." + }, + "bodyHtml": "\n

Интеграция с 1С связывает учётный контур предприятия с задачами, агентами и рабочими модулями NODE.DC. 1С остаётся источником фактов, а NODE.DC добавляет операционный, аналитический и агентный слой поверх этих данных.

\n

Какие данные подключаются

\n\n

Типовые сценарии

\n

Команда может использовать данные из 1С для сверки тендерных требований, контроля закупок, подготовки отчётов, запуска согласований, проверки остатков, анализа поставщиков и передачи результата в Ops.

\n

1С агент

\n

1С агент помогает не просто показать данные, а ответить на рабочий вопрос: найти документ, объяснить статус, собрать выборку, подготовить задачу или подсказать следующий шаг. При этом действия выполняются в рамках прав доступа и фиксируются в операционном контуре.

\n

Почему это важно

\n

Без интеграции учётная система часто остаётся отдельным окном. NODE.DC связывает её с процессами: данные становятся частью задач, отчётов, агентов и модулей, а не отдельной ручной проверкой.

\n", + "children": [] + }, + { + "id": "drone-management", + "title": "Управление беспилотниками", + "slug": "drone-management", + "enabled": true, + "useContent": true, + "seo": { + "title": "Управление беспилотниками | NODE.DC Knowledge", + "description": "Цифровой контур NODE.DC для беспилотных систем: телеметрия, IoT-состояния, видеопотоки, события и операторский интерфейс." + }, + "bodyHtml": "\n

В NODE.DC может быть реализован интеграционный слой для работы с беспилотными решениями. Платформа становится цифровым контуром вокруг системы управления: собирает телеметрию, анализирует состояние IoT-компонентов, обрабатывает события и помогает оператору принимать решения.

\n

Что контролирует платформа

\n\n

ИИ-модули

\n

Отдельное направление - анализ видеопотоков, оценка окружения, выявление препятствий, контроль состояния маршрута и подготовка кратких сводок для оператора.

\n

Опыт и переносимость

\n

Подход применим к разным автономным комплексам: курьерским решениям, гусеничным наземным платформам, распределённой технике и мониторинговым сценариям. В каждом случае NODE.DC отвечает за данные, события, наблюдаемость, интерфейс и агентное сопровождение.

\n

Результат

\n

Оператор видит не набор сырых сигналов, а понятную картину: что произошло, насколько это критично, какие действия доступны и какие данные уже собраны системой.

\n", + "children": [] + }, + { + "id": "city-digital-twins", + "title": "Цифровые двойники городов", + "slug": "city-digital-twins", + "enabled": true, + "useContent": true, + "seo": { + "title": "Цифровые двойники городов | NODE.DC Knowledge", + "description": "Как NODE.DC может выступать точкой входа в городской цифровой двойник: 3DGS, карта, BIM, облака точек и единый контекст." + }, + "bodyHtml": "\n

NODE.DC может работать как вход в цифровой двойник города или крупной территории. Пользователь получает единое пространственное окружение, где обзор, аналитика и инженерный разбор связаны геопривязкой, API и общим контекстом.

\n

Три масштаба

\n

Обзорный слой

\n

3DGS-сцены дают быстрый вход внутрь локации: прогулку от третьего лица, совместное присутствие, наблюдение других участников и естественное понимание пространства.

\n

Городской слой

\n

Карта местности становится навигацией и агрегатором слоёв: транспорт, экология, инфраструктура, распределённые объекты, статусы и события.

\n

Инженерный слой

\n

BIM-модели, облака точек и технические материалы позволяют перейти к детальному анализу объекта, конструкции, помещений и инженерных систем.

\n

Единый маршрут пользователя

\n

Человек может начать с общего обзора, перейти к городской аналитике, затем открыть конкретный объект и посмотреть его инженерную структуру без разрыва пространственной логики.

\n

Связь с Ops

\n

Любое наблюдение, отклонение или задача из цифрового двойника может быть связано с карточкой в Ops, ответственным исполнителем, сроком, материалами и историей действий.

\n", + "children": [] + }, + { + "id": "industrial-contour", + "title": "Промышленный контур", + "slug": "industrial-contour", + "enabled": true, + "useContent": true, + "seo": { + "title": "Промышленный контур | NODE.DC Knowledge", + "description": "Цифровой двойник промышленного предприятия в NODE.DC: состояния цехов, оборудование, облака точек, BIM и эксплуатационные данные." + }, + "bodyHtml": "\n

Промышленный контур NODE.DC помогает хранить и сопоставлять состояния производственных помещений, оборудования и инженерной инфраструктуры во времени. Это не только визуализация, а рабочий инструмент поддержки решений.

\n

Цифровой архив состояний

\n

Регулярная фиксация состояния предприятия позволяет сравнивать цеха, оборудование и инженерные зоны за разные периоды. На этой базе можно выявлять деформации, износ, перегрузки и отклонения.

\n

Что входит

\n\n

Оптимизация процессов

\n

Когда пространственные данные связаны с производственными узлами, легче находить причины износа, перегрузок и простоев. Решение принимается не по отдельной фотографии или таблице, а по контексту объекта.

\n

Планирование модернизаций

\n

Цифровой двойник фиксирует промежуточные состояния предприятия как живую историю объекта. Это снижает риск ошибок при проектировании, ремонте, реконструкции и внедрении нового оборудования.

\n", + "children": [] + }, + { + "id": "enterprise-management", + "title": "Управление предприятием", + "slug": "enterprise-management", + "enabled": true, + "useContent": true, + "seo": { + "title": "Управление предприятием | NODE.DC Knowledge", + "description": "Ситуационный и мониторинговый режим NODE.DC для управленческого контура предприятия." + }, + "bodyHtml": "\n

Управленческий контур NODE.DC собирает в одном рабочем пространстве финансовые показатели, производственные статусы, распределённые объекты, логистику, поставщиков, отклонения и выводы агентных систем.

\n

Единое экранное пространство

\n

Вместо разрозненных отчётов формируется живая картина предприятия. Каждый блок интерфейса связан с конкретным объектом, процессом, узлом данных или рабочим полем.

\n

Что можно выводить

\n\n

Не витрина, а рабочая проекция

\n

Ценность режима в том, что он связан с действующей архитектурой процессов. Из показателя можно перейти в задачу, из задачи - в материалы, из материалов - в ответственного, а из отклонения - в сценарий реакции.

\n

Роль руководителя

\n

Руководитель видит не только итоговый отчёт, но и причинно-следственную цепочку: от источника события до текущего статуса и следующего действия.

\n", + "children": [] + }, + { + "id": "construction-contour", + "title": "Строительный контур", + "slug": "construction-contour", + "enabled": true, + "useContent": true, + "seo": { + "title": "Строительный контур | NODE.DC Knowledge", + "description": "NODE.DC для строительства: BIM, полевые отчёты, подрядчики, дефекты, сроки, модели и операционный контроль." + }, + "bodyHtml": "\n

Строительный контур NODE.DC связывает проектные данные, площадку, подрядчиков, BIM-модели, полевые отчёты, дефекты, сроки и согласования в одной рабочей системе.

\n

Что объединяется

\n\n

Переход от модели к действию

\n

Инженерная модель становится не отдельным файлом, а источником рабочих действий. Элемент модели можно связать с задачей, актом, замечанием, фотографией, сроком или ответственным исполнителем.

\n

Контроль изменений

\n

Для стройки важна история: что было запланировано, что изменилось, кто согласовал, какие материалы приложены и почему задача перешла в новый статус. Ops сохраняет эту цепочку в операционном контуре.

\n

Агентное сопровождение

\n

Агенты могут готовить сводки по дефектам, искать просрочки, сравнивать отчёты, собирать материалы к совещанию и подсвечивать зоны риска.

\n", + "children": [] + }, + { + "id": "seo-module", + "title": "SEO модуль", + "slug": "seo-module", + "enabled": true, + "useContent": true, + "seo": { + "title": "SEO модуль | NODE.DC Knowledge", + "description": "SEO Farm как модуль NODE.DC: аудит сайта, Wordstat, карта ключей, редактор правок, media SEO и ручной apply через changeset." + }, + "bodyHtml": "\n

SEO модуль NODE.DC строится как локальный редакторский контур для работы с существующими сайтами. Его задача - не автоматически публиковать тексты и не забивать страницу ключами, а дать управляемый процесс для редактора, дизайнера и SEO-специалиста.

\n

Основной сценарий

\n
  1. проект сканируется как папка сайта или рабочий источник;
  2. собирается индекс страниц, секций, медиа и технических файлов;
  3. выполняется baseline audit по структуре, заголовкам, описаниям, видимым текстам и медиа;
  4. из текста извлекаются смыслы, seed-запросы и потенциальные интенты;
  5. через Wordstat собираются реальные запросы и частотность;
  6. формируется карта ключей по секциям;
  7. редактор утверждает правки, проверяет переспам и применяет изменения через changeset.
\n

Что проверяется

\n\n

Почему не автопостинг

\n

Модуль работает с рабочей копией текста. Исходный сайт не меняется до ручного Apply. Перед применением пользователь видит план, варианты правок, проверки качества и итоговый changeset.

\n

Связь с Knowledge

\n

Этот раздел Knowledge сам является хорошим кандидатом для SEO-модуля: каждая статья имеет отдельный URL, понятный заголовок, описание, структуру H2/H3 и может проходить через тот же редакторский контур.

\n", + "children": [] + } + ] + } + ] +} diff --git a/content/pages/home.json b/content/pages/home.json index a2cb284..3f87e24 100644 --- a/content/pages/home.json +++ b/content/pages/home.json @@ -16,7 +16,7 @@ { "label": "О продукте", "href": "", - "target": "index.html#features", + "target": "index.html#platform-composition", "enabled": true, "linkMode": "target", "linkTarget": "" @@ -37,10 +37,18 @@ "linkMode": "href", "linkTarget": "_blank" }, + { + "label": "Knowledge", + "href": "/knowledge/", + "target": "", + "enabled": true, + "linkMode": "href", + "linkTarget": "" + }, { "label": "Тарифы", "href": "", - "target": "index.html#pricing-wrap", + "target": "index.html#pricing-intro", "enabled": false, "linkMode": "target", "linkTarget": "" @@ -760,7 +768,7 @@ "type": "aiModes", "template": "project-composition.html", "adminLabel": "СОСТАВ ПРОЕКТА", - "anchor": null, + "anchor": "platform-composition", "content": { "heading": { "muted": "NODE.DC", @@ -894,7 +902,7 @@ "type": "featureVideo", "template": "project-video-first.html", "adminLabel": "HUB - PROJECT VIDEO - FIRST", - "anchor": "features", + "anchor": "hub-video", "content": { "windowTitle": "NDC_HUB.mp4", "videoSrc": "./assets/uploads/media/ai-modes/ndc_hub_base-mqv6lm07.mp4", @@ -1046,7 +1054,7 @@ "type": "componentLibrary", "template": "project-video-detail.html", "adminLabel": "HUB - PROJECT VIDEO - DETAIL", - "anchor": null, + "anchor": "hub-contour", "content": { "heading": { "muted": "Hub.", @@ -1196,7 +1204,7 @@ "type": "featureVideo", "template": "project-video-first.html", "adminLabel": "ENGINE - PROJECT VIDEO - FIRST", - "anchor": "features", + "anchor": "engine-video", "content": { "windowTitle": "ENGINE_HUB.mp4", "videoSrc": "./assets/uploads/media/ai-modes/ndc_engine_base-mqv6lvyr.mp4", @@ -1349,7 +1357,7 @@ "type": "componentLibrary", "template": "project-video-detail.html", "adminLabel": "ENGINE - PROJECT VIDEO - DETAIL", - "anchor": null, + "anchor": "engine-contour", "content": { "heading": { "muted": "Engine. ", @@ -1500,7 +1508,7 @@ "type": "featureVideo", "template": "project-video-first.html", "adminLabel": "OPS - PROJECT VIDEO - FIRST", - "anchor": "features", + "anchor": "ops-video", "content": { "windowTitle": "OPS_HUB.mp4", "videoSrc": "./assets/uploads/media/ai-modes/ndc_ops_blur-mqv6ss6k.mp4", @@ -1653,7 +1661,7 @@ "type": "componentLibrary", "template": "project-video-detail.html", "adminLabel": "OPS- PROJECT VIDEO - DETAIL", - "anchor": null, + "anchor": "ops-contour", "content": { "heading": { "muted": "OPS", @@ -1804,7 +1812,7 @@ "type": "componentLibrary", "template": "project-video-detail.html", "adminLabel": "МОДУЛИ- PROJECT VIDEO - DETAIL", - "anchor": null, + "anchor": "applied-modules", "content": { "heading": { "muted": "Модули. ", @@ -1975,7 +1983,7 @@ "type": "faqSection", "template": "faq.html", "adminLabel": "FAQ", - "anchor": null, + "anchor": "faq", "content": { "heading": { "muted": "Частые", @@ -2212,7 +2220,7 @@ "type": "publishCta", "template": "publish-cta.html", "adminLabel": "Финальный CTA", - "anchor": "hero", + "anchor": "final-cta", "content": { "cta": { "heading": { @@ -2295,7 +2303,7 @@ "type": "centralTextBlock", "template": "central-text-block.html", "adminLabel": "Центральный текстовый блок", - "anchor": "pricing", + "anchor": "pricing-intro", "content": { "eyebrow": "Тарифы", "heading": { @@ -2376,7 +2384,7 @@ "type": "pricingTable", "template": "pricing-table.html", "adminLabel": "Тарифы: таблица", - "anchor": "pricing-wrap", + "anchor": "pricing-table", "content": { "pricing": { "windowTitle": "Тарифы.app", @@ -2870,7 +2878,7 @@ "type": "screenshotCards", "template": "screenshot-cards.html", "adminLabel": "Карточки скриншотов", - "anchor": null, + "anchor": "screenshots", "enabled": false, "content": { "galleryItems": [ @@ -3018,7 +3026,7 @@ "type": "waitlistFiles", "template": "request-demo-txt-files.html", "adminLabel": "Запрос DEMO и TXT-файлы", - "anchor": "waitlist", + "anchor": "demo-request", "enabled": true, "content": { "form": { @@ -3536,7 +3544,7 @@ { "label": "Лист ожидания", "href": "", - "target": "index.html#waitlist", + "target": "index.html#demo-request", "enabled": false, "linkMode": "target", "linkTarget": "" @@ -3544,23 +3552,23 @@ { "label": "Тарифы", "href": "", - "target": "index.html#", + "target": "index.html#pricing-intro", "enabled": false, "linkMode": "target", "linkTarget": "" }, { "label": "Возможности", - "href": "https://nodedc.dctouch.ru/", - "target": "index.html#features", + "href": "", + "target": "index.html#applied-modules", "enabled": true, - "linkMode": "href", + "linkMode": "target", "linkTarget": "" }, { "label": "Демо", "href": "", - "target": "index.html#features", + "target": "index.html#hub-video", "enabled": true, "linkMode": "target", "linkTarget": "" diff --git a/index.html b/index.html index 22260fb..4044fb0 100644 --- a/index.html +++ b/index.html @@ -82,7 +82,7 @@ visibility: hidden; }

Платформа для цифровых контуров предприятия

Hub, Engine, Ops и прикладные модули в единой экосистеме управления, разработки и исполнения.

Полныйагентный контроль

Единый агентный слой с доступом к задачам, кодовой базе, данным, моделям и связанным сервисам.

Модульная архитектура

Подключаемые приложения, роли, контуры доступа, рабочие пространства и специализированные инструменты.

Разработка и запуск

Сборка workflow, интерфейсов и рабочих контуров с передачей результатов в операционный слой.
Логотип NODE.DC
Selfhost платформа

Запросить DEMO

NODE.DC - платформа для задач, процессов, данных, агентов и инженерных модулей.

Заявка принята. Спасибо.

Мы скоро свяжемся.

Подать заявку?

Или оформить ранний Founder-доступ

Оформить Founder-доступ 350€
До 200 мест

Не получилось

Что-то пошло не так.
Попробуйте ещё раз чуть позже.

DCTOUCH
© NODE.DC, 2026
-
+

@@ -152,8 +152,8 @@ visibility: hidden;

-
NDC_HUB.mp4

HUBточка входа в платформу

Универсальный инструмент для ИИ-агентов в вашем NODE.DC-контуре.
Каталог приложений
Доступные модули отображаются по роли, группе и контуру предприятия.
Рабочие пространства
Проекты, отделы и enterprise-контуры разделяются по правилам доступа.
Администрирование
Пользователи, группы, роли и права управляются из единой административной зоны.
Сквозной доступ
Переход к Engine, Ops и прикладным модулям сохраняет общий контекст платформы.
-
+
NDC_HUB.mp4

HUBточка входа в платформу

Универсальный инструмент для ИИ-агентов в вашем NODE.DC-контуре.
Каталог приложений
Доступные модули отображаются по роли, группе и контуру предприятия.
Рабочие пространства
Проекты, отделы и enterprise-контуры разделяются по правилам доступа.
Администрирование
Пользователи, группы, роли и права управляются из единой административной зоны.
Сквозной доступ
Переход к Engine, Ops и прикладным модулям сохраняет общий контекст платформы.
+

@@ -227,8 +227,8 @@ Enterprise-контуры

-
ENGINE_HUB.mp4

ENGINEСлой разработки

Engine используется для сборки workflow, прикладной логики, интеграций и интерфейсов внутри NODE.DC.
Нодовый редактор
Процессы собираются из узлов, связей и готовых компонентов.
Агентная сборка
Агент может сформировать workflow по текстовому заданию.
Библиотека узлов
500+ компонентов для данных, логики, API, визуализации и внешних систем.
Запуск в контур
Результат подключается к интерфейсу и передается в Ops.
-
+
ENGINE_HUB.mp4

ENGINEСлой разработки

Engine используется для сборки workflow, прикладной логики, интеграций и интерфейсов внутри NODE.DC.
Нодовый редактор
Процессы собираются из узлов, связей и готовых компонентов.
Агентная сборка
Агент может сформировать workflow по текстовому заданию.
Библиотека узлов
500+ компонентов для данных, логики, API, визуализации и внешних систем.
Запуск в контур
Результат подключается к интерфейсу и передается в Ops.
+

@@ -301,8 +301,8 @@ Enterprise-контуры

-
OPS_HUB.mp4

OPSОперационный слой

Ops собирает задачи, статусы, отчеты, согласования, рабочие материалы и результаты агентов в едином операционном контуре.
Проектный контур
Задачи, карточки, этапы, статусы и контроль исполнения.
Рабочие представления
Канбан, таблицы, календарь, графики, диаграммы и дашборды.
Сквозной ассистент
Контекст по задачам, коду, моделям, данным и связанным сервисам.
Результаты модулей
События, отчеты и действия из Engine и прикладных модулей попадают в Ops.
-
+
OPS_HUB.mp4

OPSОперационный слой

Ops собирает задачи, статусы, отчеты, согласования, рабочие материалы и результаты агентов в едином операционном контуре.
Проектный контур
Задачи, карточки, этапы, статусы и контроль исполнения.
Рабочие представления
Канбан, таблицы, календарь, графики, диаграммы и дашборды.
Сквозной ассистент
Контекст по задачам, коду, моделям, данным и связанным сервисам.
Результаты модулей
События, отчеты и действия из Engine и прикладных модулей попадают в Ops.
+

@@ -376,7 +376,7 @@ Enterprise-контуры

-
+

@@ -485,8 +485,8 @@ Enterprise-контуры

-

Частыевопросыи ответы

Подробный обзор возможностей, логики работы и внутреннего устройства.
FAQ
NODE.DC
Что входит в NODE.DC?
NODE.DC
Зачем нужен Hub?
NODE.DC
Как связаны Hub, Engine и Ops?
NODE.DC
Что создается в Engine?
NODE.DC
Что фиксируется в Ops?
NODE.DC
Как работает сквозной ассистент?
NODE.DC
Как работают прикладные модули?
NODE.DC
Какие модули можно подключать?
NODE.DC
Поддерживаются ли инженерные форматы?
NODE.DC
Как настраиваются роли и права доступа?
NODE.DC
Можно ли подключать корпоративные системы?
NODE.DC
Где размещается NODE.DC?
NODE.DC
Как запускается пилотный контур?
NODE.DC
Чем NODE.DC отличается от набора отдельных приложений?
NODE.DC
Ответить
Что входит в NODE.DC?

NODE.DC включает Hub для входа и управления доступами, Engine для разработки workflow и прикладной логики, Ops для операционной работы, сквозного ассистента и подключаемые модули: 1C Assistant, Tender, Digital Twin, Model Viewer / Engineering Core и отраслевые приложения.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Зачем нужен Hub?

Hub выполняет роль лаунчера и административной точки входа. В нем отображается каталог доступных приложений, рабочие пространства, роли, группы пользователей и права доступа к модулям платформы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как связаны Hub, Engine и Ops?

Hub отвечает за вход, роли и доступ к приложениям. Engine используется для сборки workflow, прикладной логики, интеграций и интерфейсов. Ops принимает задачи, отчеты, статусы, результаты работы модулей и действия агентов в операционный контур.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Что создается в Engine?

В Engine создаются workflow, интеграционные сценарии, обработчики данных, прикладная логика, интерфейсные связки и сервисные контуры. Сборка может выполняться вручную в нодовом редакторе или через ассистента по текстовому заданию.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Что фиксируется в Ops?

В Ops фиксируются задачи, статусы, карточки, отчеты, согласования, рабочие материалы, результаты агентов и данные из подключенных модулей. Слой предназначен для контроля процессов и операционной работы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как работает сквозной ассистент?

Ассистент работает как единый интерфейс к приложениям и данным платформы. В рамках прав доступа ему доступны задачи, процессы, кодовая база, проектные материалы, отчеты, инженерные модели и результаты работы агентов. Ассистент может отвечать на вопросы, декомпозировать задачи, готовить материалы и инициировать действия в связанных слоях.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как работают прикладные модули?

Прикладные модули подключаются к ролям, задачам, данным, рабочим пространствам и ассистенту. Модуль закрывает не отдельный экран, а рабочий контур: входные данные, обработку, действия, отчеты, согласования и передачу результата в Ops.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Какие модули можно подключать?

Базовый набор включает Hub, Engine и Ops. Дополнительно подключаются 1C Assistant, Tender, Digital Twin, Engineering Core, интеграционные сценарии, API-модули и специализированные приложения для подразделений.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Поддерживаются ли инженерные форматы?

Для инженерных и производственных сценариев предусмотрена работа с цифровыми моделями и техническими материалами. Поддерживаются BIM, STEP, STL, 3DGS / Gaussian Splatting, LAS/LAZ-облака точек и другие форматы, связанные с задачами, версиями и проектным контекстом.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как настраиваются роли и права доступа?

Права доступа задаются через Hub. Администратор может назначать роли, формировать группы, разделять контуры доступа по подразделениям и выдавать индивидуальные права на приложения, функции и рабочие области.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Можно ли подключать корпоративные системы?

NODE.DC рассчитан на подключение внешних сервисов, API, внутренних приложений, 1C, баз данных, файловых хранилищ и корпоративных систем. Интеграционные сценарии собираются в Engine и передают результаты в Ops, прикладные модули или пользовательские интерфейсы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Где размещается NODE.DC?

Доступны облачные, выделенные, гибридные и enterprise-варианты размещения. Итоговая конфигурация зависит от требований к безопасности, составу модулей, интеграциям и политике хранения данных.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как запускается пилотный контур?

Пилотный контур формируется по составу модулей, ролей, интеграций и рабочих сценариев. Обычно подключается Hub, настраиваются базовые роли, выбирается один или несколько процессов, после чего собираются workflow, рабочие представления и отчеты.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Чем NODE.DC отличается от набора отдельных приложений?

NODE.DC связывает приложения в единую платформенную архитектуру: общий доступ, роли, сквозной ассистент, связанные задачи, данные, workflow и операционный слой. Это снижает разрыв между разработкой, управлением и исполнением процессов.

Отправлено с NODE.DC MANUAL CORE
-

Подключить.Собрать контур. Запустить.

Единый путь от доступа к модулю до рабочего контура предприятия. Hub для входа, Engine для сборки, Ops для управления.
Модульный доступ, документация и рабочие пространства. +

Частыевопросыи ответы

Подробный обзор возможностей, логики работы и внутреннего устройства.
FAQ
NODE.DC
Что входит в NODE.DC?
NODE.DC
Зачем нужен Hub?
NODE.DC
Как связаны Hub, Engine и Ops?
NODE.DC
Что создается в Engine?
NODE.DC
Что фиксируется в Ops?
NODE.DC
Как работает сквозной ассистент?
NODE.DC
Как работают прикладные модули?
NODE.DC
Какие модули можно подключать?
NODE.DC
Поддерживаются ли инженерные форматы?
NODE.DC
Как настраиваются роли и права доступа?
NODE.DC
Можно ли подключать корпоративные системы?
NODE.DC
Где размещается NODE.DC?
NODE.DC
Как запускается пилотный контур?
NODE.DC
Чем NODE.DC отличается от набора отдельных приложений?
NODE.DC
Ответить
Что входит в NODE.DC?

NODE.DC включает Hub для входа и управления доступами, Engine для разработки workflow и прикладной логики, Ops для операционной работы, сквозного ассистента и подключаемые модули: 1C Assistant, Tender, Digital Twin, Model Viewer / Engineering Core и отраслевые приложения.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Зачем нужен Hub?

Hub выполняет роль лаунчера и административной точки входа. В нем отображается каталог доступных приложений, рабочие пространства, роли, группы пользователей и права доступа к модулям платформы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как связаны Hub, Engine и Ops?

Hub отвечает за вход, роли и доступ к приложениям. Engine используется для сборки workflow, прикладной логики, интеграций и интерфейсов. Ops принимает задачи, отчеты, статусы, результаты работы модулей и действия агентов в операционный контур.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Что создается в Engine?

В Engine создаются workflow, интеграционные сценарии, обработчики данных, прикладная логика, интерфейсные связки и сервисные контуры. Сборка может выполняться вручную в нодовом редакторе или через ассистента по текстовому заданию.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Что фиксируется в Ops?

В Ops фиксируются задачи, статусы, карточки, отчеты, согласования, рабочие материалы, результаты агентов и данные из подключенных модулей. Слой предназначен для контроля процессов и операционной работы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как работает сквозной ассистент?

Ассистент работает как единый интерфейс к приложениям и данным платформы. В рамках прав доступа ему доступны задачи, процессы, кодовая база, проектные материалы, отчеты, инженерные модели и результаты работы агентов. Ассистент может отвечать на вопросы, декомпозировать задачи, готовить материалы и инициировать действия в связанных слоях.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как работают прикладные модули?

Прикладные модули подключаются к ролям, задачам, данным, рабочим пространствам и ассистенту. Модуль закрывает не отдельный экран, а рабочий контур: входные данные, обработку, действия, отчеты, согласования и передачу результата в Ops.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Какие модули можно подключать?

Базовый набор включает Hub, Engine и Ops. Дополнительно подключаются 1C Assistant, Tender, Digital Twin, Engineering Core, интеграционные сценарии, API-модули и специализированные приложения для подразделений.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Поддерживаются ли инженерные форматы?

Для инженерных и производственных сценариев предусмотрена работа с цифровыми моделями и техническими материалами. Поддерживаются BIM, STEP, STL, 3DGS / Gaussian Splatting, LAS/LAZ-облака точек и другие форматы, связанные с задачами, версиями и проектным контекстом.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как настраиваются роли и права доступа?

Права доступа задаются через Hub. Администратор может назначать роли, формировать группы, разделять контуры доступа по подразделениям и выдавать индивидуальные права на приложения, функции и рабочие области.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Можно ли подключать корпоративные системы?

NODE.DC рассчитан на подключение внешних сервисов, API, внутренних приложений, 1C, баз данных, файловых хранилищ и корпоративных систем. Интеграционные сценарии собираются в Engine и передают результаты в Ops, прикладные модули или пользовательские интерфейсы.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Где размещается NODE.DC?

Доступны облачные, выделенные, гибридные и enterprise-варианты размещения. Итоговая конфигурация зависит от требований к безопасности, составу модулей, интеграциям и политике хранения данных.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Как запускается пилотный контур?

Пилотный контур формируется по составу модулей, ролей, интеграций и рабочих сценариев. Обычно подключается Hub, настраиваются базовые роли, выбирается один или несколько процессов, после чего собираются workflow, рабочие представления и отчеты.

Отправлено с NODE.DC MANUAL CORE
NODE.DC
Ответить
Чем NODE.DC отличается от набора отдельных приложений?

NODE.DC связывает приложения в единую платформенную архитектуру: общий доступ, роли, сквозной ассистент, связанные задачи, данные, workflow и операционный слой. Это снижает разрыв между разработкой, управлением и исполнением процессов.

Отправлено с NODE.DC MANUAL CORE
+

Подключить.Собрать контур. Запустить.

Единый путь от доступа к модулю до рабочего контура предприятия. Hub для входа, Engine для сборки, Ops для управления.
Модульный доступ, документация и рабочие пространства. Подключение прикладных модулей и ролей по контурам. -Закрытый инвайт для пилотных внедрений.
Запросить инвайт
Тарифы

Платформа NODE.DC для процессов, данных и агентов.

Подписки на приложение с
включённым объёмом в каждом тарифе.
Подключение локальных моделей и личных ассистентов
SELFHOST в контуре предприятий / внутри инфраструктуры NODE.DC / открытый контур при подключении моделей по API
-
Логотип NODE.DC
Selfhost платформа

Запросить DEMO

NODE.DC - платформа для задач, процессов, данных, агентов и инженерных модулей.

Заявка принята. Спасибо.

Мы скоро свяжемся.

Подать заявку?

Или оформить ранний Founder-доступ

Оформить Founder-доступ 350€
До 200 мест

Не получилось

Что-то пошло не так.
Попробуйте ещё раз чуть позже.

DCTOUCH
© NODE.DC, 2026
Иконка TXT-файла
Hub Access

Единая точка входа: каталог приложений, рабочие пространства, роли, группы и права доступа.

Иконка TXT-файла
Engine Workflows

Нодовые workflow, обработчики данных, интеграционная логика и сборка прикладных контуров.

Иконка TXT-файла
Ops Cards

Операционные карточки, статусы, согласования, отчеты агентов и результаты выполнения задач.

Иконка TXT-файла
1C Assistant

Связка с 1C, разбор документов, поиск данных, сценарии согласований и передача результата в Ops.

Иконка TXT-файла
Tender Agent

Тендерные подборки, проверка требований, документы, дедлайны и агентные отчеты по процедурам.

Иконка TXT-файла
Digital Twin

Цифровые модели объектов, версии, проектный контекст, инженерные данные и связанные рабочие процессы.

Иконка TXT-файла
Model Viewer

Просмотр BIM, STEP, STL, 3DGS, облаков точек и инженерных материалов внутри единого контура.

Иконка TXT-файла
Engineering Core

Обработка технических форматов, связка моделей с задачами, версиями, отчетами и согласованиями.

Иконка TXT-файла
MCP Gateway

Контекстный шлюз для агентов: проектные данные, инструменты, кодовая база и внешние сервисы.

Иконка TXT-файла
API Contracts

Контракты API, схемы данных, webhooks, события, интеграции и контроль совместимости между модулями.

Иконка TXT-файла
Kubernetes

Оркестрация сервисов, изоляция окружений, scaling policies, probes, secrets и управляемый rollout.

Иконка TXT-файла
Cloudflare Edge

CDN, WAF, R2-хранилище, edge routing, cache policies и защита публичного периметра.

Иконка TXT-файла
Zero Trust

Доступ по принципу least privilege: SSO, MFA, device posture, audit trail и сегментация контуров.

Иконка TXT-файла
Security Matrix

Ролевая матрица, группы, политики доступа, ограничения функций и контроль административных действий.

Иконка TXT-файла
RBAC Policies

Права на приложения, рабочие области, действия агентов, документы, отчеты и инженерные данные.

Иконка TXT-файла
Audit Log

Журнал действий пользователей, агентов и сервисов: изменения, статусы, запросы, публикации и доступы.

Иконка TXT-файла
Secrets Vault

Хранение токенов, ключей API, webhook-секретов и конфигураций интеграций в защищенном контуре.

Иконка TXT-файла
Postgres Cluster

Основная транзакционная база для проектов, ролей, задач, статусов, событий и конфигураций платформы.

Иконка TXT-файла
Redis Queue

Очереди фоновых задач, статусы выполнения, debounce событий, кэширование и быстрые агентные операции.

Иконка TXT-файла
Vector Index

Индексация проектных материалов, документации, отчетов и контекста для ассистента и поиска.

Иконка TXT-файла
Local Models

Контур локальных моделей для приватных сценариев, инженерных данных и корпоративных ограничений.

Иконка TXT-файла
Model Routing

Маршрутизация задач между облачными и локальными моделями по типу запроса, правам и стоимости.

Иконка TXT-файла
Agent Runtime

Запуск ассистентов, выполнение действий, контроль инструментов, журналирование и передача результата в Ops.

Иконка TXT-файла
Observability

Метрики, логи, трассировка, health checks, алерты и диагностика сервисов платформы.

Иконка TXT-файла
Backups

Резервные копии данных, конфигураций, файлов, моделей, журналов и сценариев восстановления.

Иконка TXT-файла
CI CD

Сборка, проверки, preview-окружения, controlled rollout, rollback и публикация клиентских модулей.

Иконка TXT-файла
Terraform

Описание инфраструктуры кодом: окружения, сети, хранилища, политики, сервисы и переменные.

Иконка TXT-файла
Object Storage

Хранилище файлов, изображений, моделей, отчетов, txt-материалов и артефактов рабочих процессов.

Иконка TXT-файла
BIM Pipeline

Загрузка BIM-моделей, версии, разметка, связи с задачами и публикация в рабочие пространства.

Иконка TXT-файла
STEP Import

Импорт STEP-моделей, технические материалы, карточки проверки и привязка к инженерным задачам.

Иконка TXT-файла
STL Review

Проверка STL-геометрии, заметки, версии, визуальный контроль и подготовка производственных материалов.

Иконка TXT-файла
3DGS Assets

Gaussian Splatting сцены, визуальные реконструкции объектов и связь с проектным контекстом.

Иконка TXT-файла
LAS LAZ Clouds

Облака точек, съемка, координатные данные, версии и инженерная привязка к задачам.

Иконка TXT-файла
Webhooks

Событийные сценарии: входящие запросы, уведомления, автоматические действия и передача статусов.

Иконка TXT-файла
Docs Portal

Документация, регламенты, инструкции, технические заметки и материалы для команд внедрения.

Иконка TXT-файла
Stack Map

Карта платформенного стека: Hub, Engine, Ops, агенты, данные, интеграции, безопасность и инфраструктура.

Иконка TXT-файла
Hub Access
.txt
Иконка TXT-файла
Engine Workflows
.txt
Иконка TXT-файла
Ops Cards
.txt
Иконка TXT-файла
1C Assistant
.txt
Иконка TXT-файла
Tender Agent
.txt
Иконка TXT-файла
Digital Twin
.txt
Иконка TXT-файла
Model Viewer
.txt
Иконка TXT-файла
Engineering Core
.txt
Иконка TXT-файла
MCP Gateway
.txt
Иконка TXT-файла
API Contracts
.txt
Иконка TXT-файла
Kubernetes
.txt
Иконка TXT-файла
Cloudflare Edge
.txt
Иконка TXT-файла
Zero Trust
.txt
Иконка TXT-файла
Security Matrix
.txt
Иконка TXT-файла
RBAC Policies
.txt
Иконка TXT-файла
Audit Log
.txt
Иконка TXT-файла
Secrets Vault
.txt
Иконка TXT-файла
Postgres Cluster
.txt
Иконка TXT-файла
Redis Queue
.txt
Иконка TXT-файла
Vector Index
.txt
Иконка TXT-файла
Local Models
.txt
Иконка TXT-файла
Model Routing
.txt
Иконка TXT-файла
Agent Runtime
.txt
Иконка TXT-файла
Observability
.txt
Иконка TXT-файла
Backups
.txt
Иконка TXT-файла
CI CD
.txt
Иконка TXT-файла
Terraform
.txt
Иконка TXT-файла
Object Storage
.txt
Иконка TXT-файла
BIM Pipeline
.txt
Иконка TXT-файла
STEP Import
.txt
Иконка TXT-файла
STL Review
.txt
Иконка TXT-файла
3DGS Assets
.txt
Иконка TXT-файла
LAS LAZ Clouds
.txt
Иконка TXT-файла
Webhooks
.txt
Иконка TXT-файла
Docs Portal
.txt
Иконка TXT-файла
Stack Map
.txt
РАНЕЕ — МАЙ 2026
Engineering Core выделен в отдельный модуль
Единый контур инженерных моделей, версий, согласований и рабочих материалов.
РАНЕЕ ИЮНЬ 2026
Knowledge Graph добавлен в состав экосистемы
Связи между задачами, агентами, кодовой базой, документами и инженерными сущностями собраны в общем графе.
СЕЙЧАС
Открыт alpha-доступ к Procurement Intelligence
Агентный контур для закупок, сопоставления условий и контроля поставок.
ДАЛЕЕ СЕНТЯБРЬ 2026
Field Ops включён в план релиза
Мобильный операционный модуль для выездных команд, инспекций, фотофиксации и полевых отчётов.
+Закрытый инвайт для пилотных внедрений.
Запросить инвайт
Тарифы

Платформа NODE.DC для процессов, данных и агентов.

Подписки на приложение с
включённым объёмом в каждом тарифе.
Подключение локальных моделей и личных ассистентов
SELFHOST в контуре предприятий / внутри инфраструктуры NODE.DC / открытый контур при подключении моделей по API
+
Логотип NODE.DC
Selfhost платформа

Запросить DEMO

NODE.DC - платформа для задач, процессов, данных, агентов и инженерных модулей.

Заявка принята. Спасибо.

Мы скоро свяжемся.

Подать заявку?

Или оформить ранний Founder-доступ

Оформить Founder-доступ 350€
До 200 мест

Не получилось

Что-то пошло не так.
Попробуйте ещё раз чуть позже.

DCTOUCH
© NODE.DC, 2026
Иконка TXT-файла
Hub Access

Единая точка входа: каталог приложений, рабочие пространства, роли, группы и права доступа.

Иконка TXT-файла
Engine Workflows

Нодовые workflow, обработчики данных, интеграционная логика и сборка прикладных контуров.

Иконка TXT-файла
Ops Cards

Операционные карточки, статусы, согласования, отчеты агентов и результаты выполнения задач.

Иконка TXT-файла
1C Assistant

Связка с 1C, разбор документов, поиск данных, сценарии согласований и передача результата в Ops.

Иконка TXT-файла
Tender Agent

Тендерные подборки, проверка требований, документы, дедлайны и агентные отчеты по процедурам.

Иконка TXT-файла
Digital Twin

Цифровые модели объектов, версии, проектный контекст, инженерные данные и связанные рабочие процессы.

Иконка TXT-файла
Model Viewer

Просмотр BIM, STEP, STL, 3DGS, облаков точек и инженерных материалов внутри единого контура.

Иконка TXT-файла
Engineering Core

Обработка технических форматов, связка моделей с задачами, версиями, отчетами и согласованиями.

Иконка TXT-файла
MCP Gateway

Контекстный шлюз для агентов: проектные данные, инструменты, кодовая база и внешние сервисы.

Иконка TXT-файла
API Contracts

Контракты API, схемы данных, webhooks, события, интеграции и контроль совместимости между модулями.

Иконка TXT-файла
Kubernetes

Оркестрация сервисов, изоляция окружений, scaling policies, probes, secrets и управляемый rollout.

Иконка TXT-файла
Cloudflare Edge

CDN, WAF, R2-хранилище, edge routing, cache policies и защита публичного периметра.

Иконка TXT-файла
Zero Trust

Доступ по принципу least privilege: SSO, MFA, device posture, audit trail и сегментация контуров.

Иконка TXT-файла
Security Matrix

Ролевая матрица, группы, политики доступа, ограничения функций и контроль административных действий.

Иконка TXT-файла
RBAC Policies

Права на приложения, рабочие области, действия агентов, документы, отчеты и инженерные данные.

Иконка TXT-файла
Audit Log

Журнал действий пользователей, агентов и сервисов: изменения, статусы, запросы, публикации и доступы.

Иконка TXT-файла
Secrets Vault

Хранение токенов, ключей API, webhook-секретов и конфигураций интеграций в защищенном контуре.

Иконка TXT-файла
Postgres Cluster

Основная транзакционная база для проектов, ролей, задач, статусов, событий и конфигураций платформы.

Иконка TXT-файла
Redis Queue

Очереди фоновых задач, статусы выполнения, debounce событий, кэширование и быстрые агентные операции.

Иконка TXT-файла
Vector Index

Индексация проектных материалов, документации, отчетов и контекста для ассистента и поиска.

Иконка TXT-файла
Local Models

Контур локальных моделей для приватных сценариев, инженерных данных и корпоративных ограничений.

Иконка TXT-файла
Model Routing

Маршрутизация задач между облачными и локальными моделями по типу запроса, правам и стоимости.

Иконка TXT-файла
Agent Runtime

Запуск ассистентов, выполнение действий, контроль инструментов, журналирование и передача результата в Ops.

Иконка TXT-файла
Observability

Метрики, логи, трассировка, health checks, алерты и диагностика сервисов платформы.

Иконка TXT-файла
Backups

Резервные копии данных, конфигураций, файлов, моделей, журналов и сценариев восстановления.

Иконка TXT-файла
CI CD

Сборка, проверки, preview-окружения, controlled rollout, rollback и публикация клиентских модулей.

Иконка TXT-файла
Terraform

Описание инфраструктуры кодом: окружения, сети, хранилища, политики, сервисы и переменные.

Иконка TXT-файла
Object Storage

Хранилище файлов, изображений, моделей, отчетов, txt-материалов и артефактов рабочих процессов.

Иконка TXT-файла
BIM Pipeline

Загрузка BIM-моделей, версии, разметка, связи с задачами и публикация в рабочие пространства.

Иконка TXT-файла
STEP Import

Импорт STEP-моделей, технические материалы, карточки проверки и привязка к инженерным задачам.

Иконка TXT-файла
STL Review

Проверка STL-геометрии, заметки, версии, визуальный контроль и подготовка производственных материалов.

Иконка TXT-файла
3DGS Assets

Gaussian Splatting сцены, визуальные реконструкции объектов и связь с проектным контекстом.

Иконка TXT-файла
LAS LAZ Clouds

Облака точек, съемка, координатные данные, версии и инженерная привязка к задачам.

Иконка TXT-файла
Webhooks

Событийные сценарии: входящие запросы, уведомления, автоматические действия и передача статусов.

Иконка TXT-файла
Docs Portal

Документация, регламенты, инструкции, технические заметки и материалы для команд внедрения.

Иконка TXT-файла
Stack Map

Карта платформенного стека: Hub, Engine, Ops, агенты, данные, интеграции, безопасность и инфраструктура.

Иконка TXT-файла
Hub Access
.txt
Иконка TXT-файла
Engine Workflows
.txt
Иконка TXT-файла
Ops Cards
.txt
Иконка TXT-файла
1C Assistant
.txt
Иконка TXT-файла
Tender Agent
.txt
Иконка TXT-файла
Digital Twin
.txt
Иконка TXT-файла
Model Viewer
.txt
Иконка TXT-файла
Engineering Core
.txt
Иконка TXT-файла
MCP Gateway
.txt
Иконка TXT-файла
API Contracts
.txt
Иконка TXT-файла
Kubernetes
.txt
Иконка TXT-файла
Cloudflare Edge
.txt
Иконка TXT-файла
Zero Trust
.txt
Иконка TXT-файла
Security Matrix
.txt
Иконка TXT-файла
RBAC Policies
.txt
Иконка TXT-файла
Audit Log
.txt
Иконка TXT-файла
Secrets Vault
.txt
Иконка TXT-файла
Postgres Cluster
.txt
Иконка TXT-файла
Redis Queue
.txt
Иконка TXT-файла
Vector Index
.txt
Иконка TXT-файла
Local Models
.txt
Иконка TXT-файла
Model Routing
.txt
Иконка TXT-файла
Agent Runtime
.txt
Иконка TXT-файла
Observability
.txt
Иконка TXT-файла
Backups
.txt
Иконка TXT-файла
CI CD
.txt
Иконка TXT-файла
Terraform
.txt
Иконка TXT-файла
Object Storage
.txt
Иконка TXT-файла
BIM Pipeline
.txt
Иконка TXT-файла
STEP Import
.txt
Иконка TXT-файла
STL Review
.txt
Иконка TXT-файла
3DGS Assets
.txt
Иконка TXT-файла
LAS LAZ Clouds
.txt
Иконка TXT-файла
Webhooks
.txt
Иконка TXT-файла
Docs Portal
.txt
Иконка TXT-файла
Stack Map
.txt
РАНЕЕ — МАЙ 2026
Engineering Core выделен в отдельный модуль
Единый контур инженерных моделей, версий, согласований и рабочих материалов.
РАНЕЕ ИЮНЬ 2026
Knowledge Graph добавлен в состав экосистемы
Связи между задачами, агентами, кодовой базой, документами и инженерными сущностями собраны в общем графе.
СЕЙЧАС
Открыт alpha-доступ к Procurement Intelligence
Агентный контур для закупок, сопоставления условий и контроля поставок.
ДАЛЕЕ СЕНТЯБРЬ 2026
Field Ops включён в план релиза
Мобильный операционный модуль для выездных команд, инспекций, фотофиксации и полевых отчётов.
Белая иконка корзины.
diff --git a/knowledge/agent-context/index.html b/knowledge/agent-context/index.html new file mode 100644 index 0000000..80426b4 --- /dev/null +++ b/knowledge/agent-context/index.html @@ -0,0 +1,281 @@ + + + + + + Агентный контур | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Агентный контур

+
+

Агентный контур NODE.DC - рабочая среда для сборки, запуска и контроля цифровых агентов внутри единой операционной платформы. Здесь агентная автоматизация не является набором отдельных скриптов: она встроена в процессы, роли, данные и задачи компании.

+

Что передаётся агентам

Раздел «Что передаётся агентам»
+

Агентам можно отдавать повторяющиеся и трудоёмкие операции: поиск, отбор, классификацию, разбор документов, подготовку отчётов, сверку данных и запуск следующих шагов. Человек остаётся владельцем решения, а агент снимает рутину и ускоряет подготовку материалов.

+ +

Агент работает только в доступном ему контексте. Он видит задачи, документы, модели, сервисы и историю действий в рамках настроенных прав. Это важно для enterprise-сценариев, где один и тот же процесс может касаться разных подразделений и уровней доступа.

+

Передача результата

Раздел «Передача результата»
+
  • результат может стать подзадачей в Ops;
  • может уйти в юридический, закупочный или инженерный модуль;
  • может запустить согласование или проверку;
  • может лечь в отчёт, карточку объекта или рабочий дашборд.
+

Почему это инфраструктура

Раздел «Почему это инфраструктура»
+

Главная ценность агентного слоя в том, что он поднимает автоматизацию с уровня отдельного инструмента до уровня рабочей инфраструктуры. События, причины, решения и действия остаются в одном контуре и могут быть проверены позже.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/index.html b/knowledge/index.html new file mode 100644 index 0000000..d48f5ba --- /dev/null +++ b/knowledge/index.html @@ -0,0 +1,276 @@ + + + + + + Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Knowledge

+

Knowledge - библиотека материалов NODE.DC о платформе, агентных контурах, интеграциях и прикладных модулях. Здесь собраны не инструкции ради инструкций, а объяснения: как устроены слои системы, где появляются данные, как они попадают в задачи и какие сценарии можно собирать поверх общей инфраструктуры.

+ +

Раздел помогает быстро разобраться в NODE.DC как в единой операционной платформе: от Hub, Engine и Ops до тендерного агента, интеграции с 1С, цифровых двойников, беспилотников и SEO-модуля.

+ +

Материалы можно читать как обзор архитектуры. 

+

Перейти к модулям на лендинге

  • NODE.DC — Обзор платформы NODE.DC: данные, процессы, агенты, Hub, Engine, Ops и прикладные контуры предприятия.
  • Hub — NODE.DC Hub: единая точка входа, каталог приложений, роли, группы, доступы и рабочие пространства.
  • Engine — NODE.DC Engine: нодовый слой для сборки процессов, интеграций, интерфейсов и агентных сценариев.
  • Ops — NODE.DC Ops: операционный слой для задач, статусов, отчётов, инженерных данных и результатов агентов.
  • Агентный контур — Как в NODE.DC устроена агентная автоматизация: контекст, права, задачи, данные и передача результатов между модулями.
  • Прикладные модули — Прикладные модули NODE.DC: отраслевые и корпоративные контуры поверх общей платформенной инфраструктуры.
  • Тендерный агент — Как тендерный агент NODE.DC помогает искать закупки, анализировать требования, риски, документы и передавать задачи в смежные контуры.
  • Интеграция с 1С — Как NODE.DC связывает 1С с задачами, агентами, закупками, документами, складами и операционными процессами.
  • Управление беспилотниками — Цифровой контур NODE.DC для беспилотных систем: телеметрия, IoT-состояния, видеопотоки, события и операторский интерфейс.
  • Цифровые двойники городов — Как NODE.DC может выступать точкой входа в городской цифровой двойник: 3DGS, карта, BIM, облака точек и единый контекст.
  • Промышленный контур — Цифровой двойник промышленного предприятия в NODE.DC: состояния цехов, оборудование, облака точек, BIM и эксплуатационные данные.
  • Управление предприятием — Ситуационный и мониторинговый режим NODE.DC для управленческого контура предприятия.
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/1c-integration/index.html b/knowledge/modules/1c-integration/index.html new file mode 100644 index 0000000..cdbf7b6 --- /dev/null +++ b/knowledge/modules/1c-integration/index.html @@ -0,0 +1,281 @@ + + + + + + Интеграция с 1С | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Интеграция с 1С

+
+

Интеграция с 1С связывает учётный контур предприятия с задачами, агентами и рабочими модулями NODE.DC. 1С остаётся источником фактов, а NODE.DC добавляет операционный, аналитический и агентный слой поверх этих данных.

+

Какие данные подключаются

Раздел «Какие данные подключаются»
+
  • справочники, контрагенты, номенклатура и статусы;
  • документы, счета, акты, заявки и закупочные позиции;
  • остатки, движение товаров, складские события и производственные данные;
  • связанные задачи, согласования и внутренние комментарии.
+ +

Команда может использовать данные из 1С для сверки тендерных требований, контроля закупок, подготовки отчётов, запуска согласований, проверки остатков, анализа поставщиков и передачи результата в Ops.

+ +

1С агент помогает не просто показать данные, а ответить на рабочий вопрос: найти документ, объяснить статус, собрать выборку, подготовить задачу или подсказать следующий шаг. При этом действия выполняются в рамках прав доступа и фиксируются в операционном контуре.

+ +

Без интеграции учётная система часто остаётся отдельным окном. NODE.DC связывает её с процессами: данные становятся частью задач, отчётов, агентов и модулей, а не отдельной ручной проверкой.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/city-digital-twins/index.html b/knowledge/modules/city-digital-twins/index.html new file mode 100644 index 0000000..03a038e --- /dev/null +++ b/knowledge/modules/city-digital-twins/index.html @@ -0,0 +1,284 @@ + + + + + + Цифровые двойники городов | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Цифровые двойники городов

+
+

NODE.DC может работать как вход в цифровой двойник города или крупной территории. Пользователь получает единое пространственное окружение, где обзор, аналитика и инженерный разбор связаны геопривязкой, API и общим контекстом.

+ + +

3DGS-сцены дают быстрый вход внутрь локации: прогулку от третьего лица, совместное присутствие, наблюдение других участников и естественное понимание пространства.

+ +

Карта местности становится навигацией и агрегатором слоёв: транспорт, экология, инфраструктура, распределённые объекты, статусы и события.

+ +

BIM-модели, облака точек и технические материалы позволяют перейти к детальному анализу объекта, конструкции, помещений и инженерных систем.

+

Единый маршрут пользователя

Раздел «Единый маршрут пользователя»
+

Человек может начать с общего обзора, перейти к городской аналитике, затем открыть конкретный объект и посмотреть его инженерную структуру без разрыва пространственной логики.

+ +

Любое наблюдение, отклонение или задача из цифрового двойника может быть связано с карточкой в Ops, ответственным исполнителем, сроком, материалами и историей действий.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/construction-contour/index.html b/knowledge/modules/construction-contour/index.html new file mode 100644 index 0000000..6d7ea38 --- /dev/null +++ b/knowledge/modules/construction-contour/index.html @@ -0,0 +1,281 @@ + + + + + + Строительный контур | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Строительный контур

+
+

Строительный контур NODE.DC связывает проектные данные, площадку, подрядчиков, BIM-модели, полевые отчёты, дефекты, сроки и согласования в одной рабочей системе.

+ +
  • BIM-модели, чертежи, облака точек и фотофиксация;
  • задачи, замечания, дефекты и исполнительная документация;
  • подрядчики, ответственные, сроки и этапы работ;
  • полевые отчёты и мобильные сценарии инспекций;
  • связь с закупками, складами и финансовыми контурами.
+

Переход от модели к действию

Раздел «Переход от модели к действию»
+

Инженерная модель становится не отдельным файлом, а источником рабочих действий. Элемент модели можно связать с задачей, актом, замечанием, фотографией, сроком или ответственным исполнителем.

+

Контроль изменений

Раздел «Контроль изменений»
+

Для стройки важна история: что было запланировано, что изменилось, кто согласовал, какие материалы приложены и почему задача перешла в новый статус. Ops сохраняет эту цепочку в операционном контуре.

+

Агентное сопровождение

Раздел «Агентное сопровождение»
+

Агенты могут готовить сводки по дефектам, искать просрочки, сравнивать отчёты, собирать материалы к совещанию и подсвечивать зоны риска.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/drone-management/index.html b/knowledge/modules/drone-management/index.html new file mode 100644 index 0000000..9e2c082 --- /dev/null +++ b/knowledge/modules/drone-management/index.html @@ -0,0 +1,281 @@ + + + + + + Управление беспилотниками | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Управление беспилотниками

+
+

В NODE.DC может быть реализован интеграционный слой для работы с беспилотными решениями. Платформа становится цифровым контуром вокруг системы управления: собирает телеметрию, анализирует состояние IoT-компонентов, обрабатывает события и помогает оператору принимать решения.

+

Что контролирует платформа

Раздел «Что контролирует платформа»
+
  • состояние узлов, датчиков и исполнительных компонентов;
  • параметры движения, маршруты и эксплуатационные ограничения;
  • историю событий, уведомления и отклонения;
  • видеопотоки, классификацию объектов и признаки нештатных ситуаций.
+ +

Отдельное направление - анализ видеопотоков, оценка окружения, выявление препятствий, контроль состояния маршрута и подготовка кратких сводок для оператора.

+

Опыт и переносимость

Раздел «Опыт и переносимость»
+

Подход применим к разным автономным комплексам: курьерским решениям, гусеничным наземным платформам, распределённой технике и мониторинговым сценариям. В каждом случае NODE.DC отвечает за данные, события, наблюдаемость, интерфейс и агентное сопровождение.

+ +

Оператор видит не набор сырых сигналов, а понятную картину: что произошло, насколько это критично, какие действия доступны и какие данные уже собраны системой.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/enterprise-management/index.html b/knowledge/modules/enterprise-management/index.html new file mode 100644 index 0000000..5289b8a --- /dev/null +++ b/knowledge/modules/enterprise-management/index.html @@ -0,0 +1,281 @@ + + + + + + Управление предприятием | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Управление предприятием

+
+

Управленческий контур NODE.DC собирает в одном рабочем пространстве финансовые показатели, производственные статусы, распределённые объекты, логистику, поставщиков, отклонения и выводы агентных систем.

+

Единое экранное пространство

Раздел «Единое экранное пространство»
+

Вместо разрозненных отчётов формируется живая картина предприятия. Каждый блок интерфейса связан с конкретным объектом, процессом, узлом данных или рабочим полем.

+

Что можно выводить

Раздел «Что можно выводить»
+
  • финансовые и операционные показатели;
  • производственные статусы и риски;
  • состояние объектов, складов, техники и инфраструктуры;
  • логистику, поставщиков и закупочный поток;
  • аналитические сводки агентов и рекомендации по действиям.
+

Не витрина, а рабочая проекция

Раздел «Не витрина, а рабочая проекция»
+

Ценность режима в том, что он связан с действующей архитектурой процессов. Из показателя можно перейти в задачу, из задачи - в материалы, из материалов - в ответственного, а из отклонения - в сценарий реакции.

+ +

Руководитель видит не только итоговый отчёт, но и причинно-следственную цепочку: от источника события до текущего статуса и следующего действия.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/index.html b/knowledge/modules/index.html new file mode 100644 index 0000000..9a9d991 --- /dev/null +++ b/knowledge/modules/index.html @@ -0,0 +1,280 @@ + + + + + + Прикладные модули | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Прикладные модули

+
+

Прикладные модули NODE.DC закрывают конкретные функции предприятия: закупки, тендеры, 1С, производство, строительство, цифровые двойники, беспилотники, SEO и внутренние инструменты разработки.

+

Задача прикладного слоя

Раздел «Задача прикладного слоя»
+

Смысл модулей - адаптировать платформу под специфику компании. В одном контуре можно связать учётные системы, роли, задачи, файлы, инженерные данные, агентов, отчёты и интерфейсы без разрыва между приложениями.

+

Что можно добавить

Раздел «Что можно добавить»
+
  • модуль под отдельный отдел или процесс;
  • интеграцию с внешней системой, API, 1С, CRM или ERP;
  • операционный дашборд и рабочий интерфейс;
  • агента для поиска, анализа, сверки или подготовки материалов;
  • инструмент разработки и администрирования для внутренней команды.
+ +

Модуль не живёт отдельно. Он подключается к ролям, данным, задачам и сквозному ассистенту NODE.DC. Поэтому результат работы тендерного агента может попасть в Ops, данные из 1С могут участвовать в закупочной проверке, а инженерная модель может быть связана с задачей, отчётом и согласованием.

+

Смотреть прикладные модули

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/industrial-contour/index.html b/knowledge/modules/industrial-contour/index.html new file mode 100644 index 0000000..233315d --- /dev/null +++ b/knowledge/modules/industrial-contour/index.html @@ -0,0 +1,281 @@ + + + + + + Промышленный контур | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Промышленный контур

+
+

Промышленный контур NODE.DC помогает хранить и сопоставлять состояния производственных помещений, оборудования и инженерной инфраструктуры во времени. Это не только визуализация, а рабочий инструмент поддержки решений.

+

Цифровой архив состояний

Раздел «Цифровой архив состояний»
+

Регулярная фиксация состояния предприятия позволяет сравнивать цеха, оборудование и инженерные зоны за разные периоды. На этой базе можно выявлять деформации, износ, перегрузки и отклонения.

+ +
  • BIM-данные, облака точек и фотореалистичные слои;
  • паспорта помещений, цехов и оборудования;
  • связь с ERP/1С, складами и производственными статусами;
  • карта-навигация для сотрудников и подрядчиков;
  • история изменений и материалы модернизации.
+

Оптимизация процессов

Раздел «Оптимизация процессов»
+

Когда пространственные данные связаны с производственными узлами, легче находить причины износа, перегрузок и простоев. Решение принимается не по отдельной фотографии или таблице, а по контексту объекта.

+

Планирование модернизаций

Раздел «Планирование модернизаций»
+

Цифровой двойник фиксирует промежуточные состояния предприятия как живую историю объекта. Это снижает риск ошибок при проектировании, ремонте, реконструкции и внедрении нового оборудования.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/seo-module/index.html b/knowledge/modules/seo-module/index.html new file mode 100644 index 0000000..3051c55 --- /dev/null +++ b/knowledge/modules/seo-module/index.html @@ -0,0 +1,281 @@ + + + + + + SEO модуль | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

SEO модуль

+
+

SEO модуль NODE.DC строится как локальный редакторский контур для работы с существующими сайтами. Его задача - не автоматически публиковать тексты и не забивать страницу ключами, а дать управляемый процесс для редактора, дизайнера и SEO-специалиста.

+ +
  1. проект сканируется как папка сайта или рабочий источник;
  2. собирается индекс страниц, секций, медиа и технических файлов;
  3. выполняется baseline audit по структуре, заголовкам, описаниям, видимым текстам и медиа;
  4. из текста извлекаются смыслы, seed-запросы и потенциальные интенты;
  5. через Wordstat собираются реальные запросы и частотность;
  6. формируется карта ключей по секциям;
  7. редактор утверждает правки, проверяет переспам и применяет изменения через changeset.
+ +
  • title, description, h1/h2 и порядок заголовков;
  • видимый текст, секции и смысловые дубли;
  • изображения, видео, posters, alt, title, aria-label и контекст вокруг медиа;
  • риски переспама, конкурирующие смыслы, тон и ограничения по длине.
+

Почему не автопостинг

Раздел «Почему не автопостинг»
+

Модуль работает с рабочей копией текста. Исходный сайт не меняется до ручного Apply. Перед применением пользователь видит план, варианты правок, проверки качества и итоговый changeset.

+ +

Этот раздел Knowledge сам является хорошим кандидатом для SEO-модуля: каждая статья имеет отдельный URL, понятный заголовок, описание, структуру H2/H3 и может проходить через тот же редакторский контур.

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/modules/tender-agent/index.html b/knowledge/modules/tender-agent/index.html new file mode 100644 index 0000000..eda8deb --- /dev/null +++ b/knowledge/modules/tender-agent/index.html @@ -0,0 +1,282 @@ + + + + + + Тендерный агент | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Тендерный агент

+
+

Тендерный агент NODE.DC закрывает рабочий контур тендерного отдела. В одном интерфейсе объединяются поиск и агрегация закупок, первичный отбор, AI-анализ конкретных тендеров и передача материалов в смежные модули.

+ +
  • собирает и нормализует закупочный поток с тендерных площадок;
  • выделяет требования, ограничения, сроки, финансовые параметры и рисковые зоны;
  • сопоставляет условия закупки с внутренними данными компании;
  • готовит выжимки, чек-листы и материалы для ответственных специалистов;
  • передаёт задачи в юридический, закупочный или операционный контур.
+

Поведенческий поиск

Раздел «Поведенческий поиск»
+

Система учитывает действия специалиста: какие закупки он открывает, какие отклоняет, по каким признакам принимает решение. На этой базе формируются устойчивые паттерны отбора, а релевантные тендеры находятся точнее.

+ +

Специалист тендерного отдела остаётся оператором решения. Агент сокращает ручной перебор, готовит анализ и подсвечивает риски, но финальное решение, подача и коммуникация остаются под контролем команды.

+

Связь с платформой

Раздел «Связь с платформой»
+

Материалы тендера могут стать задачами в Ops, данными для юридического анализа, входом для 1С-интеграции или основанием для закупочной цепочки.

+

Посмотреть демо на лендинге

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/platform/engine/index.html b/knowledge/platform/engine/index.html new file mode 100644 index 0000000..c625156 --- /dev/null +++ b/knowledge/platform/engine/index.html @@ -0,0 +1,279 @@ + + + + + + Engine | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Engine

+

NODE.DC Engine - рабочий слой для проектирования и запуска процессов внутри платформы. Здесь создаются интеграции сервисов, обработчики данных, диспетчерские панели, сценарии роботизации и внутренние автоматизации для бизнеса.

+ +

Процессы собираются в узловом редакторе. Узлы описывают источники данных, преобразования, действия, визуализацию, проверки, внешние API и пользовательские интерфейсы. Такой подход позволяет быстро добавлять новые сценарии без переписывания всей системы.

+ +

Сценарий можно собрать вручную или с помощью агента: пользователь описывает задачу, а агент предлагает архитектуру решения, добавляет необходимые узлы и готовит схему для подключения к интерфейсу.

+

Что можно собирать

Раздел «Что можно собирать»
+
  • интеграции с 1С, CRM, ERP, API и webhooks;
  • операционные панели и диспетчерские интерфейсы;
  • потоки обработки документов, телеметрии и событий;
  • агентные цепочки для анализа, отбора, классификации и маршрутизации задач.
+ +

Результат работы Engine не остаётся схемой в редакторе. Он передаётся в операционный слой: создаёт задачи, обновляет статусы, прикладывает отчёты, запускает согласования или отдаёт данные в другой модуль.

Посмотреть демо


+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/platform/hub/index.html b/knowledge/platform/hub/index.html new file mode 100644 index 0000000..bad1757 --- /dev/null +++ b/knowledge/platform/hub/index.html @@ -0,0 +1,277 @@ + + + + + + Hub | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Hub

+

NODE.DC Hub - единая точка доступа к среде NODE.DC. Он показывает пользователю доступные приложения, рабочие пространства и сервисы платформы с учётом роли, группы и настроек конкретного контура.

+ +
  • собирает каталог приложений и микромодулей в одном интерфейсе;
  • управляет ролями, группами, отделами и правами доступа;
  • разделяет рабочие контуры для инженерных, финансовых, юридических, закупочных и производственных команд;
  • даёт администраторам понятную точку настройки платформы.
+ +

В корпоративном контуре администратор может назначать типовые конфигурации доступа для подразделений и отдельно расширять права конкретным пользователям. За счёт этого платформа не превращается в общий склад кнопок: каждый видит только те инструменты, которые относятся к его работе.

+

Связь с другими слоями

Раздел «Связь с другими слоями»
+

Из Hub пользователь переходит в Engine, Ops и прикладные приложения. Все переходы сохраняют контекст прав доступа, поэтому один и тот же модуль может выглядеть по-разному для руководителя проекта, инженера, закупщика или внешнего подрядчика.

Посмотреть демо

+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/platform/index.html b/knowledge/platform/index.html new file mode 100644 index 0000000..0888046 --- /dev/null +++ b/knowledge/platform/index.html @@ -0,0 +1,282 @@ + + + + + + NODE.DC | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

NODE.DC

+
+

NODE.DC - операционная платформа, которая связывает распределённые объекты, данные и действия в едином контуре управления. Она подключает физические источники, цифровые сервисы, бизнес-системы и прикладные модули, превращая разрозненные сигналы в воспроизводимые потоки событий.

+ +

В основе NODE.DC лежит модель объектов: сущности, паспорта, связи, состояния, события и допустимые действия. Эта структура даёт общую картину того, что происходит в компании, где это происходит, с чем связано и какие следующие действия доступны.

+ +
  • Hub отвечает за вход, роли, группы, рабочие пространства и каталог приложений.
  • Engine собирает процессы, интеграции, обработчики данных, интерфейсы и агентные сценарии.
  • Ops принимает задачи, статусы, отчёты, согласования и результаты работы модулей.
+

Агентный интеллектуальный слой

Раздел «Агентный интеллектуальный слой»
+

Поверх данных работает AIS - агентный слой платформы. Он анализирует историю и текущее состояние, выявляет отклонения, формирует объяснения, предлагает действия и запускает сценарии строго в рамках прав доступа и контекста.

+ +

NODE.DC полезен там, где важно быстро перейти от наблюдения к действию: в закупках, проектном офисе, инженерных данных, производстве, строительстве, цифровых двойниках, робототехнике и интеграциях с учётными системами.

+

Посмотреть платформу на лендинге

+
+
+
+
+
+
+
+
+
+ + + + + diff --git a/knowledge/platform/ops/index.html b/knowledge/platform/ops/index.html new file mode 100644 index 0000000..bd2c508 --- /dev/null +++ b/knowledge/platform/ops/index.html @@ -0,0 +1,279 @@ + + + + + + Ops | NODE.DC Knowledge + + + + + + + + + + + + + + + + + + + + + + + К содержанию +
+
+
+ +
+ + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+

Ops

+

NODE.DC Ops - операционный слой платформы. В него сводятся проекты, процессы, задачи, инженерные данные, статусы, отчёты, согласования и результаты работы прикладных контуров.

+

Единая рабочая среда

Раздел «Единая рабочая среда»
+

Ops объединяет людей, агентов и цифровые контуры предприятия. Задачи можно ставить, уточнять, декомпозировать, передавать между подразделениями, контролировать по этапам и связывать с результатами из других слоёв NODE.DC.

+

Ассистент внутри процесса

Раздел «Ассистент внутри процесса»
+

Сквозной ассистент работает не как справочная кнопка, а как интерфейс к рабочему контексту: задачам, проектным материалам, кодовой базе, инженерным данным, отчётам и действиям агентов. При наличии прав он может готовить материалы, создавать подзадачи и инициировать изменения в сервисах.

+

Форматы представления

Раздел «Форматы представления»
+
  • проектные дашборды и карточки задач;
  • календарные планы и таблицы;
  • графики, отчёты и рабочие области;
  • просмотр BIM, STEP, STL, 3DGS, LAS и других инженерных материалов.
+ +

Ops делает работу наблюдаемой: каждое действие, решение и отклонение получает место в общем контуре, а не теряется в чатах, таблицах и разрозненных файлах.

Посмотреть демо

+
+
+
+
+
+
+
+
+ + + + + diff --git a/package.json b/package.json index decbd9e..de573aa 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "extract:home": "node tools/extract-home-blocks.mjs", "render:home": "node tools/render-home.mjs", + "render:knowledge": "node tools/render-knowledge.mjs", "admin": "node tools/admin-server.mjs" } } diff --git a/sitemap.xml b/sitemap.xml index ac85f22..ca82390 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -5,4 +5,79 @@ weekly 1.0 + + https://nodedc.dctouch.ru/knowledge/ + weekly + 0.8 + + + https://nodedc.dctouch.ru/knowledge/platform/ + weekly + 0.7 + + + https://nodedc.dctouch.ru/knowledge/platform/hub/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/platform/engine/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/platform/ops/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/agent-context/ + weekly + 0.7 + + + https://nodedc.dctouch.ru/knowledge/modules/ + weekly + 0.7 + + + https://nodedc.dctouch.ru/knowledge/modules/tender-agent/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/1c-integration/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/drone-management/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/city-digital-twins/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/industrial-contour/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/enterprise-management/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/construction-contour/ + weekly + 0.6 + + + https://nodedc.dctouch.ru/knowledge/modules/seo-module/ + weekly + 0.6 + diff --git a/templates/pages/home/blocks/project-video-first.html b/templates/pages/home/blocks/project-video-first.html index 89220f6..2a3bab7 100644 --- a/templates/pages/home/blocks/project-video-first.html +++ b/templates/pages/home/blocks/project-video-first.html @@ -1 +1 @@ -
{{text:content.windowTitle}}

{{text:content.heading.muted}}{{text:content.heading.main}}

{{html:content.introHtml}}
{{text:content.features.0.eyebrow}}
{{html:content.features.0.bodyHtml}}
{{text:content.features.1.eyebrow}}
{{html:content.features.1.bodyHtml}}
{{text:content.features.2.eyebrow}}
{{html:content.features.2.bodyHtml}}
{{text:content.features.3.eyebrow}}
{{html:content.features.3.bodyHtml}}
+
{{text:content.windowTitle}}

{{text:content.heading.muted}}{{text:content.heading.main}}

{{html:content.introHtml}}
{{text:content.features.0.eyebrow}}
{{html:content.features.0.bodyHtml}}
{{text:content.features.1.eyebrow}}
{{html:content.features.1.bodyHtml}}
{{text:content.features.2.eyebrow}}
{{html:content.features.2.bodyHtml}}
{{text:content.features.3.eyebrow}}
{{html:content.features.3.bodyHtml}}
diff --git a/tools/admin-server.mjs b/tools/admin-server.mjs index d276445..1cddb0e 100644 --- a/tools/admin-server.mjs +++ b/tools/admin-server.mjs @@ -10,6 +10,7 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const port = Number(process.env.PORT || 8090); const host = process.env.HOST || "127.0.0.1"; const pagePath = join(repoRoot, "content", "pages", "home.json"); +const knowledgePath = join(repoRoot, "content", "knowledge.json"); const blockTemplatesPath = join(repoRoot, "content", "block-templates", "home.json"); const assetRoot = join(repoRoot, "assets"); const assetTrashRoot = join(assetRoot, ".trash"); @@ -169,6 +170,45 @@ function sendJson(res, status, payload) { send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8"); } +function allPageBlocks(page) { + return (page.regions || []).flatMap((region) => region.blocks || []); +} + +function normalizeLandingTarget(value) { + const raw = String(value || "").trim(); + if (!raw || /^javascript:/i.test(raw) || /^(https?:)?\/\//i.test(raw) || /^(mailto:|tel:)/i.test(raw)) return ""; + const hashValue = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw; + return hashValue + .replace(/^\/+/, "") + .replace(/^index\.html#?/i, "") + .replace(/^#+/, "") + .trim(); +} + +function landingTargetsForPage(page) { + return allPageBlocks(page) + .filter((block) => block.enabled !== false) + .map((block) => ({ + id: block.id, + label: block.adminLabel || block.id, + template: block.template || block.type || "", + target: normalizeLandingTarget(block.anchor), + })) + .filter((item) => item.target); +} + +function validateLandingTargets(page) { + const targets = new Map(); + + for (const item of landingTargetsForPage(page)) { + if (targets.has(item.target)) { + const first = targets.get(item.target); + throw new Error(`Target "${item.target}" уже используется в блоках "${first.label}" и "${item.label}"`); + } + targets.set(item.target, item); + } +} + async function readBodyBuffer(req) { const chunks = []; for await (const chunk of req) { @@ -377,12 +417,15 @@ function collectAssetRefsFromText(text) { } async function buildMediaUsage() { - const [pageSource, indexSource] = await Promise.all([ + const [pageSource, knowledgeSource, indexSource, knowledgeIndexSource] = await Promise.all([ readFile(pagePath, "utf8").catch(() => "{}"), + readFile(knowledgePath, "utf8").catch(() => "{}"), readFile(join(repoRoot, "index.html"), "utf8").catch(() => ""), + readFile(join(repoRoot, "knowledge", "index.html"), "utf8").catch(() => ""), ]); const pageRefs = collectAssetRefsFromValue(JSON.parse(pageSource)); - const renderedRefs = collectAssetRefsFromText(indexSource); + collectAssetRefsFromValue(JSON.parse(knowledgeSource), pageRefs); + const renderedRefs = collectAssetRefsFromText(`${indexSource}\n${knowledgeIndexSource}`); return { pageRefs, renderedRefs }; } @@ -1389,14 +1432,19 @@ function safePublicPath(urlPath) { } async function serveStatic(req, res) { - const filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname); + let filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname); if (!filePath) { send(res, 403, "Forbidden"); return; } try { - const fileStat = await stat(filePath); + let fileStat = await stat(filePath); + if (fileStat.isDirectory()) { + filePath = join(filePath, "index.html"); + fileStat = await stat(filePath); + } + if (!fileStat.isFile()) { send(res, 404, "Not found"); return; @@ -1462,6 +1510,18 @@ const server = createServer(async (req, res) => { return; } + if (req.method === "GET" && url.pathname === "/api/knowledge") { + send(res, 200, await readFile(knowledgePath, "utf8"), "application/json; charset=utf-8"); + return; + } + + if (req.method === "GET" && url.pathname === "/api/landing-targets") { + const page = JSON.parse(await readFile(pagePath, "utf8")); + validateLandingTargets(page); + sendJson(res, 200, { ok: true, targets: landingTargetsForPage(page) }); + return; + } + if (req.method === "GET" && url.pathname === "/api/block-templates/home") { send(res, 200, await readFile(blockTemplatesPath, "utf8"), "application/json; charset=utf-8"); return; @@ -1490,17 +1550,32 @@ const server = createServer(async (req, res) => { if (req.method === "PUT" && url.pathname === "/api/page/home") { const body = await readBody(req); const parsed = JSON.parse(body); + validateLandingTargets(parsed); await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`); sendJson(res, 200, { ok: true }); return; } + if (req.method === "PUT" && url.pathname === "/api/knowledge") { + const body = await readBody(req); + const parsed = JSON.parse(body); + await writeFile(knowledgePath, `${JSON.stringify(parsed, null, 2)}\n`); + sendJson(res, 200, { ok: true }); + return; + } + if (req.method === "POST" && url.pathname === "/api/render/home") { const result = await runNodeScript("render-home.mjs"); sendJson(res, 200, { ok: true, ...result }); return; } + if (req.method === "POST" && url.pathname === "/api/render/knowledge") { + const result = await runNodeScript("render-knowledge.mjs"); + sendJson(res, 200, { ok: true, ...result }); + return; + } + if (req.method === "POST" && url.pathname === "/api/upload/asset") { const result = isMultipartRequest(req) ? await saveUploadedAssetBuffer(parseMultipartUpload(req, await readBodyBuffer(req))) diff --git a/tools/render-home.mjs b/tools/render-home.mjs index b956cc8..1383c40 100644 --- a/tools/render-home.mjs +++ b/tools/render-home.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const pagePath = join(repoRoot, "content", "pages", "home.json"); +const knowledgePath = join(repoRoot, "content", "knowledge.json"); const robotsPath = join(repoRoot, "robots.txt"); const sitemapPath = join(repoRoot, "sitemap.xml"); const templateRoot = join(repoRoot, "templates", "pages", "home"); @@ -193,6 +194,115 @@ function escapeXml(value) { .replaceAll("'", "'"); } +function stripHtml(value) { + return String(value ?? "") + .replace(/<[^>]*>/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function normalizeLandingTarget(value) { + const raw = String(value || "").trim(); + if (!raw || /^javascript:/i.test(raw) || isExternalHref(raw)) return ""; + const hashValue = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw; + return hashValue + .replace(/^\/+/, "") + .replace(/^index\.html#?/i, "") + .replace(/^#+/, "") + .trim(); +} + +function validateLandingTargets(model) { + const targets = new Map(); + + for (const block of allBlocks(model)) { + if (block.enabled === false) continue; + const target = normalizeLandingTarget(block.anchor); + if (!target) continue; + + if (targets.has(target)) { + const first = targets.get(target); + throw new Error( + `Duplicate landing target "${target}" in blocks "${first.adminLabel || first.id}" and "${block.adminLabel || block.id}"`, + ); + } + + targets.set(target, block); + } +} + +function slugify(value, fallback = "section") { + const map = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "c", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya", + }; + const safe = String(value ?? "") + .toLowerCase() + .replace(/[а-яё]/g, (char) => map[char] ?? "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + + return safe || fallback; +} + +function collectKnowledgeUrls(source, canonical) { + let model; + try { + model = JSON.parse(source); + } catch { + return []; + } + + const basePath = String(model.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge"; + const urls = [new URL(`${basePath}/`, canonical).href]; + + function walk(nodes = [], parentSegments = []) { + nodes.forEach((node, index) => { + if (node.enabled === false) return; + const segment = slugify(node.slug || node.title || node.id, `page-${index + 1}`); + const segments = [...parentSegments, segment]; + if (node.useContent !== false && stripHtml(node.bodyHtml).length > 0) { + urls.push(new URL(`${basePath}/${segments.join("/")}/`, canonical).href); + } + if (Array.isArray(node.children)) walk(node.children, segments); + }); + } + + walk(model.tree || []); + return urls; +} + function canonicalUrl(model) { const fallback = "https://nodedc.dctouch.ru/"; const value = String(model.seo?.canonicalUrl || fallback).trim() || fallback; @@ -208,6 +318,8 @@ function canonicalUrl(model) { async function writeSeoFiles(model) { const canonical = canonicalUrl(model); const sitemapUrl = new URL("sitemap.xml", canonical).href; + const knowledgeSource = await readFile(knowledgePath, "utf8").catch(() => ""); + const knowledgeUrls = collectKnowledgeUrls(knowledgeSource, canonical); const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n"); const sitemap = ` @@ -216,6 +328,15 @@ async function writeSeoFiles(model) { weekly 1.0 +${knowledgeUrls + .map( + (url) => ` + ${escapeXml(url)} + weekly + 0.7 + `, + ) + .join("\n")} `; @@ -306,6 +427,16 @@ function scopeDuplicateIds(html, scope) { return scoped; } +function htmlHasId(html, id) { + return new RegExp(`\\sid="${escapeRegExp(id)}"`).test(html); +} + +function withLandingTarget(html, block) { + const target = normalizeLandingTarget(block.anchor); + if (!target || htmlHasId(html, target)) return html; + return `${html}`; +} + async function renderBlock(block, renderCounts) { if (block.enabled === false) { return ""; @@ -316,12 +447,9 @@ async function renderBlock(block, renderCounts) { renderCounts.set(templateName, renderCount + 1); const html = renderTemplate(await readTemplate(templateName), block); + const rendered = renderCount === 0 && block.scopeIds !== true ? html : scopeDuplicateIds(html, block.id); - if (renderCount === 0 && block.scopeIds !== true) { - return html; - } - - return scopeDuplicateIds(html, block.id); + return withLandingTarget(rendered, block); } async function renderRegion(region) { @@ -367,6 +495,7 @@ async function renderSections() { applySeoDefaults(page); applySharedDemoRequest(page); applyRenderableLinks(page); +validateLandingTargets(page); let output = renderTemplate(shell, { id: page.slug, ...page }); diff --git a/tools/render-knowledge.mjs b/tools/render-knowledge.mjs new file mode 100644 index 0000000..8f90134 --- /dev/null +++ b/tools/render-knowledge.mjs @@ -0,0 +1,769 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const knowledgePath = join(repoRoot, "content", "knowledge.json"); +const homePath = join(repoRoot, "content", "pages", "home.json"); +const robotsPath = join(repoRoot, "robots.txt"); +const sitemapPath = join(repoRoot, "sitemap.xml"); + +const knowledge = JSON.parse(await readFile(knowledgePath, "utf8")); +const home = JSON.parse(await readFile(homePath, "utf8").catch(() => "{}")); + +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + +function escapeAttr(value) { + return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'"); +} + +function escapeXml(value) { + return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'"); +} + +function escapeJson(value) { + return JSON.stringify(value).replaceAll("/gi, " ") + .replace(//gi, " ") + .replace(/<[^>]*>/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function slugify(value, fallback = "section") { + const map = { + а: "a", + б: "b", + в: "v", + г: "g", + д: "d", + е: "e", + ё: "e", + ж: "zh", + з: "z", + и: "i", + й: "y", + к: "k", + л: "l", + м: "m", + н: "n", + о: "o", + п: "p", + р: "r", + с: "s", + т: "t", + у: "u", + ф: "f", + х: "h", + ц: "c", + ч: "ch", + ш: "sh", + щ: "sch", + ъ: "", + ы: "y", + ь: "", + э: "e", + ю: "yu", + я: "ya", + }; + const transliterated = String(value ?? "") + .toLowerCase() + .replace(/[а-яё]/g, (char) => map[char] ?? "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + + return transliterated || fallback; +} + +function normalizePathSegment(node, index) { + return slugify(node.slug || node.title || node.id, `page-${index + 1}`); +} + +function publicAssetUrl(value) { + const url = String(value || "").trim(); + if (!url) return ""; + if (/^(https?:|data:|blob:|\/)/i.test(url)) return url; + if (url.startsWith("./")) return `/${url.slice(2)}`; + return `/${url.replace(/^\/+/, "")}`; +} + +function normalizeContentAssets(html) { + return String(html || "") + .replace(/\s(src|href)="\.\/assets\//g, ' $1="/assets/') + .replace(/\s(src|href)="assets\//g, ' $1="/assets/'); +} + +function decodeShortcodeAttr(value) { + return String(value ?? "") + .replaceAll(""", '"') + .replaceAll(""", '"') + .replaceAll("'", "'") + .replaceAll("'", "'") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("&", "&"); +} + +function parseShortcodeAttrs(value) { + const attrs = {}; + String(value || "").replace( + /([a-zA-Z][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/g, + (match, key, doubleQuoted, singleQuoted, bare) => { + attrs[key.toLowerCase()] = decodeShortcodeAttr(doubleQuoted ?? singleQuoted ?? bare ?? ""); + return match; + }, + ); + return attrs; +} + +function normalizeKnowledgeButtonTarget(value) { + return String(value || "") + .trim() + .replace(/^#+/, "") + .replace(/^index\.html#/i, "") + .replace(/^\/?#/, ""); +} + +function normalizeKnowledgeButtonLink(value) { + return (String(value ?? "").trim() || "/").replace(/^\/+(https?:\/\/)/i, "$1").replace(/^\/+((?:mailto|tel):)/i, "$1"); +} + +function allHomeBlocks() { + return (home.regions || []).flatMap((region) => region.blocks || []); +} + +function landingBlockById(id) { + const blockId = String(id || "").trim(); + if (!blockId) return null; + return allHomeBlocks().find((block) => block.id === blockId && block.enabled !== false) || null; +} + +function landingTargetForAttrs(attrs) { + const blockId = attrs.block || attrs["block-id"] || attrs.blockid || attrs.landingblock || attrs["landing-block"]; + const blockTarget = normalizeKnowledgeButtonTarget(landingBlockById(blockId)?.anchor || ""); + return blockTarget || normalizeKnowledgeButtonTarget(attrs.target || attrs.anchor || ""); +} + +function knowledgeButtonHref(link, attrs) { + const anchor = landingTargetForAttrs(attrs); + const rawLink = normalizeKnowledgeButtonLink(link); + if (!anchor) return rawLink; + const base = rawLink.split("#")[0] || "/"; + if (/^index\.html$/i.test(base)) return `/#${anchor}`; + return `${base}#${anchor}`; +} + +function renderKnowledgeShortcodes(html) { + return String(html || "").replace(/\[button\b([^\]]*)\]/gi, (match, rawAttrs) => { + const attrs = parseShortcodeAttrs(rawAttrs); + const label = attrs.name || attrs.label || attrs.text || "Посмотреть"; + const href = knowledgeButtonHref(attrs.link || attrs.href || "/", attrs); + return `${escapeHtml(label)}`; + }); +} + +function canonicalHome() { + const fallback = "https://nodedc.dctouch.ru/"; + const value = String(home.seo?.canonicalUrl || fallback).trim() || fallback; + + try { + return new URL(value).href; + } catch { + return fallback; + } +} + +function canonicalBase() { + const url = new URL(canonicalHome()); + return `${url.origin}/`; +} + +function knowledgeBasePath() { + return String(knowledge.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge"; +} + +function nodeChildren(node) { + return Array.isArray(node.children) ? node.children : []; +} + +function nodeHasPage(node) { + return node?.enabled !== false && node.useContent !== false && stripHtml(renderKnowledgeShortcodes(node.bodyHtml || "")).length > 0; +} + +function walkNodes(nodes = knowledge.tree || [], parentSegments = [], parents = [], result = []) { + nodes.forEach((node, index) => { + if (node.enabled === false) return; + const segment = normalizePathSegment(node, index); + const segments = [...parentSegments, segment]; + const record = { + node, + parents, + segment, + segments, + urlPath: `/${knowledgeBasePath()}/${segments.join("/")}/`, + }; + result.push(record); + walkNodes(nodeChildren(node), segments, [...parents, record], result); + }); + + return result; +} + +function pageRecords() { + return walkNodes().filter((record) => nodeHasPage(record.node)); +} + +function currentAncestorIds(record) { + return new Set([record?.node?.id, ...(record?.parents || []).map((parent) => parent.node.id)].filter(Boolean)); +} + +function caretIcon(className = "caret astro-j63poqv6 astro-fr7meb3z", size = "1.25rem") { + return ``; +} + +function searchIcon() { + return ''; +} + +function anchorIcon(id, title) { + return `Раздел «${escapeHtml(title)}»`; +} + +function headingId(rawTitle, usedIds) { + const base = slugify(stripHtml(rawTitle), "section"); + let id = base; + let index = 2; + while (usedIds.has(id)) { + id = `${base}-${index}`; + index += 1; + } + usedIds.add(id); + return id; +} + +function prepareArticleHtml(html) { + const usedIds = new Set(); + const toc = []; + const normalized = renderKnowledgeShortcodes(normalizeContentAssets(html)).replace(/]*)>([\s\S]*?)<\/h\1>/gi, (match, level, attrs, inner) => { + const existingId = /\sid=(["'])(.*?)\1/i.exec(attrs); + const id = existingId?.[2] || headingId(inner, usedIds); + const title = stripHtml(inner); + if (existingId) usedIds.add(id); + toc.push({ level: Number(level), id, title }); + const nextAttrs = existingId ? attrs : `${attrs} id="${escapeAttr(id)}"`; + return `
${inner}${anchorIcon(id, title)}
`; + }); + + return { html: normalized, toc }; +} + +function renderNavRecord(record, currentIds, currentId, restoreCounter) { + const children = nodeChildren(record.node) + .map((child, index) => { + if (child.enabled === false) return ""; + const childSegment = normalizePathSegment(child, index); + return renderNavRecord( + { + node: child, + parents: [...record.parents, record], + segment: childSegment, + segments: [...record.segments, childSegment], + urlPath: `/${knowledgeBasePath()}/${[...record.segments, childSegment].join("/")}/`, + }, + currentIds, + currentId, + restoreCounter, + ); + }) + .filter(Boolean) + .join(""); + const title = escapeHtml(record.node.title || record.node.id || "Материал"); + const isCurrent = record.node.id === currentId; + const currentAttr = isCurrent ? ' aria-current="page"' : ""; + + if (!children) { + const largeClass = record.parents.length === 0 ? "large " : ""; + const href = nodeHasPage(record.node) ? record.urlPath : "#"; + return `
  • ${title}
  • `; + } + + const restoreIndex = restoreCounter.value; + restoreCounter.value += 1; + const open = currentIds.has(record.node.id) || record.parents.length === 0; + const label = nodeHasPage(record.node) + ? `${title}` + : `${title}`; + + return `
  • ${label}${caretIcon()}
      ${children}
  • `; +} + +function renderSidebar(currentRecord) { + const currentIds = currentAncestorIds(currentRecord); + const currentId = currentRecord?.node?.id || ""; + const restoreCounter = { value: 0 }; + const baseActive = currentRecord ? "" : ' aria-current="page"'; + const items = (knowledge.tree || []) + .map((node, index) => { + if (node.enabled === false) return ""; + const segment = normalizePathSegment(node, index); + return renderNavRecord( + { + node, + parents: [], + segment, + segments: [segment], + urlPath: `/${knowledgeBasePath()}/${segment}/`, + }, + currentIds, + currentId, + restoreCounter, + ); + }) + .filter(Boolean) + .join(""); + + return ``; +} + +function menuIcons() { + return ``; +} + +function renderTocItems(toc, mobile = false) { + const items = [{ level: 2, id: "_top", title: "Обзор" }, ...toc]; + return items + .map((item) => { + const depth = Math.max(0, item.level - 2); + return `
  • ${escapeHtml(item.title)}
  • `; + }) + .join(""); +} + +function renderRightSidebar(toc) { + const items = renderTocItems(toc); + const mobileItems = renderTocItems(toc, true); + return ``; +} + +function renderThemeIconsTemplate() { + return ``; +} + +function renderThemeSelect() { + return ``; +} + +function themeProviderScript() { + const defaultTheme = escapeAttr(knowledge.settings?.themeDefault || "dark"); + return ``; +} + +function interactionScript() { + return ``; +} + +function renderHeader(searchPlaceholder) { + const logoSrc = publicAssetUrl(home.staticElements?.content?.navigation?.logoSrc || home.staticElements?.content?.assets?.appIconSrc || ""); + const logoAlt = home.staticElements?.content?.navigation?.logoAlt || "NODE.DC"; + const brand = knowledge.settings?.navTitle || knowledge.title || "База знаний"; + const headerHref = knowledge.settings?.homeHref || "/"; + + return `
    +
    + +
    + + + + +
    +
    + + + ${renderThemeSelect()} +
    +
    +
    `; +} + +function pageTitle(record = null) { + if (!record) return knowledge.title || "База знаний"; + return record.node.seo?.title || record.node.title || "Материал"; +} + +function visibleTitle(record = null) { + if (!record) return knowledge.title || "База знаний"; + return record.node.title || "Материал"; +} + +function pageDescription(record = null) { + if (!record) return knowledge.settings?.description || ""; + return record.node.seo?.description || stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml)).slice(0, 180); +} + +function canonicalForPath(path) { + return new URL(path.replace(/^\//, ""), canonicalBase()).href; +} + +function renderIndexCards(records) { + const topPages = records.filter((record) => record.parents.length <= 1).slice(0, 12); + if (!topPages.length) return ""; + return `

    Материалы

    `; +} + +function renderContent(record, bodyHtml) { + const title = visibleTitle(record); + return `
    +
    +
    +
    +

    ${escapeHtml(title)}

    +
    ${bodyHtml}
    +
    +
    +
    +
    +
    `; +} + +function renderPage({ record = null, bodyHtml, toc = [], searchRecords }) { + const title = pageTitle(record); + const description = pageDescription(record); + const path = record?.urlPath || `/${knowledgeBasePath()}/`; + const canonical = canonicalForPath(path); + const searchPlaceholder = knowledge.settings?.searchPlaceholder || "Поиск"; + + return ` + + + + + ${escapeHtml(title)} + + + + + + + + + + + + + + ${themeProviderScript()} + ${renderThemeIconsTemplate()} + + + + + + + + К содержанию +
    + ${renderHeader(searchPlaceholder)} + ${renderSidebar(record)} +
    +
    + ${renderRightSidebar(toc)} + ${renderContent(record, bodyHtml)} +
    +
    +
    + + ${interactionScript()} + ${searchScript()} + + +`; +} + +function searchScript() { + return ``; +} + +async function writePage(urlPath, html) { + const outputDir = join(repoRoot, urlPath.replace(/^\/+|\/+$/g, "")); + await mkdir(outputDir, { recursive: true }); + await writeFile(join(outputDir, "index.html"), html); +} + +async function writeSeoFiles(records) { + const sitemapUrls = [ + { loc: canonicalHome(), priority: "1.0" }, + { loc: canonicalForPath(`/${knowledgeBasePath()}/`), priority: "0.8" }, + ...records.map((record) => ({ loc: canonicalForPath(record.urlPath), priority: record.parents.length ? "0.6" : "0.7" })), + ]; + const sitemapUrl = new URL("sitemap.xml", canonicalHome()).href; + const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n"); + const sitemap = ` + +${sitemapUrls + .map( + (item) => ` + ${escapeXml(item.loc)} + weekly + ${item.priority} + `, + ) + .join("\n")} + +`; + await writeFile(robotsPath, robots); + await writeFile(sitemapPath, sitemap); +} + +const records = pageRecords(); +const searchRecords = records.map((record) => ({ + title: record.node.title || "", + description: pageDescription(record), + url: record.urlPath, + text: stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml || "")), +})); + +const indexIntro = normalizeContentAssets( + knowledge.introHtml || + `

    ${escapeHtml(knowledge.settings?.description || "Материалы NODE.DC для пользователей и поисковых систем.")}

    `, +); +const indexPrepared = prepareArticleHtml(`${indexIntro}${renderIndexCards(records)}`); +await writePage(`/${knowledgeBasePath()}/`, renderPage({ bodyHtml: indexPrepared.html, toc: indexPrepared.toc, searchRecords })); + +for (const record of records) { + const prepared = prepareArticleHtml(record.node.bodyHtml || ""); + await writePage(record.urlPath, renderPage({ record, bodyHtml: prepared.html, toc: prepared.toc, searchRecords })); +} + +await writeSeoFiles(records); +console.log(`Rendered ${records.length + 1} knowledge pages -> /${knowledgeBasePath()}/`);