Commit Graph

44 Commits

Author SHA1 Message Date
shadowdao f00e4db3dc test(builder): cover useVisualViewport; fix: disable Select-Parent at ROOT in desktop context menu
- useVisualViewport.test.tsx: covers the visualViewport-undefined fallback,
  the keyboardInset math on a mocked visualViewport resize, and
  listener add/remove (resize + scroll) across mount/unmount.
- ContextMenu.tsx: Select Parent was only disabled at isRoot, so selecting
  a top-level section and choosing Select Parent silently landed on the
  un-editable ROOT (no outline, no toolbar) -- a dead end. Switched the
  guard to useNodeActions' canSelectParent (false whenever the node's
  parent is ROOT or missing), matching the mobile selection toolbar's
  existing identical guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 09:19:00 -07:00
shadowdao 77f35c4e9e fix(builder): duplicate inserts+selects after source; select-parent/styles-sheet/canvas-pad mobile fixes
Ship-blocking fix (Fable consult): useNodeActions.duplicate() appended the
regenerated tree at the end of the parent while leaving the ORIGINAL
selected, so duplicating a top-level section landed the copy off-screen at
the bottom of the page with no visible change -- shared by both the mobile
selection toolbar and the desktop right-click ContextMenu. Now inserts the
copy immediately after the source (actions.addNodeTree(tree, parentId,
sourceIndex + 1)) and selects it (actions.selectNode(tree.rootNodeId)),
falling back to append-at-end if the source's index can't be resolved.
Mobile also scrolls the new node into view.

Three cheap fast-follows:
- canSelectParent on useNodeActions (false when the node's parent is ROOT
  or missing); MobileSelectionToolbar disables "Select Parent" instead of
  dead-ending on a page-wide ROOT outline with no toolbar of its own.
- Opening the Styles sheet on mobile now scrolls the selected node above
  the 65dvh sheet; a temporary generous bottom-padding class handles the
  case where the node is the last thing on the page and there'd otherwise
  be no room left to scroll it into view.
- Canvas gets bottom padding equal to the fixed selection toolbar's height
  while it's visible, so the last section of a short page isn't stuck
  permanently underneath it.

Desktop duplicate behavior improves (inserts after + selects) via the
shared hook; no toHtml changes. Verified live via Playwright at 375px and
1280px (screenshots in craft/scratchpad/mobileB2/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 08:41:33 -07:00
shadowdao 2c8425ffb0 feat(builder): mobile-B touch editing -- selection toolbar, tap-to-add, swipe-dismiss
Phase B makes the Craft.js editor genuinely usable by touch on top of Phase
A's responsive shell, gated entirely behind useIsMobile()/<=768px:

- Extract useNodeActions(nodeId) out of ContextMenu.tsx (move/duplicate/
  delete/select-parent), shared by the desktop right-click menu (behavior
  unchanged) and the new mobile MobileSelectionToolbar.
- MobileSelectionToolbar: bottom-fixed selection toolbar (Move Up/Down,
  Duplicate, Select Parent, Edit Styles, two-tap Delete confirm), hidden
  while a sheet is open.
- BlocksPanel: tap-to-add on mobile (insert after selection, close sheet,
  select + scroll the new node into view); desktop drag/double-click
  unchanged.
- LayersPanel rows >=44px on mobile; HeadCodeModal portaled to document.body
  (same fix TemplateModal already had); BottomSheet gets swipe-to-dismiss
  and on-screen-keyboard clearance via a new useVisualViewportInsets hook.

Also fixes two pre-existing bugs surfaced only by driving a real Craft.js
document with Playwright touch input (masked by tests that mock
@craftjs/core): regenerateTreeIds structuredClone'd a live node's whole
data object, including the component function reference in data.type,
throwing DataCloneError and silently breaking Duplicate/Paste for every
node type; and an earlier useNodeActions draft cached canMoveUp/canMoveDown
inside a useEditor collector closed over nodeId, which goes stale for one
render whenever the selection changes without an unrelated store event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 08:07:51 -07:00
shadowdao 2a8a26687b fix(builder): mobile-A2 hardening -- 16px inputs, shared sheet/modal chrome, z-scale + portal
- Force font-size:16px !important on Styles-sheet/topbar/Sitesmith inputs
  inside the mobile media query so inline 12px/14px styles stop triggering
  iOS zoom-on-focus.
- Lift sheet-open + Templates/Head Code modal-open state out of private
  useState into a shared MobileChromeContext (EditorShell), so Phase B can
  open/close sheets from outside MobilePanelBar.
- Add an explicit z-index layer scale, portal TemplateModal to
  document.body (was trapped under the tab bar inside .topbar's stacking
  context), align Sitesmith to the same --z-modal layer, and make opening
  a sheet close any open modal. Also fix modal backdrops swallowing tab
  bar taps (mirrors the sheet backdrop's existing tab-bar cutout).
- Drop BottomSheet's incorrect aria-modal; mobile-aware AssetsPanel empty
  state copy.
- Tests: useIsMobile (matchMedia mock incl. legacy fallback + cleanup),
  MobileChromeContext invariants (one sheet open, sheet closes modals),
  MobilePanelBar wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 07:30:12 -07:00
shadowdao 979331b12d feat(builder): mobile-responsive editor chrome (Phase A)
Makes the Craft.js editor usable on phones (≤768px) without touching desktop
layout/behavior: a useIsMobile() hook gates a bottom tab bar + sheets (hosting
the existing Blocks/Pages/Layers/Assets/Styles panels unchanged) in place of
the side panels, a collapsed TopBar with a "..." overflow menu, 44px touch
targets, 16px inputs, dvh/safe-area-aware sizing, and small copy/overflow
fixes (empty-canvas hint, Templates modal tabs + close button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 06:54:15 -07:00
shadowdao da558fd52d fix(builder): render template header/footer nested content in zone preview + export
TemplateModal's addTemplateComponents() built each template component via
React.createElement(Component, comp.props) without ever passing
comp.children, silently dropping every nested children array authored in
templates/definitions.ts (header/footer Container > Logo/Menu/TextBlock,
page Section > Heading/TextBlock/ButtonLink). The resulting Craft.js node had
nodes: [], so the header/footer zone preview (ZonePreview -> exportBodyHtml)
rendered as an empty strip, and published output was affected the same way.

Fix converts each TemplateComponent to a SerializedTreeNode and reuses
craft-tree.ts's buildNodeTree (sanitize -> flatten -> materialize) -- the
same tested tree pipeline already used for AI-generated content -- instead
of hand-rolling a React-element tree, since a naive nested-children fix via
parseReactElement crashes any component with an internal SHELL_INNER linked
canvas (Section/BackgroundSection/FormContainer) or linked columns
(ColumnLayout). Also fixes two latent bugs in buildNodeTree itself, only
surfaced by exercising it against a real Craft.js editor for the first time:
data.type must be the actual resolved component reference (not a string or
{resolvedName} object) for correct rendering, and the synthesized SHELL_INNER
node needs data.name set for actions.addNodeTree's own validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 06:19:26 -07:00
shadowdao 2066059c35 fix(builder): phase-2 design tweaks (landing home icon, decouple layer-hover from guides, neutral zone separator)
- PagesPanel: add fa-home glyph (8px, inherits badge text color) before
  "Landing" text in the landing-page badge, restoring the icon dropped
  in an earlier polish pass.
- editor.css: split [data-craft-hovered] and [data-layer-hovered] into
  distinct rules. Layer-hover now uses a solid 2px accent outline (vs
  the dashed structural guides) and is excluded from the .guides-off
  suppression list, so the Layers-panel hover->canvas locator still
  works when "Show guides" is off. Structural guides/hover
  ([data-craft-node], [data-craft-hovered]) remain correctly gated.
- Canvas.tsx: move the header/footer ZonePreview separator border to a
  .zone-preview-sep class, recolor it from amber
  (rgba(245,158,11,0.3)) to neutral (rgba(148,163,184,0.25)) to match
  the empty-state variant, and gate it on .guides-off via CSS
  descendant selector so it disappears with the rest of the guides.
2026-07-12 21:12:09 -07:00
shadowdao 473fe8d421 feat(builder): item 16 — context menu FA icon per action
Every menu item now has an FA icon (previously only "Ask Sitesmith" did,
leaving the rest visually misaligned): Duplicate fa-clone, Copy
fa-files-o, Paste fa-clipboard, Move Up/Down fa-arrow-up/down, Select
Parent fa-level-up, Delete fa-trash (kept its danger-red color). The
clipboard-group / structure-group separator already existed via
dividerAfter — no structural change needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:56:21 -07:00
shadowdao 1c85ab93eb feat(builder): item 14 — Templates modal close button + category pill hover states
Close button (already an FA fa-times icon-button) gains a hover
background/color; category pills gain the same hover-background
treatment when not active (they were already at the brief's target 12px
font-size). Template card hover-lift (translateY(-2px) + shadow) was
already implemented — no change needed there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:56:05 -07:00
shadowdao 67228f24b4 feat(builder): item 11 — Pages panel hierarchy: neutral header/footer rows, outline Landing badge, icon+tooltip actions
Header/Footer zone rows switched from a loud amber zoneButtonStyle to a
compact neutral zoneRowStyle in the same surface language as the page
list (accent border/bg only when active, matching the page list's own
"currently open" treatment). The "Appears on all pages" subtitle moved to
a tooltip; a fa-pencil hint fades in on row hover (`.zone-row-pencil` CSS
already shipped in the previous commit) and swaps to a check icon while
editing that zone.

The "LANDING" badge (loud amber uppercase + house icon) is now a small
outline badge — muted text, 1px border, no fill, no icon.

The page-row rename button's raw `&#9998;` HTML entity is now an FA
`fa-pencil` icon with `data-tooltip`/`aria-label`; the delete button
(already FA fa-trash) got the same tooltip/aria treatment for
consistency. Both dropped their native `title` in favor of `data-tooltip`
to avoid a double tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:56:00 -07:00
shadowdao 613a44c4d4 feat(builder): items 10+12+13 — scope canvas guides, Show Guides toggle, Layers panel hover/icons, topbar unification
Item 10: dashed canvas "guide" outlines were applied via blanket tag
selectors (div/section/header/...), so a finished section showed 3-4
nested dashed boxes. RenderNode.tsx (the <Editor onRender> override) now
tags each Craft.js droppable container's real DOM node with a
`data-craft-node` attribute (node.data.isCanvas, excluding ROOT), and
editor.css's guide rules target that attribute instead — a component's own
internal wrapper markup is no longer mistaken for a drop target. Added a
"Show guides" topbar toggle (default ON, persisted to localStorage),
state lifted in EditorShell.tsx (mirrors how `device` is already lifted),
flips `.guides-off` on Canvas.tsx's `.canvas-device-frame`.

Item 12: Layers panel rows get per-type FA icons (keyed off the same
craft.displayName used for the row label, seeded from BlocksPanel's
choices), indent-guide lines connecting nested rows, and row hover
highlights the matching canvas element (`data-layer-hovered`, written
directly to the node's DOM via `query.node(id).get().dom` — NOT via
`actions.setNodeEvent`, which is stripped from useEditor()'s public
`actions` at runtime, not just in its TS type, and threw when called).

Along the way, Craft.js's own connect() was found to already wire mouse
hover to the same `hovered` node event internally (previously invisible
because the matching CSS was dead) — RenderNode now also mirrors that
onto `data-craft-hovered` for a real-mouse-hover canvas highlight, and
both hover attributes plus the guide attribute are suppressed under
`.guides-off` so the toggle stays airtight.

Item 13: unified the topbar to one button radius (`.topbar-btn` 20px pill
-> var(--radius-md)), demoted Code/Preview to icon-only with
`data-tooltip`. Fixed a latent bug the new tooltips exposed: `[data-tooltip]`
tooltips render above their trigger, but the topbar has no room above it
(`body { overflow: hidden }`) — added a `.topbar` override to render
those below instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:55:52 -07:00
shadowdao 72f85a97e5 fix(builder): phase-1 polish a11y follow-ups (dropzone keyboard, badge name, icon aria) 2026-07-12 20:22:11 -07:00
shadowdao ab28ad8f2c Merge assets-panel empty state into one dropzone
The Assets panel used to show a small always-visible dropzone plus a
separate italic "No assets uploaded yet" line stacked underneath it
when there were no assets -- two redundant messages for one state.
Replace both with a single tall dropzone (icon + "Drag images here or
click to upload") that also opens the file picker on click; it
collapses back to the original slim "Drop files here to upload" bar
once assets exist. Upload/drag-drop behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:15:15 -07:00
shadowdao b3e5009aec Fix preset-grid orphan row for 5/6-item preset sets
PresetButtonGrid rendered every preset set into a fixed 4-column
.preset-grid, so 5-item sets (RADIUS_PRESETS, SPACING_PRESETS,
IMAGE_RADIUS_PRESETS, FONT_WEIGHTS, NavStylePanel's GAP_PRESETS)
wrapped a single lone button onto its own row, and the 6-item
TEXT_SIZES split unevenly (4+2).

PresetButtonGrid now derives a column count from presets.length via
defaultPresetGridColumns() -- 5-item sets get a single row of 5,
6-item sets split into two even rows of 3, and anything else keeps
the classic 4-column grid -- with an optional `columns` prop for
explicit overrides. This fixes every existing call site automatically
rather than threading an explicit count through each one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:14:38 -07:00
shadowdao eeb0660d83 Replace emoji-as-icons in editor chrome with Font Awesome
Unicode emoji/glyphs (Sitesmith's sparkle, lock, close X) render as
tofu on systems without an emoji font. Swap for the FA4 glyphs the
rest of the chrome already uses:
- SitesmithButton/SitesmithModal: sparkle -> fa-magic, lock -> fa-lock
- ContextMenu "Ask Sitesmith" entry: sparkle -> fa-magic (via new
  optional MenuItem.icon field)
- TemplateModal/SitesmithModal close buttons, PagesPanel delete,
  AssetsPanel delete/cancel: &#10005; -> fa-times / fa-trash

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:14:29 -07:00
shadowdao 802938ec1a fix(builder): Image block placeholder no longer overridden by explicit src=""
BlocksPanel dropped a new Image block with an explicit `src=""` prop, which
overrides ImageBlock's `src = PLACEHOLDER_SRC` default parameter (defaults
only apply when a prop is undefined, not when it's an empty string). Craft
then persisted `src:''`, and the canvas rendered a broken-image icon instead
of the placeholder.

- ImageBlock render now falls back to PLACEHOLDER_SRC whenever src is falsy
  (belt-and-braces: also recovers any legacy saved src:'' state).
- BlocksPanel no longer passes src="" when dropping a new Image block, so
  the craft default applies.
- ImageStylePanel now restores the placeholder (instead of blanking to '')
  when the URL field is cleared.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:45:28 -07:00
shadowdao 25507cb57a Fix INT: ContentSlider Slides editor wrote wrong prop key (image vs imageSrc)
MediaStylePanel's Slides array editor guarded on item.image !== undefined
and wrote `image` via AssetPicker's onChange, but ContentSlider (render
+ toHtml) reads slide.imageSrc. Default slides (imageSrc:'', no `image`
key) never showed an image picker at all, and any `image` value written
was a silent no-op on render/export.

Editor now guards/reads/writes `imageSrc` throughout, and the
"add slide" emptyItem matches defaultSlides' exact shape
(type/imageSrc/heading/text/bgColor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:32:47 -07:00
shadowdao b88b242b6a refactor(builder): extract shared Modal component for the three editor modals
New src/ui/Modal.tsx ({ open, onClose, title?, children, width?,
closeOnEscape?, closeOnBackdropClick?, backdropStyle?, backdropProps? })
owns the backdrop, Escape-to-close, backdrop-click-to-close, and a new
body-scroll-lock while open. Adopted by TemplateModal, HeadCodeModal, and
SitesmithModal; each keeps its own panel styling/header/footer as children
since those differ per modal.

- TemplateModal: Escape/backdrop-click still close the confirm-template
  sub-dialog first via a wrapped onClose passed to Modal; the header's X
  button keeps using the raw onClose prop (always fully closes), matching
  prior asymmetric behavior.
- HeadCodeModal: straightforward adoption, no prior custom close logic.
- SitesmithModal: previously had no `open` prop, no Escape-to-close, and no
  backdrop-click-to-close. Preserved via open (always mounted-open by its
  parent already), closeOnEscape={false}, closeOnBackdropClick={false}; role
  and aria-modal are passed through via backdropProps to keep them on the
  same backdrop element as before.

Body scroll-lock while a modal is open is a small new addition (requested by
the task) applied uniformly; it has no visible effect since each modal's
opaque fixed-position backdrop already fully covers the viewport.

Added a light Modal.test.tsx (renders children, Escape/backdrop-click close
behavior, scroll-lock) using the existing react-dom/client + act harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:22:05 -07:00
shadowdao 8f51b5144a refactor(builder): extract shared ArrayItemFieldsEditor from SectionTypePanel/GenericPropsEditor
Both panels had a ~90-line near-byte-identical per-item field editor
(boolean->checkbox, number->number, /color/->color, long->textarea, else
text, fields derived from Object.keys(items[0])). Moved into a new
ArrayItemFieldsEditor component in ArrayItemFields.tsx used by both.
SectionTypePanel keeps its own FeaturesGrid special-case (key === 'features'
-> FeaturesEditor) and its empty-array guard ahead of the shared call.
Behavior-preserving (task E4.2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:15:34 -07:00
shadowdao 4733fe84b5 refactor(builder): extract useNodeProp hook, dedup StylePanel setProp boilerplate
Adopted in BackgroundSectionStylePanel, ButtonStylePanel, ContainerStylePanel,
FormStylePanel, GenericPropsEditor, HeroStylePanel, ImageStylePanel,
MediaStylePanel, NavStylePanel, SectionTypePanel, SocialStylePanel,
TextStylePanel — only where the inline setProp/setPropStyle were behaviorally
identical to the hook. PricingStylePanel and FeaturesEditor keep their own
array-mutation logic since it differs. Behavior-preserving (task E4.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:13:35 -07:00
shadowdao 6421306849 feat(builder): implement Ctrl+C/V copy-paste shortcuts
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>
2026-07-12 15:01:30 -07:00
shadowdao 1958b8e0b5 chore(builder): remove GuidedStyles classification cruft
Drop the computed-but-unused resolvedName/resolverMap, anchor the hero
regex (/^hero/i) so it stops matching any type name containing "hero",
and remove the isContainer alternations for the now-deleted
HeaderZone/FooterZone components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:59:39 -07:00
shadowdao 4b8dd8baee ux: in-app confirm for asset/sitesmith delete + safe copy
- AssetsPanel: asset delete now requires an in-app two-step confirm
  (tile-button turns into "Delete?" + cancel, auto-resets after 4s or on
  click-elsewhere) instead of deleting with no confirmation at all.
- AssetsPanel: copyUrl uses a new copyToClipboard() helper that tries the
  async Clipboard API and falls back to a hidden-textarea execCommand copy
  in non-secure contexts, surfacing a visible "Copy failed" state instead
  of silently doing nothing.
- SitesmithModal: replaced window.confirm(...) for "Clear chat" with the
  same in-app two-step confirm pattern -- no native dialogs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:51:00 -07:00
shadowdao 46ebd253f3 a11y: keyboard-operable editor chrome + topbar aria-labels
Clickable <div> rows/tiles (page list, layer tree, template cards, asset
picker grid) now expose role="button", a tab stop, and Enter/Space
activation via a shared clickableProps() helper, matching their existing
onClick behavior. TopBar icon-only controls (device switcher, undo/redo,
save, publish, templates, code, preview, back) gain aria-label alongside
their existing title tooltips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:50:50 -07:00
shadowdao 36ce256760 fix(builder): emit Container cssId/cssClass with live panel controls (D1a)
Container.craft.props collected cssId/cssClass since before Phase E1 but
nothing rendered or exported them. Add live "CSS ID" / "CSS Class" text
inputs to ContainerStylePanel (guarded on nodeProps.cssId/cssClass
!== undefined) and emit id=/class= in both the editor render and toHtml.
cssId takes precedence over the existing anchorId prop when both are set
(only one id attribute can be emitted); anchorId is used as a fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:46:23 -07:00
shadowdao e1d381dc8a fix(builder): Preview includes site headCode and is \$-safe (D8)
Two bugs in TopBar's Preview handler:

1. exportToHtml() was called without a headCode option, so Preview
   silently ignored SiteDesign.headCode (custom <head> code the user
   set via the Code modal). Pull `design.headCode` from useSiteDesign()
   and pass it through, matching exportToHtml's ExportOptions.headCode
   signature.

2. html.replace(bodyMatch[1], composedBody) used the string-replacer
   overload of String.prototype.replace, which treats `$&`, `$1`,
   `$$`, etc. in the replacement string as special patterns -- any
   user content containing a literal `$` sequence would corrupt the
   preview HTML. Switch to a function replacer
   (`.replace(bodyMatch[1], () => composedBody)`) so the composed body
   is inserted literally.

Verified by reading + `npm run build`; no existing TopBar test harness
to extend (dynamic import + window.open/Blob in the click handler).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:29:02 -07:00
shadowdao 7973ee9ba8 fix(builder): await TemplateModal's load sequence before closing (D5)
The template-load sequence (clear canvas -> add components -> add
pages -> apply header/footer) ran as a chain of setTimeout callbacks,
but onClose() fired synchronously right after kicking it off. The
modal disappeared immediately, so "Loading..." never had a chance to
show, and the staged mutations kept running against a dialog the
caller had already dismissed.

Convert the staged steps to async/await (a promise-based `wait()`
replaces the setTimeout chaining) and only call onClose() in a
finally block once the whole sequence has settled. Add a mountedRef
guard so the trailing setLoading(false) is skipped if the component
happens to unmount mid-sequence. End result (pages/header/footer/
design applied) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:26:46 -07:00
shadowdao 4674a99ec9 feat(builder): unify StylePanel image fields on AssetPicker
Replace the ad-hoc upload/browse/URL blocks in ImageStylePanel, HeroStylePanel
(bgImage + bgVideo), BackgroundSectionStylePanel, NavStylePanel (standalone
Logo imageSrc + Navbar logoImage), MediaStylePanel (Gallery/ContentSlider
array items), and FeaturesEditor (per-feature image) with the shared
AssetPicker component, dropping each panel's duplicated local
showBrowser/browserAssets/handleUpload state and inline asset-browser JSX.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:00:10 -07:00
shadowdao 296c10d019 feat(builder): shared asset upload/list util
Extract uploadAsset/listAssets into utils/assets.ts, lifting the exact
uploadToWhp body and list_assets fetch pattern already duplicated across
shared.tsx/ImageBlock/Logo/Navbar/etc. shared.tsx's uploadToWhp is now a
thin re-export (`export const uploadToWhp = uploadAsset`) so its ~6
existing callers are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:43:59 -07:00
shadowdao 97123c4c58 fix(builder): regenerate node ids on duplicate/paste to prevent state corruption
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>
2026-07-12 12:18:44 -07:00
shadowdao 97cb439508 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>
2026-07-10 13:10:46 -07:00
shadowdao d20b77e66d site-builder: dedicated Features editor in guided panel (per-card icon/image+upload/button, robust to template items missing keys) 2026-07-07 17:19:37 -07:00
shadowdao 1cfb51f181 site-builder: surface Spotify/Twitch + contact-form recipient in the Styles (guided) panel
The right panel only renders GuidedStyles (there is no Settings tab), so the
per-component Settings panels I'd edited never showed. Add Spotify/Twitch to
SocialStylePanel's platform dropdown and the 'Send submissions to' + thank-you
fields to FormStylePanel (shown for any form with a recipientEmail prop).
2026-07-07 14:06:24 -07:00
shadowdao d0925d9e2d site-builder: dynamic CTAs, section anchors, edit-with-Sitesmith
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>
2026-05-25 12:43:28 -07:00
shadowdao 7b747f775f site-builder: lock landing page to index.html regardless of name
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>
2026-05-25 12:14:26 -07:00
shadowdao 906695379b sitesmith: null-safe esc() in Navbar/Menu/Logo + clear chat button
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).
2026-05-24 16:03:02 -07:00
shadowdao 069ea1235a sitesmith: null-safe esc() across all toHtml + WorkingIndicator
Real-world AI output frequently sends mismatched prop names (e.g.
items vs features, cta object vs buttonText/Href). The toHtml functions
of section/form/sections-folder components each defined a local
esc = (s: string) => s.replace(...) that crashed when called with
undefined, taking the auto-save export with it.

Patched every local esc() to coerce non-strings:
  const esc = (s: any) => String(s ?? "").replace(...)
17 files touched; behavior unchanged for valid string inputs.

Also adds a WorkingIndicator (Claude Code-style spinner + rotating
phrase + elapsed seconds) shown in the modal footer while a generation
is in flight, replacing the disabled "Thinking..." placeholder.
2026-05-24 15:54:48 -07:00
shadowdao 2ca1ff0cf9 sitesmith: layers panel prefers props.aiName when present 2026-05-23 14:25:43 -07:00
shadowdao e651becdbe sitesmith: chat modal (messages, input, banner, scope confirm) 2026-05-23 14:25:28 -07:00
shadowdao b4d71340e1 sitesmith: upgrade banner + scope-replace confirmation dialog 2026-05-23 14:24:20 -07:00
shadowdao bf55ee85b9 sitesmith: topbar button with locked/capped states 2026-05-23 14:23:51 -07:00
shadowdao 606c9b78c8 fix(image-radius): split out 3x-scale IMAGE_RADIUS_PRESETS for the image picker
Image radii need to be visibly larger than the radius scale that works for
buttons/containers — at typical photo dimensions, 16px reads as nearly
square. Add an image-specific scale at 3x the shared values (S=24px,
M=48px, L=96px) and route ImageStylePanel through it. Other components
(buttons, sections, containers) keep RADIUS_PRESETS unchanged.

Note: this commit also bundles unrelated pre-existing working-tree changes
in the legacy GrapesJS site-builder root (CLAUDE.md, index.html,
css/editor.css, js/assets.js, js/editor.js, js/whp-integration.js) that
were inadvertently picked up by an earlier `git add -u`. The image-radius
change is the only intentional content of this commit; the rest is
in-progress legacy work that happened to be sitting uncommitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 05:18:31 -07:00
shadowdao 1558626b84 fix(delete): redirect Delete to owning component when target is empty linked node
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>
2026-04-26 20:42:17 -07:00
shadowdao 91a6b6f34b Add Craft.js site builder (v2) - complete rebuild from GrapesJS
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>
2026-04-05 18:31:16 -07:00