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
This commit is contained in:
Jakub Zych
2026-10-16 09:54:02 +02:00
parent 047858ae84
commit 21b0d39de0
7 changed files with 181 additions and 17 deletions

View File

@@ -1,15 +1,32 @@
// Colour scheme (D-06, flagged assumption A6): the admin follows the system
// preference only, with no toggle. 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.
// 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 the current preference and follows changes. Returns the
* unsubscribe function.
* 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): () => void {
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