Persist catalog layout and add inspector toolbar

This commit is contained in:
DCCONSTRUCTIONS
2026-07-10 23:28:10 +03:00
parent 8ca12f2676
commit 406e692a5e
14 changed files with 715 additions and 268 deletions
+250 -120
View File
@@ -23,6 +23,7 @@ import {
Inspector,
MediaSourceField,
RangeControl,
SegmentedControl,
Select,
ShareAccessModal,
ShareLinkModal,
@@ -31,14 +32,16 @@ import {
Switch,
TextAreaField,
TextField,
Toolbar,
useApplicationWorkspace,
Window,
WindowFooterActions,
type IconName,
type ShareAccessMember,
type ToolbarPlacement,
} from "@nodedc/ui-react";
type CatalogSection = "foundation" | "controls" | "media" | "windows" | "modals" | "icons";
type CatalogSection = "controls" | "media" | "modals" | "icons";
const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [
{ label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" },
@@ -52,20 +55,58 @@ type MaterialDraft = {
panelOpacity: number;
fieldHex: string;
fieldOpacity: number;
nestedHex: string;
};
interface StoredLayout {
theme?: NodedcTheme;
accentHex?: string;
materialByTheme?: Record<NodedcTheme, MaterialDraft>;
environment?: {
lightColor?: string;
brightness?: number;
glowDistance?: number;
connectionType?: string;
connectionColor?: string;
usePortColors?: boolean;
fillColor?: string;
fillOpacity?: number;
strokeColor?: string;
strokeOpacity?: number;
};
media?: {
source?: "file" | "url";
url?: string;
fileName?: string;
fileSrc?: string;
visible?: boolean;
};
toolbar?: {
placement?: ToolbarPlacement;
background?: string;
border?: string;
outline?: string;
minSize?: number;
maxSize?: number;
lensCount?: number;
autoHide?: boolean;
};
}
const materialDefaults: Record<NodedcTheme, MaterialDraft> = {
dark: {
panelHex: "#151517",
panelOpacity: 100,
fieldHex: "#2a2a2c",
fieldOpacity: 100,
nestedHex: "#0b0b0d",
},
light: {
panelHex: "#ffffff",
panelOpacity: 100,
fieldHex: "#ffffff",
fieldOpacity: 100,
nestedHex: "#f4f4f4",
},
};
@@ -84,38 +125,26 @@ const selectOptions = [
] 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",
eyebrow: "01 / CONTROLS",
title: "Контролы",
description: "Кнопки, поля, выбор, Environment Settings и сворачиваемые секции нового Engine.",
description: "Кнопки, поля, выбор, оконные действия и вызываемый Inspector нового Engine.",
icon: "sliders",
},
media: {
eyebrow: "03 / MEDIA & SETTINGS",
eyebrow: "02 / MEDIA & SETTINGS",
title: "Медиа и настройки",
description: "Общий CMS/Launcher-контракт файла, URL, превью и нейтральных карточек настроек.",
icon: "image",
},
windows: {
eyebrow: "04 / WINDOWS",
title: "Окна",
description: "Обычные окна и modeless-панели с единым portal- и focus-контрактом.",
icon: "apps",
},
modals: {
eyebrow: "05 / MODALS",
eyebrow: "03 / MODALS",
title: "Модалки",
description: "Полная карта modal-паттернов Launcher, нового Engine и BIM Viewer.",
icon: "clipboard",
},
icons: {
eyebrow: "06 / ICONS",
eyebrow: "04 / ICONS",
title: "Иконки",
description: "Канонический общий набор по Launcher, SEO, BIM Viewer и новым участкам Engine.",
icon: "grid",
@@ -234,11 +263,10 @@ export function CatalogApp() {
dark: { ...materialDefaults.dark },
light: { ...materialDefaults.light },
}));
const [environmentLayoutSaved, setEnvironmentLayoutSaved] = useState(false);
const [layoutSaveState, setLayoutSaveState] = useState<"idle" | "loading" | "saving" | "saved" | "error">("loading");
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);
const [glowDistance, setGlowDistance] = useState(105);
const [usePortColors, setUsePortColors] = useState(false);
@@ -249,8 +277,7 @@ export function CatalogApp() {
const [fillOpacity, setFillOpacity] = useState(100);
const [strokeColor, setStrokeColor] = useState("#2b2b36");
const [strokeOpacity, setStrokeOpacity] = useState(100);
const [windowOpen, setWindowOpen] = useState(false);
const [sidePanelOpen, setSidePanelOpen] = useState(false);
const [inspectorOpen, setInspectorOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [shareAccessOpen, setShareAccessOpen] = useState(false);
@@ -268,6 +295,15 @@ export function CatalogApp() {
const [mediaError, setMediaError] = useState("");
const [mediaVisible, setMediaVisible] = useState(true);
const mediaObjectUrlRef = useRef<string | null>(null);
const pendingMediaFileRef = useRef<File | null>(null);
const [toolbarPlacement, setToolbarPlacement] = useState<ToolbarPlacement>("bottom");
const [toolbarBg, setToolbarBg] = useState("#111115");
const [toolbarBorder, setToolbarBorder] = useState("#111117");
const [toolbarOutline, setToolbarOutline] = useState("#1c1c1c");
const [toolbarMinSize, setToolbarMinSize] = useState(25);
const [toolbarMaxSize, setToolbarMaxSize] = useState(87);
const [toolbarLensCount, setToolbarLensCount] = useState(5);
const [toolbarAutoHide, setToolbarAutoHide] = useState(false);
const [shareEmail, setShareEmail] = useState("");
const [shareRole, setShareRole] = useState<ShareRole>("editor");
const [shareMessage, setShareMessage] = useState("");
@@ -297,7 +333,63 @@ export function CatalogApp() {
fieldOpacity: material.fieldOpacity / 100,
},
});
}, [accent, fieldMaterial, material.fieldOpacity, material.panelOpacity, panelMaterial, theme]);
document.documentElement.style.setProperty("--nodedc-canvas-soft", material.nestedHex);
}, [accent, fieldMaterial, material.fieldOpacity, material.nestedHex, material.panelOpacity, panelMaterial, theme]);
useEffect(() => {
let active = true;
fetch("/api/layout", { cache: "no-store" })
.then(async (response) => {
if (!response.ok) throw new Error("layout_load_failed");
return await response.json() as StoredLayout | null;
})
.then((stored) => {
if (!active || !stored) return;
if (stored.theme === "dark" || stored.theme === "light") setTheme(stored.theme);
if (stored.accentHex) setAccentHex(stored.accentHex);
if (stored.materialByTheme?.dark && stored.materialByTheme?.light) setMaterialByTheme(stored.materialByTheme);
const environment = stored.environment;
if (environment) {
if (environment.lightColor) setLightColor(environment.lightColor);
if (typeof environment.brightness === "number") setBrightness(environment.brightness);
if (typeof environment.glowDistance === "number") setGlowDistance(environment.glowDistance);
if (environment.connectionType) setConnectionType(environment.connectionType);
if (environment.connectionColor) setConnectionColor(environment.connectionColor);
if (typeof environment.usePortColors === "boolean") setUsePortColors(environment.usePortColors);
if (environment.fillColor) setFillColor(environment.fillColor);
if (typeof environment.fillOpacity === "number") setFillOpacity(environment.fillOpacity);
if (environment.strokeColor) setStrokeColor(environment.strokeColor);
if (typeof environment.strokeOpacity === "number") setStrokeOpacity(environment.strokeOpacity);
}
const media = stored.media;
if (media) {
if (media.source === "file" || media.source === "url") setMediaSource(media.source);
if (typeof media.url === "string") setMediaUrl(media.url);
if (media.fileName) setMediaFileName(media.fileName);
if (media.fileSrc) setFileMediaSrc(media.fileSrc);
if (typeof media.visible === "boolean") setMediaVisible(media.visible);
}
const toolbar = stored.toolbar;
if (toolbar) {
if (toolbar.placement === "left" || toolbar.placement === "right" || toolbar.placement === "bottom") setToolbarPlacement(toolbar.placement);
if (toolbar.background) setToolbarBg(toolbar.background);
if (toolbar.border) setToolbarBorder(toolbar.border);
if (toolbar.outline) setToolbarOutline(toolbar.outline);
if (typeof toolbar.minSize === "number") setToolbarMinSize(toolbar.minSize);
if (typeof toolbar.maxSize === "number") setToolbarMaxSize(toolbar.maxSize);
if (typeof toolbar.lensCount === "number") setToolbarLensCount(toolbar.lensCount);
if (typeof toolbar.autoHide === "boolean") setToolbarAutoHide(toolbar.autoHide);
}
setLayoutSaveState("saved");
})
.catch(() => {
if (active) setLayoutSaveState("error");
})
.finally(() => {
if (active) setLayoutSaveState((current) => current === "loading" ? "idle" : current);
});
return () => { active = false; };
}, []);
useEffect(() => () => {
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
@@ -313,6 +405,7 @@ export function CatalogApp() {
if (mediaObjectUrlRef.current) URL.revokeObjectURL(mediaObjectUrlRef.current);
const objectUrl = URL.createObjectURL(file);
mediaObjectUrlRef.current = objectUrl;
pendingMediaFileRef.current = file;
setFileMediaSrc(objectUrl);
setMediaFileName(file.name);
setMediaSource("file");
@@ -344,11 +437,28 @@ export function CatalogApp() {
}));
};
const saveEnvironmentLayout = () => {
const draft = {
const saveEnvironmentLayout = async () => {
setLayoutSaveState("saving");
try {
let persistedFileSrc = fileMediaSrc;
const pendingMedia = pendingMediaFileRef.current;
if (pendingMedia) {
const uploadResponse = await fetch("/api/layout/media", {
method: "PUT",
headers: { "content-type": "video/mp4", "x-file-name": encodeURIComponent(pendingMedia.name) },
body: pendingMedia,
});
if (!uploadResponse.ok) throw new Error("media_upload_failed");
const uploaded = await uploadResponse.json() as { url: string; fileName: string };
persistedFileSrc = uploaded.url;
setFileMediaSrc(uploaded.url);
setMediaFileName(uploaded.fileName);
pendingMediaFileRef.current = null;
}
const draft: StoredLayout = {
theme,
accentHex,
material,
materialByTheme,
environment: {
lightColor,
brightness,
@@ -361,13 +471,34 @@ export function CatalogApp() {
strokeColor,
strokeOpacity,
},
media: {
source: mediaSource,
url: mediaUrl,
fileName: mediaFileName,
fileSrc: persistedFileSrc,
visible: mediaVisible,
},
toolbar: {
placement: toolbarPlacement,
background: toolbarBg,
border: toolbarBorder,
outline: toolbarOutline,
minSize: toolbarMinSize,
maxSize: toolbarMaxSize,
lensCount: toolbarLensCount,
autoHide: toolbarAutoHide,
},
};
try {
window.localStorage.setItem("nodedc-design-guideline-draft", JSON.stringify(draft));
const response = await fetch("/api/layout", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(draft),
});
if (!response.ok) throw new Error("layout_save_failed");
setLayoutSaveState("saved");
} catch {
// The preview remains useful in restricted browser contexts even without local persistence.
setLayoutSaveState("error");
}
setEnvironmentLayoutSaved(true);
};
const environmentSections = [
@@ -389,6 +520,42 @@ export function CatalogApp() {
<ColorField label="Цвет полей ввода" value={material.fieldHex} onChange={(fieldHex) => updateMaterial({ fieldHex })} />
</ControlRow>
<RangeControl label="Прозрачность полей" value={material.fieldOpacity} min={18} max={100} formatValue={(value) => `${value}%`} onChange={(fieldOpacity) => updateMaterial({ fieldOpacity })} />
<ControlRow label="Вложенная область">
<ColorField label="Цвет вложенной области" value={material.nestedHex} onChange={(nestedHex) => updateMaterial({ nestedHex })} />
</ControlRow>
</>
),
},
{
id: "toolbar-settings",
label: "Рабочее поле — Toolbar",
tone: "accent" as const,
content: (
<>
<ControlRow label="Позиция">
<Select
variant="split"
label="Позиция Toolbar"
value={toolbarPlacement}
options={[
{ value: "left", label: "Слева" },
{ value: "right", label: "Справа" },
{ value: "bottom", label: "Снизу" },
]}
onChange={(value) => setToolbarPlacement(value as ToolbarPlacement)}
/>
</ControlRow>
<ControlRow label="Фон"><ColorField value={toolbarBg} onChange={setToolbarBg} /></ControlRow>
<ControlRow label="Border"><ColorField value={toolbarBorder} onChange={setToolbarBorder} /></ControlRow>
<ControlRow label="Outline"><ColorField value={toolbarOutline} onChange={setToolbarOutline} /></ControlRow>
<RangeControl label="Минимальный размер" value={toolbarMinSize} min={18} max={42} formatValue={(value) => `${value}px`} onChange={(value) => {
const next = Math.round(value);
setToolbarMinSize(next);
setToolbarMaxSize((current) => Math.max(current, next));
}} />
<RangeControl label="Максимальный размер" value={toolbarMaxSize} min={28} max={88} formatValue={(value) => `${value}px`} onChange={(value) => setToolbarMaxSize(Math.max(toolbarMinSize, Math.round(value)))} />
<RangeControl label="Линза" value={toolbarLensCount} min={1} max={13} step={2} formatValue={(value) => `${value} иконок`} onChange={(value) => setToolbarLensCount(Math.round(value) % 2 === 0 ? Math.round(value) + 1 : Math.round(value))} />
<Checker checked={toolbarAutoHide} label="Автоскрытие" onChange={setToolbarAutoHide} />
</>
),
},
@@ -446,36 +613,21 @@ export function CatalogApp() {
const renderSectionContent = () => {
switch (activeSection) {
case "foundation":
return (
<div className="catalog-grid catalog-grid--foundation">
<Preview title="Тема приложения" note={theme === "dark" ? "Launcher / dark" : "SEO / light"}>
<div className="catalog-theme-row">
<Button variant={theme === "dark" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("dark")}>Dark</Button>
<Button variant={theme === "light" ? "primary" : "secondary"} shape="pill" onClick={() => setTheme("light")}>Light</Button>
</div>
<p className="catalog-preview__explanation">Две базовые темы фиксированы по эталонам: чёрная Launcher/Hub, белая SEO. Стекло используется только в модальных окнах.</p>
</Preview>
<Preview title="Поверхности приложения" note="Launcher / SEO">
<div className="catalog-surface-stack">
<GlassSurface padding="md">Основная поверхность</GlassSurface>
<GlassSurface tone="strong" padding="md">Контентное окно</GlassSurface>
<GlassSurface tone="soft" padding="md">Вложенная область</GlassSurface>
</div>
</Preview>
<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>
</div>
);
case "controls":
return (
<div className="catalog-grid">
<Preview title="Inspector" note="modeless / draggable">
<p className="catalog-preview__explanation">Настройки приложения и Toolbar открываются в отдельном перемещаемом Inspector и сохраняются одним серверным layout.</p>
<Button variant="primary" shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть Inspector</Button>
</Preview>
<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>
<IconButton label="Развернуть"><Icon name="expand" /></IconButton>
<IconButton label="Закрыть"><Icon name="close" /></IconButton>
</div>
</Preview>
<Preview title="Действия" note="общая геометрия">
<div className="catalog-inline catalog-inline--wrap">
<Button variant="primary" shape="pill" icon={<Icon name="save" />}>Сохранить</Button>
@@ -524,17 +676,6 @@ export function CatalogApp() {
</Dropdown>
</div>
</Preview>
<Preview title="Environment controls" note="Engine / select + accordion" className="catalog-preview--wide">
<div className="catalog-environment-demo">
<GlassSurface tone="strong" radius="panel" className="catalog-environment-card">
<div className="catalog-environment-title">
<div><strong>Настройки окружения</strong><span>ENGINE / новый канон</span></div>
</div>
<Inspector sections={environmentSections} defaultOpen={["application-material"]} />
</GlassSurface>
<p className="catalog-preview__explanation">Environment controls единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет цвет плашек и полей приложения; весь layout сохраняется общей кнопкой в шапке окна.</p>
</div>
</Preview>
<Preview title="Статусы операций" note="не цвет приложения" className="catalog-preview--wide">
<p className="catalog-preview__explanation">Статус сообщает результат операции: нейтрально, успешно, требуется внимание или ошибка. Акцент приложения для этого не переиспользуется.</p>
<div className="catalog-inline catalog-inline--wrap">
@@ -552,7 +693,7 @@ export function CatalogApp() {
<SettingsCard
eyebrow="CONTENT"
title="Видео-окно"
description="Окно демо-видео и подключённый медиа-файл. Хранилище и загрузка остаются в приложении; геометрия и переключение источника — в библиотеке."
description="MP4 или URL становятся частью единого server-side layout вместе с темой, материалом и Toolbar."
actions={<Switch checked={mediaVisible} label="Показать" onChange={setMediaVisible} />}
>
<MediaSourceField
@@ -564,7 +705,7 @@ export function CatalogApp() {
previewKind="video"
accept="video/mp4,.mp4"
path="stage.videoSrc → runtime media source"
hint="Выбранный MP4 сразу подставляется в главное фоновое окно; backend-adapter может сохранить его в storage."
hint="Выбранный MP4 сразу показывается в stage и загружается на сервер общей кнопкой Save в шапке."
error={mediaError}
onSourceChange={setMediaSource}
onUrlChange={(nextUrl) => {
@@ -574,33 +715,13 @@ export function CatalogApp() {
onFileChange={handleStageMediaFile}
/>
</SettingsCard>
<Preview title="Граница ответственности" note="component / adapter" className="catalog-preview--wide">
<Preview title="Серверное состояние" note="layout / media" className="catalog-preview--wide">
<p className="catalog-preview__explanation">
`MediaSourceField` владеет пикселями, доступностью и состояниями file/URL. Конкретный проект передаёт upload-обработчик, storage-path и результат валидации поэтому компонент можно подключить к CMS, Launcher или будущему приложению без копирования его вёрстки.
Save записывает один layout: обе темы, текущий accent, материалы, Environment controls, Toolbar и выбранный media-source. Очистка browser cache больше не удаляет конфигурацию.
</p>
</Preview>
</div>
);
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="primary" shape="pill" icon={<Icon name="apps" />} onClick={() => setWindowOpen(true)}>Открыть окно</Button>
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setSidePanelOpen(true)}>Открыть боковую панель</Button>
</div>
</Preview>
<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>
<IconButton label="Развернуть"><Icon name="expand" /></IconButton>
<IconButton label="Закрыть"><Icon name="close" /></IconButton>
</div>
</Preview>
</div>
);
case "modals":
return (
<div className="catalog-modal-groups">
@@ -753,10 +874,19 @@ export function CatalogApp() {
description={activeDefinition.description}
expanded={panelExpanded}
onExpandedChange={workspace.setContentExpanded}
headerTools={
<SegmentedControl
label="Тема приложения"
value={theme}
items={[{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }]}
onChange={setTheme}
/>
}
utilityActions={[{
label: environmentLayoutSaved ? "Layout сохранён локально" : "Сохранить layout",
label: layoutSaveState === "saving" ? "Сохраняется на сервер" : layoutSaveState === "saved" ? "Layout сохранён на сервере" : layoutSaveState === "error" ? "Повторить сохранение layout" : "Сохранить layout на сервер",
icon: "save",
onClick: saveEnvironmentLayout,
onClick: () => { void saveEnvironmentLayout(); },
disabled: layoutSaveState === "saving" || layoutSaveState === "loading",
}]}
onClose={workspace.closeView}
>
@@ -765,36 +895,36 @@ export function CatalogApp() {
) : null}
/>
<Window
open={windowOpen}
title="Настройки сервиса"
subtitle="Launcher / CMS / SEO"
onClose={() => setWindowOpen(false)}
footer={
<>
<Button variant="ghost" onClick={() => setWindowOpen(false)}>Отмена</Button>
<WindowFooterActions><Button variant="primary" 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>
{guidelineOpen ? (
<Toolbar<CatalogSection>
placement={toolbarPlacement}
background={toolbarBg}
border={toolbarBorder}
outline={toolbarOutline}
accent={accentHex}
minSize={toolbarMinSize}
maxSize={toolbarMaxSize}
lensCount={toolbarLensCount}
autoHide={toolbarAutoHide}
items={(Object.entries(sectionDefinitions) as Array<[CatalogSection, (typeof sectionDefinitions)[CatalogSection]]>).map(([id, item]) => ({
id,
label: item.title,
icon: item.icon,
active: activeSection === id,
onSelect: openSection,
}))}
/>
) : null}
<Window
open={sidePanelOpen}
title="Боковая панель"
subtitle="modeless / shell contract"
open={inspectorOpen}
title="Настройки окружения"
subtitle="ENGINE / draggable inspector"
placement="end"
onClose={() => setSidePanelOpen(false)}
draggable
onClose={() => setInspectorOpen(false)}
>
<div className="catalog-form">
<TextField label="Название панели" value="Параметры рабочей области" readOnly />
<p className="catalog-preview__explanation">Боковая панель демонстрирует только layer-механику: она modeless, не затемняет stage и не забирает фокус. Environment controls живут в разделе «Контролы».</p>
</div>
<Inspector sections={environmentSections} defaultOpen={["application-material"]} />
</Window>
<Window
+1 -141
View File
@@ -110,10 +110,6 @@ textarea {
gap: 1rem;
}
.catalog-grid--foundation {
grid-template-columns: 1.1fr 0.9fr;
}
.catalog-grid--single {
grid-template-columns: 1fr;
}
@@ -160,7 +156,6 @@ textarea {
line-height: 1.5;
}
.catalog-theme-row,
.catalog-inline,
.catalog-window-actions-demo {
display: flex;
@@ -173,37 +168,6 @@ textarea {
flex-wrap: wrap;
}
.catalog-swatches {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
}
.catalog-swatch {
display: grid;
min-width: 0;
gap: 0.4rem;
border: 0;
border-radius: 0.9rem;
background: var(--nodedc-glass-control-bg);
color: var(--nodedc-text-muted);
padding: 0.55rem;
font-size: 0.68rem;
cursor: pointer;
}
.catalog-swatch span {
height: 2.4rem;
border-radius: 0.65rem;
background: var(--swatch);
}
.catalog-swatch[data-active="true"] {
background: var(--nodedc-glass-control-hover);
color: var(--nodedc-text-primary);
}
.catalog-surface-stack,
.catalog-form {
display: grid;
gap: 0.75rem;
@@ -313,14 +277,6 @@ textarea {
flex-basis: 2rem;
}
.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%);
@@ -328,80 +284,6 @@ textarea {
gap: 0.75rem;
}
.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: var(--nodedc-canvas-soft);
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: var(--nodedc-glass-control-hover) !important;
}
.catalog-shell-diagram__content {
background: var(--nodedc-glass-control-bg) !important;
}
.catalog-environment-demo {
display: grid;
min-height: 30rem;
grid-template-columns: minmax(0, 24.375rem) minmax(14rem, 1fr);
align-items: start;
gap: clamp(1rem, 4vw, 4rem);
border-radius: var(--nodedc-radius-card);
background: var(--nodedc-canvas-soft);
padding: clamp(1rem, 3vw, 2rem);
}
.catalog-environment-card {
width: min(24.375rem, 100%);
padding: 1.125rem 0.75rem;
}
.catalog-environment-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0 0.875rem 0.875rem;
}
.catalog-environment-title > div {
display: grid;
gap: 0.3rem;
}
.catalog-environment-title span {
color: var(--nodedc-text-muted);
font-size: 0.7rem;
}
.catalog-environment-draft {
margin: -0.25rem 0.875rem 0.75rem;
color: var(--nodedc-text-muted);
font-size: 0.68rem;
line-height: 1.4;
}
.catalog-icon-catalog {
display: grid;
gap: 1rem;
@@ -489,8 +371,7 @@ textarea {
}
@media (max-width: 1280px) {
.catalog-grid,
.catalog-grid--foundation {
.catalog-grid {
grid-template-columns: 1fr;
}
@@ -557,7 +438,6 @@ textarea {
gap: 0.25rem;
}
.catalog-swatches,
.catalog-icon-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -566,32 +446,12 @@ textarea {
grid-template-columns: 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-environment-demo {
min-height: auto;
grid-template-columns: 1fr;
padding: 0.75rem;
}
.catalog-window-actions-demo .nodedc-button {
width: 100%;
}
}
@media (max-width: 430px) {
.catalog-swatches,
.catalog-icon-grid {
grid-template-columns: 1fr;
}