diff --git a/docs/superpowers/plans/2026-06-19-midsummer-theme.md b/docs/superpowers/plans/2026-06-19-midsummer-theme.md new file mode 100644 index 00000000..f431e976 --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-midsummer-theme.md @@ -0,0 +1,895 @@ +# 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 ( +