feat(theme): checkpoint navbar refraction surface

This commit is contained in:
InfinityPacer
2026-09-05 02:14:24 +08:00
parent 91859cf97f
commit e84845047f
5 changed files with 326 additions and 65 deletions
@@ -1,35 +1,77 @@
<script lang="ts" setup> <script lang="ts" setup>
const displacementMapMarkup = ` import { createGlassNavbarDisplacementMap, NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP } from '@/utils/glassNavbarRefraction'
<svg viewBox="0 0 1200 80" xmlns="http://www.w3.org/2000/svg">
<defs> const DEFAULT_NAVBAR_GEOMETRY = {
<linearGradient id="x" x1="0%" y1="0%" x2="100%" y2="0%"> height: 64,
<stop offset="0%" stop-color="rgb(24 128 0)" /> radius: 16,
<stop offset="8%" stop-color="rgb(128 128 0)" /> width: 1200,
<stop offset="92%" stop-color="rgb(128 128 0)" /> }
<stop offset="100%" stop-color="rgb(232 128 0)" /> const MAP_RESIZE_SETTLE_MS = 180
</linearGradient> const displacementMapUrl = ref(NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP)
<linearGradient id="y" x1="0%" y1="0%" x2="0%" y2="100%"> const displacementMapSize = reactive({
<stop offset="0%" stop-color="rgb(0 0 28)" /> height: DEFAULT_NAVBAR_GEOMETRY.height,
<stop offset="18%" stop-color="rgb(0 0 128)" /> width: DEFAULT_NAVBAR_GEOMETRY.width,
<stop offset="82%" stop-color="rgb(0 0 128)" /> })
<stop offset="100%" stop-color="rgb(0 0 228)" />
</linearGradient> let observedNavbar: HTMLElement | null = null
</defs> let resizeObserver: ResizeObserver | null = null
<rect width="1200" height="80" rx="20" fill="black" /> let resizeTimer: ReturnType<typeof setTimeout> | null = null
<rect width="1200" height="80" rx="20" fill="url(#x)" />
<rect width="1200" height="80" rx="20" fill="url(#y)" style="mix-blend-mode: screen" /> function syncDisplacementMap() {
<rect if (!observedNavbar) return
x="10"
y="10" const bounds = observedNavbar.getBoundingClientRect()
width="1180" const styles = getComputedStyle(observedNavbar)
height="60" const floatingRadius = Number.parseFloat(styles.getPropertyValue('--shell-floating-navbar-radius'))
rx="14" const borderRadius = Number.parseFloat(styles.borderStartStartRadius)
fill="rgb(128 128 128)" const height = Math.max(1, Math.round(bounds.height))
style="filter: blur(8px)" const width = Math.max(1, Math.round(bounds.width))
/>
</svg> displacementMapSize.height = height
` displacementMapSize.width = width
const displacementMapUrl = `data:image/svg+xml,${encodeURIComponent(displacementMapMarkup)}` displacementMapUrl.value = createGlassNavbarDisplacementMap({
height,
radius: Number.isFinite(floatingRadius)
? floatingRadius
: Number.isFinite(borderRadius)
? borderRadius
: DEFAULT_NAVBAR_GEOMETRY.radius,
width,
})
}
// 几何动画期间沿用上一张位移图,尺寸稳定后再重建,避免逐帧生成并上传位移纹理。
function scheduleDisplacementMapSync() {
if (resizeTimer !== null) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
resizeTimer = null
syncDisplacementMap()
}, MAP_RESIZE_SETTLE_MS)
}
onMounted(() => {
observedNavbar = document.querySelector('.layout-wrapper[data-glass-navbar-refraction="chromium"] .layout-navbar')
if (!observedNavbar) return
syncDisplacementMap()
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', scheduleDisplacementMapSync, { passive: true })
return
}
resizeObserver = new ResizeObserver(scheduleDisplacementMapSync)
resizeObserver.observe(observedNavbar)
})
onBeforeUnmount(() => {
if (resizeTimer !== null) clearTimeout(resizeTimer)
resizeTimer = null
resizeObserver?.disconnect()
resizeObserver = null
window.removeEventListener('resize', scheduleDisplacementMapSync)
observedNavbar = null
})
</script> </script>
<template> <template>
@@ -46,8 +88,8 @@ const displacementMapUrl = `data:image/svg+xml,${encodeURIComponent(displacement
<feImage <feImage
x="0" x="0"
y="0" y="0"
width="100%" :width="displacementMapSize.width"
height="100%" :height="displacementMapSize.height"
preserveAspectRatio="none" preserveAspectRatio="none"
:href="displacementMapUrl" :href="displacementMapUrl"
result="map" result="map"
@@ -66,8 +108,8 @@ const displacementMapUrl = `data:image/svg+xml,${encodeURIComponent(displacement
<feImage <feImage
x="0" x="0"
y="0" y="0"
width="100%" :width="displacementMapSize.width"
height="100%" :height="displacementMapSize.height"
preserveAspectRatio="none" preserveAspectRatio="none"
:href="displacementMapUrl" :href="displacementMapUrl"
result="map" result="map"
@@ -395,6 +395,8 @@ describe('glass overlay material styles', () => {
it('scopes live navbar refraction to Chromium clear and tinted floating shells', () => { it('scopes live navbar refraction to Chromium clear and tinted floating shells', () => {
const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8')
const filterDefinitions = readFileSync(resolve(cwd(), 'src/components/theme/GlassNavbarRefractionDefs.vue'), 'utf8')
const refractionUtilities = readFileSync(resolve(cwd(), 'src/utils/glassNavbarRefraction.ts'), 'utf8')
expect(styles).toContain("data-glass-navbar-refraction='chromium'") expect(styles).toContain("data-glass-navbar-refraction='chromium'")
expect(styles).toContain("url('#glass-navbar-live-refraction-balanced')") expect(styles).toContain("url('#glass-navbar-live-refraction-balanced')")
@@ -402,6 +404,63 @@ describe('glass overlay material styles', () => {
expect(styles).toMatch( expect(styles).toMatch(
/:is\(\[data-glass-appearance='clear'\], \[data-glass-appearance='tinted'\]\)[\s\S]*?\.layout-navbar-floating-eligible\.layout-navbar-away-from-top[\s\S]*?\.layout-navbar/, /:is\(\[data-glass-appearance='clear'\], \[data-glass-appearance='tinted'\]\)[\s\S]*?\.layout-navbar-floating-eligible\.layout-navbar-away-from-top[\s\S]*?\.layout-navbar/,
) )
expect(styles).toContain('inline-size: auto !important')
expect(styles).toContain('inset-block-start: 0 !important')
expect(styles).toContain('inset-inline: 0 !important')
expect(styles).toContain('inset-block-start: var(--shell-floating-navbar-inset) !important')
expect(styles).toContain('inset-inline: var(--shell-floating-navbar-inset) !important')
expect(styles).toContain('transform: none !important')
expect(styles).toContain(
'inset-block-start var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing)',
)
expect(styles).toContain(
'inset-inline-end var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing)',
)
expect(styles).toContain(
'inset-inline-start var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing)',
)
const highQualityStart = styles.indexOf("html[data-theme='glass'][data-glass-quality='high']")
const liveRefractionStart = styles.lastIndexOf(
"html[data-theme='glass']:is([data-glass-appearance='clear'], [data-glass-appearance='tinted'])",
highQualityStart,
)
const liveRefractionRule = styles.slice(liveRefractionStart, highQualityStart)
expect(liveRefractionStart).toBeGreaterThanOrEqual(0)
expect(highQualityStart).toBeGreaterThan(liveRefractionStart)
expect(liveRefractionRule).toContain("backdrop-filter: url('#glass-navbar-live-refraction-balanced')")
expect(liveRefractionRule).toContain('border: 0 !important')
expect(liveRefractionRule).toContain('linear-gradient(145deg, rgba(255, 255, 255, 0.13), transparent 34%)')
expect(liveRefractionRule).not.toContain('inset 0 1px 0 rgba(255, 255, 255, 0.4)')
expect(styles).toContain('inline-size: min(100vw, variables.$layout-boxed-content-width)')
expect(styles).toContain('transform: translateX(-50%) !important')
expect(styles).toMatch(
/\[data-glass-appearance='tinted'\][\s\S]*?\.layout-navbar\s*\{[\s\S]*?--glass-material-accent-rgb/,
)
expect(liveRefractionRule).not.toContain('&::before')
expect(styles).not.toContain('&::after')
expect(styles).not.toContain("[data-glass-appearance='frosted'][data-glass-navbar-refraction='chromium']") expect(styles).not.toContain("[data-glass-appearance='frosted'][data-glass-navbar-refraction='chromium']")
expect(filterDefinitions).toContain('createGlassNavbarDisplacementMap')
expect(filterDefinitions).toContain('NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP')
expect(filterDefinitions).toContain('getBoundingClientRect()')
expect(filterDefinitions).toContain('new ResizeObserver(scheduleDisplacementMapSync)')
expect(filterDefinitions).toContain('in2="map"')
expect(filterDefinitions).toContain(':width="displacementMapSize.width"')
expect(filterDefinitions).toContain(':height="displacementMapSize.height"')
expect(filterDefinitions).not.toContain('width="100%"')
expect(filterDefinitions).not.toContain('height="100%"')
expect(filterDefinitions).toContain('scale="-22"')
expect(filterDefinitions).toContain('scale="-34"')
expect(filterDefinitions).toContain('x="-8%"')
expect(filterDefinitions).toContain('width="116%"')
expect(filterDefinitions).toContain('x="-12%"')
expect(filterDefinitions).toContain('width="124%"')
expect(filterDefinitions).not.toContain('horizontal-continuity')
expect(filterDefinitions).not.toContain('<feComposite')
expect(filterDefinitions).not.toContain('<feGaussianBlur')
expect(refractionUtilities).toContain('createGlassNavbarDisplacementField')
expect(refractionUtilities).toContain('OUTER_NEUTRAL_GUARD_PX')
expect(refractionUtilities).toContain('Math.sin(Math.PI * normalizedDistance)')
expect(refractionUtilities).toContain("canvas.toDataURL('image/png')")
}) })
}) })
+42 -29
View File
@@ -1652,57 +1652,70 @@ html[data-theme='glass'] body[data-theme='glass'] .native-login-field:focus-with
} }
} }
// Chromium 直接位移浏览器已合成的下层内容;其他引擎保留稳定的基础顶栏材质 // 实时折射表面在贴顶与浮动状态使用同一布局坐标系,只插值真实 inset,避免切换 transform 采样空间
html[data-theme='glass']:is([data-glass-appearance='clear'], [data-glass-appearance='tinted']) html[data-theme='glass']:is([data-glass-appearance='clear'], [data-glass-appearance='tinted'])
body[data-theme='glass'] body[data-theme='glass']
.layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top .layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible
.layout-navbar { .layout-navbar {
-webkit-backdrop-filter: none !important; inline-size: auto !important;
backdrop-filter: none !important; inset-block-start: 0 !important;
background-color: transparent !important; inset-inline: 0 !important;
background-image: none !important; transform: none !important;
transition:
&::before { background-color var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
position: absolute; border-color var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
z-index: 0; border-radius var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
border: 1px solid rgba(255, 255, 255, 0.24); box-shadow var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
border-radius: inherit; inset-block-start var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
-webkit-backdrop-filter: blur(1.5px) saturate(118%); inset-inline-end var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing),
backdrop-filter: url('#glass-navbar-live-refraction-balanced') blur(1.5px) saturate(118%); inset-inline-start var(--shell-floating-navbar-motion-duration) var(--shell-floating-navbar-motion-easing) !important;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.13), transparent 34%),
linear-gradient(rgba(7, 14, 25, 0.06), rgba(7, 14, 25, 0.13));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.4),
inset 0 -1px 0 rgba(4, 10, 20, 0.22),
0 12px 32px rgba(3, 7, 18, 0.16);
content: '';
inset: 0;
pointer-events: none;
}
.navbar-content-container { .navbar-content-container {
position: relative; position: relative;
z-index: 1; z-index: 1;
// 外壳内收时,导航内容仍按视口中心对齐,避免控件位置跟随采样面的宽度变化。
inline-size: min(100vw, variables.$layout-boxed-content-width);
inset-inline-start: 50%;
transform: translateX(-50%) !important;
} }
} }
// 位移、材质和轮廓由同一个真实尺寸的表面完成;顶部不叠加硬高光线。
html[data-theme='glass']:is([data-glass-appearance='clear'], [data-glass-appearance='tinted'])
body[data-theme='glass']
.layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top
.layout-navbar {
border: 0 !important;
-webkit-backdrop-filter: blur(1.5px) saturate(118%) !important;
backdrop-filter: url('#glass-navbar-live-refraction-balanced') blur(1.5px) saturate(118%) !important;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.13), transparent 34%),
linear-gradient(rgba(7, 14, 25, 0.06), rgba(7, 14, 25, 0.13)) !important;
box-shadow:
inset 1px 0 0 rgba(255, 255, 255, 0.2),
inset -1px 0 0 rgba(255, 255, 255, 0.16),
inset 0 -1px 0 rgba(4, 10, 20, 0.22),
0 12px 32px rgba(3, 7, 18, 0.16) !important;
inset-block-start: var(--shell-floating-navbar-inset) !important;
inset-inline: var(--shell-floating-navbar-inset) !important;
}
html[data-theme='glass'][data-glass-quality='high']:is( html[data-theme='glass'][data-glass-quality='high']:is(
[data-glass-appearance='clear'], [data-glass-appearance='clear'],
[data-glass-appearance='tinted'] [data-glass-appearance='tinted']
) )
body[data-theme='glass'] body[data-theme='glass']
.layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top .layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top
.layout-navbar::before { .layout-navbar {
backdrop-filter: url('#glass-navbar-live-refraction-high') blur(1px) saturate(122%); backdrop-filter: url('#glass-navbar-live-refraction-high') blur(1px) saturate(122%) !important;
} }
html[data-theme='glass'][data-glass-appearance='tinted'] html[data-theme='glass'][data-glass-appearance='tinted']
body[data-theme='glass'] body[data-theme='glass']
.layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top .layout-wrapper[data-glass-navbar-refraction='chromium'].layout-navbar-floating-eligible.layout-navbar-away-from-top
.layout-navbar::before { .layout-navbar {
background: background:
linear-gradient(145deg, rgba(255, 255, 255, 0.14), transparent 36%), linear-gradient(145deg, rgba(255, 255, 255, 0.14), transparent 36%),
linear-gradient(rgba(var(--glass-material-accent-rgb), 0.09), rgba(var(--glass-material-accent-rgb), 0.035)), linear-gradient(rgba(var(--glass-material-accent-rgb), 0.09), rgba(var(--glass-material-accent-rgb), 0.035)),
linear-gradient(rgba(7, 14, 25, 0.08), rgba(7, 14, 25, 0.15)); linear-gradient(rgba(7, 14, 25, 0.08), rgba(7, 14, 25, 0.15)) !important;
} }
@@ -1,5 +1,34 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { supportsGlassNavbarLiveRefraction } from '@/utils/glassNavbarRefraction' import { createGlassNavbarDisplacementField, supportsGlassNavbarLiveRefraction } from '@/utils/glassNavbarRefraction'
describe('createGlassNavbarDisplacementField', () => {
function pixelAt(field: ReturnType<typeof createGlassNavbarDisplacementField>, x: number, y: number) {
const offset = (y * field.width + x) * 4
return [...field.pixels.slice(offset, offset + 4)]
}
it('keeps both contour boundary and interior neutral while bending only the narrow rim', () => {
const field = createGlassNavbarDisplacementField({ height: 41, radius: 12, width: 101 })
expect(field.width).toBe(101)
expect(field.height).toBe(41)
expect(pixelAt(field, 50, 0)).toEqual([128, 128, 128, 255])
expect(pixelAt(field, 50, 20)).toEqual([128, 128, 128, 255])
expect(pixelAt(field, 50, 5)[2]).toBeLessThan(64)
expect(pixelAt(field, 5, 20)[0]).toBeLessThan(64)
expect(pixelAt(field, 95, 20)[0]).toBeGreaterThan(192)
expect(pixelAt(field, 50, 35)[2]).toBeGreaterThan(192)
})
it('clamps invalidly small geometry to a renderable pixel surface', () => {
const field = createGlassNavbarDisplacementField({ height: 0, radius: 20, width: -10 })
expect(field.width).toBe(1)
expect(field.height).toBe(1)
expect([...field.pixels]).toEqual([128, 128, 128, 255])
})
})
describe('supportsGlassNavbarLiveRefraction', () => { describe('supportsGlassNavbarLiveRefraction', () => {
it('enables the verified Chromium engine path for Chrome and Edge', () => { it('enables the verified Chromium engine path for Chrome and Edge', () => {
+118
View File
@@ -9,6 +9,124 @@ export interface GlassNavbarRefractionBrowserIdentity {
userAgent: string userAgent: string
} }
export interface GlassNavbarDisplacementGeometry {
/** 折射表面的实际 CSS 像素高度。 */
height: number
/** 最终可见外轮廓的圆角半径。 */
radius: number
/** 折射表面的实际 CSS 像素宽度。 */
width: number
}
export interface GlassNavbarDisplacementField {
/** 位移图的 CSS 像素高度。 */
height: number
/** 按 RGBA 顺序存储的非预乘像素通道。 */
pixels: Uint8ClampedArray
/** 位移图的 CSS 像素宽度。 */
width: number
}
export const NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP =
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"%3E%3Cpath fill="%23808080" d="M0 0h1v1H0z"/%3E%3C/svg%3E'
const DISPLACEMENT_NEUTRAL_CHANNEL = 128
const DISPLACEMENT_CHANNEL_AMPLITUDE = 127
const OUTER_NEUTRAL_GUARD_PX = 2
const REFRACTION_BAND_PX = 12
const REFRACTION_PROFILE_POWER = 2
function normalizePixelSize(value: number) {
return Number.isFinite(value) ? Math.max(1, Math.round(value)) : 1
}
function roundedRectangleSignedDistance(x: number, y: number, width: number, height: number, radius: number) {
const offsetX = Math.abs(x - width / 2) - (width / 2 - radius)
const offsetY = Math.abs(y - height / 2) - (height / 2 - radius)
const outsideX = Math.max(offsetX, 0)
const outsideY = Math.max(offsetY, 0)
return Math.hypot(outsideX, outsideY) + Math.min(Math.max(offsetX, offsetY), 0) - radius
}
function clampChannel(value: number) {
return Math.max(0, Math.min(255, Math.round(value)))
}
/**
* 生成圆角表面的法线位移场。
* 外轮廓和内区都保持中性采样,避免折射在裁剪边界或主体内容区形成整带错位。
*/
export function createGlassNavbarDisplacementField({
height,
radius,
width,
}: GlassNavbarDisplacementGeometry): GlassNavbarDisplacementField {
const pixelWidth = normalizePixelSize(width)
const pixelHeight = normalizePixelSize(height)
const maxRadius = Math.min(pixelWidth, pixelHeight) / 2
const pixelRadius = Number.isFinite(radius) ? Math.max(0, Math.min(maxRadius, radius)) : 0
const bandWidth = Math.min(REFRACTION_BAND_PX, Math.min(pixelWidth, pixelHeight) / 2)
const outerGuard = Math.min(OUTER_NEUTRAL_GUARD_PX, bandWidth / 4)
const pixels = new Uint8ClampedArray(pixelWidth * pixelHeight * 4)
for (let offset = 0; offset < pixels.length; offset += 4) {
pixels[offset] = DISPLACEMENT_NEUTRAL_CHANNEL
pixels[offset + 1] = DISPLACEMENT_NEUTRAL_CHANNEL
pixels[offset + 2] = DISPLACEMENT_NEUTRAL_CHANNEL
pixels[offset + 3] = 255
}
for (let y = 0; y < pixelHeight; y += 1) {
for (let x = 0; x < pixelWidth; x += 1) {
const sampleX = x + 0.5
const sampleY = y + 0.5
const signedDistance = roundedRectangleSignedDistance(sampleX, sampleY, pixelWidth, pixelHeight, pixelRadius)
const distanceInside = -signedDistance
if (signedDistance > 0 || distanceInside <= outerGuard || distanceInside >= bandWidth) continue
const normalizedDistance = (distanceInside - outerGuard) / (bandWidth - outerGuard)
const profile = Math.sin(Math.PI * normalizedDistance) ** REFRACTION_PROFILE_POWER
const gradientX =
roundedRectangleSignedDistance(sampleX + 0.5, sampleY, pixelWidth, pixelHeight, pixelRadius) -
roundedRectangleSignedDistance(sampleX - 0.5, sampleY, pixelWidth, pixelHeight, pixelRadius)
const gradientY =
roundedRectangleSignedDistance(sampleX, sampleY + 0.5, pixelWidth, pixelHeight, pixelRadius) -
roundedRectangleSignedDistance(sampleX, sampleY - 0.5, pixelWidth, pixelHeight, pixelRadius)
const gradientLength = Math.hypot(gradientX, gradientY) || 1
const offset = (y * pixelWidth + x) * 4
pixels[offset] = clampChannel(
DISPLACEMENT_NEUTRAL_CHANNEL + (DISPLACEMENT_CHANNEL_AMPLITUDE * gradientX * profile) / gradientLength,
)
pixels[offset + 2] = clampChannel(
DISPLACEMENT_NEUTRAL_CHANNEL + (DISPLACEMENT_CHANNEL_AMPLITUDE * gradientY * profile) / gradientLength,
)
}
}
return { height: pixelHeight, pixels, width: pixelWidth }
}
/** 把位移场栅格化为浏览器可直接加载的无损 PNG。 */
export function createGlassNavbarDisplacementMap(geometry: GlassNavbarDisplacementGeometry) {
const field = createGlassNavbarDisplacementField(geometry)
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) return NEUTRAL_GLASS_NAVBAR_DISPLACEMENT_MAP
canvas.width = field.width
canvas.height = field.height
const imageData = context.createImageData(field.width, field.height)
imageData.data.set(field.pixels)
context.putImageData(imageData, 0, 0)
return canvas.toDataURL('image/png')
}
/** 仅在已验证 SVG backdrop 位移的 Chromium 引擎启用实时顶栏折射。 */ /** 仅在已验证 SVG backdrop 位移的 Chromium 引擎启用实时顶栏折射。 */
export function supportsGlassNavbarLiveRefraction(browserIdentity: GlassNavbarRefractionBrowserIdentity = navigator) { export function supportsGlassNavbarLiveRefraction(browserIdentity: GlassNavbarRefractionBrowserIdentity = navigator) {
const brands = browserIdentity.userAgentData?.brands const brands = browserIdentity.userAgentData?.brands