Refine SEO light theme and environment controls
This commit is contained in:
parent
f1e28c267a
commit
4c74c22a6e
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,19 +417,17 @@ 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="Тип связи"
|
value={connectionType}
|
||||||
value={connectionType}
|
options={[
|
||||||
options={[
|
{ value: "spline", label: "Сплайн" },
|
||||||
{ value: "spline", label: "Сплайн" },
|
{ value: "straight", label: "Прямая" },
|
||||||
{ value: "straight", label: "Прямая" },
|
{ value: "step", label: "Ступенчатая" },
|
||||||
{ value: "step", label: "Ступенчатая" },
|
]}
|
||||||
]}
|
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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 нет.
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
|
|
||||||
| Контекст | Размер glyph | Hit target |
|
| Контекст | Размер glyph | Hit target |
|
||||||
| --- | ---: | ---: |
|
| --- | ---: | ---: |
|
||||||
| Плотная строка/label | `15–16px` | задаёт строка |
|
| Плотная строка/label | `14–15px` | задаёт строка |
|
||||||
| Обычная кнопка | `16–17px` | `46px` круг или кнопка с текстом |
|
| Обычная кнопка | `15–16px` | `46px` круг или кнопка с текстом |
|
||||||
| Крупное оконное действие | `18px` | `46.72px` круг |
|
| Крупное оконное действие | `17–18px` | `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 или другому компоненту поверхности.
|
||||||
|
|
||||||
## Группы
|
## Группы
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.48–0.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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
</label>
|
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>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 332–352 px range.",
|
"Desktop page gap is 20 px and navigation width follows the Launcher 332–352 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."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue