diff --git a/apps/catalog/index.html b/apps/catalog/index.html index 84d4f2f..843b3aa 100644 --- a/apps/catalog/index.html +++ b/apps/catalog/index.html @@ -7,7 +7,7 @@ - + NODE.DC UI Catalog diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index a974a4b..0f696b3 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { applyGlassMaterial, applyNodedcTheme, defaultGlassMaterial, type GlassMaterialSettings, type NodedcTheme, type RgbTuple } from "@nodedc/ui-core"; import { createTemplateFeatures, getPageTemplate, pageTemplates, type PageTemplateDefinition } from "@nodedc/page-patterns"; import { @@ -37,6 +37,8 @@ import { Switch, TextAreaField, TextField, + ToastCard, + ToastStack, Toolbar, UserProfileMenu, useApplicationWorkspace, @@ -45,6 +47,8 @@ import { WorkspaceWindow, type IconName, type ShareAccessMember, + type ToastItem, + type ToastTone, type ToolbarPlacement, type WorkspaceWindowRect, } from "@nodedc/ui-react"; @@ -60,15 +64,38 @@ import { type DesignProfileSummary, type DesignProfileStatus, } from "./applicationManifest.js"; -import { MapFixturePreview, type MapFixturePreviewHandle, type MapPageLayout } from "./MapFixturePreview.js"; +import { createDefaultMapPageLayout, MapFixturePreview, type MapFixturePreviewHandle, type MapPageLayout } from "./MapFixturePreview.js"; +import { + mapDesignFragmentForLayout, + mapDesignFragmentFromLayout, + mapDesignOverridesFromResolved, + mapDesignProfileKey, + resolveMapDesignLayout, + type MaterialDraft, + type StoredLayout, +} from "./designProfile.js"; import { FoundrySettingsModal } from "./FoundrySettingsModal.js"; -type CatalogSection = "controls" | "media" | "glass" | "modals" | "icons"; +type CatalogSection = "controls" | "media" | "glass" | "status" | "modals" | "icons"; type StudioContext = "visual" | "pages" | "applications"; type StudioView = CatalogSection | `page-template:${string}` | `application:${string}` | `application:${string}/page:${string}`; +type DesignProfileSaveScope = { kind: "global" } | { kind: "page"; templateId: "map"; templateVersion: string }; +type DesignProfileDocument = { + id: string; + name: string; + version: string; + status: DesignProfileStatus; + layout: StoredLayout; + timestamps: { updatedAt: string; publishedAt?: string }; +}; const pageTemplateViewId = (id: string) => `page-template:${id}` as const; const pageTemplateIdFromView = (view: string | null) => view?.startsWith("page-template:") ? view.slice("page-template:".length) : null; +const designProfileDocumentPath = (reference: { id: string; version: string; status: DesignProfileStatus }) => ( + reference.status === "published" + ? `/api/design-profiles/${reference.id}/versions/${reference.version}` + : `/api/design-profiles/${reference.id}` +); const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [ { label: "NODE.DC", value: [255, 47, 146], hex: "#ff2f92" }, @@ -77,56 +104,6 @@ const accents: Array<{ label: string; value: RgbTuple; hex: string }> = [ { label: "Magenta", value: [215, 70, 255], hex: "#d746ff" }, ]; -type MaterialDraft = { - panelHex: string; - panelOpacity: number; - fieldHex: string; - fieldOpacity: number; - nestedHex: string; -}; - -interface StoredLayout { - theme?: NodedcTheme; - accentHex?: string; - materialByTheme?: Record; - 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; - logoSource?: "file" | "url"; - logoUrl?: string; - logoFileName?: string; - logoFileSrc?: string; - faviconFileName?: string; - faviconAssets?: FaviconAssetUrls; - }; - glass?: GlassMaterialSettings; - toolbar?: { - placement?: ToolbarPlacement; - background?: string; - border?: string; - outline?: string; - minSize?: number; - maxSize?: number; - lensCount?: number; - autoHide?: boolean; - }; -} - interface FoundrySessionProfile { user: { id: string; @@ -198,14 +175,20 @@ const sectionDefinitions: Record = [ { title: "Сущности", note: "Общий словарь", - icons: ["profile", "users", "building", "globe", "database", "network", "inbox", "mail"], + icons: ["profile", "users", "building", "globe", "target", "database", "network", "inbox", "mail"], }, { title: "Контент", @@ -276,6 +259,7 @@ const iconLabels: Record = { minimize: "Свернуть", network: "Связи", panel: "Панель", + target: "Таргеты", plus: "Добавить", profile: "Профиль", refresh: "Обновить", @@ -328,9 +312,13 @@ export function CatalogApp() { const [studioContext, setStudioContext] = useState("visual"); const [applicationSummaries, setApplicationSummaries] = useState([]); const [designProfiles, setDesignProfiles] = useState([]); + const [activeDesignProfileLayout, setActiveDesignProfileLayout] = useState(null); + const [applicationDesignProfileLayout, setApplicationDesignProfileLayout] = useState(null); const [applicationDraft, setApplicationDraft] = useState(null); const [applicationSaveState, setApplicationSaveState] = useState("idle"); const [applicationError, setApplicationError] = useState(""); + const [toasts, setToasts] = useState([]); + const toastSequenceRef = useRef(0); const [sessionProfile, setSessionProfile] = useState(null); const [platformSettingsOpen, setPlatformSettingsOpen] = useState(false); const [foundrySettingsOpen, setFoundrySettingsOpen] = useState(false); @@ -351,6 +339,7 @@ export function CatalogApp() { const [applicationMode, setApplicationMode] = useState<"edit" | "preview">("edit"); const [designProfileSaveOpen, setDesignProfileSaveOpen] = useState(false); const [designProfileSaveMode, setDesignProfileSaveMode] = useState<"save" | "save-as">("save"); + const [designProfileSaveScope, setDesignProfileSaveScope] = useState({ kind: "global" }); const [designProfileName, setDesignProfileName] = useState(""); const [designProfilePublishState, setDesignProfilePublishState] = useState<"idle" | "publishing" | "published" | "error">("idle"); const [activeDesignProfileId, setActiveDesignProfileId] = useState("default"); @@ -433,6 +422,64 @@ export function CatalogApp() { { id: "editor", name: "Maria Petrova", email: "maria@nodedc.ru", role: "editor", roleLabel: "Редактор" }, ]); + const dismissToast = useCallback((id: string) => { + setToasts((current) => current.filter((item) => item.id !== id)); + }, []); + + const pushToast = useCallback((tone: ToastTone, title: string, description?: string, durationMs?: number | null) => { + toastSequenceRef.current += 1; + const id = `foundry-toast-${toastSequenceRef.current}`; + setToasts((current) => [...current.slice(-3), { id, tone, title, description, durationMs }]); + return id; + }, []); + + const updateToast = useCallback((id: string, patch: Partial>) => { + setToasts((current) => current.map((item) => item.id === id ? { ...item, ...patch } : item)); + }, []); + + const applyDesignProfileLayout = useCallback((stored: StoredLayout) => { + if (stored.theme === "dark" || stored.theme === "light") setTheme(stored.theme); + if (stored.accentHex) setAccentHex(stored.accentHex); + if (stored.materialByTheme) setMaterialByTheme((current) => ({ + dark: { ...current.dark, ...stored.materialByTheme?.dark }, + light: { ...current.light, ...stored.materialByTheme?.light }, + })); + if (stored.environment) { + if (stored.environment.lightColor) setLightColor(stored.environment.lightColor); + if (typeof stored.environment.brightness === "number") setBrightness(stored.environment.brightness); + if (typeof stored.environment.glowDistance === "number") setGlowDistance(stored.environment.glowDistance); + if (stored.environment.connectionType) setConnectionType(stored.environment.connectionType); + if (stored.environment.connectionColor) setConnectionColor(stored.environment.connectionColor); + if (typeof stored.environment.usePortColors === "boolean") setUsePortColors(stored.environment.usePortColors); + if (stored.environment.fillColor) setFillColor(stored.environment.fillColor); + if (typeof stored.environment.fillOpacity === "number") setFillOpacity(stored.environment.fillOpacity); + if (stored.environment.strokeColor) setStrokeColor(stored.environment.strokeColor); + if (typeof stored.environment.strokeOpacity === "number") setStrokeOpacity(stored.environment.strokeOpacity); + } + if (stored.media) { + if (stored.media.source === "file" || stored.media.source === "url") setMediaSource(stored.media.source); + if (typeof stored.media.url === "string") setMediaUrl(stored.media.url); + if (stored.media.fileName) setMediaFileName(stored.media.fileName); + if (stored.media.fileSrc) setFileMediaSrc(stored.media.fileSrc); + if (typeof stored.media.visible === "boolean") setMediaVisible(stored.media.visible); + if (stored.media.logoSource === "file" || stored.media.logoSource === "url") setLogoSource(stored.media.logoSource); + if (typeof stored.media.logoUrl === "string") setLogoUrl(stored.media.logoUrl); + if (stored.media.logoFileName) setLogoFileName(stored.media.logoFileName); + if (stored.media.logoFileSrc) setLogoFileSrc(stored.media.logoFileSrc); + if (stored.media.faviconFileName) setFaviconFileName(stored.media.faviconFileName); + if (stored.media.faviconAssets) setFaviconAssets(stored.media.faviconAssets); + } + if (stored.glass) setGlassMaterial({ ...defaultGlassMaterial, ...stored.glass }); + if (stored.toolbar?.placement) setToolbarPlacement(stored.toolbar.placement); + if (stored.toolbar?.background) setToolbarBg(stored.toolbar.background); + if (stored.toolbar?.border) setToolbarBorder(stored.toolbar.border); + if (stored.toolbar?.outline) setToolbarOutline(stored.toolbar.outline); + if (typeof stored.toolbar?.minSize === "number") setToolbarMinSize(stored.toolbar.minSize); + if (typeof stored.toolbar?.maxSize === "number") setToolbarMaxSize(stored.toolbar.maxSize); + if (typeof stored.toolbar?.lensCount === "number") setToolbarLensCount(stored.toolbar.lensCount); + if (typeof stored.toolbar?.autoHide === "boolean") setToolbarAutoHide(stored.toolbar.autoHide); + }, []); + const externalMediaSrc = /^(https?:)?\/\//i.test(mediaUrl) && /\.(mp4|webm|mov|m4v)(\?.*)?$/i.test(mediaUrl) ? mediaUrl : null; @@ -640,6 +687,26 @@ export function CatalogApp() { return () => { active = false; }; }, []); + useEffect(() => { + let active = true; + fetch(`/api/design-profiles/${activeDesignProfileId}`, { cache: "no-store" }) + .then(async (response) => { + if (!response.ok) throw new Error("design_profile_load_failed"); + return await response.json() as DesignProfileDocument; + }) + .then((profile) => { + if (!active) return; + const stored = profile.layout; + setActiveDesignProfileLayout(stored); + applyDesignProfileLayout(stored); + setLayoutSaveState("saved"); + }) + .catch(() => { + if (active) setLayoutSaveState("error"); + }); + return () => { active = false; }; + }, [activeDesignProfileId, applyDesignProfileLayout]); + useEffect(() => { let active = true; fetch("/api/applications", { cache: "no-store" }) @@ -734,21 +801,8 @@ export function CatalogApp() { workspace.openView(next as CatalogSection); }; - const openDesignProfile = async (id: string) => { + const openDesignProfile = (id: string) => { setActiveDesignProfileId(id); - const response = await fetch(`/api/design-profiles/${id}`, { cache: "no-store" }); - if (!response.ok) return setLayoutSaveState("error"); - const profile = await response.json() as { layout: StoredLayout }; - const stored = profile.layout; - if (stored.theme === "dark" || stored.theme === "light") setTheme(stored.theme); - if (stored.accentHex) setAccentHex(stored.accentHex); - if (stored.materialByTheme) setMaterialByTheme((current) => ({ dark: { ...current.dark, ...stored.materialByTheme?.dark }, light: { ...current.light, ...stored.materialByTheme?.light } })); - if (stored.glass) setGlassMaterial({ ...defaultGlassMaterial, ...stored.glass }); - if (stored.toolbar?.placement) setToolbarPlacement(stored.toolbar.placement); - if (stored.toolbar?.background) setToolbarBg(stored.toolbar.background); - if (stored.toolbar?.border) setToolbarBorder(stored.toolbar.border); - if (stored.toolbar?.outline) setToolbarOutline(stored.toolbar.outline); - setLayoutSaveState("saved"); }; const openApplicationDraft = async (id: string) => { @@ -760,16 +814,47 @@ export function CatalogApp() { const response = await fetch(`/api/applications/${id}`, { cache: "no-store" }); if (!response.ok) throw new Error("application_load_failed"); const manifest = await response.json() as ApplicationManifestV01; + const profileResponse = await fetch(designProfileDocumentPath(manifest.designProfile), { cache: "no-store" }); + if (!profileResponse.ok) throw new Error("application_design_profile_load_failed"); + const profile = await profileResponse.json() as DesignProfileDocument; setApplicationDraft(manifest); - setTheme(manifest.designProfile.theme); + setApplicationDesignProfileLayout(profile.layout); + applyDesignProfileLayout(profile.layout); setApplicationSaveState("saved"); } catch { setApplicationDraft(null); + setApplicationDesignProfileLayout(null); setApplicationSaveState("error"); setApplicationError("Не удалось открыть Application Draft."); } }; + const selectApplicationDesignProfile = async (reference: string) => { + if (!applicationDraft) return; + const [profileId, version, statusValue] = reference.split("@"); + const profile = designProfiles.find((item) => item.id === profileId); + if (!profile) return; + const status: DesignProfileStatus = statusValue === "published" ? "published" : "draft"; + const release = status === "published" ? profile.versions.find((item) => item.version === version) : undefined; + const profileTheme = release?.theme ?? profile.theme; + setApplicationSaveState("loading"); + setApplicationError(""); + try { + const response = await fetch(designProfileDocumentPath({ id: profile.id, version, status }), { cache: "no-store" }); + if (!response.ok) throw new Error("design_profile_load_failed"); + const document = await response.json() as DesignProfileDocument; + setApplicationDesignProfileLayout(document.layout); + applyDesignProfileLayout(document.layout); + updateApplicationDraft((current) => ({ + ...current, + designProfile: { id: profile.id, version, status, theme: profileTheme }, + })); + } catch { + setApplicationSaveState("error"); + setApplicationError("Не удалось применить выбранный Design Profile."); + } + }; + const openPageTemplate = (id: string) => { setStudioContext("pages"); workspace.openView(pageTemplateViewId(id)); @@ -794,7 +879,12 @@ export function CatalogApp() { }); if (!response.ok) throw new Error("application_create_failed"); const manifest = await response.json() as ApplicationManifestV01; + const profileResponse = await fetch(designProfileDocumentPath(manifest.designProfile), { cache: "no-store" }); + if (!profileResponse.ok) throw new Error("application_design_profile_load_failed"); + const profile = await profileResponse.json() as DesignProfileDocument; setApplicationDraft(manifest); + setApplicationDesignProfileLayout(profile.layout); + applyDesignProfileLayout(profile.layout); setApplicationSummaries((current) => [ { id: manifest.id, @@ -838,12 +928,27 @@ export function CatalogApp() { const saveApplicationDraft = async () => { if (!applicationDraft) return; + const toastId = pushToast("loading", "Сохраняем Application", applicationDraft.metadata.name, null); const activeMapLayout = activeApplicationPageId ? applicationMapPreviewRef.current?.getLayout() : null; + const activePage = activeApplicationPageId + ? applicationDraft.pages.find((page) => page.id === activeApplicationPageId) + : undefined; + const activeMapFragment = activePage?.template.id === "map" + ? mapDesignFragmentForLayout(applicationDesignProfileLayout, activePage.template.version) + : null; + const activeMapOverrides = activeMapLayout && activeMapFragment + ? mapDesignOverridesFromResolved(activeMapLayout, activeMapFragment) + : undefined; const draftToSave = activeMapLayout && activeApplicationPageId ? { ...applicationDraft, pages: applicationDraft.pages.map((page) => page.id === activeApplicationPageId ? { ...page, layout: { ...page.layout, map: activeMapLayout }, + ...(activeMapFragment ? { + designOverrides: activeMapOverrides + ? { ...page.designOverrides, map: activeMapOverrides } + : undefined, + } : {}), } : page), } : applicationDraft; setApplicationSaveState("saving"); @@ -871,31 +976,20 @@ export function CatalogApp() { ...current.filter((item) => item.id !== saved.id), ]); setApplicationSaveState("saved"); + updateToast(toastId, { tone: "success", title: "Application сохранён", description: saved.metadata.name, durationMs: 4200 }); } catch { setApplicationSaveState("error"); setApplicationError("Draft не сохранён. Проверьте название и slug."); + updateToast(toastId, { tone: "error", title: "Application не сохранён", description: "Проверьте manifest и повторите попытку.", durationMs: 6500 }); } }; - const saveMapPageTemplate = async () => { - const layout = mapTemplatePreviewRef.current?.getLayout(); - if (!layout) { - setMapTemplateSaveState("error"); - return; - } - setMapTemplateSaveState("saving"); - try { - const response = await fetch("/api/page-layouts/map", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify(layout), - }); - if (!response.ok) throw new Error("map_page_layout_save_failed"); - setMapTemplateLayout(await response.json() as MapPageLayout); - setMapTemplateSaveState("saved"); - } catch { - setMapTemplateSaveState("error"); - } + const openMapPageDesignSave = (templateVersion: string) => { + const currentProfile = designProfiles.find((profile) => profile.id === activeDesignProfileId); + setDesignProfileName(currentProfile?.name ?? ""); + setDesignProfileSaveScope({ kind: "page", templateId: "map", templateVersion }); + setDesignProfileSaveMode("save"); + setDesignProfileSaveOpen(true); }; const changeStudioContext = (next: StudioContext) => { @@ -1029,45 +1123,88 @@ export function CatalogApp() { }; const saveDesignProfile = async (mode = designProfileSaveMode) => { - const layoutSaved = await saveEnvironmentLayout(); - if (!layoutSaved) return; - const layoutResponse = await fetch("/api/layout", { cache: "no-store" }); - if (!layoutResponse.ok) return setLayoutSaveState("error"); - const layout = await layoutResponse.json() as StoredLayout; - const creating = mode === "save-as"; const selected = designProfiles.find((profile) => profile.id === activeDesignProfileId); - const response = await fetch(creating ? "/api/design-profiles" : `/api/design-profiles/${activeDesignProfileId}`, { - method: creating ? "POST" : "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ name: creating ? designProfileName : selected?.name, layout }), - }); - if (!response.ok) return setLayoutSaveState("error"); - const saved = await response.json() as { id: string; name: string; version: string; status: DesignProfileStatus; layout: StoredLayout; timestamps: { updatedAt: string } }; - const previous = designProfiles.find((profile) => profile.id === saved.id); - const summary: DesignProfileSummary = { - id: saved.id, - name: saved.name, - version: saved.version, - status: saved.status, - theme: saved.layout.theme === "light" ? "light" : "dark", - updatedAt: saved.timestamps.updatedAt, - versions: previous?.versions ?? [], - latestPublishedVersion: previous?.latestPublishedVersion ?? null, - }; - setDesignProfiles((current) => [...current.filter((profile) => profile.id !== summary.id), summary].sort((left, right) => left.name.localeCompare(right.name))); - setActiveDesignProfileId(summary.id); - setDesignProfileSaveOpen(false); - setDesignProfileName(""); - setLayoutSaveState("saved"); + if (!selected) return; + const toastId = pushToast( + "loading", + designProfileSaveScope.kind === "page" ? "Сохраняем дизайн страницы" : "Сохраняем Design Profile", + designProfileSaveScope.kind === "page" ? `Map@${designProfileSaveScope.templateVersion} · ${selected.name}` : selected.name, + null, + ); + if (designProfileSaveScope.kind === "page") setMapTemplateSaveState("saving"); + else setLayoutSaveState("saving"); + try { + const currentResponse = await fetch(`/api/design-profiles/${activeDesignProfileId}`, { cache: "no-store" }); + if (!currentResponse.ok) throw new Error("design_profile_load_failed"); + const currentProfile = await currentResponse.json() as DesignProfileDocument; + let layout: StoredLayout; + if (designProfileSaveScope.kind === "page") { + const pageLayout = mapTemplatePreviewRef.current?.getLayout(); + if (!pageLayout) throw new Error("map_page_layout_unavailable"); + const pageKey = mapDesignProfileKey(designProfileSaveScope.templateVersion); + layout = { + ...currentProfile.layout, + pageTypes: { + ...currentProfile.layout.pageTypes, + [pageKey]: mapDesignFragmentFromLayout(pageLayout, designProfileSaveScope.templateVersion), + }, + }; + } else { + const layoutSaved = await saveEnvironmentLayout(); + if (!layoutSaved) throw new Error("global_layout_save_failed"); + const layoutResponse = await fetch("/api/layout", { cache: "no-store" }); + if (!layoutResponse.ok) throw new Error("global_layout_load_failed"); + const globalLayout = await layoutResponse.json() as StoredLayout; + layout = { ...globalLayout, pageTypes: currentProfile.layout.pageTypes ?? {} }; + } + const creating = mode === "save-as"; + const response = await fetch(creating ? "/api/design-profiles" : `/api/design-profiles/${activeDesignProfileId}`, { + method: creating ? "POST" : "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: creating ? designProfileName : selected.name, layout }), + }); + if (!response.ok) throw new Error("design_profile_save_failed"); + const saved = await response.json() as DesignProfileDocument; + const previous = designProfiles.find((profile) => profile.id === saved.id); + const summary: DesignProfileSummary = { + id: saved.id, + name: saved.name, + version: saved.version, + status: saved.status, + theme: saved.layout.theme === "light" ? "light" : "dark", + updatedAt: saved.timestamps.updatedAt, + versions: previous?.versions ?? [], + latestPublishedVersion: previous?.latestPublishedVersion ?? null, + }; + setDesignProfiles((current) => [...current.filter((profile) => profile.id !== summary.id), summary].sort((left, right) => left.name.localeCompare(right.name))); + setActiveDesignProfileId(summary.id); + setActiveDesignProfileLayout(saved.layout); + setDesignProfileSaveOpen(false); + setDesignProfileName(""); + setLayoutSaveState("saved"); + if (designProfileSaveScope.kind === "page") setMapTemplateSaveState("saved"); + updateToast(toastId, { + tone: "success", + title: mode === "save-as" ? "Design Profile создан" : "Design Profile обновлён", + description: `${saved.name} · v${saved.version}`, + durationMs: 4200, + }); + } catch { + if (designProfileSaveScope.kind === "page") setMapTemplateSaveState("error"); + else setLayoutSaveState("error"); + updateToast(toastId, { tone: "error", title: "Design Profile не сохранён", description: "Текущий профиль не изменён.", durationMs: 6500 }); + } }; const publishDesignProfile = async () => { const current = designProfiles.find((profile) => profile.id === activeDesignProfileId); if (!current || current.versions.some((release) => release.version === current.version)) return; + const toastId = pushToast("loading", "Публикуем Design Profile", `${current.name} · v${current.version}`, null); setDesignProfilePublishState("publishing"); const response = await fetch(`/api/design-profiles/${current.id}/publish`, { method: "POST" }); if (!response.ok) { setDesignProfilePublishState("error"); + updateToast(toastId, { tone: "error", title: "Публикация не выполнена", description: "Draft остался доступен без изменений.", durationMs: 6500 }); return; } const release = await response.json() as { version: string; status: DesignProfileStatus; layout: StoredLayout; timestamps: { publishedAt?: string } }; @@ -1078,6 +1215,7 @@ export function CatalogApp() { })); setDesignProfilePublishState("published"); setDesignProfileSaveOpen(false); + updateToast(toastId, { tone: "success", title: "Design Profile опубликован", description: `${current.name} · v${release.version}`, durationMs: 4200 }); }; const addPageTemplateToApplication = (template: PageTemplateDefinition) => { @@ -1448,6 +1586,33 @@ export function CatalogApp() { ); + case "status": + return ( +
+ +
+ + + + + +
+
+ +
+ + + + + +
+
+
+ ); case "modals": return (
@@ -1468,7 +1633,7 @@ export function CatalogApp() {

Обновляет текущий preset или создаёт новый именованный профиль.

- +
@@ -1606,16 +1771,7 @@ export function CatalogApp() { label="Design Profile" value={designProfileOptions.some((option) => option.value === selectedProfileValue) ? selectedProfileValue : `${selectedProfile?.id ?? "default"}@${selectedProfile?.version ?? "0.6.0"}@draft`} options={designProfileOptions} - onChange={(profileReference) => { - const [profileId, version, statusValue] = profileReference.split("@"); - const profile = designProfiles.find((item) => item.id === profileId); - if (!profile) return; - const status: DesignProfileStatus = statusValue === "published" ? "published" : "draft"; - const release = status === "published" ? profile.versions.find((item) => item.version === version) : undefined; - const profileTheme = release?.theme ?? profile.theme; - setTheme(profileTheme); - updateApplicationDraft((current) => ({ ...current, designProfile: { id: profile.id, version, status, theme: profileTheme } })); - }} + onChange={(profileReference) => { void selectApplicationDesignProfile(profileReference); }} /> @@ -1682,7 +1838,6 @@ export function CatalogApp() {
{applicationSaveState === "saving" ? "Сохранение…" : applicationSaveState === "saved" ? "Конфигурация сохранена" : applicationSaveState === "error" ? "Ошибка сохранения" : "Есть несохранённые изменения"} -
{applicationError ?

{applicationError}

: null} @@ -1690,14 +1845,20 @@ export function CatalogApp() { ); }; - const renderPageTemplate = (template: PageTemplateDefinition) => ( - template.id === "map" ? ( + const renderPageTemplate = (template: PageTemplateDefinition) => { + const mapProfileFragment = template.id === "map" + ? mapDesignFragmentForLayout(activeDesignProfileLayout, template.version) + : null; + const mapDesignLayout = template.id === "map" + ? resolveMapDesignLayout(mapTemplateLayout ?? createDefaultMapPageLayout(true), mapProfileFragment) + : null; + return template.id === "map" ? (
profile.id === activeDesignProfileId)?.version ?? "draft"}`} ref={mapTemplatePreviewRef} expanded - initialLayout={mapTemplateLayout} + initialLayout={mapDesignLayout} features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))} />
@@ -1736,8 +1897,8 @@ export function CatalogApp() {
- ) - ); + ); + }; const renderApplicationPage = () => { if (!applicationDraft || !activeApplicationPageId) return null; @@ -1745,6 +1906,12 @@ export function CatalogApp() { if (!page) return null; const template = getPageTemplate(page.template.id, page.template.version); if (!template) return null; + const mapProfileFragment = page.template.id === "map" + ? mapDesignFragmentForLayout(applicationDesignProfileLayout, page.template.version) + : null; + const resolvedMapLayout = page.template.id === "map" + ? resolveMapDesignLayout(page.layout?.map ?? createDefaultMapPageLayout(true), mapProfileFragment, page.designOverrides?.map) + : null; const setFeature = (featureId: string, value: boolean) => updateApplicationDraft((current) => ({ ...current, pages: current.pages.map((item) => item.id === page.id ? { ...item, features: { ...item.features, [featureId]: value } } : item), @@ -1752,11 +1919,11 @@ export function CatalogApp() { return (
@@ -1866,7 +2033,7 @@ export function CatalogApp() { emptyLabel="Модули не созданы" onChange={(id) => { void openApplicationDraft(id); }} /> - ) : studioContext === "visual" ? ( + ) : studioContext === "visual" || studioContext === "pages" ? (