MosswartOverlord/docs/superpowers/specs/2026-06-19-midsummer-theme-design.md
Erik e803c35af9 docs(plan): Sma Grodorna midsummer theme implementation plan (+ spec: WebAudio jingle)
9-task plan with complete code for the frog/maypole theme: scoped CSS
overlay, useMidsummer provider, dancing maypole, crown/frog dots, banner +
confetti, frog-hop easter egg, WebAudio jingle. Spec updated to synthesize
the jingle (no mp3 asset / licensing).

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

167 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 the *Små grodorna* melody **once** (not looping — a looping jingle
on a left-open dashboard is grating).
- **No audio asset**: the melody is synthesized with WebAudio oscillators
from the public-domain folk tune (note frequencies in code). This removes
any licensing question and ships nothing for the service worker to cache.
- Browser reality: WebAudio cannot start before a user gesture, so the tune
fires on the **first user interaction (any click) or the moment the 🐸/🔊
control is used** — never silently on page-paint. A 🔇 control disables
it; preference persisted (`soundOn`, default on).
- Single module-level `AudioContext`, reused and `resume()`d on gesture — no
per-play allocation (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)
- (no audio asset — jingle is WebAudio-synthesized)
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. No new
runtime assets (jingle is synthesized), so the service worker needs no
changes.
## 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.