site-builder: fix Menu guided panel (empty Colors) + surface layout/nav colors

The shared NavStylePanel gated its Colors controls on the Navbar's prop
names (backgroundColor/textColor/ctaColor), so a Menu -- whose color props
are linkColor/linkHoverColor/ctaBgColor/ctaTextColor -- rendered an empty
Colors section (customer report: 'nothing to select').

- Add navColorFields() helper: derives the visible color controls from the
  props actually present, covering both the Navbar and Menu schemas
  (+ Navbar's hoverColor, which render/toHtml consume but had no control).
- Add a Menu Layout section (alignment/orientation/gap/font size), guarded
  on Menu's own props so it never leaks into Navbar or a standalone Logo.
- Unit test (navColorFields.test.ts) locks each component to its real props.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 13:10:46 -07:00
parent d20b77e66d
commit 97cb439508
3 changed files with 153 additions and 8 deletions
@@ -9,6 +9,8 @@ import {
PresetButtonGrid,
CollapsibleSection,
ColorPickerField,
navColorFields,
btnActiveStyle,
labelStyle,
inputStyle,
smallInputStyle,
@@ -56,6 +58,21 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
/* Detect standalone Logo vs Navbar/Menu */
const isStandaloneLogo = nodeProps.type !== undefined && (nodeProps.type === 'text' || nodeProps.type === 'image') && nodeProps.logoText === undefined;
/* Color controls, derived from the props the selected component actually has */
const colorFields = navColorFields(nodeProps);
/* Menu layout controls (alignment/orientation/gap/font size). These props are
unique to the Menu component — Navbar uses navAlignment and has no
orientation/gap/fontSize — so guarding on their presence scopes this section
to the Menu without leaking into Navbar or a standalone Logo. */
const hasMenuLayout = !isStandaloneLogo && (
nodeProps.alignment !== undefined ||
nodeProps.orientation !== undefined ||
nodeProps.gap !== undefined ||
nodeProps.fontSize !== undefined
);
const GAP_PRESETS = ['8px', '16px', '24px', '32px', '40px'].map((g) => ({ label: g, value: g }));
return (
<>
{/* Standalone Logo component settings */}
@@ -165,17 +182,61 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
</CollapsibleSection>
)}
{/* Colors (not shown for standalone Logo - it has its own color picker) */}
{!isStandaloneLogo && (
{/* Colors (not shown for standalone Logo - it has its own color picker).
Fields are derived from whichever color props the selected component
actually has, so Menu (linkColor/ctaBg/…) and Navbar (backgroundColor/…)
each get the right controls instead of an empty section. */}
{!isStandaloneLogo && colorFields.length > 0 && (
<CollapsibleSection title="Colors">
{nodeProps.backgroundColor !== undefined && (
<ColorPickerField label="Background" value={nodeProps.backgroundColor || '#ffffff'} onChange={(v) => setProp('backgroundColor', v)} />
{colorFields.map((f) => (
<ColorPickerField
key={f.key}
label={f.label}
value={nodeProps[f.key] || f.fallback}
onChange={(v) => setProp(f.key, v)}
/>
))}
</CollapsibleSection>
)}
{/* Menu layout (alignment / orientation / gap / font size) */}
{hasMenuLayout && (
<CollapsibleSection title="Layout">
{nodeProps.alignment !== undefined && (
<div style={sectionGap}>
<label style={labelStyle}>Alignment</label>
<div style={{ display: 'flex', gap: 4 }}>
{(['left', 'center', 'right'] as const).map((a) => (
<button key={a} onClick={() => setProp('alignment', a)} style={btnActiveStyle((nodeProps.alignment || 'right') === a)}>
{a.charAt(0).toUpperCase() + a.slice(1)}
</button>
))}
</div>
</div>
)}
{nodeProps.textColor !== undefined && (
<ColorPickerField label="Text Color" value={nodeProps.textColor || '#18181b'} onChange={(v) => setProp('textColor', v)} />
{nodeProps.orientation !== undefined && (
<div style={sectionGap}>
<label style={labelStyle}>Orientation</label>
<div style={{ display: 'flex', gap: 4 }}>
{(['horizontal', 'vertical'] as const).map((o) => (
<button key={o} onClick={() => setProp('orientation', o)} style={btnActiveStyle((nodeProps.orientation || 'horizontal') === o)}>
{o.charAt(0).toUpperCase() + o.slice(1)}
</button>
))}
</div>
</div>
)}
{nodeProps.ctaColor !== undefined && (
<ColorPickerField label="CTA Color" value={nodeProps.ctaColor || '#3b82f6'} onChange={(v) => setProp('ctaColor', v)} />
{nodeProps.gap !== undefined && (
<div style={sectionGap}>
<label style={labelStyle}>Gap</label>
<PresetButtonGrid presets={GAP_PRESETS} activeValue={nodeProps.gap} onSelect={(v) => setProp('gap', v)} />
</div>
)}
{nodeProps.fontSize !== undefined && (
<div style={sectionGap}>
<label style={labelStyle}>Font Size</label>
<input type="text" value={nodeProps.fontSize || '14px'} onChange={(e) => setProp('fontSize', e.target.value)} placeholder="14px" style={inputStyle} />
</div>
)}
</CollapsibleSection>
)}