feat: design page - left nav for surface selection, main area for theme picker
Made-with: Cursor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user