From 288d46c32ac0de7dc94bd5309a7672852a87fa11 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:07:17 +0800 Subject: [PATCH] fix(glass): normalize tinted material tones (#659) --- src/App.vue | 16 +++++- src/components/theme/GlassOpticalLayer.vue | 2 +- .../theme/__tests__/GlassOpticalLayer.spec.ts | 6 +- .../__tests__/useThemeCustomizer.spec.ts | 23 ++++++++ src/composables/useThemeCustomizer.ts | 5 ++ .../__tests__/glassOverlayMaterial.spec.ts | 50 +++++++++++++++++ src/styles/__tests__/pluginCardAccent.spec.ts | 4 ++ src/styles/themes/glass.scss | 14 ++--- src/utils/__tests__/glassColor.spec.ts | 55 ++++++++++++++++++- src/utils/glassColor.ts | 26 +++++++-- 10 files changed, 183 insertions(+), 18 deletions(-) diff --git a/src/App.vue b/src/App.vue index 2b9d2489..bae25403 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,7 +12,11 @@ import { globalLoadingStateManager } from '@/utils/loadingStateManager' import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager' import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue' import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue' -import { applyStoredThemeCustomizerAppearance, useEffectiveGlassSettings } from '@/composables/useThemeCustomizer' +import { + applyStoredThemeCustomizerAppearance, + themeCustomizerPrimaryColors, + useEffectiveGlassSettings, +} from '@/composables/useThemeCustomizer' import { applyStoredTransparencySettings, TRANSPARENCY_SETTINGS_CHANGED_EVENT, @@ -24,6 +28,7 @@ import { usePWA } from '@/composables/usePWA' import { themeManager } from '@/utils/themeManager' import { applyDocumentThemeChrome, resolveThemeName } from '@/utils/themePalette' import { getDisplayImageUrl } from '@/utils/imageUtils' +import { normalizeThemeMaterialAccent } from '@/utils/glassColor' import { configureApexChartsTheme } from '@/utils/apexCharts' import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composables/useOfflineStatus' import { useAppActivityLifecycle } from '@/composables/useAppActivityLifecycle' @@ -118,6 +123,11 @@ function recordGlassLaunchTiming(stage: string, detail?: string) { // 生效主题 const vuetifyTheme = useTheme() const { global: globalTheme } = vuetifyTheme +const glassMaterialTintColor = computed( + () => + normalizeThemeMaterialAccent(globalTheme.current.value.colors.primary)?.hex ?? + normalizeThemeMaterialAccent(themeCustomizerPrimaryColors[0].value)!.hex, +) let themeValue = localStorage.getItem('theme') || 'auto' let resumeThemeSyncTimer: number | null = null globalTheme.name.value = resolveInitialThemeName(themeValue) @@ -1191,7 +1201,7 @@ onUnmounted(() => { :transmission-strength="opticalTransmissionStrength" :translation-strength="opticalTranslationStrength" :route-key="route.fullPath" - :tint-color="globalTheme.current.value.colors.primary" + :tint-color="glassMaterialTintColor" :transition-duration="BACKGROUND_CROSSFADE_DURATION_MS" :transition-started-at="backgroundCrossfadeStartedAt" :wallpaper-url="activeOpticalBackgroundImage" @@ -1314,7 +1324,7 @@ html[data-glass-appearance='tinted'] .background-container.is-glass-theme .backg html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.previous::after { background: radial-gradient(circle at 50% 18%, transparent 22%, rgba(6, 10, 19, 14%) 100%), - linear-gradient(rgba(6, 10, 19, 10%) 0%, rgba(6, 10, 19, 32%) 100%), rgba(var(--v-theme-primary), 3%); + linear-gradient(rgba(6, 10, 19, 10%) 0%, rgba(6, 10, 19, 32%) 100%), rgba(var(--glass-material-accent-rgb), 3%); } html[data-glass-appearance='frosted'] .background-container.is-glass-theme .background-image.active, diff --git a/src/components/theme/GlassOpticalLayer.vue b/src/components/theme/GlassOpticalLayer.vue index 8a5b1737..90ba6025 100644 --- a/src/components/theme/GlassOpticalLayer.vue +++ b/src/components/theme/GlassOpticalLayer.vue @@ -37,7 +37,7 @@ const props = defineProps<{ translationStrength: number /** 路由变化标识,用于在页面内容稳定后重新发现高价值表面。 */ routeKey: string - /** 当前主题主色,用于同步色调材质的光学高光。 */ + /** 由用户主色派生的大面积玻璃材料色,用于同步色调材质的光学高光。 */ tintColor: string /** 外层壁纸交叉淡化的时长,shader 使用同一时钟混合双纹理。 */ transitionDuration: number diff --git a/src/components/theme/__tests__/GlassOpticalLayer.spec.ts b/src/components/theme/__tests__/GlassOpticalLayer.spec.ts index 70a4ca17..a73330f9 100644 --- a/src/components/theme/__tests__/GlassOpticalLayer.spec.ts +++ b/src/components/theme/__tests__/GlassOpticalLayer.spec.ts @@ -126,7 +126,7 @@ afterEach(() => { }) describe('GlassOpticalLayer', () => { - it('uses exactly two visible presentation contexts with one interaction source', () => { + it('uses exactly two visible presentation contexts with one interaction source', async () => { rendererCalls.length = 0 rendererResults.length = 0 setRendererState.mockClear() @@ -159,6 +159,7 @@ describe('GlassOpticalLayer', () => { expect(rendererCalls.every(options => options.wallpaperSourceCache === wallpaperSourceCache)).toBe(true) expect(rendererCalls.every(options => options.syncDocumentState === false)).toBe(true) expect(rendererCalls.every(options => (options.dynamicsActive as { value: boolean }).value)).toBe(true) + expect(rendererCalls.map(options => (options.tintColor as () => string)())).toEqual(['#8D51F9', '#8D51F9']) expect(rendererCalls[0].pageMotion).toBeUndefined() expect(rendererCalls[1].pageMotion).toEqual( expect.objectContaining({ @@ -167,6 +168,9 @@ describe('GlassOpticalLayer', () => { }), ) + await wrapper.setProps({ tintColor: '#00A6B8' }) + expect(rendererCalls.map(options => (options.tintColor as () => string)())).toEqual(['#00A6B8', '#00A6B8']) + wrapper.unmount() expect(setRendererState).toHaveBeenLastCalledWith(expect.any(Object), 'fallback') }) diff --git a/src/composables/__tests__/useThemeCustomizer.spec.ts b/src/composables/__tests__/useThemeCustomizer.spec.ts index dc1df586..abbf5865 100644 --- a/src/composables/__tests__/useThemeCustomizer.spec.ts +++ b/src/composables/__tests__/useThemeCustomizer.spec.ts @@ -12,6 +12,7 @@ import { useEffectiveGlassSettings, } from '@/composables/useThemeCustomizer' import vuetify from '@/plugins/vuetify' +import { normalizeThemeMaterialAccent } from '@/utils/glassColor' import { mount } from '@vue/test-utils' import { defineComponent, h } from 'vue' import { beforeEach, describe, expect, it } from 'vitest' @@ -212,6 +213,28 @@ describe('useThemeCustomizer glass settings', () => { expect(Number(document.body.style.getPropertyValue('--glass-tint-density'))).toBeCloseTo(0.65) expect(document.documentElement.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') expect(document.body.style.getPropertyValue('--glass-overlay-clarity-blur')).toBe('6.7px') + expect(document.documentElement.style.getPropertyValue('--glass-material-accent-rgb')).toBe( + normalizeThemeMaterialAccent(settings.primaryColor)?.rgb, + ) + expect(document.body.style.getPropertyValue('--glass-material-accent-rgb')).toBe( + normalizeThemeMaterialAccent(settings.primaryColor)?.rgb, + ) + }) + + it('keeps the user primary color while publishing its material tone', async () => { + const { customizer, wrapper } = mountThemeCustomizer() + + await customizer.setPrimaryColor('#00BCD4') + + expect(customizer.settings.value.primaryColor).toBe('#00BCD4') + expect(vuetify.theme.current.value.colors.primary).toBe('#00BCD4') + expect(document.documentElement.style.getPropertyValue('--glass-material-accent-rgb')).toBe( + normalizeThemeMaterialAccent('#00BCD4')?.rgb, + ) + expect(document.body.style.getPropertyValue('--glass-material-accent-rgb')).toBe( + normalizeThemeMaterialAccent('#00BCD4')?.rgb, + ) + wrapper.unmount() }) it('keeps overlay clarity synchronized across preview, cancel, and commit', () => { diff --git a/src/composables/useThemeCustomizer.ts b/src/composables/useThemeCustomizer.ts index 0ffb2b6c..7a36b26c 100644 --- a/src/composables/useThemeCustomizer.ts +++ b/src/composables/useThemeCustomizer.ts @@ -19,6 +19,7 @@ import { type GlassOpticalPreset, type GlassOpticalPresetOverrides, } from '@/utils/glassOptics' +import { normalizeThemeMaterialAccent } from '@/utils/glassColor' import { themeManager } from '@/utils/themeManager' import { syncThemeFavicon } from '@/utils/themePalette' @@ -471,6 +472,7 @@ export function applyThemeCustomizerRootSettings( | 'glassTransmissionStrength' | 'glassTransparencyStrength' | 'layout' + | 'primaryColor' | 'radius' | 'semiDarkMenu' | 'shadow' @@ -482,6 +484,8 @@ export function applyThemeCustomizerRootSettings( const materialResponse = getGlassMaterialResponse(settings.glassAppearance, settings.glassTransparencyStrength) const frostBlur = getGlassCssFrostBlur(settings.glassTransparencyStrength) const overlayClarityBlur = getGlassOverlayClarityBlur(settings.glassTransparencyStrength) + const materialAccent = + normalizeThemeMaterialAccent(settings.primaryColor) ?? normalizeThemeMaterialAccent(defaultPrimaryColor)! const applyGlassResponse = (element: HTMLElement) => { element.style.setProperty('--glass-background-visibility', String(materialResponse.backgroundVisibility)) element.style.setProperty('--glass-frost-blur-scale', String(materialResponse.frostBlurScale)) @@ -491,6 +495,7 @@ export function applyThemeCustomizerRootSettings( element.style.setProperty('--glass-blur-surface', `${frostBlur.surface}px`) element.style.setProperty('--glass-blur-raised', `${frostBlur.raised}px`) element.style.setProperty('--glass-overlay-clarity-blur', `${overlayClarityBlur}px`) + element.style.setProperty('--glass-material-accent-rgb', materialAccent.rgb) } document.documentElement.setAttribute('data-glass-appearance', settings.glassAppearance) diff --git a/src/styles/__tests__/glassOverlayMaterial.spec.ts b/src/styles/__tests__/glassOverlayMaterial.spec.ts index 9d799856..a9d05cea 100644 --- a/src/styles/__tests__/glassOverlayMaterial.spec.ts +++ b/src/styles/__tests__/glassOverlayMaterial.spec.ts @@ -35,6 +35,56 @@ describe('glass overlay material styles', () => { expect(styles).toContain('rgba(255, 255, 255, calc(0.045 + var(--glass-surface-density, 0.86) * 0.09))') }) + it('uses the derived theme tone only for tinted material surfaces', () => { + const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') + const appStyles = readFileSync(resolve(cwd(), 'src/App.vue'), 'utf8') + const tintedRule = styles.match(/&\[data-glass-appearance='tinted'\]\s*\{(?[\s\S]*?)\n {2}\}/u) + ?.groups?.declarations + const wallpaperTintStart = appStyles.indexOf( + "html[data-glass-appearance='tinted'] .background-container.is-glass-theme .background-image.active::after,", + ) + const wallpaperTintEnd = appStyles.indexOf("html[data-glass-appearance='frosted']", wallpaperTintStart) + const wallpaperTintRule = appStyles.slice(wallpaperTintStart, wallpaperTintEnd) + const loginRule = styles.match( + /html\[data-theme='glass'\]\[data-glass-appearance='tinted'\] body\[data-theme='glass'\]\s*\{(?[\s\S]*?)\n\}/u, + )?.groups?.declarations + const workflowRule = styles.match( + /&\[data-glass-appearance='tinted'\] \.workflow-task-card\s*\{(?[\s\S]*?)\n {2}\}/u, + )?.groups?.declarations + const getPropertyValue = (rule: string | undefined, token: string) => + rule?.match(new RegExp(`${token}:\\s*(?[\\s\\S]*?);`))?.groups?.value + + expect(tintedRule).toBeDefined() + for (const token of [ + '--glass-surface', + '--glass-surface-soft', + '--glass-surface-raised', + '--glass-overlay-surface', + ]) { + expect(getPropertyValue(tintedRule, token)).toContain('var(--glass-material-accent-rgb)') + } + for (const token of [ + '--glass-control', + '--glass-control-prominent', + '--glass-control-prominent-focus', + '--glass-border', + '--glass-border-raised', + '--glass-border-hover', + '--glass-highlight', + '--glass-sheen', + ]) { + expect(getPropertyValue(tintedRule, token)).toContain('var(--v-theme-primary)') + } + expect(wallpaperTintStart).toBeGreaterThanOrEqual(0) + expect(wallpaperTintEnd).toBeGreaterThan(wallpaperTintStart) + expect(wallpaperTintRule).toContain('rgba(var(--glass-material-accent-rgb), 3%)') + expect(wallpaperTintRule).not.toContain('var(--v-theme-primary)') + expect(loginRule).toMatch(/\.login-card__surface[\s\S]*?var\(--glass-material-accent-rgb\)/) + expect(loginRule).toMatch(/\.native-login-field[\s\S]*?var\(--v-theme-primary\)/) + expect(workflowRule).toContain('var(--workflow-status-rgb)') + expect(workflowRule).toContain('var(--v-theme-primary)') + }) + it('renders colored chips as shadowless glass without flattening their variants', () => { const styles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') diff --git a/src/styles/__tests__/pluginCardAccent.spec.ts b/src/styles/__tests__/pluginCardAccent.spec.ts index 9dd3da47..141daf7c 100644 --- a/src/styles/__tests__/pluginCardAccent.spec.ts +++ b/src/styles/__tests__/pluginCardAccent.spec.ts @@ -20,9 +20,13 @@ describe('plugin card accent styles', () => { it('limits tinted theme mixing to six percent', () => { const glassStyles = readFileSync(resolve(cwd(), 'src/styles/themes/glass.scss'), 'utf8') + const tintedBannerRule = glassStyles.match( + /&\[data-glass-appearance='tinted'\] \.plugin-card__banner\s*\{(?[\s\S]*?)\n {2}\}/u, + )?.groups?.declarations expect(glassStyles.match(/rgba\(var\(--plugin-card-effective-accent-rgb\)/g)).toHaveLength(9) expect(glassStyles.match(/rgba\(var\(--plugin-card-effective-accent-rgb\)[\s\S]*?\) 94%/g)).toHaveLength(2) + expect(tintedBannerRule?.match(/rgba\(var\(--glass-material-accent-rgb\)[\s\S]*?\)\s*\)/g)).toHaveLength(2) expect(glassStyles).not.toContain('var(--plugin-card-accent-rgb, 40, 169, 225)') }) }) diff --git a/src/styles/themes/glass.scss b/src/styles/themes/glass.scss index 00734035..e6952229 100644 --- a/src/styles/themes/glass.scss +++ b/src/styles/themes/glass.scss @@ -131,17 +131,17 @@ html[data-theme='glass'] { --glass-surface: color-mix( in srgb, rgba(11, 19, 34, calc(0.12 + var(--glass-surface-density, 0.72) * 0.2)) 88%, - rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.28)) + rgba(var(--glass-material-accent-rgb), calc(var(--glass-tint-density, 0.65) * 0.28)) ); --glass-surface-soft: color-mix( in srgb, rgba(11, 19, 34, calc(0.13 + var(--glass-surface-density, 0.72) * 0.23)) 89%, - rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.26)) + rgba(var(--glass-material-accent-rgb), calc(var(--glass-tint-density, 0.65) * 0.26)) ); --glass-surface-raised: color-mix( in srgb, rgba(11, 19, 34, calc(0.07 + var(--glass-surface-density, 0.72) * 0.36)) 84%, - rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.32)) + rgba(var(--glass-material-accent-rgb), calc(var(--glass-tint-density, 0.65) * 0.32)) ); --glass-control: color-mix(in srgb, rgba(11, 19, 34, 52%) 88%, rgba(var(--v-theme-primary), 28%)); --glass-control-prominent: color-mix(in srgb, rgba(255, 255, 255, 7%) 82%, rgba(var(--v-theme-primary), 24%)); @@ -185,7 +185,7 @@ html[data-theme='glass'] { --glass-overlay-surface: color-mix( in srgb, rgba(11, 19, 34, calc(0.09 + var(--glass-surface-density, 0.72) * 0.2)) 88%, - rgba(var(--v-theme-primary), calc(var(--glass-tint-density, 0.65) * 0.22)) + rgba(var(--glass-material-accent-rgb), calc(var(--glass-tint-density, 0.65) * 0.22)) ); --glass-overlay-blur: var(--glass-overlay-clarity-blur, 6px); --glass-overlay-saturate: 120%; @@ -1102,13 +1102,13 @@ html[data-theme='glass'] { color-mix( in srgb, rgba(var(--plugin-card-effective-accent-rgb), calc(0.14 + var(--glass-tint-density, 0.65) * 0.33)) 94%, - rgba(var(--v-theme-primary), calc(0.14 + var(--glass-tint-density, 0.65) * 0.33)) + rgba(var(--glass-material-accent-rgb), calc(0.14 + var(--glass-tint-density, 0.65) * 0.33)) ) 0%, color-mix( in srgb, rgba(var(--plugin-card-effective-accent-rgb), calc(0.07 + var(--glass-tint-density, 0.65) * 0.2)) 94%, - rgba(var(--v-theme-primary), calc(0.07 + var(--glass-tint-density, 0.65) * 0.2)) + rgba(var(--glass-material-accent-rgb), calc(0.07 + var(--glass-tint-density, 0.65) * 0.2)) ) 58%, rgba(var(--plugin-card-effective-accent-rgb), calc(0.04 + var(--glass-tint-density, 0.65) * 0.095)) 100% @@ -1573,7 +1573,7 @@ html[data-theme='glass'][data-glass-appearance='tinted'] body[data-theme='glass' .login-card__surface { background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), transparent 36%), - linear-gradient(rgba(var(--v-theme-primary), 0.08), rgba(var(--v-theme-primary), 0.03)), + linear-gradient(rgba(var(--glass-material-accent-rgb), 0.08), rgba(var(--glass-material-accent-rgb), 0.03)), linear-gradient(rgba(7, 14, 25, 0.16), rgba(7, 14, 25, 0.3)) !important; } diff --git a/src/utils/__tests__/glassColor.spec.ts b/src/utils/__tests__/glassColor.spec.ts index 6351981a..a56a451b 100644 --- a/src/utils/__tests__/glassColor.spec.ts +++ b/src/utils/__tests__/glassColor.spec.ts @@ -1,4 +1,4 @@ -import { normalizePluginAccentColor } from '@/utils/glassColor' +import { normalizePluginAccentColor, normalizeThemeMaterialAccent } from '@/utils/glassColor' import { describe, expect, it } from 'vitest' interface OklchColor { @@ -69,3 +69,56 @@ describe('normalizePluginAccentColor', () => { expect(normalizePluginAccentColor('#gg0000')).toBeUndefined() }) }) + +describe('normalizeThemeMaterialAccent', () => { + it.each([ + '#8D51F9', + '#3F51B5', + '#1976D2', + '#00BCD4', + '#009688', + '#4CAF50', + '#FFB400', + '#FF9800', + '#FF4C51', + '#E91E63', + '#16B1FF', + '#607D8B', + ])('keeps preset %s within the material tone contract', sourceHex => { + const normalized = normalizeThemeMaterialAccent(sourceHex) + + expect(normalized).toBeDefined() + expect(normalized?.hex).toMatch(/^#[0-9a-f]{6}$/) + expect(normalized?.rgb).toMatch(/^\d{1,3}, \d{1,3}, \d{1,3}$/) + + const source = hexToOklch(sourceHex) + const output = hexToOklch(normalized!.hex) + expect(output.lightness).toBeGreaterThanOrEqual(0.555) + expect(output.lightness).toBeLessThanOrEqual(0.725) + expect(output.chroma).toBeLessThanOrEqual(Math.min(source.chroma, 0.14) + 0.005) + if (source.chroma >= 0.03 && output.chroma >= 0.03) + expect(hueDistanceDegrees(source.hue, output.hue)).toBeLessThanOrEqual(2) + }) + + it.each(['#000000', '#ffffff', '#7d7d7d', '#ffff00', '#00ffff', '#ff0000'])( + 'keeps extreme %s in gamut without inventing chroma', + sourceHex => { + const normalized = normalizeThemeMaterialAccent(sourceHex) + const source = hexToOklch(sourceHex) + const output = hexToOklch(normalized!.hex) + + expect(output.lightness).toBeGreaterThanOrEqual(0.555) + expect(output.lightness).toBeLessThanOrEqual(0.725) + expect(output.chroma).toBeLessThanOrEqual(Math.min(source.chroma, 0.14) + 0.005) + if (source.chroma >= 0.03 && output.chroma >= 0.03) + expect(hueDistanceDegrees(source.hue, output.hue)).toBeLessThanOrEqual(2) + }, + ) + + it('is deterministic, case-insensitive, and rejects non-contract inputs', () => { + expect(normalizeThemeMaterialAccent('#12ABef')).toEqual(normalizeThemeMaterialAccent('#12abef')) + expect(normalizeThemeMaterialAccent('#fff')).toBeUndefined() + expect(normalizeThemeMaterialAccent('12abef')).toBeUndefined() + expect(normalizeThemeMaterialAccent('#gg0000')).toBeUndefined() + }) +}) diff --git a/src/utils/glassColor.ts b/src/utils/glassColor.ts index 9a020ed1..e95c1c94 100644 --- a/src/utils/glassColor.ts +++ b/src/utils/glassColor.ts @@ -20,6 +20,9 @@ export interface GlassAccentColor { const PLUGIN_ACCENT_MIN_LIGHTNESS = 0.48 const PLUGIN_ACCENT_MAX_LIGHTNESS = 0.76 const PLUGIN_ACCENT_MAX_CHROMA = 0.18 +const THEME_MATERIAL_MIN_LIGHTNESS = 0.56 +const THEME_MATERIAL_MAX_LIGHTNESS = 0.72 +const THEME_MATERIAL_MAX_CHROMA = 0.14 const NEUTRAL_CHROMA_THRESHOLD = 0.02 const GAMUT_SEARCH_ITERATIONS = 24 @@ -117,19 +120,32 @@ function formatAccentColor(color: OklchColor): GlassAccentColor { } } -/** 将插件 Logo 主色限制在可读范围内,同时保持品牌色相与中性色属性。 */ -export function normalizePluginAccentColor(color: string): GlassAccentColor | undefined { +function normalizeAccentColor(color: string, minLightness: number, maxLightness: number, maxChroma: number) { const srgb = parseHexColor(color) if (!srgb) return undefined const source = oklabToOklch(srgbToOklab(srgb)) - const chroma = - source.chroma < NEUTRAL_CHROMA_THRESHOLD ? source.chroma : Math.min(source.chroma, PLUGIN_ACCENT_MAX_CHROMA) + const chroma = source.chroma < NEUTRAL_CHROMA_THRESHOLD ? source.chroma : Math.min(source.chroma, maxChroma) const normalized = mapChromaToSrgb({ - lightness: clamp(source.lightness, PLUGIN_ACCENT_MIN_LIGHTNESS, PLUGIN_ACCENT_MAX_LIGHTNESS), + lightness: clamp(source.lightness, minLightness, maxLightness), chroma, hue: source.hue, }) return formatAccentColor(normalized) } + +/** 将插件 Logo 主色限制在可读范围内,同时保持品牌色相与中性色属性。 */ +export function normalizePluginAccentColor(color: string): GlassAccentColor | undefined { + return normalizeAccentColor(color, PLUGIN_ACCENT_MIN_LIGHTNESS, PLUGIN_ACCENT_MAX_LIGHTNESS, PLUGIN_ACCENT_MAX_CHROMA) +} + +/** 派生大面积色调玻璃使用的材料色,不改变用户选择的真实主色。 */ +export function normalizeThemeMaterialAccent(color: string): GlassAccentColor | undefined { + return normalizeAccentColor( + color, + THEME_MATERIAL_MIN_LIGHTNESS, + THEME_MATERIAL_MAX_LIGHTNESS, + THEME_MATERIAL_MAX_CHROMA, + ) +}