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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user