Correct Hub and Engine component geometry
This commit is contained in:
parent
73629d68c3
commit
4a9ad01ea6
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 — source-faithful geometry
|
||||||
|
|
||||||
|
- Corrected the Hub header to the production three-axis shell with workspace, navigation and profile groups.
|
||||||
|
- Added the reusable Hub administration navigation panel and its adjacent-content layout.
|
||||||
|
- Added integrated and Engine split Select variants with source-accurate dropdown geometry.
|
||||||
|
- Fixed Engine Environment Settings metrics, accent sections, ranges, checker and action layouts.
|
||||||
|
- Made right-side inspectors modeless by default: no page dimming, backdrop close or scroll lock.
|
||||||
|
- Updated the living catalog and registry from the audited source files and supplied screenshots.
|
||||||
|
|
||||||
## 0.1.0 — baseline
|
## 0.1.0 — baseline
|
||||||
|
|
||||||
- Established the standalone NODE.DC design-system repository.
|
- Established the standalone NODE.DC design-system repository.
|
||||||
|
|
@ -9,4 +18,3 @@
|
||||||
- Added React primitives, overlays, navigation and inspector/settings patterns.
|
- Added React primitives, overlays, navigation and inspector/settings patterns.
|
||||||
- Added a living catalog and machine-readable component/source registry.
|
- Added a living catalog and machine-readable component/source registry.
|
||||||
- Recorded the initial audit boundary without modifying source applications.
|
- Recorded the initial audit boundary without modifying source applications.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
- кнопки, поля, чекеры, range, select и dropdown;
|
- кнопки, поля, чекеры, range, select и dropdown;
|
||||||
- модальные окна, подтверждения и боковые окна;
|
- модальные окна, подтверждения и боковые окна;
|
||||||
- segmented navigation и статусы;
|
- segmented navigation и статусы;
|
||||||
|
- Hub header и левая admin navigation panel;
|
||||||
- инспектор/панель настроек нового поколения;
|
- инспектор/панель настроек нового поколения;
|
||||||
- поведение portal-слоёв, Escape, outside click и фокуса;
|
- поведение portal-слоёв, Escape, outside click и фокуса;
|
||||||
- контракт темы: приложения меняют цветовую схему, а не геометрию компонентов.
|
- контракт темы: приложения меняют цветовую схему, а не геометрию компонентов.
|
||||||
|
|
@ -46,7 +47,7 @@ npm run build
|
||||||
|
|
||||||
## Статус
|
## Статус
|
||||||
|
|
||||||
Версия `0.1.x` — зафиксированный baseline. Исходные приложения на первом этапе не изменяются. Компоненты здесь построены по подтверждённым реализациям Launcher/Hub, CMS/SEO и двум новым участкам Engine: настройкам окружения и инспектору агентной ноды.
|
Версия `0.2.x` — уточнённый baseline с точной геометрией Hub header/admin panel и новых Engine settings/agent inspector. Исходные приложения на первом этапе не изменяются.
|
||||||
|
|
||||||
Подробности:
|
Подробности:
|
||||||
|
|
||||||
|
|
@ -58,4 +59,3 @@ npm run build
|
||||||
- [Правила развития](docs/GOVERNANCE.md)
|
- [Правила развития](docs/GOVERNANCE.md)
|
||||||
- [План подключения приложений](docs/ADOPTION.md)
|
- [План подключения приложений](docs/ADOPTION.md)
|
||||||
- [Инвентарь следующих компонентов](docs/CANDIDATES.md)
|
- [Инвентарь следующих компонентов](docs/CANDIDATES.md)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"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.1.0",
|
"@nodedc/ui-core": "0.2.0",
|
||||||
"@nodedc/ui-react": "0.1.0",
|
"@nodedc/ui-react": "0.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -22,4 +22,3 @@
|
||||||
"vite": "^7.0.0"
|
"vite": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react";
|
||||||
import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core";
|
import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core";
|
||||||
import {
|
import {
|
||||||
|
AdminNavigationPanel,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
Button,
|
Button,
|
||||||
Checker,
|
Checker,
|
||||||
|
|
@ -11,6 +12,8 @@ import {
|
||||||
FieldFrame,
|
FieldFrame,
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
HeaderProfile,
|
HeaderProfile,
|
||||||
|
HeaderProfileButton,
|
||||||
|
HeaderAvatar,
|
||||||
HeaderWorkspace,
|
HeaderWorkspace,
|
||||||
IconButton,
|
IconButton,
|
||||||
Inspector,
|
Inspector,
|
||||||
|
|
@ -25,6 +28,12 @@ import {
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
type CatalogSection = "foundation" | "controls" | "windows" | "inspector";
|
type CatalogSection = "foundation" | "controls" | "windows" | "inspector";
|
||||||
|
type HubMode = "showcase" | "admin" | "platform";
|
||||||
|
|
||||||
|
const engineAccentStyle = {
|
||||||
|
"--nodedc-accent-rgb": "148 123 219",
|
||||||
|
"--nodedc-on-accent-rgb": "247 248 244",
|
||||||
|
} as CSSProperties;
|
||||||
|
|
||||||
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
|
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
|
||||||
{ label: "Hub", value: [195, 255, 102], hex: "#c3ff66" },
|
{ label: "Hub", value: [195, 255, 102], hex: "#c3ff66" },
|
||||||
|
|
@ -89,9 +98,18 @@ export function CatalogApp() {
|
||||||
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
||||||
const [accentHex, setAccentHex] = useState("#c3ff66");
|
const [accentHex, setAccentHex] = useState("#c3ff66");
|
||||||
const [section, setSection] = useState<CatalogSection>("foundation");
|
const [section, setSection] = useState<CatalogSection>("foundation");
|
||||||
|
const [hubMode, setHubMode] = useState<HubMode>("showcase");
|
||||||
|
const [adminSection, setAdminSection] = useState("invites");
|
||||||
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
||||||
const [checker, setChecker] = useState(true);
|
const [checker, setChecker] = useState(true);
|
||||||
const [range, setRange] = useState(64);
|
const [brightness, setBrightness] = useState(49);
|
||||||
|
const [glowDistance, setGlowDistance] = useState(105);
|
||||||
|
const [usePortColors, setUsePortColors] = useState(false);
|
||||||
|
const [connectionType, setConnectionType] = useState("spline");
|
||||||
|
const [lightColor, setLightColor] = useState("#7a3cff");
|
||||||
|
const [connectionColor, setConnectionColor] = useState("#404040");
|
||||||
|
const [fillColor, setFillColor] = useState("#8f83ff");
|
||||||
|
const [strokeColor, setStrokeColor] = useState("#2b2b36");
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
@ -111,40 +129,56 @@ export function CatalogApp() {
|
||||||
|
|
||||||
const inspectorSections = [
|
const inspectorSections = [
|
||||||
{
|
{
|
||||||
id: "definition",
|
id: "illumination",
|
||||||
label: "Definition Control",
|
label: "Подсветка",
|
||||||
description: "Параметры агентной ноды",
|
tone: "accent" as const,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<ControlRow label="Активность"><Checker checked={checker} label="Агент активен" onChange={setChecker} /></ControlRow>
|
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
||||||
<ControlRow label="Модель">
|
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
||||||
<Select
|
<RangeControl label="Дистанция свечения" value={glowDistance} min={0} max={200} formatValue={(value) => `${value}%`} onChange={setGlowDistance} />
|
||||||
label="Модель"
|
|
||||||
value="codex"
|
|
||||||
options={[
|
|
||||||
{ value: "codex", label: "Codex" },
|
|
||||||
{ value: "local", label: "Local executor" },
|
|
||||||
]}
|
|
||||||
onChange={() => undefined}
|
|
||||||
/>
|
|
||||||
</ControlRow>
|
|
||||||
<RangeControl label="Температура" value={range} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setRange} />
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "appearance",
|
id: "connections",
|
||||||
label: "Внешний вид",
|
label: "Связи",
|
||||||
description: "Тема и акцент",
|
tone: "accent" as const,
|
||||||
content: (
|
content: (
|
||||||
<ControlRow label="Акцент"><ColorField value={accentHex} onChange={setAccentHex} /></ControlRow>
|
<>
|
||||||
|
<ControlRow label="Связи по умолчанию" layout="stack">
|
||||||
|
<Select
|
||||||
|
variant="split"
|
||||||
|
label="Тип связи"
|
||||||
|
value={connectionType}
|
||||||
|
options={[
|
||||||
|
{ value: "spline", label: "Сплайн" },
|
||||||
|
{ value: "straight", label: "Прямая" },
|
||||||
|
{ value: "step", label: "Ступенчатая" },
|
||||||
|
]}
|
||||||
|
onChange={setConnectionType}
|
||||||
|
/>
|
||||||
|
</ControlRow>
|
||||||
|
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
|
||||||
|
<ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
|
||||||
|
<ControlRow label="Применить ко всем">
|
||||||
|
<Button variant="accent" shape="pill" width="full">Применить</Button>
|
||||||
|
</ControlRow>
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime",
|
id: "ports",
|
||||||
label: "Выполнение",
|
label: "Порты",
|
||||||
description: "Параметры запуска",
|
tone: "accent" as const,
|
||||||
content: <Checker checked={false} label="Запускать автоматически" onChange={() => undefined} />,
|
content: (
|
||||||
|
<>
|
||||||
|
<ControlRow label="Заливка"><ColorField value={fillColor} onChange={setFillColor} /></ControlRow>
|
||||||
|
<RangeControl label="Прозрачность" value={100} min={0} max={100} formatValue={(value) => `${value}%`} onChange={() => undefined} />
|
||||||
|
<ControlRow label="Обводка"><ColorField value={strokeColor} onChange={setStrokeColor} /></ControlRow>
|
||||||
|
<RangeControl label="Прозрачность" value={100} min={0} max={100} formatValue={(value) => `${value}%`} onChange={() => undefined} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -154,7 +188,76 @@ export function CatalogApp() {
|
||||||
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
||||||
center={
|
center={
|
||||||
<>
|
<>
|
||||||
<HeaderWorkspace label="UI" />
|
<HeaderWorkspace label="DC" />
|
||||||
|
<SegmentedControl
|
||||||
|
label="Навигация Hub"
|
||||||
|
value={hubMode}
|
||||||
|
items={[
|
||||||
|
{ value: "showcase", label: "Витрина" },
|
||||||
|
{ value: "admin", label: "Администрирование" },
|
||||||
|
{ value: "platform", label: "Платформа" },
|
||||||
|
]}
|
||||||
|
onChange={setHubMode}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<HeaderProfile>
|
||||||
|
<IconButton label="Уведомления">▱</IconButton>
|
||||||
|
<HeaderProfileButton>Профиль</HeaderProfileButton>
|
||||||
|
<HeaderAvatar label="DC" />
|
||||||
|
</HeaderProfile>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hubMode === "admin" ? (
|
||||||
|
<div className="catalog-admin-layer">
|
||||||
|
<AdminNavigationPanel
|
||||||
|
title="Администрирование"
|
||||||
|
onClose={() => setHubMode("showcase")}
|
||||||
|
contexts={[
|
||||||
|
{
|
||||||
|
id: "public",
|
||||||
|
label: "Открытый контур",
|
||||||
|
description: "Public access pool",
|
||||||
|
icon: "◎",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "company",
|
||||||
|
groupLabel: "Компании",
|
||||||
|
label: "DCTOUCH",
|
||||||
|
description: "ООО ДИСТАЧ",
|
||||||
|
icon: "▥",
|
||||||
|
active: true,
|
||||||
|
onSelect: () => undefined,
|
||||||
|
onToggle: () => undefined,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
items={[
|
||||||
|
{ id: "overview", label: "Обзор", icon: "▦" },
|
||||||
|
{ id: "invites", label: "Инвайты", icon: "✉" },
|
||||||
|
{ id: "members", label: "Участники", icon: "♙" },
|
||||||
|
{ id: "access", label: "Доступы", icon: "⌁" },
|
||||||
|
{ id: "groups", label: "Группы", icon: "☷" },
|
||||||
|
]}
|
||||||
|
activeId={adminSection}
|
||||||
|
onItemChange={setAdminSection}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">♢</span>
|
||||||
|
<span>Root Admin</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<main className="catalog-main" data-admin-open={hubMode === "admin" ? "true" : undefined}>
|
||||||
|
<div className="catalog-hero">
|
||||||
|
<StatusBadge tone="accent">NODE.DC UI baseline</StatusBadge>
|
||||||
|
<h1>Один UI-контракт.<br />Разные приложения.</h1>
|
||||||
|
<p>Геометрия, окна и поведение остаются общими. Приложение выбирает тему и акцент, затем использует готовые компоненты.</p>
|
||||||
|
<div className="catalog-section-nav">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
label="Разделы каталога"
|
label="Разделы каталога"
|
||||||
value={section}
|
value={section}
|
||||||
|
|
@ -166,23 +269,7 @@ export function CatalogApp() {
|
||||||
]}
|
]}
|
||||||
onChange={openSection}
|
onChange={openSection}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
}
|
|
||||||
right={
|
|
||||||
<HeaderProfile>
|
|
||||||
<IconButton label="Сменить тему" onClick={() => setTheme((current) => current === "dark" ? "light" : "dark")}>
|
|
||||||
{theme === "dark" ? "☼" : "◐"}
|
|
||||||
</IconButton>
|
|
||||||
<span className="catalog-avatar">DC</span>
|
|
||||||
</HeaderProfile>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="catalog-main">
|
|
||||||
<div className="catalog-hero">
|
|
||||||
<StatusBadge tone="accent">baseline 0.1.0</StatusBadge>
|
|
||||||
<h1>Один UI-контракт.<br />Разные приложения.</h1>
|
|
||||||
<p>Геометрия, окна и поведение остаются общими. Приложение выбирает тему и акцент, затем использует готовые компоненты.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section id="foundation" eyebrow="01 / Foundation" title="Материал и тема" description="Одинаковые поверхности проверяются в тёмной и светлой схеме без fork компонентов.">
|
<Section id="foundation" eyebrow="01 / Foundation" title="Материал и тема" description="Одинаковые поверхности проверяются в тёмной и светлой схеме без fork компонентов.">
|
||||||
|
|
@ -222,7 +309,7 @@ export function CatalogApp() {
|
||||||
<div className="catalog-grid">
|
<div className="catalog-grid">
|
||||||
<Preview title="Actions" note="shared geometry">
|
<Preview title="Actions" note="shared geometry">
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<Button variant="accent">Сохранить</Button>
|
<Button variant="accent" shape="pill">Сохранить</Button>
|
||||||
<Button>Отмена</Button>
|
<Button>Отмена</Button>
|
||||||
<Button variant="ghost">Подробнее</Button>
|
<Button variant="ghost">Подробнее</Button>
|
||||||
<Button variant="danger">Удалить</Button>
|
<Button variant="danger">Удалить</Button>
|
||||||
|
|
@ -237,9 +324,24 @@ export function CatalogApp() {
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Selection" note="portal dropdown">
|
<Preview title="Selection" note="portal dropdown">
|
||||||
<div className="catalog-form">
|
<div className="catalog-form">
|
||||||
<FieldFrame label="Статус">
|
<FieldFrame label="Hub / integrated select">
|
||||||
<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">
|
||||||
|
<div style={engineAccentStyle}>
|
||||||
|
<Select
|
||||||
|
variant="split"
|
||||||
|
label="Тип связи"
|
||||||
|
value={connectionType}
|
||||||
|
options={[
|
||||||
|
{ value: "spline", label: "Сплайн" },
|
||||||
|
{ value: "straight", label: "Прямая" },
|
||||||
|
{ value: "step", label: "Ступенчатая" },
|
||||||
|
]}
|
||||||
|
onChange={setConnectionType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FieldFrame>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
|
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
|
||||||
<Button ref={setTriggerRef} aria-expanded={open} aria-controls={surfaceId} onClick={toggle}>Действия</Button>
|
<Button ref={setTriggerRef} aria-expanded={open} aria-controls={surfaceId} onClick={toggle}>Действия</Button>
|
||||||
|
|
@ -255,10 +357,12 @@ export function CatalogApp() {
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Settings controls" note="new Engine generation">
|
<Preview title="Environment controls" note="exact Engine geometry">
|
||||||
<div className="catalog-form">
|
<div className="catalog-engine-control-stack" style={engineAccentStyle}>
|
||||||
<Checker checked={checker} label="Автоматическое обновление" description="Применять изменения без перезапуска" onChange={setChecker} />
|
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
||||||
<RangeControl label="Интенсивность стекла" value={range} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setRange} />
|
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
||||||
|
<Checker checked={checker} label="Использовать цвет портов" onChange={setChecker} />
|
||||||
|
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Semantic status" note="not tied to accent" className="catalog-preview--wide">
|
<Preview title="Semantic status" note="not tied to accent" className="catalog-preview--wide">
|
||||||
|
|
@ -273,7 +377,7 @@ export function CatalogApp() {
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section id="windows" eyebrow="03 / Layers" title="Окна и подтверждения" description="Modal и side panel используют одну механику portal, Escape, focus trap и focus restore.">
|
<Section id="windows" eyebrow="03 / Layers" title="Окна и подтверждения" description="Modal и modeless side panel используют общий portal, Escape и focus restore; trap и scroll lock остаются только у modal.">
|
||||||
<Preview title="Controlled windows" note="center / end / confirm">
|
<Preview title="Controlled windows" note="center / end / confirm">
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<Button variant="accent" onClick={() => setModalOpen(true)}>Открыть окно</Button>
|
<Button variant="accent" onClick={() => setModalOpen(true)}>Открыть окно</Button>
|
||||||
|
|
@ -285,9 +389,9 @@ export function CatalogApp() {
|
||||||
|
|
||||||
<Section id="inspector" eyebrow="04 / Pattern" title="Inspector / Settings" description="Канон основан только на новом Environment Settings и NDC Agent Inspector, без legacy-инспекторов Engine.">
|
<Section id="inspector" eyebrow="04 / Pattern" title="Inspector / Settings" description="Канон основан только на новом Environment Settings и NDC Agent Inspector, без legacy-инспекторов Engine.">
|
||||||
<div className="catalog-inspector-stage">
|
<div className="catalog-inspector-stage">
|
||||||
<GlassSurface tone="strong" radius="panel" padding="md" className="catalog-inspector-card">
|
<GlassSurface tone="strong" radius="panel" className="catalog-inspector-card" style={engineAccentStyle}>
|
||||||
<div className="catalog-inspector-title"><strong>Инспектор</strong><span>Agent Definition</span></div>
|
<div className="catalog-inspector-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
|
||||||
<Inspector sections={inspectorSections} defaultOpen={["definition"]} activeId="definition" />
|
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
@ -312,8 +416,15 @@ export function CatalogApp() {
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
<Window open={inspectorOpen} title="Инспектор" subtitle="Agent Definition" placement="end" onClose={() => setInspectorOpen(false)}>
|
<Window
|
||||||
<Inspector sections={inspectorSections} defaultOpen={["definition"]} activeId="definition" />
|
open={inspectorOpen}
|
||||||
|
title="Настройки окружения"
|
||||||
|
subtitle="ENGINE / новый канон"
|
||||||
|
placement="end"
|
||||||
|
style={engineAccentStyle}
|
||||||
|
onClose={() => setInspectorOpen(false)}
|
||||||
|
>
|
||||||
|
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|
@ -328,4 +439,3 @@ export function CatalogApp() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,20 +30,16 @@ textarea {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img {
|
.catalog-admin-layer {
|
||||||
filter: brightness(0.12);
|
position: fixed;
|
||||||
|
z-index: 390;
|
||||||
|
top: 6.55rem;
|
||||||
|
bottom: 1.25rem;
|
||||||
|
left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-avatar {
|
[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img {
|
||||||
display: inline-grid;
|
filter: brightness(0.12);
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgb(var(--nodedc-accent-rgb));
|
|
||||||
color: rgb(var(--nodedc-on-accent-rgb));
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-main {
|
.catalog-main {
|
||||||
|
|
@ -52,6 +48,13 @@ textarea {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
gap: 8rem;
|
gap: 8rem;
|
||||||
padding: 5rem 0 10rem;
|
padding: 5rem 0 10rem;
|
||||||
|
transition: width 500ms cubic-bezier(0.22, 1, 0.36, 1), margin-left 500ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-main[data-admin-open="true"] {
|
||||||
|
width: min(1180px, calc(100% - 25rem));
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-hero {
|
.catalog-hero {
|
||||||
|
|
@ -80,6 +83,10 @@ textarea {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-section-nav {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-section {
|
.catalog-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
scroll-margin-top: 2rem;
|
scroll-margin-top: 2rem;
|
||||||
|
|
@ -205,6 +212,13 @@ textarea {
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-engine-control-stack {
|
||||||
|
display: grid;
|
||||||
|
width: min(var(--nodedc-inspector-inner-width), 100%);
|
||||||
|
justify-self: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-surface-stack .nodedc-glass {
|
.catalog-surface-stack .nodedc-glass {
|
||||||
min-height: 4rem;
|
min-height: 4rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -227,12 +241,13 @@ textarea {
|
||||||
|
|
||||||
.catalog-inspector-card {
|
.catalog-inspector-card {
|
||||||
width: min(24.375rem, 100%);
|
width: min(24.375rem, 100%);
|
||||||
|
padding: 1.125rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-inspector-title {
|
.catalog-inspector-title {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
padding: 0.75rem 0.75rem 1rem;
|
padding: 0 0.875rem 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-inspector-title span {
|
.catalog-inspector-title span {
|
||||||
|
|
@ -246,6 +261,11 @@ textarea {
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-main[data-admin-open="true"] {
|
||||||
|
width: min(1180px, calc(100% - 2rem));
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-grid,
|
.catalog-grid,
|
||||||
.catalog-grid--foundation {
|
.catalog-grid--foundation {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
@ -258,4 +278,11 @@ textarea {
|
||||||
.catalog-inspector-stage {
|
.catalog-inspector-stage {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-admin-layer {
|
||||||
|
top: 0.875rem;
|
||||||
|
right: 0.875rem;
|
||||||
|
bottom: 0.875rem;
|
||||||
|
left: 0.875rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ Baseline намеренно отделяет готовые exports от изв
|
||||||
- ProfileMenu — отделить визуальную карточку от identity/role API приложения.
|
- ProfileMenu — отделить визуальную карточку от identity/role API приложения.
|
||||||
- ActionDropdown — добавить типизированные command rows поверх готового Dropdown.
|
- ActionDropdown — добавить типизированные command rows поверх готового Dropdown.
|
||||||
- DataTable — вынести toolbar, cells, loading/empty и row actions без entity schemas Launcher/Plane.
|
- DataTable — вынести toolbar, cells, loading/empty и row actions без entity schemas Launcher/Plane.
|
||||||
- SideNavigation — route-aware навигация отдельно от уже готового Inspector accordion.
|
|
||||||
- AvatarStack — согласовать overflow, presence и picker mode.
|
- AvatarStack — согласовать overflow, presence и picker mode.
|
||||||
|
|
||||||
## P2 — продуктовые patterns
|
## P2 — продуктовые patterns
|
||||||
|
|
@ -32,4 +31,3 @@ Baseline намеренно отделяет готовые exports от изв
|
||||||
6. выпустить minor version.
|
6. выпустить minor version.
|
||||||
|
|
||||||
Так список сохраняет уже проделанную работу, но не превращает незавершённый локальный код в ложный канон.
|
Так список сохраняет уже проделанную работу, но не превращает незавершённый локальный код в ложный канон.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,12 @@ Dropdown владеет floating-layer поведением:
|
||||||
|
|
||||||
## Select
|
## Select
|
||||||
|
|
||||||
Select добавляет к Dropdown контролируемое значение, options и необязательный поиск. Native select не используется как видимый runtime UI, но может использоваться как fallback только в специально оговорённом legacy-контуре.
|
Select добавляет к Dropdown контролируемое значение, options и необязательный поиск. У него две канонические формы:
|
||||||
|
|
||||||
|
- `integrated` — единая pill-поверхность Hub/Launcher;
|
||||||
|
- `split` — форма Engine с отдельной областью значения и отдельной кнопкой раскрытия `46 px`, между ними зазор `8 px`.
|
||||||
|
|
||||||
|
Native select не используется как видимый runtime UI. Меню рендерится через portal; у Engine-варианта меню имеет радиус `20 px`, а строки — `14 px` и высоту `42 px`.
|
||||||
|
|
||||||
## Window
|
## Window
|
||||||
|
|
||||||
|
|
@ -66,7 +71,7 @@ Window — единая механика открытия modal и правой
|
||||||
- приложение контролирует `open`;
|
- приложение контролирует `open`;
|
||||||
- библиотека управляет portal, focus entry/trap/restore, Escape, backdrop и scroll lock;
|
- библиотека управляет portal, focus entry/trap/restore, Escape, backdrop и scroll lock;
|
||||||
- `center` — modal;
|
- `center` — modal;
|
||||||
- `end` — inspector/settings/detail panel.
|
- `end` — modeless inspector/settings/detail panel без затемнения, backdrop blur, backdrop close, focus trap и блокировки прокрутки страницы.
|
||||||
|
|
||||||
Содержание, сохранение и запросы принадлежат приложению.
|
Содержание, сохранение и запросы принадлежат приложению.
|
||||||
|
|
||||||
|
|
@ -88,6 +93,14 @@ Pill navigation для верхней панели и компактного п
|
||||||
|
|
||||||
Компонент фиксирует положение логотипа, высоту и выравнивание, но не знает маршруты конкретного приложения.
|
Компонент фиксирует положение логотипа, высоту и выравнивание, но не знает маршруты конкретного приложения.
|
||||||
|
|
||||||
|
В Hub-форме центр состоит из круглого workspace trigger и segmented navigation. Справа используется единая profile-group поверхность с иконкой, подписью и круглым avatar. Эти элементы нельзя заменять случайными standalone-кнопками, сохраняя только приблизительную позицию.
|
||||||
|
|
||||||
|
## AdminNavigationPanel
|
||||||
|
|
||||||
|
Левая выезжающая панель Hub/Launcher. Библиотека владеет оболочкой `352 px`, радиусом `21.6 px`, внутренними отступами, full-bleed context/navigation pills, круглыми icon surfaces и анимацией появления. Приложение передаёт workspace/company, маршруты, active id и footer identity.
|
||||||
|
|
||||||
|
На desktop открытая панель занимает отдельную колонку с зазором `12 px` до основного контента, а не перекрывает его затемнённым overlay.
|
||||||
|
|
||||||
## StatusBadge
|
## StatusBadge
|
||||||
|
|
||||||
Статус использует semantic tone: neutral, success, warning, danger или accent. Бизнес-статус приложения маппится на tone в самом приложении.
|
Статус использует semantic tone: neutral, success, warning, danger или accent. Бизнес-статус приложения маппится на tone в самом приложении.
|
||||||
|
|
@ -104,5 +117,6 @@ Inspector владеет:
|
||||||
- вертикальным ритмом;
|
- вертикальным ритмом;
|
||||||
- раскладкой label/control.
|
- раскладкой label/control.
|
||||||
|
|
||||||
Engine продолжает владеть определениями полей, типами нод и сохранением значений.
|
Для 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 продолжает владеть определениями полей, типами нод и сохранением значений.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
- button/modal/dropdown primitives;
|
- button/modal/dropdown primitives;
|
||||||
- общая логика admin windows.
|
- общая логика admin windows.
|
||||||
|
|
||||||
|
Для точной геометрии используются `src/widgets/top-bar/TopBar.tsx`, `src/widgets/admin-overlay/AdminOverlay.tsx` и `src/styles/globals.css`: трёхосевая шапка, круглый workspace trigger, segmented navigation, profile group и левая admin panel `352 px`.
|
||||||
|
|
||||||
Существовавший `dc-ui-guideline` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория.
|
Существовавший `dc-ui-guideline` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория.
|
||||||
|
|
||||||
## SEO и CMS
|
## SEO и CMS
|
||||||
|
|
@ -36,6 +38,8 @@ CMS подтверждает необходимость DOM-пакета без
|
||||||
|
|
||||||
Все остальные legacy inspectors и старые plates не участвуют в принятии решений. Их будущее приведение к новому дизайну должно идти через эту библиотеку.
|
Все остальные legacy inspectors и старые plates не участвуют в принятии решений. Их будущее приведение к новому дизайну должно идти через эту библиотеку.
|
||||||
|
|
||||||
|
Размеры зафиксированы непосредственно по разрешённым исходникам: панель `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.
|
||||||
|
|
||||||
## Task Manager / Ops
|
## Task Manager / Ops
|
||||||
|
|
||||||
Используются только уже стандартизированные shared patterns и документы по dropdown/modal behavior. Известные legacy CustomMenu и screen-local wrappers не являются источником компонентов.
|
Используются только уже стандартизированные shared patterns и документы по dropdown/modal behavior. Известные legacy CustomMenu и screen-local wrappers не являются источником компонентов.
|
||||||
|
|
@ -56,4 +60,3 @@ CMS подтверждает необходимость DOM-пакета без
|
||||||
- различную степень accessibility.
|
- различную степень accessibility.
|
||||||
|
|
||||||
Поэтому библиотека фиксирует очищенный API и поведение, сохраняя происхождение решения в реестре.
|
Поэтому библиотека фиксирует очищенный API и поведение, сохраняя происхождение решения в реестре.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,10 @@ Side window использует ту же механику, но placement `end
|
||||||
- будущих detail/settings panels;
|
- будущих detail/settings panels;
|
||||||
- CMS/SEO side editors, если они не требуют отдельного полноэкранного workflow.
|
- CMS/SEO side editors, если они не требуют отдельного полноэкранного workflow.
|
||||||
|
|
||||||
|
По умолчанию это modeless-слой: он не затемняет страницу, не перехватывает клики по основной области, не закрывается по backdrop, не блокирует body scroll и не удерживает Tab внутри панели. Escape и кнопка закрытия остаются доступны. Если конкретный workflow должен быть modal, это задаётся явно, а не получается случайно из placement.
|
||||||
|
|
||||||
## Управление состоянием
|
## Управление состоянием
|
||||||
|
|
||||||
Библиотека не создаёт глобальный store окон. Приложение владеет тем, какое окно открыто и какие данные в нём загружены. Библиотека владеет одинаковым поведением самого слоя.
|
Библиотека не создаёт глобальный store окон. Приложение владеет тем, какое окно открыто и какие данные в нём загружены. Библиотека владеет одинаковым поведением самого слоя.
|
||||||
|
|
||||||
Если приложению позже потребуется stack нескольких modeless-окон, это будет отдельный пакетный контракт, а не расширение локального z-index.
|
Если приложению позже потребуется stack нескольких modeless-окон, это будет отдельный пакетный контракт, а не расширение локального z-index.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"apps/*"
|
"apps/*"
|
||||||
|
|
@ -25,10 +25,10 @@
|
||||||
},
|
},
|
||||||
"apps/catalog": {
|
"apps/catalog": {
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.1.0",
|
"@nodedc/ui-core": "0.2.0",
|
||||||
"@nodedc/ui-react": "0.1.0",
|
"@nodedc/ui-react": "0.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1879,27 +1879,27 @@
|
||||||
},
|
},
|
||||||
"packages/tokens": {
|
"packages/tokens": {
|
||||||
"name": "@nodedc/tokens",
|
"name": "@nodedc/tokens",
|
||||||
"version": "0.1.0"
|
"version": "0.2.0"
|
||||||
},
|
},
|
||||||
"packages/ui-core": {
|
"packages/ui-core": {
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/tokens": "0.1.0"
|
"@nodedc/tokens": "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-dom": {
|
"packages/ui-dom": {
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.1.0"
|
"@nodedc/ui-core": "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-react": {
|
"packages/ui-react": {
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.1.0"
|
"@nodedc/ui-core": "0.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"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.1.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["tokens.json", "tokens.css", "themes.css"],
|
"files": ["tokens.json", "tokens.css", "themes.css"],
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
--nodedc-header-logo-width: 7.25rem;
|
--nodedc-header-logo-width: 7.25rem;
|
||||||
--nodedc-header-logo-height: 1.79rem;
|
--nodedc-header-logo-height: 1.79rem;
|
||||||
--nodedc-inspector-width: 24.375rem;
|
--nodedc-inspector-width: 24.375rem;
|
||||||
|
--nodedc-inspector-inner-width: 20.625rem;
|
||||||
|
--nodedc-inspector-label-width: 9.625rem;
|
||||||
|
--nodedc-inspector-control-width: 10.125rem;
|
||||||
|
--nodedc-inspector-control-gap: 0.875rem;
|
||||||
|
--nodedc-inspector-section-radius: 0.75rem;
|
||||||
|
--nodedc-inspector-button-radius: 1rem;
|
||||||
|
--nodedc-select-toggle-width: 2.875rem;
|
||||||
|
--nodedc-select-gap: 0.5rem;
|
||||||
|
|
||||||
--nodedc-space-1: 0.25rem;
|
--nodedc-space-1: 0.25rem;
|
||||||
--nodedc-space-2: 0.5rem;
|
--nodedc-space-2: 0.5rem;
|
||||||
|
|
@ -53,4 +61,3 @@
|
||||||
--nodedc-font-weight-medium: 620;
|
--nodedc-font-weight-medium: 620;
|
||||||
--nodedc-font-weight-strong: 760;
|
--nodedc-font-weight-strong: 760;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,13 @@
|
||||||
"headerPill": { "$type": "dimension", "$value": "55.2px" },
|
"headerPill": { "$type": "dimension", "$value": "55.2px" },
|
||||||
"logoWidth": { "$type": "dimension", "$value": "116px" },
|
"logoWidth": { "$type": "dimension", "$value": "116px" },
|
||||||
"logoHeight": { "$type": "dimension", "$value": "28.64px" },
|
"logoHeight": { "$type": "dimension", "$value": "28.64px" },
|
||||||
"inspectorWidth": { "$type": "dimension", "$value": "390px" }
|
"inspectorWidth": { "$type": "dimension", "$value": "390px" },
|
||||||
|
"inspectorInnerWidth": { "$type": "dimension", "$value": "330px" },
|
||||||
|
"inspectorLabelWidth": { "$type": "dimension", "$value": "154px" },
|
||||||
|
"inspectorControlWidth": { "$type": "dimension", "$value": "162px" },
|
||||||
|
"inspectorControlGap": { "$type": "dimension", "$value": "14px" },
|
||||||
|
"selectToggleWidth": { "$type": "dimension", "$value": "46px" },
|
||||||
|
"selectGap": { "$type": "dimension", "$value": "8px" }
|
||||||
},
|
},
|
||||||
"space": {
|
"space": {
|
||||||
"1": { "$type": "dimension", "$value": "4px" },
|
"1": { "$type": "dimension", "$value": "4px" },
|
||||||
|
|
@ -58,4 +64,3 @@
|
||||||
"toast": { "$type": "number", "$value": 950 }
|
"toast": { "$type": "number", "$value": 950 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"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.1.0"
|
"@nodedc/tokens": "0.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close) {
|
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-select__toggle, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close) {
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close):focus-visible {
|
:where(.nodedc-button, .nodedc-icon-button, .nodedc-checker, .nodedc-select-trigger, .nodedc-select__toggle, .nodedc-dropdown-option, .nodedc-segmented__item, .nodedc-header__nav-item, .nodedc-inspector__section-trigger, .nodedc-window__close):focus-visible {
|
||||||
background-color: var(--nodedc-focus-surface);
|
background-color: var(--nodedc-focus-surface);
|
||||||
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +126,14 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-button[data-shape="pill"] {
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-button[data-shape="rounded"] {
|
||||||
|
border-radius: var(--nodedc-inspector-button-radius);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-button[data-variant="primary"],
|
.nodedc-button[data-variant="primary"],
|
||||||
.nodedc-button[data-variant="accent"] {
|
.nodedc-button[data-variant="accent"] {
|
||||||
--nodedc-button-bg: rgb(var(--nodedc-accent-rgb));
|
--nodedc-button-bg: rgb(var(--nodedc-accent-rgb));
|
||||||
|
|
@ -313,7 +321,6 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-checker__indicator {
|
.nodedc-checker__indicator {
|
||||||
position: relative;
|
|
||||||
width: 1.625rem;
|
width: 1.625rem;
|
||||||
height: 1.625rem;
|
height: 1.625rem;
|
||||||
border-radius: var(--nodedc-radius-circle);
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
|
@ -321,29 +328,11 @@ textarea.nodedc-field__control {
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.035);
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.035);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-checker__indicator::after {
|
|
||||||
position: absolute;
|
|
||||||
inset: 50% auto auto 50%;
|
|
||||||
width: 0.375rem;
|
|
||||||
height: 0.375rem;
|
|
||||||
border-radius: var(--nodedc-radius-circle);
|
|
||||||
background: rgb(var(--nodedc-on-accent-rgb));
|
|
||||||
content: "";
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -50%) scale(0.4);
|
|
||||||
transition: opacity var(--nodedc-duration-fast) ease, transform var(--nodedc-duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-checker[aria-checked="true"] .nodedc-checker__indicator {
|
.nodedc-checker[aria-checked="true"] .nodedc-checker__indicator {
|
||||||
background: rgb(var(--nodedc-accent-rgb));
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-checker[aria-checked="true"] .nodedc-checker__indicator::after {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(-50%, -50%) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-range {
|
.nodedc-range {
|
||||||
--nodedc-range-progress: 50%;
|
--nodedc-range-progress: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -601,6 +590,59 @@ textarea.nodedc-field__control {
|
||||||
box-shadow: var(--nodedc-glass-control-shadow);
|
box-shadow: var(--nodedc-glass-control-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-header__profile .nodedc-icon-button {
|
||||||
|
width: var(--nodedc-header-row-height);
|
||||||
|
height: var(--nodedc-header-row-height);
|
||||||
|
flex-basis: var(--nodedc-header-row-height);
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-header__profile-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: calc(var(--nodedc-header-pill-height) - 0.64rem);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
padding: 0.2rem 1.2rem;
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--nodedc-font-size-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-header__profile-button:hover,
|
||||||
|
.nodedc-header__profile-button:focus-visible {
|
||||||
|
background: var(--nodedc-glass-control-hover);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-header__avatar {
|
||||||
|
display: grid;
|
||||||
|
width: var(--nodedc-header-row-height);
|
||||||
|
height: var(--nodedc-header-row-height);
|
||||||
|
flex: 0 0 var(--nodedc-header-row-height);
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.72), transparent 24%),
|
||||||
|
linear-gradient(135deg, rgb(166, 194, 109), rgb(142, 123, 139));
|
||||||
|
color: rgba(8, 8, 10, 0.96);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-header__avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-header__workspace {
|
.nodedc-header__workspace {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
width: var(--nodedc-header-row-height);
|
width: var(--nodedc-header-row-height);
|
||||||
|
|
@ -649,10 +691,276 @@ textarea.nodedc-field__control {
|
||||||
color: var(--nodedc-glass-control-active-text);
|
color: var(--nodedc-glass-control-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: min(22rem, calc(100vw - 2.5rem));
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||||
|
gap: 1.05rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--nodedc-radius-card);
|
||||||
|
background:
|
||||||
|
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);
|
||||||
|
padding: 1.1rem;
|
||||||
|
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.52);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
-webkit-backdrop-filter: blur(28px);
|
||||||
|
animation: nodedc-admin-panel-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nodedc-admin-panel-in {
|
||||||
|
from { opacity: 0; transform: translateX(-1.6rem); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 3.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__head p,
|
||||||
|
.nodedc-admin-panel__head h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__head p {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__head h2 {
|
||||||
|
margin-top: 0.55rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 780;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
display: grid;
|
||||||
|
width: 2.92rem;
|
||||||
|
height: 2.92rem;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__close:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
background: var(--nodedc-glass-control-hover);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__contexts,
|
||||||
|
.nodedc-admin-panel__context-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__context-group {
|
||||||
|
gap: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__group-label {
|
||||||
|
padding-inline: 0.35rem;
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context {
|
||||||
|
display: grid;
|
||||||
|
width: calc(100% + 2.2rem);
|
||||||
|
min-height: 3.55rem;
|
||||||
|
margin-inline: -1.1rem;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context[data-active="true"] {
|
||||||
|
background: rgba(74, 74, 74, 0.5);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context--split {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 2.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__main,
|
||||||
|
.nodedc-admin-context__toggle {
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 3.55rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 5px 0 5px 5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__toggle {
|
||||||
|
display: grid;
|
||||||
|
width: 2.92rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__toggle:hover {
|
||||||
|
background: var(--nodedc-glass-control-hover);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__icon,
|
||||||
|
.nodedc-admin-panel__nav-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 2.92rem;
|
||||||
|
height: 2.92rem;
|
||||||
|
flex: 0 0 2.92rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__body {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__body strong,
|
||||||
|
.nodedc-admin-context__body small {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__body strong {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-context__body small {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav {
|
||||||
|
display: grid;
|
||||||
|
width: calc(100% + 2.2rem);
|
||||||
|
min-height: 0;
|
||||||
|
margin-inline: -1.1rem;
|
||||||
|
align-content: start;
|
||||||
|
gap: 0.22rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-item,
|
||||||
|
.nodedc-admin-panel__footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 3.55rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.86rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--nodedc-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
padding: 5px 0.9rem 5px calc(5px + 2.92rem + 0.86rem);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.91rem;
|
||||||
|
font-weight: 570;
|
||||||
|
text-align: left;
|
||||||
|
opacity: 0.66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 160ms ease, color 160ms ease, opacity 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.085);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
opacity: 0.92;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-item[data-active="true"] {
|
||||||
|
background: rgba(255, 255, 255, 0.115);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.048), 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__nav-item[data-active="true"] .nodedc-admin-panel__nav-icon {
|
||||||
|
background: rgba(247, 248, 244, 0.96);
|
||||||
|
color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-admin-panel__footer {
|
||||||
|
width: calc(100% + 2.2rem);
|
||||||
|
margin-inline: -1.1rem;
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-dropdown-anchor {
|
.nodedc-dropdown-anchor {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-anchor.nodedc-select-anchor {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-dropdown-surface {
|
.nodedc-dropdown-surface {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: var(--nodedc-layer-popover);
|
z-index: var(--nodedc-layer-popover);
|
||||||
|
|
@ -756,6 +1064,83 @@ textarea.nodedc-field__control {
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__control {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: minmax(0, 1fr) var(--nodedc-select-toggle-width);
|
||||||
|
gap: var(--nodedc-select-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__value {
|
||||||
|
display: flex;
|
||||||
|
height: var(--nodedc-control-height);
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--nodedc-space-2);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--nodedc-radius-control-compact);
|
||||||
|
background: var(--nodedc-field-bg);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
padding: 0 0.95rem;
|
||||||
|
box-shadow: var(--nodedc-glass-control-shadow);
|
||||||
|
font-size: var(--nodedc-font-size-md);
|
||||||
|
font-weight: var(--nodedc-font-weight-medium);
|
||||||
|
line-height: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__value > span:last-child {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle {
|
||||||
|
display: inline-grid;
|
||||||
|
width: var(--nodedc-select-toggle-width);
|
||||||
|
height: var(--nodedc-control-height);
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--nodedc-radius-control-compact);
|
||||||
|
outline: 0;
|
||||||
|
background: var(--nodedc-glass-control-bg);
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: var(--nodedc-glass-control-shadow);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle:hover:not(:disabled),
|
||||||
|
.nodedc-select__toggle[aria-expanded="true"] {
|
||||||
|
background: var(--nodedc-glass-control-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle:focus-visible {
|
||||||
|
background: var(--nodedc-focus-surface);
|
||||||
|
box-shadow: var(--nodedc-glass-control-shadow), inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-dropdown-surface.nodedc-select__menu {
|
||||||
|
gap: 0.125rem;
|
||||||
|
border-radius: var(--nodedc-radius-control);
|
||||||
|
background: var(--nodedc-field-bg);
|
||||||
|
padding: 0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-select__menu .nodedc-dropdown-option {
|
||||||
|
min-height: 2.625rem;
|
||||||
|
border-radius: var(--nodedc-radius-control-compact);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-select-trigger {
|
.nodedc-select-trigger {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -799,6 +1184,10 @@ textarea.nodedc-field__control {
|
||||||
transform: translateY(2px) rotate(225deg);
|
transform: translateY(2px) rotate(225deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-select__toggle[aria-expanded="true"] .nodedc-select-trigger__chevron {
|
||||||
|
transform: translateY(2px) rotate(225deg);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-overlay {
|
.nodedc-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: var(--nodedc-layer-overlay);
|
z-index: var(--nodedc-layer-overlay);
|
||||||
|
|
@ -815,7 +1204,11 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-overlay[data-placement="end"] {
|
.nodedc-overlay[data-placement="end"] {
|
||||||
place-items: end;
|
place-items: end;
|
||||||
padding-top: 5.75rem;
|
padding: 5.75rem 1.5rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes nodedc-overlay-in {
|
@keyframes nodedc-overlay-in {
|
||||||
|
|
@ -853,6 +1246,20 @@ textarea.nodedc-field__control {
|
||||||
width: min(var(--nodedc-inspector-width), calc(100vw - 3rem));
|
width: min(var(--nodedc-inspector-width), calc(100vw - 3rem));
|
||||||
max-height: calc(100vh - 7.25rem);
|
max-height: calc(100vh - 7.25rem);
|
||||||
border-radius: var(--nodedc-radius-panel);
|
border-radius: var(--nodedc-radius-panel);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-window[data-placement="end"] .nodedc-window__head {
|
||||||
|
min-height: 4.875rem;
|
||||||
|
padding: 1.125rem 0.75rem 0.875rem 1.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-window[data-placement="end"] .nodedc-window__body {
|
||||||
|
padding: 0 0.75rem 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-window[data-placement="end"] .nodedc-window__close {
|
||||||
|
border-radius: var(--nodedc-inspector-button-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes nodedc-window-in {
|
@keyframes nodedc-window-in {
|
||||||
|
|
@ -998,6 +1405,7 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-inspector {
|
.nodedc-inspector {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: min(var(--nodedc-inspector-inner-width), 100%);
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1018,7 +1426,7 @@ textarea.nodedc-field__control {
|
||||||
min-height: 3.125rem;
|
min-height: 3.125rem;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
border-radius: var(--nodedc-radius-control-compact);
|
border-radius: var(--nodedc-inspector-section-radius);
|
||||||
background: var(--nodedc-glass-control-bg);
|
background: var(--nodedc-glass-control-bg);
|
||||||
color: var(--nodedc-text-secondary);
|
color: var(--nodedc-text-secondary);
|
||||||
padding: 0.55rem 0.75rem;
|
padding: 0.55rem 0.75rem;
|
||||||
|
|
@ -1033,7 +1441,8 @@ textarea.nodedc-field__control {
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-inspector__section-trigger[data-active="true"] {
|
.nodedc-inspector__section-trigger[data-active="true"],
|
||||||
|
.nodedc-inspector__section-trigger[data-tone="accent"] {
|
||||||
background: rgb(var(--nodedc-accent-rgb));
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
color: rgb(var(--nodedc-on-accent-rgb));
|
color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
}
|
}
|
||||||
|
|
@ -1066,10 +1475,11 @@ textarea.nodedc-field__control {
|
||||||
|
|
||||||
.nodedc-control-row {
|
.nodedc-control-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: min(var(--nodedc-inspector-inner-width), 100%);
|
||||||
min-height: var(--nodedc-control-height);
|
min-height: var(--nodedc-control-height);
|
||||||
grid-template-columns: minmax(7rem, 1fr) minmax(10rem, 1.1fr);
|
grid-template-columns: var(--nodedc-inspector-label-width) var(--nodedc-inspector-control-width);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--nodedc-space-3);
|
gap: var(--nodedc-inspector-control-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-control-row[data-layout="stack"] {
|
.nodedc-control-row[data-layout="stack"] {
|
||||||
|
|
@ -1081,7 +1491,7 @@ textarea.nodedc-field__control {
|
||||||
.nodedc-control-row__label {
|
.nodedc-control-row__label {
|
||||||
color: var(--nodedc-text-secondary);
|
color: var(--nodedc-text-secondary);
|
||||||
font-size: var(--nodedc-font-size-sm);
|
font-size: var(--nodedc-font-size-sm);
|
||||||
font-weight: var(--nodedc-font-weight-medium);
|
font-weight: var(--nodedc-font-weight-regular);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1129,6 +1539,10 @@ textarea.nodedc-field__control {
|
||||||
padding: 0.875rem;
|
padding: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-window[data-placement="end"] .nodedc-window__body {
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-control-row {
|
.nodedc-control-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"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.1.0"
|
"@nodedc/ui-core": "0.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"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.1.0"
|
"@nodedc/ui-core": "0.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import type { HTMLAttributes, ReactNode } from "react";
|
||||||
|
import { cn } from "./cn";
|
||||||
|
|
||||||
|
export interface AdminNavigationContext {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
groupLabel?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
active?: boolean;
|
||||||
|
onSelect?: () => void;
|
||||||
|
onToggle?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminNavigationItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminNavigationPanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
||||||
|
eyebrow?: string;
|
||||||
|
title: ReactNode;
|
||||||
|
contexts?: AdminNavigationContext[];
|
||||||
|
items: AdminNavigationItem[];
|
||||||
|
activeId?: string;
|
||||||
|
footer?: ReactNode;
|
||||||
|
closeLabel?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onItemChange: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminNavigationPanel({
|
||||||
|
eyebrow = "NODE.DC",
|
||||||
|
title,
|
||||||
|
contexts = [],
|
||||||
|
items,
|
||||||
|
activeId,
|
||||||
|
footer,
|
||||||
|
closeLabel = "Закрыть администрирование",
|
||||||
|
onClose,
|
||||||
|
onItemChange,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AdminNavigationPanelProps) {
|
||||||
|
return (
|
||||||
|
<aside className={cn("nodedc-admin-panel", className)} {...props}>
|
||||||
|
<header className="nodedc-admin-panel__head">
|
||||||
|
<div>
|
||||||
|
<p>{eyebrow}</p>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="nodedc-admin-panel__close" aria-label={closeLabel} onClick={onClose}>
|
||||||
|
<span className="nodedc-close-mark" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="nodedc-admin-panel__contexts">
|
||||||
|
{contexts.map((context) => (
|
||||||
|
<div className="nodedc-admin-panel__context-group" key={context.id}>
|
||||||
|
{context.groupLabel ? <span className="nodedc-admin-panel__group-label">{context.groupLabel}</span> : null}
|
||||||
|
<div
|
||||||
|
className={cn("nodedc-admin-context", context.onToggle && "nodedc-admin-context--split")}
|
||||||
|
data-active={context.active ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<button type="button" className="nodedc-admin-context__main" onClick={context.onSelect}>
|
||||||
|
<span className="nodedc-admin-context__icon" aria-hidden="true">{context.icon}</span>
|
||||||
|
<span className="nodedc-admin-context__body">
|
||||||
|
<strong>{context.label}</strong>
|
||||||
|
{context.description ? <small>{context.description}</small> : null}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{context.onToggle ? (
|
||||||
|
<button type="button" className="nodedc-admin-context__toggle" aria-label={`Выбрать: ${context.label}`} onClick={context.onToggle}>
|
||||||
|
<span className="nodedc-select-trigger__chevron" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="nodedc-admin-panel__nav" aria-label="Разделы администрирования">
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
className="nodedc-admin-panel__nav-item"
|
||||||
|
data-active={activeId === item.id ? "true" : undefined}
|
||||||
|
onClick={() => onItemChange(item.id)}
|
||||||
|
>
|
||||||
|
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">{item.icon}</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{footer ? <footer className="nodedc-admin-panel__footer">{footer}</footer> : null}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { HTMLAttributes, ReactNode } from "react";
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||||
import { cn } from "./cn";
|
import { cn } from "./cn";
|
||||||
|
|
||||||
export interface AppHeaderProps extends HTMLAttributes<HTMLElement> {
|
export interface AppHeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
|
@ -41,6 +41,23 @@ export function HeaderProfile({ children, className, ...props }: HTMLAttributes<
|
||||||
return <div className={cn("nodedc-header__profile", className)} {...props}>{children}</div>;
|
return <div className={cn("nodedc-header__profile", className)} {...props}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HeaderProfileButton({ className, children, type = "button", ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||||
|
return <button type={type} className={cn("nodedc-header__profile-button", className)} {...props}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeaderAvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
|
label: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderAvatar({ label, imageUrl, className, ...props }: HeaderAvatarProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn("nodedc-header__avatar", className)} title={label} {...props}>
|
||||||
|
{imageUrl ? <img src={imageUrl} alt="" /> : label.slice(0, 2).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface HeaderWorkspaceProps extends HTMLAttributes<HTMLSpanElement> {
|
export interface HeaderWorkspaceProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
label: string;
|
label: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
|
@ -53,4 +70,3 @@ export function HeaderWorkspace({ label, imageUrl, className, ...props }: Header
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ import { cn } from "./cn";
|
||||||
|
|
||||||
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "accent";
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "accent";
|
||||||
export type ButtonSize = "default" | "compact";
|
export type ButtonSize = "default" | "compact";
|
||||||
|
export type ButtonShape = "default" | "pill" | "rounded";
|
||||||
|
|
||||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
width?: "auto" | "full";
|
width?: "auto" | "full";
|
||||||
|
shape?: ButtonShape;
|
||||||
accent?: RgbTuple;
|
accent?: RgbTuple;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +19,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||||
variant = "secondary",
|
variant = "secondary",
|
||||||
size = "default",
|
size = "default",
|
||||||
width = "auto",
|
width = "auto",
|
||||||
|
shape = "default",
|
||||||
accent,
|
accent,
|
||||||
icon,
|
icon,
|
||||||
className,
|
className,
|
||||||
|
|
@ -34,6 +37,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size === "default" ? undefined : size}
|
data-size={size === "default" ? undefined : size}
|
||||||
data-width={width === "auto" ? undefined : width}
|
data-width={width === "auto" ? undefined : width}
|
||||||
|
data-shape={shape === "default" ? undefined : shape}
|
||||||
style={accentStyle ? { ...accentStyle, ...style } : style}
|
style={accentStyle ? { ...accentStyle, ...style } : style}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface InspectorSectionSpec {
|
||||||
group?: string;
|
group?: string;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
tone?: "default" | "accent";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InspectorProps {
|
export interface InspectorProps {
|
||||||
|
|
@ -64,6 +65,7 @@ export function Inspector({
|
||||||
className="nodedc-inspector__section-trigger"
|
className="nodedc-inspector__section-trigger"
|
||||||
data-open={isOpen ? "true" : undefined}
|
data-open={isOpen ? "true" : undefined}
|
||||||
data-active={activeId === section.id ? "true" : undefined}
|
data-active={activeId === section.id ? "true" : undefined}
|
||||||
|
data-tone={section.tone === "accent" ? "accent" : undefined}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
disabled={section.disabled}
|
disabled={section.disabled}
|
||||||
onClick={() => toggle(section.id)}
|
onClick={() => toggle(section.id)}
|
||||||
|
|
@ -98,4 +100,3 @@ export function ControlRow({ label, children, layout = "inline", className }: Co
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export interface SelectOption<T extends string> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SelectVariant = "integrated" | "split";
|
||||||
|
|
||||||
export interface SelectProps<T extends string> {
|
export interface SelectProps<T extends string> {
|
||||||
value: T;
|
value: T;
|
||||||
options: Array<SelectOption<T>>;
|
options: Array<SelectOption<T>>;
|
||||||
|
|
@ -23,6 +25,7 @@ export interface SelectProps<T extends string> {
|
||||||
minMenuWidth?: number;
|
minMenuWidth?: number;
|
||||||
menuWidth?: number | "anchor";
|
menuWidth?: number | "anchor";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
variant?: SelectVariant;
|
||||||
className?: string;
|
className?: string;
|
||||||
triggerClassName?: string;
|
triggerClassName?: string;
|
||||||
menuClassName?: string;
|
menuClassName?: string;
|
||||||
|
|
@ -40,6 +43,7 @@ export function Select<T extends string>({
|
||||||
minMenuWidth = 180,
|
minMenuWidth = 180,
|
||||||
menuWidth = "anchor",
|
menuWidth = "anchor",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
variant = "integrated",
|
||||||
className,
|
className,
|
||||||
triggerClassName,
|
triggerClassName,
|
||||||
menuClassName,
|
menuClassName,
|
||||||
|
|
@ -54,13 +58,13 @@ export function Select<T extends string>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className={className}
|
className={cn("nodedc-select-anchor", className)}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
minWidth={minMenuWidth}
|
minWidth={minMenuWidth}
|
||||||
width={menuWidth}
|
width={menuWidth}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
surfaceRole="listbox"
|
surfaceRole="listbox"
|
||||||
surfaceClassName={menuClassName}
|
surfaceClassName={cn(variant === "split" && "nodedc-select__menu", menuClassName)}
|
||||||
trigger={({ open, toggle, setTriggerRef, surfaceId }) => {
|
trigger={({ open, toggle, setTriggerRef, surfaceId }) => {
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||||||
|
|
@ -68,6 +72,32 @@ export function Select<T extends string>({
|
||||||
if (!open) toggle();
|
if (!open) toggle();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (variant === "split") {
|
||||||
|
return (
|
||||||
|
<div className={cn("nodedc-select", triggerClassName)}>
|
||||||
|
<div className="nodedc-select__control">
|
||||||
|
<div ref={setTriggerRef} className="nodedc-select__value">
|
||||||
|
{selected?.icon ? <span className="nodedc-select__value-icon">{selected.icon}</span> : null}
|
||||||
|
<span>{selected?.label ?? "—"}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-select__toggle"
|
||||||
|
aria-label={label}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-controls={surfaceId}
|
||||||
|
aria-expanded={open}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={toggle}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<span className="nodedc-select-trigger__chevron" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={setTriggerRef}
|
ref={setTriggerRef}
|
||||||
|
|
@ -126,4 +156,3 @@ export function Select<T extends string>({
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export interface WindowProps extends Omit<HTMLAttributes<HTMLDivElement>, "title
|
||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
closeOnEscape?: boolean;
|
closeOnEscape?: boolean;
|
||||||
lockBodyScroll?: boolean;
|
lockBodyScroll?: boolean;
|
||||||
|
trapFocus?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,9 +46,10 @@ export function Window({
|
||||||
size = "md",
|
size = "md",
|
||||||
placement = "center",
|
placement = "center",
|
||||||
closeLabel = "Закрыть",
|
closeLabel = "Закрыть",
|
||||||
closeOnBackdrop = true,
|
closeOnBackdrop,
|
||||||
closeOnEscape = true,
|
closeOnEscape = true,
|
||||||
lockBodyScroll = true,
|
lockBodyScroll,
|
||||||
|
trapFocus,
|
||||||
onClose,
|
onClose,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
|
|
@ -55,12 +57,14 @@ export function Window({
|
||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
const descriptionId = useId();
|
const descriptionId = useId();
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
const shouldLockBodyScroll = lockBodyScroll ?? placement === "center";
|
||||||
|
const shouldTrapFocus = trapFocus ?? placement === "center";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || typeof document === "undefined") return;
|
if (!open || typeof document === "undefined") return;
|
||||||
const previousActiveElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
const previousActiveElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
const previousOverflow = document.body.style.overflow;
|
const previousOverflow = document.body.style.overflow;
|
||||||
if (lockBodyScroll) document.body.style.overflow = "hidden";
|
if (shouldLockBodyScroll) document.body.style.overflow = "hidden";
|
||||||
|
|
||||||
const frame = window.requestAnimationFrame(() => {
|
const frame = window.requestAnimationFrame(() => {
|
||||||
const firstFocusable = dialogRef.current?.querySelector<HTMLElement>(focusableSelector);
|
const firstFocusable = dialogRef.current?.querySelector<HTMLElement>(focusableSelector);
|
||||||
|
|
@ -73,7 +77,7 @@ export function Window({
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key !== "Tab" || !dialogRef.current) return;
|
if (event.key !== "Tab" || !shouldTrapFocus || !dialogRef.current) return;
|
||||||
const focusable = Array.from(dialogRef.current.querySelectorAll<HTMLElement>(focusableSelector));
|
const focusable = Array.from(dialogRef.current.querySelectorAll<HTMLElement>(focusableSelector));
|
||||||
if (focusable.length === 0) {
|
if (focusable.length === 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -95,15 +99,17 @@ export function Window({
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(frame);
|
window.cancelAnimationFrame(frame);
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
if (lockBodyScroll) document.body.style.overflow = previousOverflow;
|
if (shouldLockBodyScroll) document.body.style.overflow = previousOverflow;
|
||||||
previousActiveElement?.focus();
|
previousActiveElement?.focus();
|
||||||
};
|
};
|
||||||
}, [closeOnEscape, lockBodyScroll, onClose, open]);
|
}, [closeOnEscape, onClose, open, shouldLockBodyScroll, shouldTrapFocus]);
|
||||||
|
|
||||||
if (!open || typeof document === "undefined") return null;
|
if (!open || typeof document === "undefined") return null;
|
||||||
|
|
||||||
|
const allowBackdropClose = closeOnBackdrop ?? placement === "center";
|
||||||
|
|
||||||
const handleBackdropPointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
const handleBackdropPointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
||||||
if (closeOnBackdrop && event.target === event.currentTarget) onClose();
|
if (allowBackdropClose && event.target === event.currentTarget) onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
|
|
@ -114,7 +120,7 @@ export function Window({
|
||||||
data-size={size === "md" ? undefined : size}
|
data-size={size === "md" ? undefined : size}
|
||||||
data-placement={placement}
|
data-placement={placement}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal={placement === "center" ? "true" : undefined}
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
aria-describedby={subtitle ? descriptionId : undefined}
|
aria-describedby={subtitle ? descriptionId : undefined}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
@ -140,4 +146,3 @@ export function Window({
|
||||||
export function WindowFooterActions({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
export function WindowFooterActions({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
return <div className={cn("nodedc-window__footer-actions", className)} {...props}>{children}</div>;
|
return <div className={cn("nodedc-window__footer-actions", className)} {...props}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./AppHeader";
|
export * from "./AppHeader";
|
||||||
|
export * from "./AdminNavigationPanel";
|
||||||
export * from "./Button";
|
export * from "./Button";
|
||||||
export * from "./Checker";
|
export * from "./Checker";
|
||||||
export * from "./ConfirmationModal";
|
export * from "./ConfirmationModal";
|
||||||
|
|
@ -11,4 +12,3 @@ export * from "./SegmentedControl";
|
||||||
export * from "./Select";
|
export * from "./Select";
|
||||||
export * from "./StatusBadge";
|
export * from "./StatusBadge";
|
||||||
export * from "./Window";
|
export * from "./Window";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,6 @@
|
||||||
"requiredBehavior": ["toolbar", "responsive overflow", "row actions", "editable cells", "loading", "empty state"],
|
"requiredBehavior": ["toolbar", "responsive overflow", "row actions", "editable cells", "loading", "empty state"],
|
||||||
"reasonNotBaseline": "The library should own presentation and interaction slots, not adopt Launcher entity schemas or Plane table models."
|
"reasonNotBaseline": "The library should own presentation and interaction slots, not adopt Launcher entity schemas or Plane table models."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "side-navigation",
|
|
||||||
"classification": "planned-pattern",
|
|
||||||
"priority": "P1",
|
|
||||||
"referenceSources": ["launcher", "engine"],
|
|
||||||
"expectedPackage": "@nodedc/ui-react",
|
|
||||||
"dependsOn": ["glass-surface", "button", "status-badge"],
|
|
||||||
"requiredBehavior": ["sections", "active item", "optional nested groups", "compact viewport"],
|
|
||||||
"reasonNotBaseline": "Inspector accordion is implemented; application-level navigation requires a separate route-aware contract."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "avatar-stack",
|
"id": "avatar-stack",
|
||||||
"classification": "planned-primitive",
|
"classification": "planned-primitive",
|
||||||
|
|
@ -124,4 +114,3 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"domContract": ["nodedc-button", "nodedc-icon-button"],
|
"domContract": ["nodedc-button", "nodedc-icon-button"],
|
||||||
"summary": "Text and icon actions with shared sizing, states and computed accent contrast.",
|
"summary": "Text and icon actions with shared sizing, states and computed accent contrast.",
|
||||||
"variants": ["primary", "secondary", "ghost", "danger", "accent"],
|
"variants": ["primary", "secondary", "ghost", "danger", "accent"],
|
||||||
|
"shapes": ["default", "pill", "rounded"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"Icon-only actions are circular by default.",
|
"Icon-only actions are circular by default.",
|
||||||
"Filled accent actions derive foreground contrast from the actual accent.",
|
"Filled accent actions derive foreground contrast from the actual accent.",
|
||||||
|
|
@ -97,7 +98,13 @@
|
||||||
"domPackage": "@nodedc/ui-dom",
|
"domPackage": "@nodedc/ui-dom",
|
||||||
"domExports": ["createSelectController"],
|
"domExports": ["createSelectController"],
|
||||||
"summary": "Theme-independent selection control using the canonical dropdown layer.",
|
"summary": "Theme-independent selection control using the canonical dropdown layer.",
|
||||||
"behavior": ["controlled value", "optional search", "disabled options", "selected state", "portal menu"]
|
"variants": ["integrated", "split"],
|
||||||
|
"behavior": ["controlled value", "optional search", "disabled options", "selected state", "portal menu"],
|
||||||
|
"rules": [
|
||||||
|
"Integrated is the single-pill Hub/Launcher form.",
|
||||||
|
"Split is the Engine form: a separate value surface and a 46 px toggle separated by an 8 px gap.",
|
||||||
|
"The portal menu preserves its source-family row geometry instead of inheriting card radii."
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "window",
|
"id": "window",
|
||||||
|
|
@ -108,10 +115,11 @@
|
||||||
"domExports": ["createModalController"],
|
"domExports": ["createModalController"],
|
||||||
"domContract": ["nodedc-overlay", "nodedc-window"],
|
"domContract": ["nodedc-overlay", "nodedc-window"],
|
||||||
"summary": "Canonical controlled window for modal and right-side panel presentations.",
|
"summary": "Canonical controlled window for modal and right-side panel presentations.",
|
||||||
"behavior": ["portal to body", "focus entry", "focus trap", "focus restore", "Escape close", "backdrop close policy", "body scroll lock"],
|
"behavior": ["portal to body", "focus entry", "modal-only focus trap", "focus restore", "Escape close", "backdrop close policy", "modal-only body scroll lock"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"Open state belongs to the application; focus/layer behavior belongs to the component.",
|
"Open state belongs to the application; focus/layer behavior belongs to the component.",
|
||||||
"The same window contract is used by Launcher, CMS and SEO; theme variables supply color differences."
|
"The same window contract is used by Launcher, CMS and SEO; theme variables supply color differences.",
|
||||||
|
"A right-side inspector is modeless by default: no dimming, no backdrop blur, no backdrop close, no focus trap and no body scroll lock."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -134,7 +142,7 @@
|
||||||
"id": "app-header",
|
"id": "app-header",
|
||||||
"status": "baseline",
|
"status": "baseline",
|
||||||
"package": "@nodedc/ui-react",
|
"package": "@nodedc/ui-react",
|
||||||
"exports": ["AppHeader", "HeaderProfile", "HeaderWorkspace"],
|
"exports": ["AppHeader", "HeaderProfile", "HeaderProfileButton", "HeaderWorkspace", "HeaderAvatar"],
|
||||||
"domContract": ["nodedc-header-shell", "nodedc-header"],
|
"domContract": ["nodedc-header-shell", "nodedc-header"],
|
||||||
"summary": "Three-axis NODE.DC application header with fixed brand geometry and composable center/right slots.",
|
"summary": "Three-axis NODE.DC application header with fixed brand geometry and composable center/right slots.",
|
||||||
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
||||||
|
|
@ -143,6 +151,20 @@
|
||||||
"Product routes and profile content are application data passed into stable slots."
|
"Product routes and profile content are application data passed into stable slots."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-navigation-panel",
|
||||||
|
"status": "baseline",
|
||||||
|
"package": "@nodedc/ui-react",
|
||||||
|
"exports": ["AdminNavigationPanel"],
|
||||||
|
"domContract": ["nodedc-admin-panel"],
|
||||||
|
"summary": "Hub-derived left administration panel with context selectors, route navigation, close action and footer identity.",
|
||||||
|
"anatomy": ["eyebrow and title", "close action", "workspace context", "company context", "route items", "footer identity"],
|
||||||
|
"rules": [
|
||||||
|
"The panel shell, full-bleed pills and icon circles are library geometry; routes and identity data remain application-owned.",
|
||||||
|
"The desktop panel is 352 px wide with a 21.6 px shell radius and opens beside, not over, the main Hub content.",
|
||||||
|
"Active route state uses the active surface and icon contrast from the theme."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "status-badge",
|
"id": "status-badge",
|
||||||
"status": "baseline",
|
"status": "baseline",
|
||||||
|
|
@ -160,9 +182,10 @@
|
||||||
"summary": "Accordion settings/definition inspector derived only from the redesigned Engine environment and NDC agent inspector.",
|
"summary": "Accordion settings/definition inspector derived only from the redesigned Engine environment and NDC agent inspector.",
|
||||||
"rules": [
|
"rules": [
|
||||||
"Legacy Engine inspectors are not reference implementations.",
|
"Legacy Engine inspectors are not reference implementations.",
|
||||||
"Domain-specific field definitions remain in Engine; reusable layout and controls live here."
|
"Domain-specific field definitions remain in Engine; reusable layout and controls live here.",
|
||||||
|
"Desktop Engine geometry is fixed at 390 px panel, 330 px content, 154/14/162 px control rows and 50 px section headers with a 12 px radius.",
|
||||||
|
"Accent-filled section headers belong to Environment Settings; neutral headers remain available for other approved inspector contexts."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": "1.0.0",
|
"schemaVersion": "1.0.0",
|
||||||
"libraryVersion": "0.1.0",
|
"libraryVersion": "0.2.0",
|
||||||
"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