Files
Rpa_Skills/rpa-dashboard-ai/templates/reference/web/theme.tsx
T

91 lines
3.3 KiB
TypeScript

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<ThemeContextValue | null>(null)
function readStored<T extends string>(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<Mode>(() =>
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 <ThemeContext.Provider value={{ mode, setMode }}>{children}</ThemeContext.Provider>
}
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 (
<div className="sb-themes" role="radiogroup" aria-label="Appearance">
{MODES.map((m) => (
<button
key={m.value}
type="button"
role="radio"
aria-checked={mode === m.value}
aria-pressed={mode === m.value}
title={m.hint}
onClick={() => setMode(m.value)}
>
{m.label}
</button>
))}
</div>
)
}