diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5c9433a8..4e3ff977 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,23 +1,22 @@
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
- *
- * We don't pull in react-router for one extra view — when a third view
- * appears, swap this for proper routing.
*/
export default function App() {
const view = new URLSearchParams(window.location.search).get('view');
- if (view === 'dashboard') {
- return ;
- }
- // Default: full app with map + sidebar.
- return ;
+ return (
+
+ {view === 'dashboard' ? : }
+
+ );
}
/** Default map-and-sidebar layout. Split out so the dashboard tab doesn't
diff --git a/frontend/src/hooks/useMidsummer.tsx b/frontend/src/hooks/useMidsummer.tsx
new file mode 100644
index 00000000..2847de50
--- /dev/null
+++ b/frontend/src/hooks/useMidsummer.tsx
@@ -0,0 +1,45 @@
+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;
+}
diff --git a/frontend/src/styles/midsummer.css b/frontend/src/styles/midsummer.css
new file mode 100644
index 00000000..434133ec
--- /dev/null
+++ b/frontend/src/styles/midsummer.css
@@ -0,0 +1,2 @@
+/* Midsummer "Små grodorna" theme overlay. All rules scoped under
+ :root[data-midsummer] so they only apply when the theme is on. */