Extend the save payload with head_code + design so the backend can
inject SiteDesign.headCode into published pages, and restore design
tokens on load() so the editor reflects the last-saved state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildSavePayload matched the active page by page.id === activePageId,
but activePageId defaults to the hardcoded 'home' and load() never
updated it. Deleting the original Home page (its replacement gets id
page_<ts>), reloading, editing, and saving would then match no page --
the live edit only reached the legacy top-level craft_state, while the
authoritative pages[]/pages_craft_state[0] for index.html fell back to
stale state.
Belt-and-suspenders fix:
- buildSavePayload now falls back to pages[0] as the effective active
page when a real page is active but activePageId matches nothing.
- useWhpApi's load() now points activePageId at the freshly restored
first page via a new PageContext setActivePageIdDirect setter
(bookkeeping only -- no re-serialize/deserialize), so activePageId
stays valid after every load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression coverage for the safety-sensitive part of keyboard copy/paste:
nothing previously tested that Ctrl/Cmd+V regenerates node ids via
regenerateTreeIds before actions.addNodeTree -- the exact logic whose
absence caused a duplicate-id corruption bug. Also covers sibling-parent
targeting (with ROOT fallback), the empty-clipboard no-op, and the
existing input/contentEditable focus guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The context menu advertised Ctrl+C/Ctrl+V hints but useKeyboardShortcuts
only handled undo/redo/delete/duplicate/escape -- pressing them did
nothing. Add a tiny shared module-level clipboard (src/hooks/clipboard.ts)
used by both the keyboard hook and the context menu so copying via one
entry point and pasting via the other stay consistent.
Ctrl/Cmd+C stores the selected node id (skipping ROOT). Ctrl/Cmd+V
inserts a copy as a sibling of the current selection via
regenerateTreeIds, mirroring the existing context-menu paste behavior.
Both respect the existing "disabled while typing" guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PageData.headCode was never populated from anything but '' and never
read anywhere -- the real, live concept is SiteDesign.headCode (set
via the Code modal, wired into Preview/export in D8). Remove the dead
field from the PageData interface and its ~7 hardcoded headCode: ''
seeds/discards in PageContext.tsx (DEFAULT_PAGE, DEFAULT_HEADER,
DEFAULT_FOOTER, the context default value, addPage, setPagesCraftState,
replaceAllPages). Update the PageData literals in
useWhpApi.save.test.ts to match the trimmed interface.
Verified via grep that nothing reads page.headCode, and `npm run
build` (strict tsc) is clean -- no live reader broke.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Editing the Header/Footer sets activePageId to '__header__'/'__footer__',
which matches no entry in `pages`. save() was serializing the live canvas
into the top-level page slots (mislabeled as page content, matching no
page) while exporting header/footer from stale stored state — auto-save
every 30s silently dropped header/footer edits.
Extract buildSavePayload() as a pure, unit-tested helper: header/footer
craft state now comes from the live canvas when that zone is being
edited (else stored state), and the top-level page fields fall back to
the landing page's stored state when a header/footer zone is active,
so page content is never clobbered or mislabeled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Craft.js duplicate (ContextMenu + keyboard shortcut) and paste were reusing
the original node's toNodeTree() output verbatim, so addNodeTree() inserted
duplicate node ids into the editor tree. Added regenerateTreeIds() which
deep-clones a NodeTree and remaps rootNodeId, node map keys, node.id,
internal node.data.parent, node.data.nodes, and node.data.linkedNodes via
Craft.js's own getRandomId(). Also fixed pasteNode to insert as a sibling
of the right-clicked node (using its parent) instead of using a leaf node
as the new parent, which previously threw.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three related features:
1. Dynamic CTA buttons on HeroSimple, CTASection, CallToAction.
New shared ctas[] array (text + href + variant + target) replaces the
primary/secondary pair. Settings panel gets add/remove/reorder controls.
Legacy fields stay readable for backwards compat — first user edit
migrates the section onto the new array.
2. Anchor IDs on all layout/section components (Container, Section,
BackgroundSection, ColumnLayout, plus 6 section blocks done by parallel
subagent, plus Hero/CTA/CallToAction). Anchor input lives in the
settings panel with an "auto from heading" button that walks the
subtree for the first Heading.text. Renders as id="..." on the
outermost element so #anchor URLs resolve.
3. Edit-with-Sitesmith targeted invocation. Right-click → "Ask Sitesmith"
and a button at the top of the right-side settings panel both open the
modal pre-targeted at the selected node. The node's serialized subtree
is sent to the server; system prompt is augmented to require a patch
with replace_node. Editor lifts modal state into a new SitesmithContext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first page is now treated as the landing page: it always publishes to
index.html no matter what the user names it, and its slug is forced to
'index' in state so .htaccess clean-URL rewrites stay consistent.
- useWhpApi.ts: force pages[0].filename='index.html' at save time
- PageContext.tsx: heal pages[0].slug to 'index' on load and on rename
- PagesPanel.tsx: "LANDING" badge on first page, slug shown as '/',
rename hides slug input (locked), delete button hidden
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior null-safe esc patch only matched 'const esc =' declarations;
Menu/Navbar/Logo use 'function esc(str: string)' syntax and slipped
through. Patched those three to coerce non-strings the same way.
Added "Clear chat" button in the modal header that appears when there's
any message history. Confirms with the user before posting to the new
clear_history endpoint, which deletes all messages + the thread row
for the current site (usage rows are preserved for billing).
Linked Craft.js nodes (column children of ColumnLayout, section-inner of
Section, etc.) are structurally non-deletable — actions.delete throws and
the error was silently swallowed. Empty layouts ended up undeletable from
the canvas because clicks always landed on the linked children that fill
the layout's visible area.
Adds findDeletableTarget(): when target is a linked node and ALL its
linked siblings are also empty (i.e., the layout itself is empty),
redirect deletion to the owning parent. Refuses to redirect when any
sibling has content, to protect against nuking a 3-col layout that has
content in other cols.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebuilt the visual site builder from scratch using Craft.js, React 18,
and TypeScript. The new editor renders directly in the DOM (no iframe),
supports 40+ components, multi-page with shared header/footer, 16
templates, full-spectrum color/gradient controls, custom head code
injection, save/publish workflow, and auto-save.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>