diff --git a/CHANGELOG.md b/CHANGELOG.md index 029a200..9f46e8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.3.0 — launcher application template + +- Rebuilt the living catalog as a Launcher-style windowed application instead of a long documentation page. +- Added the fixed three-axis AppHeader, central media stage and reusable ApplicationShell/ApplicationPanel exports. +- Added Guideline navigation with separate expandable content windows and removed the product-specific bottom service rail. +- Added a responsive mobile panel/modal flow and verified it at a 390 × 844 viewport. +- Added the audited Icon component, 42 semantic glyphs, a living icon table and machine-readable icon registry. +- Reused the Launcher stage media as an optimized MP4 with a static mobile/poster fallback. + ## 0.2.0 — source-faithful geometry - Corrected the Hub header to the production three-axis shell with workspace, navigation and profile groups. diff --git a/README.md b/README.md index ed3dbca..48d3351 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ - модальные окна, подтверждения и боковые окна; - segmented navigation и статусы; - Hub header и левая admin navigation panel; +- launcher-style ApplicationShell без обязательной нижней витрины; +- аудированный общий набор иконок; - инспектор/панель настроек нового поколения; - поведение portal-слоёв, Escape, outside click и фокуса; - контракт темы: приложения меняют цветовую схему, а не геометрию компонентов. @@ -47,12 +49,14 @@ npm run build ## Статус -Версия `0.2.x` — уточнённый baseline с точной геометрией Hub header/admin panel и новых Engine settings/agent inspector. Исходные приложения на первом этапе не изменяются. +Версия `0.3.x` — готовый launcher-style шаблон приложения, точная геометрия Hub/Engine и центральный каталог иконок. Исходные приложения на первом этапе не изменяются. Подробности: - [Архитектура](docs/ARCHITECTURE.md) - [Компоненты](docs/COMPONENTS.md) +- [Шаблон приложения](docs/APPLICATION_TEMPLATE.md) +- [Иконки](docs/ICONS.md) - [Темы](docs/THEMING.md) - [Окна и слои](docs/WINDOWS_AND_LAYERS.md) - [Границы исходного аудита](docs/SOURCE_BASELINE.md) diff --git a/apps/catalog/package.json b/apps/catalog/package.json index 190f67a..54180d7 100644 --- a/apps/catalog/package.json +++ b/apps/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-catalog", - "version": "0.2.0", + "version": "0.3.0", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "typecheck": "tsc -b --pretty false" }, "dependencies": { - "@nodedc/ui-core": "0.2.0", - "@nodedc/ui-react": "0.2.0", + "@nodedc/ui-core": "0.3.0", + "@nodedc/ui-react": "0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/catalog/public/launcher-stage-poster.png b/apps/catalog/public/launcher-stage-poster.png new file mode 100644 index 0000000..31e3331 Binary files /dev/null and b/apps/catalog/public/launcher-stage-poster.png differ diff --git a/apps/catalog/public/launcher-stage.mp4 b/apps/catalog/public/launcher-stage.mp4 new file mode 100644 index 0000000..6ef3210 Binary files /dev/null and b/apps/catalog/public/launcher-stage.mp4 differ diff --git a/apps/catalog/public/nodedc-mark.svg b/apps/catalog/public/nodedc-mark.svg new file mode 100644 index 0000000..836bf0e --- /dev/null +++ b/apps/catalog/public/nodedc-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index 62e3d39..a7f1f61 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -3,6 +3,8 @@ import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-co import { AdminNavigationPanel, AppHeader, + ApplicationPanel, + ApplicationShell, Button, Checker, ColorField, @@ -11,24 +13,24 @@ import { Dropdown, FieldFrame, GlassSurface, + HeaderAvatar, HeaderProfile, HeaderProfileButton, - HeaderAvatar, HeaderWorkspace, + Icon, IconButton, Inspector, RangeControl, - SegmentedControl, Select, StatusBadge, TextAreaField, TextField, Window, WindowFooterActions, + type IconName, } from "@nodedc/ui-react"; -type CatalogSection = "foundation" | "controls" | "windows" | "inspector"; -type HubMode = "showcase" | "admin" | "platform"; +type CatalogSection = "foundation" | "controls" | "windows" | "modals" | "inspector" | "icons"; const engineAccentStyle = { "--nodedc-accent-rgb": "148 123 219", @@ -48,6 +50,123 @@ const selectOptions = [ { value: "disabled", label: "Отключён", description: "Запуск запрещён" }, ] as const; +const sectionDefinitions: Record = { + foundation: { + eyebrow: "01 / FOUNDATION", + title: "Основа", + description: "Тема, акцент, стекло и неизменяемая геометрия приложения.", + icon: "panel", + }, + controls: { + eyebrow: "02 / CONTROLS", + title: "Контролы", + description: "Кнопки, поля, selection, Engine-настройки и понятные статусы операций.", + icon: "sliders", + }, + windows: { + eyebrow: "03 / WINDOWS", + title: "Окна", + description: "Обычные окна и modeless-панели с единым portal- и focus-контрактом.", + icon: "apps", + }, + modals: { + eyebrow: "04 / MODALS", + title: "Модалки", + description: "Создание, подтверждение и разрушительные действия в адаптивном формате.", + icon: "clipboard", + }, + inspector: { + eyebrow: "05 / INSPECTOR", + title: "Инспектор", + description: "Точный новый Engine Environment Settings без legacy-инспекторов.", + icon: "settings", + }, + icons: { + eyebrow: "06 / ICONS", + title: "Иконки", + description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.", + icon: "grid", + }, +}; + +const iconGroups: Array<{ title: string; note: string; icons: IconName[] }> = [ + { + title: "Окно и слой", + note: "Launcher / Hub", + icons: ["close", "plus", "expand", "minimize", "refresh", "panel", "apps"], + }, + { + title: "Навигация", + note: "Launcher / Engine / BIM", + icons: ["chevron-left", "chevron-right", "chevron-down", "grid", "list", "sliders", "search"], + }, + { + title: "Редактирование", + note: "Launcher / SEO", + icons: ["save", "edit", "trash", "copy", "upload", "download", "external"], + }, + { + title: "Состояние и доступ", + note: "Вся платформа", + icons: ["check", "alert", "activity", "lock", "key", "shield", "circle"], + }, + { + title: "Сущности", + note: "Общий словарь", + icons: ["profile", "users", "building", "globe", "database", "network", "inbox", "mail"], + }, + { + title: "Контент", + note: "SEO / BIM / Engine", + icons: ["image", "video", "file", "folder", "clipboard", "settings"], + }, +]; + +const iconLabels: Record = { + activity: "Активность", + alert: "Предупреждение", + apps: "Приложения", + building: "Компания", + check: "Готово", + "chevron-down": "Раскрыть", + "chevron-left": "Назад", + "chevron-right": "Вперёд", + circle: "Маркер", + clipboard: "Список", + close: "Закрыть", + copy: "Копировать", + database: "База данных", + download: "Скачать", + edit: "Редактировать", + expand: "Развернуть", + external: "Открыть снаружи", + file: "Файл", + folder: "Папка", + globe: "Публичный контур", + grid: "Обзор", + image: "Изображение", + inbox: "Уведомления", + key: "Ключ", + list: "Список задач", + lock: "Закрыто", + mail: "Приглашение", + minimize: "Свернуть", + network: "Связи", + panel: "Панель", + plus: "Добавить", + profile: "Профиль", + refresh: "Обновить", + save: "Сохранить", + search: "Поиск", + settings: "Настройки", + shield: "Доступ", + sliders: "Параметры", + trash: "Удалить", + upload: "Загрузить", + users: "Участники", + video: "Видео", +}; + function rgbFromHex(hex: string): RgbTuple | null { const normalized = hex.trim().replace(/^#/, ""); if (!/^[0-9a-f]{6}$/i.test(normalized)) return null; @@ -58,25 +177,6 @@ function rgbFromHex(hex: string): RgbTuple | null { ]; } -function Section({ id, eyebrow, title, description, children }: { - id: string; - eyebrow: string; - title: string; - description: string; - children: ReactNode; -}) { - return ( -
-
- {eyebrow} -

{title}

-

{description}

-
- {children} -
- ); -} - function Preview({ title, note, children, className }: { title: string; note?: string; @@ -97,9 +197,9 @@ function Preview({ title, note, children, className }: { 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 [guidelineOpen, setGuidelineOpen] = useState(false); + const [activeSection, setActiveSection] = useState(null); + const [panelExpanded, setPanelExpanded] = useState(true); const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active"); const [checker, setChecker] = useState(true); const [brightness, setBrightness] = useState(49); @@ -110,8 +210,9 @@ export function CatalogApp() { const [connectionColor, setConnectionColor] = useState("#404040"); const [fillColor, setFillColor] = useState("#8f83ff"); const [strokeColor, setStrokeColor] = useState("#2b2b36"); - const [modalOpen, setModalOpen] = useState(false); + const [windowOpen, setWindowOpen] = useState(false); const [inspectorOpen, setInspectorOpen] = useState(false); + const [createModalOpen, setCreateModalOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [projectName, setProjectName] = useState("NODE.DC Platform"); const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения."); @@ -122,9 +223,16 @@ export function CatalogApp() { applyNodedcTheme(document.documentElement, { theme, accent }); }, [accent, theme]); - const openSection = (next: CatalogSection) => { - setSection(next); - document.getElementById(next)?.scrollIntoView({ behavior: "smooth", block: "start" }); + const openSection = (next: string) => { + const section = next as CatalogSection; + setGuidelineOpen(true); + setActiveSection(section); + setPanelExpanded(true); + }; + + const closeGuideline = () => { + setGuidelineOpen(false); + setActiveSection(null); }; const inspectorSections = [ @@ -161,9 +269,7 @@ export function CatalogApp() { - - - + ), }, @@ -182,102 +288,15 @@ export function CatalogApp() { }, ]; - return ( -
- } - 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-контракт.
Разные приложения.

-

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

-
- -
-
- -
+ const renderSectionContent = () => { + switch (activeSection) { + case "foundation": + return (
- +
- - + +
{accents.map((item) => ( @@ -295,34 +314,42 @@ export function CatalogApp() {
- +
Default surface Strong floating surface Soft nested surface
-
-
- -
-
- -
- - - - - + +
+ Header + Navigation + Content window + Stage
- +
+ ); + case "controls": + return ( +
+ +
+ + + + + +
+
+
setProjectName(event.target.value)} /> setNotes(event.target.value)} />
- +
- -
- + const activeDefinition = activeSection ? sectionDefinitions[activeSection] : null; - setInspectorOpen(false)} - > - - + return ( + <> + } + brandHref="/" + center={ + <> + + + + } + right={ + + + Профиль + + + } + /> + } + stage={ +
+ +
+
+ NODE.DC + DESIGN
GUIDELINE
+

Общий форм-фактор приложений, компоненты и оконная механика платформы.

+
+
+ } + navigation={ + ).map(([id, item]) => ({ + id, + label: item.title, + icon: , + }))} + activeId={activeSection ?? undefined} + onItemChange={openSection} + footer={ + <> + + Design System 0.3 + + } + /> + } + content={activeDefinition ? ( + setActiveSection(null)} + > +
{renderSectionContent()}
+
+ ) : null} + /> - Конфигурация будет удалена. Это действие нельзя отменить.} - confirmLabel="Удалить" - danger - onClose={() => setConfirmOpen(false)} - onConfirm={() => setConfirmOpen(false)} - /> -
+ setWindowOpen(false)} + footer={ + <> + + + + } + > +
+ setProjectName(event.target.value)} /> + +
+
+ + Конфигурация будет удалена. Это действие нельзя отменить.} + confirmLabel="Удалить" + danger + onClose={() => setConfirmOpen(false)} + onConfirm={() => setConfirmOpen(false)} + /> + ); } diff --git a/apps/catalog/src/styles.css b/apps/catalog/src/styles.css index febf6aa..76e6943 100644 --- a/apps/catalog/src/styles.css +++ b/apps/catalog/src/styles.css @@ -2,22 +2,20 @@ background: var(--nodedc-canvas); } -html { - scroll-behavior: smooth; +html, +body, +#root { + width: 100%; + min-width: 320px; + height: 100%; + min-height: 100%; + margin: 0; + overflow: hidden; } body { - min-width: 320px; - min-height: 100vh; - margin: 0; - background: - radial-gradient(circle at 72% 10%, rgba(var(--nodedc-accent-rgb), 0.09), transparent 28rem), - linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px), - linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px), - var(--nodedc-canvas); - background-size: auto, 44px 44px, 44px 44px, auto; + background: #050506; color: var(--nodedc-text-primary); - transition: background-color 220ms ease, color 220ms ease; } button, @@ -30,96 +28,100 @@ textarea { min-height: 100vh; } -.catalog-admin-layer { - position: fixed; - z-index: 390; - top: 6.55rem; - bottom: 1.25rem; - left: 1.25rem; +.catalog-header-nav { + display: inline-flex; + min-height: var(--nodedc-header-pill-height); + align-items: center; + border-radius: var(--nodedc-radius-circle); + background: var(--nodedc-glass-panel-bg-soft); + padding: 0.32rem; + box-shadow: var(--nodedc-glass-control-shadow); } -[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img { +.catalog-header-nav .nodedc-header__nav-item { + min-width: 8.5rem; +} + +.catalog-header-workspace img { + width: 1.75rem; + height: 1.75rem; + object-fit: contain; +} + +[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img, +[data-nodedc-theme="light"] .catalog-app .nodedc-header__workspace img { filter: brightness(0.12); } -.catalog-main { +.catalog-launcher-stage { + position: relative; + isolation: isolate; + overflow: hidden; + border-radius: var(--nodedc-radius-card); + background: #0b0b0d url("/launcher-stage-poster.png") center / cover no-repeat; + box-shadow: 0 44px 150px rgba(0, 0, 0, 0.62); +} + +.catalog-launcher-stage__media, +.catalog-launcher-stage__shade { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.catalog-launcher-stage__media { + object-fit: cover; + opacity: 0.92; +} + +.catalog-launcher-stage__shade { + z-index: 1; + background: + radial-gradient(circle at 52% 48%, transparent 0 38%, rgba(0, 0, 0, 0.34) 100%), + linear-gradient(90deg, rgba(0, 0, 0, 0.5), transparent 38% 72%, rgba(0, 0, 0, 0.2)); +} + +.catalog-launcher-stage__title { + position: absolute; + z-index: 2; + top: clamp(2.8rem, 9vh, 5.75rem); + left: clamp(2.8rem, 6vw, 5.75rem); display: grid; - width: min(1180px, calc(100% - 2rem)); - 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); + max-width: 28rem; + gap: 0.24rem; + pointer-events: none; } -.catalog-main[data-admin-open="true"] { - width: min(1180px, calc(100% - 25rem)); - margin-right: 1rem; - margin-left: 24rem; +.catalog-launcher-stage__title span, +.catalog-launcher-stage__title strong { + font-size: clamp(2rem, 3vw, 3.25rem); + font-weight: 350; + letter-spacing: -0.035em; + line-height: 0.98; } -.catalog-hero { +.catalog-launcher-stage__title span { + color: rgba(38, 38, 42, 0.76); +} + +.catalog-launcher-stage__title strong { + color: rgba(255, 255, 255, 0.96); +} + +.catalog-launcher-stage__title p { + max-width: 22rem; + margin: 0.9rem 0 0; + color: rgba(255, 255, 255, 0.68); + font-size: 0.86rem; + line-height: 1.45; +} + +.catalog-panel-content { display: grid; - min-height: 32rem; - align-content: center; - justify-items: start; - gap: 1.4rem; -} - -.catalog-hero h1 { - max-width: 820px; - margin: 0; - color: var(--nodedc-text-primary); - font-size: clamp(3.1rem, 7vw, 6.4rem); - font-weight: 670; - letter-spacing: -0.065em; - line-height: 0.92; -} - -.catalog-hero p { - max-width: 650px; - margin: 0; - color: var(--nodedc-text-secondary); - font-size: clamp(1rem, 1.6vw, 1.3rem); - line-height: 1.5; -} - -.catalog-section-nav { - margin-top: 0.6rem; -} - -.catalog-section { - display: grid; - scroll-margin-top: 2rem; - gap: 2rem; -} - -.catalog-section__head { - display: grid; - max-width: 760px; - gap: 0.75rem; -} - -.catalog-section__head span { - color: rgb(var(--nodedc-accent-rgb)); - font-size: 0.72rem; - font-weight: 780; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.catalog-section__head h2 { - margin: 0; - font-size: clamp(2rem, 4vw, 3.6rem); - font-weight: 650; - letter-spacing: -0.05em; - line-height: 1; -} - -.catalog-section__head p { - margin: 0; - color: var(--nodedc-text-secondary); - font-size: 1rem; - line-height: 1.5; + align-content: start; + gap: 1rem; + padding: 0.15rem 0.1rem 1rem; } .catalog-grid { @@ -132,8 +134,12 @@ textarea { grid-template-columns: 1.1fr 0.9fr; } +.catalog-grid--single { + grid-template-columns: 1fr; +} + .catalog-preview { - min-height: 18rem; + min-height: 16rem; } .catalog-preview--wide { @@ -145,7 +151,7 @@ textarea { align-items: baseline; justify-content: space-between; gap: 1rem; - margin-bottom: 2rem; + margin-bottom: 1.5rem; } .catalog-preview__head strong { @@ -155,24 +161,35 @@ textarea { .catalog-preview__head span { color: var(--nodedc-text-muted); font-size: 0.68rem; + text-align: right; text-transform: uppercase; } .catalog-preview__body { display: grid; - min-height: 10rem; + min-height: 8rem; align-content: center; gap: 1rem; } +.catalog-preview__explanation { + max-width: 42rem; + margin: 0; + color: var(--nodedc-text-muted); + font-size: 0.78rem; + line-height: 1.5; +} + .catalog-theme-row, -.catalog-inline { +.catalog-inline, +.catalog-window-actions-demo { display: flex; align-items: center; gap: 0.6rem; } -.catalog-inline--wrap { +.catalog-inline--wrap, +.catalog-window-actions-demo { flex-wrap: wrap; } @@ -212,6 +229,14 @@ textarea { gap: 0.75rem; } +.catalog-surface-stack .nodedc-glass { + display: flex; + min-height: 4rem; + align-items: center; + color: var(--nodedc-text-secondary); + font-size: 0.78rem; +} + .catalog-engine-control-stack { display: grid; width: min(var(--nodedc-inspector-inner-width), 100%); @@ -219,24 +244,49 @@ textarea { gap: 0.75rem; } -.catalog-surface-stack .nodedc-glass { - min-height: 4rem; - display: flex; - align-items: center; - color: var(--nodedc-text-secondary); - font-size: 0.78rem; +.catalog-shell-diagram { + display: grid; + min-height: 16rem; + grid-template-columns: minmax(8rem, 0.28fr) minmax(15rem, 1fr) minmax(7rem, 0.22fr); + grid-template-rows: 3.2rem 1fr; + gap: 0.6rem; + border-radius: 1.1rem; + background: rgba(0, 0, 0, 0.22); + padding: 0.6rem; +} + +.catalog-shell-diagram span { + display: grid; + place-items: center; + border-radius: 0.9rem; + background: var(--nodedc-glass-control-bg); + color: var(--nodedc-text-muted); + font-size: 0.72rem; + font-weight: 700; +} + +.catalog-shell-diagram__header { + grid-column: 1 / -1; +} + +.catalog-shell-diagram__nav { + background: color-mix(in srgb, var(--nodedc-glass-control-hover) 90%, transparent) !important; +} + +.catalog-shell-diagram__content { + background: color-mix(in srgb, var(--nodedc-glass-control-hover) 70%, transparent) !important; } .catalog-inspector-stage { display: flex; - min-height: 40rem; + min-height: 43rem; justify-content: flex-end; align-items: flex-start; - padding: 4rem; - border-radius: 2rem; + border-radius: var(--nodedc-radius-card); background: radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem), color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent); + padding: 2rem; } .catalog-inspector-card { @@ -255,17 +305,89 @@ textarea { font-size: 0.7rem; } -@media (max-width: 820px) { - .catalog-main { - gap: 6rem; - padding-top: 2rem; - } +.catalog-icon-catalog { + display: grid; + gap: 1rem; +} - .catalog-main[data-admin-open="true"] { - width: min(1180px, calc(100% - 2rem)); - margin-inline: auto; - } +.catalog-icon-group { + display: grid; + gap: 0.75rem; + border-radius: var(--nodedc-radius-card); + background: var(--nodedc-glass-panel-surface); + padding: 1rem; +} +.catalog-icon-group > header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + padding-inline: 0.2rem; +} + +.catalog-icon-group > header strong { + font-size: 0.88rem; +} + +.catalog-icon-group > header span { + color: var(--nodedc-text-muted); + font-size: 0.68rem; + text-transform: uppercase; +} + +.catalog-icon-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.55rem; +} + +.catalog-icon-tile { + display: grid; + min-width: 0; + grid-template-columns: 2.92rem minmax(0, 1fr); + align-items: center; + gap: 0.7rem; + border-radius: var(--nodedc-radius-control); + background: var(--nodedc-glass-control-bg); + padding: 0.35rem 0.55rem 0.35rem 0.35rem; +} + +.catalog-icon-tile__surface { + display: grid; + width: 2.92rem; + height: 2.92rem; + place-items: center; + border-radius: var(--nodedc-radius-circle); + background: var(--nodedc-glass-control-hover); + color: var(--nodedc-text-primary); +} + +.catalog-icon-tile > span:last-child { + display: grid; + min-width: 0; + gap: 0.18rem; +} + +.catalog-icon-tile strong, +.catalog-icon-tile code { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.catalog-icon-tile strong { + font-size: 0.72rem; +} + +.catalog-icon-tile code { + color: var(--nodedc-text-muted); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.62rem; +} + +@media (max-width: 1280px) { .catalog-grid, .catalog-grid--foundation { grid-template-columns: 1fr; @@ -275,14 +397,92 @@ textarea { grid-column: auto; } - .catalog-inspector-stage { - padding: 1rem; - } - - .catalog-admin-layer { - top: 0.875rem; - right: 0.875rem; - bottom: 0.875rem; - left: 0.875rem; + .catalog-icon-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 900px) { + .catalog-icon-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .catalog-launcher-stage__media { + display: none; + } + + .catalog-header-nav { + width: 100%; + } + + .catalog-header-nav .nodedc-header__nav-item { + width: 100%; + } + + .catalog-app .nodedc-header__profile-button { + display: none; + } + + .catalog-launcher-stage__title { + top: 2rem; + left: 2rem; + max-width: calc(100% - 4rem); + } + + .catalog-launcher-stage__title span, + .catalog-launcher-stage__title strong { + font-size: clamp(1.8rem, 9vw, 3rem); + } + + .catalog-launcher-stage__title p { + display: none; + } + + .catalog-preview { + min-height: auto; + } + + .catalog-preview__head, + .catalog-icon-group > header { + align-items: flex-start; + flex-direction: column; + gap: 0.25rem; + } + + .catalog-swatches, + .catalog-icon-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .catalog-shell-diagram { + grid-template-columns: 1fr 1.5fr; + grid-template-rows: 3rem 7rem 7rem; + } + + .catalog-shell-diagram__header { + grid-column: 1 / -1; + } + + .catalog-shell-diagram__stage { + grid-column: 1 / -1; + } + + .catalog-inspector-stage { + min-height: 40rem; + justify-content: center; + padding: 0.75rem; + } + + .catalog-window-actions-demo .nodedc-button { + width: 100%; + } +} + +@media (max-width: 430px) { + .catalog-swatches, + .catalog-icon-grid { + grid-template-columns: 1fr; } } diff --git a/docs/ADOPTION.md b/docs/ADOPTION.md index 5bfbce0..d7be58f 100644 --- a/docs/ADOPTION.md +++ b/docs/ADOPTION.md @@ -19,6 +19,8 @@ - Dropdown/Select; - Window/Confirmation; - AppHeader. +- ApplicationShell/ApplicationPanel; +- Icon/IconButton. Локальные реализации удаляются только после функционального и визуального сравнения. @@ -40,14 +42,17 @@ ## Новые приложения -Шаблон приложения пока не входит в scope. До его появления новый проект должен: +Новый React-проект начинает с `ApplicationShell`: - установить опубликованные NODE.DC UI packages; - выбрать theme и accent; -- использовать AppHeader и Window/Dropdown primitives; +- передать фиксированный AppHeader, stage, navigation и content; +- использовать Window/Dropdown и канонический Icon вместо локальных реализаций; - проверять registry перед созданием локального control; - хранить доменные компоненты у себя. +Нижняя service rail добавляется только продуктам с витриной сервисов. В обычном оконном приложении она отсутствует. + ## Adoption matrix | Приложение | Текущий источник | Целевой adapter | Первый набор | @@ -66,4 +71,3 @@ - не поддерживать отдельную тему путём fork компонента; - не использовать legacy Engine inspector как промежуточный канон; - не удалять production local component до проверки package replacement. - diff --git a/docs/APPLICATION_TEMPLATE.md b/docs/APPLICATION_TEMPLATE.md new file mode 100644 index 0000000..2f7488e --- /dev/null +++ b/docs/APPLICATION_TEMPLATE.md @@ -0,0 +1,40 @@ +# Шаблон приложения + +`ApplicationShell` — канонический форм-фактор нового приложения NODE.DC. Он повторяет устойчивую механику Launcher/Hub, но не включает продуктовую витрину и нижнюю service rail. + +## Состав + +1. `AppHeader` — фиксированная трёхосевая шапка `logo / workspace + navigation / profile`. +2. `stage` — главное медиа- или предметное окно приложения. +3. `navigation` — левая панель шириной `clamp(332px, 19.5vw, 352px)`. +4. `content` — отдельное рабочее окно справа от навигации. +5. `ApplicationPanel` — заголовок, действия expand/close и независимо прокручиваемое тело контента. + +Позиции шапки не пересчитываются под конкретное приложение. Меняются подписи, доступные navigation actions, профиль и содержимое stage. + +## Состояния desktop + +| Состояние | Навигация | Content window | Stage | +| --- | --- | --- | --- | +| Главная | закрыта | закрыто | занимает рабочую область | +| Guideline открыт | слева | закрыто | сдвинут вправо | +| Оконный режим | слева | средняя колонка | остаётся видимым справа | +| Развёрнутый режим | слева | до правого края | уезжает за viewport | + +Между панелями используется page gap `20px`. На Guideline-главной нижней rail нет: она является продуктовым элементом витрины Launcher, а не частью общего shell. + +## Узкий viewport + +До `760px` шапка перестраивается в две строки. Navigation и content занимают доступную область под шапкой последовательно: открытый content скрывает navigation, но не уничтожает его состояние. Кнопка expand скрывается, потому что content уже использует всю доступную ширину. + +Modal остаётся portal-слоем. Его тело прокручивается независимо, footer остаётся внутри viewport, а поля складываются в одну колонку. + +## Что принадлежит приложению + +- route/state выбранного раздела; +- данные профиля; +- медиа и содержание stage; +- пункты navigation; +- предметное содержимое content window. + +Shell владеет геометрией, слоями, переходами и breakpoint-поведением. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 38434cb..8bffa7a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,6 +27,8 @@ Core не зависит от React. `@nodedc/ui-react` предоставляет компоненты для Launcher/Hub, Engine, Ops и React-модулей. Компоненты контролируемые: бизнес-состояние остаётся у приложения, а визуальное и базовое интерактивное поведение принадлежит библиотеке. +`ApplicationShell` и `ApplicationPanel` образуют готовый launcher-style шаблон нового React-приложения. `Icon` предоставляет ограниченный аудированный словарь glyph, чтобы приложения не импортировали vendor-наборы независимо. + ### DOM `@nodedc/ui-dom` предоставляет контроллеры для CMS, BIM Viewer и других приложений без React. DOM-слой не создаёт вторую стилизацию: он использует те же классы и токены `@nodedc/ui-core`. @@ -44,6 +46,8 @@ Core не зависит от React. - floating layers; - окна; - навигационная геометрия; +- application shell и фиксированная шапка; +- канонический набор иконок; - inspector/settings layout; - состояния загрузки, пустоты и ошибки. @@ -76,4 +80,3 @@ Core не зависит от React. - major — удаление/переименование экспорта или несовместимая геометрия/семантика. Приложение должно зависеть от опубликованной версии пакета, а не от копии файлов или произвольного commit URL. - diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 9cf9a66..6478cdb 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -95,11 +95,32 @@ Pill navigation для верхней панели и компактного п В Hub-форме центр состоит из круглого workspace trigger и segmented navigation. Справа используется единая profile-group поверхность с иконкой, подписью и круглым avatar. Эти элементы нельзя заменять случайными standalone-кнопками, сохраняя только приблизительную позицию. +В каноническом `ApplicationShell` шапка фиксирована. Её три оси не двигаются при открытии navigation/content и не зависят от ширины предметного контента. + +## ApplicationShell и ApplicationPanel + +Общий шаблон приложения по механике Launcher/Hub: фиксированный `AppHeader`, центральный stage, левая navigation panel и отдельное правое content window. + +- на главной виден только stage; +- открытая navigation сдвигает stage; +- content открывается справа от navigation; +- expand растягивает content до правого края и уводит stage за viewport; +- на мобильном navigation и content используют всю область под шапкой последовательно; +- нижняя service rail не входит в шаблон — это элемент витрины Launcher. + +Точные состояния и размеры зафиксированы в `docs/APPLICATION_TEMPLATE.md`. + ## 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. +На desktop открытая панель занимает отдельную колонку с Launcher page gap `20 px` до content/stage, а не перекрывает их затемнённым overlay. + +## Icon + +`Icon` предоставляет только подтверждённый общий subset иконок. Каноническое имя описывает смысл (`close`, `expand`, `refresh`), а не конкретный путь SVG. Базовые размеры — `16`, `18` и `20 px`, stroke — `1.8`. + +Поверхность, круглая форма, hit target, active и disabled состояния принадлежат `IconButton`, `Button` или navigation item. Полный список находится в `registry/icons.json` и `docs/ICONS.md`. ## StatusBadge diff --git a/docs/ICONS.md b/docs/ICONS.md new file mode 100644 index 0000000..f7971cc --- /dev/null +++ b/docs/ICONS.md @@ -0,0 +1,30 @@ +# Иконки + +Канонический набор собран по реально используемым действиям Launcher/Hub, SEO, BIM Viewer и разрешённым новым участкам Engine. Полные vendor-наборы не являются частью дизайн-системы: наличие SVG в Font Awesome или export в Lucide не делает иконку канонической. + +## Геометрия + +| Контекст | Размер glyph | Hit target | +| --- | ---: | ---: | +| Плотная строка/label | `16px` | задаёт строка | +| Обычная кнопка | `18px` | `46.72px` круг или кнопка с текстом | +| Крупное оконное действие | `20px` | `46.72px` круг | + +Базовый stroke — `1.8`. Иконка наследует `currentColor`. Фон и active/disabled/hover состояния принадлежат `IconButton`, `Button`, navigation item или другому компоненту поверхности. + +## Группы + +| Группа | Канонические имена | +| --- | --- | +| Окно и слой | `close`, `plus`, `expand`, `minimize`, `refresh`, `panel`, `apps` | +| Навигация | `chevron-left`, `chevron-right`, `chevron-down`, `grid`, `list`, `sliders`, `search` | +| Редактирование | `save`, `edit`, `trash`, `copy`, `upload`, `download`, `external` | +| Состояние и доступ | `check`, `alert`, `activity`, `lock`, `key`, `shield`, `circle` | +| Сущности | `profile`, `users`, `building`, `globe`, `database`, `network`, `inbox`, `mail` | +| Контент | `image`, `video`, `file`, `folder`, `clipboard`, `settings` | + +Живая таблица с названиями, поверхностями и размерами находится в разделе `Guideline → Иконки`. Машинный список — в `registry/icons.json`. + +## Правило расширения + +Новая иконка добавляется только после подтверждённого применения в продукте. Нужно выбрать семантическое имя, добавить export в `Icon`, запись в registry и specimen в каталоге. Локальный импорт иконки только ради немного другой формы запрещён. diff --git a/docs/SOURCE_BASELINE.md b/docs/SOURCE_BASELINE.md index 61a3fda..b267e5c 100644 --- a/docs/SOURCE_BASELINE.md +++ b/docs/SOURCE_BASELINE.md @@ -15,6 +15,8 @@ Для точной геометрии используются `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`. +`src/app/LauncherApp.tsx`, `src/widgets/service-stage/ServiceStage.tsx` и те же global styles подтверждают общий application shell: stage сдвигается при открытии navigation/content, а content разворачивается независимо. Нижняя ServiceRail исключена из общего шаблона как продуктовая часть витрины. + Существовавший `dc-ui-guideline` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория. ## SEO и CMS @@ -48,6 +50,10 @@ CMS подтверждает необходимость DOM-пакета без Используется как ограничение архитектуры: общий UI должен работать без React. В baseline включены общие требования к toolbar, settings menu и glass select, но не BIM-domain controls. +## Аудит иконок + +Launcher/Hub и SEO подтверждают Lucide как основной outline-язык; BIM Viewer использует Font Awesome в legacy/vendor-слое; Engine содержит локальные inline SVG. В библиотеку перенесён только пересекающийся семантический subset действий и сущностей. Полные vendor-каталоги и legacy Engine glyph не являются каноном. + ## Почему код не копируется целиком Исторические реализации содержат: diff --git a/package-lock.json b/package-lock.json index bad4583..f62c473 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodedc-design-guideline", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodedc-design-guideline", - "version": "0.2.0", + "version": "0.3.0", "workspaces": [ "packages/*", "apps/*" @@ -25,10 +25,10 @@ }, "apps/catalog": { "name": "@nodedc/ui-catalog", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { - "@nodedc/ui-core": "0.2.0", - "@nodedc/ui-react": "0.2.0", + "@nodedc/ui-core": "0.3.0", + "@nodedc/ui-react": "0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -1539,6 +1539,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1879,27 +1888,28 @@ }, "packages/tokens": { "name": "@nodedc/tokens", - "version": "0.2.0" + "version": "0.3.0" }, "packages/ui-core": { "name": "@nodedc/ui-core", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { - "@nodedc/tokens": "0.2.0" + "@nodedc/tokens": "0.3.0" } }, "packages/ui-dom": { "name": "@nodedc/ui-dom", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { - "@nodedc/ui-core": "0.2.0" + "@nodedc/ui-core": "0.3.0" } }, "packages/ui-react": { "name": "@nodedc/ui-react", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { - "@nodedc/ui-core": "0.2.0" + "@nodedc/ui-core": "0.3.0", + "lucide-react": "^0.468.0" }, "devDependencies": { "@types/react": "^19.1.0", diff --git a/package.json b/package.json index 45f75d5..c7152dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodedc-design-guideline", - "version": "0.2.0", + "version": "0.3.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 f9ccfe0..7e2cb2a 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/tokens", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "files": ["tokens.json", "tokens.css", "themes.css"], "exports": { diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index f57e313..f8ead0e 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-core", - "version": "0.2.0", + "version": "0.3.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.2.0" + "@nodedc/tokens": "0.3.0" } } diff --git a/packages/ui-core/styles.css b/packages/ui-core/styles.css index 66519a2..3eae4f6 100644 --- a/packages/ui-core/styles.css +++ b/packages/ui-core/styles.css @@ -532,6 +532,14 @@ textarea.nodedc-field__control { padding: 1rem var(--nodedc-page-padding) 0.75rem; } +.nodedc-header-shell[data-fixed="true"] { + position: fixed; + top: 0; + left: 50%; + margin-inline: 0; + transform: translateX(-50%); +} + .nodedc-header { display: grid; min-height: 4.25rem; @@ -603,6 +611,7 @@ textarea.nodedc-field__control { min-height: calc(var(--nodedc-header-pill-height) - 0.64rem); align-items: center; justify-content: center; + gap: 0.45rem; border: 0; border-radius: var(--nodedc-radius-circle); outline: 0; @@ -691,6 +700,189 @@ textarea.nodedc-field__control { color: var(--nodedc-glass-control-active-text); } +.nodedc-app-shell { + --nodedc-app-header-height: 6rem; + --nodedc-app-page-pad: 1.25rem; + --nodedc-app-panel-gap: 1.25rem; + --nodedc-app-nav-width: clamp(20.75rem, 19.5vw, 22rem); + --nodedc-app-content-width: calc( + 100vw - (var(--nodedc-app-page-pad) * 2) - (var(--nodedc-app-panel-gap) * 2) - (var(--nodedc-app-nav-width) * 2) + ); + min-height: 100vh; + overflow: hidden; + background: var(--nodedc-canvas); +} + +.nodedc-app-shell__main { + position: relative; + height: calc(100vh - var(--nodedc-app-header-height)); + min-height: 0; + margin-top: var(--nodedc-app-header-height); + overflow: hidden; +} + +.nodedc-app-shell__stage { + position: absolute; + inset: 0; + overflow: hidden; + padding: 0 var(--nodedc-app-page-pad) var(--nodedc-app-page-pad); + transition: padding 440ms cubic-bezier(0.22, 1, 0.36, 1), transform 440ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.nodedc-app-shell__stage > * { + width: 100%; + height: 100%; +} + +.nodedc-app-shell__navigation, +.nodedc-app-shell__content { + position: absolute; + z-index: var(--nodedc-layer-panel); + top: 0; + bottom: var(--nodedc-app-page-pad); + min-height: 0; +} + +.nodedc-app-shell__navigation { + left: var(--nodedc-app-page-pad); + width: var(--nodedc-app-nav-width); +} + +.nodedc-app-shell__content { + left: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap)); + width: max(32rem, var(--nodedc-app-content-width)); + transition: width 500ms cubic-bezier(0.22, 1, 0.36, 1), right 500ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.nodedc-app-shell[data-navigation-open="true"] .nodedc-app-shell__stage { + padding-left: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap)); +} + +.nodedc-app-shell[data-content-open="true"]:not([data-content-expanded="true"]) .nodedc-app-shell__stage { + padding-left: calc( + var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap) + + max(32rem, var(--nodedc-app-content-width)) + var(--nodedc-app-panel-gap) + ); +} + +.nodedc-app-shell[data-content-open="true"][data-content-expanded="true"] .nodedc-app-shell__content { + right: var(--nodedc-app-page-pad); + width: auto; +} + +.nodedc-app-shell[data-content-open="true"][data-content-expanded="true"] .nodedc-app-shell__stage { + pointer-events: none; + transform: translateX(calc(100vw + var(--nodedc-app-page-pad))); +} + +.nodedc-application-panel { + display: grid; + width: 100%; + height: 100%; + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); + gap: 0.75rem; + 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: 1rem; + box-shadow: 0 34px 110px rgba(0, 0, 0, 0.42); + backdrop-filter: blur(28px); + -webkit-backdrop-filter: blur(28px); + animation: nodedc-application-panel-in 460ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes nodedc-application-panel-in { + from { opacity: 0; transform: translateX(-1.1rem); } + to { opacity: 1; transform: translateX(0); } +} + +.nodedc-application-panel__head { + display: flex; + min-height: 3.55rem; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.nodedc-application-panel__titles { + display: grid; + min-width: 0; + gap: 0.24rem; + padding: 0.2rem 0.25rem; +} + +.nodedc-application-panel__titles span { + color: var(--nodedc-text-muted); + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.nodedc-application-panel__titles h1, +.nodedc-application-panel__titles p { + margin: 0; +} + +.nodedc-application-panel__titles h1 { + font-size: 1.15rem; + font-weight: 780; + line-height: 1.15; +} + +.nodedc-application-panel__titles p { + max-width: 42rem; + color: var(--nodedc-text-muted); + font-size: 0.75rem; + line-height: 1.35; +} + +.nodedc-application-panel__actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 0.45rem; +} + +.nodedc-application-panel__action { + 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-application-panel__action:hover, +.nodedc-application-panel__action:focus-visible { + border-color: rgba(255, 255, 255, 0.3); + background: var(--nodedc-glass-control-hover); + color: var(--nodedc-text-primary); +} + +.nodedc-application-panel[data-expanded="true"] .nodedc-application-panel__action:first-child { + border-color: transparent; + background: var(--nodedc-glass-control-active); + color: var(--nodedc-glass-control-active-text); +} + +.nodedc-application-panel__body { + min-height: 0; + overflow: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.15) transparent; +} + .nodedc-admin-panel { position: relative; display: grid; @@ -713,6 +905,10 @@ textarea.nodedc-field__control { animation: nodedc-admin-panel-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both; } +.nodedc-admin-panel:not([data-has-contexts="true"]) { + grid-template-rows: auto minmax(0, 1fr) auto; +} + @keyframes nodedc-admin-panel-in { from { opacity: 0; transform: translateX(-1.6rem); } to { opacity: 1; transform: translateX(0); } @@ -915,6 +1111,7 @@ textarea.nodedc-field__control { } .nodedc-admin-panel__nav-item { + outline: 0; cursor: pointer; transition: background 160ms ease, color 160ms ease, opacity 160ms ease, transform 160ms ease; } @@ -933,6 +1130,15 @@ textarea.nodedc-field__control { 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-item:focus-visible { + outline: 0; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 10px 22px rgba(0, 0, 0, 0.12); +} + +.nodedc-app-shell__navigation > .nodedc-admin-panel { + width: 100%; +} + .nodedc-admin-panel__nav-icon { position: absolute; top: 50%; @@ -1514,9 +1720,30 @@ textarea.nodedc-field__control { font-size: var(--nodedc-font-size-lg); } +@media (max-width: 1100px) { + .nodedc-app-shell[data-content-open="true"] .nodedc-app-shell__content { + right: var(--nodedc-app-page-pad); + width: auto; + } + + .nodedc-app-shell[data-content-open="true"] .nodedc-app-shell__stage { + pointer-events: none; + transform: translateX(calc(100vw + var(--nodedc-app-page-pad))); + } +} + @media (max-width: 760px) { + .nodedc-header-shell { + width: 100%; + padding: 0.85rem 1rem 0.65rem; + background: var(--nodedc-canvas); + } + .nodedc-header { + min-height: 8.6rem; grid-template-columns: minmax(0, 1fr) auto; + align-content: start; + row-gap: 0.65rem; } .nodedc-header__center { @@ -1530,6 +1757,54 @@ textarea.nodedc-field__control { min-width: max-content; } + .nodedc-app-shell { + --nodedc-app-header-height: 10.25rem; + --nodedc-app-page-pad: 0.7rem; + --nodedc-app-panel-gap: 0.7rem; + } + + .nodedc-app-shell__stage { + padding: 0 var(--nodedc-app-page-pad) var(--nodedc-app-page-pad); + } + + .nodedc-app-shell__navigation, + .nodedc-app-shell__content { + right: var(--nodedc-app-page-pad); + bottom: var(--nodedc-app-page-pad); + left: var(--nodedc-app-page-pad); + width: auto; + } + + .nodedc-app-shell[data-navigation-open="true"] .nodedc-app-shell__stage, + .nodedc-app-shell[data-content-open="true"] .nodedc-app-shell__stage { + padding-left: var(--nodedc-app-page-pad); + pointer-events: none; + transform: translateX(calc(100vw + var(--nodedc-app-page-pad))); + } + + .nodedc-app-shell[data-content-open="true"] .nodedc-app-shell__navigation { + opacity: 0; + pointer-events: none; + } + + .nodedc-application-panel, + .nodedc-admin-panel { + width: 100%; + max-width: none; + } + + .nodedc-application-panel { + padding: 0.75rem; + } + + .nodedc-application-panel__titles p { + display: none; + } + + .nodedc-application-panel__action:first-child { + display: none; + } + .nodedc-window[data-placement="end"] { width: calc(100vw - 1.75rem); max-height: calc(100vh - 1.75rem); @@ -1554,6 +1829,7 @@ textarea.nodedc-field__control { .nodedc-dropdown-surface, .nodedc-overlay, .nodedc-window, + .nodedc-application-panel, .nodedc-inspector__section-content { animation: none; } diff --git a/packages/ui-dom/package.json b/packages/ui-dom/package.json index 5dffc33..18e7731 100644 --- a/packages/ui-dom/package.json +++ b/packages/ui-dom/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-dom", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "files": ["dist"], "main": "./dist/index.js", @@ -16,6 +16,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.2.0" + "@nodedc/ui-core": "0.3.0" } } diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 7e4f1ac..2c7480a 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -1,6 +1,6 @@ { "name": "@nodedc/ui-react", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "files": ["dist"], "main": "./dist/index.js", @@ -16,7 +16,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@nodedc/ui-core": "0.2.0" + "@nodedc/ui-core": "0.3.0", + "lucide-react": "^0.468.0" }, "peerDependencies": { "react": ">=18", diff --git a/packages/ui-react/src/AdminNavigationPanel.tsx b/packages/ui-react/src/AdminNavigationPanel.tsx index 01a9705..669a7e2 100644 --- a/packages/ui-react/src/AdminNavigationPanel.tsx +++ b/packages/ui-react/src/AdminNavigationPanel.tsx @@ -26,6 +26,7 @@ export interface AdminNavigationPanelProps extends Omit void; onItemChange: (id: string) => void; } @@ -38,13 +39,14 @@ export function AdminNavigationPanel({ activeId, footer, closeLabel = "Закрыть администрирование", + navigationLabel = "Разделы администрирования", onClose, onItemChange, className, ...props }: AdminNavigationPanelProps) { return ( -
: null} -
+ +
{children}
+ + ); +} diff --git a/packages/ui-react/src/Icon.tsx b/packages/ui-react/src/Icon.tsx new file mode 100644 index 0000000..c0f85f4 --- /dev/null +++ b/packages/ui-react/src/Icon.tsx @@ -0,0 +1,114 @@ +import { + Activity, + AlertTriangle, + Boxes, + Building2, + Check, + ChevronDown, + ChevronLeft, + ChevronRight, + Circle, + ClipboardList, + Copy, + Database, + Download, + ExternalLink, + FileText, + FolderOpen, + Globe2, + Image, + Inbox, + KeyRound, + LayoutDashboard, + ListChecks, + LockKeyhole, + MailPlus, + Maximize2, + Minimize2, + Network, + PanelTop, + Pencil, + Plus, + RefreshCw, + Save, + Search, + Settings, + ShieldCheck, + SlidersHorizontal, + Trash2, + UploadCloud, + UserCircle, + UsersRound, + Video, + X, + type LucideIcon, + type LucideProps, +} from "lucide-react"; + +const icons = { + activity: Activity, + alert: AlertTriangle, + apps: Boxes, + building: Building2, + check: Check, + "chevron-down": ChevronDown, + "chevron-left": ChevronLeft, + "chevron-right": ChevronRight, + circle: Circle, + clipboard: ClipboardList, + close: X, + copy: Copy, + database: Database, + download: Download, + edit: Pencil, + expand: Maximize2, + external: ExternalLink, + file: FileText, + folder: FolderOpen, + globe: Globe2, + grid: LayoutDashboard, + image: Image, + inbox: Inbox, + key: KeyRound, + list: ListChecks, + lock: LockKeyhole, + mail: MailPlus, + minimize: Minimize2, + network: Network, + panel: PanelTop, + plus: Plus, + profile: UserCircle, + refresh: RefreshCw, + save: Save, + search: Search, + settings: Settings, + shield: ShieldCheck, + sliders: SlidersHorizontal, + trash: Trash2, + upload: UploadCloud, + users: UsersRound, + video: Video, +} satisfies Record; + +export type IconName = keyof typeof icons; + +export interface IconProps extends Omit { + name: IconName; + label?: string; +} + +export function Icon({ name, label, size = 18, strokeWidth = 1.8, ...props }: IconProps) { + const IconComponent = icons[name]; + + return ( + + ); +} diff --git a/packages/ui-react/src/index.ts b/packages/ui-react/src/index.ts index f9b421c..9984c4f 100644 --- a/packages/ui-react/src/index.ts +++ b/packages/ui-react/src/index.ts @@ -1,5 +1,6 @@ export * from "./AppHeader"; export * from "./AdminNavigationPanel"; +export * from "./ApplicationShell"; export * from "./Button"; export * from "./Checker"; export * from "./ConfirmationModal"; @@ -7,6 +8,7 @@ export * from "./Dropdown"; export * from "./Field"; export * from "./Glass"; export * from "./Inspector"; +export * from "./Icon"; export * from "./RangeControl"; export * from "./SegmentedControl"; export * from "./Select"; diff --git a/registry/components.json b/registry/components.json index c0458e1..a9881d6 100644 --- a/registry/components.json +++ b/registry/components.json @@ -148,7 +148,37 @@ "anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"], "rules": [ "The logo occupies the same visual position and geometry across applications.", - "Product routes and profile content are application data passed into stable slots." + "Product routes and profile content are application data passed into stable slots.", + "The canonical application header is fixed and preserves the Launcher three-axis logo / navigation / profile positions." + ] + }, + { + "id": "application-shell", + "status": "baseline", + "package": "@nodedc/ui-react", + "exports": ["ApplicationShell", "ApplicationPanel"], + "domContract": ["nodedc-app-shell", "nodedc-application-panel"], + "summary": "Launcher-derived application template with fixed header, media stage, left navigation and a separate expandable content window.", + "anatomy": ["fixed AppHeader", "stage", "left navigation", "right content window", "expand and close actions"], + "behavior": ["navigation shifts stage", "content opens beside navigation", "expanded content replaces stage", "mobile panels use the available viewport sequentially", "independent content scrolling"], + "rules": [ + "The bottom Launcher service rail is product-specific and is not part of the general application template.", + "Desktop page gap is 20 px and navigation width follows the Launcher 332–352 px range.", + "Applications supply routes, profile data, stage media and panel content without changing shell geometry." + ] + }, + { + "id": "icon", + "status": "baseline", + "package": "@nodedc/ui-react", + "exports": ["Icon"], + "domContract": ["svg using currentColor"], + "summary": "Audited semantic icon subset shared by NODE.DC applications instead of product-local vendor imports.", + "variants": ["16 px", "18 px", "20 px"], + "rules": [ + "Use semantic names from registry/icons.json.", + "The containing component owns surface, hit target and state; the icon owns only the glyph.", + "Do not copy complete Lucide or Font Awesome catalogs into an application." ] }, { @@ -161,7 +191,7 @@ "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.", + "The desktop panel follows the Launcher 332–352 px width range with a 21.6 px shell radius and a 20 px page gap.", "Active route state uses the active surface and icon contrast from the theme." ] }, diff --git a/registry/icons.json b/registry/icons.json new file mode 100644 index 0000000..b9d1a25 --- /dev/null +++ b/registry/icons.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": "1.0.0", + "component": "Icon", + "package": "@nodedc/ui-react", + "baseLibrary": "lucide-react", + "defaultSize": 18, + "supportedSizes": [16, 18, 20], + "strokeWidth": 1.8, + "groups": [ + { + "id": "window-layer", + "label": "Окно и слой", + "referenceSources": ["launcher", "engine"], + "names": ["close", "plus", "expand", "minimize", "refresh", "panel", "apps"] + }, + { + "id": "navigation", + "label": "Навигация", + "referenceSources": ["launcher", "engine", "bim-viewer"], + "names": ["chevron-left", "chevron-right", "chevron-down", "grid", "list", "sliders", "search"] + }, + { + "id": "editing", + "label": "Редактирование", + "referenceSources": ["launcher", "seo"], + "names": ["save", "edit", "trash", "copy", "upload", "download", "external"] + }, + { + "id": "state-access", + "label": "Состояние и доступ", + "referenceSources": ["launcher", "seo", "task-manager"], + "names": ["check", "alert", "activity", "lock", "key", "shield", "circle"] + }, + { + "id": "entities", + "label": "Сущности", + "referenceSources": ["launcher", "task-manager"], + "names": ["profile", "users", "building", "globe", "database", "network", "inbox", "mail"] + }, + { + "id": "content", + "label": "Контент", + "referenceSources": ["seo", "bim-viewer", "engine"], + "names": ["image", "video", "file", "folder", "clipboard", "settings"] + } + ], + "rules": [ + "Use semantic icon names from this registry instead of importing an application-local icon by shape.", + "Icons inherit currentColor; their button surface, active state and hit target are owned by Button, IconButton or the containing pattern.", + "Do not copy the complete Font Awesome or Lucide vendor catalog into application code.", + "A new icon requires a confirmed product use, a registry entry and a catalog specimen." + ] +} diff --git a/registry/registry.json b/registry/registry.json index 42f7650..732cd08 100644 --- a/registry/registry.json +++ b/registry/registry.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0.0", - "libraryVersion": "0.2.0", + "libraryVersion": "0.3.0", "name": "NODE.DC Design System", "purpose": "Canonical code-level UI system for current and future NODE.DC applications.", "packages": [ @@ -28,6 +28,7 @@ "sourceRegistry": "sources.json", "componentRegistry": "components.json", "candidateRegistry": "candidates.json", + "iconRegistry": "icons.json", "documentation": { "architecture": "../docs/ARCHITECTURE.md", "components": "../docs/COMPONENTS.md", @@ -36,7 +37,9 @@ "baseline": "../docs/SOURCE_BASELINE.md", "governance": "../docs/GOVERNANCE.md", "adoption": "../docs/ADOPTION.md", - "candidates": "../docs/CANDIDATES.md" + "candidates": "../docs/CANDIDATES.md", + "applicationTemplate": "../docs/APPLICATION_TEMPLATE.md", + "icons": "../docs/ICONS.md" }, "operatingRule": "Check this registry before creating a NODE.DC interface element. Reuse or extend the canonical export instead of creating an application-local copy." } diff --git a/scripts/validate-registry.mjs b/scripts/validate-registry.mjs index f00ebf1..6d92cbb 100644 --- a/scripts/validate-registry.mjs +++ b/scripts/validate-registry.mjs @@ -9,6 +9,7 @@ const registry = await parse("registry/registry.json"); const sources = await parse("registry/sources.json"); const components = await parse("registry/components.json"); const candidates = await parse("registry/candidates.json"); +const icons = await parse("registry/icons.json"); const failures = []; const requireValue = (condition, message) => { @@ -21,12 +22,16 @@ requireValue(Array.isArray(registry.packages) && registry.packages.length >= 4, requireValue(Array.isArray(sources.sources) && sources.sources.length >= 6, "source audit is incomplete"); requireValue(Array.isArray(components.components) && components.components.length >= 10, "component registry is incomplete"); requireValue(Array.isArray(candidates.candidates) && candidates.candidates.length >= 10, "candidate registry is incomplete"); +requireValue(Array.isArray(icons.groups) && icons.groups.length >= 5, "icon registry is incomplete"); const ids = components.components.map((component) => component.id); requireValue(new Set(ids).size === ids.length, "component ids must be unique"); const candidateIds = candidates.candidates.map((component) => component.id); requireValue(new Set(candidateIds).size === candidateIds.length, "candidate ids must be unique"); requireValue(candidateIds.every((id) => !ids.includes(id)), "a component cannot be both baseline and candidate"); +const iconNames = icons.groups.flatMap((group) => group.names ?? []); +requireValue(iconNames.length >= 35, "icon registry must contain the audited baseline set"); +requireValue(new Set(iconNames).size === iconNames.length, "icon names must be unique across groups"); for (const component of components.components) { requireValue(component.id && component.status && component.summary, `component entry is incomplete: ${component.id ?? "unknown"}`); @@ -53,4 +58,4 @@ if (failures.length > 0) { process.exit(1); } -console.log(`Registry valid: ${components.components.length} components, ${candidates.candidates.length} candidates, ${sources.sources.length} audited sources.`); +console.log(`Registry valid: ${components.components.length} components, ${iconNames.length} icons, ${candidates.candidates.length} candidates, ${sources.sources.length} audited sources.`);