Add Launcher application template and icon system

This commit is contained in:
DCCONSTRUCTIONS
2026-07-10 11:56:27 +03:00
parent 4a9ad01ea6
commit 9261d3af36
30 changed files with 1466 additions and 363 deletions
+392 -203
View File
@@ -3,6 +3,8 @@ import { applyNodedcTheme, type NodedcTheme, type RgbTuple } from "@nodedc/ui-co
import {
AdminNavigationPanel,
AppHeader,
ApplicationPanel,
ApplicationShell,
Button,
Checker,
ColorField,
@@ -11,24 +13,24 @@ import {
Dropdown,
FieldFrame,
GlassSurface,
HeaderAvatar,
HeaderProfile,
HeaderProfileButton,
HeaderAvatar,
HeaderWorkspace,
Icon,
IconButton,
Inspector,
RangeControl,
SegmentedControl,
Select,
StatusBadge,
TextAreaField,
TextField,
Window,
WindowFooterActions,
type IconName,
} from "@nodedc/ui-react";
type CatalogSection = "foundation" | "controls" | "windows" | "inspector";
type HubMode = "showcase" | "admin" | "platform";
type CatalogSection = "foundation" | "controls" | "windows" | "modals" | "inspector" | "icons";
const engineAccentStyle = {
"--nodedc-accent-rgb": "148 123 219",
@@ -48,6 +50,123 @@ const selectOptions = [
{ value: "disabled", label: "Отключён", description: "Запуск запрещён" },
] as const;
const sectionDefinitions: Record<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 {
const normalized = hex.trim().replace(/^#/, "");
if (!/^[0-9a-f]{6}$/i.test(normalized)) return null;
@@ -58,25 +177,6 @@ function rgbFromHex(hex: string): RgbTuple | null {
];
}
function Section({ id, eyebrow, title, description, children }: {
id: string;
eyebrow: string;
title: string;
description: string;
children: ReactNode;
}) {
return (
<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 }: {
title: string;
note?: string;
@@ -97,9 +197,9 @@ function Preview({ title, note, children, className }: {
export function CatalogApp() {
const [theme, setTheme] = useState<NodedcTheme>("dark");
const [accentHex, setAccentHex] = useState("#c3ff66");
const [section, setSection] = useState<CatalogSection>("foundation");
const [hubMode, setHubMode] = useState<HubMode>("showcase");
const [adminSection, setAdminSection] = useState("invites");
const [guidelineOpen, setGuidelineOpen] = useState(false);
const [activeSection, setActiveSection] = useState<CatalogSection | null>(null);
const [panelExpanded, setPanelExpanded] = useState(true);
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
const [checker, setChecker] = useState(true);
const [brightness, setBrightness] = useState(49);
@@ -110,8 +210,9 @@ export function CatalogApp() {
const [connectionColor, setConnectionColor] = useState("#404040");
const [fillColor, setFillColor] = useState("#8f83ff");
const [strokeColor, setStrokeColor] = useState("#2b2b36");
const [modalOpen, setModalOpen] = useState(false);
const [windowOpen, setWindowOpen] = useState(false);
const [inspectorOpen, setInspectorOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [projectName, setProjectName] = useState("NODE.DC Platform");
const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения.");
@@ -122,9 +223,16 @@ export function CatalogApp() {
applyNodedcTheme(document.documentElement, { theme, accent });
}, [accent, theme]);
const openSection = (next: CatalogSection) => {
setSection(next);
document.getElementById(next)?.scrollIntoView({ behavior: "smooth", block: "start" });
const openSection = (next: string) => {
const section = next as CatalogSection;
setGuidelineOpen(true);
setActiveSection(section);
setPanelExpanded(true);
};
const closeGuideline = () => {
setGuidelineOpen(false);
setActiveSection(null);
};
const inspectorSections = [
@@ -161,9 +269,7 @@ export function CatalogApp() {
</ControlRow>
<Checker checked={usePortColors} label="Использовать цвет портов" onChange={setUsePortColors} />
<ControlRow label="Цвет связей по умолчанию"><ColorField value={connectionColor} onChange={setConnectionColor} /></ControlRow>
<ControlRow label="Применить ко всем">
<Button variant="accent" shape="pill" width="full">Применить</Button>
</ControlRow>
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
</>
),
},
@@ -182,102 +288,15 @@ export function CatalogApp() {
},
];
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 компонентов.">
const renderSectionContent = () => {
switch (activeSection) {
case "foundation":
return (
<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">
<Button variant={theme === "dark" ? "accent" : "secondary"} onClick={() => setTheme("dark")}>Dark</Button>
<Button variant={theme === "light" ? "accent" : "secondary"} onClick={() => setTheme("light")}>Light</Button>
<Button variant={theme === "dark" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
<Button variant={theme === "light" ? "accent" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
</div>
<div className="catalog-swatches">
{accents.map((item) => (
@@ -295,34 +314,42 @@ export function CatalogApp() {
</div>
<ColorField label="Свой accent" value={accentHex} onChange={setAccentHex} />
</Preview>
<Preview title="Glass hierarchy" note="default / strong / soft">
<Preview title="Иерархия стекла" note="default / strong / soft">
<div className="catalog-surface-stack">
<GlassSurface padding="md">Default surface</GlassSurface>
<GlassSurface tone="strong" padding="md">Strong floating surface</GlassSurface>
<GlassSurface tone="soft" padding="md">Soft nested surface</GlassSurface>
</div>
</Preview>
</div>
</Section>
<Section id="controls" eyebrow="02 / Primitives" title="Контролы" description="Общие actions, fields и selection primitives для React и DOM consumers.">
<div className="catalog-grid">
<Preview title="Actions" note="shared geometry">
<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>
<Preview title="Каркас Launcher" note="fixed header / stage / panels" className="catalog-preview--wide">
<div className="catalog-shell-diagram" aria-label="Схема каркаса приложения">
<span className="catalog-shell-diagram__header">Header</span>
<span className="catalog-shell-diagram__nav">Navigation</span>
<span className="catalog-shell-diagram__content">Content window</span>
<span className="catalog-shell-diagram__stage">Stage</span>
</div>
</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">
<TextField label="Название проекта" hint="обязательно" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
<TextAreaField label="Описание" value={notes} onChange={(event) => setNotes(event.target.value)} />
</div>
</Preview>
<Preview title="Selection" note="portal dropdown">
<Preview title="Selection" note="integrated / split">
<div className="catalog-form">
<FieldFrame label="Hub / integrated select">
<Select label="Статус" value={selectedStatus} options={[...selectOptions]} searchable onChange={setSelectedStatus} />
@@ -344,20 +371,20 @@ export function CatalogApp() {
</FieldFrame>
<Dropdown
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 }) => (
<>
<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}><span /><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="edit" /><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}><Icon name="folder" /><span className="nodedc-dropdown-option__label">Архивировать</span></button>
</>
)}
</Dropdown>
</div>
</Preview>
<Preview title="Environment controls" note="exact Engine geometry">
<Preview title="Environment controls" note="точная геометрия Engine">
<div className="catalog-engine-control-stack" style={engineAccentStyle}>
<ControlRow label="Цвет"><ColorField value={lightColor} onChange={setLightColor} /></ControlRow>
<RangeControl label="Яркость" value={brightness} min={0} max={100} formatValue={(value) => `${value}%`} onChange={setBrightness} />
@@ -365,77 +392,239 @@ export function CatalogApp() {
<ControlRow label="Применить ко всем"><Button variant="accent" shape="pill" width="full">Применить</Button></ControlRow>
</div>
</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">
<StatusBadge>Черновик</StatusBadge>
<StatusBadge tone="success">Готово</StatusBadge>
<StatusBadge tone="success">Синхронизировано</StatusBadge>
<StatusBadge tone="warning">Нужна проверка</StatusBadge>
<StatusBadge tone="danger">Ошибка</StatusBadge>
<StatusBadge tone="accent">В работе</StatusBadge>
</div>
</Preview>
</div>
</Section>
<Section id="windows" eyebrow="03 / Layers" title="Окна и подтверждения" description="Modal и modeless side panel используют общий portal, Escape и focus restore; trap и scroll lock остаются только у modal.">
<Preview title="Controlled windows" note="center / end / confirm">
<div className="catalog-inline catalog-inline--wrap">
<Button variant="accent" onClick={() => setModalOpen(true)}>Открыть окно</Button>
<Button onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button>
<Button variant="danger" onClick={() => setConfirmOpen(true)}>Проверить удаление</Button>
</div>
</Preview>
</Section>
<Section id="inspector" eyebrow="04 / Pattern" title="Inspector / Settings" description="Канон основан только на новом Environment Settings и NDC Agent Inspector, без legacy-инспекторов Engine.">
);
case "windows":
return (
<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">
<Button variant="accent" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button>
</div>
</Preview>
<Preview title="Оконные действия" note="круг `46.72 px`">
<div className="catalog-window-actions-demo">
<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">
<GlassSurface tone="strong" radius="panel" className="catalog-inspector-card" style={engineAccentStyle}>
<div className="catalog-inspector-title"><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
</GlassSurface>
</div>
</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
open={modalOpen}
title="Настройки сервиса"
subtitle="Единое окно Launcher / CMS / SEO"
onClose={() => setModalOpen(false)}
footer={
<>
<Button variant="ghost" onClick={() => setModalOpen(false)}>Отмена</Button>
<WindowFooterActions><Button variant="accent" onClick={() => setModalOpen(false)}>Сохранить</Button></WindowFooterActions>
</>
}
>
<div className="catalog-form">
<TextField label="Название" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
<FieldFrame label="Статус"><Select label="Статус" value={selectedStatus} options={[...selectOptions]} onChange={setSelectedStatus} /></FieldFrame>
<Checker checked={checker} label="Сервис активен" onChange={setChecker} />
</div>
</Window>
const activeDefinition = activeSection ? sectionDefinitions[activeSection] : null;
<Window
open={inspectorOpen}
title="Настройки окружения"
subtitle="ENGINE / новый канон"
placement="end"
style={engineAccentStyle}
onClose={() => setInspectorOpen(false)}
>
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
</Window>
return (
<>
<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={
<>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
<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}
/>
<ConfirmationModal
open={confirmOpen}
title="Удалить конфигурацию?"
description={<>Конфигурация будет удалена. Это действие нельзя отменить.</>}
confirmLabel="Удалить"
danger
onClose={() => setConfirmOpen(false)}
onConfirm={() => setConfirmOpen(false)}
/>
</div>
<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>
</>
}
>
<div className="catalog-form">
<TextField label="Название" value={projectName} onChange={(event) => setProjectName(event.target.value)} />
<FieldFrame label="Статус"><Select label="Статус" value={selectedStatus} options={[...selectOptions]} onChange={setSelectedStatus} /></FieldFrame>
<Checker checked={checker} label="Сервис активен" onChange={setChecker} />
</div>
</Window>
<Window
open={inspectorOpen}
title="Настройки окружения"
subtitle="ENGINE / новый канон"
placement="end"
style={engineAccentStyle}
onClose={() => setInspectorOpen(false)}
>
<Inspector sections={inspectorSections} defaultOpen={["illumination", "connections", "ports"]} />
</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
open={confirmOpen}
title="Удалить конфигурацию?"
description={<>Конфигурация будет удалена. Это действие нельзя отменить.</>}
confirmLabel="Удалить"
danger
onClose={() => setConfirmOpen(false)}
onConfirm={() => setConfirmOpen(false)}
/>
</>
);
}