Build contract for the WYSIWYG portal-hero editor on hero-feature, derived from the design doc and corrected against the codebase: - public settings is a whitelist (getPublic/PUBLIC_KEYS), so hero_layout must be added there — the doc's "no backend changes" was wrong - moon is the reusable MoonDot component; route vs nav live in App.jsx vs AdminLayout.jsx; admin content is 1000px (canvas scales to fit) Locked decisions: full v1, buttons as a first-class element type, pre-populate the current hero on first run, native Pointer Events for drag. Phased plan with per-phase exit checks. No schema change (JSON in settings). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.4 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. Add
hero_layoutto the public whitelist;HeroElement.jsx; Portal reads the layout and renders elements with aDEFAULT_LAYOUTfallback (pre-populated current hero). Exit: portal looks identical with no key set; settinghero_layoutby hand re-renders the hero. - Phase 2 — Editor shell + background/overlay.
/admin/heroview + nav; canvas preview; background image upload + 3×3 position + opacity slider; draft auto-save, publish, preview, revert. Exit: change the background image WYSIWYG and publish. - Phase 3 — Elements: select / drag / text_block / buttons. Add/select/move (native pointer)/delete/z-order; text_block + buttons property panels. Exit: add a heading + CTA row, drag to place, publish, see it live.
- Phase 4 — moon + badge + image + resize + snap grid. Remaining element types, resize handles, 8px snap. Exit: place a moon and an uploaded image, resize, publish.
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).