import React, { useState } from 'react'; import { useSiteDesign, DEFAULT_SITE_DESIGN } from '../../state/SiteDesignContext'; import { usePages } from '../../state/PageContext'; import { useEditorConfig } from '../../state/EditorConfigContext'; import { SerializedTreeNode } from '../../types/sitesmith'; import { FONT_FAMILIES } from '../../constants/presets'; import { AssetPicker } from '../../ui/AssetPicker'; import { inputStyle } from './styles/shared'; type DesignTab = 'basic' | 'advanced'; /* ---------- Color picker with preset swatches ---------- */ const COLOR_SWATCHES = [ '#3b82f6', '#8b5cf6', '#10b981', '#ef4444', '#f59e0b', '#ec4899', '#06b6d4', '#f97316', ]; const NEUTRAL_SWATCHES = [ '#ffffff', '#f9fafb', '#e5e7eb', '#9ca3af', '#6b7280', '#374151', '#1f2937', '#111827', ]; interface ColorFieldProps { label: string; value: string; onChange: (value: string) => void; swatches?: string[]; } const ColorField: React.FC = ({ label, value, onChange, swatches = COLOR_SWATCHES }) => (
onChange(e.target.value)} style={{ width: 32, height: 32, padding: 0, border: '2px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer', background: 'none', }} /> onChange(e.target.value)} style={{ flex: 1, fontSize: 11, fontFamily: 'monospace' }} />
{swatches.map((c) => (
); /* ---------- Font dropdown ---------- */ interface FontFieldProps { label: string; value: string; onChange: (value: string) => void; } const FontField: React.FC = ({ label, value, onChange }) => (
); /* ---------- Border radius presets ---------- */ const BORDER_RADIUS_PRESETS = [ { label: '0', value: '0' }, { label: '4px', value: '4px' }, { label: '8px', value: '8px' }, { label: '12px', value: '12px' }, { label: '16px', value: '16px' }, ]; interface RadiusFieldProps { label: string; value: string; onChange: (value: string) => void; } const RadiusField: React.FC = ({ label, value, onChange }) => (
{BORDER_RADIUS_PRESETS.map((p) => ( ))}
); /* ---------- Nav style toggle ---------- */ interface NavStyleFieldProps { value: 'light' | 'dark'; onChange: (value: 'light' | 'dark') => void; } const NavStyleField: React.FC = ({ value, onChange }) => (
{(['light', 'dark'] as const).map((s) => ( ))}
); /* ---------- Reset Entire Site (danger zone) ---------- */ // Blank replacement trees for the "Reset Entire Site" escape hatch. Shaped // as a `SerializedTreeNode` (type + props + nodes), NOT a flat Craft.js // state entry -- this is what `replaceAllPages`/`setHeader`/`setFooter` // feed into `treeToCraftState` -> `sanitizeAiTree`/`flattenTreeForCraft` // (see `state/PageContext.tsx`/`utils/craft-tree.ts`). A single `Container` // root with no children mirrors the (unexported) EMPTY_CANVAS/EMPTY_HEADER/ // EMPTY_FOOTER constants already used elsewhere for "blank". const BLANK_PAGE_TREE: SerializedTreeNode = { type: { resolvedName: 'Container' }, props: { style: { minHeight: '100vh', backgroundColor: '#ffffff' }, tag: 'div' }, nodes: [], }; const BLANK_HEADER_TREE: SerializedTreeNode = { type: { resolvedName: 'Container' }, props: { style: { minHeight: '60px', backgroundColor: '#ffffff', padding: '12px 24px', display: 'flex', alignItems: 'center' }, tag: 'header', }, nodes: [], }; const BLANK_FOOTER_TREE: SerializedTreeNode = { type: { resolvedName: 'Container' }, props: { style: { minHeight: '60px', backgroundColor: '#0f172a', color: '#94a3b8', padding: '40px 24px', textAlign: 'center' }, tag: 'footer', }, nodes: [], }; /** * Single source of truth for "has the user typed enough to arm the confirm * button" -- used for the `disabled` attribute/cursor/opacity AND, more * importantly, inside `handleResetSite` itself. `disabled` is a UI * affordance, not a safety mechanism (it only stops a plain mouse click); * for the one action in this app that irreversibly wipes a user's whole * site draft, the real guard has to live in the handler, checked against * this exact same predicate rather than a second hand-rolled comparison * that could drift out of sync with it. * * An empty/falsy `domain` always returns `false`, even if `typed` is also * empty -- `''.trim() === ''.trim()` would otherwise "match" trivially. * Standalone mode (no `WHP_CONFIG`) has `siteDomain === ''`, and while the * entry point that would let a user reach this code is hidden in that case * (see the `siteDomain &&` guard around the whole danger zone below), this * function must not depend on that -- it has to fail safe on its own if * ever reached with no configured domain. * * Exported so it's directly unit-testable without needing a way to render * the (deliberately unreachable-when-`siteDomain`-is-empty) confirm UI. */ export function siteResetConfirmMatches(typed: string, domain: string): boolean { return !!domain && typed.trim() === domain.trim(); } /* ---------- Main SiteDesignPanel ---------- */ export const SiteDesignPanel: React.FC = () => { const { design, updateDesign, resetToDefaults } = useSiteDesign(); const { replaceAllPages, setHeader, setFooter } = usePages(); const { whpConfig } = useEditorConfig(); // Standalone mode (no WHP_CONFIG) has no site domain to confirm against -- // an empty typed input would then trivially "match" an empty siteDomain, // arming the destructive confirm button with no guard at all. The entry // point itself is hidden in that case (see the `siteDomain &&` guard below). const siteDomain = whpConfig?.siteDomain ?? ''; const [siteResetOpen, setSiteResetOpen] = useState(false); const [siteResetTyped, setSiteResetTyped] = useState(''); const [tab, setTab] = useState('basic'); const handleResetSite = (): void => { // Load-bearing guard -- see `siteResetConfirmMatches`'s docstring. Not // just a UI nicety: this must hold even if the confirm button's // `disabled` attribute were ever bypassed (a synthetic click, or a // future refactor that drops it). if (!siteResetConfirmMatches(siteResetTyped, siteDomain)) return; replaceAllPages([{ name: 'Home', tree: BLANK_PAGE_TREE }]); setHeader(BLANK_HEADER_TREE); setFooter(BLANK_FOOTER_TREE); resetToDefaults(); setSiteResetOpen(false); setSiteResetTyped(''); }; return (
{/* Header */}
Site Design Tokens
{/* Tab switcher */}
{(['basic', 'advanced'] as DesignTab[]).map((t) => ( ))}
{/* Basic tab */} {tab === 'basic' && ( <>
updateDesign({ favicon: url })} mediaType="image" variant="full" placeholder="Or paste favicon URL..." />

Shown in browser tabs and bookmarks. Applies site-wide.

updateDesign({ primaryColor: v })} /> updateDesign({ secondaryColor: v })} /> updateDesign({ accentColor: v })} /> updateDesign({ headingFont: v })} /> updateDesign({ bodyFont: v })} /> updateDesign({ linkColor: v })} /> )} {/* Advanced tab */} {tab === 'advanced' && ( <> updateDesign({ primaryColor: v })} /> updateDesign({ secondaryColor: v })} /> updateDesign({ accentColor: v })} /> updateDesign({ successColor: v })} /> updateDesign({ warningColor: v })} /> updateDesign({ errorColor: v })} /> updateDesign({ backgroundColor: v })} swatches={NEUTRAL_SWATCHES} /> updateDesign({ textColor: v })} swatches={NEUTRAL_SWATCHES} /> updateDesign({ mutedTextColor: v })} swatches={NEUTRAL_SWATCHES} /> updateDesign({ borderColor: v })} swatches={NEUTRAL_SWATCHES} /> updateDesign({ headingFont: v })} /> updateDesign({ bodyFont: v })} /> updateDesign({ buttonFont: v })} /> updateDesign({ linkColor: v })} /> updateDesign({ borderRadius: v })} /> updateDesign({ buttonRadius: v })} /> updateDesign({ navStyle: v })} /> )} {/* Reset button */}

Design tokens are saved with your project and applied to new components.

{/* Reset Entire Site (danger zone) -- hidden entirely in standalone mode, where siteDomain is '' and typing nothing would trivially satisfy an empty-string match. */} {siteDomain && (
Danger zone
{!siteResetOpen ? ( ) : ( <>

This blanks every page, the header, the footer, all design tokens (colors, fonts, radii), and your custom head code and favicon, leaving one empty Home page. Uploaded images are kept. This cannot be undone. Your published site stays as it is until you publish again — but the editor auto-saves roughly every 30 seconds, so the blank version becomes your saved draft shortly after.

Type {siteDomain} to confirm:

setSiteResetTyped(e.target.value)} placeholder={siteDomain} style={inputStyle} />
)}
)}
); };