Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"tough-cookie": "^5.1.2"
},
"relativeDependencies": {
"@data-fair/lib-vue": "../lib/packages/vue",
"@data-fair/lib-vuetify": "../lib/packages/vuetify"
}
}
2 changes: 2 additions & 0 deletions portal/app/components/layout/layout-nav-bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

<v-toolbar-items v-if="portalConfig.authentication !== 'none'">
<notification-queue />
<theme-switcher :theme="portalConfig.theme" />
<layout-personal-menu
:login-color="navBarConfig.loginColor"
:nav-bar-color="navBarConfig.color"
Expand All @@ -50,6 +51,7 @@
import type { NavBar } from '#api/types/portal-config'
import { useDisplay } from 'vuetify'
import { useElementSize } from '@vueuse/core'
import themeSwitcher from '@data-fair/lib-vuetify/theme-switcher.vue'

const { navBarConfig } = defineProps<{
navBarConfig: NavBar
Expand Down
4 changes: 3 additions & 1 deletion portal/app/components/layout/layout-personal-app-bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<div id="agent-chat-appbar" class="d-flex align-center" />
<v-toolbar-items>
<notification-queue />
<theme-switcher :theme="portalConfig.theme" />

<!-- Personal Menu -->
<layout-personal-menu personal />
Expand All @@ -28,8 +29,9 @@
<script setup lang="ts">
import { VToolbar, VAppBar } from 'vuetify/components'
import { mdiMenu, mdiMenuOpen } from '@mdi/js'
import themeSwitcher from '@data-fair/lib-vuetify/theme-switcher.vue'

const { preview } = usePortalStore()
const { preview, portalConfig } = usePortalStore()
const { personalDrawer, breadcrumbs } = useNavigationStore()
const { t } = useI18n()

Expand Down
68 changes: 49 additions & 19 deletions portal/app/plugins/03-vuetify.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
import type { AppliedTheme, Theme } from '@data-fair/lib-vue/session.js'
import { createUiNotif } from '@data-fair/lib-vue/ui-notif.js'
import { fr, en } from 'vuetify/locale'

// vuetify-nuxt-module's useState('vuetify:nuxt:ssr-client-hints') shape — we only read prefers-color-scheme
interface VuetifySSRClientHints {
prefersColorScheme?: 'dark' | 'light' | 'no-preference'
}

export default defineNuxtPlugin((nuxtApp) => {
const cspNonce = useNonce()
const themeCookie = useCookie<'default' | 'hc' | 'dark' | 'hc-dark'>('theme', { default: () => 'default' })
const themeCookie = useCookie<Theme>('theme', { default: () => 'system' })
const langCookie = useCookie<'fr' | 'en'>('i18n_lang', { default: () => 'fr' })

const portalConfig = useNuxtApp().$portal.config
let colors = portalConfig.theme.colors
let dark = false

if (themeCookie.value === 'hc' && portalConfig.theme.hcColors) {
colors = portalConfig.theme.hcColors
}
if (themeCookie.value === 'dark' && portalConfig.theme.darkColors) {
colors = portalConfig.theme.darkColors
dark = true
}
if (themeCookie.value === 'hc-dark' && portalConfig.theme.hcDarkColors) {
colors = portalConfig.theme.hcDarkColors
dark = true
}

// https://nuxt.vuetifyjs.com/guide/advanced/runtime-hooks.html
nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => {
const applied = resolvePortalTheme(themeCookie.value)
let colors = portalConfig.theme.colors
let dark = false
if (applied === 'hc' && portalConfig.theme.hcColors) {
colors = portalConfig.theme.hcColors
}
if (applied === 'dark' && portalConfig.theme.darkColors) {
colors = portalConfig.theme.darkColors
dark = true
}
if (applied === 'hc-dark' && portalConfig.theme.hcDarkColors) {
colors = portalConfig.theme.hcDarkColors
dark = true
}

vuetifyOptions.locale = {
locale: langCookie.value,
fallback: 'en',
messages: { fr, en }
}

vuetifyOptions.theme = {
cspNonce,
defaultTheme: themeCookie.value,
defaultTheme: applied,
themes: {
[themeCookie.value]: {
[applied]: {
dark,
colors,
variables: {
// deactivate automatic partial transparencies
// best to control colors precisely and ensure sufficient contrast for readability
// disable partial transparencies for precise color control and readable contrast
'high-emphasis-opacity': 1,
'medium-emphasis-opacity': 0.87
}
Expand All @@ -50,4 +57,27 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('vuetify:ready', () => {
nuxtApp.vueApp.use(createUiNotif())
})

// SSR variant of resolveTheme: on 'system', read prefers-color-scheme from the SSR Client Hint
// (exposed by vuetify-nuxt-module's ssrClientHints) and fall back to matchMedia on the client.
// Sec-CH-Forced-Colors exists but vuetify-nuxt-module doesn't expose it, so HC can't be
// auto-picked at SSR — it requires an explicit user choice.
function resolvePortalTheme (theme: Theme): AppliedTheme {
if (theme !== 'system') return theme

let prefersDark = false
let prefersHC = false
if (import.meta.server) {
const hints = useState<VuetifySSRClientHints | undefined>('vuetify:nuxt:ssr-client-hints')
prefersDark = hints.value?.prefersColorScheme === 'dark'
} else {
prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false
prefersHC = window.matchMedia?.('(forced-colors: active)').matches ?? false
}

if (portalConfig.theme.hcDark && prefersDark && prefersHC) return 'hc-dark'
if (portalConfig.theme.hc && prefersHC) return 'hc'
if (portalConfig.theme.dark && prefersDark) return 'dark'
return 'default'
}
})
3 changes: 2 additions & 1 deletion portal/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export default defineNuxtConfig({
},
ssrClientHints: {
// reloadOnFirstRequest: false, // disabled because broken with Brave unfortunately
viewportSize: true
viewportSize: true, // help SSR choose the right breakpoint on first load to avoid hydration mismatches
prefersColorScheme: true // handled manually in 03-vuetify.ts to avoid conflicts with our 'system' cookie value semantics
}
},
vuetifyOptions: {
Expand Down
Loading