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

View File

@@ -221,9 +221,9 @@ function ShadcnSettings({ t }: { t: ThemeColor }) {
); );
} }
export function WebAppShadcn() { export function WebAppShadcn({ themeColor }: { themeColor?: ThemeColor }) {
const [page, setPage] = useState<Page>("Dashboard"); const [page, setPage] = useState<Page>("Dashboard");
const [theme, setTheme] = useState<ThemeColor>(SHADCN_THEMES[0]); const theme = themeColor ?? SHADCN_THEMES[0];
const NAV_ITEMS: { label: Page; icon: string }[] = [ const NAV_ITEMS: { label: Page; icon: string }[] = [
{ label: "Dashboard", icon: "▦" }, { label: "Dashboard", icon: "▦" },
{ label: "Users", icon: "◎" }, { label: "Users", icon: "◎" },
@@ -231,7 +231,6 @@ export function WebAppShadcn() {
]; ];
return ( return (
<div className="flex h-full bg-white font-sans text-sm"> <div className="flex h-full bg-white font-sans text-sm">
{/* Sidebar */}
<div className="w-44 border-r flex flex-col py-4 px-3 gap-1 bg-white shrink-0"> <div className="w-44 border-r flex flex-col py-4 px-3 gap-1 bg-white shrink-0">
<div className="flex items-center gap-2 px-2 mb-4"> <div className="flex items-center gap-2 px-2 mb-4">
<div className="w-6 h-6 rounded" style={{ background: theme.primary }} /> <div className="w-6 h-6 rounded" style={{ background: theme.primary }} />
@@ -245,12 +244,10 @@ export function WebAppShadcn() {
</button> </button>
))} ))}
</div> </div>
{/* Main */}
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
<div className="h-12 border-b flex items-center justify-between px-5 shrink-0"> <div className="h-12 border-b flex items-center justify-between px-5 shrink-0">
<span className="font-semibold text-zinc-900 text-sm">{page}</span> <span className="font-semibold text-zinc-900 text-sm">{page}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<ThemeSwatches themes={SHADCN_THEMES} selected={theme} onSelect={setTheme} />
<div className="h-7 px-3 rounded-md border border-zinc-200 text-[11px] flex items-center text-zinc-600">Export</div> <div className="h-7 px-3 rounded-md border border-zinc-200 text-[11px] flex items-center text-zinc-600">Export</div>
<div className="h-7 px-3 rounded-md text-[11px] flex items-center" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</div> <div className="h-7 px-3 rounded-md text-[11px] flex items-center" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</div>
</div> </div>
@@ -357,9 +354,9 @@ function MantineSettings({ t }: { t: ThemeColor }) {
); );
} }
export function WebAppMantine() { export function WebAppMantine({ themeColor }: { themeColor?: ThemeColor }) {
const [page, setPage] = useState<Page>("Dashboard"); const [page, setPage] = useState<Page>("Dashboard");
const [theme, setTheme] = useState<ThemeColor>(MANTINE_THEMES[0]); const theme = themeColor ?? MANTINE_THEMES[0];
const NAV: { label: Page; icon: string }[] = [ const NAV: { label: Page; icon: string }[] = [
{ label: "Dashboard", icon: "▦" }, { label: "Dashboard", icon: "▦" },
{ label: "Users", icon: "◎" }, { label: "Users", icon: "◎" },
@@ -383,8 +380,7 @@ export function WebAppMantine() {
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
<div className="h-12 flex items-center justify-between px-5 shrink-0" style={{ background:"#fff", borderBottom:"1px solid #e9ecef" }}> <div className="h-12 flex items-center justify-between px-5 shrink-0" style={{ background:"#fff", borderBottom:"1px solid #e9ecef" }}>
<span className="font-bold text-sm" style={{ color: "#212529" }}>{page}</span> <span className="font-bold text-sm" style={{ color: "#212529" }}>{page}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<ThemeSwatches themes={MANTINE_THEMES} selected={theme} onSelect={setTheme} />
<button className="h-7 px-3 rounded text-[11px] font-medium" style={{ border:"1px solid #dee2e6", color:"#495057" }}>Export</button> <button className="h-7 px-3 rounded text-[11px] font-medium" style={{ border:"1px solid #dee2e6", color:"#495057" }}>Export</button>
<button className="h-7 px-3 rounded text-[11px] font-medium" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button> <button className="h-7 px-3 rounded text-[11px] font-medium" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button>
</div> </div>
@@ -491,9 +487,9 @@ function HeroUISettings() {
); );
} }
export function WebAppHeroUI() { export function WebAppHeroUI({ themeColor }: { themeColor?: ThemeColor }) {
const [page, setPage] = useState<Page>("Dashboard"); const [page, setPage] = useState<Page>("Dashboard");
const [theme, setTheme] = useState<ThemeColor>(HEROUI_THEMES[0]); const theme = themeColor ?? HEROUI_THEMES[0];
const NAV: { label: Page; icon: string }[] = [ const NAV: { label: Page; icon: string }[] = [
{ label: "Dashboard", icon: "▦" }, { label: "Dashboard", icon: "▦" },
{ label: "Users", icon: "◎" }, { label: "Users", icon: "◎" },
@@ -517,8 +513,7 @@ export function WebAppHeroUI() {
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
<div className="h-12 flex items-center justify-between px-5 bg-white shrink-0" style={{ borderBottom: "1px solid #f0f0f0" }}> <div className="h-12 flex items-center justify-between px-5 bg-white shrink-0" style={{ borderBottom: "1px solid #f0f0f0" }}>
<span className="font-bold text-sm" style={{ color: theme.primary }}>{page}</span> <span className="font-bold text-sm" style={{ color: theme.primary }}>{page}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<ThemeSwatches themes={HEROUI_THEMES} selected={theme} onSelect={setTheme} />
<button className="h-7 px-3 rounded-full text-[11px] font-medium" style={{ border:"1px solid #e4e4e7", color:"#71717a" }}>Export</button> <button className="h-7 px-3 rounded-full text-[11px] font-medium" style={{ border:"1px solid #e4e4e7", color:"#71717a" }}>Export</button>
<button className="h-7 px-3 rounded-full text-[11px] font-semibold" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button> <button className="h-7 px-3 rounded-full text-[11px] font-semibold" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button>
</div> </div>
@@ -632,9 +627,9 @@ function TremorSettings() {
); );
} }
export function WebAppTremor() { export function WebAppTremor({ themeColor }: { themeColor?: ThemeColor }) {
const [page, setPage] = useState<Page>("Dashboard"); const [page, setPage] = useState<Page>("Dashboard");
const [theme, setTheme] = useState<ThemeColor>(TREMOR_THEMES[0]); const theme = themeColor ?? TREMOR_THEMES[0];
const NAV: { label: Page; icon: string }[] = [ const NAV: { label: Page; icon: string }[] = [
{ label: "Dashboard", icon: "▦" }, { label: "Dashboard", icon: "▦" },
{ label: "Users", icon: "◎" }, { label: "Users", icon: "◎" },
@@ -658,8 +653,7 @@ export function WebAppTremor() {
<div className="flex-1 flex flex-col min-w-0" style={{ background:"#f9fafb" }}> <div className="flex-1 flex flex-col min-w-0" style={{ background:"#f9fafb" }}>
<div className="h-12 flex items-center justify-between px-5 bg-white shrink-0" style={{ borderBottom:"1px solid #e5e7eb" }}> <div className="h-12 flex items-center justify-between px-5 bg-white shrink-0" style={{ borderBottom:"1px solid #e5e7eb" }}>
<span className="font-bold text-sm" style={{ color:"#111827" }}>{page}</span> <span className="font-bold text-sm" style={{ color:"#111827" }}>{page}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<ThemeSwatches themes={TREMOR_THEMES} selected={theme} onSelect={setTheme} />
<button className="h-7 px-3 rounded-lg text-[11px] font-medium" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button> <button className="h-7 px-3 rounded-lg text-[11px] font-medium" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button>
</div> </div>
</div> </div>
@@ -675,11 +669,8 @@ export function WebAppTremor() {
// MARKETING scaffolds // MARKETING scaffolds
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function MarketingDaisy() { export function MarketingDaisy({ themeColor }: { themeColor?: ThemeColor }) {
const [theme, setTheme] = useState<ThemeColor>(DAISY_THEMES[0]); const theme = themeColor ?? DAISY_THEMES[0];
const isDark = !!theme.bg && ["#1d232a","#282a36","#1a103c","#171212","#20150e","#ffee00"].includes(theme.bg) === false
? theme.bg.startsWith("#1") || theme.bg.startsWith("#2") || theme.bg === "#ffee00"
: false;
const textColor = theme.textColor ?? "#f8f8f2"; const textColor = theme.textColor ?? "#f8f8f2";
const mutedText = theme.mutedText ?? "rgba(255,255,255,0.5)"; const mutedText = theme.mutedText ?? "rgba(255,255,255,0.5)";
const cardBg = theme.cardBg ?? "rgba(255,255,255,0.05)"; const cardBg = theme.cardBg ?? "rgba(255,255,255,0.05)";
@@ -697,7 +688,6 @@ export function MarketingDaisy() {
{["Features","Pricing","Docs","Blog"].map(i=><span key={i}>{i}</span>)} {["Features","Pricing","Docs","Blog"].map(i=><span key={i}>{i}</span>)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ThemeSwatches themes={DAISY_THEMES} selected={theme} onSelect={setTheme} />
<button className="h-7 px-3 rounded-full text-[11px] font-bold" style={{ background:`${borderColor}`, color: textColor }}>Login</button> <button className="h-7 px-3 rounded-full text-[11px] font-bold" style={{ background:`${borderColor}`, color: textColor }}>Login</button>
<button className="h-7 px-3 rounded-full text-[11px] font-bold" style={{ background: theme.primary, color: theme.primaryFg }}>Get started</button> <button className="h-7 px-3 rounded-full text-[11px] font-bold" style={{ background: theme.primary, color: theme.primaryFg }}>Get started</button>
</div> </div>
@@ -749,8 +739,8 @@ const HEROUI_MARKETING_THEMES: ThemeColor[] = [
{ id: "modern", label: "Modern", primary: "#06b6d4", primaryFg: "#fff", activeBg: "rgba(6,182,212,0.08)", activeFg: "#06b6d4", ring: "rgba(6,182,212,0.15)", bg: "#fff" }, { id: "modern", label: "Modern", primary: "#06b6d4", primaryFg: "#fff", activeBg: "rgba(6,182,212,0.08)", activeFg: "#06b6d4", ring: "rgba(6,182,212,0.15)", bg: "#fff" },
]; ];
export function MarketingHeroUI() { export function MarketingHeroUI({ themeColor }: { themeColor?: ThemeColor }) {
const [theme, setTheme] = useState<ThemeColor>(HEROUI_MARKETING_THEMES[0]); const theme = themeColor ?? HEROUI_MARKETING_THEMES[0];
const isDark = theme.id === "dark"; const isDark = theme.id === "dark";
const bg = theme.bg ?? "#fff"; const bg = theme.bg ?? "#fff";
const textColor = theme.textColor ?? "#18181b"; const textColor = theme.textColor ?? "#18181b";
@@ -769,7 +759,6 @@ export function MarketingHeroUI() {
{["Features","Pricing","Docs","Blog"].map(i=><span key={i}>{i}</span>)} {["Features","Pricing","Docs","Blog"].map(i=><span key={i}>{i}</span>)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ThemeSwatches themes={HEROUI_MARKETING_THEMES} selected={theme} onSelect={setTheme} />
<button className="h-7 px-3 rounded-full text-[11px]" style={{ border:`1px solid ${borderColor}`, color: mutedText }}>Login</button> <button className="h-7 px-3 rounded-full text-[11px]" style={{ border:`1px solid ${borderColor}`, color: mutedText }}>Login</button>
<button className="h-7 px-3 rounded-full text-[11px] font-semibold text-white" style={{ background: theme.primary }}>Get started</button> <button className="h-7 px-3 rounded-full text-[11px] font-semibold text-white" style={{ background: theme.primary }}>Get started</button>
</div> </div>
@@ -1118,7 +1107,7 @@ export function DocsShadcnCustom() {
// Registry — maps surface+theme to scaffold component // Registry — maps surface+theme to scaffold component
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentType>> = { export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentType<{ themeColor?: ThemeColor }>>> = {
"web-app": { "web-app": {
"shadcn": WebAppShadcn, "shadcn": WebAppShadcn,
"mantine": WebAppMantine, "mantine": WebAppMantine,
@@ -1148,3 +1137,17 @@ export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentTyp
"shadcn": DocsShadcnCustom, "shadcn": DocsShadcnCustom,
}, },
}; };
// Maps surface → library → available theme colors for that library
export const THEME_REGISTRY: Record<string, Record<string, ThemeColor[]>> = {
"web-app": {
"shadcn": SHADCN_THEMES,
"mantine": MANTINE_THEMES,
"hero-ui": HEROUI_THEMES,
"tremor": TREMOR_THEMES,
},
"marketing": {
"daisy-ui": DAISY_THEMES,
"hero-ui": HEROUI_MARKETING_THEMES,
},
};