From 98bd33e3fc826b63eeff76d0027062be4b6d212d Mon Sep 17 00:00:00 2001 From: InfinityPacer Date: Sun, 6 Sep 2026 20:00:25 +0800 Subject: [PATCH] fix(glass): align navigation spacing radii and plugin branding --- .../theme/GlassFixedShellBackplate.vue | 1 + .../theme/GlassNavbarRefractionDefs.vue | 9 +++- .../GlassFixedShellBackplate.spec.ts | 16 +++++++ .../GlassNavbarRefractionDefs.spec.ts | 16 +++++++ .../__tests__/glassOverlayMaterial.spec.ts | 20 ++++++++ src/styles/themes/_glass-v3.scss | 48 ++++++++++++------- .../__tests__/glassNavbarRefraction.spec.ts | 4 ++ 7 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/components/theme/GlassFixedShellBackplate.vue b/src/components/theme/GlassFixedShellBackplate.vue index 488fe53b..04146a75 100644 --- a/src/components/theme/GlassFixedShellBackplate.vue +++ b/src/components/theme/GlassFixedShellBackplate.vue @@ -53,6 +53,7 @@ const GEOMETRY_ATTRIBUTE_FILTER = [ 'data-shell-mode', 'data-shell-navbar-attachment', 'data-theme', + 'data-theme-radius', 'style', ] diff --git a/src/components/theme/GlassNavbarRefractionDefs.vue b/src/components/theme/GlassNavbarRefractionDefs.vue index 2306984e..d8412e95 100644 --- a/src/components/theme/GlassNavbarRefractionDefs.vue +++ b/src/components/theme/GlassNavbarRefractionDefs.vue @@ -350,7 +350,14 @@ onMounted(() => { stateObserver.observe(document.documentElement, { attributes: true, attributeOldValue: true, - attributeFilter: ['class', 'style', 'data-theme', 'data-glass-appearance', 'data-glass-quality'], + attributeFilter: [ + 'class', + 'style', + 'data-theme', + 'data-theme-radius', + 'data-glass-appearance', + 'data-glass-quality', + ], }) stateObserver.observe(observedShell, { attributes: true, diff --git a/src/components/theme/__tests__/GlassFixedShellBackplate.spec.ts b/src/components/theme/__tests__/GlassFixedShellBackplate.spec.ts index baa0e606..ddb659ed 100644 --- a/src/components/theme/__tests__/GlassFixedShellBackplate.spec.ts +++ b/src/components/theme/__tests__/GlassFixedShellBackplate.spec.ts @@ -161,6 +161,7 @@ describe('GlassFixedShellBackplate', () => { for (const wrapper of mountedWrappers.splice(0)) wrapper.unmount() document.querySelectorAll('.layout-wrapper').forEach(element => element.remove()) document.documentElement.removeAttribute('data-theme') + document.documentElement.removeAttribute('data-theme-radius') vi.restoreAllMocks() vi.unstubAllGlobals() }) @@ -265,6 +266,21 @@ describe('GlassFixedShellBackplate', () => { expect(wrapper.findAll('clipPath rect')).toHaveLength(0) }) + it('updates the shared clip when theme radius changes without resizing navigation', async () => { + const { wrapper, sidebar } = mountConnectedShell() + await settleGeometry() + const style = window.getComputedStyle(sidebar) + vi.mocked(window.getComputedStyle).mockReturnValue({ ...style, borderTopLeftRadius: '24px' }) + + document.documentElement.dataset.themeRadius = 'extra' + await settleGeometry() + + for (const rect of wrapper.findAll('clipPath rect')) { + expect(Number(rect.attributes('rx'))).toBeCloseTo(24 / backplateRect.width, 8) + expect(Number(rect.attributes('ry'))).toBeCloseTo(24 / backplateRect.height, 8) + } + }) + it('refreshes dimensions and disconnects the resize observer on unmount', async () => { const { wrapper } = mountConnectedShell() await settleGeometry() diff --git a/src/components/theme/__tests__/GlassNavbarRefractionDefs.spec.ts b/src/components/theme/__tests__/GlassNavbarRefractionDefs.spec.ts index 91f3117b..b4d41024 100644 --- a/src/components/theme/__tests__/GlassNavbarRefractionDefs.spec.ts +++ b/src/components/theme/__tests__/GlassNavbarRefractionDefs.spec.ts @@ -107,6 +107,7 @@ describe('GlassNavbarRefractionDefs', () => { wrapper?.unmount() wrapper = undefined shell.remove() + delete document.documentElement.dataset.themeRadius vi.restoreAllMocks() vi.unstubAllGlobals() vi.useRealTimers() @@ -420,6 +421,21 @@ describe('GlassNavbarRefractionDefs', () => { ) }) + it('regenerates the optical outline when the theme radius attribute changes', async () => { + wrapper = mount(GlassNavbarRefractionDefs) + await settle() + + radius = 24 + document.documentElement.dataset.themeRadius = 'extra' + await flushPromises() + await settle() + + expect(createGlassNavbarDisplacementMap).toHaveBeenLastCalledWith( + expect.objectContaining({ width: 1423, height: 64, radius: 24 }), + ) + expect(shell.dataset.glassNavbarRefractionReady).toBe('true') + }) + it('does not retry a failed geometry in a feedback loop', async () => { wrapper = mount(GlassNavbarRefractionDefs) await vi.advanceTimersByTimeAsync(65) diff --git a/src/styles/__tests__/glassOverlayMaterial.spec.ts b/src/styles/__tests__/glassOverlayMaterial.spec.ts index 8cf92d58..fd64c09a 100644 --- a/src/styles/__tests__/glassOverlayMaterial.spec.ts +++ b/src/styles/__tests__/glassOverlayMaterial.spec.ts @@ -4,6 +4,26 @@ import { cwd } from 'node:process' import { describe, expect, it } from 'vitest' describe('glass overlay material styles', () => { + it('reserves space below detached desktop navigation and follows the compact theme radius', () => { + const styles = readFileSync(resolve(cwd(), 'src/styles/themes/_glass-v3.scss'), 'utf8') + + expect(styles).toContain('--glass-v3-navigation-radius: var(--app-field-radius, 16px)') + expect(styles).toContain('--glass-v3-navigation-content-gap: 16px') + expect(styles).toContain('border-radius: var(--glass-v3-navigation-radius)') + expect(styles).toContain('--shell-floating-navbar-radius: var(--glass-v3-navigation-radius)') + expect(styles).toMatch(/var\(--layout-navbar-block-size\)\s*-\s*var\(--navbar-tab-height, 0px\)/u) + expect(styles).toContain('var(--glass-v3-navigation-content-gap)') + expect(styles).toContain('.layout-window-controls-overlay-shell') + expect(styles).not.toContain('border-radius: 16px') + }) + + it('keeps plugin logo tinting on the material formulas and displays complete logos', () => { + const styles = readFileSync(resolve(cwd(), 'src/styles/themes/_glass-v3.scss'), 'utf8') + + expect(styles).not.toMatch(/--plugin-card-banner-(?:tint|scrim)\s*:/u) + expect(styles).toMatch(/\.plugin-card__plugin-icon \.v-img__img\s*\{\s*object-fit:\s*contain;/u) + }) + it('keeps overlays translucent enough for CSS backdrop compositing in every material', () => { const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') diff --git a/src/styles/themes/_glass-v3.scss b/src/styles/themes/_glass-v3.scss index 7a512e32..9407d23d 100644 --- a/src/styles/themes/_glass-v3.scss +++ b/src/styles/themes/_glass-v3.scss @@ -9,6 +9,10 @@ --glass-v3-sheen: clamp(0.04, calc(0.035 + var(--glass-reflection, 0.38) * 0.18), 0.2); --glass-v3-shadow: 0 12px 30px rgba(0, 0, 0, 0.14); --glass-v3-card-tint: 0; + // 导航使用随主题递增的紧凑半径档,比大块内容表面略收敛。 + --glass-v3-navigation-radius: var(--app-field-radius, 16px); + --glass-v3-navigation-inset: 8px; + --glass-v3-navigation-content-gap: 16px; --glass-v3-navigation-blur: clamp( 0.65px, calc(0.9px + var(--glass-surface-density, 0.62) * 0.6px - var(--glass-background-visibility, 0.58) * 0.25px), @@ -122,13 +126,7 @@ } .layout-page-content .plugin-card__banner { - --plugin-card-banner-scrim: linear-gradient(rgba(23, 27, 32, 0.035), rgba(23, 27, 32, 0.1)); - --plugin-card-banner-tint: linear-gradient( - 125deg, - rgba(var(--plugin-card-effective-accent-rgb), 0.16), - rgba(var(--plugin-card-effective-accent-rgb), 0.045) 70% - ); - + // 品牌染色与吸收继续由各材质公式决定,轮廓只补充轻量的透光边缘。 border-block-end: 1px solid rgba(255, 255, 255, 0.12); } @@ -137,12 +135,18 @@ } .layout-page-content .plugin-card__plugin-icon { + background-color: rgba(255, 255, 255, 0.08); border-radius: var(--app-control-radius, 14px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 5px 12px rgba(0, 0, 0, 0.12); } + .layout-page-content .plugin-card__plugin-icon .v-img__img { + object-fit: contain; + padding: 3px; + } + .layout-page-content .media-card > .v-card-text { // 电影画面仍可检查,文字所在的下半区才承担阅读吸收。 background: linear-gradient( @@ -169,26 +173,34 @@ } .layout-vertical-nav { - border-radius: 16px; + border-radius: var(--glass-v3-navigation-radius); box-shadow: var(--glass-v3-shadow) !important; - block-size: calc(100% - 16px); - inline-size: calc(var(--glass-v3-nav-reserved-width) - 8px) !important; - inset-block-start: 8px; - inset-inline-start: 8px; + block-size: calc(100% - 2 * var(--glass-v3-navigation-inset)); + inline-size: calc(var(--glass-v3-nav-reserved-width) - var(--glass-v3-navigation-inset)) !important; + inset-block-start: var(--glass-v3-navigation-inset); + inset-inline-start: var(--glass-v3-navigation-inset); overflow: clip; } &.layout-vertical-nav-collapsed .layout-vertical-nav.hovered { - inline-size: calc(#{variables.$layout-vertical-nav-width} - 8px) !important; + inline-size: calc(#{variables.$layout-vertical-nav-width} - var(--glass-v3-navigation-inset)) !important; } .layout-navbar { - border-radius: 16px !important; - inline-size: calc(100% - var(--glass-v3-nav-reserved-width) - 16px) !important; - inset-block-start: 8px !important; - inset-inline-start: calc(var(--glass-v3-nav-reserved-width) + 8px) !important; + border-radius: var(--glass-v3-navigation-radius) !important; + inline-size: calc(100% - var(--glass-v3-nav-reserved-width) - 2 * var(--glass-v3-navigation-inset)) !important; + inset-block-start: var(--glass-v3-navigation-inset) !important; + inset-inline-start: calc(var(--glass-v3-nav-reserved-width) + var(--glass-v3-navigation-inset)) !important; overflow: clip; } + + // 标签栏由内容层另行预留;这里只补主导航内缩和下方间距,避免标签高度重复占位。 + .layout-page-content { + padding-block-start: calc( + var(--layout-navbar-block-size) - var(--navbar-tab-height, 0px) + var(--glass-v3-navigation-inset) + + var(--glass-v3-navigation-content-gap) + ); + } } @media (hover: hover) { @@ -272,6 +284,8 @@ ) body[data-theme='glass'] .layout-wrapper.layout-navbar-floating-eligible { + --shell-floating-navbar-radius: var(--glass-v3-navigation-radius); + .layout-navbar, .navbar-content-container { transition-property: diff --git a/src/utils/__tests__/glassNavbarRefraction.spec.ts b/src/utils/__tests__/glassNavbarRefraction.spec.ts index 1b8b870d..df8a5d62 100644 --- a/src/utils/__tests__/glassNavbarRefraction.spec.ts +++ b/src/utils/__tests__/glassNavbarRefraction.spec.ts @@ -79,6 +79,10 @@ describe('createGlassNavbarDisplacementField', () => { { width: 68, height: 862, radius: 0 }, { width: 252, height: 846, radius: 16 }, { width: 60, height: 846, radius: 16 }, + { width: 252, height: 846, radius: 8 }, + { width: 252, height: 846, radius: 24 }, + { width: 60, height: 846, radius: 8 }, + { width: 60, height: 846, radius: 24 }, ])('keeps two-dimensional sampling forward and inside the image for $width x $height r$radius', geometry => { for (const deformation of [0, 48, 100]) for (const translation of [0, 48, 100])