diff --git a/admin/admin.css b/admin/admin.css index 613862e..49b60ea 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -400,6 +400,7 @@ p { .breadcrumb, .panel-note, .field-path, +.media-upload-hint, .block-meta, .group-desc, .status, @@ -1103,6 +1104,13 @@ textarea { line-height: 1.42; } +.editor-error-note { + border-radius: var(--launcher-radius-control); + background: rgba(255, 116, 116, 0.09); + color: #ffd8d8; + padding: 1rem; +} + .service-media-field { min-width: 0; } @@ -1213,14 +1221,35 @@ textarea { height: 2.78rem; place-items: center; overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.16); border-radius: var(--launcher-radius-circle); - background: rgba(255, 255, 255, 0.055); + background: + linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%), + linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%), + rgba(18, 18, 20, 0.92); + background-position: + 0 0, + 6px 6px, + 0 0; + background-size: + 12px 12px, + 12px 12px, + auto; color: rgba(255, 255, 255, 0.62); font-size: 0.78rem; text-transform: none; } -.service-media-preview img, +.service-media-preview img { + position: absolute; + inset: 0.28rem; + display: block; + width: calc(100% - 0.56rem); + height: calc(100% - 0.56rem); + border-radius: inherit; + object-fit: contain; +} + .service-media-preview video { position: absolute; inset: 0; @@ -1231,6 +1260,28 @@ textarea { object-fit: cover; } +.service-media-preview.is-solid-dark { + border-color: rgba(255, 255, 255, 0.34); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.08), + 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.media-upload-hint { + font-size: 0.72rem; + font-weight: 720; + line-height: 1.32; + text-transform: none; +} + +.media-upload-hint:empty { + display: none; +} + +.media-upload-hint:not(:empty) { + color: rgba(255, 228, 170, 0.9); +} + .media-upload-error { color: #ffd2d2; font-size: 0.72rem; diff --git a/admin/admin.js b/admin/admin.js index 7b6d88a..3d501f7 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -362,6 +362,12 @@ function mediaKindFromValue(value) { return null; } +function shouldWarnAboutSolidLogoBackground(editableField) { + const path = String(editableField?.path || "").toLowerCase(); + const label = String(editableField?.label || "").toLowerCase(); + return /logo|icon|wordmark|логотип|икон/.test(`${path} ${label}`); +} + function uploadBucketForField(block, editableField) { const path = editableField.path.toLowerCase(); const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase(); @@ -773,6 +779,19 @@ function setStructuralControlsDisabled(disabled) { } } +function renderEditorError(error) { + console.error(error); + el.empty.classList.add("hidden"); + el.form.classList.remove("hidden"); + el.contentFields.innerHTML = ""; + + const note = document.createElement("div"); + note.className = "empty-note editor-error-note"; + note.textContent = `Редактор не смог отрисовать поля блока: ${error.message}`; + el.contentFields.append(note); + setStatus(`Ошибка редактора: ${error.message}`); +} + function renderEditor() { const region = activeRegion(); const block = activeBlock(); @@ -801,13 +820,24 @@ function renderEditor() { el.enabled.disabled = isStatic; el.enabled.closest(".toggle-row").hidden = isStatic; setStructuralControlsDisabled(isStatic); - renderContentFields(block); + + try { + renderContentFields(block); + } catch (error) { + renderEditorError(error); + } + el.json.value = JSON.stringify(block, null, 2); } function renderAll() { - renderRegions(); - renderEditor(); + try { + renderRegions(); + renderEditor(); + } catch (error) { + console.error(error); + setStatus(`Ошибка рендера админки: ${error.message}`); + } } function syncBlockFromFields() { @@ -840,8 +870,62 @@ function groupEditableFields(block) { return groups; } -function renderMediaPreview(preview, value) { +function resetMediaPreview(preview, hint) { + preview.classList.remove("is-image", "is-video", "is-solid-dark"); preview.innerHTML = ""; + preview.title = ""; + if (hint) hint.textContent = ""; +} + +function analyzeLogoPreview(image, preview, hint, editableField) { + if (!hint || !shouldWarnAboutSolidLogoBackground(editableField)) return; + + try { + const sampleSize = 24; + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d", { willReadFrequently: true }); + if (!context) return; + + canvas.width = sampleSize; + canvas.height = sampleSize; + context.drawImage(image, 0, 0, sampleSize, sampleSize); + const data = context.getImageData(0, 0, sampleSize, sampleSize).data; + let opaque = 0; + let edgeOpaque = 0; + let edgeDark = 0; + + for (let y = 0; y < sampleSize; y += 1) { + for (let x = 0; x < sampleSize; x += 1) { + const offset = (y * sampleSize + x) * 4; + const alpha = data[offset + 3]; + if (alpha < 240) continue; + + opaque += 1; + const isEdge = x <= 1 || y <= 1 || x >= sampleSize - 2 || y >= sampleSize - 2; + const isDark = data[offset] + data[offset + 1] + data[offset + 2] < 66; + + if (isEdge) { + edgeOpaque += 1; + if (isDark) edgeDark += 1; + } + } + } + + const total = sampleSize * sampleSize; + const edgeRatio = edgeOpaque ? edgeDark / edgeOpaque : 0; + const opaqueRatio = opaque / total; + + if (opaqueRatio > 0.92 && edgeRatio > 0.82) { + preview.classList.add("is-solid-dark"); + hint.textContent = "Файл с непрозрачным тёмным фоном. Для логотипа на тёмном интерфейсе лучше PNG/SVG с прозрачностью."; + } + } catch { + // External images can be blocked by canvas CORS. Preview still works, just without diagnostics. + } +} + +function renderMediaPreview(preview, value, hint = null, editableField = null) { + resetMediaPreview(preview, hint); const src = adminPreviewUrl(value); const kind = mediaKindFromValue(value); @@ -853,6 +937,7 @@ function renderMediaPreview(preview, value) { if (kind === "video") { const video = document.createElement("video"); + preview.classList.add("is-video"); video.src = src; video.autoplay = true; video.loop = true; @@ -864,12 +949,15 @@ function renderMediaPreview(preview, value) { if (kind === "image") { const image = document.createElement("img"); + preview.classList.add("is-image"); image.src = src; image.alt = ""; image.loading = "lazy"; + image.addEventListener("load", () => analyzeLogoPreview(image, preview, hint, editableField)); image.addEventListener("error", () => { - preview.innerHTML = ""; + resetMediaPreview(preview, hint); preview.textContent = "FILE"; + if (hint) hint.textContent = "Превью не загрузилось. Проверьте путь или внешний URL."; }); preview.append(image); return; @@ -878,13 +966,13 @@ function renderMediaPreview(preview, value) { preview.textContent = "FILE"; } -function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, value }) { +function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, hint, value }) { hiddenInput.value = value; urlInput.value = value; fileName.textContent = truncateText(fileNameFromUrl(value), 28); fileName.title = fileNameFromUrl(value); setByPath(block, editableField.path, value); - renderMediaPreview(preview, value); + renderMediaPreview(preview, value, hint, editableField); if (activeBlock() === block) { el.json.value = JSON.stringify(block, null, 2); @@ -911,6 +999,7 @@ function renderMediaField({ block, editableField, grid }) { const urlSourceButton = document.createElement("button"); const preview = document.createElement("div"); const path = document.createElement("span"); + const hint = document.createElement("span"); const error = document.createElement("span"); const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`; let source = /^(https?:)?\/\//i.test(value) ? "url" : "file"; @@ -971,10 +1060,11 @@ function renderMediaField({ block, editableField, grid }) { sourceSwitch.append(fileSourceButton, urlSourceButton); preview.className = "service-media-preview"; - renderMediaPreview(preview, value); + renderMediaPreview(preview, value, hint, editableField); path.className = "field-path"; path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`; + hint.className = "media-upload-hint"; error.className = "media-upload-error"; fileSourceButton.addEventListener("click", () => setSource("file")); @@ -991,6 +1081,7 @@ function renderMediaField({ block, editableField, grid }) { urlInput, fileName, preview, + hint, value: urlInput.value, }); }); @@ -1027,6 +1118,7 @@ function renderMediaField({ block, editableField, grid }) { urlInput, fileName, preview, + hint, value: payload.url, }); setSource("file"); @@ -1042,7 +1134,7 @@ function renderMediaField({ block, editableField, grid }) { setSource(source); control.append(fileControl, urlInput, sourceSwitch, preview); - container.append(labelRow, hiddenInput, control, path, error); + container.append(labelRow, hiddenInput, control, path, hint, error); grid.append(container); } diff --git a/assets/uploads/images/static-elements/2025-05-14-23.46.35-mqupw9qd.jpg b/assets/uploads/images/static-elements/2025-05-14-23.46.35-mqupw9qd.jpg new file mode 100644 index 0000000..4c283bb Binary files /dev/null and b/assets/uploads/images/static-elements/2025-05-14-23.46.35-mqupw9qd.jpg differ diff --git a/assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png b/assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png new file mode 100644 index 0000000..ee8a16a Binary files /dev/null and b/assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png differ diff --git a/assets/uploads/images/static-elements/dclogo_darck-mquq1l13.png b/assets/uploads/images/static-elements/dclogo_darck-mquq1l13.png new file mode 100644 index 0000000..ffef888 Binary files /dev/null and b/assets/uploads/images/static-elements/dclogo_darck-mquq1l13.png differ diff --git a/assets/uploads/images/static-elements/dclogo_darck-mquq2kmb.png b/assets/uploads/images/static-elements/dclogo_darck-mquq2kmb.png new file mode 100644 index 0000000..ffef888 Binary files /dev/null and b/assets/uploads/images/static-elements/dclogo_darck-mquq2kmb.png differ diff --git a/assets/uploads/images/static-elements/dcnt_dark-mqupt7tb.png b/assets/uploads/images/static-elements/dcnt_dark-mqupt7tb.png new file mode 100644 index 0000000..f6624aa Binary files /dev/null and b/assets/uploads/images/static-elements/dcnt_dark-mqupt7tb.png differ diff --git a/assets/uploads/images/static-elements/dcnt_dark-mqupuge5.png b/assets/uploads/images/static-elements/dcnt_dark-mqupuge5.png new file mode 100644 index 0000000..f6624aa Binary files /dev/null and b/assets/uploads/images/static-elements/dcnt_dark-mqupuge5.png differ diff --git a/assets/uploads/images/static-elements/dcnt_dark-mqupvf12.png b/assets/uploads/images/static-elements/dcnt_dark-mqupvf12.png new file mode 100644 index 0000000..f6624aa Binary files /dev/null and b/assets/uploads/images/static-elements/dcnt_dark-mqupvf12.png differ diff --git a/content/pages/home.json b/content/pages/home.json index f6339be..2c46b74 100644 --- a/content/pages/home.json +++ b/content/pages/home.json @@ -34,7 +34,7 @@ "target": "" } ], - "logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg" + "logoSrc": "./assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png" }, "notifications": [ { diff --git a/index.html b/index.html index 4e658f6..1d44d30 100644 --- a/index.html +++ b/index.html @@ -120,4 +120,4 @@ visibility: hidden;
Как и любой кастомный код, это добавляет только ту нагрузку, которую несёт сам код.
Все файлы собираются, минифицируются и сжимаются, чтобы минимально влиять на проект и не плодить внешние библиотеки и CDN-скрипты.
После сборки файлы загружаются в Cloudflare R2 и отдаются через CDN-ссылку на выделенном домене проекта, чтобы получить быстрый и совместимый с Webflow результат.
В панели деплоя есть мгновенный откат.
Нажмите откат, верните предыдущую рабочую версию, исправьте локально и опубликуйте заново.
Помимо @plan, @build и @explore стандартных режимов агентов мы готовим @webflow режим, специально заточенный под Webflow: структуру проекта, взаимодействие с ним и эффективную разработку.
Основано на многолетнем опыте написания кастомного кода для Webflow-анимаций.
WebGL, Three.js, GSAP и кастомная функциональность собраны в набор практик, пайплайнов и модульную систему, чтобы всё работало согласованно.
Также доступна библиотека компонентов: выберите компонент, добавьте в проект и попросите ИИ адаптировать его под задачу.
При запуске NODE.DC доступна отдельная вкладка для встраивания.
В зависимости от задачи можно получить скрипты для конкретной страницы или единый app.js для всего проекта.
Сгенерированная ссылка содержит и продакшен-версию, следующую за URL деплоя, и локальную версию для предпросмотра без влияния на посетителей сайта.
Мы используем основные доступные модели: OpenAI до Claude до Gemini.
Оркестратор выбирает модель под задачу автоматически, но вы можете вручную переключиться на нужную модель.
Во время работы NODE.DC выполняет две задачи параллельно.
Поднимает локальную среду разработки на Bun, синхронизирует JavaScript и TypeScript, включает HMR и держит webhook для автоматического обновления сайта при изменениях.
Одновременно работает оркестратор ИИ-агентов: обрабатывает запросы, меняет код, проверяет состояние сборки и при необходимости обновляет проект Webflow.
Процесс многоэтапный: сначала Bun собирает, сжимает, минифицирует и подготавливает проект вместе с библиотеками.
Затем файлы отправляются в CDN: cdn.node.dc/{USERNAME}/{PROJECT-NAME}. Ссылка уже содержит нужную структуру: вы видите локальную версию, а пользователи получают опубликованный скрипт.
Система изначально рассчитана на работу с Webflow MCP.
Подключите проект и авторизацию, чтобы ИИ-агенты получили доступ к Webflow-сборке. Подключение приложения в Webflow Designer даёт агентам доступ к контенту, элементам, стилям и CSS, а кастомный код получает полный контекст проекта.






































































