import { createContext, useContext, useEffect, useState } from 'react' /** * Two named appearances, not a palette picker. * * The original control was a row of six raw colour dots that changed which hue * the confirm/approve buttons used. It read as decoration sitting next to the * confidence-band legend — two colour controls side by side, only one of which * meant anything. The accent is now fixed to the brand, and this control does * the thing a reviewer actually wants: choose the light level for the room. * * There was a third mode, `eye` — the light palette under a warm sepia filter, * for long shifts. It is gone. It was a third appearance to keep in sync * forever, it tinted the scanned page a reviewer is trying to read accurately, * and both the OS and the monitor already offer the same warmth system-wide. */ export type Mode = 'light' | 'dark' const MODE_KEY = 'superbill.theme.mode' const MODES: { value: Mode; label: string; hint: string }[] = [ { value: 'light', label: 'Light', hint: 'Bright rooms' }, { value: 'dark', label: 'Dark', hint: 'Low-light rooms' }, ] type ThemeContextValue = { mode: Mode setMode: (m: Mode) => void } const ThemeContext = createContext(null) function readStored(key: string, valid: readonly T[], fallback: T): T { const v = localStorage.getItem(key) return (valid as readonly string[]).includes(v ?? '') ? (v as T) : fallback } export function ThemeProvider({ children }: { children: React.ReactNode }) { // A stored `eye` from before that mode was removed is not in the valid list, // so readStored falls back to `dark` rather than leaving the app in a mode // with no styles behind it. const [mode, setMode] = useState(() => readStored(MODE_KEY, ['light', 'dark'] as const, 'dark'), ) useEffect(() => { const root = document.documentElement root.classList.toggle('dark', mode === 'dark') root.setAttribute('data-mode', mode) localStorage.setItem(MODE_KEY, mode) }, [mode]) return {children} } export function useTheme(): ThemeContextValue { const ctx = useContext(ThemeContext) if (!ctx) throw new Error('useTheme must be used inside ThemeProvider') return ctx } /** Two text pills in a pill, matching the mockup's header control. The icons * that used to sit beside each label are gone with the third mode: with only * Light and Dark left, the two words are already unambiguous and the glyphs * were competing with the Sign out button next to them. * * Colours here are literal white-on-black alphas rather than tokens, and have * to be: this control sits ON the brand gradient, which is the one saturated * surface in the product, and a token-coloured control there reads as a hole * punched through the band. See `.sb-themes` in index.css. */ export function ThemeSwitcher() { const { mode, setMode } = useTheme() return (
{MODES.map((m) => ( ))}
) }