Make application shell components package-native

This commit is contained in:
DCCONSTRUCTIONS
2026-07-10 12:41:12 +03:00
parent 9261d3af36
commit ebae662b52
52 changed files with 1036 additions and 185 deletions
+70 -35
View File
@@ -14,23 +14,28 @@ import {
FieldFrame,
GlassSurface,
HeaderAvatar,
HeaderNavigation,
HeaderProfile,
HeaderProfileButton,
HeaderWorkspace,
Icon,
IconButton,
Inspector,
MediaSourceField,
RangeControl,
Select,
SettingsCard,
StatusBadge,
Switch,
TextAreaField,
TextField,
useApplicationWorkspace,
Window,
WindowFooterActions,
type IconName,
} from "@nodedc/ui-react";
type CatalogSection = "foundation" | "controls" | "windows" | "modals" | "inspector" | "icons";
type CatalogSection = "foundation" | "controls" | "media" | "windows" | "modals" | "inspector" | "icons";
const engineAccentStyle = {
"--nodedc-accent-rgb": "148 123 219",
@@ -63,26 +68,32 @@ const sectionDefinitions: Record<CatalogSection, { eyebrow: string; title: strin
description: "Кнопки, поля, selection, Engine-настройки и понятные статусы операций.",
icon: "sliders",
},
media: {
eyebrow: "03 / MEDIA & SETTINGS",
title: "Медиа и настройки",
description: "Общий CMS/Launcher-контракт файла, URL, превью и нейтральных карточек настроек.",
icon: "image",
},
windows: {
eyebrow: "03 / WINDOWS",
eyebrow: "04 / WINDOWS",
title: "Окна",
description: "Обычные окна и modeless-панели с единым portal- и focus-контрактом.",
icon: "apps",
},
modals: {
eyebrow: "04 / MODALS",
eyebrow: "05 / MODALS",
title: "Модалки",
description: "Создание, подтверждение и разрушительные действия в адаптивном формате.",
icon: "clipboard",
},
inspector: {
eyebrow: "05 / INSPECTOR",
eyebrow: "06 / INSPECTOR",
title: "Инспектор",
description: "Точный новый Engine Environment Settings без legacy-инспекторов.",
icon: "settings",
},
icons: {
eyebrow: "06 / ICONS",
eyebrow: "07 / ICONS",
title: "Иконки",
description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.",
icon: "grid",
@@ -197,9 +208,8 @@ function Preview({ title, note, children, className }: {
export function CatalogApp() {
const [theme, setTheme] = useState<NodedcTheme>("dark");
const [accentHex, setAccentHex] = useState("#c3ff66");
const [guidelineOpen, setGuidelineOpen] = useState(false);
const [activeSection, setActiveSection] = useState<CatalogSection | null>(null);
const [panelExpanded, setPanelExpanded] = useState(true);
const workspace = useApplicationWorkspace<CatalogSection>();
const { navigationOpen: guidelineOpen, activeView: activeSection, contentExpanded: panelExpanded } = workspace;
const [selectedStatus, setSelectedStatus] = useState<(typeof selectOptions)[number]["value"]>("active");
const [checker, setChecker] = useState(true);
const [brightness, setBrightness] = useState(49);
@@ -216,6 +226,10 @@ export function CatalogApp() {
const [confirmOpen, setConfirmOpen] = useState(false);
const [projectName, setProjectName] = useState("NODE.DC Platform");
const [notes, setNotes] = useState("Общий контракт компонентов без доменной логики приложения.");
const [mediaSource, setMediaSource] = useState<"file" | "url">("file");
const [mediaUrl, setMediaUrl] = useState("");
const [mediaFileName, setMediaFileName] = useState("hub_vitrina-mqv7yd7k.mov");
const [mediaVisible, setMediaVisible] = useState(true);
const accent = useMemo(() => rgbFromHex(accentHex) ?? accents[0].value, [accentHex]);
@@ -224,16 +238,10 @@ export function CatalogApp() {
}, [accent, theme]);
const openSection = (next: string) => {
const section = next as CatalogSection;
setGuidelineOpen(true);
setActiveSection(section);
setPanelExpanded(true);
workspace.openView(next as CatalogSection);
};
const closeGuideline = () => {
setGuidelineOpen(false);
setActiveSection(null);
};
const closeGuideline = workspace.closeNavigation;
const inspectorSections = [
{
@@ -403,6 +411,38 @@ export function CatalogApp() {
</Preview>
</div>
);
case "media":
return (
<div className="catalog-grid catalog-grid--single">
<SettingsCard
eyebrow="CONTENT"
title="Видео-окно"
description="Окно демо-видео и подключённый медиа-файл. Хранилище и загрузка остаются в приложении; геометрия и переключение источника — в библиотеке."
actions={<Switch checked={mediaVisible} label="Показать" onChange={setMediaVisible} />}
>
<MediaSourceField
label="Видео / картинка"
source={mediaSource}
url={mediaUrl}
fileName={mediaFileName}
previewSrc={mediaSource === "url" && mediaUrl ? mediaUrl : "/launcher-stage-poster.png"}
previewKind={mediaSource === "file" ? "image" : undefined}
path="content.videoSrc → assets/uploads/media/component-library"
hint="Адаптер приложения сохраняет файл и возвращает URL; компонент не знает API storage."
onSourceChange={setMediaSource}
onUrlChange={setMediaUrl}
onFileChange={(file) => {
if (file) setMediaFileName(file.name);
}}
/>
</SettingsCard>
<Preview title="Граница ответственности" note="component / adapter" className="catalog-preview--wide">
<p className="catalog-preview__explanation">
`MediaSourceField` владеет пикселями, доступностью и состояниями file/URL. Конкретный проект передаёт upload-обработчик, storage-path и результат валидации поэтому компонент можно подключить к CMS, Launcher или будущему приложению без копирования его вёрстки.
</p>
</Preview>
</div>
);
case "windows":
return (
<div className="catalog-grid catalog-grid--single">
@@ -413,7 +453,7 @@ export function CatalogApp() {
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть инспектор</Button>
</div>
</Preview>
<Preview title="Оконные действия" note="круг `46.72 px`">
<Preview title="Оконные действия" note="круг `46 px`">
<div className="catalog-window-actions-demo">
<Button shape="pill" icon={<Icon name="refresh" />}>Обновить источник</Button>
<IconButton label="Добавить"><Icon name="plus" /></IconButton>
@@ -485,7 +525,7 @@ export function CatalogApp() {
className="catalog-app nodedc-ui-root"
style={{ "--catalog-accent": accentHex } as CSSProperties}
navigationOpen={guidelineOpen}
contentOpen={Boolean(activeSection)}
contentOpen={workspace.contentOpen}
contentExpanded={panelExpanded}
header={
<AppHeader
@@ -493,24 +533,19 @@ export function CatalogApp() {
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>
<HeaderWorkspace kind="mark" label="NODE.DC Design" imageUrl="/nodedc-mark.svg" />
<HeaderNavigation
label="Навигация приложения"
value={guidelineOpen ? "guideline" : undefined}
items={[{ value: "guideline", label: "Guideline" }]}
onChange={() => guidelineOpen ? closeGuideline() : workspace.openNavigation()}
/>
</>
}
right={
<HeaderProfile>
<IconButton label="Уведомления"><Icon name="inbox" /></IconButton>
<HeaderProfileButton><Icon name="profile" size={17} />Профиль</HeaderProfileButton>
<IconButton label="Уведомления"><Icon name="inbox" size={20} /></IconButton>
<HeaderProfileButton>Профиль</HeaderProfileButton>
<HeaderAvatar label="DC" />
</HeaderProfile>
}
@@ -546,7 +581,7 @@ export function CatalogApp() {
footer={
<>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"><Icon name="shield" /></span>
<span>Design System 0.3</span>
<span>Design System 0.4</span>
</>
}
/>
@@ -558,8 +593,8 @@ export function CatalogApp() {
title={activeDefinition.title}
description={activeDefinition.description}
expanded={panelExpanded}
onExpandedChange={setPanelExpanded}
onClose={() => setActiveSection(null)}
onExpandedChange={workspace.setContentExpanded}
onClose={workspace.closeView}
>
<div className="catalog-panel-content">{renderSectionContent()}</div>
</ApplicationPanel>
+2 -22
View File
@@ -28,26 +28,6 @@ textarea {
min-height: 100vh;
}
.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);
}
.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);
@@ -413,11 +393,11 @@ textarea {
display: none;
}
.catalog-header-nav {
.catalog-app .nodedc-header-navigation {
width: 100%;
}
.catalog-header-nav .nodedc-header__nav-item {
.catalog-app .nodedc-header-navigation .nodedc-header__nav-item {
width: 100%;
}