diff --git a/packages/types-builder/build.ts b/packages/types-builder/build.ts index ac17fea..df2f7d0 100755 --- a/packages/types-builder/build.ts +++ b/packages/types-builder/build.ts @@ -359,6 +359,7 @@ const emit = defineEmits(emits) for (const locale of vjsfLocales) { const schemaVjsfOpts = { ...schema['x-vjsf'] } delete schemaVjsfOpts.compName + console.log(` compiledLayout ${locale} options: ${JSON.stringify(schemaVjsfOpts)}`) const otherSchemas = { ...schemas } for (const [key, otherSchema] of Object.entries(schemas)) { if (key === schema.$id) continue @@ -375,7 +376,6 @@ const emit = defineEmits(emits) fullOptions.components[componentInfo.name] = componentInfo } - console.log(` compiledLayout ${locale} options: ${JSON.stringify(schemaVjsfOpts)}`) const compiledLayout = compileLayout(schema, fullOptions) let compiledLayoutCode = await serializeCompiledLayout(compiledLayout) // The serialized code declares `const compiledLayout = {...}`. 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/layout-fetch-error.vue b/packages/vuetify/layout-fetch-error.vue index f56e0bc..57d10fe 100644 --- a/packages/vuetify/layout-fetch-error.vue +++ b/packages/vuetify/layout-fetch-error.vue @@ -64,7 +64,7 @@ const { error, backTo = '/', backLabel } = defineProps<{ }>() const { t } = useI18n({ useScope: 'local' }) -const { switchOrganization, user } = useSession() +const { switchOrganization, user, account } = useSession() const statusCode = computed(() => error?.statusCode ?? error?.status ?? 500) @@ -99,9 +99,25 @@ const switchOrg = computed(() => { try { owner = JSON.parse(rawOwner) } catch { return null } if (!owner || owner.type !== 'organization' || !owner.id) return null - return user.value.organizations?.find(o => - o.id === owner!.id && (o.department ?? undefined) === (owner!.department ?? undefined) - ) ?? null + const orgs = user.value.organizations ?? [] + const ownerDept = owner.department ?? undefined + const isCurrentAccount = (o: { id: string, department?: string }) => + account.value?.type === 'organization' && + account.value.id === o.id && + (account.value.department ?? undefined) === (o.department ?? undefined) + + // Prefer a membership matching the resource's department exactly... + const exact = orgs.find(o => + o.id === owner!.id && (o.department ?? undefined) === ownerDept && !isCurrentAccount(o) + ) + if (exact) return exact + // ...otherwise fall back to a membership at the organization root: in + // simple-directory's authz model, root access generally grants visibility + // over department-scoped resources. + if (ownerDept) { + return orgs.find(o => o.id === owner!.id && !o.department && !isCurrentAccount(o)) ?? null + } + return null }) const doSwitch = () => { diff --git a/packages/vuetify/personal-menu.vue b/packages/vuetify/personal-menu.vue index 83e0410..508349c 100644 --- a/packages/vuetify/personal-menu.vue +++ b/packages/vuetify/personal-menu.vue @@ -182,9 +182,8 @@ fr: openPersonalMenu: Ouvrez le menu personnel personalAccount: Compte personnel switchAccount: Changer de compte - adminMode: mode admin + adminMode: Mode admin backToAdmin: Revenir à ma session administrateur - darkMode: mode nuit plannedDeletion: La suppression de l'utilisateur {name} et toutes ses informations est programmée le {plannedDeletion}. cancelDeletion: Annuler la suppression de l'utilisateur en: @@ -193,9 +192,8 @@ en: openPersonalMenu: Open personal menu personalAccount: Personal account switchAccount: Switch account - adminMode: admin mode + adminMode: Admin mode backToAdmin: Return to administrator session - darkMode: night mode plannedDeletion: The deletion of the user {name} and all its data is planned on the {plannedDeletion}. cancelDeletion: Cancel the deletion of the user 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 @@