2026-04-05 18:31:16 -07:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useSiteDesign, DEFAULT_SITE_DESIGN } from '../../state/SiteDesignContext';
|
2026-08-09 10:17:53 -07:00
|
|
|
import { usePages } from '../../state/PageContext';
|
|
|
|
|
import { useEditorConfig } from '../../state/EditorConfigContext';
|
|
|
|
|
import { SerializedTreeNode } from '../../types/sitesmith';
|
2026-04-05 18:31:16 -07:00
|
|
|
import { FONT_FAMILIES } from '../../constants/presets';
|
2026-07-14 07:14:56 -07:00
|
|
|
import { AssetPicker } from '../../ui/AssetPicker';
|
2026-08-09 10:17:53 -07:00
|
|
|
import { inputStyle } from './styles/shared';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
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<ColorFieldProps> = ({ label, value, onChange, swatches = COLOR_SWATCHES }) => (
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<label className="guided-section-label">{label}</label>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
|
|
|
<input
|
|
|
|
|
type="color"
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => 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',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
className="guided-input"
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => onChange(e.target.value)}
|
|
|
|
|
style={{ flex: 1, fontSize: 11, fontFamily: 'monospace' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: 4 }}>
|
|
|
|
|
{swatches.map((c) => (
|
|
|
|
|
<button
|
|
|
|
|
key={c}
|
|
|
|
|
onClick={() => onChange(c)}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%',
|
|
|
|
|
aspectRatio: '1',
|
|
|
|
|
borderRadius: 'var(--radius-sm)',
|
|
|
|
|
border: value === c ? '2px solid var(--color-accent)' : '2px solid var(--color-border)',
|
|
|
|
|
background: c,
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
boxShadow: value === c ? '0 0 0 2px var(--color-accent)' : 'none',
|
|
|
|
|
transition: 'border-color 0.15s, box-shadow 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
title={c}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/* ---------- Font dropdown ---------- */
|
|
|
|
|
|
|
|
|
|
interface FontFieldProps {
|
|
|
|
|
label: string;
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FontField: React.FC<FontFieldProps> = ({ label, value, onChange }) => (
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<label className="guided-section-label">{label}</label>
|
|
|
|
|
<select
|
|
|
|
|
className="control-select"
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => onChange(e.target.value)}
|
|
|
|
|
style={{ fontSize: 12 }}
|
|
|
|
|
>
|
|
|
|
|
{FONT_FAMILIES.map((f) => (
|
|
|
|
|
<option key={f.value} value={f.value} style={{ fontFamily: f.value }}>
|
|
|
|
|
{f.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/* ---------- 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<RadiusFieldProps> = ({ label, value, onChange }) => (
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<label className="guided-section-label">{label}</label>
|
|
|
|
|
<div className="preset-grid" style={{ gridTemplateColumns: 'repeat(5, 1fr)' }}>
|
|
|
|
|
{BORDER_RADIUS_PRESETS.map((p) => (
|
|
|
|
|
<button
|
|
|
|
|
key={p.value}
|
|
|
|
|
className={`preset-btn ${value === p.value ? 'active' : ''}`}
|
|
|
|
|
onClick={() => onChange(p.value)}
|
|
|
|
|
>
|
|
|
|
|
{p.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/* ---------- Nav style toggle ---------- */
|
|
|
|
|
|
|
|
|
|
interface NavStyleFieldProps {
|
|
|
|
|
value: 'light' | 'dark';
|
|
|
|
|
onChange: (value: 'light' | 'dark') => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NavStyleField: React.FC<NavStyleFieldProps> = ({ value, onChange }) => (
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<label className="guided-section-label">Nav Style</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
|
|
|
{(['light', 'dark'] as const).map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s}
|
|
|
|
|
className={`preset-btn ${value === s ? 'active' : ''}`}
|
|
|
|
|
style={{ flex: 1, textTransform: 'capitalize' }}
|
|
|
|
|
onClick={() => onChange(s)}
|
|
|
|
|
>
|
|
|
|
|
{s === 'light' ? 'Light' : 'Dark'}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-09 10:17:53 -07:00
|
|
|
/* ---------- 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: [],
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-09 10:30:20 -07:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
/* ---------- Main SiteDesignPanel ---------- */
|
|
|
|
|
|
|
|
|
|
export const SiteDesignPanel: React.FC = () => {
|
|
|
|
|
const { design, updateDesign, resetToDefaults } = useSiteDesign();
|
2026-08-09 10:17:53 -07:00
|
|
|
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('');
|
2026-04-05 18:31:16 -07:00
|
|
|
const [tab, setTab] = useState<DesignTab>('basic');
|
|
|
|
|
|
2026-08-09 10:17:53 -07:00
|
|
|
const handleResetSite = (): void => {
|
2026-08-09 10:30:20 -07:00
|
|
|
// 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;
|
|
|
|
|
|
2026-08-09 10:17:53 -07:00
|
|
|
replaceAllPages([{ name: 'Home', tree: BLANK_PAGE_TREE }]);
|
|
|
|
|
setHeader(BLANK_HEADER_TREE);
|
|
|
|
|
setFooter(BLANK_FOOTER_TREE);
|
|
|
|
|
resetToDefaults();
|
|
|
|
|
setSiteResetOpen(false);
|
|
|
|
|
setSiteResetTyped('');
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
return (
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
<i className="fa fa-paint-brush" style={{ color: 'var(--color-accent)', fontSize: 14 }} />
|
|
|
|
|
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
|
|
|
|
|
Site Design Tokens
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Tab switcher */}
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
|
|
|
|
|
{(['basic', 'advanced'] as DesignTab[]).map((t) => (
|
|
|
|
|
<button
|
|
|
|
|
key={t}
|
|
|
|
|
onClick={() => setTab(t)}
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1,
|
|
|
|
|
padding: '6px 12px',
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.5px',
|
|
|
|
|
color: tab === t ? '#fff' : 'var(--color-text-muted)',
|
|
|
|
|
background: tab === t ? 'var(--color-accent)' : 'var(--color-bg-elevated)',
|
|
|
|
|
border: tab === t ? '1px solid var(--color-accent)' : '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 'var(--radius-sm)',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s ease',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Basic tab */}
|
|
|
|
|
{tab === 'basic' && (
|
|
|
|
|
<>
|
2026-07-14 07:14:56 -07:00
|
|
|
<div className="guided-section">
|
|
|
|
|
<label className="guided-section-label">Favicon</label>
|
|
|
|
|
<AssetPicker
|
|
|
|
|
value={design.favicon}
|
|
|
|
|
onChange={(url) => updateDesign({ favicon: url })}
|
|
|
|
|
mediaType="image"
|
|
|
|
|
variant="full"
|
|
|
|
|
placeholder="Or paste favicon URL..."
|
|
|
|
|
/>
|
|
|
|
|
<p style={{ fontSize: 10, color: 'var(--color-text-dim)', margin: '4px 0 0' }}>
|
|
|
|
|
Shown in browser tabs and bookmarks. Applies site-wide.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-04-05 18:31:16 -07:00
|
|
|
<ColorField
|
|
|
|
|
label="Primary Color"
|
|
|
|
|
value={design.primaryColor}
|
|
|
|
|
onChange={(v) => updateDesign({ primaryColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Secondary Color"
|
|
|
|
|
value={design.secondaryColor}
|
|
|
|
|
onChange={(v) => updateDesign({ secondaryColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Accent Color"
|
|
|
|
|
value={design.accentColor}
|
|
|
|
|
onChange={(v) => updateDesign({ accentColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<FontField
|
|
|
|
|
label="Heading Font"
|
|
|
|
|
value={design.headingFont}
|
|
|
|
|
onChange={(v) => updateDesign({ headingFont: v })}
|
|
|
|
|
/>
|
|
|
|
|
<FontField
|
|
|
|
|
label="Body Font"
|
|
|
|
|
value={design.bodyFont}
|
|
|
|
|
onChange={(v) => updateDesign({ bodyFont: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Link Color"
|
|
|
|
|
value={design.linkColor}
|
|
|
|
|
onChange={(v) => updateDesign({ linkColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Advanced tab */}
|
|
|
|
|
{tab === 'advanced' && (
|
|
|
|
|
<>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Primary Color"
|
|
|
|
|
value={design.primaryColor}
|
|
|
|
|
onChange={(v) => updateDesign({ primaryColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Secondary Color"
|
|
|
|
|
value={design.secondaryColor}
|
|
|
|
|
onChange={(v) => updateDesign({ secondaryColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Accent Color"
|
|
|
|
|
value={design.accentColor}
|
|
|
|
|
onChange={(v) => updateDesign({ accentColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Success Color"
|
|
|
|
|
value={design.successColor}
|
|
|
|
|
onChange={(v) => updateDesign({ successColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Warning Color"
|
|
|
|
|
value={design.warningColor}
|
|
|
|
|
onChange={(v) => updateDesign({ warningColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Error Color"
|
|
|
|
|
value={design.errorColor}
|
|
|
|
|
onChange={(v) => updateDesign({ errorColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Background Color"
|
|
|
|
|
value={design.backgroundColor}
|
|
|
|
|
onChange={(v) => updateDesign({ backgroundColor: v })}
|
|
|
|
|
swatches={NEUTRAL_SWATCHES}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Text Color"
|
|
|
|
|
value={design.textColor}
|
|
|
|
|
onChange={(v) => updateDesign({ textColor: v })}
|
|
|
|
|
swatches={NEUTRAL_SWATCHES}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Muted Text Color"
|
|
|
|
|
value={design.mutedTextColor}
|
|
|
|
|
onChange={(v) => updateDesign({ mutedTextColor: v })}
|
|
|
|
|
swatches={NEUTRAL_SWATCHES}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Border Color"
|
|
|
|
|
value={design.borderColor}
|
|
|
|
|
onChange={(v) => updateDesign({ borderColor: v })}
|
|
|
|
|
swatches={NEUTRAL_SWATCHES}
|
|
|
|
|
/>
|
|
|
|
|
<FontField
|
|
|
|
|
label="Heading Font"
|
|
|
|
|
value={design.headingFont}
|
|
|
|
|
onChange={(v) => updateDesign({ headingFont: v })}
|
|
|
|
|
/>
|
|
|
|
|
<FontField
|
|
|
|
|
label="Body Font"
|
|
|
|
|
value={design.bodyFont}
|
|
|
|
|
onChange={(v) => updateDesign({ bodyFont: v })}
|
|
|
|
|
/>
|
|
|
|
|
<FontField
|
|
|
|
|
label="Button Font"
|
|
|
|
|
value={design.buttonFont}
|
|
|
|
|
onChange={(v) => updateDesign({ buttonFont: v })}
|
|
|
|
|
/>
|
|
|
|
|
<ColorField
|
|
|
|
|
label="Link Color"
|
|
|
|
|
value={design.linkColor}
|
|
|
|
|
onChange={(v) => updateDesign({ linkColor: v })}
|
|
|
|
|
/>
|
|
|
|
|
<RadiusField
|
|
|
|
|
label="Default Border Radius"
|
|
|
|
|
value={design.borderRadius}
|
|
|
|
|
onChange={(v) => updateDesign({ borderRadius: v })}
|
|
|
|
|
/>
|
|
|
|
|
<RadiusField
|
|
|
|
|
label="Button Radius"
|
|
|
|
|
value={design.buttonRadius}
|
|
|
|
|
onChange={(v) => updateDesign({ buttonRadius: v })}
|
|
|
|
|
/>
|
|
|
|
|
<NavStyleField
|
|
|
|
|
value={design.navStyle}
|
|
|
|
|
onChange={(v) => updateDesign({ navStyle: v })}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Reset button */}
|
|
|
|
|
<div style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border)' }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={resetToDefaults}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
color: 'var(--color-text-muted)',
|
|
|
|
|
background: 'var(--color-bg-elevated)',
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 'var(--radius-sm)',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s ease',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Reset to Defaults
|
|
|
|
|
</button>
|
|
|
|
|
<p style={{
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
color: 'var(--color-text-dim)',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
margin: '8px 0 0',
|
|
|
|
|
lineHeight: 1.4,
|
|
|
|
|
}}>
|
|
|
|
|
Design tokens are saved with your project and applied to new components.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-08-09 10:17:53 -07:00
|
|
|
{/* Reset Entire Site (danger zone) -- hidden entirely in standalone
|
|
|
|
|
mode, where siteDomain is '' and typing nothing would trivially
|
|
|
|
|
satisfy an empty-string match. */}
|
|
|
|
|
{siteDomain && (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
marginTop: 18, padding: 12,
|
|
|
|
|
border: '1px solid rgba(239,68,68,0.35)',
|
|
|
|
|
borderRadius: 'var(--radius-sm)',
|
|
|
|
|
background: 'rgba(239,68,68,0.06)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: '#ef4444', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6 }}>
|
|
|
|
|
Danger zone
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!siteResetOpen ? (
|
|
|
|
|
<button
|
|
|
|
|
data-action="open-site-reset"
|
|
|
|
|
onClick={() => setSiteResetOpen(true)}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%', padding: '8px 12px', fontSize: 11, fontWeight: 600,
|
|
|
|
|
color: '#ef4444', background: 'transparent',
|
|
|
|
|
border: '1px solid rgba(239,68,68,0.5)', borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Reset Entire Site
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p style={{ fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.5, margin: '0 0 8px' }}>
|
2026-08-09 10:30:20 -07:00
|
|
|
This blanks <strong>every page</strong>, 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.
|
2026-08-09 10:17:53 -07:00
|
|
|
<strong> This cannot be undone.</strong> 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.
|
|
|
|
|
</p>
|
|
|
|
|
<p style={{ fontSize: 11, color: 'var(--color-text-muted)', margin: '0 0 4px' }}>
|
|
|
|
|
Type <code>{siteDomain}</code> to confirm:
|
|
|
|
|
</p>
|
|
|
|
|
<input
|
|
|
|
|
data-testid="site-reset-domain"
|
|
|
|
|
value={siteResetTyped}
|
|
|
|
|
onChange={(e) => setSiteResetTyped(e.target.value)}
|
|
|
|
|
placeholder={siteDomain}
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
|
|
|
|
|
<button
|
|
|
|
|
data-action="confirm-site-reset"
|
2026-08-09 10:30:20 -07:00
|
|
|
disabled={!siteResetConfirmMatches(siteResetTyped, siteDomain)}
|
2026-08-09 10:17:53 -07:00
|
|
|
onClick={handleResetSite}
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1, padding: '7px 10px', fontSize: 11, fontWeight: 600,
|
|
|
|
|
color: '#fff', background: '#ef4444', border: 'none',
|
|
|
|
|
borderRadius: 'var(--radius-sm)',
|
2026-08-09 10:30:20 -07:00
|
|
|
cursor: siteResetConfirmMatches(siteResetTyped, siteDomain) ? 'pointer' : 'not-allowed',
|
|
|
|
|
opacity: siteResetConfirmMatches(siteResetTyped, siteDomain) ? 1 : 0.5,
|
2026-08-09 10:17:53 -07:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Reset everything
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setSiteResetOpen(false); setSiteResetTyped(''); }}
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1, padding: '7px 10px', fontSize: 11, fontWeight: 600,
|
|
|
|
|
color: 'var(--color-text-muted)', background: 'var(--color-bg-elevated)',
|
|
|
|
|
border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|