From 21b0d39de02cebeb5bc00f67009dd218370bf9cc Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 16 Oct 2026 09:54:02 +0200 Subject: [PATCH] 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 --- admin/src/app/theme.ts | 31 ++++-- admin/src/components/shell/PluginRail.vue | 23 +++- admin/src/main.ts | 8 +- admin/src/state/useAuth.ts | 3 + admin/src/state/usePreferences.ts | 124 ++++++++++++++++++++++ admin/src/state/useSidebar.ts | 7 +- docs/backend/admin-spa.md | 2 + 7 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 admin/src/state/usePreferences.ts diff --git a/admin/src/app/theme.ts b/admin/src/app/theme.ts index 8ba825a..76a851e 100644 --- a/admin/src/app/theme.ts +++ b/admin/src/app/theme.ts @@ -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 diff --git a/admin/src/components/shell/PluginRail.vue b/admin/src/components/shell/PluginRail.vue index 14a1c48..8cb36fc 100644 --- a/admin/src/components/shell/PluginRail.vue +++ b/admin/src/components/shell/PluginRail.vue @@ -1,11 +1,12 @@