handlePublish's JSON response has always included a `warnings` array
(e.g. the contact-form relay's "submissions will not be delivered"
notice), but nothing in the editor ever read it. Adds a PublishWarnings
banner, held in its own state independent of the 3s publishStatus
flash so the customer has time to read it, rendered in both the
desktop and mobile TopBar branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
data-animation-delay is stored as a plain seconds string (e.g. '2'); the reveal
script assigned it raw to el.style.animationDelay, which is invalid CSS and no-ops.
Suffix 's' onto bare numbers (leaving '2s'/'200ms' alone) so entrance-animation
delays actually apply. Backend generateCompiledHTML gets the byte-identical change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NavStylePanel (Navbar/Menu/Logo/Footer):
- LinkPicker: dropdown of the site's pages (read-only via usePages()) plus
manual URL / #anchor / tel: / mailto: entry, wired into every link-href
field (standalone Logo href, Navbar logoUrl, Navbar/Menu link items).
- "Sync links with Pages" button in the Links section: repopulates the
links array from the current pages list (label = page name, href = '/'
for the landing page else '/{slug}'), preserving any existing CTA link.
Regression-fix vs the legacy GrapesJS builder, which had this.
- `download` checkbox per link (Navbar/Menu links, standalone Logo href)
emits the `download` attribute on export for links to files.
- Links/Colors sections now gate on the component actually carrying a
`links`/color prop, so Footer (no links array) no longer shows a dead
"Add Link" editor.
- Box-model (Margin/Padding via SpacingControl, Border & Effects via
BorderControl + box-shadow presets + opacity), AnimationControl, and
VisibilityControl added for all four owned components, backed by new
animation/animationDelay/hideOnDesktop/hideOnTablet/hideOnMobile props
(with blank/default values in each component's .craft.props).
Tests: NavStylePanel.test.tsx (new, 17 tests: LinkPicker modes, sync
preserves CTA, download toggle, box-model/animation/visibility wiring) +
extended Navbar/Menu/Logo/Footer .toHtml.test.ts (download attribute,
craft.props defaults). Full suite: 683/683 passing. `npm run build` green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SectionTypePanel (Accordion/Tabs/Testimonials/Countdown/NumberCounter/
CTASection/CallToAction/FeaturesGrid), PricingStylePanel, and
SocialStylePanel all gain a Spacing & Border section (margin/padding
per-side, border, box-shadow, opacity), an Animation section, and a
Visibility section, wired to the shared SpacingControl/BorderControl/
AnimationControl/VisibilityControl.
PricingTable's per-card colors (cardBg/textColor/subColor/featColor/
checkColor/btnBg/btnColor) were previously hard-coded literals computed
from featuredBg inside toHtml -- promoted to real optional props (each
falling back to the exact prior literal when unset) and exposed via
ColorPickerField in PricingStylePanel.
SocialStylePanel now exposes SocialLinks' iconShape/gap (already-built
props with no control), plus Icon's bgColor/bgShape/bgSize/link and
StarRating's filledColor/emptyColor, which the panel's generic
iconBgColor/starColor checks never matched since those aren't Icon's or
StarRating's real prop names.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ColumnLayout: exposes style.alignItems on its flex ROW (aligns uneven
columns) -- render/toHtml already spread `style` onto the row div, so this
is a craft.props default + panel control addition, no structural change.
- Container/Section: root element is now unconditionally display:flex;
flex-direction:column (both editor render and toHtml), so the new
Vertical Alignment control maps to style.justifyContent, paired with a
Min Height (NumericUnitInput) control on style.minHeight. Default
justify-content/align-items reproduce ordinary block-flow stacking, so
this is a visual no-op for existing published content. Works in both
normal and "boxed" (contentWidth) modes -- the boxed inner wrapper's own
margin:0-auto horizontal centering is preserved via flex auto-margin
override semantics.
- ContainerStylePanel (serves Container/Section/Columns) distinguishes the
Columns case from Container/Section via nodeProps.columns/split presence
(no typeName plumbing needed) to pick align-items vs justify-content for
the shared Vertical Alignment control.
- All 3 owned components: added margin/padding (per-side)/border/box-shadow/
opacity style defaults + AnimationControl/VisibilityControl-backed
animation/animationDelay/hideOnDesktop/hideOnTablet/hideOnMobile props.
New containerBoxModel.tsx (package-local, not shared.tsx) DRYs the
box-model + border/effects + animation/visibility panel sections across
the single shared ContainerStylePanel, mirroring the sibling media
package's mediaBoxModel.tsx.
- Tests: extended all 3 *.toHtml.test.ts files (align-items/justify-content/
min-height emission, box-model style emission, craft.props presence).
673 tests green, tsc + vite build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FormStylePanel: ContactForm field editor (add/remove/reorder via
ArrayPropEditor + manual move up/down) covering label/name/placeholder/
type (full sanitizeInputType allowlist + textarea/select)/required/
options.
- SubscribeForm.toHtml: was a dead `<form method="POST">` with no action at
all -- wired through the same relayFormWiring contract as ContactForm/
FormContainer so a recipientEmail makes it actually submit (marker +
placeholder action + honeypot), falling back to action="#" otherwise.
- SearchBar: was purely decorative (no action/method/input name) -- now a
real GET form (configurable target, default "/") with input name="q",
safeUrl-guarded against javascript:/vbscript: breakout.
- Box-model (margin/padding per-side, border, shadow, opacity), entrance
animation, and hide-on-device controls added to FormStylePanel and
rolled out (blank/false craft.props defaults) across ContactForm,
FormContainer, InputField, TextareaField, FormButton, SubscribeForm,
SearchBar. No toHtml changes needed for animation/visibility --
html-export.ts's buildDataAttrs() already emits data-animation/
data-hide-* generically from these prop names.
- Extended toHtml tests for all 7 components: field-type rendering
(incl. textarea/select), type-attribute XSS sanitization, relay/GET
functional wiring, box-model style passthrough, craft.props defaults.
npx vitest run: 689/689 passed. npm run build: green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the plain single-line input (HtmlBlock's `code` prop, via
GenericPropsEditor) and the plain <textarea> (HeadCodeModal's site-wide
head code) with a proper syntax-highlighted, tab-completing code editor.
- New src/ui/CodeEditor.tsx: reusable CodeMirror 6 editor (state/view/
commands/autocomplete/language/lang-html/lang-css/lang-javascript/
theme-one-dark). All @codemirror/* packages are pulled in via a single
dynamic import() inside the component so they land in separate lazy
chunks instead of the main bundle -- confirmed via `npm run build`:
main editor.js grew by only ~5.5KB (657KB -> 663KB raw) while ~570KB of
CodeMirror source split into index*.js chunks that only load when a
code editor modal is actually opened. While that import is in flight,
or if it ever fails, the component renders a plain <textarea> so typing
never breaks.
- GenericPropsEditor.tsx: special-cases the `code` prop (used only by
HtmlBlock today) into an "Edit HTML" button that opens the CodeEditor
in a modal (language="html"), instead of rendering it as a single-line
text input alongside the component's other string props.
- HeadCodeModal.tsx: swaps its <textarea> for CodeEditor (language="html"
-- head code is HTML with embedded <script>/<style>), keeping the
existing SiteDesignContext.updateDesign({ headCode }) wiring.
GuidedStyles.tsx untouched: HtmlBlock's displayName ("HTML") already
matches GuidedStyles' `isUtility` regex and routes to GenericPropsEditor,
so no dispatcher change was needed.
HtmlBlock.tsx untouched: purifyHtml/toHtml sanitization is unchanged, as
specified. Skipped the optional AnimationControl/VisibilityControl
addition -- HtmlBlock has no dedicated StylePanel (it shares
GenericPropsEditor with Divider/Spacer/every unmatched type), and doing
it well would mean adding real Animation/Visibility widgets to that
shared editor for every consumer, which is a bigger change than "trivial"
for this package's scope.
Tests: CodeEditor.test.tsx and HeadCodeModal.test.tsx both exploit the
fact that dynamic import() always resolves on a later microtask, so
asserting against the DOM immediately after the initial synchronous
render deterministically exercises the <textarea> fallback path (value
display, onChange wiring, language prop plumbing) without needing to
mock @codemirror/*.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TextStylePanel (Heading/TextBlock): line-height/letter-spacing presets,
text-transform (none/uppercase/lowercase/capitalize), italic + underline
toggles (fontStyle/textDecoration), and a custom NumericUnitInput
font-size alongside the existing preset row. All write to component
`style`; no toHtml changes needed (style already flows through
cssPropsToString for both components).
- ButtonStylePanel/ButtonLink: "Open in new tab" checkbox writes the
existing `target` prop ('_self'/'_blank' -- toHtml already emitted
rel="noopener noreferrer" for _blank). New Hover State section
(hoverBg/hoverColor via ColorPickerField) renders a scoped
`<style>.btn_<hash>:hover{...}</style>` block before the `<a>` in
toHtml, scoped per-node via scopeId (same pattern as Navbar/Menu) so
two buttons on one page don't collide; both values sanitized through
cssValue against <style>-element breakout. Editor canvas gets a live
hover preview via onMouseEnter/onMouseLeave local state (mirrors Menu's
approach), since there's no way to preview a CSS :hover rule directly
on an inline-styled React element.
- Heading/TextBlock/ButtonLink: added margin(per-side)/padding(per-side,
Text only)/border/box-shadow/opacity style defaults + AnimationControl/
VisibilityControl-backed animation/animationDelay/hideOnDesktop/
hideOnTablet/hideOnMobile props, each panel gaining collapsible
Spacing / Border & Effects / Animation & Visibility sections. Button
keeps its existing padding-shorthand preset row rather than adding a
redundant per-side padding control; only margin got the new
per-side SpacingControl.
- Tests: extended all three *.toHtml.test.ts files -- typography style
emission (line-height/letter-spacing/text-transform/font-style/
text-decoration/custom font-size), button target+rel, scoped hover
style emission + two adversarial style-breakout cases (</style><script>
and rule-injection via `;}selector{`), and craft.props assertions for
every new prop on all three components. 674 tests green, tsc + vite
build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ImageBlock/ImageStylePanel: SizeControl width(absorbs old maxWidth
presets)/height, AspectRatioControl + OBJECT_FIT grid + FocalPointGrid for
CSS framing crop (aspect-ratio/object-fit/object-position on the <img>).
Exported <img> always gets loading="lazy" decoding="async", plus width/
height attrs when the style has a plain px length (pxAttr helper).
- VideoBlock/MediaStylePanel: Video URL text input replaced with
AssetPicker(mediaType="video") writing videoUrl (paste-URL still handles
YouTube/Vimeo, upload/browse handle files). Added SizeControl(width) +
AspectRatioControl so the video frame honors real size/aspect instead of
a hardcoded 16:9 (padding-bottom hack replaced with CSS aspect-ratio).
New optional `poster` prop (image AssetPicker) + preload="metadata" on
file-type <video>.
- Gallery: surfaced the existing-but-unexposed `columns` and `lightbox`
props with panel controls.
- All 5 owned components (ImageBlock, VideoBlock, Gallery, ContentSlider,
MapEmbed): added margin/padding (per-side)/border/box-shadow/opacity style
defaults + AnimationControl/VisibilityControl-backed animation/
animationDelay/hideOnDesktop/hideOnTablet/hideOnMobile props. New shared
mediaBoxModel.tsx (package-local, not shared.tsx) DRYs the box-model +
border/effects + animation/visibility panel sections across
ImageStylePanel and MediaStylePanel.
- Tests: extended *.toHtml.test.ts for all 5 components (crop/perf attrs,
video size/aspect/poster/preload, gallery columns/lightbox, box-model
style emission, craft.props presence) + new MediaStylePanel.video.test.tsx
verifying the AssetPicker wiring writes videoUrl/poster and the video-only
size controls are gated on videoUrl. 694 tests green, tsc + vite build
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds @codemirror/state, view, commands, autocomplete, language,
lang-html, lang-javascript, lang-css, and theme-one-dark to package.json
now (unused for now, tree-shaken out of the build) so the code-editor
feature branch doesn't need to touch package.json/package-lock.json itself
and conflict with other enh-batch branches doing the same.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds NumericUnitInput, SizeControl, AspectRatioControl, FocalPointGrid,
SpacingControl, BorderControl, AnimationControl, and VisibilityControl to
src/panels/right/styles/shared.tsx -- presentational value/onChange controls
that upcoming feature panels will import instead of reinventing size,
spacing, border, and animation/visibility UI per panel.
AnimationControl/VisibilityControl emit the exact prop names
(animation/animationDelay, hideOnDesktop/hideOnTablet/hideOnMobile) already
consumed by html-export.ts's buildDataAttrs(), verified by reading that file
directly. ANIMATIONS matches the export's actual data-animation set
(fade-in/slide-up/slide-left/slide-right/zoom-in/bounce) rather than the
slide-down variant that doesn't exist in the export.
No existing shared.tsx exports were touched, and nothing is wired into any
feature panel yet -- that's the downstream feature branches' job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ASPECT_RATIOS, SHADOW_PRESETS, LINE_HEIGHTS, LETTER_SPACINGS, OBJECT_FIT,
BORDER_STYLES, and SIZE_PRESETS -- foundation for the new shared StylePanel
controls (SizeControl, AspectRatioControl, BorderControl) so upcoming
feature panels share one preset source instead of each defining their own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mocked unit tests for regenerateTreeIds, TemplateModal.addTemplateComponents,
and buildNodeTree each let a real bug ship (DataCloneError on duplicate/paste,
dropped template children, no-op AI section-replace/insert) because their
fake @craftjs/core query/actions never exercised Craft's real node shape
(data.type as a live component reference) or real parseFreshNode validation.
Adds src/test-utils/editorHarness.tsx, which mounts a REAL <Editor>+<Frame>
(no vi.mock('@craftjs/core') anywhere) via react-dom/client + act, plus the
jsdom shims Craft/this component library actually needs (ResizeObserver,
matchMedia, and an HTMLElement.prototype.innerText polyfill -- jsdom has no
native innerText, which Heading/TextBlock rely on to paint their text).
Adds 3 integration suites under src/test-utils/integration/ driving the real
useNodeActions/useKeyboardShortcuts, TemplateModal's real tree-build pipeline,
and the real useApplyAiResponse hook against a live EditorStore, asserting on
both the real rendered DOM and query.serialize()/exportBodyHtml output.
Red-proofed the duplicate/paste suite: temporarily reverted
regenerateTreeIds to structuredClone(oldNode.data) and confirmed both tests
fail with the historical DataCloneError before restoring the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
- 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.
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>
DEFAULT_HEADER_STATE's Navbar linked Home/About/Services/Contact, but a
brand new site only has a Home page — About/Services/Contact were dead
links on first click. Simplified the default to Home + an inert "Get
Started" CTA (href: '#') rather than seeding placeholder pages nobody
asked for. Updated default-header.test.ts's HTML-export assertions to
match the new default (and assert the removed links are gone).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>