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>
8.1 KiB
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-midsummerondocument.documentElementand persists"on"/"off". - Exposes
{ enabled, toggle, soundOn, toggleSound }.
- Reads
- Provider mounted at the top of
App.tsxso both the default app and the?view=dashboardpage 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
PlayerDotsinside.ml-map-groupinMapView.tsx(so it pans/zooms with the world automatically), only whenimgSize.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.tsxdata flow. Under[data-midsummer],midsummer.cssdecorates.ml-dotwith a wildflower-crown ring via a::beforepseudo-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) whenenabled. - One-shot snaps-glass / flower confetti burst on first load per session
(guarded by
sessionStorage), a lightweight self-removing CSS-particle effect (no library). Honorsprefers-reduced-motion(skips the burst).
4. Frog-hop easter egg (replaces the rickroll)
Sidebar.tsx:62-80currently appends a fullscreen/rick.mp4overlay + 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. Therick.mp4reference 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 andresume()d on gesture — no per-play allocation (the audit flagged per-notificationAudioContextleaks elsewhere; don't repeat that pattern).
File plan
New:
frontend/src/hooks/useMidsummer.ts(context + hook)frontend/src/hooks/useMidsummerSound.tsfrontend/src/styles/midsummer.cssfrontend/src/components/midsummer/Maypole.tsxfrontend/src/components/midsummer/MidsummerBanner.tsxfrontend/src/components/midsummer/FrogToggle.tsxfrontend/src/components/midsummer/confetti.ts(tiny helper)- (no audio asset — jingle is WebAudio-synthesized)
Edited:
App.tsx— wrap inMidsummerProvider; importmidsummer.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-midsummerremoved, 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-motionstops 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.mp4request remains. - Jingle plays once after first interaction; 🔇 stops it; no audio-context leak across repeated toggles.
npm run buildsucceeds; 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.