From cb77d28a6882274a8f195e91afe267199584bfa7 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 3 Jul 2026 13:42:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20=E8=B7=9F=E9=9A=8F=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=88=87=E6=8D=A2=E6=94=B9=E4=B8=BA=E7=9E=AC=E5=88=87?= =?UTF-8?q?=EF=BC=8C=E6=94=BE=E5=BC=83=20VT=20=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 诊断确认:跟随系统与手动点击的 VT 流程在 JS/CSS 层完全等价(animationName、 --tt-r、bg 翻转、clip-path 全部正确),但视觉上跟随系统场景下圆形展开动画 不显示,仅瞬切。系统偏好变化是后台事件,VT 在此上下文下动画不可靠。 改用设置语义的瞬切:JS 侧新增 applyResolvedTheme(isDark) 入口(非翻转语义, 直接 add/remove dark class),Rust effect 改调 __applyResolvedTheme 传实际 明暗值,移除圆心计算逻辑。职责清晰: - 手动点击 → __startThemeTransition → VT 圆形展开(用户手势,动画可靠) - 跟随系统 → __applyResolvedTheme → 瞬切(后台同步语义) --- libs/yggdrasil-core/src/index.ts | 4 +- libs/yggdrasil-core/src/theme-transition.ts | 12 ++++++ src/theme.rs | 44 ++++++--------------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/libs/yggdrasil-core/src/index.ts b/libs/yggdrasil-core/src/index.ts index e3e32de..055cbad 100644 --- a/libs/yggdrasil-core/src/index.ts +++ b/libs/yggdrasil-core/src/index.ts @@ -1,13 +1,15 @@ import { initPostContent } from './post-content'; -import { startThemeTransition } from './theme-transition'; +import { applyResolvedTheme, startThemeTransition } from './theme-transition'; import './style.css'; declare global { interface Window { __initPostContent: (selector: string) => void; __startThemeTransition: (x: number, y: number) => void; + __applyResolvedTheme: (isDark: boolean) => void; } } window.__initPostContent = initPostContent; window.__startThemeTransition = startThemeTransition; +window.__applyResolvedTheme = applyResolvedTheme; diff --git a/libs/yggdrasil-core/src/theme-transition.ts b/libs/yggdrasil-core/src/theme-transition.ts index 8b58a5e..61c9e92 100644 --- a/libs/yggdrasil-core/src/theme-transition.ts +++ b/libs/yggdrasil-core/src/theme-transition.ts @@ -40,6 +40,18 @@ function applyDarkClass(isDark: boolean): void { } } +/** + * 直接设置 的 dark class(设置语义,非翻转)。 + * + * 用于跟随系统偏好变化时的后台同步:系统偏好变化是后台事件,View Transitions + * 在此上下文下动画不可靠(实测圆形展开不显示,仅瞬切),故跟随系统场景不走 + * startThemeTransition 的 VT 路径,改用此函数直接同步 class,做无动画的瞬切。 + * 手动点击主题按钮仍走 startThemeTransition,保留圆形展开动画。 + */ +export function applyResolvedTheme(isDark: boolean): void { + applyDarkClass(isDark); +} + export function startThemeTransition(x: number, y: number): void { const html = document.documentElement; const isDark = !html.classList.contains('dark'); diff --git a/src/theme.rs b/src/theme.rs index 72abae2..43f1a8c 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -206,9 +206,12 @@ pub fn use_theme_provider() -> Signal { // // 仅在 System 模式(theme == System)且 resolved 实际翻转时触发:Light/Dark 显式 // 模式下 resolved 由 theme 决定,DOM 已由 ThemeToggle::onclick 全权处理,effect - // 不介入,避免与手动点击的 VT 动画重复触发。以主题按钮位置为圆心复用同一条 JS - // 路径(__startThemeTransition 从 DOM 现状推导目标明暗),让自动切换与手动点击 - // 的动画起点一致;屏幕中心圆心会让圆形展开近似「整体覆盖」,被误看成淡入淡出。 + // 不介入,避免与手动点击的 VT 动画重复触发。 + // + // 采用无动画的瞬切(__applyResolvedTheme,设置语义)而非 VT 圆形展开:系统偏好 + // 变化是后台事件,实测 View Transitions 在此上下文下动画不显示(VT 流程虽正常 + // 完成、CSS 也正确挂上 tt-expand,但视觉上仅瞬切),故跟随系统走瞬切更可靠; + // 手动点击仍走 __startThemeTransition 保留圆形展开动画。 // // 首次挂载自然跳过:effect 通过 prev 信号记录上一次 resolved,初次运行 prev 为 // None,此时 DOM 已由 ThemePreload 脚本设为正确状态,直接对齐 prev 后返回。 @@ -221,7 +224,7 @@ pub fn use_theme_provider() -> Signal { return; } let current = resolved(); - // 首次运行:DOM 已由 ThemePreload 设好,仅记录基线,不触发动画。 + // 首次运行:DOM 已由 ThemePreload 设好,仅记录基线,不触发同步。 let Some(prev) = prev_resolved() else { prev_resolved.set(Some(current)); return; @@ -231,40 +234,17 @@ pub fn use_theme_provider() -> Signal { return; } prev_resolved.set(Some(current)); - // resolved 翻转 → 触发 VT 圆形展开动画。 + // resolved 翻转 → 瞬切同步 dark class(设置语义,非翻转)。 let Some(window) = web_sys::window() else { return; }; - // 圆心优先取主题切换按钮(.theme-toggle)的中心,与手动点击的动画起点一致; - // 按钮不在 DOM(如未渲染导航栏的页面)时回退到视口中心。 - use wasm_bindgen::JsCast; - let (x, y) = window - .document() - .and_then(|doc| doc.query_selector(".theme-toggle").ok().flatten()) - .map(|el| { - let r = el.unchecked_into::().get_bounding_client_rect(); - (r.x() + r.width() / 2.0, r.y() + r.height() / 2.0) - }) - .unwrap_or_else(|| { - let w = window - .inner_width() - .ok() - .and_then(|v| v.as_f64()) - .map(|v| v / 2.0) - .unwrap_or(0.0); - let h = window - .inner_height() - .ok() - .and_then(|v| v.as_f64()) - .map(|v| v / 2.0) - .unwrap_or(0.0); - (w, h) - }); - let key = "__startThemeTransition".into(); + let key = "__applyResolvedTheme".into(); if let Ok(fn_val) = js_sys::Reflect::get(&window, &key) { if !fn_val.is_undefined() && !fn_val.is_null() { + use wasm_bindgen::JsCast; let fn_obj = fn_val.unchecked_into::(); - let _ = fn_obj.call2(&window, &x.into(), &y.into()); + let is_dark = current == ResolvedTheme::Dark; + let _ = fn_obj.call1(&window, &is_dark.into()); } } });