diff --git a/libs/yggdrasil-core/src/style.css b/libs/yggdrasil-core/src/style.css index 19b9349..c9f6240 100644 --- a/libs/yggdrasil-core/src/style.css +++ b/libs/yggdrasil-core/src/style.css @@ -1,6 +1,17 @@ /* ========== 主题切换圆形展开(View Transitions API) ========== */ -/* 取消默认交叉淡入淡出,由 clip-path: circle() 接管新快照的展开。 */ +/* VT 回调期间临时冻结所有 CSS 过渡。 + 原因:body 有 transition: background-color 0.3s(input.css), + toggle dark class 后真实渲染的背景色要 0.3s 才过渡到新主题。 + 而 VT 在 toggle 后的下一帧(~16ms)拍 NEW 快照——此时背景色才走了 ~5%, + 新旧两张快照几乎一样,圆形展开就看不出颜色差。 + 冻结过渡让 NEW 快照立即拿到新主题配色,圆形才有可见的颜色对比。 */ +html.vt-freeze, +html.vt-freeze * { + transition: none !important; +} + +/* 取消默认交叉淡入淡出,由 clip-path: circle() 接管新快照的展开。 */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; @@ -20,3 +31,4 @@ clip-path: circle(var(--tt-max-r, 100vmax) at var(--tt-x, 0px) var(--tt-y, 0px)); } } + diff --git a/libs/yggdrasil-core/src/theme-transition.test.ts b/libs/yggdrasil-core/src/theme-transition.test.ts index 634b74c..cf5c7f4 100644 --- a/libs/yggdrasil-core/src/theme-transition.test.ts +++ b/libs/yggdrasil-core/src/theme-transition.test.ts @@ -10,11 +10,13 @@ import './index'; describe('startThemeTransition', () => { beforeEach(() => { document.documentElement.classList.remove('dark'); + document.documentElement.classList.remove('vt-freeze'); document.documentElement.style.cssText = ''; vi.restoreAllMocks(); }); afterEach(() => { document.documentElement.classList.remove('dark'); + document.documentElement.classList.remove('vt-freeze'); document.documentElement.style.cssText = ''; }); @@ -66,6 +68,32 @@ describe('startThemeTransition', () => { expect(document.documentElement.classList.contains('dark')).toBe(false); cbRef.cb!(); expect(document.documentElement.classList.contains('dark')).toBe(true); + // 回调里同时冻结 CSS 过渡,让 NEW 快照立即反映新主题配色 + expect(document.documentElement.classList.contains('vt-freeze')).toBe(true); + + delete (document as unknown as { startViewTransition?: unknown }).startViewTransition; + }); + + it('主路径:vt.finished 结束后移除 vt-freeze 并复位重入标志', async () => { + const finishedPromise = Promise.resolve(); + const startVT = vi.fn((cb: () => void) => { + cb(); + return { finished: finishedPromise, skipTransition: () => {} }; + }); + Object.defineProperty(document, 'startViewTransition', { + value: startVT, + configurable: true, + writable: true, + }); + + window.__startThemeTransition(0, 0, true); + // 回调执行后 freeze 已加 + expect(document.documentElement.classList.contains('vt-freeze')).toBe(true); + + await finishedPromise; + // 微任务:.finally 在 finished resolve 后于微任务阶段执行,等一轮 + await Promise.resolve(); + expect(document.documentElement.classList.contains('vt-freeze')).toBe(false); delete (document as unknown as { startViewTransition?: unknown }).startViewTransition; }); diff --git a/libs/yggdrasil-core/src/theme-transition.ts b/libs/yggdrasil-core/src/theme-transition.ts index 0142f44..9738433 100644 --- a/libs/yggdrasil-core/src/theme-transition.ts +++ b/libs/yggdrasil-core/src/theme-transition.ts @@ -65,15 +65,19 @@ export function startThemeTransition(x: number, y: number, isDark: boolean): voi html.style.setProperty('--tt-y', `${y}px`); html.style.setProperty('--tt-max-r', `${maxCornerDistance(x, y)}px`); + // VT 回调里:先冻结所有 CSS 过渡(让 NEW 快照立即拿到新主题配色, + // 不被 body 的 background-color .3s 过渡拖慢),再 toggle dark class。 const vt = document.startViewTransition(() => { + html.classList.add('vt-freeze'); applyDarkClass(isDark); }); // vt.finished 在 skipTransition 或页面跳转时会 reject(属预期),用 .catch 吞掉 - // 以避免 unhandled rejection 刷控制台;无论 resolve/reject 都复位重入标志。 + // 以避免 unhandled rejection 刷控制台;无论 resolve/reject 都复位重入标志与冻结。 vt.finished .catch(() => {}) .finally(() => { + html.classList.remove('vt-freeze'); transitioning = false; }); }