refactor: move all design controls below scaffold render

Theme swatches removed from inside scaffold components. Theme state
lifted to SurfaceSection which passes themeColor down as a prop.

Controls bar below the scaffold now has three rows:
  1. Library tabs (shadcn / Mantine / HeroUI / Tremor etc.)
  2. Color theme swatches — only shown when the active library has
     theme variants (shadcn: 8, Mantine: 6, HeroUI: 5, Tremor: 5,
     DaisyUI: 12, HeroUI marketing: 6)
  3. Description + tags + Docs link + Lock in button

Scaffold renders cleanly with no UI chrome inside it.

Made-with: Cursor
This commit is contained in:
2026-03-02 14:06:53 -08:00
parent 16766f587d
commit 7ba3b9563e
2 changed files with 82 additions and 58 deletions

View File

@@ -9,7 +9,7 @@ import {
Monitor, Globe, Settings, Smartphone, Mail, BookOpen,
Lock, CheckCircle2, Loader2, ChevronRight, Pencil,
} from "lucide-react";
import { SCAFFOLD_REGISTRY } from "@/components/design-scaffolds";
import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } from "@/components/design-scaffolds";
// ---------------------------------------------------------------------------
// Surface definitions
@@ -314,30 +314,33 @@ function SurfaceSection({
// 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;
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<ThemeColor | null>(null);
const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null;
return (
<div className="flex flex-col h-full gap-0">
{/* Scaffold preview — browser chrome frame */}
<div className="flex-1 rounded-xl overflow-hidden border" style={{ minHeight: 460 }}>
{/* Browser chrome */}
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50 shrink-0">
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
<div className="ml-3 flex-1 max-w-xs h-5 rounded-md bg-border/60 flex items-center px-2">
<span className="text-[9px] text-muted-foreground truncate">
{activeTheme ? `/${surface.id}${activeTheme.name}` : ""}
{activeTheme ? `/${surface.id}${activeTheme.name}${activeColorTheme ? ` / ${activeColorTheme.label}` : ""}` : ""}
</span>
</div>
</div>
{/* Scaffold */}
<div style={{ height: 460 }}>
{ScaffoldComponent
? <ScaffoldComponent />
? <ScaffoldComponent themeColor={activeColorTheme ?? undefined} />
: (
<div className="h-full flex items-center justify-center text-muted-foreground text-xs">
Select a library below to preview
@@ -347,9 +350,10 @@ function SurfaceSection({
</div>
</div>
{/* Controls bar — library tabs + description + lock in */}
<div className="shrink-0 pt-3 space-y-2">
{/* Library tab row */}
{/* Controls bar — all below the render */}
<div className="shrink-0 pt-4 space-y-3">
{/* Row 1: library tabs */}
<div className="flex items-center gap-1.5 flex-wrap">
{surface.themes.map(theme => {
const isActive = theme.id === previewId;
@@ -357,13 +361,11 @@ function SurfaceSection({
return (
<button
key={theme.id}
onClick={() => !lockedThemeId && onSelect(theme.id)}
onClick={() => { if (!lockedThemeId) { onSelect(theme.id); setSelectedColorTheme(null); } }}
disabled={!!lockedThemeId && !isLocked}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all border",
isActive && isLocked
? "bg-foreground text-background border-foreground"
: isActive
isActive
? "bg-foreground text-background border-foreground"
: lockedThemeId
? "opacity-30 border-transparent text-muted-foreground cursor-not-allowed"
@@ -377,7 +379,35 @@ function SurfaceSection({
})}
</div>
{/* Description + tags + actions */}
{/* Row 2: color theme swatches (only if this library has color variants) */}
{availableColorThemes.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-[10px] text-muted-foreground shrink-0">Theme</span>
<div className="flex items-center gap-1.5 flex-wrap">
{availableColorThemes.map(ct => (
<button
key={ct.id}
title={ct.label}
onClick={() => !lockedThemeId && setSelectedColorTheme(ct)}
disabled={!!lockedThemeId}
className="w-5 h-5 rounded-full transition-transform hover:scale-110 disabled:opacity-40"
style={{
background: ct.bg
? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)`
: ct.primary,
outline: activeColorTheme?.id === ct.id ? `2px solid ${ct.primary}` : "none",
outlineOffset: 2,
}}
/>
))}
{activeColorTheme && (
<span className="text-[10px] text-muted-foreground ml-1">{activeColorTheme.label}</span>
)}
</div>
</div>
)}
{/* Row 3: description + tags + docs + lock in */}
<div className="flex items-center gap-3">
{activeTheme && (
<>
@@ -387,28 +417,19 @@ function SurfaceSection({
<Badge key={t} variant="secondary" className="text-[9px] px-1.5 py-0">{t}</Badge>
))}
</div>
<a
href={activeTheme.url}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors shrink-0"
>
<a href={activeTheme.url} target="_blank" rel="noopener noreferrer"
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors shrink-0">
Docs
</a>
</>
)}
{lockedThemeId ? (
<Button variant="outline" size="sm" onClick={onUnlock} className="gap-1.5 text-xs h-7 shrink-0">
<Pencil className="h-3 w-3" />
Change
<Pencil className="h-3 w-3" />Change
</Button>
) : (
<Button
size="sm"
onClick={onLock}
disabled={!selectedThemeId || saving}
className="gap-1.5 text-xs h-7 shrink-0"
>
<Button size="sm" onClick={onLock} disabled={!selectedThemeId || saving}
className="gap-1.5 text-xs h-7 shrink-0">
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Lock className="h-3 w-3" />}
Lock in
</Button>