MosswartOverlord/docs/superpowers/specs/2026-06-19-midsummer-theme-design.md
Erik b3753d1ab0 docs(spec): Sma Grodorna midsummer theme design
Full-takeover frog/maypole midsummer theme for the React frontend:
scoped [data-midsummer] CSS overlay, useMidsummer hook (localStorage,
default on), dancing maypole inside the map pan/zoom group, frog +
flower-crown dots, Glad midsommar banner + confetti, frog-hop easter egg
replacing the rickroll, play-once unmuted jingle. Manual 🐸 toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-19 09:15:31 +02:00

8.1 KiB
Raw Blame History

Midsummer theme — "Små grodorna" — design spec

Date: 2026-06-19 Repo: MosswartOverlord (React frontend in frontend/) Status: approved design, ready for implementation plan

Goal

A "SUPER epic" Swedish-midsummer takeover of the Overlord dashboard, themed around Små grodorna (the little frogs) — fitting because Asheron's Call mosswarts are frog-men. Full visual takeover with a dancing maypole, frog + flower-crown player markers, a "Glad midsommar!" banner, and a frog-hop easter egg. Per-browser 🐸 toggle, default ON, with an unmuted jingle.

Scope

In scope: the React frontend only (frontend/). The classic v1 frontend (static/classic/) and legacy vanilla pages are dead and explicitly NOT themed.

Out of scope: backend changes, DB, the plugin. No server-side flag — the theme is a pure client concern toggled per browser.

Approach: scoped overlay, not a rewrite

A single attribute data-midsummer on <html> (document.documentElement) gates the entire theme. All midsummer styling lives in a NEW stylesheet frontend/src/styles/midsummer.css, every rule scoped under :root[data-midsummer] …, layered on top of the untouched base map-layout.css. Removing the attribute fully reverts the UI — the base theme remains the single source of truth.

Rejected alternative: swapping in a full second stylesheet (à la the old christmastheme/). Too heavy and it drifts from the base theme on every future change. The scoped overlay avoids duplication.

State & toggle

  • frontend/src/hooks/useMidsummer.ts — a hook backed by a tiny context so every component reads one source of truth.
    • Reads localStorage["mo-midsummer"]; absent ⇒ enabled (default ON). Only the literal string "off" disables it.
    • On change, sets/removes data-midsummer on document.documentElement and persists "on"/"off".
    • Exposes { enabled, toggle, soundOn, toggleSound }.
  • Provider mounted at the top of App.tsx so both the default app and the ?view=dashboard page inherit it.
  • 🐸 toggle button added to the sidebar tool-links (components/sidebar/SidebarWindowButtons.tsx), label reflects state.

Components (all gated by enabled)

1. Maypole — components/midsummer/Maypole.tsx

  • Rendered as a sibling of PlayerDots inside .ml-map-group in MapView.tsx (so it pans/zooms with the world automatically), only when imgSize.w > 0 && enabled.
  • Positioned via worldToPx(MAYPOLE_EW, MAYPOLE_NS, imgW, imgH). Default location: map center (the midpoint of the Dereth image / a central hub). Coordinate is a single named constant, trivial to move.
  • Visual: a midsommarstång (pole + flowered cross-bar + ribbons) built in CSS/SVG — no image asset — so it inherits theme colors and stays crisp at any zoom.
  • Carries its OWN ring of CSS-animated decorative frogs circling the pole (keyframe rotation on a wrapper). The spectacle is independent of live data, so it always looks alive even with nobody online. Real player dots near the pole read as "joining the dance" by proximity.
  • Pure CSS animation (transform-based) for 60fps; respects prefers-reduced-motion (ring holds still).

2. Frog + flower-crown player dots

  • No change to PlayerDots.tsx data flow. Under [data-midsummer], midsummer.css decorates .ml-dot with a wildflower-crown ring via a ::before pseudo-element, and turns the hovered/selected dot (.ml-dot-selected) into a little frog (pseudo-element eyes + green body).
  • Falls back gracefully: if a dot has an inline backgroundColor, the crown sits on top; the frog variant overrides the fill only on select/hover.

3. Glad midsummer banner + confetti — components/midsummer/MidsummerBanner.tsx

  • A festive top strip ("Glad midsommar! 🐸") rendered at the app shell level (in MapLayout.tsx, and on the dashboard page) when enabled.
  • One-shot snaps-glass / flower confetti burst on first load per session (guarded by sessionStorage), a lightweight self-removing CSS-particle effect (no library). Honors prefers-reduced-motion (skips the burst).

4. Frog-hop easter egg (replaces the rickroll)

  • Sidebar.tsx:62-80 currently appends a fullscreen /rick.mp4 overlay + shake on title click. Replace it with a Små grodorna hop: clicking the title toggles a body class running a bounce/hop keyframe across the UI for a few seconds, with frogs hopping across the screen. Self-cleans; clicking again re-triggers without stacking. The rick.mp4 reference is removed.
  • This easter egg is active regardless of the theme toggle (it's a gag, not a palette), but uses the same frog assets.

5. Jingle — hooks/useMidsummerSound.ts

  • Plays a short Små grodorna clip once (not looping — a looping jingle on a left-open dashboard is grating).
  • Asset: static/midsummer/sma-grodorna.mp3 (a short royalty-free / public clip sourced during implementation; documented in the plan).
  • Browser reality: unmuted audio cannot autoplay before a user gesture, so the clip fires on the first user interaction (any click) or the moment the 🐸/🔊 control is used — never silently on page-paint. A 🔇 control stops/disables it; preference persisted (soundOn).
  • Single module-level Audio/AudioContext, reused — no per-play leak (the audit flagged per-notification AudioContext leaks elsewhere; don't repeat that pattern).

File plan

New:

  • frontend/src/hooks/useMidsummer.ts (context + hook)
  • frontend/src/hooks/useMidsummerSound.ts
  • frontend/src/styles/midsummer.css
  • frontend/src/components/midsummer/Maypole.tsx
  • frontend/src/components/midsummer/MidsummerBanner.tsx
  • frontend/src/components/midsummer/FrogToggle.tsx
  • frontend/src/components/midsummer/confetti.ts (tiny helper)
  • static/midsummer/sma-grodorna.mp3 (audio asset)

Edited:

  • App.tsx — wrap in MidsummerProvider; import midsummer.css.
  • components/map/MapView.tsx — mount <Maypole> inside .ml-map-group.
  • components/map/MapLayout.tsx — mount <MidsummerBanner>.
  • components/map/Sidebar.tsx — replace rickroll block with frog-hop.
  • components/sidebar/SidebarWindowButtons.tsx — add 🐸 toggle (+ 🔊).
  • components/PlayerDashboardFullPage.tsx — render banner/toggle so the new-tab dashboard matches.

Decisions (locked)

  • Maypole location: map center (named constant, easy to relocate).
  • Jingle: plays once, unmuted, fires on first gesture.
  • Toggle default: ON, per-browser via localStorage.
  • Auto-date-gating: NOT implemented (user chose manual toggle). A future enhancement could default the toggle from the date (~Jun 1925).

Deploy

bash deploy-frontend.sh && git add static/ && git commit && git push, then git pull on the host (bind-mounted static/). No container restart. The new audio file lives under static/midsummer/ so it ships with the static bundle; confirm the service worker (sw.js) either ignores it or caches it intentionally.

Testing / verification

  • Toggle off ⇒ data-midsummer removed, UI identical to today (base theme intact). Toggle on ⇒ full takeover. Preference survives reload.
  • Maypole sits at map center and stays pinned to the world through pan/zoom; frogs animate; prefers-reduced-motion stops motion.
  • Player dots show crowns; hover/select shows frog.
  • Banner shows once per session; confetti does not re-fire on every render.
  • Easter egg hops and self-cleans; no rick.mp4 request remains.
  • Jingle plays once after first interaction; 🔇 stops it; no audio-context leak across repeated toggles.
  • npm run build succeeds; bundle includes the new chunk.

Risks / caveats

  • Default-on + shared dashboard: anyone opening it during the demo gets the full theme. That's intended; the 🐸 toggle is one click to calm it.
  • Unmuted autoplay is gesture-gated by browsers — communicated above; not a bug.
  • Animation perf: the map already re-renders on high-frequency telemetry; keep all midsummer animation pure-CSS/transform and outside React state so it doesn't add re-renders. Cap decorative frog count.
  • Asset licensing: use a clearly royalty-free / public-domain audio clip and note its source in the plan.