feat(mermaid): 流程图配色对齐 Catppuccin + 容器美化

mermaid.initialize 之前用内置 'default'/'dark' 主题,配色与站点 Catppuccin
调色板完全脱节,图是"外来"的样子;且零 mermaid 专用 CSS,SVG 直接套在通用
pre 容器里,宽图横向滚动、无居中无留白。

改动:
- mermaid.ts:theme 切到 'base'(base 不硬编码颜色,themeVariables 能完全
  控制调色板;'default' 主题硬编码 mainBkg=#ECECFF 会阻断覆盖)。新增 Latte/
  Mocha 两套 themeVariables,节点用 surface 色阶、边框/连线用 subtext 色阶、
  文字用 primary text,hex 取自 themes/*.tmTheme。flowchart 配 basis 平滑曲线
  + diagramPadding 16 + useMaxWidth:true(配合 CSS 消除横向滚动)。
- input.css:新增 .md-content pre[data-mermaid-rendered] 规则,flex 居中 + svg
  max-width:100% 缩放适配;.mermaid-error 加 accent 色左边条提示。
- mermaid.test.ts:theme 断言同步到 'base'+darkMode;新增 themeVariables
  注入测试(钉住 Latte/Mocha 关键 hex)。

范围:纯前端 TS + CSS,零 Rust 改动。
This commit is contained in:
xfy 2026-07-17 10:13:20 +08:00
parent 302e05fe1f
commit fac247f60a
3 changed files with 195 additions and 3 deletions

View File

@ -663,6 +663,29 @@
display: block;
}
/* mermaid 图表容器
data-mermaid-rendered yggdrasil-core/mermaid.ts 渲染成功后标记在 <pre>
渲染后 pre 唯一子元素是 <svg>mermaid 块不注入 copy-code 按钮flex 安全
居中 + 缩放适配宽度消除常规图的横向滚动超宽图 overflow-x:auto 兜底 */
.md-content pre[data-mermaid-rendered] {
display: flex;
justify-content: center;
align-items: center;
padding: 24px 16px;
overflow-x: auto;
}
.md-content pre[data-mermaid-rendered] > svg {
max-width: 100%;
height: auto;
display: block;
}
/* 渲染失败:保留原始源码,加 accent 色左边条提示(站点 accent 为绿,沿用配色体系) */
.md-content pre.mermaid-error {
border-left: 3px solid var(--color-paper-accent);
}
.anchor {
display: none;
text-decoration: none;

View File

@ -67,10 +67,46 @@ describe('initMermaid', () => {
window.__initMermaid('.post-content', 'dark');
await vi.waitFor(() => {
expect(mockInitialize).toHaveBeenCalledWith(expect.objectContaining({ theme: 'dark' }));
// base 主题 + darkMode 标志:让 themeVariables 完全控制 Catppuccin 调色板
expect(mockInitialize).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'base', darkMode: true }),
);
});
});
it('亮/暗主题传入对应的 Catppuccin themeVariables', async () => {
// 暗色Mocha 调色板(背景 #313244、文字 #cdd6f4
const darkRoot = document.createElement('div');
darkRoot.className = 'post-content';
darkRoot.innerHTML = '<pre><code class="language-mermaid">graph TD; A--&gt;B</code></pre>';
document.body.appendChild(darkRoot);
window.__initMermaid('.post-content', 'dark');
await vi.waitFor(() => {
expect(mockInitialize).toHaveBeenCalled();
});
const darkCall = mockInitialize.mock.calls.slice(-1)[0]?.[0] as Record<string, unknown>;
const darkVars = darkCall.themeVariables as Record<string, string>;
expect(darkVars.background).toBe('#313244');
expect(darkVars.primaryTextColor).toBe('#cdd6f4');
expect(darkVars.lineColor).toBe('#a6adc8');
// 亮色Latte 调色板(背景 #dce0e8、文字 #4c4f69
document.body.innerHTML = '';
const lightRoot = document.createElement('div');
lightRoot.className = 'post-content';
lightRoot.innerHTML = '<pre><code class="language-mermaid">graph TD; A--&gt;B</code></pre>';
document.body.appendChild(lightRoot);
window.__initMermaid('.post-content', 'light');
await vi.waitFor(() => {
expect(mockInitialize).toHaveBeenCalled();
});
const lightCall = mockInitialize.mock.calls.slice(-1)[0]?.[0] as Record<string, unknown>;
const lightVars = lightCall.themeVariables as Record<string, string>;
expect(lightVars.background).toBe('#dce0e8');
expect(lightVars.primaryTextColor).toBe('#4c4f69');
expect(lightVars.lineColor).toBe('#5c5f77');
});
it('幂等:重复调用不重复渲染已处理的块', async () => {
const root = document.createElement('div');
root.className = 'post-content';
@ -137,7 +173,9 @@ describe('initMermaid', () => {
await vi.waitFor(() => {
expect(mockRender.mock.calls.length).toBeGreaterThan(firstRenderCalls);
});
expect(mockInitialize).toHaveBeenLastCalledWith(expect.objectContaining({ theme: 'dark' }));
expect(mockInitialize).toHaveBeenLastCalledWith(
expect.objectContaining({ theme: 'base', darkMode: true }),
);
expect(root.querySelector('pre')?.dataset.mermaidTheme).toBe('dark');
});

View File

@ -30,6 +30,127 @@ type MermaidApi = {
render: (id: string, text: string) => Promise<{ svg: string }>;
};
/**
* Catppuccin themeVariables --color-paper-* / .tmTheme
*
* mermaid SVG style CSS initialize
* `theme: 'base'` 'default'/'dark'base themeVariables
* 'default' mainBkg=#ECECFF
*
* surface /线 subtext
* primary text绿/ accent classDef
* hex themes/Catppuccin Latte.tmTheme Catppuccin Mocha.tmTheme
*
* flowchart / sequence / class
*/
/** Latte主题节点 surface 色阶、文字 #4c4f69、连线 subtext1 #5c5f77。 */
const LATTE_VARS = {
background: '#dce0e8', // = --color-paper-code-block图背景与 pre 无缝衔接
// 节点填充surface 色阶递进(主/次/三级),卡片质感
primaryColor: '#e6e9ef',
secondaryColor: '#ccd0da',
tertiaryColor: '#bcc0cc',
mainBkg: '#e6e9ef',
nodeBkg: '#e6e9ef',
secondBkg: '#ccd0da',
// 节点边框surface1 偏冷灰
primaryBorderColor: '#bcc0cc',
secondaryBorderColor: '#acb0be',
tertiaryBorderColor: '#9ca0b0',
nodeBorder: '#bcc0cc',
clusterBorder: '#bcc0cc',
labelBoxBorderColor: '#bcc0cc',
// 文字primary text #4c4f69
primaryTextColor: '#4c4f69',
secondaryTextColor: '#5c5f77',
tertiaryTextColor: '#6c6f85',
textColor: '#4c4f69',
nodeTextColor: '#4c4f69',
titleColor: '#4c4f69',
classText: '#4c4f69',
labelTextColor: '#4c4f69',
// 连线/箭头subtext1 #5c5f77
lineColor: '#5c5f77',
defaultLinkColor: '#5c5f77',
arrowheadColor: '#5c5f77',
// 时序图
actorBkg: '#e6e9ef',
actorBorder: '#bcc0cc',
actorTextColor: '#4c4f69',
actorLineColor: '#5c5f77',
signalColor: '#5c5f77',
signalTextColor: '#4c4f69',
loopTextColor: '#4c4f69',
sequenceNumberColor: '#eff1f5',
activationBkgColor: '#ccd0da',
activationBorderColor: '#bcc0cc',
// 边标签 / 子图背景
edgeLabelBackground: '#eff1f5',
labelBoxBkgColor: '#eff1f5',
clusterBkg: 'rgba(239, 241, 245, 0.5)',
// 注释低饱和黄Latte yellow #df8e1d
noteBkgColor: 'rgba(223, 142, 29, 0.15)',
noteBorderColor: '#df8e1d',
noteTextColor: '#4c4f69',
// 字体:与正文 sans 对齐,中文友好
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif",
fontSize: '16px',
} as const;
/** Mocha主题节点 surface 色阶、文字 #cdd6f4、连线 subtext0 #a6adc8。 */
const MOCHA_VARS = {
background: '#313244', // = --color-paper-code-block
primaryColor: '#45475a',
secondaryColor: '#585b70',
tertiaryColor: '#1e1e2e',
mainBkg: '#45475a',
nodeBkg: '#45475a',
secondBkg: '#585b70',
primaryBorderColor: '#585b70',
secondaryBorderColor: '#45475a',
tertiaryBorderColor: '#313244',
nodeBorder: '#585b70',
clusterBorder: '#585b70',
labelBoxBorderColor: '#585b70',
primaryTextColor: '#cdd6f4',
secondaryTextColor: '#bac2de',
tertiaryTextColor: '#a6adc8',
textColor: '#cdd6f4',
nodeTextColor: '#cdd6f4',
titleColor: '#cdd6f4',
classText: '#cdd6f4',
labelTextColor: '#cdd6f4',
lineColor: '#a6adc8',
defaultLinkColor: '#a6adc8',
arrowheadColor: '#a6adc8',
actorBkg: '#45475a',
actorBorder: '#585b70',
actorTextColor: '#cdd6f4',
actorLineColor: '#a6adc8',
signalColor: '#a6adc8',
signalTextColor: '#cdd6f4',
loopTextColor: '#cdd6f4',
sequenceNumberColor: '#1e1e2e',
activationBkgColor: '#585b70',
activationBorderColor: '#6c7086',
edgeLabelBackground: '#1e1e2e',
labelBoxBkgColor: '#1e1e2e',
clusterBkg: 'rgba(30, 30, 46, 0.5)',
noteBkgColor: 'rgba(249, 226, 175, 0.12)',
noteBorderColor: '#f9e2af',
noteTextColor: '#cdd6f4',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif",
fontSize: '16px',
} as const;
/** 按主题返回对应 Catppuccin themeVariables。 */
function themeVarsFor(theme: ThemeName): Record<string, unknown> {
return theme === 'dark' ? { ...MOCHA_VARS } : { ...LATTE_VARS };
}
declare global {
interface Window {
MermaidRenderer?: MermaidApi;
@ -102,8 +223,18 @@ async function renderBlock(pre: HTMLPreElement, source: string, theme: ThemeName
const mermaid = await loadMermaid();
mermaid.initialize({
startOnLoad: false,
theme: theme === 'dark' ? 'dark' : 'default',
// base 主题不硬编码颜色,让 themeVariables 完全控制 Catppuccin 调色板。
theme: 'base',
darkMode: theme === 'dark',
securityLevel: 'strict',
// flowchart平滑曲线 + 边缘留白 + 缩放至容器宽度(配合 CSS 消除横向滚动)。
flowchart: {
curve: 'basis',
diagramPadding: 16,
useMaxWidth: true,
htmlLabels: true,
},
themeVariables: themeVarsFor(theme),
});
const id = `mermaid-svg-${++renderCounter}`;
const { svg } = await mermaid.render(id, source);