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>
- 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>
- 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.
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 `✎` 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>
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>
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>
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: ✕ -> fa-times / fa-trash
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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>
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>
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>