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>
This commit is contained in:
Erik 2026-06-19 09:15:31 +02:00
parent 52bf9342df
commit b3753d1ab0

View file

@ -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 `<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.