diff --git a/CHANGELOG.md b/CHANGELOG.md index 57770f9..029a200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 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 - Established the standalone NODE.DC design-system repository. @@ -9,4 +18,3 @@ - Added React primitives, overlays, navigation and inspector/settings patterns. - Added a living catalog and machine-readable component/source registry. - Recorded the initial audit boundary without modifying source applications. - diff --git a/README.md b/README.md index 08e59ff..ed3dbca 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - кнопки, поля, чекеры, range, select и dropdown; - модальные окна, подтверждения и боковые окна; - segmented navigation и статусы; +- Hub header и левая admin navigation panel; - инспектор/панель настроек нового поколения; - поведение 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/ADOPTION.md) - [Инвентарь следующих компонентов](docs/CANDIDATES.md) - diff --git a/apps/catalog/package.json b/apps/catalog/package.json index 4914bb8..190f67a 100644 --- a/apps/catalog/package.json +++ b/apps/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-catalog", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "typecheck": "tsc -b --pretty false" }, "dependencies": { - "@nodedc/ui-core": "0.1.0", - "@nodedc/ui-react": "0.1.0", + "@nodedc/ui-core": "0.2.0", + "@nodedc/ui-react": "0.2.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -22,4 +22,3 @@ "vite": "^7.0.0" } } - diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index 9e00f94..62e3d39 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react"; import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core"; import { + AdminNavigationPanel, AppHeader, Button, Checker, @@ -11,6 +12,8 @@ import { FieldFrame, GlassSurface, HeaderProfile, + HeaderProfileButton, + HeaderAvatar, HeaderWorkspace, IconButton, Inspector, @@ -25,6 +28,12 @@ import { } from "@nodedc/ui-react"; 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 }> = [ { label: "Hub", value: [195, 255, 102], hex: "#c3ff66" }, @@ -89,9 +98,18 @@ export function CatalogApp() { const [theme, setTheme] = useState("dark"); const [accentHex, setAccentHex] = useState("#c3ff66"); const [section, setSection] = useState("foundation"); + const [hubMode, setHubMode] = useState("showcase"); + const [adminSection, setAdminSection] = useState("invites"); const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active"); 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 [inspectorOpen, setInspectorOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); @@ -111,40 +129,56 @@ export function CatalogApp() { const inspectorSections = [ { - id: "definition", - label: "Definition Control", - description: "Параметры агентной ноды", + id: "illumination", + label: "Подсветка", + tone: "accent" as const, content: ( <> - - - + + + + + + + ), }, { - id: "runtime", - label: "Выполнение", - description: "Параметры запуска", - content: undefined} />, + id: "ports", + label: "Порты", + tone: "accent" as const, + content: ( + <> + + `${value}%`} onChange={() => undefined} /> + + `${value}%`} onChange={() => undefined} /> + + ), }, ]; @@ -154,7 +188,76 @@ export function CatalogApp() { brand={NODE.DC} center={ <> - + + + + } + right={ + + + Профиль + + + } + /> + + {hubMode === "admin" ? ( +
+ 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={ + <> + + Root Admin + + } + /> +
+ ) : null} + +
+
+ NODE.DC UI baseline +

Один UI-контракт.
Разные приложения.

+

Геометрия, окна и поведение остаются общими. Приложение выбирает тему и акцент, затем использует готовые компоненты.

+
- - } - right={ - - setTheme((current) => current === "dark" ? "light" : "dark")}> - {theme === "dark" ? "☼" : "◐"} - - DC - - } - /> - -
-
- baseline 0.1.0 -

Один UI-контракт.
Разные приложения.

-

Геометрия, окна и поведение остаются общими. Приложение выбирает тему и акцент, затем использует готовые компоненты.

+
@@ -222,7 +309,7 @@ export function CatalogApp() {
- + @@ -237,9 +324,24 @@ export function CatalogApp() {
- + +
+ ( @@ -255,10 +357,12 @@ export function CatalogApp() {
- -
- - `${value}%`} onChange={setRange} /> + +
+ + `${value}%`} onChange={setBrightness} /> + +
@@ -273,7 +377,7 @@ export function CatalogApp() {
-
+
@@ -285,9 +389,9 @@ export function CatalogApp() {
- -
ИнспекторAgent Definition
- + +
Настройки окруженияENGINE / новый канон
+
@@ -312,8 +416,15 @@ export function CatalogApp() {
- setInspectorOpen(false)}> - + setInspectorOpen(false)} + > + ); } - diff --git a/apps/catalog/src/styles.css b/apps/catalog/src/styles.css index 747f2c6..febf6aa 100644 --- a/apps/catalog/src/styles.css +++ b/apps/catalog/src/styles.css @@ -30,20 +30,16 @@ textarea { min-height: 100vh; } -[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img { - filter: brightness(0.12); +.catalog-admin-layer { + position: fixed; + z-index: 390; + top: 6.55rem; + bottom: 1.25rem; + left: 1.25rem; } -.catalog-avatar { - display: inline-grid; - 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; +[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img { + filter: brightness(0.12); } .catalog-main { @@ -52,6 +48,13 @@ textarea { margin: 0 auto; gap: 8rem; 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 { @@ -80,6 +83,10 @@ textarea { line-height: 1.5; } +.catalog-section-nav { + margin-top: 0.6rem; +} + .catalog-section { display: grid; scroll-margin-top: 2rem; @@ -205,6 +212,13 @@ textarea { 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 { min-height: 4rem; display: flex; @@ -227,12 +241,13 @@ textarea { .catalog-inspector-card { width: min(24.375rem, 100%); + padding: 1.125rem 0.75rem; } .catalog-inspector-title { display: grid; gap: 0.3rem; - padding: 0.75rem 0.75rem 1rem; + padding: 0 0.875rem 0.875rem; } .catalog-inspector-title span { @@ -246,6 +261,11 @@ textarea { padding-top: 2rem; } + .catalog-main[data-admin-open="true"] { + width: min(1180px, calc(100% - 2rem)); + margin-inline: auto; + } + .catalog-grid, .catalog-grid--foundation { grid-template-columns: 1fr; @@ -258,4 +278,11 @@ textarea { .catalog-inspector-stage { padding: 1rem; } + + .catalog-admin-layer { + top: 0.875rem; + right: 0.875rem; + bottom: 0.875rem; + left: 0.875rem; + } } diff --git a/docs/CANDIDATES.md b/docs/CANDIDATES.md index fd6ff63..2e790db 100644 --- a/docs/CANDIDATES.md +++ b/docs/CANDIDATES.md @@ -8,7 +8,6 @@ Baseline намеренно отделяет готовые exports от изв - ProfileMenu — отделить визуальную карточку от identity/role API приложения. - ActionDropdown — добавить типизированные command rows поверх готового Dropdown. - DataTable — вынести toolbar, cells, loading/empty и row actions без entity schemas Launcher/Plane. -- SideNavigation — route-aware навигация отдельно от уже готового Inspector accordion. - AvatarStack — согласовать overflow, presence и picker mode. ## P2 — продуктовые patterns @@ -32,4 +31,3 @@ Baseline намеренно отделяет готовые exports от изв 6. выпустить minor version. Так список сохраняет уже проделанную работу, но не превращает незавершённый локальный код в ложный канон. - diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 7546f86..9cf9a66 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -57,7 +57,12 @@ Dropdown владеет floating-layer поведением: ## 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 @@ -66,7 +71,7 @@ Window — единая механика открытия modal и правой - приложение контролирует `open`; - библиотека управляет portal, focus entry/trap/restore, Escape, backdrop и scroll lock; - `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 Статус использует semantic tone: neutral, success, warning, danger или accent. Бизнес-статус приложения маппится на tone в самом приложении. @@ -104,5 +117,6 @@ Inspector владеет: - вертикальным ритмом; - раскладкой 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 продолжает владеть определениями полей, типами нод и сохранением значений. diff --git a/docs/SOURCE_BASELINE.md b/docs/SOURCE_BASELINE.md index b76480f..61a3fda 100644 --- a/docs/SOURCE_BASELINE.md +++ b/docs/SOURCE_BASELINE.md @@ -13,6 +13,8 @@ - button/modal/dropdown primitives; - общая логика 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` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория. ## SEO и CMS @@ -36,6 +38,8 @@ CMS подтверждает необходимость DOM-пакета без Все остальные 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 Используются только уже стандартизированные shared patterns и документы по dropdown/modal behavior. Известные legacy CustomMenu и screen-local wrappers не являются источником компонентов. @@ -56,4 +60,3 @@ CMS подтверждает необходимость DOM-пакета без - различную степень accessibility. Поэтому библиотека фиксирует очищенный API и поведение, сохраняя происхождение решения в реестре. - diff --git a/docs/WINDOWS_AND_LAYERS.md b/docs/WINDOWS_AND_LAYERS.md index bdb22f3..2c3207a 100644 --- a/docs/WINDOWS_AND_LAYERS.md +++ b/docs/WINDOWS_AND_LAYERS.md @@ -53,9 +53,10 @@ Side window использует ту же механику, но placement `end - будущих detail/settings panels; - CMS/SEO side editors, если они не требуют отдельного полноэкранного workflow. +По умолчанию это modeless-слой: он не затемняет страницу, не перехватывает клики по основной области, не закрывается по backdrop, не блокирует body scroll и не удерживает Tab внутри панели. Escape и кнопка закрытия остаются доступны. Если конкретный workflow должен быть modal, это задаётся явно, а не получается случайно из placement. + ## Управление состоянием Библиотека не создаёт глобальный store окон. Приложение владеет тем, какое окно открыто и какие данные в нём загружены. Библиотека владеет одинаковым поведением самого слоя. Если приложению позже потребуется stack нескольких modeless-окон, это будет отдельный пакетный контракт, а не расширение локального z-index. - diff --git a/package-lock.json b/package-lock.json index 7085691..bad4583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodedc-design-guideline", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodedc-design-guideline", - "version": "0.1.0", + "version": "0.2.0", "workspaces": [ "packages/*", "apps/*" @@ -25,10 +25,10 @@ }, "apps/catalog": { "name": "@nodedc/ui-catalog", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { - "@nodedc/ui-core": "0.1.0", - "@nodedc/ui-react": "0.1.0", + "@nodedc/ui-core": "0.2.0", + "@nodedc/ui-react": "0.2.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -1879,27 +1879,27 @@ }, "packages/tokens": { "name": "@nodedc/tokens", - "version": "0.1.0" + "version": "0.2.0" }, "packages/ui-core": { "name": "@nodedc/ui-core", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { - "@nodedc/tokens": "0.1.0" + "@nodedc/tokens": "0.2.0" } }, "packages/ui-dom": { "name": "@nodedc/ui-dom", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { - "@nodedc/ui-core": "0.1.0" + "@nodedc/ui-core": "0.2.0" } }, "packages/ui-react": { "name": "@nodedc/ui-react", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { - "@nodedc/ui-core": "0.1.0" + "@nodedc/ui-core": "0.2.0" }, "devDependencies": { "@types/react": "^19.1.0", diff --git a/package.json b/package.json index 8090e6e..45f75d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodedc-design-guideline", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.", "type": "module", diff --git a/packages/tokens/package.json b/packages/tokens/package.json index 1b9f926..f9ccfe0 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/tokens", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "files": ["tokens.json", "tokens.css", "themes.css"], "exports": { diff --git a/packages/tokens/tokens.css b/packages/tokens/tokens.css index 9f66656..f55cf84 100644 --- a/packages/tokens/tokens.css +++ b/packages/tokens/tokens.css @@ -16,6 +16,14 @@ --nodedc-header-logo-width: 7.25rem; --nodedc-header-logo-height: 1.79rem; --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-2: 0.5rem; @@ -53,4 +61,3 @@ --nodedc-font-weight-medium: 620; --nodedc-font-weight-strong: 760; } - diff --git a/packages/tokens/tokens.json b/packages/tokens/tokens.json index 555e0b4..855aefc 100644 --- a/packages/tokens/tokens.json +++ b/packages/tokens/tokens.json @@ -26,7 +26,13 @@ "headerPill": { "$type": "dimension", "$value": "55.2px" }, "logoWidth": { "$type": "dimension", "$value": "116px" }, "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": { "1": { "$type": "dimension", "$value": "4px" }, @@ -58,4 +64,3 @@ "toast": { "$type": "number", "$value": 950 } } } - diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index 425d77e..f57e313 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-core", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "files": ["dist", "styles.css"], "main": "./dist/index.js", @@ -17,6 +17,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/tokens": "0.1.0" + "@nodedc/tokens": "0.2.0" } } diff --git a/packages/ui-core/styles.css b/packages/ui-core/styles.css index 635caf9..66519a2 100644 --- a/packages/ui-core/styles.css +++ b/packages/ui-core/styles.css @@ -15,13 +15,13 @@ 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; outline: 0; 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); 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%; } +.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="accent"] { --nodedc-button-bg: rgb(var(--nodedc-accent-rgb)); @@ -313,7 +321,6 @@ textarea.nodedc-field__control { } .nodedc-checker__indicator { - position: relative; width: 1.625rem; height: 1.625rem; 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); } -.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 { background: rgb(var(--nodedc-accent-rgb)); box-shadow: none; } -.nodedc-checker[aria-checked="true"] .nodedc-checker__indicator::after { - opacity: 1; - transform: translate(-50%, -50%) scale(1); -} - .nodedc-range { --nodedc-range-progress: 50%; position: relative; @@ -601,6 +590,59 @@ textarea.nodedc-field__control { 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 { display: inline-grid; width: var(--nodedc-header-row-height); @@ -649,10 +691,276 @@ textarea.nodedc-field__control { 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 { display: inline-flex; } +.nodedc-dropdown-anchor.nodedc-select-anchor { + display: block; + width: 100%; +} + .nodedc-dropdown-surface { position: fixed; z-index: var(--nodedc-layer-popover); @@ -756,6 +1064,83 @@ textarea.nodedc-field__control { 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 { display: grid; width: 100%; @@ -799,6 +1184,10 @@ textarea.nodedc-field__control { transform: translateY(2px) rotate(225deg); } +.nodedc-select__toggle[aria-expanded="true"] .nodedc-select-trigger__chevron { + transform: translateY(2px) rotate(225deg); +} + .nodedc-overlay { position: fixed; z-index: var(--nodedc-layer-overlay); @@ -815,7 +1204,11 @@ textarea.nodedc-field__control { .nodedc-overlay[data-placement="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 { @@ -853,6 +1246,20 @@ textarea.nodedc-field__control { width: min(var(--nodedc-inspector-width), calc(100vw - 3rem)); max-height: calc(100vh - 7.25rem); 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 { @@ -998,6 +1405,7 @@ textarea.nodedc-field__control { .nodedc-inspector { display: grid; + width: min(var(--nodedc-inspector-inner-width), 100%); gap: 1px; } @@ -1018,7 +1426,7 @@ textarea.nodedc-field__control { min-height: 3.125rem; align-content: center; gap: 0.2rem; - border-radius: var(--nodedc-radius-control-compact); + border-radius: var(--nodedc-inspector-section-radius); background: var(--nodedc-glass-control-bg); color: var(--nodedc-text-secondary); padding: 0.55rem 0.75rem; @@ -1033,7 +1441,8 @@ textarea.nodedc-field__control { 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)); color: rgb(var(--nodedc-on-accent-rgb)); } @@ -1066,10 +1475,11 @@ textarea.nodedc-field__control { .nodedc-control-row { display: grid; + width: min(var(--nodedc-inspector-inner-width), 100%); 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; - gap: var(--nodedc-space-3); + gap: var(--nodedc-inspector-control-gap); } .nodedc-control-row[data-layout="stack"] { @@ -1081,7 +1491,7 @@ textarea.nodedc-field__control { .nodedc-control-row__label { color: var(--nodedc-text-secondary); 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; } @@ -1129,6 +1539,10 @@ textarea.nodedc-field__control { padding: 0.875rem; } + .nodedc-window[data-placement="end"] .nodedc-window__body { + padding-inline: 0.75rem; + } + .nodedc-control-row { grid-template-columns: 1fr; align-items: stretch; diff --git a/packages/ui-dom/package.json b/packages/ui-dom/package.json index 2db549e..5dffc33 100644 --- a/packages/ui-dom/package.json +++ b/packages/ui-dom/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-dom", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "files": ["dist"], "main": "./dist/index.js", @@ -16,6 +16,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.1.0" + "@nodedc/ui-core": "0.2.0" } } diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 239049d..7e4f1ac 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-react", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "files": ["dist"], "main": "./dist/index.js", @@ -16,7 +16,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.1.0" + "@nodedc/ui-core": "0.2.0" }, "peerDependencies": { "react": ">=18", diff --git a/packages/ui-react/src/AdminNavigationPanel.tsx b/packages/ui-react/src/AdminNavigationPanel.tsx new file mode 100644 index 0000000..01a9705 --- /dev/null +++ b/packages/ui-react/src/AdminNavigationPanel.tsx @@ -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, "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 ( + + ); +} + diff --git a/packages/ui-react/src/AppHeader.tsx b/packages/ui-react/src/AppHeader.tsx index bf6e3c0..4f41f71 100644 --- a/packages/ui-react/src/AppHeader.tsx +++ b/packages/ui-react/src/AppHeader.tsx @@ -1,4 +1,4 @@ -import type { HTMLAttributes, ReactNode } from "react"; +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react"; import { cn } from "./cn"; export interface AppHeaderProps extends HTMLAttributes { @@ -41,6 +41,23 @@ export function HeaderProfile({ children, className, ...props }: HTMLAttributes< return
{children}
; } +export function HeaderProfileButton({ className, children, type = "button", ...props }: ButtonHTMLAttributes) { + return ; +} + +export interface HeaderAvatarProps extends HTMLAttributes { + label: string; + imageUrl?: string; +} + +export function HeaderAvatar({ label, imageUrl, className, ...props }: HeaderAvatarProps) { + return ( + + {imageUrl ? : label.slice(0, 2).toUpperCase()} + + ); +} + export interface HeaderWorkspaceProps extends HTMLAttributes { label: string; imageUrl?: string; @@ -53,4 +70,3 @@ export function HeaderWorkspace({ label, imageUrl, className, ...props }: Header ); } - diff --git a/packages/ui-react/src/Button.tsx b/packages/ui-react/src/Button.tsx index 0c78955..a9447f9 100644 --- a/packages/ui-react/src/Button.tsx +++ b/packages/ui-react/src/Button.tsx @@ -4,11 +4,13 @@ import { cn } from "./cn"; export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "accent"; export type ButtonSize = "default" | "compact"; +export type ButtonShape = "default" | "pill" | "rounded"; export interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant; size?: ButtonSize; width?: "auto" | "full"; + shape?: ButtonShape; accent?: RgbTuple; icon?: ReactNode; } @@ -17,6 +19,7 @@ export const Button = forwardRef(function Button variant = "secondary", size = "default", width = "auto", + shape = "default", accent, icon, className, @@ -34,6 +37,7 @@ export const Button = forwardRef(function Button data-variant={variant} data-size={size === "default" ? undefined : size} data-width={width === "auto" ? undefined : width} + data-shape={shape === "default" ? undefined : shape} style={accentStyle ? { ...accentStyle, ...style } : style} {...props} > diff --git a/packages/ui-react/src/Inspector.tsx b/packages/ui-react/src/Inspector.tsx index 080977e..1bc0c3e 100644 --- a/packages/ui-react/src/Inspector.tsx +++ b/packages/ui-react/src/Inspector.tsx @@ -8,6 +8,7 @@ export interface InspectorSectionSpec { group?: string; content: ReactNode; disabled?: boolean; + tone?: "default" | "accent"; } export interface InspectorProps { @@ -64,6 +65,7 @@ export function Inspector({ className="nodedc-inspector__section-trigger" data-open={isOpen ? "true" : undefined} data-active={activeId === section.id ? "true" : undefined} + data-tone={section.tone === "accent" ? "accent" : undefined} aria-expanded={isOpen} disabled={section.disabled} onClick={() => toggle(section.id)} @@ -98,4 +100,3 @@ export function ControlRow({ label, children, layout = "inline", className }: Co
); } - diff --git a/packages/ui-react/src/Select.tsx b/packages/ui-react/src/Select.tsx index 334bc80..b543b5b 100644 --- a/packages/ui-react/src/Select.tsx +++ b/packages/ui-react/src/Select.tsx @@ -11,6 +11,8 @@ export interface SelectOption { disabled?: boolean; } +export type SelectVariant = "integrated" | "split"; + export interface SelectProps { value: T; options: Array>; @@ -23,6 +25,7 @@ export interface SelectProps { minMenuWidth?: number; menuWidth?: number | "anchor"; disabled?: boolean; + variant?: SelectVariant; className?: string; triggerClassName?: string; menuClassName?: string; @@ -40,6 +43,7 @@ export function Select({ minMenuWidth = 180, menuWidth = "anchor", disabled = false, + variant = "integrated", className, triggerClassName, menuClassName, @@ -54,13 +58,13 @@ export function Select({ return ( { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") { @@ -68,6 +72,32 @@ export function Select({ if (!open) toggle(); } }; + if (variant === "split") { + return ( +
+
+
+ {selected?.icon ? {selected.icon} : null} + {selected?.label ?? "—"} +
+ +
+
+ ); + } + return (