Files
docs/website/HERO_EDITOR.md
whitlocktech 2e382893fe Add hero canvas editor spec (corrected to current code)
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>
2026-06-28 02:08:21 -05:00

6.4 KiB
Raw Blame History

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)

  1. Public settings is a whitelist, not getAll(). GET /api/v1/public/settingssettings.getPublic()PUBLIC_KEYS in settings.model.js. The doc's "no backend changes / picked up automatically" is wrong. Fix: add hero_layout to PUBLIC_KEYS (one line). hero_layout_draft stays out (admin-only) — which is why preview reads the draft via api.admin.getSettings().
  2. Moon is a reusable component (MoonDot.jsx, props size/glow), used in logo/login/maintenance — not "only the header." The moon element reuses it; it gains an optional color.
  3. Route vs. nav live in different files. /admin/hero route → App.jsx; sidebar link/title → NAV/TITLES in AdminLayout.jsx.
  4. 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' to PUBLIC_KEYS. No new routes/controllers — layout saves through the existing PUT /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 a DEFAULT_LAYOUT built 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.js changes 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_layout to the public whitelist; HeroElement.jsx; Portal reads the layout and renders elements with a DEFAULT_LAYOUT fallback (pre-populated current hero). Exit: portal looks identical with no key set; setting hero_layout by hand re-renders the hero.
  • Phase 2 — Editor shell + background/overlay. /admin/hero view + 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.parse wrapped in try/catch + version check → fall back to DEFAULT_LAYOUT.
  • Element ids via crypto.randomUUID() (never array index).
  • Empty elements → render DEFAULT_LAYOUT so 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).