Design panel: correct order + fix Lock In saving

- Right panel order now: Lock → Library → Mode → Colour → Font →
  Background → Nav → Hero → Sections
- Lock In was always disabled because selectedThemeId was null until
  user explicitly clicked a library button; now uses previewId (which
  defaults to first theme) for the disabled check
- Added useEffect to notify parent of the default library selection on
  mount so handleLock always has a theme to save
- handleLock also falls back to first theme as double safety net

Made-with: Cursor
This commit is contained in:
2026-03-06 10:39:11 -08:00
parent ef9f5a6ad3
commit a1b605febf

View File

@@ -555,10 +555,24 @@ function SurfaceSection({
}
const patchConfig = (patch: Partial<DesignConfig>) => setDesignConfig(prev => ({ ...prev, ...patch }));
const hasConfigurator = !!previewId && !!LIBRARY_STYLE_OPTIONS[previewId];
const opts = previewId ? LIBRARY_STYLE_OPTIONS[previewId] : null;
const hasConfigurator = !!previewId && !!opts;
const isLocked = !!lockedThemeId;
// Ensure parent always knows the currently-displayed theme (even before user clicks)
// so Lock In works immediately without requiring an explicit library click first.
useEffect(() => {
if (!selectedThemeId && previewId) onSelect(previewId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [previewId]);
const toggleComponent = (id: string) => {
const next = designConfig.components.includes(id)
? designConfig.components.filter(c => c !== id)
: [...designConfig.components, id];
patchConfig({ components: next });
};
return (
<div style={{ display: "flex", flexDirection: "row", height: "100%", gap: 0 }}>
@@ -605,7 +619,6 @@ function SurfaceSection({
}
</div>
</div>
</div>{/* end center scaffold wrapper */}
{/* Right — controls panel */}
@@ -616,7 +629,7 @@ function SurfaceSection({
}}>
<div style={{ padding: "14px 16px", display: "flex", flexDirection: "column", gap: 12 }}>
{/* Lock / unlock — top of panel */}
{/* 1. Lock / unlock */}
<div style={{ display: "flex", alignItems: "center", gap: 8, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
{isLocked ? (
<button
@@ -632,16 +645,16 @@ function SurfaceSection({
) : (
<button
onClick={onLock}
disabled={!selectedThemeId || saving}
disabled={!previewId || saving}
style={{
flex: 1, padding: "7px 14px", borderRadius: 7, border: "1px solid #1a1a1a",
background: !selectedThemeId || saving ? "#e0dcd4" : "#1a1a1a",
color: !selectedThemeId || saving ? "#b5b0a6" : "#fff",
flex: 1, padding: "7px 14px", borderRadius: 7, border: `1px solid ${previewId && !saving ? "#1a1a1a" : "#e0dcd4"}`,
background: previewId && !saving ? "#1a1a1a" : "#e0dcd4",
color: previewId && !saving ? "#fff" : "#b5b0a6",
fontSize: "0.76rem", fontWeight: 600, fontFamily: "Outfit",
cursor: !selectedThemeId || saving ? "not-allowed" : "pointer",
cursor: !previewId || saving ? "not-allowed" : "pointer",
transition: "opacity 0.15s",
}}
onMouseEnter={e => { if (selectedThemeId && !saving) (e.currentTarget.style.opacity = "0.8"); }}
onMouseEnter={e => { if (previewId && !saving) (e.currentTarget.style.opacity = "0.8"); }}
onMouseLeave={e => { (e.currentTarget.style.opacity = "1"); }}
>{saving ? "Saving…" : "🔒 Lock in"}</button>
)}
@@ -654,37 +667,7 @@ function SurfaceSection({
)}
</div>
{/* Palette (colour) — top of mind, shown right after Lock */}
{availableColorThemes.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
<div style={{ display: "flex", gap: 7, flexWrap: "wrap", alignItems: "center" }}>
{availableColorThemes.map(ct => (
<button
key={ct.id}
title={ct.label}
onClick={() => !isLocked && setSelectedColorTheme(ct)}
disabled={isLocked}
style={{
width: 22, height: 22, borderRadius: "50%", border: "none", cursor: isLocked ? "not-allowed" : "pointer",
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
outlineOffset: 2, transition: "transform 0.12s",
opacity: isLocked ? 0.4 : 1,
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
}}
onMouseEnter={e => { if (!isLocked) (e.currentTarget as HTMLElement).style.transform = "scale(1.18)"; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; }}
/>
))}
</div>
{activeColorTheme && (
<span style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "Outfit" }}>{activeColorTheme.label}</span>
)}
</div>
)}
{/* Library — simple name buttons */}
{/* 2. Library */}
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Library</span>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
@@ -717,13 +700,112 @@ function SurfaceSection({
</div>
</div>
{/* Design configurator — mode, background, nav, header, sections, font */}
{hasConfigurator && !isLocked && (
<DesignConfigurator
libraryId={previewId!}
config={designConfig}
onChange={patchConfig}
/>
{hasConfigurator && !isLocked && opts && (<>
{/* 3. Mode */}
<ConfigRow label="Mode">
<ModeToggle value={designConfig.mode} onChange={v => patchConfig({ mode: v })} />
</ConfigRow>
{/* 4. Colour */}
{availableColorThemes.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 7, paddingBottom: 12, borderBottom: "1px solid #f0ece4" }}>
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
<div style={{ display: "flex", gap: 7, flexWrap: "wrap", alignItems: "center" }}>
{availableColorThemes.map(ct => (
<button
key={ct.id}
title={ct.label}
onClick={() => setSelectedColorTheme(ct)}
style={{
width: 22, height: 22, borderRadius: "50%", border: "none", cursor: "pointer",
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
outlineOffset: 2, transition: "transform 0.12s",
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1.18)"; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; }}
/>
))}
</div>
{activeColorTheme && (
<span style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "Outfit" }}>{activeColorTheme.label}</span>
)}
</div>
)}
{/* 5. Font */}
<ConfigRow label="Font">
{opts.fonts.map(f => (
<OptionChip key={f.id} label={f.label}
active={designConfig.font === f.id}
onClick={() => patchConfig({ font: f.id })}
/>
))}
</ConfigRow>
{/* 6. Background */}
<ConfigRow label="Background">
{opts.backgrounds.map(bg => (
<OptionChip key={bg.id} label={bg.label} description={bg.description}
active={designConfig.background === bg.id}
onClick={() => patchConfig({ background: bg.id })}
/>
))}
</ConfigRow>
{/* 7. Nav */}
<ConfigRow label="Nav style">
{opts.navStyles.map(n => (
<OptionChip key={n.id} label={n.label} description={n.description}
active={designConfig.nav === n.id}
onClick={() => patchConfig({ nav: n.id })}
/>
))}
</ConfigRow>
{/* 8. Hero */}
<ConfigRow label="Hero header">
{opts.headerStyles.map(h => (
<OptionChip key={h.id} label={h.label} description={h.description}
active={designConfig.header === h.id}
onClick={() => patchConfig({ header: h.id })}
/>
))}
</ConfigRow>
{/* 9. Sections */}
<ConfigRow label="Sections">
{opts.components.map(c => (
<OptionChip key={c.id} label={c.label} multi
active={designConfig.components.includes(c.id)}
checked={designConfig.components.includes(c.id)}
onClick={() => toggleComponent(c.id)}
/>
))}
</ConfigRow>
</>)}
{/* Colour swatches when locked (read-only) */}
{isLocked && availableColorThemes.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
<span style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "Outfit" }}>Colour</span>
<div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
{availableColorThemes.map(ct => (
<button key={ct.id} title={ct.label} disabled
style={{
width: 22, height: 22, borderRadius: "50%", border: "none",
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
outline: activeColorTheme?.id === ct.id ? `2.5px solid ${ct.primary}` : "2px solid transparent",
outlineOffset: 2, opacity: 0.4, cursor: "not-allowed",
boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
}}
/>
))}
</div>
</div>
)}
</div>{/* end inner padding div */}
@@ -931,7 +1013,8 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
};
const handleLock = async (surfaceId: string) => {
const themeId = selectedThemes[surfaceId];
const surface = ALL_SURFACES.find(s => s.id === surfaceId);
const themeId = selectedThemes[surfaceId] ?? surface?.themes[0]?.id;
if (!themeId) return;
setSavingLock(surfaceId);
try {