From c1781d78311b3cbec18ae522ed61c0c34328d3cc Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 26 Jun 2026 14:36:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20VT=20=E6=9C=9F=E9=97=B4=E5=86=BB?= =?UTF-8?q?=E7=BB=93=20CSS=20=E8=BF=87=E6=B8=A1,=E8=AE=A9=E5=9C=86?= =?UTF-8?q?=E5=BD=A2=E5=B1=95=E5=BC=80=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit body 有 transition: background-color .3s,VT 在 toggle dark class 后下一帧拍 NEW 快照时背景色才走了 ~5%,新旧快照几乎一样, 圆形展开看不出颜色差。修复:回调里加 vt-freeze 类禁用所有 过渡,让 NEW 快照立即拿到新主题配色;动画结束(finished)后 移除。补 1 个测试覆盖 freeze 生命周期。 --- libs/yggdrasil-core/src/style.css | 14 +++++++++- .../src/theme-transition.test.ts | 28 +++++++++++++++++++ libs/yggdrasil-core/src/theme-transition.ts | 6 +++- 3 files changed, 46 insertions(+), 2 deletions(-) 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; }); }