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