feat(midsummer): theme state provider + data-midsummer attribute

This commit is contained in:
Erik 2026-06-19 09:25:54 +02:00
parent e803c35af9
commit 568992d0f9
3 changed files with 54 additions and 8 deletions

View file

@ -1,23 +1,22 @@
import { MapLayout } from './components/map/MapLayout'; import { MapLayout } from './components/map/MapLayout';
import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage'; import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage';
import { MidsummerProvider } from './hooks/useMidsummer';
import { useLiveData } from './hooks/useLiveData'; import { useLiveData } from './hooks/useLiveData';
import './styles/map-layout.css'; import './styles/map-layout.css';
import './styles/midsummer.css';
/** /**
* Single SPA entry. Branches on `?view=` query param: * Single SPA entry. Branches on `?view=` query param:
* /?view=dashboard fullscreen PlayerDashboardFullPage (new-tab target) * /?view=dashboard fullscreen PlayerDashboardFullPage (new-tab target)
* / default map + sidebar layout * / 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() { export default function App() {
const view = new URLSearchParams(window.location.search).get('view'); const view = new URLSearchParams(window.location.search).get('view');
if (view === 'dashboard') { return (
return <PlayerDashboardFullPage />; <MidsummerProvider>
} {view === 'dashboard' ? <PlayerDashboardFullPage /> : <DefaultApp />}
// Default: full app with map + sidebar. </MidsummerProvider>
return <DefaultApp />; );
} }
/** Default map-and-sidebar layout. Split out so the dashboard tab doesn't /** Default map-and-sidebar layout. Split out so the dashboard tab doesn't

View file

@ -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<MidsummerCtx | null>(null);
export const MidsummerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// Default ON: only the literal "off" disables it.
const [enabled, setEnabled] = useState<boolean>(() => localStorage.getItem(KEY) !== 'off');
const [soundOn, setSoundOn] = useState<boolean>(() => 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 (
<Ctx.Provider value={{ enabled, toggle, soundOn, toggleSound }}>
{children}
</Ctx.Provider>
);
};
export function useMidsummer(): MidsummerCtx {
const c = useContext(Ctx);
if (!c) throw new Error('useMidsummer must be used within MidsummerProvider');
return c;
}

View file

@ -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. */