feat: design page - left nav for surface selection, main area for theme picker

Made-with: Cursor
This commit is contained in:
2026-03-02 12:36:40 -08:00
parent ea54440be7
commit 7cf4f2ef78

View File

@@ -553,6 +553,7 @@ export default function DesignPage({
const [surfaces, setSurfaces] = useState<string[]>([]);
const [surfaceThemes, setSurfaceThemes] = useState<Record<string, string>>({});
const [selectedThemes, setSelectedThemes] = useState<Record<string, string>>({});
const [activeSurfaceId, setActiveSurfaceId] = useState<string | null>(null);
const [savingLock, setSavingLock] = useState<string | null>(null);
const [savingSurfaces, setSavingSurfaces] = useState(false);
const [loading, setLoading] = useState(true);
@@ -561,9 +562,11 @@ export default function DesignPage({
fetch(`/api/projects/${projectId}/design-surfaces`)
.then(r => r.json())
.then(d => {
setSurfaces(d.surfaces ?? []);
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));
@@ -579,6 +582,7 @@ export default function DesignPage({
});
if (!res.ok) throw new Error();
setSurfaces(ids);
setActiveSurfaceId(ids[0] ?? null);
toast.success("Surfaces saved");
} catch {
toast.error("Failed to save surfaces");
@@ -619,7 +623,7 @@ export default function DesignPage({
if (loading) {
return (
<div className="flex items-center justify-center py-32">
<div className="flex items-center justify-center h-full">
<Loader2 className="h-7 w-7 animate-spin text-muted-foreground" />
</div>
);
@@ -634,51 +638,74 @@ export default function DesignPage({
);
}
// Phase 2 — surfaces set, pick themes
// 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 (
<div className="space-y-10 p-6 max-w-5xl mx-auto">
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-bold">Design</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Choose a UI library for each surface the AI coder will reference these when building.
</p>
<div className="flex h-full">
{/* Left nav */}
<div className="w-52 shrink-0 border-r flex flex-col">
<div className="px-4 py-4 border-b">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Surfaces</p>
</div>
<div className="flex items-center gap-2">
<nav className="flex-1 py-2">
{activeSurfaces.map(surface => {
const Icon = surface.icon;
const isActive = surface.id === currentSurface?.id;
const isLocked = !!surfaceThemes[surface.id];
return (
<button
key={surface.id}
onClick={() => setActiveSurfaceId(surface.id)}
className={cn(
"flex items-center gap-2.5 w-full px-4 py-2.5 text-left transition-colors relative",
isActive
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
)}
>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-6 w-0.5 bg-foreground rounded-r" />
)}
<Icon className="h-4 w-4 shrink-0" />
<span className="text-sm font-medium flex-1 truncate">{surface.name}</span>
{isLocked && <Lock className="h-3 w-3 shrink-0 text-muted-foreground" />}
</button>
);
})}
</nav>
<div className="px-4 py-3 border-t space-y-2">
{lockedCount === activeSurfaces.length && lockedCount > 0 && (
<Badge variant="default" className="gap-1.5">
<Lock className="w-3 h-3" />
All locked in
</Badge>
<p className="text-[10px] text-muted-foreground flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" /> All surfaces locked
</p>
)}
<Button
variant="outline"
size="sm"
<button
onClick={() => setSurfaces([])}
className="text-xs"
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
>
Change surfaces
</Button>
+ Edit surfaces
</button>
</div>
</div>
<div className="space-y-10 divide-y divide-border">
{activeSurfaces.map((surface, i) => (
<div key={surface.id} className={i > 0 ? "pt-10" : ""}>
<SurfaceSection
surface={surface}
selectedThemeId={selectedThemes[surface.id] ?? null}
lockedThemeId={surfaceThemes[surface.id] ?? null}
onSelect={themeId => setSelectedThemes(prev => ({ ...prev, [surface.id]: themeId }))}
onLock={() => handleLock(surface.id)}
onUnlock={() => handleUnlock(surface.id)}
saving={savingLock === surface.id}
/>
</div>
))}
{/* Main content */}
<div className="flex-1 overflow-y-auto p-8">
{currentSurface && (
<SurfaceSection
surface={currentSurface}
selectedThemeId={selectedThemes[currentSurface.id] ?? null}
lockedThemeId={surfaceThemes[currentSurface.id] ?? null}
onSelect={themeId => setSelectedThemes(prev => ({ ...prev, [currentSurface.id]: themeId }))}
onLock={() => handleLock(currentSurface.id)}
onUnlock={() => handleUnlock(currentSurface.id)}
saving={savingLock === currentSurface.id}
/>
)}
</div>
</div>
);