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:
@@ -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>
|
||||
|
||||
@@ -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 [theme, setTheme] = useState<ThemeColor>(SHADCN_THEMES[0]);
|
||||
const theme = themeColor ?? SHADCN_THEMES[0];
|
||||
const NAV_ITEMS: { label: Page; icon: string }[] = [
|
||||
{ label: "Dashboard", icon: "▦" },
|
||||
{ label: "Users", icon: "◎" },
|
||||
@@ -231,7 +231,6 @@ export function WebAppShadcn() {
|
||||
];
|
||||
return (
|
||||
<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="flex items-center gap-2 px-2 mb-4">
|
||||
<div className="w-6 h-6 rounded" style={{ background: theme.primary }} />
|
||||
@@ -245,12 +244,10 @@ export function WebAppShadcn() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Main */}
|
||||
<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">
|
||||
<span className="font-semibold text-zinc-900 text-sm">{page}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeSwatches themes={SHADCN_THEMES} selected={theme} onSelect={setTheme} />
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
@@ -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 [theme, setTheme] = useState<ThemeColor>(MANTINE_THEMES[0]);
|
||||
const theme = themeColor ?? MANTINE_THEMES[0];
|
||||
const NAV: { label: Page; icon: string }[] = [
|
||||
{ label: "Dashboard", icon: "▦" },
|
||||
{ label: "Users", icon: "◎" },
|
||||
@@ -383,8 +380,7 @@ export function WebAppMantine() {
|
||||
<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" }}>
|
||||
<span className="font-bold text-sm" style={{ color: "#212529" }}>{page}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeSwatches themes={MANTINE_THEMES} selected={theme} onSelect={setTheme} />
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
@@ -491,9 +487,9 @@ function HeroUISettings() {
|
||||
);
|
||||
}
|
||||
|
||||
export function WebAppHeroUI() {
|
||||
export function WebAppHeroUI({ themeColor }: { themeColor?: ThemeColor }) {
|
||||
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 }[] = [
|
||||
{ label: "Dashboard", icon: "▦" },
|
||||
{ label: "Users", icon: "◎" },
|
||||
@@ -517,8 +513,7 @@ export function WebAppHeroUI() {
|
||||
<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" }}>
|
||||
<span className="font-bold text-sm" style={{ color: theme.primary }}>{page}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeSwatches themes={HEROUI_THEMES} selected={theme} onSelect={setTheme} />
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
@@ -632,9 +627,9 @@ function TremorSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
export function WebAppTremor() {
|
||||
export function WebAppTremor({ themeColor }: { themeColor?: ThemeColor }) {
|
||||
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 }[] = [
|
||||
{ label: "Dashboard", 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="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>
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeSwatches themes={TREMOR_THEMES} selected={theme} onSelect={setTheme} />
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="h-7 px-3 rounded-lg text-[11px] font-medium" style={{ background: theme.primary, color: theme.primaryFg }}>+ New</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -675,11 +669,8 @@ export function WebAppTremor() {
|
||||
// MARKETING scaffolds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function MarketingDaisy() {
|
||||
const [theme, setTheme] = useState<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;
|
||||
export function MarketingDaisy({ themeColor }: { themeColor?: ThemeColor }) {
|
||||
const theme = themeColor ?? DAISY_THEMES[0];
|
||||
const textColor = theme.textColor ?? "#f8f8f2";
|
||||
const mutedText = theme.mutedText ?? "rgba(255,255,255,0.5)";
|
||||
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>)}
|
||||
</div>
|
||||
<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: theme.primary, color: theme.primaryFg }}>Get started</button>
|
||||
</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" },
|
||||
];
|
||||
|
||||
export function MarketingHeroUI() {
|
||||
const [theme, setTheme] = useState<ThemeColor>(HEROUI_MARKETING_THEMES[0]);
|
||||
export function MarketingHeroUI({ themeColor }: { themeColor?: ThemeColor }) {
|
||||
const theme = themeColor ?? HEROUI_MARKETING_THEMES[0];
|
||||
const isDark = theme.id === "dark";
|
||||
const bg = theme.bg ?? "#fff";
|
||||
const textColor = theme.textColor ?? "#18181b";
|
||||
@@ -769,7 +759,6 @@ export function MarketingHeroUI() {
|
||||
{["Features","Pricing","Docs","Blog"].map(i=><span key={i}>{i}</span>)}
|
||||
</div>
|
||||
<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] font-semibold text-white" style={{ background: theme.primary }}>Get started</button>
|
||||
</div>
|
||||
@@ -1118,7 +1107,7 @@ export function DocsShadcnCustom() {
|
||||
// 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": {
|
||||
"shadcn": WebAppShadcn,
|
||||
"mantine": WebAppMantine,
|
||||
@@ -1148,3 +1137,17 @@ export const SCAFFOLD_REGISTRY: Record<string, Record<string, React.ComponentTyp
|
||||
"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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user