Hero Phase 3: element select / drag / edit (text_block + buttons)

Third phase of the hero canvas editor (see HERO_EDITOR.md).

- HeroElement: editor mode — inner content made non-interactive so the wrapper
  handles select/drag; selection outline; box width now canvas-relative
  (calc(100% - 36px)) so text blocks fit the smaller editor canvas
- HeroEditor: element tray (+ Text / + Buttons), click-to-select, native
  Pointer Events drag (position as % of the canvas, clamped), Delete key + panel
  delete, z-order (send back / bring forward), and per-type property panels:
  - text_block: per-line text / tag / font size (px) / color / bold, add+remove
    lines, alignment
  - buttons: per-item label / path / variant, add+remove, alignment
  empty-canvas click deselects (back to the background panel)
- theme.css: .hero-el-editable outline/hover/selected + grid helper

Verified in-browser: selecting shows the line editor, editing updates the canvas
live, drag repositions, add/delete and z-order work, deselect returns to the
background panel; no console errors. Moon/badge/image + resize + snap are Phase 4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 08:40:52 -05:00
parent ccad727ec3
commit 785090eb97
4 changed files with 321 additions and 91 deletions

View File

@@ -105,9 +105,17 @@ function content(element) {
}
}
// Absolute-positioned wrapper + type-specific content. `wrapperStyle` lets the
// editor layer on selection affordances without changing positioning logic.
export default function HeroElement({ element, wrapperStyle, children }) {
// Absolute-positioned wrapper + type-specific content. In `editor` mode the inner
// content is made non-interactive (so clicks select/drag the wrapper) and the
// wrapper takes selection styling + an onPointerDown handler.
export default function HeroElement({
element,
wrapperStyle,
editor = false,
selected = false,
onPointerDown,
children,
}) {
const anchor = element.anchor || 'center'
const transform =
anchor === 'center'
@@ -115,13 +123,17 @@ export default function HeroElement({ element, wrapperStyle, children }) {
: anchor === 'top-right'
? 'translateX(-100%)'
: undefined
// text_block/buttons may set a box width (px); kept within the viewport on mobile.
// text_block/buttons may set a box width (px); kept within the containing block
// (the hero section live, or the editor canvas) with small side gutters.
const boxWidth =
(element.type === 'text_block' || element.type === 'buttons') && element.props?.width
? `min(${element.props.width}px, calc(100vw - 36px))`
? `min(${element.props.width}px, calc(100% - 36px))`
: undefined
const cls = [editor ? 'hero-el-editable' : '', selected ? 'is-selected' : ''].filter(Boolean).join(' ')
return (
<div
className={cls || undefined}
onPointerDown={onPointerDown}
style={{
position: 'absolute',
left: `${element.x}%`,
@@ -129,10 +141,11 @@ export default function HeroElement({ element, wrapperStyle, children }) {
zIndex: element.z || 0,
transform,
width: boxWidth,
cursor: editor ? 'move' : undefined,
...wrapperStyle,
}}
>
{content(element)}
<div style={editor ? { pointerEvents: 'none' } : undefined}>{content(element)}</div>
{children}
</div>
)