From be2294d3fa3fa37fc233c4ec2ce6fd2e218e198f Mon Sep 17 00:00:00 2001 From: CODEX Date: Fri, 5 Jun 2026 15:35:54 +0300 Subject: [PATCH] feat: align Beam settings controls --- frontend/dcViewer.css | 298 +++++++++++++++++++++++++++--- frontend/dcViewer.js | 2 +- frontend/index.html | 4 +- frontend/src/ui/controls/color.js | 60 ++++++ frontend/src/ui/settingsMenu.js | 166 ++++++++++------- 5 files changed, 434 insertions(+), 96 deletions(-) create mode 100644 frontend/src/ui/controls/color.js diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index c2ff846..de1e49e 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -376,6 +376,7 @@ header { } .inspector-collapse-btn { + position: relative; display: grid; place-items: center; width: 28px; @@ -388,13 +389,25 @@ header { color: var(--modal-btn-primary, var(--accent)); cursor: pointer; font: inherit; - font-size: 18px; + font-size: 0; font-weight: 900; line-height: 1; outline: none; transition: background 140ms ease, transform 100ms ease; } +.inspector-collapse-btn::before { + content: ""; + position: absolute; + left: 50%; + top: 38%; + width: 11px; + height: 2px; + border-radius: 999px; + background: currentColor; + transform: translate(-50%, -50%); +} + .inspector-collapse-btn:hover { background: var(--nodedc-glass-control-hover); } @@ -1183,17 +1196,23 @@ header { .settings-panel { position: fixed; + --env-control-h: 46px; + --env-button-radius: 17px; + --env-pill-radius: 999px; + --env-glass-control-bg: var(--nodedc-glass-control-bg); + --env-glass-control-hover: var(--nodedc-glass-control-hover); + --env-glass-control-shadow: var(--nodedc-glass-control-shadow); left: var(--settings-left, 72px); bottom: var(--settings-bottom, 12px); background: color-mix(in srgb, var(--modal-bg) var(--modal-bg-alpha, 100%), transparent); border: 1px solid var(--modal-border); border-radius: var(--modal-radius, 14px); - padding: 8px; + padding: 12px; display: grid; - gap: 6px; - width: var(--settings-width, 320px); - min-height: 300px; - max-height: 520px; + gap: 10px; + width: var(--settings-width, 350px); + min-height: 420px; + max-height: min(720px, calc(100vh - 72px)); overflow: auto; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); z-index: 22; @@ -1201,37 +1220,68 @@ header { } .settings-panel .settings-header { - display: flex; + display: grid; + grid-template-columns: minmax(0, 1fr) var(--env-control-h); align-items: center; - justify-content: space-between; + gap: 10px; color: var(--text); font-weight: 700; - padding-bottom: 2px; + min-height: var(--env-control-h); + padding: 0 0 0 4px; } .settings-panel .settings-title { - font-size: 12px; + font-size: 13px; + line-height: 1.2; } .settings-panel .settings-close { - width: 20px; - height: 20px; - border-radius: 7px; - border: 1px solid var(--modal-border); - background: color-mix(in srgb, var(--modal-focus) var(--modal-focus-alpha, 100%), transparent); - color: var(--text); - cursor: pointer; display: grid; place-items: center; + width: var(--env-control-h); + height: var(--env-control-h); + padding: 0; + border: 0; + border-radius: var(--env-button-radius); + background: var(--env-glass-control-bg); + color: rgba(245, 245, 250, 0.78); + box-shadow: var(--env-glass-control-shadow); + cursor: pointer; + line-height: 1; + outline: none; + backdrop-filter: blur(24px) saturate(128%) brightness(1.05); + -webkit-backdrop-filter: blur(24px) saturate(128%) brightness(1.05); + transition: background 140ms ease, color 140ms ease, transform 100ms ease; +} + +.settings-panel .settings-close svg { + display: block; + width: 16px; + height: 16px; +} + +.settings-panel .settings-close:hover { + background: var(--env-glass-control-hover); + color: #fff; +} + +.settings-panel .settings-close:active { + transform: translateY(1px); +} + +.settings-panel .settings-close:focus, +.settings-panel .settings-close:focus-visible { + outline: none; + box-shadow: none; } .settings-panel .settings-section { border: 1px solid var(--modal-border); border-radius: var(--modal-radius, 10px); - padding: 6px; + padding: 8px; background: color-mix(in srgb, var(--modal-focus) var(--modal-focus-alpha, 100%), transparent); display: grid; - gap: 4px; + gap: 8px; } .settings-panel .section-title { @@ -1242,10 +1292,82 @@ header { .settings-panel .color-row { display: grid; - grid-template-columns: 140px 1fr 1fr; - gap: 10px; + grid-template-columns: minmax(110px, 1fr) minmax(150px, 168px); + gap: 12px; align-items: center; - padding: 4px 6px 4px 6px; + min-height: var(--env-control-h); + padding: 0; +} + +.settings-panel .color-row > :first-child { + min-width: 0; + color: rgba(235, 235, 245, 0.62); + font-size: 12px; + line-height: 1.25; +} + +.settings-panel .env-color-field { + display: flex; + width: 100%; + min-width: 0; + min-height: var(--env-control-h); + align-items: center; + gap: 8px; + box-sizing: border-box; + padding: 5px 10px 5px 7px; + border-radius: var(--env-pill-radius); + background: var(--env-glass-control-bg); + box-shadow: var(--env-glass-control-shadow); + backdrop-filter: blur(24px) saturate(128%) brightness(1.05); + -webkit-backdrop-filter: blur(24px) saturate(128%) brightness(1.05); +} + +.settings-panel .env-color-field__swatch { + position: relative; + width: 26px; + height: 26px; + min-height: 26px; + flex: 0 0 26px; + overflow: hidden; + border-radius: 999px; + cursor: pointer; +} + +.settings-panel .env-color-field__preview { + position: absolute; + inset: 0; + border-radius: inherit; +} + +.settings-panel .env-color-field__swatch input[type="color"] { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 0; + opacity: 0; + cursor: pointer; +} + +.settings-panel .env-color-field__value { + height: 32px; + min-height: 32px; + flex: 1 1 auto; + min-width: 0; +} + +.settings-panel .env-color-field__value input { + width: 100%; + height: 100%; + box-sizing: border-box; + border: 0; + padding: 0 4px; + background: transparent; + color: rgba(255, 255, 255, 0.66); + font-size: 12px; + font-weight: 500; + letter-spacing: 0; + text-align: center; } .slider-row { @@ -1256,6 +1378,116 @@ header { padding: 4px 6px 4px 6px; } +.settings-panel .env-range-control.slider-row { + position: relative; + display: block; + width: 100%; + height: var(--env-control-h); + overflow: hidden; + border-radius: var(--env-pill-radius); + background: var(--env-glass-control-bg); + box-shadow: var(--env-glass-control-shadow); + backdrop-filter: blur(24px) saturate(128%) brightness(1.05); + -webkit-backdrop-filter: blur(24px) saturate(128%) brightness(1.05); +} + +.settings-panel .env-range-control.slider-row::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + z-index: 1; + width: var(--slider-percent, 0%); + border-radius: inherit; + background: var(--modal-btn-primary, var(--accent)); +} + +.settings-panel .env-range-control .slider-label, +.settings-panel .env-range-control .slider-value { + position: absolute; + top: 50%; + z-index: 2; + font-size: 12px; + font-weight: 760; + line-height: 1; + transform: translateY(-50%); + pointer-events: none; +} + +.settings-panel .env-range-control .slider-label { + left: 24px; + max-width: calc(100% - 132px); + overflow: hidden; + color: rgba(255, 255, 255, 0.92); + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-panel .env-range-control .slider-value { + right: 20px; + color: rgba(255, 255, 255, 0.9); + text-align: right; +} + +.settings-panel .env-range-control .slider-input { + position: absolute; + inset: 0; + z-index: 4; + width: 100%; + height: 100%; + margin: 0; + appearance: none; + -webkit-appearance: none; + border-radius: inherit; + background: transparent; + opacity: 0; + cursor: pointer; +} + +.settings-panel .env-range-control .slider-input::-webkit-slider-runnable-track { + height: var(--env-control-h); + border: 0; + background: transparent; +} + +.settings-panel .env-range-control .slider-input::-webkit-slider-thumb { + width: 24px; + height: var(--env-control-h); + margin-top: 0; + appearance: none; + -webkit-appearance: none; + border: 0; + background: transparent; + box-shadow: none; +} + +.settings-panel .env-range-control .slider-input::-moz-range-track { + height: var(--env-control-h); + border: 0; + background: transparent; +} + +.settings-panel .env-range-control .slider-input::-moz-range-thumb { + width: 24px; + height: var(--env-control-h); + border: 0; + background: transparent; + box-shadow: none; +} + +.settings-panel .env-range-control .slider-fill-text { + position: absolute; + inset: 0; + z-index: 3; + overflow: hidden; + clip-path: inset(0 calc(100% - var(--slider-percent, 0%)) 0 0); + pointer-events: none; +} + +.settings-panel .env-range-control .slider-fill-text .slider-label, +.settings-panel .env-range-control .slider-fill-text .slider-value { + color: var(--modal-btn-primary-contrast, #15170f); +} + .slider-label { color: var(--text); font-size: 13px; @@ -1434,6 +1666,28 @@ header { padding: 5px 6px; } +.settings-panel .env-color-field .env-color-field__value input[type="text"] { + width: 100%; + max-width: none; + height: 100%; + box-sizing: border-box; + border: 0; + border-radius: 0; + padding: 0 4px; + background: transparent; + color: rgba(255, 255, 255, 0.66); + font-size: 12px; + font-weight: 500; + letter-spacing: 0; + text-align: center; + outline: none; +} + +.settings-panel .env-color-field .env-color-field__value input[type="text"]:focus { + border: 0; + box-shadow: none; +} + .log { background: color-mix(in srgb, var(--modal-element) var(--modal-focus-alpha, 100%), transparent); border: 1px solid var(--modal-element-outline, var(--modal-element)); diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 286b628..bac4cf9 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -4,7 +4,7 @@ const SDK_URL = "./lib/dc-camera-context.es.js"; import { createLogo } from "./src/ui/logo.js"; import { toolbarConfig, applyToolbarVars } from "./src/ui/toolbar.js"; import { createTemplatesMenu } from "./src/ui/templatesMenu.js"; -import { createSettingsMenu } from "./src/ui/settingsMenu.js"; +import { createSettingsMenu } from "./src/ui/settingsMenu.js?v=2"; import { createMeasurementModal } from "./src/ui/measurementModal.js"; import { createGlassSelect } from "./src/ui/glassSelect.js"; import { createSliderControl } from "./src/ui/controls/slider.js"; diff --git a/frontend/index.html b/frontend/index.html index 5538782..dc5f2c9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -87,7 +87,7 @@ - +
@@ -251,6 +251,6 @@ - + diff --git a/frontend/src/ui/controls/color.js b/frontend/src/ui/controls/color.js new file mode 100644 index 0000000..fe373b4 --- /dev/null +++ b/frontend/src/ui/controls/color.js @@ -0,0 +1,60 @@ +const isHexColor = (value) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || "").trim()); + +export function createColorControl({ + value = "#ffffff", + previewValue = value, + ariaLabel = "Выбрать цвет", + valueLabel = "Значение цвета", + onChange, +}) { + const root = document.createElement("div"); + root.className = "env-color-field"; + + const swatch = document.createElement("label"); + swatch.className = "env-color-field__swatch"; + + const preview = document.createElement("span"); + preview.className = "env-color-field__preview"; + + const picker = document.createElement("input"); + picker.type = "color"; + picker.setAttribute("aria-label", ariaLabel); + + const valueWrap = document.createElement("div"); + valueWrap.className = "env-color-field__value"; + + const valueInput = document.createElement("input"); + valueInput.type = "text"; + valueInput.setAttribute("aria-label", valueLabel); + valueWrap.appendChild(valueInput); + + const update = (nextValue, { emit = false } = {}) => { + const safeValue = String(nextValue ?? ""); + const safePreview = isHexColor(safeValue) + ? safeValue + : (isHexColor(previewValue) ? previewValue : "#000000"); + + valueInput.value = safeValue; + picker.value = isHexColor(safeValue) ? safeValue : safePreview; + preview.style.background = safePreview; + + if (emit && typeof onChange === "function") { + onChange(safeValue); + } + }; + + picker.addEventListener("input", () => update(picker.value, { emit: true })); + valueInput.addEventListener("change", () => update(valueInput.value, { emit: true })); + + swatch.appendChild(preview); + swatch.appendChild(picker); + root.appendChild(swatch); + root.appendChild(valueWrap); + + update(value); + + root.setValue = (nextValue) => update(nextValue); + root.getValue = () => valueInput.value; + + return root; +} diff --git a/frontend/src/ui/settingsMenu.js b/frontend/src/ui/settingsMenu.js index 74cbd95..ab1a075 100644 --- a/frontend/src/ui/settingsMenu.js +++ b/frontend/src/ui/settingsMenu.js @@ -1,4 +1,5 @@ import { createSliderControl } from "./controls/slider.js"; +import { createColorControl } from "./controls/color.js"; const defaultSettings = { themeVersion: "v4-beam-lime", @@ -186,7 +187,12 @@ export function createSettingsMenu({ panel, controls = {}, onChange = null, onSh header.innerHTML = `Настройки`; const closeBtn = document.createElement("button"); closeBtn.className = "settings-close"; - closeBtn.textContent = "—"; + closeBtn.setAttribute("aria-label", "Закрыть настройки"); + closeBtn.innerHTML = ` + + `; closeBtn.addEventListener("click", hide); header.appendChild(closeBtn); panel.appendChild(header); @@ -202,31 +208,53 @@ export function createSettingsMenu({ panel, controls = {}, onChange = null, onSh }; const addColor = (parent, label, key) => { - const isHex = (val) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test((val || "").trim()); const row = document.createElement("div"); row.className = "color-row"; - const lbl = document.createElement("label"); + const lbl = document.createElement("div"); lbl.textContent = label; - const input = document.createElement("input"); - input.type = "color"; - input.value = isHex(state[key]) ? state[key] : "#ffffff"; - const hex = document.createElement("input"); - hex.type = "text"; - hex.value = state[key]; - input.addEventListener("change", (e) => { - hex.value = e.target.value; - setVal(key, e.target.value); - }); - hex.addEventListener("change", (e) => { - input.value = e.target.value; - setVal(key, e.target.value); + const control = createColorControl({ + value: state[key], + onChange: (nextValue) => { + setVal(key, nextValue); + }, }); row.appendChild(lbl); - row.appendChild(input); - row.appendChild(hex); + row.appendChild(control); parent.appendChild(row); }; + const addSettingsSlider = ({ + parent, + label, + key, + value, + min = 0, + max = 100, + step = 1, + formatValue = (nextValue) => String(nextValue), + onChange, + }) => { + const slider = createSliderControl({ + label, + value, + min, + max, + step, + className: "env-range-control", + showValue: true, + formatValue, + onChange: (nextValue) => { + if (typeof onChange === "function") { + onChange(nextValue); + return; + } + setVal(key, nextValue); + }, + }); + parent.appendChild(slider); + return slider; + }; + const font = section("Фон"); addColor(font, "Фон внешний", "bgOuter"); addColor(font, "Фон холста верх", "canvasTop"); @@ -235,65 +263,59 @@ export function createSettingsMenu({ panel, controls = {}, onChange = null, onSh addColor(font, "Фон загрузки низ", "loadingBottom"); panel.appendChild(font); - const buttons = section("Кнопки"); - addColor(buttons, "Плюс", "plus"); - addColor(buttons, "Кнопки (заливка)", "templateFill"); - addColor(buttons, "Кнопки (бордер)", "templateOutline"); - addColor(buttons, "Кнопки (outline hover)", "templateOutlineHover"); - const brightnessSlider = createSliderControl({ - label: "Яркость подсветки", - value: Number(state.templateGlow ?? 80), - min: 0, - max: 100, - step: 1, - onChange: (val) => setVal("templateGlow", val), - }); - buttons.appendChild(brightnessSlider); - panel.appendChild(buttons); + const buttons = section("Кнопки"); + addColor(buttons, "Плюс", "plus"); + addColor(buttons, "Кнопки (заливка)", "templateFill"); + addColor(buttons, "Кнопки (бордер)", "templateOutline"); + addColor(buttons, "Кнопки (outline hover)", "templateOutlineHover"); + addSettingsSlider({ + parent: buttons, + label: "Яркость подсветки", + key: "templateGlow", + value: Number(state.templateGlow ?? 80), + min: 0, + max: 100, + step: 1, + formatValue: (val) => `${Math.round(val)}%`, + }); + panel.appendChild(buttons); - const projectButtons = section("Кнопки проектов"); - addColor(projectButtons, "Проекты (заливка)", "projectBtnFill"); - addColor(projectButtons, "Проекты (бордер)", "projectBtnBorder"); - addColor(projectButtons, "Проекты (hover)", "projectBtnHover"); - panel.appendChild(projectButtons); + const projectButtons = section("Кнопки проектов"); + addColor(projectButtons, "Проекты (заливка)", "projectBtnFill"); + addColor(projectButtons, "Проекты (бордер)", "projectBtnBorder"); + addColor(projectButtons, "Проекты (hover)", "projectBtnHover"); + panel.appendChild(projectButtons); - const modalButtons = section("Кнопки модалок"); - addColor(modalButtons, "Модальная primary", "modalBtnPrimary"); - addColor(modalButtons, "Модальная secondary", "modalBtnSecondary"); - addColor(modalButtons, "Лоудер", "loaderColor"); - panel.appendChild(modalButtons); + const modalButtons = section("Кнопки модалок"); + addColor(modalButtons, "Модальная primary", "modalBtnPrimary"); + addColor(modalButtons, "Модальная secondary", "modalBtnSecondary"); + addColor(modalButtons, "Лоудер", "loaderColor"); + panel.appendChild(modalButtons); - const toolbar = section("Toolbar"); - addColor(toolbar, "Фон", "toolbarBg"); - addColor(toolbar, "Актив", "toolbarBgActive"); - addColor(toolbar, "Бордер", "toolbarBorder"); - addColor(toolbar, "Outline", "toolbarOutline"); - panel.appendChild(toolbar); + const toolbar = section("Toolbar"); + addColor(toolbar, "Фон", "toolbarBg"); + addColor(toolbar, "Актив", "toolbarBgActive"); + addColor(toolbar, "Бордер", "toolbarBorder"); + addColor(toolbar, "Outline", "toolbarOutline"); + panel.appendChild(toolbar); const navCube = section("NavCube"); addColor(navCube, "Грани", "navCubeBase"); addColor(navCube, "Hover", "navCubeHover"); addColor(navCube, "Текст", "navCubeText"); addColor(navCube, "Рёбра", "navCubeEdge"); - const edgeAlphaRow = document.createElement("div"); - edgeAlphaRow.className = "color-row"; - const edgeAlphaLbl = document.createElement("label"); - edgeAlphaLbl.textContent = "Прозрачность рёбер (0-1)"; - const edgeAlphaInput = document.createElement("input"); - edgeAlphaInput.type = "number"; - edgeAlphaInput.step = "0.05"; - edgeAlphaInput.min = "0"; - edgeAlphaInput.max = "1"; - edgeAlphaInput.value = state.navCubeEdgeAlpha; - edgeAlphaInput.addEventListener("change", () => { - const v = Math.min(1, Math.max(0, parseFloat(edgeAlphaInput.value) || 0)); - edgeAlphaInput.value = v; - setVal("navCubeEdgeAlpha", v); + addSettingsSlider({ + parent: navCube, + label: "Прозрачность рёбер", + value: Math.round(Math.max(0, Math.min(1, Number(state.navCubeEdgeAlpha ?? 0.6))) * 100), + min: 0, + max: 100, + step: 5, + formatValue: (val) => `${Math.round(val)}%`, + onChange: (val) => { + setVal("navCubeEdgeAlpha", Math.max(0, Math.min(1, Number(val) / 100))); + }, }); - edgeAlphaRow.appendChild(edgeAlphaLbl); - edgeAlphaRow.appendChild(edgeAlphaInput); - edgeAlphaRow.appendChild(document.createElement("div")); - navCube.appendChild(edgeAlphaRow); panel.appendChild(navCube); const measure = section("Линейка"); @@ -325,33 +347,35 @@ export function createSettingsMenu({ panel, controls = {}, onChange = null, onSh const modals = section("Модальные окна"); addColor(modals, "Основной фон", "modalBg"); addColor(modals, "Фокусный фон", "modalFocus"); - const modalBgAlphaRow = createSliderControl({ + addSettingsSlider({ + parent: modals, label: "Прозрачность основного фона", min: 0, max: 1, step: 0.05, value: parseFloat(state.modalBgAlpha ?? 1), + formatValue: (val) => `${Math.round(Number(val) * 100)}%`, onChange: (val) => { const num = Math.max(0, Math.min(1, parseFloat(val))); setVal("modalBgAlpha", num); document.documentElement.style.setProperty("--modal-bg-alpha", `${num * 100}%`); }, }); - modals.appendChild(modalBgAlphaRow); - const modalFocusAlphaRow = createSliderControl({ + addSettingsSlider({ + parent: modals, label: "Прозрачность фокусного фона", min: 0, max: 1, step: 0.05, value: parseFloat(state.modalFocusAlpha ?? 1), + formatValue: (val) => `${Math.round(Number(val) * 100)}%`, onChange: (val) => { const num = Math.max(0, Math.min(1, parseFloat(val))); setVal("modalFocusAlpha", num); document.documentElement.style.setProperty("--modal-focus-alpha", `${num * 100}%`); }, }); - modals.appendChild(modalFocusAlphaRow); addColor(modals, "Аутлайн", "modalBorder"); addColor(modals, "Фон элемента", "modalElement");