Refine SEO light theme and environment controls

This commit is contained in:
DCCONSTRUCTIONS 2026-07-10 20:22:42 +03:00
parent f1e28c267a
commit 4c74c22a6e
21 changed files with 437 additions and 155 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.5.1 — SEO light glass and controllable Environment Settings
- Replaced hard-coded dark Application/Admin panel surfaces with theme tokens; the light preset now uses the audited SEO white-glass panel treatment.
- Rebuilt `ColorField` from the Engine/BIM swatch-and-HEX anatomy with a portable saturation/value palette, hue slider and quick colors instead of a browser-native dark picker.
- Made the application accent a true global runtime variable: it now flows through Environment Settings, active actions, range fill, media actions and theme-aware windows.
- Moved the complete Engine Environment Settings specimen into `Guideline → Контролы`, including split select and collapsible sections; removed the separate catalog Inspector route.
- Reduced default icon optical size/weight again while preserving all button and circular hit targets.
## 0.5.0 — media stage and modal inventory ## 0.5.0 — media stage and modal inventory
- Switched the default NODE.DC application accent to pink while retaining live accent editing and semantic status colors. - Switched the default NODE.DC application accent to pink while retaining live accent editing and semantic status colors.

View File

@ -1,6 +1,6 @@
{ {
"name": "@nodedc/ui-catalog", "name": "@nodedc/ui-catalog",
"version": "0.5.0", "version": "0.5.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -9,8 +9,8 @@
"typecheck": "tsc -b --pretty false" "typecheck": "tsc -b --pretty false"
}, },
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0", "@nodedc/ui-core": "0.5.1",
"@nodedc/ui-react": "0.5.0", "@nodedc/ui-react": "0.5.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"
}, },

View File

@ -38,12 +38,7 @@ import {
type ShareAccessMember, type ShareAccessMember,
} from "@nodedc/ui-react"; } from "@nodedc/ui-react";
type CatalogSection = "foundation" | "controls" | "media" | "windows" | "modals" | "inspector" | "icons"; type CatalogSection = "foundation" | "controls" | "media" | "windows" | "modals" | "icons";
const engineAccentStyle = {
"--nodedc-accent-rgb": "255 47 146",
"--nodedc-on-accent-rgb": "11 17 23",
} as CSSProperties;
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [ const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
{ label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" }, { label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" },
@ -76,7 +71,7 @@ const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: strin
controls: { controls: {
eyebrow: "02 / CONTROLS", eyebrow: "02 / CONTROLS",
title: "Контролы", title: "Контролы",
description: "Кнопки, поля, selection, Engine-настройки и понятные статусы операций.", description: "Кнопки, поля, выбор, Environment Settings и сворачиваемые секции нового Engine.",
icon: "sliders", icon: "sliders",
}, },
media: { media: {
@ -97,14 +92,8 @@ const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: strin
description: "Полная карта modal-паттернов Launcher, нового Engine и BIM Viewer.", description: "Полная карта modal-паттернов Launcher, нового Engine и BIM Viewer.",
icon: "clipboard", icon: "clipboard",
}, },
inspector: {
eyebrow: "06 / INSPECTOR",
title: "Инспектор",
description: "Точный новый Engine Environment Settings без legacy-инспекторов.",
icon: "settings",
},
icons: { icons: {
eyebrow: "07 / ICONS", eyebrow: "06 / ICONS",
title: "Иконки", title: "Иконки",
description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.", description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.",
icon: "grid", icon: "grid",
@ -232,7 +221,7 @@ export function CatalogApp() {
const [fillColor, setFillColor] = useState("#ff6caf"); const [fillColor, setFillColor] = useState("#ff6caf");
const [strokeColor, setStrokeColor] = useState("#2b2b36"); const [strokeColor, setStrokeColor] = useState("#2b2b36");
const [windowOpen, setWindowOpen] = useState(false); const [windowOpen, setWindowOpen] = useState(false);
const [inspectorOpen, setInspectorOpen] = useState(false); const [sidePanelOpen, setSidePanelOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const [shareAccessOpen, setShareAccessOpen] = useState(false); const [shareAccessOpen, setShareAccessOpen] = useState(false);
@ -307,7 +296,7 @@ export function CatalogApp() {
const closeGuideline = workspace.closeNavigation; const closeGuideline = workspace.closeNavigation;
const inspectorSections = [ const environmentSections = [
{ {
id: "illumination", id: "illumination",
label: "Подсветка", label: "Подсветка",
@ -384,7 +373,8 @@ export function CatalogApp() {
</button> </button>
))} ))}
</div> </div>
<ColorField label="Свой accent" value={accentHex} onChange={setAccentHex} /> <ColorField label="Акцент приложения" value={accentHex} onChange={setAccentHex} />
<p className="catalog-preview__explanation">Акцент сразу перекрашивает environment-controls, активные действия, заливку range, file action и состояния окон. Геометрия и semantic-status при этом не меняются.</p>
</Preview> </Preview>
<Preview title="Иерархия стекла" note="default / strong / soft"> <Preview title="Иерархия стекла" note="default / strong / soft">
<div className="catalog-surface-stack"> <div className="catalog-surface-stack">
@ -427,7 +417,6 @@ export function CatalogApp() {
<Select label="Статус" value={selectedStatus} options={[...selectOptions]} searchable onChange={setSelectedStatus} /> <Select label="Статус" value={selectedStatus} options={[...selectOptions]} searchable onChange={setSelectedStatus} />
</FieldFrame> </FieldFrame>
<FieldFrame label="Engine / split select"> <FieldFrame label="Engine / split select">
<div style={engineAccentStyle}>
<Select <Select
variant="split" variant="split"
label="Тип связи" label="Тип связи"
@ -439,7 +428,6 @@ export function CatalogApp() {
]} ]}
onChange={setConnectionType} onChange={setConnectionType}
/> />
</div>
</FieldFrame> </FieldFrame>
<Dropdown <Dropdown
trigger={({ open, toggle, setTriggerRef, surfaceId }) => ( trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
@ -456,12 +444,13 @@ export function CatalogApp() {
</Dropdown> </Dropdown>
</div> </div>
</Preview> </Preview>
<Preview title="Environment controls" note="точная геометрия Engine"> <Preview title="Environment controls" note="Engine / select + accordion" className="catalog-preview--wide">
<div className="catalog-engine-control-stack" style={engineAccentStyle}> <div className="catalog-environment-demo">
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow> <GlassSurface tone="strong" radius="panel" className="catalog-environment-card">
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} /> <div className="catalog-environment-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
<Checker checked={checker} label="Использовать цвет портов" onChange={setChecker} /> <Inspector sections={environmentSections} defaultOpen={["illumination", "connections"]} />
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow> </GlassSurface>
<p className="catalog-preview__explanation">Здесь собраны все контролы, которые нужны новым приложениям: color palette, range, checker, split-select, accent action и раскрывающиеся sections. Отдельной вкладки Inspector в каталоге больше нет.</p>
</div> </div>
</Preview> </Preview>
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide"> <Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">
@ -517,7 +506,7 @@ export function CatalogApp() {
<p className="catalog-preview__explanation">Обычное окно блокирует фон и удерживает фокус. Правый инспектор не затемняет, не блокирует stage и не превращается в modal.</p> <p className="catalog-preview__explanation">Обычное окно блокирует фон и удерживает фокус. Правый инспектор не затемняет, не блокирует stage и не превращается в modal.</p>
<div className="catalog-inline catalog-inline--wrap"> <div className="catalog-inline catalog-inline--wrap">
<Button variant="accent" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button> <Button variant="accent" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button> <Button shape="pill" icon={<Icon name="panel" />} onClick={() => setSidePanelOpen(true)}>Открыть боковую панель</Button>
</div> </div>
</Preview> </Preview>
<Preview title="Оконные действия" note="круг `46 px`"> <Preview title="Оконные действия" note="круг `46 px`">
@ -563,15 +552,6 @@ export function CatalogApp() {
</Preview> </Preview>
</div> </div>
); );
case "inspector":
return (
<div className="catalog-inspector-stage">
<GlassSurface tone="strong" radius="panel" className="catalog-inspector-card" style={engineAccentStyle}>
<div className="catalog-inspector-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
</GlassSurface>
</div>
);
case "icons": case "icons":
return ( return (
<div className="catalog-icon-catalog"> <div className="catalog-icon-catalog">
@ -582,7 +562,7 @@ export function CatalogApp() {
<IconButton label="Развернуть"><Icon name="expand" size={20} /></IconButton> <IconButton label="Развернуть"><Icon name="expand" size={20} /></IconButton>
<IconButton label="Закрыть"><Icon name="close" size={18} /></IconButton> <IconButton label="Закрыть"><Icon name="close" size={18} /></IconButton>
</div> </div>
<p className="catalog-preview__explanation">Иконка наследует `currentColor`; фон, размер кнопки и active-state задаёт компонент поверхности, а не SVG.</p> <p className="catalog-preview__explanation">Иконка наследует `currentColor`; фон, размер кнопки и active-state задаёт компонент поверхности, а не SVG. Glyph оптически уменьшен на 18% без изменения hit target.</p>
</Preview> </Preview>
{iconGroups.map((group) => ( {iconGroups.map((group) => (
<section className="catalog-icon-group" key={group.title}> <section className="catalog-icon-group" key={group.title}>
@ -668,7 +648,7 @@ export function CatalogApp() {
footer={ footer={
<> <>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span> <span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
<span>Design System 0.5</span> <span>Design System 0.5.1</span>
</> </>
} }
/> />
@ -708,14 +688,16 @@ export function CatalogApp() {
</Window> </Window>
<Window <Window
open={inspectorOpen} open={sidePanelOpen}
title="Настройки окружения" title="Боковая панель"
subtitle="ENGINE / новый канон" subtitle="modeless / shell contract"
placement="end" placement="end"
style={engineAccentStyle} onClose={() => setSidePanelOpen(false)}
onClose={() => setInspectorOpen(false)}
> >
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} /> <div className="catalog-form">
<TextField label="Название панели" value="Параметры рабочей области" readOnly />
<p className="catalog-preview__explanation">Боковая панель демонстрирует только layer-механику: она modeless, не затемняет stage и не забирает фокус. Environment controls живут в разделе «Контролы».</p>
</div>
</Window> </Window>
<Window <Window

View File

@ -14,7 +14,7 @@ body,
} }
body { body {
background: #050506; background: var(--nodedc-canvas);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
} }
@ -38,8 +38,8 @@ textarea {
isolation: isolate; isolation: isolate;
overflow: hidden; overflow: hidden;
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);
background: #0b0b0d url("/launcher-stage-poster.png") center / cover no-repeat; background: var(--nodedc-canvas-soft) url("/launcher-stage-poster.png") center / cover no-repeat;
box-shadow: 0 44px 150px rgba(0, 0, 0, 0.62); box-shadow: 0 44px 150px color-mix(in srgb, var(--nodedc-canvas) 70%, transparent);
} }
.catalog-launcher-stage__media, .catalog-launcher-stage__media,
@ -229,7 +229,7 @@ textarea {
grid-template-columns: minmax(12rem, 1.7fr) minmax(7rem, 0.7fr) minmax(6rem, 0.6fr) minmax(9rem, 0.9fr) 6rem; grid-template-columns: minmax(12rem, 1.7fr) minmax(7rem, 0.7fr) minmax(6rem, 0.6fr) minmax(9rem, 0.9fr) 6rem;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid var(--nodedc-glass-outline);
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
padding: 0.62rem 0.8rem; padding: 0.62rem 0.8rem;
font-size: var(--nodedc-font-size-sm); font-size: var(--nodedc-font-size-sm);
@ -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: rgba(0, 0, 0, 0.22); background: color-mix(in srgb, var(--nodedc-canvas-soft) 76%, transparent);
padding: 0.6rem; padding: 0.6rem;
} }
@ -307,30 +307,31 @@ textarea {
background: color-mix(in srgb, var(--nodedc-glass-control-hover) 70%, transparent) !important; background: color-mix(in srgb, var(--nodedc-glass-control-hover) 70%, transparent) !important;
} }
.catalog-inspector-stage { .catalog-environment-demo {
display: flex; display: grid;
min-height: 43rem; min-height: 30rem;
justify-content: flex-end; grid-template-columns: minmax(0, 24.375rem) minmax(14rem, 1fr);
align-items: flex-start; align-items: start;
gap: clamp(1rem, 4vw, 4rem);
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);
background: background:
radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem), radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem),
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent); color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
padding: 2rem; padding: clamp(1rem, 3vw, 2rem);
} }
.catalog-inspector-card { .catalog-environment-card {
width: min(24.375rem, 100%); width: min(24.375rem, 100%);
padding: 1.125rem 0.75rem; padding: 1.125rem 0.75rem;
} }
.catalog-inspector-title { .catalog-environment-title {
display: grid; display: grid;
gap: 0.3rem; gap: 0.3rem;
padding: 0 0.875rem 0.875rem; padding: 0 0.875rem 0.875rem;
} }
.catalog-inspector-title span { .catalog-environment-title span {
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
font-size: 0.7rem; font-size: 0.7rem;
} }
@ -393,6 +394,10 @@ textarea {
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
} }
.catalog-icon-tile__surface > svg {
transform: scale(var(--nodedc-icon-glyph-scale));
}
.catalog-icon-tile > span:last-child { .catalog-icon-tile > span:last-child {
display: grid; display: grid;
min-width: 0; min-width: 0;
@ -499,9 +504,9 @@ textarea {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.catalog-inspector-stage { .catalog-environment-demo {
min-height: 40rem; min-height: auto;
justify-content: center; grid-template-columns: 1fr;
padding: 0.75rem; padding: 0.75rem;
} }

View File

@ -39,7 +39,9 @@ Pill-range с заполнением акцентным цветом, встро
## ColorField ## ColorField
Связка круглого color picker и текстового значения. Валидация конкретного формата остаётся у формы приложения. Engine/BIM-геометрия color field: круглый swatch и HEX-значение в pill control. Swatch открывает общий portal-picker: saturation/value plane, hue slider и быстрые цвета. Это устраняет зависимость от тёмной системной палитры браузера и делает выбор одинаковым в dark/light.
Значение контролируемое; приложение по-прежнему отвечает за допустимый формат, сохранение и доменную реакцию на цвет.
## Dropdown ## Dropdown
@ -140,7 +142,7 @@ Pill navigation для верхней панели и компактного п
## Icon ## Icon
`Icon` предоставляет только подтверждённый общий subset иконок. Каноническое имя описывает смысл (`close`, `expand`, `refresh`), а не конкретный путь SVG. Default glyph — `17 px`, stroke — `1.7`; внутри кнопок glyph оптически уменьшается до `90%`, но размеры surface/hit target остаются прежними. `Icon` предоставляет только подтверждённый общий subset иконок. Каноническое имя описывает смысл (`close`, `expand`, `refresh`), а не конкретный путь SVG. Default glyph — `16 px`, stroke — `1.6`; внутри кнопок glyph оптически уменьшается до `78%`, но размеры surface/hit target остаются прежними.
Поверхность, круглая форма, hit target, active и disabled состояния принадлежат `IconButton`, `Button` или navigation item. Полный список находится в `registry/icons.json` и `docs/ICONS.md`. Поверхность, круглая форма, hit target, active и disabled состояния принадлежат `IconButton`, `Button` или navigation item. Полный список находится в `registry/icons.json` и `docs/ICONS.md`.
@ -148,7 +150,7 @@ Pill navigation для верхней панели и компактного п
Статус использует semantic tone: neutral, success, warning, danger или accent. Бизнес-статус приложения маппится на tone в самом приложении. Статус использует semantic tone: neutral, success, warning, danger или accent. Бизнес-статус приложения маппится на tone в самом приложении.
## Inspector и ControlRow ## Environment Controls: Inspector и ControlRow
Единая accordion-панель для settings и definition controls. Источник — только новый Environment Settings и NDC Agent Inspector. Единая accordion-панель для settings и definition controls. Источник — только новый Environment Settings и NDC Agent Inspector.
@ -162,4 +164,4 @@ Inspector владеет:
Для Engine Environment Settings desktop-контракт точный: окно `390 px`, рабочая колонка `330 px`, строка `154 + 14 + 162 px`, высота контрола `46 px`, section header `50 px` с радиусом `12 px`. Accent-filled section headers и полноширинные range/checker/select сохраняют геометрию исходника. Для Engine Environment Settings desktop-контракт точный: окно `390 px`, рабочая колонка `330 px`, строка `154 + 14 + 162 px`, высота контрола `46 px`, section header `50 px` с радиусом `12 px`. Accent-filled section headers и полноширинные range/checker/select сохраняют геометрию исходника.
Engine продолжает владеть определениями полей, типами нод и сохранением значений. Engine продолжает владеть определениями полей, типами нод и сохранением значений. В живом catalog эти collapsible sections находятся в `Guideline → Контролы`; отдельной product-вкладки Inspector нет.

View File

@ -6,11 +6,11 @@
| Контекст | Размер glyph | Hit target | | Контекст | Размер glyph | Hit target |
| --- | ---: | ---: | | --- | ---: | ---: |
| Плотная строка/label | `1516px` | задаёт строка | | Плотная строка/label | `1415px` | задаёт строка |
| Обычная кнопка | `1617px` | `46px` круг или кнопка с текстом | | Обычная кнопка | `1516px` | `46px` круг или кнопка с текстом |
| Крупное оконное действие | `18px` | `46.72px` круг | | Крупное оконное действие | `1718px` | `46.72px` круг |
Базовый stroke — `1.7`. Внутри кнопок glyph дополнительно получает optical scale `0.9`; hit target и наружная окружность не уменьшаются. Иконка наследует `currentColor`. Фон и active/disabled/hover состояния принадлежат `IconButton`, `Button`, navigation item или другому компоненту поверхности. Базовый stroke — `1.6`. Внутри кнопок glyph дополнительно получает optical scale `0.78`; hit target и наружная окружность не уменьшаются. Иконка наследует `currentColor`. Фон и active/disabled/hover состояния принадлежат `IconButton`, `Button`, navigation item или другому компоненту поверхности.
## Группы ## Группы

View File

@ -25,7 +25,7 @@
Рассматриваются как одна продуктовая семья с Launcher/Hub. Их различия должны выражаться темой и содержанием, а не отдельной реализацией окон и controls. Рассматриваются как одна продуктовая семья с Launcher/Hub. Их различия должны выражаться темой и содержанием, а не отдельной реализацией окон и controls.
SEO содержит большой объём накопленных локальных overrides. Они являются материалом аудита, но не переносятся автоматически. В библиотеку попадает очищенный общий контракт. SEO содержит большой объём накопленных локальных overrides. Они являются материалом аудита, но не переносятся автоматически. В библиотеку попадает очищенный общий контракт. Его light palette закреплена по `app/src/styles.css`: canvas `#ececec`, white glass `rgba(255,255,255,0.480.72)`, светлый panel gradient и нейтральная тёмная типографика.
CMS подтверждает необходимость DOM-пакета без React. `admin/admin.js` и `admin/admin.css` независимо повторяют устойчивый media pattern с file/URL source, круглым preview, path и upload hint; поэтому он перенесён в baseline, а не оставлен product candidate. CMS подтверждает необходимость DOM-пакета без React. `admin/admin.js` и `admin/admin.css` независимо повторяют устойчивый media pattern с file/URL source, круглым preview, path и upload hint; поэтому он перенесён в baseline, а не оставлен product candidate.
@ -45,6 +45,8 @@ CMS подтверждает необходимость DOM-пакета без
Размеры зафиксированы непосредственно по разрешённым исходникам: панель `390 px`, контент `330 px`, control row `154/14/162 px`, control height `46 px`, split-select `276/8/46 px`, section header `50 px` и radius `12 px`. Скриншоты от 10 июля 2026 года использованы как визуальная проверка, а не как замена исходному CSS. Размеры зафиксированы непосредственно по разрешённым исходникам: панель `390 px`, контент `330 px`, control row `154/14/162 px`, control height `46 px`, split-select `276/8/46 px`, section header `50 px` и radius `12 px`. Скриншоты от 10 июля 2026 года использованы как визуальная проверка, а не как замена исходному CSS.
`EnvironmentColorField` и BIM-эквивалент подтверждают каноническую anatomy `26 px` swatch + HEX value внутри glass pill. Библиотека сохраняет эту геометрию, но заменяет OS-native popup на единый portal-picker, чтобы палитра не зависела от светлой/тёмной системной темы браузера.
Workflow sharing добавлен как отдельный разрешённый reference: modal `600 px`, compact stack до трёх avatar, roles `viewer/editor/admin`, immutable owner и приглашение через email. ACL endpoints, directory lookup и тексты backend-ошибок остаются Engine-domain. Workflow sharing добавлен как отдельный разрешённый reference: modal `600 px`, compact stack до трёх avatar, roles `viewer/editor/admin`, immutable owner и приглашение через email. ACL endpoints, directory lookup и тексты backend-ошибок остаются Engine-domain.
## Task Manager / Ops ## Task Manager / Ops

View File

@ -31,14 +31,14 @@ NODE.DC-приложения отличаются цветовой схемой,
В baseline включены `dark` и `light`. Это не два отдельных продукта, а примеры одного контракта: В baseline включены `dark` и `light`. Это не два отдельных продукта, а примеры одного контракта:
- dark соответствует текущей основной семье Launcher/Hub/CMS; - dark соответствует текущей основной семье Launcher/Hub/CMS;
- light фиксирует возможность светлой схемы, наблюдаемой в SEO; - light воспроизводит SEO-поверхности: `#ececec` canvas, полупрозрачные белые glass panels, тёмный текст и тонкие нейтральные outlines;
- конкретный продукт может поверх 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`. Каталог позволяет менять его через presets и `ColorField`, чтобы проверить любой consumer-цвет без создания новой темы или 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 компонентов.
Semantic status не обязан совпадать с accent. Например, ошибка остаётся danger, даже если приложение использует красный брендовый accent. Semantic status не обязан совпадать с accent. Например, ошибка остаётся danger, даже если приложение использует красный брендовый accent.

24
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "nodedc-design-guideline", "name": "nodedc-design-guideline",
"version": "0.5.0", "version": "0.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nodedc-design-guideline", "name": "nodedc-design-guideline",
"version": "0.5.0", "version": "0.5.1",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
"apps/*" "apps/*"
@ -25,10 +25,10 @@
}, },
"apps/catalog": { "apps/catalog": {
"name": "@nodedc/ui-catalog", "name": "@nodedc/ui-catalog",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0", "@nodedc/ui-core": "0.5.1",
"@nodedc/ui-react": "0.5.0", "@nodedc/ui-react": "0.5.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"
}, },
@ -1888,27 +1888,27 @@
}, },
"packages/tokens": { "packages/tokens": {
"name": "@nodedc/tokens", "name": "@nodedc/tokens",
"version": "0.5.0" "version": "0.5.1"
}, },
"packages/ui-core": { "packages/ui-core": {
"name": "@nodedc/ui-core", "name": "@nodedc/ui-core",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"@nodedc/tokens": "0.5.0" "@nodedc/tokens": "0.5.1"
} }
}, },
"packages/ui-dom": { "packages/ui-dom": {
"name": "@nodedc/ui-dom", "name": "@nodedc/ui-dom",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0" "@nodedc/ui-core": "0.5.1"
} }
}, },
"packages/ui-react": { "packages/ui-react": {
"name": "@nodedc/ui-react", "name": "@nodedc/ui-react",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0", "@nodedc/ui-core": "0.5.1",
"lucide-react": "^0.468.0" "lucide-react": "^0.468.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "nodedc-design-guideline", "name": "nodedc-design-guideline",
"version": "0.5.0", "version": "0.5.1",
"private": true, "private": true,
"description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.", "description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.",
"type": "module", "type": "module",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nodedc/tokens", "name": "@nodedc/tokens",
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"files": ["tokens.json", "tokens.css", "themes.css"], "files": ["tokens.json", "tokens.css", "themes.css"],
"exports": { "exports": {

View File

@ -31,6 +31,19 @@
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.075), 0 22px 72px rgba(0, 0, 0, 0.42); --nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.075), 0 22px 72px rgba(0, 0, 0, 0.42);
--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: rgba(var(--nodedc-accent-rgb), 0.14); --nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.14);
--nodedc-glass-outline: rgba(255, 255, 255, 0.11);
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)), rgba(10, 10, 13, 0.9);
--nodedc-app-panel-shadow: 0 34px 110px rgba(0, 0, 0, 0.42);
--nodedc-app-panel-shadow-strong: 0 34px 110px rgba(0, 0, 0, 0.52);
--nodedc-panel-action-border: rgba(255, 255, 255, 0.22);
--nodedc-panel-action-border-hover: rgba(255, 255, 255, 0.3);
--nodedc-panel-item-bg: rgba(255, 255, 255, 0.04);
--nodedc-panel-item-hover-bg: rgba(255, 255, 255, 0.085);
--nodedc-panel-item-active-bg: rgba(255, 255, 255, 0.115);
--nodedc-panel-icon-bg: rgba(255, 255, 255, 0.045);
--nodedc-panel-active-icon-bg: rgba(247, 248, 244, 0.96);
--nodedc-panel-active-icon-color: rgb(var(--nodedc-on-accent-rgb));
--nodedc-scrollbar-thumb: rgba(255, 255, 255, 0.15);
} }
[data-nodedc-theme="light"] { [data-nodedc-theme="light"] {
@ -65,4 +78,17 @@
--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: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
--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: rgba(var(--nodedc-accent-rgb), 0.1); --nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.1);
--nodedc-glass-outline: rgba(24, 32, 29, 0.12);
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 255, 255, 0.22) 100%), rgba(255, 255, 255, 0.48);
--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-strong: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 82px rgba(24, 32, 29, 0.2);
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
--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-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-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-icon-bg: rgba(255, 255, 255, 0.38);
--nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96);
--nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96);
--nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22);
} }

View File

@ -25,7 +25,7 @@
--nodedc-inspector-button-radius: 1rem; --nodedc-inspector-button-radius: 1rem;
--nodedc-select-toggle-width: 2.875rem; --nodedc-select-toggle-width: 2.875rem;
--nodedc-select-gap: 0.5rem; --nodedc-select-gap: 0.5rem;
--nodedc-icon-glyph-scale: 0.9; --nodedc-icon-glyph-scale: 0.78;
--nodedc-space-1: 0.25rem; --nodedc-space-1: 0.25rem;
--nodedc-space-2: 0.5rem; --nodedc-space-2: 0.5rem;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nodedc/ui-core", "name": "@nodedc/ui-core",
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"files": ["dist", "styles.css"], "files": ["dist", "styles.css"],
"main": "./dist/index.js", "main": "./dist/index.js",
@ -17,6 +17,6 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@nodedc/tokens": "0.5.0" "@nodedc/tokens": "0.5.1"
} }
} }

View File

@ -723,24 +723,137 @@ textarea.nodedc-field__control {
box-shadow: var(--nodedc-glass-control-shadow); box-shadow: var(--nodedc-glass-control-shadow);
} }
.nodedc-color-field__picker { .nodedc-color-field__dropdown {
position: relative; display: contents;
width: 1.625rem;
height: 1.625rem;
overflow: hidden;
border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-color-value, #000);
} }
.nodedc-color-field__picker input { .nodedc-color-field__picker {
position: absolute; position: relative;
inset: 0; display: grid;
width: 100%; width: 1.625rem;
height: 100%; height: 1.625rem;
opacity: 0; place-items: center;
overflow: hidden;
border: 0;
border-radius: var(--nodedc-radius-circle);
outline: 0;
background: var(--nodedc-color-value, #000);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.24);
cursor: pointer; cursor: pointer;
} }
.nodedc-color-field__picker[data-open="true"],
.nodedc-color-field__picker:focus-visible {
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.3), 0 0 0 3px var(--nodedc-canvas);
}
.nodedc-color-field__picker:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.nodedc-color-palette-surface {
width: min(18.5rem, calc(100vw - 1rem));
overflow: visible;
border-radius: var(--nodedc-radius-card);
background: var(--nodedc-glass-panel-surface-strong);
padding: 0.7rem;
}
.nodedc-color-palette {
display: grid;
gap: 0.75rem;
}
.nodedc-color-palette__spectrum {
position: relative;
aspect-ratio: 1.5 / 1;
overflow: hidden;
border: 1px solid var(--nodedc-glass-outline);
border-radius: var(--nodedc-radius-control);
outline: 0;
background:
linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
linear-gradient(to right, rgba(255, 255, 255, 1), var(--nodedc-color-hue));
cursor: crosshair;
touch-action: none;
}
.nodedc-color-palette__spectrum:focus-visible {
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.28);
}
.nodedc-color-palette__marker {
position: absolute;
top: var(--nodedc-color-v);
left: var(--nodedc-color-s);
width: 0.82rem;
height: 0.82rem;
border: 2px solid rgba(255, 255, 255, 0.96);
border-radius: var(--nodedc-radius-circle);
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
pointer-events: none;
transform: translate(-50%, -50%);
}
.nodedc-color-palette__hue {
display: grid;
gap: 0.35rem;
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
font-weight: var(--nodedc-font-weight-medium);
}
.nodedc-color-palette__hue input {
width: 100%;
height: 0.7rem;
margin: 0;
appearance: none;
border-radius: var(--nodedc-radius-circle);
outline: 0;
background: linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
cursor: pointer;
}
.nodedc-color-palette__hue input::-webkit-slider-thumb {
width: 0.88rem;
height: 0.88rem;
appearance: none;
border: 2px solid rgba(255, 255, 255, 0.96);
border-radius: var(--nodedc-radius-circle);
background: rgba(12, 12, 15, 0.92);
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
}
.nodedc-color-palette__hue input::-moz-range-thumb {
width: 0.68rem;
height: 0.68rem;
border: 2px solid rgba(255, 255, 255, 0.96);
border-radius: var(--nodedc-radius-circle);
background: rgba(12, 12, 15, 0.92);
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
}
.nodedc-color-palette__presets {
display: grid;
grid-template-columns: repeat(10, minmax(0, 1fr));
gap: 0.35rem;
}
.nodedc-color-palette__presets button {
aspect-ratio: 1;
border: 1px solid var(--nodedc-glass-outline);
border-radius: var(--nodedc-radius-circle);
outline: 0;
background: var(--nodedc-palette-swatch);
cursor: pointer;
}
.nodedc-color-palette__presets button[aria-pressed="true"],
.nodedc-color-palette__presets button:focus-visible {
box-shadow: 0 0 0 2px var(--nodedc-canvas), 0 0 0 3px rgb(var(--nodedc-accent-rgb));
}
.nodedc-color-field__text { .nodedc-color-field__text {
width: 100%; width: 100%;
border: 0; border: 0;
@ -1117,12 +1230,10 @@ textarea.nodedc-field__control {
overflow: hidden; overflow: hidden;
border: 0; border: 0;
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);
background: background: var(--nodedc-app-panel-surface);
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)),
rgba(10, 10, 13, 0.9);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
padding: 1rem; padding: 1rem;
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.42); box-shadow: var(--nodedc-app-panel-shadow);
backdrop-filter: blur(28px); backdrop-filter: blur(28px);
-webkit-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;
@ -1186,7 +1297,7 @@ textarea.nodedc-field__control {
width: 2.92rem; width: 2.92rem;
height: 2.92rem; height: 2.92rem;
place-items: center; place-items: center;
border: 1px solid rgba(255, 255, 255, 0.22); border: 1px solid var(--nodedc-panel-action-border);
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
outline: 0; outline: 0;
background: transparent; background: transparent;
@ -1196,7 +1307,7 @@ textarea.nodedc-field__control {
.nodedc-application-panel__action:hover, .nodedc-application-panel__action:hover,
.nodedc-application-panel__action:focus-visible { .nodedc-application-panel__action:focus-visible {
border-color: rgba(255, 255, 255, 0.3); border-color: var(--nodedc-panel-action-border-hover);
background: var(--nodedc-glass-control-hover); background: var(--nodedc-glass-control-hover);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
} }
@ -1212,7 +1323,7 @@ textarea.nodedc-field__control {
overflow: auto; overflow: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent; scrollbar-color: var(--nodedc-scrollbar-thumb) transparent;
} }
.nodedc-admin-panel { .nodedc-admin-panel {
@ -1226,12 +1337,10 @@ textarea.nodedc-field__control {
overflow: hidden; overflow: hidden;
border: 0; border: 0;
border-radius: var(--nodedc-radius-card); border-radius: var(--nodedc-radius-card);
background: background: var(--nodedc-app-panel-surface);
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)),
rgba(10, 10, 13, 0.9);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
padding: 1.1rem; padding: 1.1rem;
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.52); box-shadow: var(--nodedc-app-panel-shadow-strong);
backdrop-filter: blur(28px); backdrop-filter: blur(28px);
-webkit-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;
@ -1281,7 +1390,7 @@ textarea.nodedc-field__control {
width: 2.92rem; width: 2.92rem;
height: 2.92rem; height: 2.92rem;
place-items: center; place-items: center;
border: 1px solid rgba(255, 255, 255, 0.22); border: 1px solid var(--nodedc-panel-action-border);
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
outline: 0; outline: 0;
background: transparent; background: transparent;
@ -1290,7 +1399,7 @@ textarea.nodedc-field__control {
} }
.nodedc-admin-panel__close:hover { .nodedc-admin-panel__close:hover {
border-color: rgba(255, 255, 255, 0.3); border-color: var(--nodedc-panel-action-border-hover);
background: var(--nodedc-glass-control-hover); background: var(--nodedc-glass-control-hover);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
} }
@ -1322,13 +1431,13 @@ textarea.nodedc-field__control {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
overflow: hidden; overflow: hidden;
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
background: rgba(255, 255, 255, 0.04); background: var(--nodedc-panel-item-bg);
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
opacity: 0.72; opacity: 0.72;
} }
.nodedc-admin-context[data-active="true"] { .nodedc-admin-context[data-active="true"] {
background: rgba(74, 74, 74, 0.5); background: var(--nodedc-panel-item-active-bg);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
opacity: 1; opacity: 1;
} }
@ -1378,7 +1487,7 @@ textarea.nodedc-field__control {
flex: 0 0 2.92rem; flex: 0 0 2.92rem;
place-items: center; place-items: center;
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
background: rgba(255, 255, 255, 0.06); background: var(--nodedc-panel-icon-bg);
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
} }
@ -1432,7 +1541,7 @@ textarea.nodedc-field__control {
gap: 0.86rem; gap: 0.86rem;
border: 0; border: 0;
border-radius: var(--nodedc-radius-circle); border-radius: var(--nodedc-radius-circle);
background: rgba(255, 255, 255, 0.04); background: var(--nodedc-panel-item-bg);
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
padding: 5px 0.9rem 5px calc(5px + 2.92rem + 0.86rem); padding: 5px 0.9rem 5px calc(5px + 2.92rem + 0.86rem);
font: inherit; font: inherit;
@ -1449,22 +1558,22 @@ textarea.nodedc-field__control {
} }
.nodedc-admin-panel__nav-item:hover { .nodedc-admin-panel__nav-item:hover {
background: rgba(255, 255, 255, 0.085); background: var(--nodedc-panel-item-hover-bg);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
opacity: 0.92; opacity: 0.92;
transform: translateY(-1px); transform: translateY(-1px);
} }
.nodedc-admin-panel__nav-item[data-active="true"] { .nodedc-admin-panel__nav-item[data-active="true"] {
background: rgba(255, 255, 255, 0.115); background: var(--nodedc-panel-item-active-bg);
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
opacity: 1; opacity: 1;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.048), 0 10px 22px rgba(0, 0, 0, 0.12); box-shadow: var(--nodedc-glass-control-shadow), 0 10px 22px rgba(24, 32, 29, 0.12);
} }
.nodedc-admin-panel__nav-item:focus-visible { .nodedc-admin-panel__nav-item:focus-visible {
outline: 0; outline: 0;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 10px 22px rgba(0, 0, 0, 0.12); box-shadow: inset 0 0 0 1px var(--nodedc-glass-outline), 0 10px 22px rgba(24, 32, 29, 0.12);
} }
.nodedc-app-shell__navigation > .nodedc-admin-panel { .nodedc-app-shell__navigation > .nodedc-admin-panel {
@ -1475,13 +1584,13 @@ textarea.nodedc-field__control {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 5px; left: 5px;
background: rgba(255, 255, 255, 0.045); background: var(--nodedc-panel-icon-bg);
transform: translateY(-50%); transform: translateY(-50%);
} }
.nodedc-admin-panel__nav-item[data-active="true"] .nodedc-admin-panel__nav-icon { .nodedc-admin-panel__nav-item[data-active="true"] .nodedc-admin-panel__nav-icon {
background: rgba(247, 248, 244, 0.96); background: var(--nodedc-panel-active-icon-bg);
color: rgb(var(--nodedc-on-accent-rgb)); color: var(--nodedc-panel-active-icon-color);
} }
.nodedc-admin-panel__footer { .nodedc-admin-panel__footer {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nodedc/ui-dom", "name": "@nodedc/ui-dom",
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"files": ["dist"], "files": ["dist"],
"main": "./dist/index.js", "main": "./dist/index.js",
@ -16,6 +16,6 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0" "@nodedc/ui-core": "0.5.1"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@nodedc/ui-react", "name": "@nodedc/ui-react",
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"files": ["dist"], "files": ["dist"],
"main": "./dist/index.js", "main": "./dist/index.js",
@ -16,7 +16,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@nodedc/ui-core": "0.5.0", "@nodedc/ui-core": "0.5.1",
"lucide-react": "^0.468.0" "lucide-react": "^0.468.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -97,7 +97,7 @@ export interface IconProps extends Omit<LucideProps, "ref"> {
label?: string; label?: string;
} }
export function Icon({ name, label, size = 17, strokeWidth = 1.7, ...props }: IconProps) { export function Icon({ name, label, size = 16, strokeWidth = 1.6, ...props }: IconProps) {
const IconComponent = icons[name]; const IconComponent = icons[name];
return ( return (

View File

@ -1,5 +1,6 @@
import type { CSSProperties, InputHTMLAttributes } from "react"; import { useEffect, useState, type CSSProperties, type InputHTMLAttributes, type PointerEvent } from "react";
import { cn } from "./cn.js"; import { cn } from "./cn.js";
import { Dropdown } from "./Dropdown.js";
export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> { export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
label: string; label: string;
@ -52,18 +53,159 @@ export interface ColorFieldProps extends Omit<InputHTMLAttributes<HTMLInputEleme
label?: string; label?: string;
} }
export function ColorField({ value, onChange, label = "Цвет", className, ...props }: ColorFieldProps) { type Hsv = { h: number; s: number; v: number };
const style = { "--nodedc-color-value": value } as CSSProperties;
const palettePresets = ["#ff2f92", "#ff6caf", "#d746ff", "#7a3cff", "#4ea4ff", "#35d7c1", "#c3ff66", "#ffffff", "#404040", "#111116"];
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function parseHexColor(value: string): [number, number, number] | null {
const match = /^#?([0-9a-f]{6})$/i.exec(value.trim());
if (!match) return null;
const hex = match[1];
return [
Number.parseInt(hex.slice(0, 2), 16),
Number.parseInt(hex.slice(2, 4), 16),
Number.parseInt(hex.slice(4, 6), 16),
];
}
function hexToHsv(value: string): Hsv | null {
const rgb = parseHexColor(value);
if (!rgb) return null;
const [red, green, blue] = rgb.map((channel) => channel / 255) as [number, number, number];
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
const delta = max - min;
let hue = 0;
if (delta) {
if (max === red) hue = 60 * (((green - blue) / delta) % 6);
else if (max === green) hue = 60 * ((blue - red) / delta + 2);
else hue = 60 * ((red - green) / delta + 4);
}
return { h: (hue + 360) % 360, s: max === 0 ? 0 : (delta / max) * 100, v: max * 100 };
}
function hsvToHex({ h, s, v }: Hsv): string {
const hue = ((h % 360) + 360) % 360;
const saturation = clamp(s, 0, 100) / 100;
const value = clamp(v, 0, 100) / 100;
const chroma = value * saturation;
const component = (hue / 60) % 2;
const match = chroma * (1 - Math.abs(component - 1));
const base = hue < 60 ? [chroma, match, 0]
: hue < 120 ? [match, chroma, 0]
: hue < 180 ? [0, chroma, match]
: hue < 240 ? [0, match, chroma]
: hue < 300 ? [match, 0, chroma]
: [chroma, 0, match];
const offset = value - chroma;
return `#${base.map((channel) => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")).join("")}`;
}
export function ColorField({ value, onChange, label = "Цвет", className, disabled = false, ...props }: ColorFieldProps) {
const validColor = hexToHsv(value);
const [hsv, setHsv] = useState<Hsv>(() => validColor ?? { h: 330, s: 82, v: 100 });
const style = { "--nodedc-color-value": validColor ? hsvToHex(validColor) : hsvToHex(hsv) } as CSSProperties;
useEffect(() => {
if (validColor) setHsv(validColor);
}, [value, validColor?.h, validColor?.s, validColor?.v]);
const commit = (next: Hsv) => {
const normalized = { h: clamp(next.h, 0, 360), s: clamp(next.s, 0, 100), v: clamp(next.v, 0, 100) };
setHsv(normalized);
onChange(hsvToHex(normalized));
};
const updateSpectrum = (event: PointerEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
commit({ ...hsv, s: ((event.clientX - rect.left) / rect.width) * 100, v: 100 - ((event.clientY - rect.top) / rect.height) * 100 });
};
return ( return (
<div className={cn("nodedc-color-field", className)} style={style}> <div className={cn("nodedc-color-field", className)} style={style}>
<label className="nodedc-color-field__picker" aria-label={label}> <Dropdown
<input type="color" value={value} onChange={(event) => onChange(event.target.value)} {...props} /> className="nodedc-color-field__dropdown"
placement="bottom-start"
minWidth={296}
width={296}
disabled={disabled}
surfaceRole="dialog"
surfaceClassName="nodedc-color-palette-surface"
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
<button
ref={setTriggerRef}
type="button"
className="nodedc-color-field__picker"
aria-label={`Выбрать ${label}`}
aria-haspopup="dialog"
aria-controls={surfaceId}
aria-expanded={open}
data-open={open ? "true" : undefined}
disabled={disabled}
onClick={toggle}
><span aria-hidden="true" /></button>
)}
>
<div className="nodedc-color-palette" aria-label={`Палитра: ${label}`}>
<div
className="nodedc-color-palette__spectrum"
role="slider"
tabIndex={disabled ? -1 : 0}
aria-label={`${label}: насыщенность и яркость`}
aria-valuetext={hsvToHex(hsv)}
style={{ "--nodedc-color-hue": hsvToHex({ h: hsv.h, s: 100, v: 100 }), "--nodedc-color-s": `${hsv.s}%`, "--nodedc-color-v": `${100 - hsv.v}%` } as CSSProperties}
onPointerDown={(event) => {
if (disabled) return;
event.currentTarget.setPointerCapture(event.pointerId);
updateSpectrum(event);
}}
onPointerMove={(event) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) updateSpectrum(event);
}}
onPointerUp={(event) => {
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
}}
onKeyDown={(event) => {
const delta = event.shiftKey ? 10 : 2;
if (event.key === "ArrowLeft") { event.preventDefault(); commit({ ...hsv, s: hsv.s - delta }); }
if (event.key === "ArrowRight") { event.preventDefault(); commit({ ...hsv, s: hsv.s + delta }); }
if (event.key === "ArrowDown") { event.preventDefault(); commit({ ...hsv, v: hsv.v - delta }); }
if (event.key === "ArrowUp") { event.preventDefault(); commit({ ...hsv, v: hsv.v + delta }); }
}}
><span className="nodedc-color-palette__marker" aria-hidden="true" /></div>
<label className="nodedc-color-palette__hue">
<span>Оттенок</span>
<input type="range" min={0} max={360} value={Math.round(hsv.h)} aria-label={`${label}: оттенок`} disabled={disabled} onChange={(event) => commit({ ...hsv, h: Number(event.target.value) })} />
</label> </label>
<div className="nodedc-color-palette__presets" aria-label="Быстрые цвета">
{palettePresets.map((preset) => (
<button
key={preset}
type="button"
aria-label={`Выбрать ${preset}`}
aria-pressed={hsvToHex(hsv).toLowerCase() === preset}
style={{ "--nodedc-palette-swatch": preset } as CSSProperties}
onClick={() => {
const next = hexToHsv(preset);
if (next) commit(next);
}}
/>
))}
</div>
</div>
</Dropdown>
<input <input
className="nodedc-color-field__text" className="nodedc-color-field__text"
value={value} value={value}
aria-label={`${label}: HEX`} aria-label={`${label}: HEX`}
disabled={disabled}
spellCheck={false}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
{...props}
/> />
</div> </div>
); );

View File

@ -73,7 +73,12 @@
"package": "@nodedc/ui-react", "package": "@nodedc/ui-react",
"exports": ["ColorField"], "exports": ["ColorField"],
"domContract": ["nodedc-color-field"], "domContract": ["nodedc-color-field"],
"summary": "Combined color picker and textual value for settings/inspectors." "summary": "Engine/BIM-derived swatch and HEX field with a theme-neutral portal palette.",
"behavior": ["controlled HEX value", "saturation/value plane", "hue slider", "quick colors", "portal palette", "keyboard spectrum adjustment"],
"rules": [
"Do not expose a browser-native color dialog as the product runtime UI.",
"The palette owns choice geometry; the consumer owns color validation, persistence and domain reaction."
]
}, },
{ {
"id": "dropdown", "id": "dropdown",
@ -196,7 +201,8 @@
"rules": [ "rules": [
"The bottom Launcher service rail is product-specific and is not part of the general application template.", "The bottom Launcher service rail is product-specific and is not part of the general application template.",
"Desktop page gap is 20 px and navigation width follows the Launcher 332352 px range.", "Desktop page gap is 20 px and navigation width follows the Launcher 332352 px range.",
"Applications supply routes, profile data, stage media and panel content without changing shell geometry." "Applications supply routes, profile data, stage media and panel content without changing shell geometry.",
"Light panels use the SEO white-glass surface tokens rather than a dark shell with inverted text."
] ]
}, },
{ {
@ -206,10 +212,10 @@
"exports": ["Icon"], "exports": ["Icon"],
"domContract": ["svg using currentColor"], "domContract": ["svg using currentColor"],
"summary": "Audited semantic icon subset shared by NODE.DC applications instead of product-local vendor imports.", "summary": "Audited semantic icon subset shared by NODE.DC applications instead of product-local vendor imports.",
"variants": ["15 px", "17 px", "18 px"], "variants": ["14 px", "16 px", "18 px"],
"rules": [ "rules": [
"Use semantic names from registry/icons.json.", "Use semantic names from registry/icons.json.",
"The containing component owns surface, hit target and state; the icon owns only the glyph and is optically scaled to 90% inside controls.", "The containing component owns surface, hit target and state; the icon owns only the glyph and is optically scaled to 78% inside controls.",
"Do not copy complete Lucide or Font Awesome catalogs into an application." "Do not copy complete Lucide or Font Awesome catalogs into an application."
] ]
}, },

View File

@ -1,6 +1,6 @@
{ {
"schemaVersion": "1.0.0", "schemaVersion": "1.0.0",
"libraryVersion": "0.5.0", "libraryVersion": "0.5.1",
"name": "NODE.DC Design System", "name": "NODE.DC Design System",
"purpose": "Canonical code-level UI system for current and future NODE.DC applications.", "purpose": "Canonical code-level UI system for current and future NODE.DC applications.",
"packages": [ "packages": [