- the rail's top button cycles system, light and dark and names the current choice; light and dark ignore prefers-color-scheme - usePreferences applies the cached theme before the first await of boot, saves the choice through PUT /account/preferences and reverts with a toast when the save fails; me() applies the stored preference
43 lines
1.8 KiB
TypeScript
43 lines
1.8 KiB
TypeScript
// Colour scheme (D-06; assumption A6 superseded). The admin follows the
|
|
// system preference by default; light and dark are manual choices the admin
|
|
// makes on the rail's top button and keeps per account (usePreferences). The
|
|
// .dark class on the document element swaps the design's dark tokens in
|
|
// main.css; the sidebar stays dark in both modes and gains its #222b40 right
|
|
// border in dark mode.
|
|
export const DARK_QUERY = '(prefers-color-scheme: dark)'
|
|
|
|
/** The admin's theme: follow the system, or always light or dark. */
|
|
export type ThemeChoice = 'system' | 'light' | 'dark'
|
|
|
|
/** The theme choices in the order the rail button cycles through them. */
|
|
export const THEME_CHOICES: readonly ThemeChoice[] = ['system', 'light', 'dark']
|
|
|
|
export function isThemeChoice(value: unknown): value is ThemeChoice {
|
|
return typeof value === 'string' && (THEME_CHOICES as readonly string[]).includes(value)
|
|
}
|
|
|
|
/**
|
|
* Applies a theme choice. light and dark set the class once and subscribe
|
|
* to nothing; system applies the current preference and follows changes.
|
|
* Returns the unsubscribe function.
|
|
*/
|
|
export function applyColorScheme(win: Window = window, choice: ThemeChoice = 'system'): () => void {
|
|
const root = win.document.documentElement
|
|
if (choice !== 'system') {
|
|
root.classList.toggle('dark', choice === 'dark')
|
|
return () => undefined
|
|
}
|
|
if (typeof win.matchMedia !== 'function') {
|
|
root.classList.remove('dark')
|
|
return () => undefined
|
|
}
|
|
const query = win.matchMedia(DARK_QUERY)
|
|
const apply = (dark: boolean): void => {
|
|
root.classList.toggle('dark', dark)
|
|
}
|
|
apply(query.matches)
|
|
const onChange = (event: MediaQueryListEvent): void => apply(event.matches)
|
|
query.addEventListener('change', onChange)
|
|
return () => query.removeEventListener('change', onChange)
|
|
}
|