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