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>
167 lines
8.1 KiB
Markdown
167 lines
8.1 KiB
Markdown
# 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 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. 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.
|