"use client"; import { use, useState, useEffect } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Monitor, Globe, Settings, Smartphone, Mail, BookOpen, Lock, CheckCircle2, Loader2, ChevronRight, Pencil, } from "lucide-react"; import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } from "@/components/design-scaffolds"; // --------------------------------------------------------------------------- // Surface definitions // --------------------------------------------------------------------------- interface Surface { id: string; name: string; description: string; icon: React.ElementType; themes: Theme[]; } interface Theme { id: string; name: string; description: string; tags: string[]; url: string; preview: React.ReactNode; } // Mini UI mockups — each styled to feel like the library const ShadcnPreview = () => (
Save Cancel
); const MantiinePreview = () => (
New
{[1,2].map(i => (
))}
Apply
); const HeroUIPreview = () => (
Get started →
); const DaisyPreview = () => (
Primary Success
); const AcernityPreview = () => (
Get started
); const TailwindPreview = () => (
className="flex…"
Custom →
); const TremorPreview = () => (
{[60, 80, 45, 90, 70, 55].map((h, i) => (
))}
); const NativewindPreview = () => (
Button
); const GluestackPreview = () => (
Submit
); const ReactEmailPreview = () => (
Open App →
); const NextraPreview = () => (
{['Getting started', 'Installation', 'API', 'Examples'].map(l => (
))}
); const ALL_SURFACES: Surface[] = [ { id: "web-app", name: "Web App", description: "The core product your users log into — dashboards, features, settings", icon: Monitor, themes: [ { id: "shadcn", name: "shadcn/ui", description: "Copy-paste components on Radix primitives. You own the code, fully customisable.", tags: ["Tailwind", "Radix", "Copy-paste"], url: "https://ui.shadcn.com", preview: }, { id: "mantine", name: "Mantine", description: "100+ components with hooks, forms, charts. Best for data-heavy apps.", tags: ["React", "Charts", "Forms"], url: "https://mantine.dev", preview: }, { id: "hero-ui", name: "HeroUI", description: "Beautiful, accessible components with smooth animations and dark mode.", tags: ["Tailwind", "Animations", "Accessible"], url: "https://heroui.com", preview: }, { id: "tremor", name: "Tremor", description: "Dashboard components — charts, KPIs, tables — designed for analytics UIs.", tags: ["Charts", "Dashboard", "Analytics"], url: "https://tremor.so", preview: }, ], }, { id: "marketing", name: "Marketing Site", description: "Public-facing landing page, blog, pricing — brand expression and conversion", icon: Globe, themes: [ { id: "daisy-ui", name: "DaisyUI", description: "Tailwind plugin with 48 built-in themes. Fastest path to a beautiful site.", tags: ["Tailwind", "Themes", "Plugin"], url: "https://daisyui.com", preview: }, { id: "hero-ui", name: "HeroUI", description: "Beautiful components with gradients and smooth animations.", tags: ["Tailwind", "Animations", "Modern"], url: "https://heroui.com", preview: }, { id: "aceternity", name: "Aceternity UI", description: "Animated, visually striking components for premium landing pages.", tags: ["Animations", "Dark", "Premium"], url: "https://ui.aceternity.com", preview: }, { id: "tailwind-only", name: "Tailwind only", description: "No component library — full creative control with pure Tailwind CSS.", tags: ["Custom", "Flexible", "Minimal"], url: "https://tailwindcss.com", preview: }, ], }, { id: "admin", name: "Admin Panel", description: "Internal tool for managing your business — users, support, billing, analytics", icon: Settings, themes: [ { id: "mantine", name: "Mantine", description: "The best choice for admin — comprehensive tables, forms, and data components.", tags: ["Tables", "Forms", "Charts"], url: "https://mantine.dev", preview: }, { id: "shadcn", name: "shadcn/ui", description: "Clean, neutral components. Great if you want the admin to match the main app.", tags: ["Tailwind", "Consistent", "Clean"], url: "https://ui.shadcn.com", preview: }, { id: "tremor", name: "Tremor", description: "Analytics-first — built for KPI dashboards, charts, and data tables.", tags: ["Analytics", "Charts", "KPIs"], url: "https://tremor.so", preview: }, ], }, { id: "mobile", name: "Mobile App", description: "iOS and Android companion app — touch-first, native feel", icon: Smartphone, themes: [ { id: "nativewind", name: "NativeWind", description: "Use Tailwind CSS in React Native. Consistent style across web and mobile.", tags: ["Tailwind", "React Native", "Expo"], url: "https://nativewind.dev", preview: }, { id: "gluestack", name: "Gluestack UI", description: "Universal components for React Native — accessible, well-tested, comprehensive.", tags: ["Universal", "Accessible", "Expo"], url: "https://gluestack.io", preview: }, ], }, { id: "email", name: "Email", description: "Transactional and marketing emails — welcome, billing, notifications", icon: Mail, themes: [ { id: "react-email", name: "React Email", description: "Build emails with React components. Works with any email provider.", tags: ["React", "Resend", "Cross-client"], url: "https://react.email", preview: }, ], }, { id: "docs", name: "Docs / Content", description: "Documentation, knowledge base, or blog for your product", icon: BookOpen, themes: [ { id: "nextra", name: "Nextra", description: "Next.js-based docs site. Markdown-first, fast, with great search.", tags: ["Next.js", "Markdown", "Search"], url: "https://nextra.site", preview: }, { id: "shadcn", name: "shadcn/ui + custom", description: "Build a fully custom docs site that matches your product exactly.", tags: ["Custom", "Tailwind", "Flexible"], url: "https://ui.shadcn.com", preview: }, ], }, ]; // --------------------------------------------------------------------------- // Surface section — tab toggle + scaffold preview + lock in // --------------------------------------------------------------------------- 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; }) { // Active preview tab — if locked show that, otherwise the selected/first 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; // Theme color variants for the active library (e.g. shadcn has 8 color themes) const availableColorThemes: ThemeColor[] = previewId ? (THEME_REGISTRY[surface.id]?.[previewId] ?? []) : []; const [selectedColorTheme, setSelectedColorTheme] = useState(null); const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null; return (
{/* Scaffold preview — browser chrome frame */}
{activeTheme ? `/${surface.id} — ${activeTheme.name}${activeColorTheme ? ` / ${activeColorTheme.label}` : ""}` : ""}
{ScaffoldComponent ? : (
Select a library below to preview
) }
{/* Controls bar — all below the render */}
{/* Row 1: library tabs */}
{surface.themes.map(theme => { const isActive = theme.id === previewId; const isLocked = theme.id === lockedThemeId; return ( ); })}
{/* Row 2: color theme swatches (only if this library has color variants) */} {availableColorThemes.length > 0 && (
Theme
{availableColorThemes.map(ct => (
)} {/* Row 3: description + tags + docs + lock in */}
{activeTheme && ( <>

{activeTheme.description}

{activeTheme.tags.map(t => ( {t} ))}
Docs ↗ )} {lockedThemeId ? ( ) : ( )}
); } // --------------------------------------------------------------------------- // Phase 1 — Surface picker // --------------------------------------------------------------------------- function SurfacePicker({ onConfirm, saving, }: { onConfirm: (ids: string[]) => void; saving: boolean; }) { const [selected, setSelected] = useState>(new Set()); 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

Which surfaces does your product need? Select all that apply — you can always add more later.

{ALL_SURFACES.map(surface => { const Icon = surface.icon; const isSelected = selected.has(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); useEffect(() => { 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]); }) .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 (
); } // Phase 1 — no surfaces set yet if (surfaces.length === 0) { return (
); } // Phase 2 — left nav + main content 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 surfaces locked

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