Files
summercms/admin/src/app/theme.ts
Jakub Zych 21b0d39de0 feat(quick-261016-0fm): manual theme switch on the rail
- 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
2026-10-16 09:54:02 +02:00

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)
}