fix(glass): align navigation spacing radii and plugin branding

This commit is contained in:
InfinityPacer
2026-09-07 11:19:07 +08:00
parent f466912ccd
commit 98bd33e3fc
7 changed files with 96 additions and 18 deletions
@@ -53,6 +53,7 @@ const GEOMETRY_ATTRIBUTE_FILTER = [
'data-shell-mode',
'data-shell-navbar-attachment',
'data-theme',
'data-theme-radius',
'style',
]
@@ -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,
@@ -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()
@@ -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)
@@ -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')
+31 -17
View File
@@ -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:
@@ -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])