"use client"; import { use, useState, useEffect } from "react"; import { toast } from "sonner"; import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } 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"], }, ], }, ]; // --------------------------------------------------------------------------- // 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; const isLocked = !!lockedThemeId; return (
{/* 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
) }
{/* Controls below render */}
{/* Library capability cards */}
{surface.themes.map(theme => { const isActive = theme.id === previewId; const isThisLocked = theme.id === lockedThemeId; const dimmed = isLocked && !isThisLocked; return ( ); })}
{/* Color swatches — shown below the cards when available */} {availableColorThemes.length > 0 && (
Colour
{availableColorThemes.map(ct => (
)} {/* Docs link + lock action bar */}
{activeTheme && ( <> (e.currentTarget.style.color = "#1a1a1a")} onMouseLeave={e => (e.currentTarget.style.color = "#a09a90")} > {activeTheme.name} docs ↗
)} {isLocked ? ( ) : ( )}
); } // --------------------------------------------------------------------------- // 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 themeId = selectedThemes[surfaceId]; 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} /> )}
); }