diff --git a/docs/superpowers/specs/2026-06-19-midsummer-theme-design.md b/docs/superpowers/specs/2026-06-19-midsummer-theme-design.md new file mode 100644 index 00000000..df87e60e --- /dev/null +++ b/docs/superpowers/specs/2026-06-19-midsummer-theme-design.md @@ -0,0 +1,167 @@ +# 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 `` (`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 `` inside `.ml-map-group`. +- `components/map/MapLayout.tsx` — mount ``. +- `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 19–25). + +## 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.