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:
parent
52bf9342df
commit
b3753d1ab0
1 changed files with 167 additions and 0 deletions
167
docs/superpowers/specs/2026-06-19-midsummer-theme-design.md
Normal file
167
docs/superpowers/specs/2026-06-19-midsummer-theme-design.md
Normal 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 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue