From 1f2dffc031bbf0efdf2cd0ea0cf2e550b71a2408 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 11 Jul 2026 22:34:01 +0300 Subject: [PATCH] Add versioned design profile releases --- apps/catalog/src/CatalogApp.tsx | 70 ++++- apps/catalog/src/applicationManifest.ts | 12 + docs/MODULE_STUDIO.md | 13 +- registry/registry.json | 1 + .../application-manifest-v0.1.schema.json | 3 +- .../schemas/design-profile-v0.1.schema.json | 134 ++++++++++ scripts/validate-registry.mjs | 6 + server/catalog-server.mjs | 251 +++++++++++++++++- 8 files changed, 473 insertions(+), 17 deletions(-) create mode 100644 registry/schemas/design-profile-v0.1.schema.json diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx index f9e7e08..3c240e0 100644 --- a/apps/catalog/src/CatalogApp.tsx +++ b/apps/catalog/src/CatalogApp.tsx @@ -56,6 +56,7 @@ import { type ApplicationManifestV01, type ApplicationSummary, type DesignProfileSummary, + type DesignProfileStatus, } from "./applicationManifest.js"; type CatalogSection = "controls" | "media" | "glass" | "modals" | "icons"; @@ -315,6 +316,7 @@ export function CatalogApp() { const [designProfileSaveOpen, setDesignProfileSaveOpen] = useState(false); const [designProfileSaveMode, setDesignProfileSaveMode] = useState<"save" | "save-as">("save"); const [designProfileName, setDesignProfileName] = useState(""); + const [designProfilePublishState, setDesignProfilePublishState] = useState<"idle" | "publishing" | "published" | "error">("idle"); const [activeDesignProfileId, setActiveDesignProfileId] = useState("default"); const workspace = useApplicationWorkspace(); const { navigationOpen: guidelineOpen, activeView, contentExpanded: panelExpanded } = workspace; @@ -885,8 +887,18 @@ export function CatalogApp() { 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; layout: StoredLayout; timestamps: { updatedAt: string } }; - const summary: DesignProfileSummary = { id: saved.id, name: saved.name, version: saved.version, theme: saved.layout.theme === "light" ? "light" : "dark", updatedAt: saved.timestamps.updatedAt }; + 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); @@ -894,6 +906,25 @@ export function CatalogApp() { setLayoutSaveState("saved"); }; + const publishDesignProfile = async () => { + const current = designProfiles.find((profile) => profile.id === activeDesignProfileId); + if (!current || current.versions.some((release) => release.version === current.version)) return; + setDesignProfilePublishState("publishing"); + const response = await fetch(`/api/design-profiles/${current.id}/publish`, { method: "POST" }); + if (!response.ok) { + setDesignProfilePublishState("error"); + return; + } + const release = await response.json() as { version: string; status: DesignProfileStatus; layout: StoredLayout; timestamps: { publishedAt?: string } }; + setDesignProfiles((profiles) => profiles.map((profile) => profile.id !== current.id ? profile : { + ...profile, + versions: [{ version: release.version, status: release.status, theme: release.layout.theme === "light" ? "light" : "dark", publishedAt: release.timestamps.publishedAt }, ...profile.versions.filter((item) => item.version !== release.version)], + latestPublishedVersion: release.version, + })); + setDesignProfilePublishState("published"); + setDesignProfileSaveOpen(false); + }; + const addPageTemplateToApplication = (template: PageTemplateDefinition) => { updateApplicationDraft((current) => { const order = current.pages.length; @@ -1312,6 +1343,19 @@ export function CatalogApp() { const renderApplicationDraft = () => { if (!applicationDraft || applicationDraft.id !== activeApplicationId) return null; const selectedProfile = designProfiles.find((profile) => profile.id === applicationDraft.designProfile.id) ?? designProfiles[0]; + const designProfileOptions = designProfiles.flatMap((profile) => [ + ...profile.versions.map((release) => ({ + value: `${profile.id}@${release.version}@published`, + label: profile.name, + description: `${release.version} · Published · ${release.theme}`, + })), + { + value: `${profile.id}@${profile.version}@draft`, + label: `${profile.name} — Draft`, + description: `${profile.version} · Draft · ${profile.theme}`, + }, + ]); + const selectedProfileValue = `${applicationDraft.designProfile.id}@${applicationDraft.designProfile.version}@${applicationDraft.designProfile.status}`; const availableTemplates = pageTemplates; if (applicationMode === "preview") { @@ -1373,13 +1417,17 @@ export function CatalogApp() { >