Align application surfaces with Launcher and SEO

This commit is contained in:
DCCONSTRUCTIONS 2026-07-10 21:35:06 +03:00
parent 0e030136bb
commit 41e491191c
7 changed files with 102 additions and 160 deletions

View File

@ -56,16 +56,16 @@ type MaterialDraft = {
const materialDefaults: Record<NodedcTheme, MaterialDraft> = { const materialDefaults: Record<NodedcTheme, MaterialDraft> = {
dark: { dark: {
panelHex: "#16161a", panelHex: "#151517",
panelOpacity: 68, panelOpacity: 100,
fieldHex: "#101016", fieldHex: "#2a2a2c",
fieldOpacity: 78, fieldOpacity: 100,
}, },
light: { light: {
panelHex: "#ffffff", panelHex: "#ffffff",
panelOpacity: 62, panelOpacity: 100,
fieldHex: "#ffffff", fieldHex: "#ffffff",
fieldOpacity: 72, fieldOpacity: 100,
}, },
}; };
@ -87,7 +87,7 @@ const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: strin
foundation: { foundation: {
eyebrow: "01 / FOUNDATION", eyebrow: "01 / FOUNDATION",
title: "Основа", title: "Основа",
description: "Тема, акцент, стекло и неизменяемая геометрия приложения.", description: "Тема, нейтральные поверхности и неизменяемая геометрия приложения.",
icon: "panel", icon: "panel",
}, },
controls: { controls: {
@ -234,7 +234,7 @@ export function CatalogApp() {
dark: { ...materialDefaults.dark }, dark: { ...materialDefaults.dark },
light: { ...materialDefaults.light }, light: { ...materialDefaults.light },
})); }));
const [savedEnvironmentControl, setSavedEnvironmentControl] = useState<string | null>(null); const [environmentLayoutSaved, setEnvironmentLayoutSaved] = useState(false);
const workspace = useApplicationWorkspace<CatalogSection>(); const workspace = useApplicationWorkspace<CatalogSection>();
const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace; const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace;
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active"); const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
@ -344,7 +344,7 @@ export function CatalogApp() {
})); }));
}; };
const saveEnvironmentLayout = (control: string) => { const saveEnvironmentLayout = () => {
const draft = { const draft = {
theme, theme,
accentHex, accentHex,
@ -367,15 +367,9 @@ export function CatalogApp() {
} catch { } catch {
// The preview remains useful in restricted browser contexts even without local persistence. // The preview remains useful in restricted browser contexts even without local persistence.
} }
setSavedEnvironmentControl(control); setEnvironmentLayoutSaved(true);
}; };
const savedAction = (control: string) => (
<IconButton label={`Сохранить «${control}» в локальный layout`} onClick={() => saveEnvironmentLayout(control)}>
<Icon name="save" />
</IconButton>
);
const environmentSections = [ const environmentSections = [
{ {
id: "application-material", id: "application-material",
@ -384,24 +378,21 @@ export function CatalogApp() {
tone: "accent" as const, tone: "accent" as const,
content: ( content: (
<> <>
<ControlRow label="Акцент" action={savedAction("Акцент")}> <ControlRow label="Акцент">
<ColorField label="Акцент приложения" value={accentHex} onChange={setAccentHex} /> <ColorField label="Акцент приложения" value={accentHex} onChange={setAccentHex} />
</ControlRow> </ControlRow>
<ControlRow label="Плашки" action={savedAction("Плашки")}> <ControlRow label="Плашки">
<ColorField label="Цвет плашек" value={material.panelHex} onChange={(panelHex) => updateMaterial({ panelHex })} /> <ColorField label="Цвет плашек" value={material.panelHex} onChange={(panelHex) => updateMaterial({ panelHex })} />
</ControlRow> </ControlRow>
<ControlRow label="Прозрачность плашки" action={savedAction("Прозрачность плашки")}> <ControlRow label="Прозрачность плашки">
<RangeControl label="Прозрачность плашки" showLabel={false} value={material.panelOpacity} min={18} max={96} formatValue={(value) => `${value}%`} onChange={(panelOpacity) => updateMaterial({ panelOpacity })} /> <RangeControl label="Прозрачность плашки" showLabel={false} value={material.panelOpacity} min={18} max={96} formatValue={(value) => `${value}%`} onChange={(panelOpacity) => updateMaterial({ panelOpacity })} />
</ControlRow> </ControlRow>
<ControlRow label="Поля ввода" action={savedAction("Поля ввода")}> <ControlRow label="Поля ввода">
<ColorField label="Цвет полей ввода" value={material.fieldHex} onChange={(fieldHex) => updateMaterial({ fieldHex })} /> <ColorField label="Цвет полей ввода" value={material.fieldHex} onChange={(fieldHex) => updateMaterial({ fieldHex })} />
</ControlRow> </ControlRow>
<ControlRow label="Прозрачность полей" action={savedAction("Прозрачность полей")}> <ControlRow label="Прозрачность полей">
<RangeControl label="Прозрачность полей" showLabel={false} value={material.fieldOpacity} min={18} max={100} formatValue={(value) => `${value}%`} onChange={(fieldOpacity) => updateMaterial({ fieldOpacity })} /> <RangeControl label="Прозрачность полей" showLabel={false} value={material.fieldOpacity} min={18} max={100} formatValue={(value) => `${value}%`} onChange={(fieldOpacity) => updateMaterial({ fieldOpacity })} />
</ControlRow> </ControlRow>
<p className="catalog-environment-draft" aria-live="polite">
{savedEnvironmentControl ? `«${savedEnvironmentControl}» сохранён в локальный layout. Сервер и MCP не используются.` : "Дискета сохраняет текущий параметр в локальный layout браузера."}
</p>
</> </>
), ),
}, },
@ -411,15 +402,9 @@ export function CatalogApp() {
tone: "accent" as const, tone: "accent" as const,
content: ( content: (
<> <>
<ControlRow label="Цвет" action={savedAction("Цвет подсветки")}><ColorField value={lightColor} onChange={setLightColor} /></ControlRow> <ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
<div className="catalog-environment-with-action"> <RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} /> <RangeControl label="Дистанция свечения" value={glowDistance} min={0} max={200} formatValue={(value) => `${value}%`} onChange={setGlowDistance} />
{savedAction("Яркость")}
</div>
<div className="catalog-environment-with-action">
<RangeControl label="Дистанция свечения" value={glowDistance} min={0} max={200} formatValue={(value) => `${value}%`} onChange={setGlowDistance} />
{savedAction("Дистанция свечения")}
</div>
</> </>
), ),
}, },
@ -429,7 +414,7 @@ export function CatalogApp() {
tone: "accent" as const, tone: "accent" as const,
content: ( content: (
<> <>
<ControlRow label="Связи по умолчанию" action={savedAction("Связи по умолчанию")}> <ControlRow label="Связи по умолчанию">
<Select <Select
variant="split" variant="split"
label="Тип связи" label="Тип связи"
@ -442,11 +427,8 @@ export function CatalogApp() {
onChange={setConnectionType} onChange={setConnectionType}
/> />
</ControlRow> </ControlRow>
<div className="catalog-environment-with-action"> <Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} /> <ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
{savedAction("Использовать цвет портов")}
</div>
<ControlRow label="Цвет связей по умолчанию" action={savedAction("Цвет связей")}><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
<ControlRow label="Применить ко всем"><Button variant="primary" shape="pill" width="full">Применить</Button></ControlRow> <ControlRow label="Применить ко всем"><Button variant="primary" shape="pill" width="full">Применить</Button></ControlRow>
</> </>
), ),
@ -457,16 +439,10 @@ export function CatalogApp() {
tone: "accent" as const, tone: "accent" as const,
content: ( content: (
<> <>
<ControlRow label="Заливка" action={savedAction("Заливка портов")}><ColorField value={fillColor} onChange={setFillColor} /></ControlRow> <ControlRow label="Заливка"><ColorField value={fillColor} onChange={setFillColor} /></ControlRow>
<div className="catalog-environment-with-action"> <RangeControl label="Прозрачность заливки" value={fillOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setFillOpacity} />
<RangeControl label="Прозрачность заливки" value={fillOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setFillOpacity} /> <ControlRow label="Обводка"><ColorField value={strokeColor} onChange={setStrokeColor} /></ControlRow>
{savedAction("Прозрачность заливки")} <RangeControl label="Прозрачность обводки" value={strokeOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setStrokeOpacity} />
</div>
<ControlRow label="Обводка" action={savedAction("Обводка портов")}><ColorField value={strokeColor} onChange={setStrokeColor} /></ControlRow>
<div className="catalog-environment-with-action">
<RangeControl label="Прозрачность обводки" value={strokeOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setStrokeOpacity} />
{savedAction("Прозрачность обводки")}
</div>
</> </>
), ),
}, },
@ -477,18 +453,18 @@ export function CatalogApp() {
case "foundation": case "foundation":
return ( return (
<div className="catalog-grid catalog-grid--foundation"> <div className="catalog-grid catalog-grid--foundation">
<Preview title="Тема приложения" note={theme === "dark" ? "dark glass" : "light glass"}> <Preview title="Тема приложения" note={theme === "dark" ? "Launcher / dark" : "SEO / light"}>
<div className="catalog-theme-row"> <div className="catalog-theme-row">
<Button variant={theme === "dark" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button> <Button variant={theme === "dark" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
<Button variant={theme === "light" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button> <Button variant={theme === "light" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
</div> </div>
<p className="catalog-preview__explanation">Тема задаёт нейтральный каркас. Акцент, плашки, поля и их прозрачность настраиваются живым Environment control там же сохраняется локальный draft.</p> <p className="catalog-preview__explanation">Две базовые темы фиксированы по эталонам: чёрная Launcher/Hub, белая SEO. Стекло используется только в модальных окнах.</p>
</Preview> </Preview>
<Preview title="Иерархия стекла" note="default / strong / soft"> <Preview title="Поверхности приложения" note="Launcher / SEO">
<div className="catalog-surface-stack"> <div className="catalog-surface-stack">
<GlassSurface padding="md">Default surface</GlassSurface> <GlassSurface padding="md">Основная поверхность</GlassSurface>
<GlassSurface tone="strong" padding="md">Strong floating surface</GlassSurface> <GlassSurface tone="strong" padding="md">Контентное окно</GlassSurface>
<GlassSurface tone="soft" padding="md">Soft nested surface</GlassSurface> <GlassSurface tone="soft" padding="md">Вложенная область</GlassSurface>
</div> </div>
</Preview> </Preview>
<Preview title="Каркас Launcher" note="fixed header / stage / panels" className="catalog-preview--wide"> <Preview title="Каркас Launcher" note="fixed header / stage / panels" className="catalog-preview--wide">
@ -555,10 +531,14 @@ export function CatalogApp() {
<Preview title="Environment controls" note="Engine / select + accordion" className="catalog-preview--wide"> <Preview title="Environment controls" note="Engine / select + accordion" className="catalog-preview--wide">
<div className="catalog-environment-demo"> <div className="catalog-environment-demo">
<GlassSurface tone="strong" radius="panel" className="catalog-environment-card"> <GlassSurface tone="strong" radius="panel" className="catalog-environment-card">
<div className="catalog-environment-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div> <div className="catalog-environment-title">
<div><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
<IconButton label="Сохранить текущий layout" onClick={saveEnvironmentLayout}><Icon name="save" /></IconButton>
</div>
{environmentLayoutSaved ? <p className="catalog-environment-draft" aria-live="polite">Layout сохранён локально. Сервер и MCP не используются.</p> : null}
<Inspector sections={environmentSections} defaultOpen={["application-material"]} /> <Inspector sections={environmentSections} defaultOpen={["application-material"]} />
</GlassSurface> </GlassSurface>
<p className="catalog-preview__explanation">Environment controls единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет настоящий material приложения; отдельной вкладки Inspector больше нет.</p> <p className="catalog-preview__explanation">Environment controls единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет цвет плашек и полей приложения; сохраняется весь текущий layout одной кнопкой сверху.</p>
</div> </div>
</Preview> </Preview>
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide"> <Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">

View File

@ -281,7 +281,7 @@ textarea {
grid-template-rows: 3.2rem 1fr; grid-template-rows: 3.2rem 1fr;
gap: 0.6rem; gap: 0.6rem;
border-radius: 1.1rem; border-radius: 1.1rem;
background: color-mix(in srgb, var(--nodedc-canvas-soft) 76%, transparent); background: var(--nodedc-canvas-soft);
padding: 0.6rem; padding: 0.6rem;
} }
@ -300,11 +300,11 @@ textarea {
} }
.catalog-shell-diagram__nav { .catalog-shell-diagram__nav {
background: color-mix(in srgb, var(--nodedc-glass-control-hover) 90%, transparent) !important; background: var(--nodedc-glass-control-hover) !important;
} }
.catalog-shell-diagram__content { .catalog-shell-diagram__content {
background: color-mix(in srgb, var(--nodedc-glass-control-hover) 70%, transparent) !important; background: var(--nodedc-glass-control-bg) !important;
} }
.catalog-environment-demo { .catalog-environment-demo {
@ -314,9 +314,7 @@ textarea {
align-items: start; align-items: start;
gap: clamp(1rem, 4vw, 4rem); gap: clamp(1rem, 4vw, 4rem);
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);
background: background: var(--nodedc-canvas-soft);
radial-gradient(circle at 18% 24%, rgb(var(--nodedc-accent-rgb) / 0.13), transparent 22rem),
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
padding: clamp(1rem, 3vw, 2rem); padding: clamp(1rem, 3vw, 2rem);
} }
@ -326,9 +324,16 @@ textarea {
} }
.catalog-environment-title { .catalog-environment-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0 0.875rem 0.875rem;
}
.catalog-environment-title > div {
display: grid; display: grid;
gap: 0.3rem; gap: 0.3rem;
padding: 0 0.875rem 0.875rem;
} }
.catalog-environment-title span { .catalog-environment-title span {
@ -337,20 +342,12 @@ textarea {
} }
.catalog-environment-draft { .catalog-environment-draft {
margin: 0; margin: -0.25rem 0.875rem 0.75rem;
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
font-size: 0.68rem; font-size: 0.68rem;
line-height: 1.4; line-height: 1.4;
} }
.catalog-environment-with-action {
display: grid;
width: min(var(--nodedc-inspector-inner-width), 100%);
grid-template-columns: minmax(0, 1fr) var(--nodedc-control-height);
align-items: center;
gap: var(--nodedc-space-2);
}
.catalog-icon-catalog { .catalog-icon-catalog {
display: grid; display: grid;
gap: 1rem; gap: 1rem;

View File

@ -4,13 +4,13 @@
## GlassSurface ## GlassSurface
Базовая поверхность для карточки, панели и большого окна. Варианты меняют плотность материала, но не создают отдельный дизайн. Базовая непрозрачная поверхность для карточки, панели и большого окна. Варианты меняют плотность нейтральной поверхности, но не создают отдельный дизайн. Реальный glass-материал с blur и rim используется только у modal `Window`.
- `default` — обычная карточка/панель; - `default` — обычная карточка/панель;
- `strong` — dropdown, modal и поверхность над сложным фоном; - `strong` — dropdown, modal и поверхность над сложным фоном;
- `soft` — вложенная или вторичная область. - `soft` — вложенная или вторичная область.
Material rim допустим только как часть стекла. Жёсткая цветная рамка, случайный browser outline или debug border не являются rim. Material rim допустим только как часть modal-стекла. Жёсткая цветная рамка, случайный browser outline или debug border не являются rim.
## Button и IconButton ## Button и IconButton

View File

@ -9,10 +9,10 @@ NODE.DC-приложения отличаются цветовой схемой,
- canvas/background; - canvas/background;
- основной и вторичный текст; - основной и вторичный текст;
- accent и автоматически вычисляемый on-accent; - accent и автоматически вычисляемый on-accent;
- плотность/цвет glass surfaces; - цвет непрозрачных application surfaces;
- field/control surfaces; - field/control surfaces;
- overlay; - overlay;
- shadows и material rim; - тени controls и modal material;
- semantic success/warning/danger tones при необходимости. - semantic success/warning/danger tones при необходимости.
## Что тема не меняет ## Что тема не меняет
@ -31,14 +31,14 @@ NODE.DC-приложения отличаются цветовой схемой,
В baseline включены `dark` и `light`. Это не два отдельных продукта, а примеры одного контракта: В baseline включены `dark` и `light`. Это не два отдельных продукта, а примеры одного контракта:
- dark соответствует текущей основной семье Launcher/Hub/CMS; - dark соответствует текущей основной семье Launcher/Hub/CMS;
- light воспроизводит SEO-поверхности: `#ececec` canvas, полупрозрачные белые glass panels, тёмный текст и тонкие нейтральные outlines; - light воспроизводит SEO-поверхности: `#ececec` canvas, белые панели и поля, тёмный текст, чёрные primary actions и мягкую тень у белых secondary controls;
- конкретный продукт может поверх preset задать свой accent и surface variables. - конкретный продукт может поверх preset задать свой accent и surface variables.
## Accent ## Accent
Accent задаётся RGB tuple. On-accent вычисляется по luminance и не должен вручную прописываться белым или чёрным в отдельном компоненте. Accent задаётся RGB tuple. On-accent вычисляется по luminance и не должен вручную прописываться белым или чёрным в отдельном компоненте.
Default accent текущего NODE.DC shell — `#ff2f92`. `ColorField` открывает единый portal-picker с saturation/value plane, hue slider, HEX и быстрыми цветами — системный `input[type=color]` не является runtime UI. Изменение accent сразу меняет Environment Settings, active actions, range fill, media file action и theme-aware window states без создания новой темы или fork компонентов. Default accent текущего NODE.DC shell — `#ff2f92`. `ColorField` открывает единый portal-picker с saturation/value plane, hue slider, HEX и быстрыми цветами — системный `input[type=color]` не является runtime UI. Изменение accent сразу меняет Environment Settings, active actions, range fill, media file action и theme-aware window states без создания новой темы или fork компонентов. Blur, gloss и material rim не наследуются application panels: это материал только для modal `Window`.
Semantic status не обязан совпадать с accent. Например, ошибка остаётся danger, даже если приложение использует красный брендовый accent. Semantic status не обязан совпадать с accent. Например, ошибка остаётся danger, даже если приложение использует красный брендовый accent.

View File

@ -15,27 +15,27 @@
--nodedc-text-disabled: rgba(247, 248, 244, 0.34); --nodedc-text-disabled: rgba(247, 248, 244, 0.34);
/* Runtime material controls override only these five values. */ /* Runtime material controls override only these five values. */
--nodedc-panel-material-rgb: 22 22 26; --nodedc-panel-material-rgb: 21 21 23;
--nodedc-panel-material-opacity: 0.68; --nodedc-panel-material-opacity: 1;
--nodedc-panel-material-strong-opacity: 0.8; --nodedc-panel-material-strong-opacity: 1;
--nodedc-panel-material-soft-opacity: 0.48; --nodedc-panel-material-soft-opacity: 1;
--nodedc-field-material-rgb: 16 16 22; --nodedc-field-material-rgb: 42 42 44;
--nodedc-field-material-opacity: 0.78; --nodedc-field-material-opacity: 1;
--nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity)); --nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity));
--nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity)); --nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity));
--nodedc-glass-panel-bg-soft: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-soft-opacity)); --nodedc-glass-panel-bg-soft: var(--nodedc-canvas-soft);
--nodedc-header-pill-bg: rgba(64, 64, 64, 0.48); --nodedc-header-pill-bg: rgba(64, 64, 64, 0.48);
--nodedc-glass-panel-surface: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.075), transparent 38%), linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.012)), var(--nodedc-glass-panel-bg); --nodedc-glass-panel-surface: var(--nodedc-glass-panel-bg);
--nodedc-glass-panel-surface-strong: radial-gradient(circle at 16% 0%, rgba(255, 255, 255, 0.09), transparent 36%), linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.016)), var(--nodedc-glass-panel-bg-strong); --nodedc-glass-panel-surface-strong: var(--nodedc-glass-panel-bg-strong);
--nodedc-glass-control-bg: rgba(110, 110, 110, 0.07); --nodedc-glass-control-bg: rgba(110, 110, 110, 0.07);
--nodedc-glass-control-hover: rgba(129, 129, 129, 0.13); --nodedc-glass-control-hover: rgba(129, 129, 129, 0.13);
--nodedc-glass-control-active: rgba(255, 255, 255, 0.92); --nodedc-glass-control-active: rgba(255, 255, 255, 0.92);
--nodedc-glass-control-active-text: rgba(8, 8, 10, 0.96); --nodedc-glass-control-active-text: rgba(8, 8, 10, 0.96);
--nodedc-field-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.022), transparent), rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity)); --nodedc-field-bg: rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
--nodedc-overlay-bg: rgba(0, 0, 0, 0.46); --nodedc-overlay-bg: rgba(0, 0, 0, 0.46);
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.035)); --nodedc-glass-rim: transparent;
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025); --nodedc-glass-control-shadow: none;
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.11), 0 28px 90px rgba(0, 0, 0, 0.38); --nodedc-glass-panel-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.2); --nodedc-glass-dropdown-shadow: 0 22px 60px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.2);
--nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.14); --nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.14);
--nodedc-glass-outline: rgba(255, 255, 255, 0.11); --nodedc-glass-outline: rgba(255, 255, 255, 0.11);
@ -55,6 +55,8 @@
--nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white); --nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
--nodedc-primary-action-color: rgb(var(--nodedc-on-accent-rgb)); --nodedc-primary-action-color: rgb(var(--nodedc-on-accent-rgb));
--nodedc-primary-action-shadow: 0 12px 28px rgb(var(--nodedc-accent-rgb) / 0.2); --nodedc-primary-action-shadow: 0 12px 28px rgb(var(--nodedc-accent-rgb) / 0.2);
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.014)), rgba(7, 7, 10, 0.54);
--nodedc-modal-shadow: 0 34px 120px rgba(0, 0, 0, 0.64), 0 10px 32px rgba(0, 0, 0, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.045);
} }
[data-nodedc-theme="light"] { [data-nodedc-theme="light"] {
@ -72,40 +74,40 @@
--nodedc-text-muted: rgba(18, 20, 22, 0.48); --nodedc-text-muted: rgba(18, 20, 22, 0.48);
--nodedc-text-disabled: rgba(18, 20, 22, 0.32); --nodedc-text-disabled: rgba(18, 20, 22, 0.32);
/* The light values follow the SEO launcher material: white glass, soft depth. */ /* SEO light theme: solid application surfaces with soft, white controls. */
--nodedc-panel-material-rgb: 255 255 255; --nodedc-panel-material-rgb: 255 255 255;
--nodedc-panel-material-opacity: 0.62; --nodedc-panel-material-opacity: 1;
--nodedc-panel-material-strong-opacity: 0.8; --nodedc-panel-material-strong-opacity: 1;
--nodedc-panel-material-soft-opacity: 0.46; --nodedc-panel-material-soft-opacity: 1;
--nodedc-field-material-rgb: 255 255 255; --nodedc-field-material-rgb: 255 255 255;
--nodedc-field-material-opacity: 0.72; --nodedc-field-material-opacity: 1;
--nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity)); --nodedc-glass-panel-bg: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-opacity));
--nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity)); --nodedc-glass-panel-bg-strong: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-strong-opacity));
--nodedc-glass-panel-bg-soft: rgb(var(--nodedc-panel-material-rgb) / var(--nodedc-panel-material-soft-opacity)); --nodedc-glass-panel-bg-soft: var(--nodedc-canvas-soft);
--nodedc-header-pill-bg: rgba(255, 255, 255, 0.56); --nodedc-header-pill-bg: rgba(255, 255, 255, 0.56);
--nodedc-glass-panel-surface: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.92), transparent 45%), linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(255, 255, 255, 0.26)), var(--nodedc-glass-panel-bg); --nodedc-glass-panel-surface: var(--nodedc-glass-panel-bg);
--nodedc-glass-panel-surface-strong: radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.98), transparent 45%), linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.36)), var(--nodedc-glass-panel-bg-strong); --nodedc-glass-panel-surface-strong: var(--nodedc-glass-panel-bg-strong);
--nodedc-glass-control-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.46)), rgba(255, 255, 255, 0.7); --nodedc-glass-control-bg: #ffffff;
--nodedc-glass-control-hover: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.58)), rgba(255, 255, 255, 0.84); --nodedc-glass-control-hover: #f8f8f8;
--nodedc-glass-control-active: rgba(32, 32, 34, 0.96); --nodedc-glass-control-active: rgba(32, 32, 34, 0.96);
--nodedc-glass-control-active-text: rgba(247, 248, 244, 0.96); --nodedc-glass-control-active-text: rgba(247, 248, 244, 0.96);
--nodedc-field-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.3)), rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity)); --nodedc-field-bg: rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
--nodedc-overlay-bg: rgba(24, 32, 29, 0.22); --nodedc-overlay-bg: rgba(24, 32, 29, 0.22);
--nodedc-glass-rim: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(24, 32, 29, 0.1)); --nodedc-glass-rim: transparent;
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 10px 22px rgba(24, 32, 29, 0.08); --nodedc-glass-control-shadow: 0 10px 22px rgba(24, 32, 29, 0.08);
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16); --nodedc-glass-panel-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(24, 32, 29, 0.2), 0 6px 18px rgba(24, 32, 29, 0.1); --nodedc-glass-dropdown-shadow: 0 22px 60px rgba(24, 32, 29, 0.2), 0 6px 18px rgba(24, 32, 29, 0.1);
--nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.1); --nodedc-focus-surface: rgb(var(--nodedc-accent-rgb) / 0.1);
--nodedc-glass-outline: rgba(24, 32, 29, 0.12); --nodedc-glass-outline: rgba(24, 32, 29, 0.12);
--nodedc-app-panel-surface: var(--nodedc-glass-panel-surface-strong); --nodedc-app-panel-surface: var(--nodedc-glass-panel-surface-strong);
--nodedc-app-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16); --nodedc-app-panel-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
--nodedc-app-panel-shadow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 82px rgba(24, 32, 29, 0.2); --nodedc-app-panel-shadow-strong: 0 18px 44px rgba(0, 0, 0, 0.14);
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12); --nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
--nodedc-panel-action-border-hover: rgba(24, 32, 29, 0.22); --nodedc-panel-action-border-hover: rgba(24, 32, 29, 0.22);
--nodedc-panel-item-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(255, 255, 255, 0.16)), rgba(255, 255, 255, 0.28); --nodedc-panel-item-bg: #f4f4f4;
--nodedc-panel-item-hover-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.24)), rgba(255, 255, 255, 0.38); --nodedc-panel-item-hover-bg: #eeeeee;
--nodedc-panel-item-active-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.28)), rgba(255, 255, 255, 0.46); --nodedc-panel-item-active-bg: #e8e8e8;
--nodedc-panel-icon-bg: rgba(255, 255, 255, 0.38); --nodedc-panel-icon-bg: #ffffff;
--nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96); --nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96);
--nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96); --nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96);
--nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22); --nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22);
@ -113,4 +115,6 @@
--nodedc-primary-action-hover: rgba(8, 8, 10, 0.98); --nodedc-primary-action-hover: rgba(8, 8, 10, 0.98);
--nodedc-primary-action-color: rgba(247, 248, 244, 0.96); --nodedc-primary-action-color: rgba(247, 248, 244, 0.96);
--nodedc-primary-action-shadow: none; --nodedc-primary-action-shadow: none;
--nodedc-modal-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.36)), rgba(255, 255, 255, 0.72);
--nodedc-modal-shadow: 0 24px 72px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.8);
} }

View File

@ -54,8 +54,6 @@
background: var(--nodedc-glass-panel-surface); background: var(--nodedc-glass-panel-surface);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
box-shadow: var(--nodedc-glass-panel-shadow); box-shadow: var(--nodedc-glass-panel-shadow);
backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.28) brightness(1.05);
-webkit-backdrop-filter: blur(var(--nodedc-blur-panel)) saturate(1.28) brightness(1.05);
} }
.nodedc-glass[data-tone="strong"] { .nodedc-glass[data-tone="strong"] {
@ -652,6 +650,7 @@ textarea.nodedc-field__control {
.nodedc-range { .nodedc-range {
--nodedc-range-progress: 50%; --nodedc-range-progress: 50%;
position: relative; position: relative;
display: block;
width: 100%; width: 100%;
height: var(--nodedc-control-height); height: var(--nodedc-control-height);
overflow: hidden; overflow: hidden;
@ -918,11 +917,9 @@ textarea.nodedc-field__control {
align-items: center; align-items: center;
gap: 0.18rem; gap: 0.18rem;
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-glass-panel-bg-soft); background: var(--nodedc-header-pill-bg);
padding: 0.32rem; padding: 0.32rem;
box-shadow: var(--nodedc-glass-control-shadow); box-shadow: var(--nodedc-glass-control-shadow);
backdrop-filter: blur(var(--nodedc-blur-control)) saturate(1.2);
-webkit-backdrop-filter: blur(var(--nodedc-blur-control)) saturate(1.2);
} }
.nodedc-segmented__item { .nodedc-segmented__item {
@ -1246,8 +1243,6 @@ textarea.nodedc-field__control {
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
padding: 1rem; padding: 1rem;
box-shadow: var(--nodedc-app-panel-shadow); box-shadow: var(--nodedc-app-panel-shadow);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
animation: nodedc-application-panel-in 460ms cubic-bezier(0.22, 1, 0.36, 1) both; animation: nodedc-application-panel-in 460ms cubic-bezier(0.22, 1, 0.36, 1) both;
} }
@ -1353,8 +1348,6 @@ textarea.nodedc-field__control {
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
padding: 1.1rem; padding: 1.1rem;
box-shadow: var(--nodedc-app-panel-shadow-strong); box-shadow: var(--nodedc-app-panel-shadow-strong);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
animation: nodedc-admin-panel-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both; animation: nodedc-admin-panel-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
} }
@ -1634,8 +1627,6 @@ textarea.nodedc-field__control {
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
padding: 0.45rem; padding: 0.45rem;
box-shadow: var(--nodedc-glass-dropdown-shadow); box-shadow: var(--nodedc-glass-dropdown-shadow);
backdrop-filter: blur(var(--nodedc-blur-dropdown)) saturate(1.2) brightness(1.02);
-webkit-backdrop-filter: blur(var(--nodedc-blur-dropdown)) saturate(1.2) brightness(1.02);
animation: nodedc-popover-in var(--nodedc-duration-fast) var(--nodedc-ease-standard); animation: nodedc-popover-in var(--nodedc-duration-fast) var(--nodedc-ease-standard);
} }
@ -1885,9 +1876,9 @@ textarea.nodedc-field__control {
overflow: hidden; overflow: hidden;
border: 0; border: 0;
border-radius: var(--nodedc-radius-modal); border-radius: var(--nodedc-radius-modal);
background: var(--nodedc-glass-panel-surface-strong); background: var(--nodedc-modal-surface);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
box-shadow: var(--nodedc-glass-panel-shadow); box-shadow: var(--nodedc-modal-shadow);
backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06); backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
-webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06); -webkit-backdrop-filter: blur(var(--nodedc-blur-modal)) saturate(1.38) brightness(1.06);
animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard); animation: nodedc-window-in var(--nodedc-duration-normal) var(--nodedc-ease-standard);
@ -1905,6 +1896,10 @@ textarea.nodedc-field__control {
width: min(var(--nodedc-inspector-width), calc(100vw - 3rem)); width: min(var(--nodedc-inspector-width), calc(100vw - 3rem));
max-height: calc(100vh - 7.25rem); max-height: calc(100vh - 7.25rem);
border-radius: var(--nodedc-radius-panel); border-radius: var(--nodedc-radius-panel);
background: var(--nodedc-app-panel-surface);
box-shadow: var(--nodedc-app-panel-shadow);
backdrop-filter: none;
-webkit-backdrop-filter: none;
pointer-events: auto; pointer-events: auto;
} }
@ -2085,8 +2080,6 @@ textarea.nodedc-field__control {
background: var(--nodedc-glass-panel-surface-strong); background: var(--nodedc-glass-panel-surface-strong);
padding: 0.6rem; padding: 0.6rem;
box-shadow: var(--nodedc-glass-dropdown-shadow); box-shadow: var(--nodedc-glass-dropdown-shadow);
backdrop-filter: blur(var(--nodedc-blur-dropdown)) saturate(1.2);
-webkit-backdrop-filter: blur(var(--nodedc-blur-dropdown)) saturate(1.2);
} }
.nodedc-share-access__summary { .nodedc-share-access__summary {
@ -2347,11 +2340,6 @@ textarea.nodedc-field__control {
gap: var(--nodedc-inspector-control-gap); gap: var(--nodedc-inspector-control-gap);
} }
.nodedc-control-row[data-action="true"] {
grid-template-columns: minmax(6.4rem, 1fr) minmax(0, 1fr) var(--nodedc-control-height);
gap: var(--nodedc-space-2);
}
.nodedc-control-row[data-layout="stack"] { .nodedc-control-row[data-layout="stack"] {
grid-template-columns: 1fr; grid-template-columns: 1fr;
align-items: stretch; align-items: stretch;
@ -2369,12 +2357,6 @@ textarea.nodedc-field__control {
min-width: 0; min-width: 0;
} }
.nodedc-control-row__action {
display: grid;
min-width: 0;
place-items: center;
}
.nodedc-empty-state { .nodedc-empty-state {
display: grid; display: grid;
min-height: 12rem; min-height: 12rem;
@ -2517,23 +2499,6 @@ textarea.nodedc-field__control {
gap: var(--nodedc-space-2); gap: var(--nodedc-space-2);
} }
.nodedc-control-row[data-action="true"] {
grid-template-columns: minmax(0, 1fr) var(--nodedc-control-height);
}
.nodedc-control-row[data-action="true"] .nodedc-control-row__label {
grid-column: 1 / -1;
}
.nodedc-control-row[data-action="true"] .nodedc-control-row__control {
grid-column: 1;
}
.nodedc-control-row[data-action="true"] .nodedc-control-row__action {
grid-column: 2;
grid-row: 2;
}
.nodedc-media-control { .nodedc-media-control {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);

View File

@ -88,22 +88,18 @@ export function Inspector({
export interface ControlRowProps { export interface ControlRowProps {
label: ReactNode; label: ReactNode;
children: ReactNode; children: ReactNode;
/** Optional trailing action, for example a per-control save-to-layout affordance. */
action?: ReactNode;
layout?: "inline" | "stack"; layout?: "inline" | "stack";
className?: string; className?: string;
} }
export function ControlRow({ label, children, action, layout = "inline", className }: ControlRowProps) { export function ControlRow({ label, children, layout = "inline", className }: ControlRowProps) {
return ( return (
<div <div
className={cn("nodedc-control-row", className)} className={cn("nodedc-control-row", className)}
data-layout={layout === "inline" ? undefined : layout} data-layout={layout === "inline" ? undefined : layout}
data-action={action ? "true" : undefined}
> >
<div className="nodedc-control-row__label">{label}</div> <div className="nodedc-control-row__label">{label}</div>
<div className="nodedc-control-row__control">{children}</div> <div className="nodedc-control-row__control">{children}</div>
{action ? <div className="nodedc-control-row__action">{action}</div> : null}
</div> </div>
); );
} }