8259bd468d
The six section links leave the top bar for a thin shelf under it, each a bordered pill at the page's normal text size, so the bar itself stays a row of actions and the jumps still read as buttons. The Theme menu gains the workspace's colour groups — the landing palette now takes its hue from the accent tokens — an Auto mode that follows the OS scheme live, and a dropdown in place of the three font chips.
51 lines
2.0 KiB
TypeScript
51 lines
2.0 KiB
TypeScript
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
import { ACCENTS, applyTheme, readAccent, readFonts, readTheme, type AccentId, type FontSetId, type ThemeMode } from './themes';
|
|
|
|
interface ThemeValue {
|
|
mode: ThemeMode;
|
|
accent: AccentId;
|
|
fonts: FontSetId;
|
|
setMode: (mode: ThemeMode) => void;
|
|
setAccent: (accent: AccentId) => void;
|
|
setFonts: (fonts: FontSetId) => void;
|
|
}
|
|
|
|
const Ctx = createContext<ThemeValue | null>(null);
|
|
|
|
// The tokens live in CSS; this provider only owns the three attributes on
|
|
// <html> and the persisted choice. No component ever reads a colour from JS.
|
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
const [mode, setModeState] = useState<ThemeMode>(readTheme);
|
|
const [accent, setAccentState] = useState<AccentId>(readAccent);
|
|
const [fonts, setFontsState] = useState<FontSetId>(readFonts);
|
|
|
|
useEffect(() => {
|
|
applyTheme(mode, accent, fonts);
|
|
// Auto has to follow the OS while the page is open, not just at mount.
|
|
if (mode !== 'auto') return;
|
|
const media = window.matchMedia('(prefers-color-scheme: light)');
|
|
const onChange = () => applyTheme(mode, accent, fonts);
|
|
media.addEventListener('change', onChange);
|
|
return () => media.removeEventListener('change', onChange);
|
|
}, [mode, accent, fonts]);
|
|
|
|
const setMode = useCallback((next: ThemeMode) => setModeState(next), []);
|
|
const setAccent = useCallback((next: AccentId) => setAccentState(next), []);
|
|
const setFonts = useCallback((next: FontSetId) => setFontsState(next), []);
|
|
|
|
const value = useMemo(
|
|
() => ({ mode, accent, fonts, setMode, setAccent, setFonts }),
|
|
[mode, accent, fonts, setMode, setAccent, setFonts]
|
|
);
|
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
}
|
|
|
|
export function useTheme(): ThemeValue {
|
|
const value = useContext(Ctx);
|
|
if (!value) throw new Error('useTheme used outside ThemeProvider');
|
|
return value;
|
|
}
|
|
|
|
export { ACCENTS, FONT_SETS } from './themes';
|
|
export type { AccentId, FontSetId, ThemeMode } from './themes';
|