diff --git a/src/runtime/plugins/types.d.ts b/src/runtime/plugins/types.d.ts index 51a2e7b..97ec291 100644 --- a/src/runtime/plugins/types.d.ts +++ b/src/runtime/plugins/types.d.ts @@ -7,12 +7,14 @@ export interface ClientHintRequestFeatures { prefersReducedMotionAvailable: boolean viewportHeightAvailable: boolean viewportWidthAvailable: boolean + devicePixelRatioAvailable: boolean } export interface SSRClientHints extends ClientHintRequestFeatures { prefersColorScheme?: 'dark' | 'light' | 'no-preference' prefersReducedMotion?: 'no-preference' | 'reduce' viewportHeight?: number viewportWidth?: number + devicePixelRatio?: number colorSchemeFromCookie?: string colorSchemeCookie?: string } diff --git a/src/runtime/plugins/vuetify-client-hints.client.ts b/src/runtime/plugins/vuetify-client-hints.client.ts index b19d352..ff1ae5a 100644 --- a/src/runtime/plugins/vuetify-client-hints.client.ts +++ b/src/runtime/plugins/vuetify-client-hints.client.ts @@ -60,57 +60,58 @@ const plugin: Plugin<{ window.location.reload() } - if (viewportSize || (prefersColorScheme && prefersColorSchemeOptions)) { - // restore SSR state - nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => { - // on client, we update the display to avoid hydration mismatch on page refresh - // there will be some hydration mismatch since the headers sent by the user agent may not be accurate - if (viewportSize) { - const clientWidth = state.value.viewportWidth - const clientHeight = state.value.viewportHeight - vuetifyOptions.ssr = typeof clientWidth === 'number' - ? { - clientWidth, - clientHeight, - } - : true - } - - // update the theme - if (prefersColorScheme && prefersColorSchemeOptions) { - if (vuetifyOptions.theme === false) { - vuetifyOptions.theme = { defaultTheme: state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme } - } - else { - vuetifyOptions.theme = vuetifyOptions.theme ?? {} - vuetifyOptions.theme.defaultTheme = state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme - } - } - }) + // restore SSR state + nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => { + // on client, we update the display to avoid hydration mismatch on page refresh + // there will be some hydration mismatch since the headers sent by the user agent may not be accurate + if (viewportSize) { + const clientWidth = state.value.viewportWidth + const clientHeight = state.value.viewportHeight + vuetifyOptions.ssr = typeof clientWidth === 'number' + ? { + clientWidth, + clientHeight, + } + : true + } + else { + vuetifyOptions.ssr = true + } - // update theme logic + // update the theme if (prefersColorScheme && prefersColorSchemeOptions) { - const themeCookie = state.value.colorSchemeCookie - if (themeCookie) { - nuxtApp.hook('app:beforeMount', () => { - const vuetify = useNuxtApp().$vuetify - // update the theme - const cookieName = prefersColorSchemeOptions.cookieName - const parseCookieName = `${cookieName}=` - const cookieEntry = `${parseCookieName}${state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};` - watch(vuetify.theme.global.name, (newThemeName) => { - document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`) - }) - if (prefersColorSchemeOptions.useBrowserThemeOnly) { - const { darkThemeName, lightThemeName } = prefersColorSchemeOptions - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)') - prefersDark.addEventListener('change', (e) => { - vuetify.theme.global.name.value = e.matches ? darkThemeName : lightThemeName - }) - } - }) + if (vuetifyOptions.theme === false) { + vuetifyOptions.theme = { defaultTheme: state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme } + } + else { + vuetifyOptions.theme = vuetifyOptions.theme ?? {} + vuetifyOptions.theme.defaultTheme = state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme } } + }) + + // update theme logic + if (prefersColorScheme && prefersColorSchemeOptions) { + const themeCookie = state.value.colorSchemeCookie + if (themeCookie) { + nuxtApp.hook('app:beforeMount', () => { + const vuetify = useNuxtApp().$vuetify + // update the theme + const cookieName = prefersColorSchemeOptions.cookieName + const parseCookieName = `${cookieName}=` + const cookieEntry = `${parseCookieName}${state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};` + watch(vuetify.theme.global.name, (newThemeName) => { + document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`) + }) + if (prefersColorSchemeOptions.useBrowserThemeOnly) { + const { darkThemeName, lightThemeName } = prefersColorSchemeOptions + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)') + prefersDark.addEventListener('change', (e) => { + vuetify.theme.global.name.value = e.matches ? darkThemeName : lightThemeName + }) + } + }) + } } return { diff --git a/src/runtime/plugins/vuetify-client-hints.server.ts b/src/runtime/plugins/vuetify-client-hints.server.ts index a08c596..4d98056 100644 --- a/src/runtime/plugins/vuetify-client-hints.server.ts +++ b/src/runtime/plugins/vuetify-client-hints.server.ts @@ -23,6 +23,7 @@ const AcceptClientHintsHeaders = { prefersReducedMotion: 'Sec-CH-Prefers-Reduced-Motion', viewportHeight: 'Sec-CH-Viewport-Height', viewportWidth: 'Sec-CH-Viewport-Width', + devicePixelRatio: 'Sec-CH-DPR', } type AcceptClientHintsHeadersKey = keyof typeof AcceptClientHintsHeaders @@ -38,7 +39,7 @@ const plugin: Plugin<{ ssrClientHints: UnwrapNestedRefs }> = defineNuxtPlugin({ name: 'vuetify:client-hints:server:plugin', - order: -25, + order: 24, parallel: true, setup(nuxtApp) { const state = useState(VuetifyHTTPClientHints) @@ -107,23 +108,29 @@ type BrowserFeatures = Record v[0] >= 93, prefersReducedMotion: (_, v) => v[0] >= 108, viewportHeight: (_, v) => v[0] >= 108, viewportWidth: (_, v) => v[0] >= 108, + devicePixelRatio: (_, v) => v[0] >= 46, } const allowedBrowsers: [browser: Browser, features: BrowserFeatures][] = [ // 'edge', // 'edge-ios', ['chrome', chromiumBasedBrowserFeatures], - ['edge-chromium', chromiumBasedBrowserFeatures], + ['edge-chromium', { + ...chromiumBasedBrowserFeatures, + devicePixelRatio: (_, v) => v[0] >= 79, + }], ['chromium-webview', chromiumBasedBrowserFeatures], ['opera', { prefersColorScheme: (android, v) => v[0] >= (android ? 66 : 79), prefersReducedMotion: (android, v) => v[0] >= (android ? 73 : 94), viewportHeight: (android, v) => v[0] >= (android ? 73 : 94), viewportWidth: (android, v) => v[0] >= (android ? 73 : 94), + devicePixelRatio: (_, v) => v[0] >= 33, }], ] @@ -164,6 +171,7 @@ function lookupClientHints( prefersReducedMotionAvailable: false, viewportHeightAvailable: false, viewportWidthAvailable: false, + devicePixelRatioAvailable: false, } if (userAgent == null || userAgent.type !== 'browser') @@ -178,6 +186,7 @@ function lookupClientHints( if (ssrClientHintsConfiguration.viewportSize) { features.viewportHeightAvailable = browserFeatureAvailable(userAgent, 'viewportHeight') features.viewportWidthAvailable = browserFeatureAvailable(userAgent, 'viewportWidth') + features.devicePixelRatioAvailable = browserFeatureAvailable(userAgent, 'devicePixelRatio') } return features @@ -266,6 +275,25 @@ function collectClientHints( hints.viewportWidth = ssrClientHintsConfiguration.clientWidth } + if (hints.devicePixelRatioAvailable && ssrClientHintsConfiguration.viewportSize) { + const header = headers[AcceptClientHintsRequestHeaders.devicePixelRatio] + if (header) { + hints.firstRequest = false + try { + hints.devicePixelRatio = Number.parseFloat(header) + if (!Number.isNaN(hints.devicePixelRatio) && hints.devicePixelRatio > 0) { + if (typeof hints.viewportWidth === 'number') + hints.viewportWidth = Math.round(hints.viewportWidth / hints.devicePixelRatio) + if (typeof hints.viewportHeight === 'number') + hints.viewportHeight = Math.round(hints.viewportHeight / hints.devicePixelRatio) + } + } + catch { + // just ignore + } + } + } + return hints } @@ -292,6 +320,8 @@ function writeClientHintsResponseHeaders( if (ssrClientHintsConfiguration.viewportSize && clientHintsRequest.viewportHeightAvailable && clientHintsRequest.viewportWidthAvailable) { writeClientHintHeaders(AcceptClientHintsHeaders.viewportHeight, headers) writeClientHintHeaders(AcceptClientHintsHeaders.viewportWidth, headers) + if (clientHintsRequest.devicePixelRatioAvailable) + writeClientHintHeaders(AcceptClientHintsHeaders.devicePixelRatio, headers) } if (Object.keys(headers).length === 0)