# Midsummer "Små grodorna" Theme Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** A full-takeover Swedish-midsummer frog/maypole theme for the Overlord React dashboard, toggled per browser (default on), with a dancing maypole, frog + flower-crown player dots, a Glad midsommar banner + confetti, a frog-hop easter egg replacing the rickroll, and a WebAudio-synthesized *Små grodorna* jingle. **Architecture:** A `data-midsummer` attribute on `` gates a scoped CSS overlay (`midsummer.css`, all rules under `:root[data-midsummer]`) layered over the untouched base `map-layout.css`. A `MidsummerProvider`/`useMidsummer` context holds the on/off + sound state in `localStorage`. Dynamic pieces (maypole, banner, confetti, toggle, jingle) are small React components/hooks gated by the flag; palette, crowns and the hop are pure CSS. **Tech Stack:** React 19 + Vite + TypeScript, plain CSS, WebAudio API. No new dependencies, no audio asset. **Testing note:** This repo has **no automated frontend test runner** (verification is build + manual browser checks, per the repo's own docs). Each task therefore verifies via `npm run build` and a dev-server browser check rather than a unit-test runner. Run the dev server once up front: `cd frontend && npm run dev` (Vite on :5173, `/api` proxied to :8765) and keep it open across tasks. --- ## File structure New files: - `frontend/src/hooks/useMidsummer.tsx` — context + provider + `useMidsummer()` hook (state, persistence, attribute). - `frontend/src/hooks/useMidsummerSound.ts` — WebAudio jingle synth + first-gesture hook. - `frontend/src/styles/midsummer.css` — entire scoped theme overlay (palette, maypole, dots, banner, confetti, hop). - `frontend/src/components/midsummer/Maypole.tsx` — the dancing maypole (mounted in the map group). - `frontend/src/components/midsummer/MidsummerBanner.tsx` — banner + first-load confetti. - `frontend/src/components/midsummer/FrogToggle.tsx` — 🐸 theme + 🔊 sound toggle links. - `frontend/src/components/midsummer/confetti.ts` — DOM confetti burst helper. Modified files: - `frontend/src/App.tsx` — wrap in `MidsummerProvider`, import `midsummer.css`. - `frontend/src/components/map/MapView.tsx` — mount `` inside `.ml-map-group`. - `frontend/src/components/map/MapLayout.tsx` — mount ``, call `useMidsummerSound()`. - `frontend/src/components/PlayerDashboardFullPage.tsx` — same banner + sound for the new-tab dashboard. - `frontend/src/components/sidebar/SidebarWindowButtons.tsx` — add ``. - `frontend/src/components/map/Sidebar.tsx` — replace rickroll easter egg with frog-hop. --- ## Task 1: Theme state — provider, hook, attribute **Files:** - Create: `frontend/src/hooks/useMidsummer.tsx` - Create (empty): `frontend/src/styles/midsummer.css` - Modify: `frontend/src/App.tsx` - [ ] **Step 1: Create the context/provider/hook** Create `frontend/src/hooks/useMidsummer.tsx`: ```tsx import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; const KEY = 'mo-midsummer'; const SOUND_KEY = 'mo-midsummer-sound'; interface MidsummerCtx { enabled: boolean; toggle: () => void; soundOn: boolean; toggleSound: () => void; } const Ctx = createContext(null); export const MidsummerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { // Default ON: only the literal "off" disables it. const [enabled, setEnabled] = useState(() => localStorage.getItem(KEY) !== 'off'); const [soundOn, setSoundOn] = useState(() => localStorage.getItem(SOUND_KEY) !== 'off'); useEffect(() => { const el = document.documentElement; if (enabled) el.setAttribute('data-midsummer', ''); else el.removeAttribute('data-midsummer'); localStorage.setItem(KEY, enabled ? 'on' : 'off'); }, [enabled]); useEffect(() => { localStorage.setItem(SOUND_KEY, soundOn ? 'on' : 'off'); }, [soundOn]); const toggle = useCallback(() => setEnabled(e => !e), []); const toggleSound = useCallback(() => setSoundOn(s => !s), []); return ( {children} ); }; export function useMidsummer(): MidsummerCtx { const c = useContext(Ctx); if (!c) throw new Error('useMidsummer must be used within MidsummerProvider'); return c; } ``` - [ ] **Step 2: Create the (empty) overlay stylesheet** Create `frontend/src/styles/midsummer.css` with a single header comment so the import resolves: ```css /* Midsummer "Små grodorna" theme overlay. All rules scoped under :root[data-midsummer] so they only apply when the theme is on. */ ``` - [ ] **Step 3: Wrap the app in the provider and import the stylesheet** Replace the entire contents of `frontend/src/App.tsx` with: ```tsx import { MapLayout } from './components/map/MapLayout'; import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage'; import { MidsummerProvider } from './hooks/useMidsummer'; import { useLiveData } from './hooks/useLiveData'; import './styles/map-layout.css'; import './styles/midsummer.css'; /** * Single SPA entry. Branches on `?view=` query param: * /?view=dashboard → fullscreen PlayerDashboardFullPage (new-tab target) * / → default map + sidebar layout */ export default function App() { const view = new URLSearchParams(window.location.search).get('view'); return ( {view === 'dashboard' ? : } ); } /** Default map-and-sidebar layout. Split out so the dashboard tab doesn't * spin up useLiveData twice for the same render. */ function DefaultApp() { const data = useLiveData(); return ; } ``` - [ ] **Step 4: Verify build + attribute toggling** Run: `cd frontend && npm run build` Expected: build succeeds, no TS errors. In the dev server browser console, run `document.documentElement.hasAttribute('data-midsummer')` → expect `true` (default on). Run `localStorage.setItem('mo-midsummer','off')` then reload → expect `false`. Set back to `'on'`. - [ ] **Step 5: Commit** ```bash git add frontend/src/hooks/useMidsummer.tsx frontend/src/styles/midsummer.css frontend/src/App.tsx git commit -m "feat(midsummer): theme state provider + data-midsummer attribute" ``` --- ## Task 2: Base palette overlay (pond-green takeover) **Files:** - Modify: `frontend/src/styles/midsummer.css` - [ ] **Step 1: Append the palette overlay** Append to `frontend/src/styles/midsummer.css`: ```css :root[data-midsummer] .ml-sidebar { background: #0a1f16; border-right: 2px solid #1c5a2c; } :root[data-midsummer] .ml-map-container { background: #0e2a1e; } :root[data-midsummer] .ml-sidebar-title { color: #7ed957; text-shadow: 0 0 6px rgba(126, 217, 87, 0.35); } :root[data-midsummer] .ml-tool-link { color: #bfe9a8; } :root[data-midsummer] .ml-tool-link:hover { color: #eafbe0; } :root[data-midsummer] .ml-server-status, :root[data-midsummer] .ml-counters, :root[data-midsummer] .ml-player-row { border-color: #1c5a2c; } :root[data-midsummer] .ml-player-row.ml-player-selected { background: rgba(126, 217, 87, 0.14); outline: 1px solid rgba(126, 217, 87, 0.5); } :root[data-midsummer] .ml-sort-btn, :root[data-midsummer] .ml-btn { border-color: #2c6e36; } ``` - [ ] **Step 2: Verify in browser** Reload the dev server with the theme on. Expect: sidebar turns deep pond-green, title turns lime with a glow, map background darkens to forest green, tool links go pale green. Toggle `data-midsummer` off in console → expect the original dark theme returns exactly. Run: `cd frontend && npm run build` → expect success. - [ ] **Step 3: Commit** ```bash git add frontend/src/styles/midsummer.css git commit -m "feat(midsummer): pond-green palette overlay for sidebar and map" ``` --- ## Task 3: 🐸 toggle (+ 🔊 sound) in the sidebar **Files:** - Create: `frontend/src/components/midsummer/FrogToggle.tsx` - Modify: `frontend/src/components/sidebar/SidebarWindowButtons.tsx` > Note: `FrogToggle` imports `playSmaGrodorna` from `useMidsummerSound`, which is created in Task 8. To keep tasks independently buildable, this task includes a minimal stub of that module; Task 8 replaces the stub with the full synth. If executing in order, create the stub now. - [ ] **Step 1: Create the jingle module stub (replaced fully in Task 8)** Create `frontend/src/hooks/useMidsummerSound.ts`: ```ts // Stub — replaced with the full WebAudio synth in Task 8. export function playSmaGrodorna(): void {} ``` - [ ] **Step 2: Create the toggle component** Create `frontend/src/components/midsummer/FrogToggle.tsx`: ```tsx import React from 'react'; import { useMidsummer } from '../../hooks/useMidsummer'; import { playSmaGrodorna } from '../../hooks/useMidsummerSound'; /** 🐸 theme toggle + 🔊 jingle toggle, rendered among the sidebar tool links. */ export const FrogToggle: React.FC = () => { const { enabled, toggle, soundOn, toggleSound } = useMidsummer(); return ( <> 🐸 Midsommar {enabled ? 'on' : 'off'} {enabled && ( { const turningOn = !soundOn; toggleSound(); if (turningOn) playSmaGrodorna(); // this click is a user gesture }} > {soundOn ? '🔊' : '🔇'} Jingle )} ); }; ``` - [ ] **Step 3: Mount it in the sidebar tool links** In `frontend/src/components/sidebar/SidebarWindowButtons.tsx`, add the import at the top: ```tsx import { FrogToggle } from '../midsummer/FrogToggle'; ``` Then add `` as the first child inside the `
` (immediately before the `🤖 Assistant` span): ```tsx
openWindow('agent', 'Overlord Assistant')}>🤖 Assistant ``` - [ ] **Step 4: Verify** Run: `cd frontend && npm run build` → expect success. In the browser: the sidebar shows `🐸 Midsommar on` and `🔊 Jingle`. Click `🐸` → theme turns off, label becomes `Midsommar off`, the `🔊 Jingle` link disappears, and the page reverts to the base dark theme. Click again → back on, preference survives reload. - [ ] **Step 5: Commit** ```bash git add frontend/src/components/midsummer/FrogToggle.tsx frontend/src/components/sidebar/SidebarWindowButtons.tsx frontend/src/hooks/useMidsummerSound.ts git commit -m "feat(midsummer): sidebar frog toggle + jingle toggle (sound stubbed)" ``` --- ## Task 4: Dancing maypole on the map **Files:** - Create: `frontend/src/components/midsummer/Maypole.tsx` - Modify: `frontend/src/styles/midsummer.css` - Modify: `frontend/src/components/map/MapView.tsx` - [ ] **Step 1: Create the Maypole component** Create `frontend/src/components/midsummer/Maypole.tsx`: ```tsx import React from 'react'; import { useMidsummer } from '../../hooks/useMidsummer'; interface Props { imgW: number; imgH: number; } // Kept small for perf — these orbit the pole via one CSS animation. const FROG_COUNT = 6; // Default: dead centre of the Dereth map image. To plant at a landmark, // import { worldToPx } from '../../utils/coordinates' and compute from // world coords instead. const center = (imgW: number, imgH: number) => ({ x: imgW / 2, y: imgH / 2 }); /** * Midsommarstång planted inside the map's pan/zoom group, so it scales and * pans with the world automatically. Carries its own ring of dancing frogs * (one CSS rotation) so the spectacle is independent of who is online. */ export const Maypole: React.FC = ({ imgW, imgH }) => { const { enabled } = useMidsummer(); if (!enabled || imgW === 0) return null; const { x, y } = center(imgW, imgH); return (