diff --git a/packages/vue/session.ts b/packages/vue/session.ts index dbe62db..bbbbc75 100644 --- a/packages/vue/session.ts +++ b/packages/vue/session.ts @@ -57,16 +57,16 @@ export interface Colors { 'on-admin': string } -interface FullSiteInfo { +export interface FullSiteInfo { main?: boolean theme: { logo?: string colors: Colors - dark: boolean + dark?: boolean darkColors?: Colors - hc: boolean + hc?: boolean hcColors?: Colors - hcDark: boolean + hcDark?: boolean hcDarkColors?: Colors } } @@ -82,7 +82,14 @@ export interface SiteInfo { owner: AccountKeys } -type Theme = 'default' | 'dark' | 'hc' | 'hc-dark' +export type AppliedTheme = 'default' | 'dark' | 'hc' | 'hc-dark' +// `theme` cookie semantics: +// - absent: implicit 'system' (no choice made yet) +// - 'system': explicit "follow the OS preference" +// - other: explicit override +// In both 'system' cases the applied theme is computed at runtime via +// resolveTheme() using prefers-color-scheme + forced-colors. +export type Theme = AppliedTheme | 'system' export interface Session { state: SessionState @@ -120,10 +127,26 @@ export type SessionAuthenticated = Omit): Promise // cookies are the source of truth and this information is transformed into the state reactive object const cookies = initOptions?.cookies ?? new Cookies(options.req?.headers.cookie) const readState = () => { - theme.value = cookies.get('theme') ?? null + // absent cookie is treated as implicit 'system' so consumers (theme-switcher + // radios, host plugins) always have a meaningful value to bind to. + theme.value = (cookies.get('theme') as Theme | undefined) ?? 'system' const langCookie = cookies.get('i18n_lang') state.lang = langCookie ?? options.defaultLang @@ -412,13 +437,13 @@ export async function getSession (initOptions: Partial): Promise authOnlyOtherSite: siteInfo.authOnlyOtherSite, owner: siteInfo.owner } - if (theme.value == null) theme.value = getDefaultTheme(siteInfo) - if (theme.value === 'hc') partialSite.colors = siteInfo.theme.hcColors - if (theme.value === 'dark') { + const applied = resolveTheme(theme.value, siteInfo) + if (applied === 'hc') partialSite.colors = siteInfo.theme.hcColors + if (applied === 'dark') { partialSite.colors = siteInfo.theme.darkColors partialSite.dark = true } - if (theme.value === 'hc-dark') { + if (applied === 'hc-dark') { partialSite.colors = siteInfo.theme.hcDarkColors partialSite.dark = true } @@ -433,6 +458,16 @@ export async function getSession (initOptions: Partial): Promise // @ts-ignore if (!ssr && window.__PUBLIC_SITE_INFO) setSiteInfo(window.__PUBLIC_SITE_INFO) + // re-apply the theme when the OS preference changes while the user is on + // 'system'. Important for mobile devices that switch light/dark over the day. + if (!ssr && typeof window !== 'undefined' && window.matchMedia) { + const onOsPrefChange = () => { + if (theme.value === 'system' && fullSite.value) setSiteInfo(fullSite.value) + } + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', onOsPrefChange) + window.matchMedia('(forced-colors: active)').addEventListener('change', onOsPrefChange) + } + // immediately performs a keepalive, but only on top windows (not iframes or popups) // and only if it was not done very recently (maybe from a refreshed page next to this one) // also run an auto-refresh loop diff --git a/packages/vuetify/index.ts b/packages/vuetify/index.ts index cdf8586..fd32cd8 100644 --- a/packages/vuetify/index.ts +++ b/packages/vuetify/index.ts @@ -1,4 +1,4 @@ -import { type Session } from '@data-fair/lib-vue/session.js' +import { resolveTheme, type Session } from '@data-fair/lib-vue/session.js' import { VuetifyOptions } from 'vuetify' import { fr, en } from 'vuetify/locale' @@ -20,6 +20,9 @@ const baseDarkColors = { export function vuetifySessionOptions (session: Session, cspNonce?: string): VuetifyOptions { if (!session.site.value) throw new Error('vuetifySessionOptions requires fetching site info in session util') const colors = { ...baseColors, ...session.site.value?.colors } + const themeName = session.fullSite.value + ? resolveTheme(session.theme.value, session.fullSite.value) + : 'default' return { ssr: false, locale: { @@ -28,9 +31,9 @@ export function vuetifySessionOptions (session: Session, cspNonce?: string): Vue }, theme: { cspNonce, - defaultTheme: session.theme.value ?? 'default', + defaultTheme: themeName, themes: { - [session.theme.value ?? 'default']: { + [themeName]: { dark: session.site.value?.dark, colors, variables: { diff --git a/packages/vuetify/theme-switcher.vue b/packages/vuetify/theme-switcher.vue index 16f18cd..d9bb460 100644 --- a/packages/vuetify/theme-switcher.vue +++ b/packages/vuetify/theme-switcher.vue @@ -1,20 +1,20 @@