"use client"; import { use, useState, useEffect } from "react"; import { toast } from "sonner"; import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor, type DesignConfig, type LibraryStyleOptions, } from "@/components/design-scaffolds"; // --------------------------------------------------------------------------- // Surface definitions // --------------------------------------------------------------------------- interface Surface { id: string; name: string; description: string; icon: string; themes: Theme[]; } interface Theme { id: string; name: string; description: string; tags: string[]; url: string; // Richer metadata shown in the capability card picker highlights?: string[]; // 3 key things this library does well hasTemplates?: boolean; // pre-built page templates available bestFor?: string; // one-line "best for" summary darkFirst?: boolean; // dark-mode-first aesthetic } const ALL_SURFACES: Surface[] = [ { id: "web-app", name: "Web App", icon: "⬡", description: "The core product users log into — dashboards, features, settings", themes: [ { id: "shadcn", name: "shadcn/ui", url: "https://ui.shadcn.com", description: "Copy-paste components on Radix primitives. You own the code.", tags: ["Tailwind", "Radix", "Copy-paste"], bestFor: "Most SaaS apps — you control every pixel", highlights: ["Code lives in your repo, not node_modules", "Radix primitives for full accessibility", "New York & Default variants built in"], hasTemplates: true, }, { id: "mantine", name: "Mantine", url: "https://mantine.dev", description: "100+ fully featured components including charts, forms, date pickers.", tags: ["React", "Charts", "Forms", "Hooks"], bestFor: "Data-heavy apps that need charts and complex forms", highlights: ["Built-in charts, tables, and data grid", "60+ hooks (useForm, useLocalStorage, etc.)", "Date/time pickers, rich text editor included"], }, { id: "hero-ui", name: "HeroUI", url: "https://heroui.com", description: "Smooth animations, blur effects, and dark mode out of the box.", tags: ["Tailwind", "Framer Motion", "Dark mode"], bestFor: "Modern SaaS with polished animations", highlights: ["Blur backdrop navigation built in", "Framer Motion on every interaction", "Dark mode first, light mode included"], hasTemplates: true, }, { id: "tremor", name: "Tremor", url: "https://tremor.so", description: "Purpose-built dashboard components — charts, KPIs, tables.", tags: ["Charts", "Dashboard", "Analytics"], bestFor: "Analytics products and internal dashboards", highlights: ["Chart library built in (area, bar, donut)", "KPI cards, progress bars, trackers", "Area chart with tooltip out of the box"], hasTemplates: true, }, ], }, { id: "marketing", name: "Marketing Site", icon: "◎", description: "Public-facing landing page, blog, pricing — brand expression and conversion", themes: [ { id: "daisy-ui", name: "DaisyUI", url: "https://daisyui.com", description: "Tailwind plugin with 48 built-in themes — swap the entire look with one word.", tags: ["Tailwind", "48 Themes", "Plugin"], bestFor: "Teams that want theme variety without custom CSS", highlights: ["48 built-in themes (dark, cupcake, synthwave, cyberpunk…)", "One-line theme switching: data-theme=\"synthwave\"", "Hero, Features, Pricing, CTA, Footer all included"], hasTemplates: true, }, { id: "hero-ui", name: "HeroUI", url: "https://heroui.com", description: "Gradient buttons, animated badges, blur nav — high-end SaaS look.", tags: ["Tailwind", "Framer Motion", "Gradients"], bestFor: "Modern SaaS landing page with a premium feel", highlights: ["Gradient & glassmorphism components", "Animated feature cards with hover depth", "Navbar with blur-backdrop effect"], hasTemplates: true, }, { id: "aceternity", name: "Aceternity UI", url: "https://ui.aceternity.com", description: "Signature background effects — beams, meteors, sparkles, wavy gradients.", tags: ["Framer Motion", "Background FX", "Dark-first"], bestFor: "Premium, visually striking launch pages", highlights: ["Background Beams, Meteors, Sparkles effects", "Infinite moving cards, Lamp effect, Wavy BG", "Text generate & typewriter animations"], darkFirst: true, }, { id: "tailwind-only", name: "Tailwind only", url: "https://tailwindcss.com", description: "Zero component library. Full creative control — design exactly what you want.", tags: ["Custom", "Zero constraints", "Typography-first"], bestFor: "Strong design opinions, no library overhead", highlights: ["No component library constraints", "Typography-first editorial layouts", "Fastest builds, smallest bundle"], }, ], }, { id: "admin", name: "Admin Panel", icon: "◫", description: "Internal tool for managing your business — users, support, billing, analytics", themes: [ { id: "mantine", name: "Mantine", url: "https://mantine.dev", description: "Comprehensive component set built for admin — tables, forms, charts.", tags: ["Tables", "Forms", "Charts", "Date picker"], bestFor: "Feature-rich internal tools", highlights: ["Data grid with sorting, filtering, pagination", "Complex form handling with validation", "Stats, charts, and KPI components"], }, { id: "shadcn", name: "shadcn/ui", url: "https://ui.shadcn.com", description: "Clean components that match your main app if it uses shadcn.", tags: ["Tailwind", "Consistent", "Copy-paste"], bestFor: "Keeping admin and app visually consistent", highlights: ["Matches the web app if using shadcn", "Data table with react-table built in", "Command palette, sheets, dialogs"], }, { id: "tremor", name: "Tremor", url: "https://tremor.so", description: "Analytics-first admin — built for KPI dashboards and data tables.", tags: ["Analytics", "Charts", "KPIs"], bestFor: "Analytics-heavy operations dashboards", highlights: ["KPI cards, area charts, bar lists built in", "Data table with search + filter", "Progress bars, badges, trackers"], hasTemplates: true, }, ], }, { id: "mobile", name: "Mobile App", icon: "▢", description: "iOS and Android companion app — touch-first, native feel", themes: [ { id: "nativewind", name: "NativeWind", url: "https://nativewind.dev", description: "Use Tailwind classes in React Native. Consistent web + mobile styling.", tags: ["Tailwind", "React Native", "Expo"], bestFor: "Teams already using Tailwind on web", highlights: ["Same Tailwind classes on web and mobile", "Expo compatible, dark mode support", "Platform-specific variants built in"], }, { id: "gluestack", name: "Gluestack UI", url: "https://gluestack.io", description: "Universal component library for React Native with accessibility baked in.", tags: ["Universal", "Accessible", "Expo"], bestFor: "Comprehensive native component coverage", highlights: ["50+ components (ActionSheet, Toast, FAB…)", "ARIA accessible out of the box", "Works on iOS, Android, and web"], }, ], }, { id: "email", name: "Email", icon: "✉", description: "Transactional and marketing emails — welcome, billing, notifications", themes: [ { id: "react-email", name: "React Email", url: "https://react.email", description: "Build beautiful emails with React. Test across 90+ email clients.", tags: ["React", "Resend", "Cross-client", "Preview"], bestFor: "Teams wanting React-based email workflows", highlights: ["Live preview across Gmail, Outlook, Apple Mail", "Works with Resend, SendGrid, Postmark", "50+ pre-built email components"], hasTemplates: true, }, ], }, { id: "docs", name: "Docs / Content", icon: "☰", description: "Documentation, knowledge base, or blog for your product", themes: [ { id: "nextra", name: "Nextra", url: "https://nextra.site", description: "Next.js-based docs with built-in search, MDX, and versioning.", tags: ["Next.js", "MDX", "Search", "Dark mode"], bestFor: "Developer-facing documentation", highlights: ["Full-text search out of the box (Pagefind)", "MDX — React components inside Markdown", "Auto-generated sidebar from file structure"], hasTemplates: true, }, { id: "shadcn", name: "shadcn/ui + custom", url: "https://ui.shadcn.com", description: "Fully custom docs site that matches your product exactly.", tags: ["Custom", "Tailwind", "Flexible"], bestFor: "Brand-consistent docs that stand out", highlights: ["Matches your app's exact design language", "Full control over layout and navigation", "Blog, changelog, and API ref in one"], }, ], }, ]; // --------------------------------------------------------------------------- // Library style configurator options — per library, for the Marketing surface // (other surfaces can adopt this pattern as needed) // --------------------------------------------------------------------------- const LIBRARY_STYLE_OPTIONS: Record = { "daisy-ui": { modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }], backgrounds: [ { id: "solid", label: "Solid", description: "Clean theme background" }, { id: "gradient", label: "Gradient", description: "Radial glow from primary color" }, { id: "pattern", label: "Dot grid", description: "Subtle repeating dot pattern" }, { id: "noise", label: "Noise", description: "Fine grain texture" }, ], navStyles: [ { id: "standard", label: "Standard", description: "Bordered top bar" }, { id: "transparent", label: "Transparent", description: "Overlaps the hero section" }, { id: "pill", label: "Pill", description: "Floating centered nav" }, ], headerStyles: [ { id: "centered", label: "Centered", description: "Title and CTAs centred" }, { id: "split", label: "Split", description: "Left text, right mockup" }, { id: "stats", label: "With stats", description: "Metrics row below CTAs" }, ], components: [ { id: "logos", label: "Logo strip" }, { id: "features", label: "Feature cards" }, { id: "steps", label: "How it works" }, { id: "testimonials", label: "Testimonials" }, { id: "pricing", label: "Pricing table" }, { id: "faq", label: "FAQ" }, { id: "cta", label: "CTA banner" }, ], fonts: [ { id: "system", label: "System UI" }, { id: "plus-jakarta", label: "Plus Jakarta" }, { id: "dm-sans", label: "DM Sans" }, { id: "geist", label: "Geist" }, ], defaultConfig: { mode: "dark", background: "solid", nav: "standard", header: "centered", components: ["logos", "features", "steps", "testimonials", "pricing", "faq", "cta"], font: "system" }, }, "hero-ui": { modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }], backgrounds: [ { id: "clean", label: "Clean", description: "Solid background" }, { id: "gradient-mesh", label: "Gradient mesh", description: "Soft multi-colour mesh" }, { id: "glass", label: "Glassmorphism", description: "Frosted glass feel" }, { id: "aurora", label: "Aurora", description: "Animated colour glow" }, ], navStyles: [ { id: "blur", label: "Blur backdrop", description: "Frosted glass sticky navbar" }, { id: "standard", label: "Standard", description: "Solid bordered bar" }, { id: "minimal", label: "Minimal", description: "No border, clean" }, ], headerStyles: [ { id: "animated-badge", label: "Animated", description: "Chip badge + gradient headline" }, { id: "split", label: "Split", description: "Text + dashboard preview" }, { id: "gradient", label: "Gradient", description: "Full gradient headline" }, ], components: [ { id: "features", label: "Feature cards" }, { id: "metrics", label: "Metrics grid" }, { id: "avatars", label: "Social proof" }, { id: "pricing", label: "Pricing" }, { id: "testimonials", label: "Testimonials" }, { id: "cta", label: "CTA section" }, ], fonts: [ { id: "system", label: "System UI" }, { id: "inter", label: "Inter" }, { id: "geist", label: "Geist" }, { id: "nunito", label: "Nunito" }, ], defaultConfig: { mode: "light", background: "clean", nav: "blur", header: "animated-badge", components: ["features", "metrics", "avatars", "pricing", "testimonials", "cta"], font: "system" }, }, "aceternity": { modes: [{ id: "dark", label: "Dark" }, { id: "light", label: "Light" }], backgrounds: [ { id: "gradient", label: "Gradient anim", description: "Animated blob gradient (Background Gradient Animation)" }, { id: "shader", label: "Line shader", description: "Bold diagonal gradient: purple→pink→orange→yellow" }, { id: "beams", label: "Beams", description: "Radial SVG-style light beams on dark bg" }, { id: "meteors", label: "Meteors", description: "Diagonal shooting-star streaks with glow tails" }, { id: "sparkles", label: "Sparkles", description: "Black bg with twinkling star particles" }, { id: "aurora", label: "Aurora", description: "Soft lavender/blue aurora on light bg" }, { id: "wavy", label: "Wavy", description: "Smooth wave shapes" }, { id: "dot-grid", label: "Dot grid", description: "Perspective dot matrix" }, ], navStyles: [ { id: "minimal", label: "Minimal", description: "Barely-there top bar" }, { id: "floating", label: "Floating", description: "Centred floating pill" }, ], headerStyles: [ { id: "gradient-text", label: "Gradient text", description: "Fade-to-transparent heading" }, { id: "lamp", label: "Lamp effect", description: "Spotlight cone from above" }, { id: "typewriter", label: "Typewriter", description: "Animated text reveal" }, ], components: [ { id: "badge", label: "Glow badge" }, { id: "features", label: "Feature cards" }, { id: "moving-cards", label: "Moving cards" }, { id: "bento", label: "Bento grid" }, { id: "pricing", label: "Pricing" }, { id: "cta", label: "CTA section" }, ], fonts: [ { id: "system", label: "System UI" }, { id: "geist", label: "Geist" }, { id: "plus-jakarta", label: "Plus Jakarta" }, ], defaultConfig: { mode: "dark", background: "gradient", nav: "minimal", header: "gradient-text", components: ["badge", "features", "moving-cards", "bento", "pricing", "cta"], font: "system" }, }, "tailwind-only": { modes: [{ id: "light", label: "Light" }, { id: "dark", label: "Dark" }], backgrounds: [ { id: "clean", label: "Clean", description: "Pure white or dark" }, { id: "dot-grid", label: "Dot grid", description: "Subtle dot pattern" }, { id: "lines", label: "Grid lines", description: "Faint gridlines" }, { id: "noise", label: "Noise", description: "Fine grain texture" }, ], navStyles: [ { id: "minimal", label: "Minimal", description: "Logo + links, no decoration" }, { id: "bordered", label: "Bordered", description: "With bottom border" }, { id: "spaced", label: "Spread", description: "Logo left, CTA right" }, ], headerStyles: [ { id: "editorial", label: "Editorial", description: "Big bold display typography" }, { id: "split", label: "Split", description: "Text left + terminal preview" }, { id: "centered", label: "Centered", description: "Minimal centred layout" }, ], components: [ { id: "badge", label: "Release badge" }, { id: "logos", label: "Logo strip" }, { id: "features", label: "Features grid" }, { id: "stats", label: "Stats row" }, { id: "testimonials", label: "Testimonials" }, { id: "pricing", label: "Pricing" }, { id: "faq", label: "FAQ" }, { id: "cta", label: "CTA" }, ], fonts: [ { id: "system", label: "System UI" }, { id: "inter", label: "Inter" }, { id: "plus-jakarta", label: "Plus Jakarta" }, { id: "dm-sans", label: "DM Sans" }, ], defaultConfig: { mode: "light", background: "clean", nav: "minimal", header: "editorial", components: ["badge", "logos", "features", "stats", "testimonials", "pricing", "faq", "cta"], font: "system" }, }, }; // --------------------------------------------------------------------------- // DesignConfigurator — shows mode/bg/nav/header/components/font pickers // --------------------------------------------------------------------------- function ConfigRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function OptionChip({ label, description, active, onClick, multi, checked, }: { label: string; description?: string; active: boolean; onClick: () => void; multi?: boolean; checked?: boolean; }) { return ( ); } function ModeToggle({ value, onChange }: { value: string; onChange: (v: "dark" | "light") => void }) { return (
{(["Light", "Dark"] as const).map(m => { const id = m.toLowerCase() as "light" | "dark"; const active = value === id; return ( ); })}
); } function DesignConfigurator({ libraryId, config, onChange, }: { libraryId: string; config: DesignConfig; onChange: (patch: Partial) => void; }) { const opts = LIBRARY_STYLE_OPTIONS[libraryId]; if (!opts) return null; const toggleComponent = (id: string) => { const next = config.components.includes(id) ? config.components.filter(c => c !== id) : [...config.components, id]; onChange({ components: next }); }; return (
{/* Mode */} onChange({ mode: v })} /> {/* Background */} {opts.backgrounds.map(bg => ( onChange({ background: bg.id })} /> ))} {/* Nav */} {opts.navStyles.map(n => ( onChange({ nav: n.id })} /> ))} {/* Header */} {opts.headerStyles.map(h => ( onChange({ header: h.id })} /> ))} {/* Sections */} {opts.components.map(c => ( toggleComponent(c.id)} /> ))} {/* Font */} {opts.fonts.map(f => ( onChange({ font: f.id })} /> ))}
); } // --------------------------------------------------------------------------- // Surface section // --------------------------------------------------------------------------- function SurfaceSection({ surface, selectedThemeId, lockedThemeId, onSelect, onLock, onUnlock, saving, }: { surface: Surface; selectedThemeId: string | null; lockedThemeId: string | null; onSelect: (themeId: string) => void; onLock: () => void; onUnlock: () => void; saving: boolean; }) { const previewId = lockedThemeId ?? selectedThemeId ?? surface.themes[0]?.id ?? null; const activeTheme = surface.themes.find(t => t.id === previewId); const ScaffoldComponent = previewId ? SCAFFOLD_REGISTRY[surface.id]?.[previewId] : null; const availableColorThemes: ThemeColor[] = previewId ? (THEME_REGISTRY[surface.id]?.[previewId] ?? []) : []; const [selectedColorTheme, setSelectedColorTheme] = useState(null); const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null; // Design config — per-library style choices (mode, background, nav, header, sections, font) const defaultForLibrary = previewId ? LIBRARY_STYLE_OPTIONS[previewId]?.defaultConfig : undefined; const [designConfig, setDesignConfig] = useState( defaultForLibrary ?? { mode: "light", background: "solid", nav: "standard", header: "centered", components: ["features", "pricing"], font: "system" } ); // Reset config when library changes const [lastPreviewId, setLastPreviewId] = useState(previewId); if (previewId !== lastPreviewId) { setLastPreviewId(previewId); const def = previewId ? LIBRARY_STYLE_OPTIONS[previewId]?.defaultConfig : undefined; if (def) setDesignConfig(def); } const patchConfig = (patch: Partial) => setDesignConfig(prev => ({ ...prev, ...patch })); const opts = previewId ? LIBRARY_STYLE_OPTIONS[previewId] : null; const hasConfigurator = !!previewId && !!opts; const isLocked = !!lockedThemeId; // Ensure parent always knows the currently-displayed theme (even before user clicks) // so Lock In works immediately without requiring an explicit library click first. useEffect(() => { if (!selectedThemeId && previewId) onSelect(previewId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [previewId]); const toggleComponent = (id: string) => { const next = designConfig.components.includes(id) ? designConfig.components.filter(c => c !== id) : [...designConfig.components, id]; patchConfig({ components: next }); }; return (
{/* Center — browser chrome + scaffold */}
{/* Chrome bar */}
{["#d0ccc4", "#d0ccc4", "#d0ccc4"].map((c, i) => (
))}
{activeTheme ? `/${surface.id} · ${activeTheme.name}${activeColorTheme ? ` · ${activeColorTheme.label}` : ""}` : ""}
{/* Scaffold */}
{ScaffoldComponent ? : (
Select a library below to preview
) }
{/* end center scaffold wrapper */} {/* Right — controls panel */}
{/* 1. Lock / unlock */}
{isLocked ? ( ) : ( )} {activeTheme && ( (e.currentTarget.style.color = "#1a1a1a")} onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")} >Docs ↗ )}
{/* 2. Library */}
Library
{surface.themes.map(theme => { const isActive = theme.id === previewId; const isThisLocked = theme.id === lockedThemeId; const dimmed = isLocked && !isThisLocked; return ( ); })}
{hasConfigurator && !isLocked && opts && (<> {/* 3. Mode */} patchConfig({ mode: v })} /> {/* 4. Colour */} {availableColorThemes.length > 0 && (
Colour
{availableColorThemes.map(ct => (
{activeColorTheme && ( {activeColorTheme.label} )}
)} {/* 5. Font */} {opts.fonts.map(f => ( patchConfig({ font: f.id })} /> ))} {/* 6. Background */} {opts.backgrounds.map(bg => ( patchConfig({ background: bg.id })} /> ))} {/* 7. Nav */} {opts.navStyles.map(n => ( patchConfig({ nav: n.id })} /> ))} {/* 8. Hero */} {opts.headerStyles.map(h => ( patchConfig({ header: h.id })} /> ))} {/* 9. Sections */} {opts.components.map(c => ( toggleComponent(c.id)} /> ))} )} {/* Colour swatches when locked (read-only) */} {isLocked && availableColorThemes.length > 0 && (
Colour
{availableColorThemes.map(ct => (
)}
{/* end inner padding div */}
{/* end right panel */}
); } // --------------------------------------------------------------------------- // Surface ID fuzzy-match helper — maps AI-generated labels to our surface IDs // --------------------------------------------------------------------------- function matchSurfaceId(label: string): string | null { const l = label.toLowerCase(); if (l.includes("web app") || l.includes("webapp") || l.includes("saas") || l.includes("dashboard") || l.includes("pwa")) return "web-app"; if (l.includes("marketing") || l.includes("landing") || l.includes("site") || l.includes("homepage")) return "marketing"; if (l.includes("admin") || l.includes("internal") || l.includes("back office") || l.includes("backoffice")) return "admin"; if (l.includes("mobile") || l.includes("ios") || l.includes("android") || l.includes("native")) return "mobile"; if (l.includes("email") || l.includes("notification")) return "email"; if (l.includes("doc") || l.includes("content") || l.includes("blog") || l.includes("knowledge")) return "docs"; return null; } // --------------------------------------------------------------------------- // Phase 1 — Surface picker // --------------------------------------------------------------------------- function SurfacePicker({ onConfirm, saving, aiSuggested, }: { onConfirm: (ids: string[]) => void; saving: boolean; aiSuggested: string[]; }) { const [selected, setSelected] = useState>(new Set(aiSuggested)); const toggle = (id: string) => { setSelected(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; return (

Design surfaces

0 ? 10 : 24 }}> Which surfaces does your product need?

{aiSuggested.length > 0 && (
Based on your PRD, the AI pre-selected the surfaces your product needs. Adjust if needed.
)}
{ALL_SURFACES.map(surface => { const isSelected = selected.has(surface.id); const isAiPick = aiSuggested.includes(surface.id); return ( ); })}
{selected.size === 0 && (

Select at least one surface to continue

)}
); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export default function DesignPage({ params }: { params: Promise<{ workspace: string; projectId: string }> }) { const { projectId } = use(params); const [surfaces, setSurfaces] = useState([]); const [surfaceThemes, setSurfaceThemes] = useState>({}); const [selectedThemes, setSelectedThemes] = useState>({}); const [activeSurfaceId, setActiveSurfaceId] = useState(null); const [savingLock, setSavingLock] = useState(null); const [savingSurfaces, setSavingSurfaces] = useState(false); const [loading, setLoading] = useState(true); const [aiSuggestedSurfaces, setAiSuggestedSurfaces] = useState([]); useEffect(() => { // Load saved design surfaces const designP = fetch(`/api/projects/${projectId}/design-surfaces`) .then(r => r.json()) .then(d => { const loaded = (d.surfaces ?? []) as string[]; setSurfaces(loaded); setSurfaceThemes(d.surfaceThemes ?? {}); setSelectedThemes(d.surfaceThemes ?? {}); if (loaded.length > 0) setActiveSurfaceId(loaded[0]); return loaded; }); // Load architecture to get AI-suggested surfaces const archP = fetch(`/api/projects/${projectId}/architecture`) .then(r => r.json()) .then(d => { const arch = d.architecture; if (arch?.designSurfaces && Array.isArray(arch.designSurfaces)) { const matched = (arch.designSurfaces as string[]) .map(matchSurfaceId) .filter((id): id is string => id !== null); setAiSuggestedSurfaces([...new Set(matched)]); } }) .catch(() => {}); Promise.all([designP, archP]) .catch(() => toast.error("Failed to load design data")) .finally(() => setLoading(false)); }, [projectId]); const handleConfirmSurfaces = async (ids: string[]) => { setSavingSurfaces(true); try { const res = await fetch(`/api/projects/${projectId}/design-surfaces`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ surfaces: ids }), }); if (!res.ok) throw new Error(); setSurfaces(ids); setActiveSurfaceId(ids[0] ?? null); toast.success("Surfaces saved"); } catch { toast.error("Failed to save surfaces"); } finally { setSavingSurfaces(false); } }; const handleLock = async (surfaceId: string) => { const surface = ALL_SURFACES.find(s => s.id === surfaceId); const themeId = selectedThemes[surfaceId] ?? surface?.themes[0]?.id; if (!themeId) return; setSavingLock(surfaceId); try { const res = await fetch(`/api/projects/${projectId}/design-surfaces`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ surface: surfaceId, theme: themeId }), }); if (!res.ok) throw new Error(); setSurfaceThemes(prev => ({ ...prev, [surfaceId]: themeId })); const surface = ALL_SURFACES.find(s => s.id === surfaceId); const theme = surface?.themes.find(t => t.id === themeId); toast.success(`${surface?.name} → ${theme?.name} locked in`); } catch { toast.error("Failed to lock in theme"); } finally { setSavingLock(null); } }; const handleUnlock = (surfaceId: string) => { setSurfaceThemes(prev => { const next = { ...prev }; delete next[surfaceId]; return next; }); }; if (loading) { return (
); } if (surfaces.length === 0) { return ; } const activeSurfaces = ALL_SURFACES.filter(s => surfaces.includes(s.id)); const currentSurface = activeSurfaces.find(s => s.id === activeSurfaceId) ?? activeSurfaces[0]; const lockedCount = Object.keys(surfaceThemes).length; return (
{/* Left nav */}
Surfaces
{lockedCount === activeSurfaces.length && lockedCount > 0 && (

✓ All locked

)}
{/* Main content */}
{currentSurface && ( setSelectedThemes(prev => ({ ...prev, [currentSurface.id]: themeId }))} onLock={() => handleLock(currentSurface.id)} onUnlock={() => handleUnlock(currentSurface.id)} saving={savingLock === currentSurface.id} /> )}
); }