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)}
/>
</>
);
}
+320 -120
View File
@@ -2,22 +2,20 @@
background: var(--nodedc-canvas);
}
html {
scroll-behavior: smooth;
html,
body,
#root {
width: 100%;
min-width: 320px;
height: 100%;
min-height: 100%;
margin: 0;
overflow: hidden;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at 72% 10%, rgba(var(--nodedc-accent-rgb), 0.09), transparent 28rem),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
var(--nodedc-canvas);
background-size: auto, 44px 44px, 44px 44px, auto;
background: #050506;
color: var(--nodedc-text-primary);
transition: background-color 220ms ease, color 220ms ease;
}
button,
@@ -30,96 +28,100 @@ textarea {
min-height: 100vh;
}
.catalog-admin-layer {
position: fixed;
z-index: 390;
top: 6.55rem;
bottom: 1.25rem;
left: 1.25rem;
.catalog-header-nav {
display: inline-flex;
min-height: var(--nodedc-header-pill-height);
align-items: center;
border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-glass-panel-bg-soft);
padding: 0.32rem;
box-shadow: var(--nodedc-glass-control-shadow);
}
[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img {
.catalog-header-nav .nodedc-header__nav-item {
min-width: 8.5rem;
}
.catalog-header-workspace img {
width: 1.75rem;
height: 1.75rem;
object-fit: contain;
}
[data-nodedc-theme="light"] .catalog-app .nodedc-header__brand img,
[data-nodedc-theme="light"] .catalog-app .nodedc-header__workspace img {
filter: brightness(0.12);
}
.catalog-main {
.catalog-launcher-stage {
position: relative;
isolation: isolate;
overflow: hidden;
border-radius: var(--nodedc-radius-card);
background: #0b0b0d url("/launcher-stage-poster.png") center / cover no-repeat;
box-shadow: 0 44px 150px rgba(0, 0, 0, 0.62);
}
.catalog-launcher-stage__media,
.catalog-launcher-stage__shade {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.catalog-launcher-stage__media {
object-fit: cover;
opacity: 0.92;
}
.catalog-launcher-stage__shade {
z-index: 1;
background:
radial-gradient(circle at 52% 48%, transparent 0 38%, rgba(0, 0, 0, 0.34) 100%),
linear-gradient(90deg, rgba(0, 0, 0, 0.5), transparent 38% 72%, rgba(0, 0, 0, 0.2));
}
.catalog-launcher-stage__title {
position: absolute;
z-index: 2;
top: clamp(2.8rem, 9vh, 5.75rem);
left: clamp(2.8rem, 6vw, 5.75rem);
display: grid;
width: min(1180px, calc(100% - 2rem));
margin: 0 auto;
gap: 8rem;
padding: 5rem 0 10rem;
transition: width 500ms cubic-bezier(0.22, 1, 0.36, 1), margin-left 500ms cubic-bezier(0.22, 1, 0.36, 1);
max-width: 28rem;
gap: 0.24rem;
pointer-events: none;
}
.catalog-main[data-admin-open="true"] {
width: min(1180px, calc(100% - 25rem));
margin-right: 1rem;
margin-left: 24rem;
.catalog-launcher-stage__title span,
.catalog-launcher-stage__title strong {
font-size: clamp(2rem, 3vw, 3.25rem);
font-weight: 350;
letter-spacing: -0.035em;
line-height: 0.98;
}
.catalog-hero {
.catalog-launcher-stage__title span {
color: rgba(38, 38, 42, 0.76);
}
.catalog-launcher-stage__title strong {
color: rgba(255, 255, 255, 0.96);
}
.catalog-launcher-stage__title p {
max-width: 22rem;
margin: 0.9rem 0 0;
color: rgba(255, 255, 255, 0.68);
font-size: 0.86rem;
line-height: 1.45;
}
.catalog-panel-content {
display: grid;
min-height: 32rem;
align-content: center;
justify-items: start;
gap: 1.4rem;
}
.catalog-hero h1 {
max-width: 820px;
margin: 0;
color: var(--nodedc-text-primary);
font-size: clamp(3.1rem, 7vw, 6.4rem);
font-weight: 670;
letter-spacing: -0.065em;
line-height: 0.92;
}
.catalog-hero p {
max-width: 650px;
margin: 0;
color: var(--nodedc-text-secondary);
font-size: clamp(1rem, 1.6vw, 1.3rem);
line-height: 1.5;
}
.catalog-section-nav {
margin-top: 0.6rem;
}
.catalog-section {
display: grid;
scroll-margin-top: 2rem;
gap: 2rem;
}
.catalog-section__head {
display: grid;
max-width: 760px;
gap: 0.75rem;
}
.catalog-section__head span {
color: rgb(var(--nodedc-accent-rgb));
font-size: 0.72rem;
font-weight: 780;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.catalog-section__head h2 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.6rem);
font-weight: 650;
letter-spacing: -0.05em;
line-height: 1;
}
.catalog-section__head p {
margin: 0;
color: var(--nodedc-text-secondary);
font-size: 1rem;
line-height: 1.5;
align-content: start;
gap: 1rem;
padding: 0.15rem 0.1rem 1rem;
}
.catalog-grid {
@@ -132,8 +134,12 @@ textarea {
grid-template-columns: 1.1fr 0.9fr;
}
.catalog-grid--single {
grid-template-columns: 1fr;
}
.catalog-preview {
min-height: 18rem;
min-height: 16rem;
}
.catalog-preview--wide {
@@ -145,7 +151,7 @@ textarea {
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}
.catalog-preview__head strong {
@@ -155,24 +161,35 @@ textarea {
.catalog-preview__head span {
color: var(--nodedc-text-muted);
font-size: 0.68rem;
text-align: right;
text-transform: uppercase;
}
.catalog-preview__body {
display: grid;
min-height: 10rem;
min-height: 8rem;
align-content: center;
gap: 1rem;
}
.catalog-preview__explanation {
max-width: 42rem;
margin: 0;
color: var(--nodedc-text-muted);
font-size: 0.78rem;
line-height: 1.5;
}
.catalog-theme-row,
.catalog-inline {
.catalog-inline,
.catalog-window-actions-demo {
display: flex;
align-items: center;
gap: 0.6rem;
}
.catalog-inline--wrap {
.catalog-inline--wrap,
.catalog-window-actions-demo {
flex-wrap: wrap;
}
@@ -212,6 +229,14 @@ textarea {
gap: 0.75rem;
}
.catalog-surface-stack .nodedc-glass {
display: flex;
min-height: 4rem;
align-items: center;
color: var(--nodedc-text-secondary);
font-size: 0.78rem;
}
.catalog-engine-control-stack {
display: grid;
width: min(var(--nodedc-inspector-inner-width), 100%);
@@ -219,24 +244,49 @@ textarea {
gap: 0.75rem;
}
.catalog-surface-stack .nodedc-glass {
min-height: 4rem;
display: flex;
align-items: center;
color: var(--nodedc-text-secondary);
font-size: 0.78rem;
.catalog-shell-diagram {
display: grid;
min-height: 16rem;
grid-template-columns: minmax(8rem, 0.28fr) minmax(15rem, 1fr) minmax(7rem, 0.22fr);
grid-template-rows: 3.2rem 1fr;
gap: 0.6rem;
border-radius: 1.1rem;
background: rgba(0, 0, 0, 0.22);
padding: 0.6rem;
}
.catalog-shell-diagram span {
display: grid;
place-items: center;
border-radius: 0.9rem;
background: var(--nodedc-glass-control-bg);
color: var(--nodedc-text-muted);
font-size: 0.72rem;
font-weight: 700;
}
.catalog-shell-diagram__header {
grid-column: 1 / -1;
}
.catalog-shell-diagram__nav {
background: color-mix(in srgb, var(--nodedc-glass-control-hover) 90%, transparent) !important;
}
.catalog-shell-diagram__content {
background: color-mix(in srgb, var(--nodedc-glass-control-hover) 70%, transparent) !important;
}
.catalog-inspector-stage {
display: flex;
min-height: 40rem;
min-height: 43rem;
justify-content: flex-end;
align-items: flex-start;
padding: 4rem;
border-radius: 2rem;
border-radius: var(--nodedc-radius-card);
background:
radial-gradient(circle at 18% 24%, rgba(var(--nodedc-accent-rgb), 0.13), transparent 22rem),
color-mix(in srgb, var(--nodedc-canvas-soft) 86%, transparent);
padding: 2rem;
}
.catalog-inspector-card {
@@ -255,17 +305,89 @@ textarea {
font-size: 0.7rem;
}
@media (max-width: 820px) {
.catalog-main {
gap: 6rem;
padding-top: 2rem;
}
.catalog-icon-catalog {
display: grid;
gap: 1rem;
}
.catalog-main[data-admin-open="true"] {
width: min(1180px, calc(100% - 2rem));
margin-inline: auto;
}
.catalog-icon-group {
display: grid;
gap: 0.75rem;
border-radius: var(--nodedc-radius-card);
background: var(--nodedc-glass-panel-surface);
padding: 1rem;
}
.catalog-icon-group > header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding-inline: 0.2rem;
}
.catalog-icon-group > header strong {
font-size: 0.88rem;
}
.catalog-icon-group > header span {
color: var(--nodedc-text-muted);
font-size: 0.68rem;
text-transform: uppercase;
}
.catalog-icon-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.55rem;
}
.catalog-icon-tile {
display: grid;
min-width: 0;
grid-template-columns: 2.92rem minmax(0, 1fr);
align-items: center;
gap: 0.7rem;
border-radius: var(--nodedc-radius-control);
background: var(--nodedc-glass-control-bg);
padding: 0.35rem 0.55rem 0.35rem 0.35rem;
}
.catalog-icon-tile__surface {
display: grid;
width: 2.92rem;
height: 2.92rem;
place-items: center;
border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-glass-control-hover);
color: var(--nodedc-text-primary);
}
.catalog-icon-tile > span:last-child {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.catalog-icon-tile strong,
.catalog-icon-tile code {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.catalog-icon-tile strong {
font-size: 0.72rem;
}
.catalog-icon-tile code {
color: var(--nodedc-text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.62rem;
}
@media (max-width: 1280px) {
.catalog-grid,
.catalog-grid--foundation {
grid-template-columns: 1fr;
@@ -275,14 +397,92 @@ textarea {
grid-column: auto;
}
.catalog-icon-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.catalog-icon-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.catalog-launcher-stage__media {
display: none;
}
.catalog-header-nav {
width: 100%;
}
.catalog-header-nav .nodedc-header__nav-item {
width: 100%;
}
.catalog-app .nodedc-header__profile-button {
display: none;
}
.catalog-launcher-stage__title {
top: 2rem;
left: 2rem;
max-width: calc(100% - 4rem);
}
.catalog-launcher-stage__title span,
.catalog-launcher-stage__title strong {
font-size: clamp(1.8rem, 9vw, 3rem);
}
.catalog-launcher-stage__title p {
display: none;
}
.catalog-preview {
min-height: auto;
}
.catalog-preview__head,
.catalog-icon-group > header {
align-items: flex-start;
flex-direction: column;
gap: 0.25rem;
}
.catalog-swatches,
.catalog-icon-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.catalog-shell-diagram {
grid-template-columns: 1fr 1.5fr;
grid-template-rows: 3rem 7rem 7rem;
}
.catalog-shell-diagram__header {
grid-column: 1 / -1;
}
.catalog-shell-diagram__stage {
grid-column: 1 / -1;
}
.catalog-inspector-stage {
padding: 1rem;
min-height: 40rem;
justify-content: center;
padding: 0.75rem;
}
.catalog-window-actions-demo .nodedc-button {
width: 100%;
}
}
.catalog-admin-layer {
top: 0.875rem;
right: 0.875rem;
bottom: 0.875rem;
left: 0.875rem;
@media (max-width: 430px) {
.catalog-swatches,
.catalog-icon-grid {
grid-template-columns: 1fr;
}
}