Final phase of the hero canvas editor (see HERO_EDITOR.md) — v1 complete. - element tray adds moon, badge, and image; property panels: - moon: size / glow / color - badge: text / background / text color / corner radius - image: upload (/admin/uploads, >1MB warning) / width% / alt - corner resize handle on selected elements (image→width%, moon→size, text_block→box width) - 8px snap-grid toggle with a faint canvas grid overlay; drag snaps when on - HeroElement: image element shows an "Upload an image" placeholder until a source is set (a srcless image never ships live) Verified in-browser: all five element types add + edit; moon resized 64->104px via the handle; snap grid overlays; a published moon + badge render on the live portal; no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.5 KiB
UOMysticmoon — Hero Canvas Editor Spec
Branch:
hero-feature. Build contract for the WYSIWYG portal-hero editor. Derived from the design doc Hero Canvas Editor — Design Document, corrected to match the current codebase and with the open questions resolved. Same workflow as the wiki upgrade: design → phased build → verify.
1. Goal
Let staff compose the portal hero (background image, overlay opacity, and floating
elements — text, CTA buttons, moon, badge, image) in-browser, then preview and
publish — no source edits. Layout persists as JSON in the existing settings table.
2. Locked decisions
| # | Decision |
|---|---|
| Scope | Full v1 — background/overlay, all element types, drag/resize/z-order, draft→preview→publish (built in phases) |
| CTA buttons | First-class buttons element type (independently positioned), not baked into a text block |
| First run | Pre-populate the canvas with today's hero (headline, subtitle, teaser, CTAs) as editable elements so nothing changes visually until edited |
| Drag | Native Pointer Events (mouse/touch/pen), zero dependencies |
| Font size | Stored in px (fixed reference canvas) |
| Image compression | None server-side; client warns when a file is > ~1 MB |
| Preview | ?preview=1 renders the draft by reading it through the authenticated admin settings endpoint |
| Other pages | Out of scope for v1 (design allows a per-page key later) |
3. Corrections to the design doc (current-code reality)
- Public settings is a whitelist, not
getAll().GET /api/v1/public/settings→settings.getPublic()→PUBLIC_KEYSin settings.model.js. The doc's "no backend changes / picked up automatically" is wrong. Fix: addhero_layouttoPUBLIC_KEYS(one line).hero_layout_draftstays out (admin-only) — which is why preview reads the draft viaapi.admin.getSettings(). - Moon is a reusable component (MoonDot.jsx,
props
size/glow), used in logo/login/maintenance — not "only the header." Themoonelement reuses it; it gains an optionalcolor. - Route vs. nav live in different files.
/admin/heroroute → App.jsx; sidebar link/title →NAV/TITLESin AdminLayout.jsx. - Admin content area is
maxWidth: 1000px— the editor canvas renders scaled-to-fit; percentage positions stay faithful.
Everything else in the doc matches (hardcoded HERO_BG + CTAs + homepage_teaser
in Portal.jsx; updateSettings accepts
arbitrary keys; /admin/uploads exists; default hero asset present; TEXT settings
columns — no schema change).
4. Data model — no schema change
Two settings keys (TEXT): hero_layout (live) and hero_layout_draft (admin).
{
"version": 1,
"background": { "image_url": null, "position_x": "left", "position_y": "center", "size": "cover" },
"overlay": { "opacity": 0.72 },
"elements": [
{ "id": "uuid", "type": "text_block|buttons|moon|badge|image",
"x": 50, "y": 42, "z": 1, "anchor": "center", "props": { /* per type */ } }
]
}
Positions are % of canvas (reference width 1080, matching .shell), so the
layout adapts across viewports without breakpoint data. version is validated
(=== 1) before use; anything else falls back.
Element props
| Type | Props |
|---|---|
text_block |
lines: [{ text, tag(h1/h2/p/span), fontSize(px), color, weight }], align |
buttons |
items: [{ label, to, variant(primary/ghost) }], align, gap |
moon |
size, glow, color |
badge |
text, bgColor, textColor, borderRadius |
image |
src, width(%), alt |
5. Backend changes
- One line: add
'hero_layout'toPUBLIC_KEYS. No new routes/controllers — layout saves through the existingPUT /admin/settings; images via/admin/uploads.
6. Frontend changes
- New
client/src/components/HeroElement.jsx— renders one element by type (shared by the live portal and the editor canvas). - New
client/src/routes/admin/views/HeroEditor.jsx— canvas + element tray + properties panel; native-pointer drag/resize; background/overlay panel; snap grid; auto-save draft, preview, publish, revert. - Edit Portal.jsx — parse
hero_layout(or draft when?preview=1+ admin), render elements, fall back to aDEFAULT_LAYOUTbuilt from today's hero so the page is unchanged until edited. - Edit AdminLayout.jsx (nav) +
App.jsx (route
/admin/hero). - Edit MoonDot.jsx — optional
color. - No
client/src/api/client.jschanges needed beyond what exists (admin.updateSettings,admin.getSettings,admin.upload).
7. Phased build (each phase: build → verify in preview → commit)
- Phase 0 — Spec ✅ this document.
- Phase 1 — Data path & renderer ✅ (verified 2026-06-28).
hero_layoutwhitelisted;HeroElement.jsx; Portal renders the layout with aDEFAULT_LAYOUTfallback. Default render matches the old hero; publishing a layout re-renders; draft key not exposed publicly. Shared helpers moved toclient/src/lib/heroLayout.js. - Phase 2 — Editor shell + background/overlay ✅ (verified 2026-06-28).
/admin/heroview + sidebar nav; canvas live-preview; background upload + 3×3 position + overlay opacity; debounced draft auto-save; publish;?preview=1reads the draft (admin) with a banner; revert. Verified: overlay/position update the canvas, auto-save writes the draft, publish writes live, preview shows the draft while the normal portal shows live. - Phase 3 — Elements: select / drag / text_block / buttons ✅ (verified 2026-06-28). Element tray (+ Text / + Buttons); click-to-select with outline; native Pointer Events drag (% of canvas); Delete key + panel delete; z-order (send back / bring forward); text_block line editor (text/tag/size/color/bold, add/remove lines, align) and buttons editor (label/path/variant, add/remove). Verified: select shows the line editor, editing a line updates the canvas live, drag moved 50%→65%, add→3/delete→2 elements, empty-canvas click deselects.
- Phase 4 — moon + badge + image + resize + snap grid ✅ (verified 2026-06-28). Tray adds moon/badge/image; property panels (moon: size/glow/color; badge: text/colors/radius; image: upload/width/alt); corner resize handle (image→width%, moon→size, text→box width); 8px snap-grid toggle with overlay; image placeholder until a file is chosen. Verified: each type adds + edits, resize moved a moon 64→104px, snap grid shows, and a published moon+badge render on the live portal.
Status: v1 feature-complete. All phases verified end-to-end; ready for PR. Deferred (noted in the design doc as follow-ups): 8-point resize (only a corner handle for now), per-viewport layouts, server-side image compression.
8. Edge cases (from the doc, carried forward)
JSON.parsewrapped in try/catch +versioncheck → fall back toDEFAULT_LAYOUT.- Element ids via
crypto.randomUUID()(never array index). - Empty
elements→ renderDEFAULT_LAYOUTso the hero is never blank. - Last-write-wins on concurrent admin edits (acceptable for this shard).
- Client-side warning for background files > ~1 MB (no hard block; 8 MB server cap).