feat(mermaid): 流程图主题切换跟随 VT 圆形扩散动画
此前 mermaid.render() 异步,而 VT 回调同步,Dioxus effect 重跑发生在 VT 回调 之后的微任务,导致动画期间流程图不变、动画结束后才瞬切。 利用 startViewTransition 回调可返回 Promise 的特性(浏览器等其 resolve 才拍 NEW 快照、播动画),让 VT 回调等 mermaid 重渲染完成,新主题流程图进入快照, 圆形扩散覆盖整页含 mermaid 区域。 改动: - theme-transition.ts: 新增 onThemeChange registry(回调可返回 Promise)。 notifyThemeChange 遍历 registry 收集 Promise 返回聚合结果;THEME_CHANGE_EVENT 事件保留(给 codemirror/xterm 同步换肤,向后兼容)。 startThemeTransition 的 VT 回调改为 async,await notifyThemeChange 聚合 Promise, 确保异步换肤组件(mermaid)完成后再拍 NEW 快照。 applyResolvedTheme(跟随系统瞬切)不等 registry,保持瞬切语义。 - mermaid.ts: 顶层注册 onThemeChange 订阅,listener 调 rerenderExistingBlocks 并 返回其 Promise,供 VT 回调等待。initMermaid/rerenderExistingBlocks 返回 Promise。 - post_content.rs: VT 期间(is-theme-transitioning)跳过 __initMermaid 调用,避免 Dioxus effect 抢先重渲染穿透伪元素快照(照搬 code_runner/runner.rs 守卫); 动画结束后 effect 重跑做幂等兜底。 - theme-transition.test.ts: VT callback 签名 void→Promise<void>,新增 registry 等待测试 + 降级路径不等 registry 测试。
This commit is contained in:
parent
6eb7a743e5
commit
bdfc7a19e9
@ -1,15 +1,15 @@
|
|||||||
|
import type { ThemeName } from '@yggdrasil/shared';
|
||||||
import { initAnchorClick } from './anchor-click';
|
import { initAnchorClick } from './anchor-click';
|
||||||
import { scrollToHash } from './hash-scroll';
|
import { scrollToHash } from './hash-scroll';
|
||||||
import { initMermaid } from './mermaid';
|
import { initMermaid } from './mermaid';
|
||||||
import { initPostContent } from './post-content';
|
import { initPostContent } from './post-content';
|
||||||
import { applyResolvedTheme, startThemeTransition } from './theme-transition';
|
import { applyResolvedTheme, startThemeTransition } from './theme-transition';
|
||||||
import type { ThemeName } from '@yggdrasil/shared';
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__initPostContent: (selector: string) => void;
|
__initPostContent: (selector: string) => void;
|
||||||
__initMermaid: (selector: string, theme: ThemeName) => void;
|
__initMermaid: (selector: string, theme: ThemeName) => Promise<void>;
|
||||||
__initAnchorClick: () => void;
|
__initAnchorClick: () => void;
|
||||||
__scrollToHash: () => void;
|
__scrollToHash: () => void;
|
||||||
__startThemeTransition: (x: number, y: number) => void;
|
__startThemeTransition: (x: number, y: number) => void;
|
||||||
|
|||||||
@ -20,6 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeName } from '@yggdrasil/shared';
|
import type { ThemeName } from '@yggdrasil/shared';
|
||||||
|
import { onThemeChange } from './theme-transition';
|
||||||
|
|
||||||
|
/** 文章正文容器选择器(与 post_content.rs 的 __initMermaid 调用一致)。 */
|
||||||
|
const POST_CONTENT_SELECTOR = '.post-content';
|
||||||
|
|
||||||
type MermaidApi = {
|
type MermaidApi = {
|
||||||
initialize: (config: Record<string, unknown>) => void;
|
initialize: (config: Record<string, unknown>) => void;
|
||||||
@ -146,7 +150,7 @@ function observeBlock(pre: HTMLPreElement, render: () => Promise<void>): void {
|
|||||||
* @param selector 文章正文容器选择器(如 '.post-content')
|
* @param selector 文章正文容器选择器(如 '.post-content')
|
||||||
* @param theme 当前生效主题,传给 mermaid 适配暗色
|
* @param theme 当前生效主题,传给 mermaid 适配暗色
|
||||||
*/
|
*/
|
||||||
export function initMermaid(selector: string, theme: ThemeName): void {
|
export function initMermaid(selector: string, theme: ThemeName): Promise<void> {
|
||||||
const root = document.querySelector(selector);
|
const root = document.querySelector(selector);
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
||||||
@ -172,8 +176,9 @@ export function initMermaid(selector: string, theme: ThemeName): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 路径 2:已渲染的块(<code> 已被 SVG 替换,按 dataset 回找)。无条件执行,
|
// 路径 2:已渲染的块(<code> 已被 SVG 替换,按 dataset 回找)。无条件执行,
|
||||||
// 覆盖「页面上未渲染块与已渲染块并存」的场景。
|
// 覆盖「页面上未渲染块与已渲染块并存」的场景。主题切换时由 onThemeChange 订阅
|
||||||
rerenderExistingBlocks(root, theme);
|
// 返回其 Promise,供 VT callback await(让新主题流程图进入 NEW 快照)。
|
||||||
|
return rerenderExistingBlocks(root, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,16 +186,40 @@ export function initMermaid(selector: string, theme: ThemeName): void {
|
|||||||
*
|
*
|
||||||
* 主题切换重跑 initMermaid 时,已渲染的 pre 里 <code> 已被 SVG 替换,
|
* 主题切换重跑 initMermaid 时,已渲染的 pre 里 <code> 已被 SVG 替换,
|
||||||
* `pre > code.language-mermaid` 选择器不再命中。这里用 dataset 标记回找。
|
* `pre > code.language-mermaid` 选择器不再命中。这里用 dataset 标记回找。
|
||||||
|
*
|
||||||
|
* 返回聚合 Promise:所有需要重渲染的块完成后 resolve。onThemeChange 订阅返回它,
|
||||||
|
* VT callback await 它以等 mermaid.render 异步完成后再拍 NEW 快照。单块失败不中断
|
||||||
|
* 聚合(catch 吞错 + 加 mermaid-error class)。
|
||||||
*/
|
*/
|
||||||
function rerenderExistingBlocks(root: Element, theme: ThemeName): void {
|
function rerenderExistingBlocks(root: Element, theme: ThemeName): Promise<void> {
|
||||||
const rendered = root.querySelectorAll<HTMLPreElement>('pre[data-mermaid-rendered]');
|
const rendered = root.querySelectorAll<HTMLPreElement>('pre[data-mermaid-rendered]');
|
||||||
|
const tasks: Promise<void>[] = [];
|
||||||
rendered.forEach((pre) => {
|
rendered.forEach((pre) => {
|
||||||
if (pre.dataset.mermaidTheme === theme) return;
|
if (pre.dataset.mermaidTheme === theme) return;
|
||||||
const source = pre.dataset.mermaidSource;
|
const source = pre.dataset.mermaidSource;
|
||||||
if (!source) return; // 无缓存源码无法重渲染,保守跳过
|
if (!source) return; // 无缓存源码无法重渲染,保守跳过
|
||||||
void renderBlock(pre, source, theme).catch((err) => {
|
tasks.push(
|
||||||
console.error('mermaid re-render failed:', err);
|
renderBlock(pre, source, theme).catch((err) => {
|
||||||
pre.classList.add('mermaid-error');
|
console.error('mermaid re-render failed:', err);
|
||||||
});
|
pre.classList.add('mermaid-error');
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
return Promise.all(tasks).then(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅主题切换:主题变化时重渲染已渲染的 mermaid 块,返回重渲染 Promise。
|
||||||
|
*
|
||||||
|
* VT 协调的关键:本 listener 返回 Promise → notifyThemeChange 收集它 → VT callback
|
||||||
|
* await 它 → 浏览器等 mermaid.render 完成、拍含新主题流程图的 NEW 快照、再播圆形扩散。
|
||||||
|
* 无 VT(降级 / 跟随系统瞬切)时调用方不等,mermaid 后台重渲染。
|
||||||
|
*
|
||||||
|
* 顶层注册(IIFE 加载时),确保任何时刻主题切换都能命中。首次渲染前无已渲染块,
|
||||||
|
* rerenderExistingBlocks 自然 no-op。
|
||||||
|
*/
|
||||||
|
onThemeChange((isDark) => {
|
||||||
|
const root = document.querySelector(POST_CONTENT_SELECTOR);
|
||||||
|
if (!root) return;
|
||||||
|
return rerenderExistingBlocks(root, isDark ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
* 且事件在 applyDarkClass 之前触发(编辑器换肤先于 class 翻转,同一 reflow 捕获)。
|
* 且事件在 applyDarkClass 之前触发(编辑器换肤先于 class 翻转,同一 reflow 捕获)。
|
||||||
*/
|
*/
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { THEME_CHANGE_EVENT } from './theme-transition';
|
import { onThemeChange, THEME_CHANGE_EVENT } from './theme-transition';
|
||||||
import './index';
|
import './index';
|
||||||
|
|
||||||
describe('startThemeTransition', () => {
|
describe('startThemeTransition', () => {
|
||||||
@ -44,10 +44,10 @@ describe('startThemeTransition', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('主路径:有 startViewTransition 时调用它,注入变量,callback 切换 dark class', async () => {
|
it('主路径:有 startViewTransition 时调用它,注入变量,callback 切换 dark class', async () => {
|
||||||
const cbRef: { cb: (() => void) | null } = { cb: null };
|
const cbRef: { cb: (() => Promise<void>) | null } = { cb: null };
|
||||||
const readyP = Promise.resolve();
|
const readyP = Promise.resolve();
|
||||||
const finishedP = Promise.resolve();
|
const finishedP = Promise.resolve();
|
||||||
const startVT = vi.fn((cb: () => void) => {
|
const startVT = vi.fn((cb: () => Promise<void>) => {
|
||||||
cbRef.cb = cb;
|
cbRef.cb = cb;
|
||||||
return { ready: readyP, finished: finishedP, skipTransition: () => {} };
|
return { ready: readyP, finished: finishedP, skipTransition: () => {} };
|
||||||
});
|
});
|
||||||
@ -69,8 +69,8 @@ describe('startThemeTransition', () => {
|
|||||||
// is-theme-transitioning 应在 VT 之前添加
|
// is-theme-transitioning 应在 VT 之前添加
|
||||||
expect(document.documentElement.classList.contains('is-theme-transitioning')).toBe(true);
|
expect(document.documentElement.classList.contains('is-theme-transitioning')).toBe(true);
|
||||||
|
|
||||||
// callback 里根据 DOM 现状(无 dark)切到 dark
|
// callback 里根据 DOM 现状(无 dark)切到 dark(callback 现为 async,需 await)
|
||||||
cbRef.cb?.();
|
await cbRef.cb?.();
|
||||||
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
||||||
|
|
||||||
// finished 后移除 is-theme-transitioning 和 CSS 变量
|
// finished 后移除 is-theme-transitioning 和 CSS 变量
|
||||||
@ -117,10 +117,10 @@ describe('startThemeTransition', () => {
|
|||||||
delete (document as unknown as { startViewTransition?: unknown }).startViewTransition;
|
delete (document as unknown as { startViewTransition?: unknown }).startViewTransition;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('主路径:VT callback 内 dispatch 主题变更事件,且先于 dark class 翻转', () => {
|
it('主路径:VT callback 内 dispatch 主题变更事件,且先于 dark class 翻转', async () => {
|
||||||
const cbRef: { cb: (() => void) | null } = { cb: null };
|
const cbRef: { cb: (() => Promise<void>) | null } = { cb: null };
|
||||||
Object.defineProperty(document, 'startViewTransition', {
|
Object.defineProperty(document, 'startViewTransition', {
|
||||||
value: (cb: () => void) => {
|
value: (cb: () => Promise<void>) => {
|
||||||
cbRef.cb = cb;
|
cbRef.cb = cb;
|
||||||
return { ready: Promise.resolve(), finished: Promise.resolve(), skipTransition: () => {} };
|
return { ready: Promise.resolve(), finished: Promise.resolve(), skipTransition: () => {} };
|
||||||
},
|
},
|
||||||
@ -141,7 +141,7 @@ describe('startThemeTransition', () => {
|
|||||||
|
|
||||||
// 亮→暗:无 dark class,isDark=true
|
// 亮→暗:无 dark class,isDark=true
|
||||||
window.__startThemeTransition(0, 0);
|
window.__startThemeTransition(0, 0);
|
||||||
cbRef.cb?.();
|
await cbRef.cb?.();
|
||||||
|
|
||||||
expect(eventSnapshots).toHaveLength(1);
|
expect(eventSnapshots).toHaveLength(1);
|
||||||
expect(eventSnapshots[0].isDark).toBe(true);
|
expect(eventSnapshots[0].isDark).toBe(true);
|
||||||
@ -198,4 +198,62 @@ describe('startThemeTransition', () => {
|
|||||||
|
|
||||||
window.removeEventListener(THEME_CHANGE_EVENT, listener);
|
window.removeEventListener(THEME_CHANGE_EVENT, listener);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('onThemeChange:VT callback 等待 registry 注册的异步回调 Promise', async () => {
|
||||||
|
const cbRef: { cb: (() => Promise<void>) | null } = { cb: null };
|
||||||
|
Object.defineProperty(document, 'startViewTransition', {
|
||||||
|
value: (cb: () => Promise<void>) => {
|
||||||
|
cbRef.cb = cb;
|
||||||
|
return { ready: Promise.resolve(), finished: Promise.resolve(), skipTransition: () => {} };
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// registry 回调返回一个可控的 Promise,记录它是否在 callback resolve 前完成
|
||||||
|
let resolveAsync: (() => void) | null = null;
|
||||||
|
const asyncDone = { value: false };
|
||||||
|
const off = onThemeChange((isDark) => {
|
||||||
|
void isDark;
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
resolveAsync = () => {
|
||||||
|
asyncDone.value = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__startThemeTransition(0, 0);
|
||||||
|
|
||||||
|
// callback 尚未 resolve(async 任务未完成)——VT callback 的 Promise 仍 pending
|
||||||
|
const callbackPromise = cbRef.cb?.();
|
||||||
|
expect(asyncDone.value).toBe(false);
|
||||||
|
|
||||||
|
// 触发异步任务完成
|
||||||
|
resolveAsync?.();
|
||||||
|
await callbackPromise;
|
||||||
|
expect(asyncDone.value).toBe(true);
|
||||||
|
|
||||||
|
off();
|
||||||
|
delete (document as unknown as { startViewTransition?: unknown }).startViewTransition;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onThemeChange:降级路径(无 VT)不等 registry 异步回调', async () => {
|
||||||
|
// 降级路径不 await notifyThemeChange,applyDarkClass 同步完成,registry 后台跑
|
||||||
|
let registryCalled = false;
|
||||||
|
const off = onThemeChange(() => {
|
||||||
|
registryCalled = true;
|
||||||
|
return new Promise<void>(() => {}); // 永不 resolve
|
||||||
|
});
|
||||||
|
|
||||||
|
// 无 startViewTransition → 降级路径
|
||||||
|
window.__startThemeTransition(0, 0);
|
||||||
|
|
||||||
|
// dark class 应已同步翻转(不等 registry)
|
||||||
|
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
||||||
|
// registry 回调被调用(同步触发),但 Promise 未被 await
|
||||||
|
expect(registryCalled).toBe(true);
|
||||||
|
|
||||||
|
off();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -53,16 +53,47 @@ function applyDarkClass(isDark: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步通知命令式换肤的组件(CodeMirror / xterm)切换主题。
|
* 主题切换 registry:命令式 / 异步换肤组件注册的回调。
|
||||||
*
|
*
|
||||||
* CustomEvent 的 dispatch 是同步的:listener 在本函数返回前执行完毕,
|
* 与 THEME_CHANGE_EVENT 事件并存:
|
||||||
* 故编辑器的 setTheme(reconfigure / options.theme =) 在调用方继续前已完成。
|
* - 事件(CustomEvent):同步 dispatch,供 CodeMirror / xterm 等同步换肤组件用
|
||||||
* 这对 VT 至关重要——必须在 NEW 快照捕获前完成换肤,否则快照里仍是旧色。
|
* (它们在 listener 里同步 setTheme,被同一 reflow 捕获进 NEW 快照)。事件拿不到
|
||||||
*
|
* listener 返回值,fire-and-forget。
|
||||||
* 幂等:与 Dioxus use_effect 驱动的 set_theme 并存,重复设置相同主题是 no-op。
|
* - registry:回调**可返回 Promise**,调用方(如 VT callback)能 await 它,等异步换肤
|
||||||
|
* 组件(如 mermaid 的 render())完成。这是让异步渲染内容参与 VT 动画的关键——
|
||||||
|
* mermaid.render 是异步的,必须在 VT 拍 NEW 快照前完成,否则快照里仍是旧图。
|
||||||
*/
|
*/
|
||||||
function notifyThemeChange(isDark: boolean): void {
|
const themeChangeCallbacks = new Set<(isDark: boolean) => Promise<void> | void>();
|
||||||
|
|
||||||
|
/** 注册主题切换回调,返回取消注册函数。回调可返回 Promise,调用方会等待它。 */
|
||||||
|
export function onThemeChange(cb: (isDark: boolean) => Promise<void> | void): () => void {
|
||||||
|
themeChangeCallbacks.add(cb);
|
||||||
|
return () => themeChangeCallbacks.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知命令式换肤的组件(CodeMirror / xterm / mermaid)切换主题。
|
||||||
|
*
|
||||||
|
* 双通道:
|
||||||
|
* 1. 同步 dispatch THEME_CHANGE_EVENT(给 CodeMirror / xterm,它们在 listener 内
|
||||||
|
* 同步 setTheme,被同一 reflow 捕获进 NEW 快照)。
|
||||||
|
* 2. 遍历 registry 调每个 cb,收集返回的 Promise,返回聚合 Promise(VT callback
|
||||||
|
* await 它以等 mermaid 等异步换肤完成)。同步组件不返回值,聚合自动忽略。
|
||||||
|
*
|
||||||
|
* 返回聚合 Promise,调用方可选 await(走 VT 的路径必须 await,瞬切路径可不 await)。
|
||||||
|
*/
|
||||||
|
function notifyThemeChange(isDark: boolean): Promise<void> {
|
||||||
window.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, { detail: { isDark } }));
|
window.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, { detail: { isDark } }));
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
themeChangeCallbacks.forEach((cb) => {
|
||||||
|
try {
|
||||||
|
const ret = cb(isDark);
|
||||||
|
if (ret) promises.push(ret.catch(() => {})); // 单个失败不中断聚合
|
||||||
|
} catch {
|
||||||
|
// 同步抛错的 cb 忽略,不中断其他回调
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.all(promises).then(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,8 +121,8 @@ export function startThemeTransition(x: number, y: number): void {
|
|||||||
|
|
||||||
if (!hasVT || reduced) {
|
if (!hasVT || reduced) {
|
||||||
// 降级路径:无 VT 动画,同步换肤 + 翻 class(瞬切)。
|
// 降级路径:无 VT 动画,同步换肤 + 翻 class(瞬切)。
|
||||||
// 同样 dispatch 事件,保持与主路径对称(编辑器不依赖动画存在与否)。
|
// 同样通知换肤(不 await,保持瞬切语义;mermaid 等异步组件后台重渲染)。
|
||||||
notifyThemeChange(isDark);
|
void notifyThemeChange(isDark);
|
||||||
applyDarkClass(isDark);
|
applyDarkClass(isDark);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,16 +137,19 @@ export function startThemeTransition(x: number, y: number): void {
|
|||||||
// 禁用所有 CSS transition,确保 VT 截图是最终颜色
|
// 禁用所有 CSS transition,确保 VT 截图是最终颜色
|
||||||
html.classList.add('is-theme-transitioning');
|
html.classList.add('is-theme-transitioning');
|
||||||
|
|
||||||
const vt = document.startViewTransition(() => {
|
const vt = document.startViewTransition(async () => {
|
||||||
// ★ 关键:先 dispatch 事件让编辑器同步换肤,再翻 .dark class。
|
// ★ 关键:先通知换肤(同步 dispatch 事件让编辑器同步换肤 + 收集 registry 的
|
||||||
// 顺序不能反——编辑器换肤 + class 翻转必须被同一个 getComputedStyle
|
// 异步 Promise),再翻 .dark class。顺序不能反——编辑器换肤 + class 翻转必须被
|
||||||
// reflow 捕获进 NEW 快照。若先翻 class 后换肤,reflow 可能漏掉编辑器。
|
// 同一个 getComputedStyle reflow 捕获进 NEW 快照。
|
||||||
notifyThemeChange(isDark);
|
const asyncWork = notifyThemeChange(isDark);
|
||||||
applyDarkClass(isDark);
|
applyDarkClass(isDark);
|
||||||
// 强制同步样式重算:确保 body 的 background-color 解析为目标值,
|
// 强制同步样式重算:确保 body 的 background-color 解析为目标值,
|
||||||
// 同时 flush 编辑器的同步换肤(CodeMirror <style> / xterm inline bg)。
|
// 同时 flush 编辑器的同步换肤(CodeMirror <style> / xterm inline bg)。
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
getComputedStyle(document.body).backgroundColor;
|
getComputedStyle(document.body).backgroundColor;
|
||||||
|
// ★ 等 registry 里异步换肤组件(mermaid.render)完成。callback 返回 Promise 时,
|
||||||
|
// 浏览器等它 resolve 才拍 NEW 快照、播圆形扩散——这样快照里已是新主题流程图。
|
||||||
|
await asyncWork;
|
||||||
});
|
});
|
||||||
|
|
||||||
vt.ready.catch(() => {});
|
vt.ready.catch(() => {});
|
||||||
|
|||||||
@ -166,7 +166,21 @@ pub fn PostContent(content_html: String) -> Element {
|
|||||||
} else {
|
} else {
|
||||||
"light".into()
|
"light".into()
|
||||||
};
|
};
|
||||||
invoke_optional_global(&window, "__initMermaid", &[".post-content".into(), theme_str.into()]);
|
// VT 动画期间跳过:手动点击主题按钮时,__startThemeTransition 的 VT 回调内已通过
|
||||||
|
// onThemeChange registry 同步触发 mermaid 重渲染(被 VT 等待,出现在 NEW 快照里)。
|
||||||
|
// 但本 effect 在 theme.set(next) 后立即触发——早于 VT 回调的异步执行,会抢先改
|
||||||
|
// 实时 DOM。VT 动画播的是伪元素快照,实时 DOM 改动会穿透伪元素,表现为「圆形
|
||||||
|
// 还没展开到流程图,流程图就瞬切」。is-theme-transitioning 期间跳过,让 VT 回调
|
||||||
|
// 内的 registry 重渲染负责;动画结束后此 effect 因 resolved 信号变化重跑(此时
|
||||||
|
// is-theme-transitioning 已移除),做幂等兜底。照搬 code_runner/runner.rs 的守卫。
|
||||||
|
let transitioning = window
|
||||||
|
.document()
|
||||||
|
.and_then(|d| d.document_element())
|
||||||
|
.map(|el| el.class_list().contains("is-theme-transitioning"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !transitioning {
|
||||||
|
invoke_optional_global(&window, "__initMermaid", &[".post-content".into(), theme_str.into()]);
|
||||||
|
}
|
||||||
|
|
||||||
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
|
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
|
||||||
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
|
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user