Add Launcher application template and icon system
This commit is contained in:
parent
4a9ad01ea6
commit
9261d3af36
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# 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
|
## 0.2.0 — source-faithful geometry
|
||||||
|
|
||||||
- Corrected the Hub header to the production three-axis shell with workspace, navigation and profile groups.
|
- Corrected the Hub header to the production three-axis shell with workspace, navigation and profile groups.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
- модальные окна, подтверждения и боковые окна;
|
- модальные окна, подтверждения и боковые окна;
|
||||||
- segmented navigation и статусы;
|
- segmented navigation и статусы;
|
||||||
- Hub header и левая admin navigation panel;
|
- Hub header и левая admin navigation panel;
|
||||||
|
- launcher-style ApplicationShell без обязательной нижней витрины;
|
||||||
|
- аудированный общий набор иконок;
|
||||||
- инспектор/панель настроек нового поколения;
|
- инспектор/панель настроек нового поколения;
|
||||||
- поведение portal-слоёв, Escape, outside click и фокуса;
|
- поведение 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/ARCHITECTURE.md)
|
||||||
- [Компоненты](docs/COMPONENTS.md)
|
- [Компоненты](docs/COMPONENTS.md)
|
||||||
|
- [Шаблон приложения](docs/APPLICATION_TEMPLATE.md)
|
||||||
|
- [Иконки](docs/ICONS.md)
|
||||||
- [Темы](docs/THEMING.md)
|
- [Темы](docs/THEMING.md)
|
||||||
- [Окна и слои](docs/WINDOWS_AND_LAYERS.md)
|
- [Окна и слои](docs/WINDOWS_AND_LAYERS.md)
|
||||||
- [Границы исходного аудита](docs/SOURCE_BASELINE.md)
|
- [Границы исходного аудита](docs/SOURCE_BASELINE.md)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"typecheck": "tsc -b --pretty false"
|
"typecheck": "tsc -b --pretty false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0",
|
"@nodedc/ui-core": "0.3.0",
|
||||||
"@nodedc/ui-react": "0.2.0",
|
"@nodedc/ui-react": "0.3.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
Binary file not shown.
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg id="nodedc-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 93.22 54.55">
|
||||||
|
<path fill="#e2e1e1" d="M52.8 23.61 46.92 33.76 41.05 23.61H52.8m18-10.39H23.06l23.86 41.33Z"/>
|
||||||
|
<polygon fill="#e2e1e1" points="31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 322 B |
|
|
@ -3,6 +3,8 @@ import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-co
|
||||||
import {
|
import {
|
||||||
AdminNavigationPanel,
|
AdminNavigationPanel,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
|
ApplicationPanel,
|
||||||
|
ApplicationShell,
|
||||||
Button,
|
Button,
|
||||||
Checker,
|
Checker,
|
||||||
ColorField,
|
ColorField,
|
||||||
|
|
@ -11,24 +13,24 @@ import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FieldFrame,
|
FieldFrame,
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
|
HeaderAvatar,
|
||||||
HeaderProfile,
|
HeaderProfile,
|
||||||
HeaderProfileButton,
|
HeaderProfileButton,
|
||||||
HeaderAvatar,
|
|
||||||
HeaderWorkspace,
|
HeaderWorkspace,
|
||||||
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Inspector,
|
Inspector,
|
||||||
RangeControl,
|
RangeControl,
|
||||||
SegmentedControl,
|
|
||||||
Select,
|
Select,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
Window,
|
Window,
|
||||||
WindowFooterActions,
|
WindowFooterActions,
|
||||||
|
type IconName,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
type CatalogSection = "foundation" | "controls" | "windows" | "inspector";
|
type CatalogSection = "foundation" | "controls" | "windows" | "modals" | "inspector" | "icons";
|
||||||
type HubMode = "showcase" | "admin" | "platform";
|
|
||||||
|
|
||||||
const engineAccentStyle = {
|
const engineAccentStyle = {
|
||||||
"--nodedc-accent-rgb": "148 123 219",
|
"--nodedc-accent-rgb": "148 123 219",
|
||||||
|
|
@ -48,6 +50,123 @@ const selectOptions = [
|
||||||
{ value: "disabled", label: "Отключён", description: "Запуск запрещён" },
|
{ value: "disabled", label: "Отключён", description: "Запуск запрещён" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: string; description: string; icon: IconName }> = {
|
||||||
|
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<IconName, string> = {
|
||||||
|
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 {
|
function rgbFromHex(hex: string): RgbTuple | null {
|
||||||
const normalized = hex.trim().replace(/^#/, "");
|
const normalized = hex.trim().replace(/^#/, "");
|
||||||
if (!/^[0-9a-f]{6}$/i.test(normalized)) return null;
|
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 (
|
|
||||||
<section id={id} className="catalog-section">
|
|
||||||
<header className="catalog-section__head">
|
|
||||||
<span>{eyebrow}</span>
|
|
||||||
<h2>{title}</h2>
|
|
||||||
<p>{description}</p>
|
|
||||||
</header>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Preview({ title, note, children, className }: {
|
function Preview({ title, note, children, className }: {
|
||||||
title: string;
|
title: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
@ -97,9 +197,9 @@ function Preview({ title, note, children, className }: {
|
||||||
export function CatalogApp() {
|
export function CatalogApp() {
|
||||||
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
const [theme, setTheme] = useState<NodedcTheme>("dark");
|
||||||
const [accentHex, setAccentHex] = useState("#c3ff66");
|
const [accentHex, setAccentHex] = useState("#c3ff66");
|
||||||
const [section, setSection] = useState<CatalogSection>("foundation");
|
const [guidelineOpen, setGuidelineOpen] = useState(false);
|
||||||
const [hubMode, setHubMode] = useState<HubMode>("showcase");
|
const [activeSection, setActiveSection] = useState<CatalogSection | null>(null);
|
||||||
const [adminSection, setAdminSection] = useState("invites");
|
const [panelExpanded, setPanelExpanded] = useState(true);
|
||||||
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
|
||||||
const [checker, setChecker] = useState(true);
|
const [checker, setChecker] = useState(true);
|
||||||
const [brightness, setBrightness] = useState(49);
|
const [brightness, setBrightness] = useState(49);
|
||||||
|
|
@ -110,8 +210,9 @@ export function CatalogApp() {
|
||||||
const [connectionColor, setConnectionColor] = useState("#404040");
|
const [connectionColor, setConnectionColor] = useState("#404040");
|
||||||
const [fillColor, setFillColor] = useState("#8f83ff");
|
const [fillColor, setFillColor] = useState("#8f83ff");
|
||||||
const [strokeColor, setStrokeColor] = useState("#2b2b36");
|
const [strokeColor, setStrokeColor] = useState("#2b2b36");
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [windowOpen, setWindowOpen] = useState(false);
|
||||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||||
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const [projectName, setProjectName] = useState("NODE.DC Platform");
|
const [projectName, setProjectName] = useState("NODE.DC Platform");
|
||||||
const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения.");
|
const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения.");
|
||||||
|
|
@ -122,9 +223,16 @@ export function CatalogApp() {
|
||||||
applyNodedcTheme(document.documentElement, { theme, accent });
|
applyNodedcTheme(document.documentElement, { theme, accent });
|
||||||
}, [accent, theme]);
|
}, [accent, theme]);
|
||||||
|
|
||||||
const openSection = (next: CatalogSection) => {
|
const openSection = (next: string) => {
|
||||||
setSection(next);
|
const section = next as CatalogSection;
|
||||||
document.getElementById(next)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
setGuidelineOpen(true);
|
||||||
|
setActiveSection(section);
|
||||||
|
setPanelExpanded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeGuideline = () => {
|
||||||
|
setGuidelineOpen(false);
|
||||||
|
setActiveSection(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorSections = [
|
const inspectorSections = [
|
||||||
|
|
@ -161,9 +269,7 @@ export function CatalogApp() {
|
||||||
</ControlRow>
|
</ControlRow>
|
||||||
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
|
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
|
||||||
<ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
|
<ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
|
||||||
<ControlRow label="Применить ко всем">
|
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
|
||||||
<Button variant="accent" shape="pill" width="full">Применить</Button>
|
|
||||||
</ControlRow>
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -182,102 +288,15 @@ export function CatalogApp() {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const renderSectionContent = () => {
|
||||||
|
switch (activeSection) {
|
||||||
|
case "foundation":
|
||||||
return (
|
return (
|
||||||
<div className="catalog-app nodedc-ui-root" style={{ "--catalog-accent": accentHex } as CSSProperties}>
|
|
||||||
<AppHeader
|
|
||||||
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
|
||||||
center={
|
|
||||||
<>
|
|
||||||
<HeaderWorkspace label="DC" />
|
|
||||||
<SegmentedControl
|
|
||||||
label="Навигация Hub"
|
|
||||||
value={hubMode}
|
|
||||||
items={[
|
|
||||||
{ value: "showcase", label: "Витрина" },
|
|
||||||
{ value: "admin", label: "Администрирование" },
|
|
||||||
{ value: "platform", label: "Платформа" },
|
|
||||||
]}
|
|
||||||
onChange={setHubMode}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
right={
|
|
||||||
<HeaderProfile>
|
|
||||||
<IconButton label="Уведомления">▱</IconButton>
|
|
||||||
<HeaderProfileButton>Профиль</HeaderProfileButton>
|
|
||||||
<HeaderAvatar label="DC" />
|
|
||||||
</HeaderProfile>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hubMode === "admin" ? (
|
|
||||||
<div className="catalog-admin-layer">
|
|
||||||
<AdminNavigationPanel
|
|
||||||
title="Администрирование"
|
|
||||||
onClose={() => setHubMode("showcase")}
|
|
||||||
contexts={[
|
|
||||||
{
|
|
||||||
id: "public",
|
|
||||||
label: "Открытый контур",
|
|
||||||
description: "Public access pool",
|
|
||||||
icon: "◎",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "company",
|
|
||||||
groupLabel: "Компании",
|
|
||||||
label: "DCTOUCH",
|
|
||||||
description: "ООО ДИСТАЧ",
|
|
||||||
icon: "▥",
|
|
||||||
active: true,
|
|
||||||
onSelect: () => undefined,
|
|
||||||
onToggle: () => undefined,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
items={[
|
|
||||||
{ id: "overview", label: "Обзор", icon: "▦" },
|
|
||||||
{ id: "invites", label: "Инвайты", icon: "✉" },
|
|
||||||
{ id: "members", label: "Участники", icon: "♙" },
|
|
||||||
{ id: "access", label: "Доступы", icon: "⌁" },
|
|
||||||
{ id: "groups", label: "Группы", icon: "☷" },
|
|
||||||
]}
|
|
||||||
activeId={adminSection}
|
|
||||||
onItemChange={setAdminSection}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">♢</span>
|
|
||||||
<span>Root Admin</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<main className="catalog-main" data-admin-open={hubMode === "admin" ? "true" : undefined}>
|
|
||||||
<div className="catalog-hero">
|
|
||||||
<StatusBadge tone="accent">NODE.DC UI baseline</StatusBadge>
|
|
||||||
<h1>Один UI-контракт.<br />Разные приложения.</h1>
|
|
||||||
<p>Геометрия, окна и поведение остаются общими. Приложение выбирает тему и акцент, затем использует готовые компоненты.</p>
|
|
||||||
<div className="catalog-section-nav">
|
|
||||||
<SegmentedControl
|
|
||||||
label="Разделы каталога"
|
|
||||||
value={section}
|
|
||||||
items={[
|
|
||||||
{ value: "foundation", label: "Основа" },
|
|
||||||
{ value: "controls", label: "Контролы" },
|
|
||||||
{ value: "windows", label: "Окна" },
|
|
||||||
{ value: "inspector", label: "Инспектор" },
|
|
||||||
]}
|
|
||||||
onChange={openSection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Section id="foundation" eyebrow="01 / Foundation" title="Материал и тема" description="Одинаковые поверхности проверяются в тёмной и светлой схеме без fork компонентов.">
|
|
||||||
<div className="catalog-grid catalog-grid--foundation">
|
<div className="catalog-grid catalog-grid--foundation">
|
||||||
<Preview title="Theme contract" note={theme === "dark" ? "dark glass" : "light glass"}>
|
<Preview title="Тема приложения" note={theme === "dark" ? "dark glass" : "light glass"}>
|
||||||
<div className="catalog-theme-row">
|
<div className="catalog-theme-row">
|
||||||
<Button variant={theme === "dark" ? "accent" : "secondary"} onClick={() => setTheme("dark")}>Dark</Button>
|
<Button variant={theme === "dark" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
|
||||||
<Button variant={theme === "light" ? "accent" : "secondary"} onClick={() => setTheme("light")}>Light</Button>
|
<Button variant={theme === "light" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="catalog-swatches">
|
<div className="catalog-swatches">
|
||||||
{accents.map((item) => (
|
{accents.map((item) => (
|
||||||
|
|
@ -295,34 +314,42 @@ export function CatalogApp() {
|
||||||
</div>
|
</div>
|
||||||
<ColorField label="Свой accent" value={accentHex} onChange={setAccentHex} />
|
<ColorField label="Свой accent" value={accentHex} onChange={setAccentHex} />
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Glass hierarchy" note="default / strong / soft">
|
<Preview title="Иерархия стекла" note="default / strong / soft">
|
||||||
<div className="catalog-surface-stack">
|
<div className="catalog-surface-stack">
|
||||||
<GlassSurface padding="md">Default surface</GlassSurface>
|
<GlassSurface padding="md">Default surface</GlassSurface>
|
||||||
<GlassSurface tone="strong" padding="md">Strong floating surface</GlassSurface>
|
<GlassSurface tone="strong" padding="md">Strong floating surface</GlassSurface>
|
||||||
<GlassSurface tone="soft" padding="md">Soft nested surface</GlassSurface>
|
<GlassSurface tone="soft" padding="md">Soft nested surface</GlassSurface>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
</div>
|
<Preview title="Каркас Launcher" note="fixed header / stage / panels" className="catalog-preview--wide">
|
||||||
</Section>
|
<div className="catalog-shell-diagram" aria-label="Схема каркаса приложения">
|
||||||
|
<span className="catalog-shell-diagram__header">Header</span>
|
||||||
<Section id="controls" eyebrow="02 / Primitives" title="Контролы" description="Общие actions, fields и selection primitives для React и DOM consumers.">
|
<span className="catalog-shell-diagram__nav">Navigation</span>
|
||||||
<div className="catalog-grid">
|
<span className="catalog-shell-diagram__content">Content window</span>
|
||||||
<Preview title="Actions" note="shared geometry">
|
<span className="catalog-shell-diagram__stage">Stage</span>
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
|
||||||
<Button variant="accent" shape="pill">Сохранить</Button>
|
|
||||||
<Button>Отмена</Button>
|
|
||||||
<Button variant="ghost">Подробнее</Button>
|
|
||||||
<Button variant="danger">Удалить</Button>
|
|
||||||
<IconButton label="Настройки">⚙</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Fields" note="label / hint / control">
|
</div>
|
||||||
|
);
|
||||||
|
case "controls":
|
||||||
|
return (
|
||||||
|
<div className="catalog-grid">
|
||||||
|
<Preview title="Действия" note="общая геометрия">
|
||||||
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
|
<Button variant="accent" shape="pill" icon={<Icon name="save" />}>Сохранить</Button>
|
||||||
|
<Button icon={<Icon name="refresh" />}>Обновить</Button>
|
||||||
|
<Button variant="ghost" icon={<Icon name="external" />}>Открыть</Button>
|
||||||
|
<Button variant="danger" icon={<Icon name="trash" />}>Удалить</Button>
|
||||||
|
<IconButton label="Добавить"><Icon name="plus" /></IconButton>
|
||||||
|
</div>
|
||||||
|
</Preview>
|
||||||
|
<Preview title="Поля" note="label / hint / control">
|
||||||
<div className="catalog-form">
|
<div className="catalog-form">
|
||||||
<TextField label="Название проекта" hint="обязательно" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
|
<TextField label="Название проекта" hint="обязательно" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
|
||||||
<TextAreaField label="Описание" value={notes} onChange={(event) => setNotes(event.target.value)} />
|
<TextAreaField label="Описание" value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Selection" note="portal dropdown">
|
<Preview title="Selection" note="integrated / split">
|
||||||
<div className="catalog-form">
|
<div className="catalog-form">
|
||||||
<FieldFrame label="Hub / integrated select">
|
<FieldFrame label="Hub / integrated select">
|
||||||
<Select label="Статус" value={selectedStatus} options={[...selectOptions]} searchable onChange={setSelectedStatus} />
|
<Select label="Статус" value={selectedStatus} options={[...selectOptions]} searchable onChange={setSelectedStatus} />
|
||||||
|
|
@ -344,20 +371,20 @@ export function CatalogApp() {
|
||||||
</FieldFrame>
|
</FieldFrame>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
|
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
|
||||||
<Button ref={setTriggerRef} aria-expanded={open} aria-controls={surfaceId} onClick={toggle}>Действия</Button>
|
<Button ref={setTriggerRef} aria-expanded={open} aria-controls={surfaceId} icon={<Icon name="chevron-down" />} onClick={toggle}>Действия</Button>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<>
|
<>
|
||||||
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><span /><span className="nodedc-dropdown-option__label">Переименовать</span></button>
|
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><Icon name="edit" /><span className="nodedc-dropdown-option__label">Переименовать</span></button>
|
||||||
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><span /><span className="nodedc-dropdown-option__label">Дублировать</span></button>
|
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><Icon name="copy" /><span className="nodedc-dropdown-option__label">Дублировать</span></button>
|
||||||
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><span /><span className="nodedc-dropdown-option__label">Архивировать</span></button>
|
<button className="nodedc-dropdown-option" data-nodedc-option onClick={close}><Icon name="folder" /><span className="nodedc-dropdown-option__label">Архивировать</span></button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Environment controls" note="exact Engine geometry">
|
<Preview title="Environment controls" note="точная геометрия Engine">
|
||||||
<div className="catalog-engine-control-stack" style={engineAccentStyle}>
|
<div className="catalog-engine-control-stack" style={engineAccentStyle}>
|
||||||
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
|
||||||
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
|
||||||
|
|
@ -365,47 +392,189 @@ export function CatalogApp() {
|
||||||
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
|
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Semantic status" note="not tied to accent" className="catalog-preview--wide">
|
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">
|
||||||
|
<p className="catalog-preview__explanation">Статус сообщает результат операции: нейтрально, успешно, требуется внимание или ошибка. Акцент приложения для этого не переиспользуется.</p>
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<StatusBadge>Черновик</StatusBadge>
|
<StatusBadge>Черновик</StatusBadge>
|
||||||
<StatusBadge tone="success">Готово</StatusBadge>
|
<StatusBadge tone="success">Синхронизировано</StatusBadge>
|
||||||
<StatusBadge tone="warning">Нужна проверка</StatusBadge>
|
<StatusBadge tone="warning">Нужна проверка</StatusBadge>
|
||||||
<StatusBadge tone="danger">Ошибка</StatusBadge>
|
<StatusBadge tone="danger">Ошибка</StatusBadge>
|
||||||
<StatusBadge tone="accent">В работе</StatusBadge>
|
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
);
|
||||||
|
case "windows":
|
||||||
<Section id="windows" eyebrow="03 / Layers" title="Окна и подтверждения" description="Modal и modeless side panel используют общий portal, Escape и focus restore; trap и scroll lock остаются только у modal.">
|
return (
|
||||||
<Preview title="Controlled windows" note="center / end / confirm">
|
<div className="catalog-grid catalog-grid--single">
|
||||||
|
<Preview title="Управляемые окна" note="center / modeless end">
|
||||||
|
<p className="catalog-preview__explanation">Обычное окно блокирует фон и удерживает фокус. Правый инспектор не затемняет, не блокирует stage и не превращается в modal.</p>
|
||||||
<div className="catalog-inline catalog-inline--wrap">
|
<div className="catalog-inline catalog-inline--wrap">
|
||||||
<Button variant="accent" onClick={() => setModalOpen(true)}>Открыть окно</Button>
|
<Button variant="accent" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
|
||||||
<Button onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button>
|
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button>
|
||||||
<Button variant="danger" onClick={() => setConfirmOpen(true)}>Проверить удаление</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
</Section>
|
<Preview title="Оконные действия" note="круг `46.72 px`">
|
||||||
|
<div className="catalog-window-actions-demo">
|
||||||
<Section id="inspector" eyebrow="04 / Pattern" title="Inspector / Settings" description="Канон основан только на новом Environment Settings и NDC Agent Inspector, без legacy-инспекторов Engine.">
|
<Button shape="pill" icon={<Icon name="refresh" />}>Обновить источник</Button>
|
||||||
|
<IconButton label="Добавить"><Icon name="plus" /></IconButton>
|
||||||
|
<IconButton label="Развернуть"><Icon name="expand" /></IconButton>
|
||||||
|
<IconButton label="Закрыть"><Icon name="close" /></IconButton>
|
||||||
|
</div>
|
||||||
|
</Preview>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "modals":
|
||||||
|
return (
|
||||||
|
<div className="catalog-grid">
|
||||||
|
<Preview title="Создание" note="форма адаптируется по ширине">
|
||||||
|
<p className="catalog-preview__explanation">Поля переходят в одну колонку на узком viewport; footer остаётся видимым, прокручивается только тело окна.</p>
|
||||||
|
<Button variant="accent" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(true)}>Создать проект</Button>
|
||||||
|
</Preview>
|
||||||
|
<Preview title="Подтверждение удаления" note="async-safe">
|
||||||
|
<p className="catalog-preview__explanation">Destructive-цвет появляется у подтверждения, а не окрашивает всё окно заранее.</p>
|
||||||
|
<Button variant="danger" shape="pill" icon={<Icon name="trash" />} onClick={() => setConfirmOpen(true)}>Проверить удаление</Button>
|
||||||
|
</Preview>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "inspector":
|
||||||
|
return (
|
||||||
<div className="catalog-inspector-stage">
|
<div className="catalog-inspector-stage">
|
||||||
<GlassSurface tone="strong" radius="panel" className="catalog-inspector-card" style={engineAccentStyle}>
|
<GlassSurface tone="strong" radius="panel" className="catalog-inspector-card" style={engineAccentStyle}>
|
||||||
<div className="catalog-inspector-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
|
<div className="catalog-inspector-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
|
||||||
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
);
|
||||||
</main>
|
case "icons":
|
||||||
|
return (
|
||||||
|
<div className="catalog-icon-catalog">
|
||||||
|
<Preview title="Размеры и поверхности" note="16 / 18 / 20 px" className="catalog-preview--wide">
|
||||||
|
<div className="catalog-window-actions-demo">
|
||||||
|
<IconButton label="Добавить"><Icon name="plus" size={16} /></IconButton>
|
||||||
|
<IconButton label="Обновить"><Icon name="refresh" size={18} /></IconButton>
|
||||||
|
<IconButton label="Развернуть"><Icon name="expand" size={20} /></IconButton>
|
||||||
|
<IconButton label="Закрыть"><Icon name="close" size={18} /></IconButton>
|
||||||
|
</div>
|
||||||
|
<p className="catalog-preview__explanation">Иконка наследует `currentColor`; фон, размер кнопки и active-state задаёт компонент поверхности, а не SVG.</p>
|
||||||
|
</Preview>
|
||||||
|
{iconGroups.map((group) => (
|
||||||
|
<section className="catalog-icon-group" key={group.title}>
|
||||||
|
<header><strong>{group.title}</strong><span>{group.note}</span></header>
|
||||||
|
<div className="catalog-icon-grid">
|
||||||
|
{group.icons.map((name) => (
|
||||||
|
<div className="catalog-icon-tile" key={name}>
|
||||||
|
<span className="catalog-icon-tile__surface"><Icon name={name} size={20} /></span>
|
||||||
|
<span><strong>{iconLabels[name]}</strong><code>{name}</code></span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<Window
|
const activeDefinition = activeSection ? sectionDefinitions[activeSection] : null;
|
||||||
open={modalOpen}
|
|
||||||
title="Настройки сервиса"
|
return (
|
||||||
subtitle="Единое окно Launcher / CMS / SEO"
|
<>
|
||||||
onClose={() => setModalOpen(false)}
|
<ApplicationShell
|
||||||
|
className="catalog-app nodedc-ui-root"
|
||||||
|
style={{ "--catalog-accent": accentHex } as CSSProperties}
|
||||||
|
navigationOpen={guidelineOpen}
|
||||||
|
contentOpen={Boolean(activeSection)}
|
||||||
|
contentExpanded={panelExpanded}
|
||||||
|
header={
|
||||||
|
<AppHeader
|
||||||
|
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
||||||
|
brandHref="/"
|
||||||
|
center={
|
||||||
|
<>
|
||||||
|
<HeaderWorkspace className="catalog-header-workspace" label="NODE.DC Design" imageUrl="/nodedc-mark.svg" />
|
||||||
|
<nav className="catalog-header-nav" aria-label="Навигация приложения">
|
||||||
|
<button
|
||||||
|
className="nodedc-header__nav-item"
|
||||||
|
type="button"
|
||||||
|
data-active={guidelineOpen ? "true" : undefined}
|
||||||
|
aria-expanded={guidelineOpen}
|
||||||
|
onClick={() => guidelineOpen ? closeGuideline() : setGuidelineOpen(true)}
|
||||||
|
>
|
||||||
|
Guideline
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<HeaderProfile>
|
||||||
|
<IconButton label="Уведомления"><Icon name="inbox" /></IconButton>
|
||||||
|
<HeaderProfileButton><Icon name="profile" size={17} />Профиль</HeaderProfileButton>
|
||||||
|
<HeaderAvatar label="DC" />
|
||||||
|
</HeaderProfile>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
stage={
|
||||||
|
<section className="catalog-launcher-stage">
|
||||||
|
<video className="catalog-launcher-stage__media" autoPlay muted loop playsInline poster="/launcher-stage-poster.png" aria-hidden="true">
|
||||||
|
<source src="/launcher-stage.mp4" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
<div className="catalog-launcher-stage__shade" />
|
||||||
|
<div className="catalog-launcher-stage__title">
|
||||||
|
<span>NODE.DC</span>
|
||||||
|
<strong>DESIGN<br />GUIDELINE</strong>
|
||||||
|
<p>Общий форм-фактор приложений, компоненты и оконная механика платформы.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
navigation={
|
||||||
|
<AdminNavigationPanel
|
||||||
|
eyebrow="NODE.DC"
|
||||||
|
title="Visual Guideline"
|
||||||
|
closeLabel="Закрыть Guideline"
|
||||||
|
navigationLabel="Разделы Guideline"
|
||||||
|
onClose={closeGuideline}
|
||||||
|
items={(Object.entries(sectionDefinitions) as Array<[CatalogSection, (typeof sectionDefinitions)[CatalogSection]]>).map(([id, item]) => ({
|
||||||
|
id,
|
||||||
|
label: item.title,
|
||||||
|
icon: <Icon name={item.icon} />,
|
||||||
|
}))}
|
||||||
|
activeId={activeSection ?? undefined}
|
||||||
|
onItemChange={openSection}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={() => setModalOpen(false)}>Отмена</Button>
|
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
|
||||||
<WindowFooterActions><Button variant="accent" onClick={() => setModalOpen(false)}>Сохранить</Button></WindowFooterActions>
|
<span>Design System 0.3</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
content={activeDefinition ? (
|
||||||
|
<ApplicationPanel
|
||||||
|
key={activeSection}
|
||||||
|
eyebrow={activeDefinition.eyebrow}
|
||||||
|
title={activeDefinition.title}
|
||||||
|
description={activeDefinition.description}
|
||||||
|
expanded={panelExpanded}
|
||||||
|
onExpandedChange={setPanelExpanded}
|
||||||
|
onClose={() => setActiveSection(null)}
|
||||||
|
>
|
||||||
|
<div className="catalog-panel-content">{renderSectionContent()}</div>
|
||||||
|
</ApplicationPanel>
|
||||||
|
) : null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Window
|
||||||
|
open={windowOpen}
|
||||||
|
title="Настройки сервиса"
|
||||||
|
subtitle="Launcher / CMS / SEO"
|
||||||
|
onClose={() => setWindowOpen(false)}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="ghost" onClick={() => setWindowOpen(false)}>Отмена</Button>
|
||||||
|
<WindowFooterActions><Button variant="accent" shape="pill" onClick={() => setWindowOpen(false)}>Сохранить</Button></WindowFooterActions>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -427,6 +596,26 @@ export function CatalogApp() {
|
||||||
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
|
||||||
</Window>
|
</Window>
|
||||||
|
|
||||||
|
<Window
|
||||||
|
open={createModalOpen}
|
||||||
|
title="Создать проект"
|
||||||
|
subtitle="Адаптивная форма"
|
||||||
|
size="sm"
|
||||||
|
onClose={() => setCreateModalOpen(false)}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="ghost" onClick={() => setCreateModalOpen(false)}>Отмена</Button>
|
||||||
|
<WindowFooterActions><Button variant="accent" shape="pill" icon={<Icon name="plus" />} onClick={() => setCreateModalOpen(false)}>Создать</Button></WindowFooterActions>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="catalog-form">
|
||||||
|
<TextField label="Название проекта" hint="обязательно" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
|
||||||
|
<TextAreaField label="Описание" value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||||
|
<FieldFrame label="Статус"><Select label="Статус" value={selectedStatus} options={[...selectOptions]} onChange={setSelectedStatus} /></FieldFrame>
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
open={confirmOpen}
|
open={confirmOpen}
|
||||||
title="Удалить конфигурацию?"
|
title="Удалить конфигурацию?"
|
||||||
|
|
@ -436,6 +625,6 @@ export function CatalogApp() {
|
||||||
onClose={() => setConfirmOpen(false)}
|
onClose={() => setConfirmOpen(false)}
|
||||||
onConfirm={() => setConfirmOpen(false)}
|
onConfirm={() => setConfirmOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,20 @@
|
||||||
background: var(--nodedc-canvas);
|
background: var(--nodedc-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html,
|
||||||
scroll-behavior: smooth;
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 320px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-width: 320px;
|
background: #050506;
|
||||||
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;
|
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
transition: background-color 220ms ease, color 220ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|
@ -30,96 +28,100 @@ textarea {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-admin-layer {
|
.catalog-header-nav {
|
||||||
position: fixed;
|
display: inline-flex;
|
||||||
z-index: 390;
|
min-height: var(--nodedc-header-pill-height);
|
||||||
top: 6.55rem;
|
align-items: center;
|
||||||
bottom: 1.25rem;
|
border-radius: var(--nodedc-radius-circle);
|
||||||
left: 1.25rem;
|
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);
|
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;
|
display: grid;
|
||||||
width: min(1180px, calc(100% - 2rem));
|
max-width: 28rem;
|
||||||
margin: 0 auto;
|
gap: 0.24rem;
|
||||||
gap: 8rem;
|
pointer-events: none;
|
||||||
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"] {
|
.catalog-launcher-stage__title span,
|
||||||
width: min(1180px, calc(100% - 25rem));
|
.catalog-launcher-stage__title strong {
|
||||||
margin-right: 1rem;
|
font-size: clamp(2rem, 3vw, 3.25rem);
|
||||||
margin-left: 24rem;
|
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;
|
display: grid;
|
||||||
min-height: 32rem;
|
align-content: start;
|
||||||
align-content: center;
|
gap: 1rem;
|
||||||
justify-items: start;
|
padding: 0.15rem 0.1rem 1rem;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-grid {
|
.catalog-grid {
|
||||||
|
|
@ -132,8 +134,12 @@ textarea {
|
||||||
grid-template-columns: 1.1fr 0.9fr;
|
grid-template-columns: 1.1fr 0.9fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-grid--single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-preview {
|
.catalog-preview {
|
||||||
min-height: 18rem;
|
min-height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-preview--wide {
|
.catalog-preview--wide {
|
||||||
|
|
@ -145,7 +151,7 @@ textarea {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-preview__head strong {
|
.catalog-preview__head strong {
|
||||||
|
|
@ -155,24 +161,35 @@ textarea {
|
||||||
.catalog-preview__head span {
|
.catalog-preview__head span {
|
||||||
color: var(--nodedc-text-muted);
|
color: var(--nodedc-text-muted);
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
|
text-align: right;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-preview__body {
|
.catalog-preview__body {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 10rem;
|
min-height: 8rem;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
gap: 1rem;
|
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-theme-row,
|
||||||
.catalog-inline {
|
.catalog-inline,
|
||||||
|
.catalog-window-actions-demo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-inline--wrap {
|
.catalog-inline--wrap,
|
||||||
|
.catalog-window-actions-demo {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +229,14 @@ textarea {
|
||||||
gap: 0.75rem;
|
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 {
|
.catalog-engine-control-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(var(--nodedc-inspector-inner-width), 100%);
|
width: min(var(--nodedc-inspector-inner-width), 100%);
|
||||||
|
|
@ -219,24 +244,49 @@ textarea {
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-surface-stack .nodedc-glass {
|
.catalog-shell-diagram {
|
||||||
min-height: 4rem;
|
display: grid;
|
||||||
display: flex;
|
min-height: 16rem;
|
||||||
align-items: center;
|
grid-template-columns: minmax(8rem, 0.28fr) minmax(15rem, 1fr) minmax(7rem, 0.22fr);
|
||||||
color: var(--nodedc-text-secondary);
|
grid-template-rows: 3.2rem 1fr;
|
||||||
font-size: 0.78rem;
|
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 {
|
.catalog-inspector-stage {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 40rem;
|
min-height: 43rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 4rem;
|
border-radius: var(--nodedc-radius-card);
|
||||||
border-radius: 2rem;
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem),
|
radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem),
|
||||||
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
|
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
|
||||||
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-inspector-card {
|
.catalog-inspector-card {
|
||||||
|
|
@ -255,17 +305,89 @@ textarea {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
.catalog-icon-catalog {
|
||||||
.catalog-main {
|
display: grid;
|
||||||
gap: 6rem;
|
gap: 1rem;
|
||||||
padding-top: 2rem;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.catalog-main[data-admin-open="true"] {
|
.catalog-icon-group {
|
||||||
width: min(1180px, calc(100% - 2rem));
|
display: grid;
|
||||||
margin-inline: auto;
|
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,
|
||||||
.catalog-grid--foundation {
|
.catalog-grid--foundation {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
@ -275,14 +397,92 @@ textarea {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-inspector-stage {
|
.catalog-icon-grid {
|
||||||
padding: 1rem;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.catalog-admin-layer {
|
|
||||||
top: 0.875rem;
|
@media (max-width: 900px) {
|
||||||
right: 0.875rem;
|
.catalog-icon-grid {
|
||||||
bottom: 0.875rem;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
left: 0.875rem;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
- Dropdown/Select;
|
- Dropdown/Select;
|
||||||
- Window/Confirmation;
|
- Window/Confirmation;
|
||||||
- AppHeader.
|
- AppHeader.
|
||||||
|
- ApplicationShell/ApplicationPanel;
|
||||||
|
- Icon/IconButton.
|
||||||
|
|
||||||
Локальные реализации удаляются только после функционального и визуального сравнения.
|
Локальные реализации удаляются только после функционального и визуального сравнения.
|
||||||
|
|
||||||
|
|
@ -40,14 +42,17 @@
|
||||||
|
|
||||||
## Новые приложения
|
## Новые приложения
|
||||||
|
|
||||||
Шаблон приложения пока не входит в scope. До его появления новый проект должен:
|
Новый React-проект начинает с `ApplicationShell`:
|
||||||
|
|
||||||
- установить опубликованные NODE.DC UI packages;
|
- установить опубликованные NODE.DC UI packages;
|
||||||
- выбрать theme и accent;
|
- выбрать theme и accent;
|
||||||
- использовать AppHeader и Window/Dropdown primitives;
|
- передать фиксированный AppHeader, stage, navigation и content;
|
||||||
|
- использовать Window/Dropdown и канонический Icon вместо локальных реализаций;
|
||||||
- проверять registry перед созданием локального control;
|
- проверять registry перед созданием локального control;
|
||||||
- хранить доменные компоненты у себя.
|
- хранить доменные компоненты у себя.
|
||||||
|
|
||||||
|
Нижняя service rail добавляется только продуктам с витриной сервисов. В обычном оконном приложении она отсутствует.
|
||||||
|
|
||||||
## Adoption matrix
|
## Adoption matrix
|
||||||
|
|
||||||
| Приложение | Текущий источник | Целевой adapter | Первый набор |
|
| Приложение | Текущий источник | Целевой adapter | Первый набор |
|
||||||
|
|
@ -66,4 +71,3 @@
|
||||||
- не поддерживать отдельную тему путём fork компонента;
|
- не поддерживать отдельную тему путём fork компонента;
|
||||||
- не использовать legacy Engine inspector как промежуточный канон;
|
- не использовать legacy Engine inspector как промежуточный канон;
|
||||||
- не удалять production local component до проверки package replacement.
|
- не удалять production local component до проверки package replacement.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-поведением.
|
||||||
|
|
@ -27,6 +27,8 @@ Core не зависит от React.
|
||||||
|
|
||||||
`@nodedc/ui-react` предоставляет компоненты для Launcher/Hub, Engine, Ops и React-модулей. Компоненты контролируемые: бизнес-состояние остаётся у приложения, а визуальное и базовое интерактивное поведение принадлежит библиотеке.
|
`@nodedc/ui-react` предоставляет компоненты для Launcher/Hub, Engine, Ops и React-модулей. Компоненты контролируемые: бизнес-состояние остаётся у приложения, а визуальное и базовое интерактивное поведение принадлежит библиотеке.
|
||||||
|
|
||||||
|
`ApplicationShell` и `ApplicationPanel` образуют готовый launcher-style шаблон нового React-приложения. `Icon` предоставляет ограниченный аудированный словарь glyph, чтобы приложения не импортировали vendor-наборы независимо.
|
||||||
|
|
||||||
### DOM
|
### DOM
|
||||||
|
|
||||||
`@nodedc/ui-dom` предоставляет контроллеры для CMS, BIM Viewer и других приложений без React. DOM-слой не создаёт вторую стилизацию: он использует те же классы и токены `@nodedc/ui-core`.
|
`@nodedc/ui-dom` предоставляет контроллеры для CMS, BIM Viewer и других приложений без React. DOM-слой не создаёт вторую стилизацию: он использует те же классы и токены `@nodedc/ui-core`.
|
||||||
|
|
@ -44,6 +46,8 @@ Core не зависит от React.
|
||||||
- floating layers;
|
- floating layers;
|
||||||
- окна;
|
- окна;
|
||||||
- навигационная геометрия;
|
- навигационная геометрия;
|
||||||
|
- application shell и фиксированная шапка;
|
||||||
|
- канонический набор иконок;
|
||||||
- inspector/settings layout;
|
- inspector/settings layout;
|
||||||
- состояния загрузки, пустоты и ошибки.
|
- состояния загрузки, пустоты и ошибки.
|
||||||
|
|
||||||
|
|
@ -76,4 +80,3 @@ Core не зависит от React.
|
||||||
- major — удаление/переименование экспорта или несовместимая геометрия/семантика.
|
- major — удаление/переименование экспорта или несовместимая геометрия/семантика.
|
||||||
|
|
||||||
Приложение должно зависеть от опубликованной версии пакета, а не от копии файлов или произвольного commit URL.
|
Приложение должно зависеть от опубликованной версии пакета, а не от копии файлов или произвольного commit URL.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,11 +95,32 @@ Pill navigation для верхней панели и компактного п
|
||||||
|
|
||||||
В Hub-форме центр состоит из круглого workspace trigger и segmented navigation. Справа используется единая profile-group поверхность с иконкой, подписью и круглым avatar. Эти элементы нельзя заменять случайными standalone-кнопками, сохраняя только приблизительную позицию.
|
В 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
|
## AdminNavigationPanel
|
||||||
|
|
||||||
Левая выезжающая панель Hub/Launcher. Библиотека владеет оболочкой `352 px`, радиусом `21.6 px`, внутренними отступами, full-bleed context/navigation pills, круглыми icon surfaces и анимацией появления. Приложение передаёт workspace/company, маршруты, active id и footer identity.
|
Левая выезжающая панель 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
|
## StatusBadge
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 в каталоге. Локальный импорт иконки только ради немного другой формы запрещён.
|
||||||
|
|
@ -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/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` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория.
|
Существовавший `dc-ui-guideline` использован как исходный инвентарь. Он больше не должен развиваться как независимая копия после подключения центрального репозитория.
|
||||||
|
|
||||||
## SEO и CMS
|
## SEO и CMS
|
||||||
|
|
@ -48,6 +50,10 @@ CMS подтверждает необходимость DOM-пакета без
|
||||||
|
|
||||||
Используется как ограничение архитектуры: общий UI должен работать без React. В baseline включены общие требования к toolbar, settings menu и glass select, но не BIM-domain controls.
|
Используется как ограничение архитектуры: общий 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 не являются каноном.
|
||||||
|
|
||||||
## Почему код не копируется целиком
|
## Почему код не копируется целиком
|
||||||
|
|
||||||
Исторические реализации содержат:
|
Исторические реализации содержат:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"apps/*"
|
"apps/*"
|
||||||
|
|
@ -25,10 +25,10 @@
|
||||||
},
|
},
|
||||||
"apps/catalog": {
|
"apps/catalog": {
|
||||||
"name": "@nodedc/ui-catalog",
|
"name": "@nodedc/ui-catalog",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0",
|
"@nodedc/ui-core": "0.3.0",
|
||||||
"@nodedc/ui-react": "0.2.0",
|
"@nodedc/ui-react": "0.3.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1539,6 +1539,15 @@
|
||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
@ -1879,27 +1888,28 @@
|
||||||
},
|
},
|
||||||
"packages/tokens": {
|
"packages/tokens": {
|
||||||
"name": "@nodedc/tokens",
|
"name": "@nodedc/tokens",
|
||||||
"version": "0.2.0"
|
"version": "0.3.0"
|
||||||
},
|
},
|
||||||
"packages/ui-core": {
|
"packages/ui-core": {
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/tokens": "0.2.0"
|
"@nodedc/tokens": "0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-dom": {
|
"packages/ui-dom": {
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0"
|
"@nodedc/ui-core": "0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/ui-react": {
|
"packages/ui-react": {
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0"
|
"@nodedc/ui-core": "0.3.0",
|
||||||
|
"lucide-react": "^0.468.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "nodedc-design-guideline",
|
"name": "nodedc-design-guideline",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.",
|
"description": "Canonical NODE.DC design system, reusable UI packages and living component catalog.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/tokens",
|
"name": "@nodedc/tokens",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["tokens.json", "tokens.css", "themes.css"],
|
"files": ["tokens.json", "tokens.css", "themes.css"],
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-core",
|
"name": "@nodedc/ui-core",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist", "styles.css"],
|
"files": ["dist", "styles.css"],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/tokens": "0.2.0"
|
"@nodedc/tokens": "0.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,14 @@ textarea.nodedc-field__control {
|
||||||
padding: 1rem var(--nodedc-page-padding) 0.75rem;
|
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 {
|
.nodedc-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 4.25rem;
|
min-height: 4.25rem;
|
||||||
|
|
@ -603,6 +611,7 @@ textarea.nodedc-field__control {
|
||||||
min-height: calc(var(--nodedc-header-pill-height) - 0.64rem);
|
min-height: calc(var(--nodedc-header-pill-height) - 0.64rem);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 0.45rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--nodedc-radius-circle);
|
border-radius: var(--nodedc-radius-circle);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
@ -691,6 +700,189 @@ textarea.nodedc-field__control {
|
||||||
color: var(--nodedc-glass-control-active-text);
|
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 {
|
.nodedc-admin-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
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;
|
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 {
|
@keyframes nodedc-admin-panel-in {
|
||||||
from { opacity: 0; transform: translateX(-1.6rem); }
|
from { opacity: 0; transform: translateX(-1.6rem); }
|
||||||
to { opacity: 1; transform: translateX(0); }
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
|
@ -915,6 +1111,7 @@ textarea.nodedc-field__control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-admin-panel__nav-item {
|
.nodedc-admin-panel__nav-item {
|
||||||
|
outline: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 160ms ease, color 160ms ease, opacity 160ms ease, transform 160ms ease;
|
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);
|
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 {
|
.nodedc-admin-panel__nav-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|
@ -1514,9 +1720,30 @@ textarea.nodedc-field__control {
|
||||||
font-size: var(--nodedc-font-size-lg);
|
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) {
|
@media (max-width: 760px) {
|
||||||
|
.nodedc-header-shell {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.85rem 1rem 0.65rem;
|
||||||
|
background: var(--nodedc-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-header {
|
.nodedc-header {
|
||||||
|
min-height: 8.6rem;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-content: start;
|
||||||
|
row-gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-header__center {
|
.nodedc-header__center {
|
||||||
|
|
@ -1530,6 +1757,54 @@ textarea.nodedc-field__control {
|
||||||
min-width: max-content;
|
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"] {
|
.nodedc-window[data-placement="end"] {
|
||||||
width: calc(100vw - 1.75rem);
|
width: calc(100vw - 1.75rem);
|
||||||
max-height: calc(100vh - 1.75rem);
|
max-height: calc(100vh - 1.75rem);
|
||||||
|
|
@ -1554,6 +1829,7 @@ textarea.nodedc-field__control {
|
||||||
.nodedc-dropdown-surface,
|
.nodedc-dropdown-surface,
|
||||||
.nodedc-overlay,
|
.nodedc-overlay,
|
||||||
.nodedc-window,
|
.nodedc-window,
|
||||||
|
.nodedc-application-panel,
|
||||||
.nodedc-inspector__section-content {
|
.nodedc-inspector__section-content {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-dom",
|
"name": "@nodedc/ui-dom",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist"],
|
"files": ["dist"],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
@ -16,6 +16,6 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0"
|
"@nodedc/ui-core": "0.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nodedc/ui-react",
|
"name": "@nodedc/ui-react",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist"],
|
"files": ["dist"],
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodedc/ui-core": "0.2.0"
|
"@nodedc/ui-core": "0.3.0",
|
||||||
|
"lucide-react": "^0.468.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface AdminNavigationPanelProps extends Omit<HTMLAttributes<HTMLEleme
|
||||||
activeId?: string;
|
activeId?: string;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
|
navigationLabel?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onItemChange: (id: string) => void;
|
onItemChange: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -38,13 +39,14 @@ export function AdminNavigationPanel({
|
||||||
activeId,
|
activeId,
|
||||||
footer,
|
footer,
|
||||||
closeLabel = "Закрыть администрирование",
|
closeLabel = "Закрыть администрирование",
|
||||||
|
navigationLabel = "Разделы администрирования",
|
||||||
onClose,
|
onClose,
|
||||||
onItemChange,
|
onItemChange,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: AdminNavigationPanelProps) {
|
}: AdminNavigationPanelProps) {
|
||||||
return (
|
return (
|
||||||
<aside className={cn("nodedc-admin-panel", className)} {...props}>
|
<aside className={cn("nodedc-admin-panel", className)} data-has-contexts={contexts.length > 0 ? "true" : undefined} {...props}>
|
||||||
<header className="nodedc-admin-panel__head">
|
<header className="nodedc-admin-panel__head">
|
||||||
<div>
|
<div>
|
||||||
<p>{eyebrow}</p>
|
<p>{eyebrow}</p>
|
||||||
|
|
@ -55,7 +57,7 @@ export function AdminNavigationPanel({
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="nodedc-admin-panel__contexts">
|
{contexts.length > 0 ? <div className="nodedc-admin-panel__contexts">
|
||||||
{contexts.map((context) => (
|
{contexts.map((context) => (
|
||||||
<div className="nodedc-admin-panel__context-group" key={context.id}>
|
<div className="nodedc-admin-panel__context-group" key={context.id}>
|
||||||
{context.groupLabel ? <span className="nodedc-admin-panel__group-label">{context.groupLabel}</span> : null}
|
{context.groupLabel ? <span className="nodedc-admin-panel__group-label">{context.groupLabel}</span> : null}
|
||||||
|
|
@ -78,9 +80,9 @@ export function AdminNavigationPanel({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div> : null}
|
||||||
|
|
||||||
<nav className="nodedc-admin-panel__nav" aria-label="Разделы администрирования">
|
<nav className="nodedc-admin-panel__nav" aria-label={navigationLabel}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|
@ -99,4 +101,3 @@ export function AdminNavigationPanel({
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface AppHeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
brand: ReactNode;
|
brand: ReactNode;
|
||||||
brandHref?: string;
|
brandHref?: string;
|
||||||
brandLabel?: string;
|
brandLabel?: string;
|
||||||
|
fixed?: boolean;
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
center?: ReactNode;
|
center?: ReactNode;
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
|
|
@ -14,6 +15,7 @@ export function AppHeader({
|
||||||
brand,
|
brand,
|
||||||
brandHref,
|
brandHref,
|
||||||
brandLabel = "NODE.DC",
|
brandLabel = "NODE.DC",
|
||||||
|
fixed = true,
|
||||||
left,
|
left,
|
||||||
center,
|
center,
|
||||||
right,
|
right,
|
||||||
|
|
@ -27,7 +29,7 @@ export function AppHeader({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn("nodedc-header-shell", className)} {...props}>
|
<header className={cn("nodedc-header-shell", className)} data-fixed={fixed ? "true" : undefined} {...props}>
|
||||||
<div className="nodedc-header">
|
<div className="nodedc-header">
|
||||||
<div className="nodedc-header__left">{brandNode}{left}</div>
|
<div className="nodedc-header__left">{brandNode}{left}</div>
|
||||||
<div className="nodedc-header__center">{center}</div>
|
<div className="nodedc-header__center">{center}</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import type { HTMLAttributes, ReactNode } from "react";
|
||||||
|
import { cn } from "./cn";
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
|
export interface ApplicationShellProps extends Omit<HTMLAttributes<HTMLDivElement>, "content"> {
|
||||||
|
header: ReactNode;
|
||||||
|
stage: ReactNode;
|
||||||
|
navigation?: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
navigationOpen?: boolean;
|
||||||
|
contentOpen?: boolean;
|
||||||
|
contentExpanded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationShell({
|
||||||
|
header,
|
||||||
|
stage,
|
||||||
|
navigation,
|
||||||
|
content,
|
||||||
|
navigationOpen = false,
|
||||||
|
contentOpen = false,
|
||||||
|
contentExpanded = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ApplicationShellProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("nodedc-app-shell", className)}
|
||||||
|
data-navigation-open={navigationOpen ? "true" : undefined}
|
||||||
|
data-content-open={contentOpen ? "true" : undefined}
|
||||||
|
data-content-expanded={contentOpen && contentExpanded ? "true" : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{header}
|
||||||
|
<main className="nodedc-app-shell__main">
|
||||||
|
<div className="nodedc-app-shell__stage">{stage}</div>
|
||||||
|
{navigationOpen && navigation ? <div className="nodedc-app-shell__navigation">{navigation}</div> : null}
|
||||||
|
{contentOpen && content ? <div className="nodedc-app-shell__content">{content}</div> : null}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationPanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
||||||
|
eyebrow?: ReactNode;
|
||||||
|
title: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
|
expanded?: boolean;
|
||||||
|
expandLabel?: string;
|
||||||
|
closeLabel?: string;
|
||||||
|
onExpandedChange?: (expanded: boolean) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationPanel({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
expanded = false,
|
||||||
|
expandLabel = "Развернуть окно",
|
||||||
|
closeLabel = "Закрыть окно",
|
||||||
|
onExpandedChange,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ApplicationPanelProps) {
|
||||||
|
return (
|
||||||
|
<section className={cn("nodedc-application-panel", className)} data-expanded={expanded ? "true" : undefined} {...props}>
|
||||||
|
<header className="nodedc-application-panel__head">
|
||||||
|
<div className="nodedc-application-panel__titles">
|
||||||
|
{eyebrow ? <span>{eyebrow}</span> : null}
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{description ? <p>{description}</p> : null}
|
||||||
|
</div>
|
||||||
|
<div className="nodedc-application-panel__actions">
|
||||||
|
{onExpandedChange ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-application-panel__action"
|
||||||
|
aria-label={expanded ? "Свернуть окно" : expandLabel}
|
||||||
|
onClick={() => onExpandedChange(!expanded)}
|
||||||
|
>
|
||||||
|
<Icon name={expanded ? "minimize" : "expand"} />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button type="button" className="nodedc-application-panel__action" aria-label={closeLabel} onClick={onClose}>
|
||||||
|
<Icon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="nodedc-application-panel__body">{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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<string, LucideIcon>;
|
||||||
|
|
||||||
|
export type IconName = keyof typeof icons;
|
||||||
|
|
||||||
|
export interface IconProps extends Omit<LucideProps, "ref"> {
|
||||||
|
name: IconName;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Icon({ name, label, size = 18, strokeWidth = 1.8, ...props }: IconProps) {
|
||||||
|
const IconComponent = icons[name];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
size={size}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
aria-hidden={label ? undefined : "true"}
|
||||||
|
aria-label={label}
|
||||||
|
role={label ? "img" : undefined}
|
||||||
|
focusable="false"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export * from "./AppHeader";
|
export * from "./AppHeader";
|
||||||
export * from "./AdminNavigationPanel";
|
export * from "./AdminNavigationPanel";
|
||||||
|
export * from "./ApplicationShell";
|
||||||
export * from "./Button";
|
export * from "./Button";
|
||||||
export * from "./Checker";
|
export * from "./Checker";
|
||||||
export * from "./ConfirmationModal";
|
export * from "./ConfirmationModal";
|
||||||
|
|
@ -7,6 +8,7 @@ export * from "./Dropdown";
|
||||||
export * from "./Field";
|
export * from "./Field";
|
||||||
export * from "./Glass";
|
export * from "./Glass";
|
||||||
export * from "./Inspector";
|
export * from "./Inspector";
|
||||||
|
export * from "./Icon";
|
||||||
export * from "./RangeControl";
|
export * from "./RangeControl";
|
||||||
export * from "./SegmentedControl";
|
export * from "./SegmentedControl";
|
||||||
export * from "./Select";
|
export * from "./Select";
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,37 @@
|
||||||
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"The logo occupies the same visual position and geometry across applications.",
|
"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"],
|
"anatomy": ["eyebrow and title", "close action", "workspace context", "company context", "route items", "footer identity"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"The panel shell, full-bleed pills and icon circles are library geometry; routes and identity data remain application-owned.",
|
"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."
|
"Active route state uses the active surface and icon contrast from the theme."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": "1.0.0",
|
"schemaVersion": "1.0.0",
|
||||||
"libraryVersion": "0.2.0",
|
"libraryVersion": "0.3.0",
|
||||||
"name": "NODE.DC Design System",
|
"name": "NODE.DC Design System",
|
||||||
"purpose": "Canonical code-level UI system for current and future NODE.DC applications.",
|
"purpose": "Canonical code-level UI system for current and future NODE.DC applications.",
|
||||||
"packages": [
|
"packages": [
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"sourceRegistry": "sources.json",
|
"sourceRegistry": "sources.json",
|
||||||
"componentRegistry": "components.json",
|
"componentRegistry": "components.json",
|
||||||
"candidateRegistry": "candidates.json",
|
"candidateRegistry": "candidates.json",
|
||||||
|
"iconRegistry": "icons.json",
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"architecture": "../docs/ARCHITECTURE.md",
|
"architecture": "../docs/ARCHITECTURE.md",
|
||||||
"components": "../docs/COMPONENTS.md",
|
"components": "../docs/COMPONENTS.md",
|
||||||
|
|
@ -36,7 +37,9 @@
|
||||||
"baseline": "../docs/SOURCE_BASELINE.md",
|
"baseline": "../docs/SOURCE_BASELINE.md",
|
||||||
"governance": "../docs/GOVERNANCE.md",
|
"governance": "../docs/GOVERNANCE.md",
|
||||||
"adoption": "../docs/ADOPTION.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."
|
"operatingRule": "Check this registry before creating a NODE.DC interface element. Reuse or extend the canonical export instead of creating an application-local copy."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const registry = await parse("registry/registry.json");
|
||||||
const sources = await parse("registry/sources.json");
|
const sources = await parse("registry/sources.json");
|
||||||
const components = await parse("registry/components.json");
|
const components = await parse("registry/components.json");
|
||||||
const candidates = await parse("registry/candidates.json");
|
const candidates = await parse("registry/candidates.json");
|
||||||
|
const icons = await parse("registry/icons.json");
|
||||||
|
|
||||||
const failures = [];
|
const failures = [];
|
||||||
const requireValue = (condition, message) => {
|
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(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(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(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);
|
const ids = components.components.map((component) => component.id);
|
||||||
requireValue(new Set(ids).size === ids.length, "component ids must be unique");
|
requireValue(new Set(ids).size === ids.length, "component ids must be unique");
|
||||||
const candidateIds = candidates.candidates.map((component) => component.id);
|
const candidateIds = candidates.candidates.map((component) => component.id);
|
||||||
requireValue(new Set(candidateIds).size === candidateIds.length, "candidate ids must be unique");
|
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");
|
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) {
|
for (const component of components.components) {
|
||||||
requireValue(component.id && component.status && component.summary, `component entry is incomplete: ${component.id ?? "unknown"}`);
|
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);
|
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.`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue