feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,208 @@
# Animations & Transitions
> 所有动画、过渡效果与运动设计清单。
---
## 1. CSS 变量控制的动画
| 变量 | 默认值 | 用途 |
|---|---|---|
| `--typewriter-fade-duration` | `900ms` | 流式打字机淡入时长 |
| `--typewriter-fade-ease` | `ease-out` | 流式打字机缓动 |
| `--stream-update-fade-duration` | 同上回退 | 流式内容更新淡入时长 |
| `--stream-update-fade-ease` | 同上回退 | 流式内容更新缓动 |
---
## 2. Keyframe 动画
### 2.1 流式内容淡入
**组件**:InlineCodeNode
```css
@keyframes inline-code-stream-update-fade-a { from { opacity: 0 } to { opacity: 1 } }
@keyframes inline-code-stream-update-fade-b { from { opacity: 0 } to { opacity: 1 } }
```
- 使用 a/b 交替触发避免重复动画不生效
- Duration:`var(--stream-update-fade-duration, var(--typewriter-fade-duration, 900ms))`
- Easing:`var(--stream-update-fade-ease, var(--typewriter-fade-ease, ease-out))`
- `will-change: opacity`
### 2.2 骨架屏 Shimmer
**组件**:CodeBlockNode, TableNode
```css
/* CodeBlockNode */
@keyframes code-skeleton-shimmer {
0% { background-position: -200% 0 }
100% { background-position: 200% 0 }
}
/* background: linear-gradient(90deg, rgba(0,0,0,0.04) 25%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.04) 75%) */
/* TableNode */
@keyframes table-node-shimmer {
0% { background-position: -200% 0 }
100% { background-position: 200% 0 }
}
```
### 2.3 Spinner 旋转
**组件**:MathBlockNode, TableNode, ImageNode
```css
/* MathBlockNode */
@keyframes math-spin {
to { transform: rotate(360deg) }
}
/* 0.8s linear infinite */
/* TableNode */
/* Tailwind animate-spin */
/* ImageNode */
/* Tailwind animate-spin */
```
### 2.4 链接脉冲下划线
**组件**:LinkNode
```css
@keyframes underlinePulse {
0%, 100% { opacity: var(--underline-rest-opacity, 0.18) }
50% { opacity: var(--underline-opacity, 0.35) }
}
/* Duration: var(--underline-duration, 1.6s) */
/* Timing: var(--underline-timing, ease-in-out) */
/* Iteration: var(--underline-iteration, infinite) */
```
---
## 3. CSS Transitions
### 3.1 通用过渡模式
| 组件 | 属性 | 时长 | 缓动 |
|---|---|---|---|
| CodeBlockNode | `height, max-height` | `180ms` | `ease` |
| MathBlockNode 渲染中 | `opacity` | `200ms` | `ease` |
| MermaidBlockNode 容器 | `height` | `150ms` | `ease-out` |
| Tooltip 位移 | `transform, box-shadow` | `220ms` | `cubic-bezier(.16,1,.3,1)` |
### 3.2 Vue Transition 组件
#### ImageNode — 图片切换
```
名称:img-switch
enter: opacity 220ms ease + translateY(6px → 0)
leave: opacity 220ms ease + translateY(0 → 6px)
```
#### MathBlockNode — 公式切换
```
名称:math-fade
enter: opacity 0.3s ease (0 → 1)
leave: opacity 0.3s ease (1 → 0)
```
#### TableNode — 表格淡入
```
名称:table-node-fade
enter: opacity 0.18s ease
leave: opacity 0.18s ease
```
#### Tooltip — 浮层出入
```
名称:tooltip
enter-from: opacity: 0, translateY(-6px) scale(0.98)
enter-to: opacity: 1, translateY(0) scale(1)
enter-active: opacity 120ms linear
leave-active: opacity 120ms linear
```
#### MermaidBlockNode — 弹窗
```
名称:mermaid-dialog
enter: opacity 200ms ease + scale(0.98 → 1.0)
leave: opacity 200ms ease + scale(1.0 → 0.98)
```
---
## 4. 交互反馈
### 4.1 按钮点击缩放
| 组件 | 选择器 | 效果 |
|---|---|---|
| CodeBlockNode | `.code-action-btn:active` | `transform: scale(0.98)` |
| MermaidBlockNode | `.mermaid-action-btn:active` | `transform: scale(0.98)` |
| AdmonitionNode | `.admonition-toggle` | `focus: outline 2px solid rgba(0,0,0,0.08)` |
### 4.2 禁用状态
| 组件 | 效果 |
|---|---|
| MermaidBlockNode 按钮 | `opacity: 0.5`, `cursor: not-allowed` |
---
## 5. 性能优化
### 5.1 `will-change` 使用
| 组件 | 元素 | `will-change` 值 |
|---|---|---|
| InlineCodeNode | `.inline-code-stream-delta` | `opacity` |
| LinkNode | `.link-loading-indicator` | `opacity` |
### 5.2 减少动效偏好
支持 `prefers-reduced-motion`:
```css
@media (prefers-reduced-motion: reduce) {
.inline-code-stream-delta { animation: none !important }
}
```
**组件**:InlineCodeNode, LinkNode
---
## 6. 动画时长速查表
| 速度等级 | 时长 | 用途 |
|---|---|---|
| 极快 | `120ms` | Tooltip 出入 |
| 快速 | `150ms` | Mermaid 容器高度 |
| 快速 | `180ms` | 代码块高度、表格淡入 |
| 标准 | `200ms` | Mermaid 弹窗、数学公式渲染 |
| 标准 | `220ms` | 图片切换、Tooltip 位移 |
| 慢速 | `300ms` | 数学公式切换 |
| 流式 | `900ms` | 打字机/流式更新淡入 |
| 脉冲 | `1600ms` | 链接加载指示器 |
---
## 7. 缓动函数速查
| 缓动 | 用途 |
|---|---|
| `ease` | 代码块高度、数学公式、图片 |
| `ease-out` | Mermaid 容器、打字机淡入 |
| `ease-in-out` | 链接脉冲 |
| `linear` | Spinner 旋转、Tooltip 透明度 |
| `cubic-bezier(.16,1,.3,1)` | Tooltip 位移(弹性过冲) |
@@ -0,0 +1,521 @@
# 样式架构方案
> 核心决策:**带默认样式 + CSS 变量全量可定制**,非 headless。
> shadcn 兼容:继承宿主变量,不覆盖;无宿主时回退到内置默认值。
---
## 设计原则
1. **开箱即用** — 引入即有完整合理的 Markdown 渲染样式
2. **变量换肤** — 用户只需覆盖 CSS 变量即可定制全部视觉表现
3. **零硬编码** — 组件内不允许出现硬编码的色值、间距等魔法数字
4. **Light/Dark 内建** — 两套主题开关切换,暗色模式完整覆盖所有组件
5. **不污染宿主** — 所有样式限定在 `.markstream-vue` 作用域内
6. **不覆盖宿主** — 不重定义 shadcn 同名变量,通过继承 + 回退兼容
---
## 变量不冲突策略
### 问题
如果我们在 `.markstream-vue` 上直接定义 `--background`,会覆盖宿主从 `:root` 继承下来的值:
```
宿主 :root { --background: 210 20% 98% } ← 用户自定义的暖灰
我们 .markstream-vue { --background: 0 0% 100% } ← 覆盖成了纯白 ❌
```
### 解决方案:Bridge Tokens
引入 `--ms-*` 前缀的桥接层,用 `var()` 继承宿主变量,未定义时回退到默认值:
```css
.markstream-vue {
--ms-background: var(--background, 0 0% 100%);
/* ↑ 宿主有则用宿主的 ↑ 没有则用回退值 */
}
```
**shadcn 宿主**:
```
:root { --background: 210 20% 98% } ← 宿主定义
└─ .markstream-vue 继承 --background = 210 20% 98%
└─ --ms-background: var(--background, 0 0% 100%)
= 210 20% 98% ✅ 宿主主题穿透生效
```
**独立使用(无 shadcn)**:
```
:root 未定义 --background
└─ .markstream-vue 未继承到 --background
└─ --ms-background: var(--background, 0 0% 100%)
= 0 0% 100% ✅ 回退值生效,开箱即用
```
---
## 分层架构
```
┌──────────────────────────────────────────────┐
│ Layer 4: Dark Theme │ 仅覆盖 Layer 1 bridge + ext 的回退值
│ .dark .markstream-vue { --ms-bg: var(...) } │
├──────────────────────────────────────────────┤
│ Layer 3: Component Styles │ 引用 Layer 2 变量,不允许硬编码
│ .heading { color: var(--blockquote-fg) } │
├──────────────────────────────────────────────┤
│ Layer 2: Semantic Tokens │ 组件级变量,引用 Layer 1
│ --code-bg: hsl(var(--ms-background)); │
├──────────────────────────────────────────────┤
│ Layer 1: Base Tokens │
│ ├─ Bridge (--ms-*): 继承 shadcn + 回退 │
│ └─ Extension (--*): 本项目独有 │
└──────────────────────────────────────────────┘
```
---
## Layer 1: Base Tokens
### 1.1 Bridge Tokens(`--ms-*`,继承 shadcn 同名变量)
> 内部桥接用。用户定制时直接覆盖 shadcn 变量名(如 `--background`)或覆盖 semantic token 即可。
#### Light(默认)
```css
.markstream-vue {
/* ── 背景 / 前景 ── */
--ms-background: var(--background, 0 0% 100%);
--ms-foreground: var(--foreground, 222.2 84% 4.9%);
/* ── 弱化 ── */
--ms-muted: var(--muted, 210 40% 96.1%);
--ms-muted-foreground: var(--muted-foreground, 215.4 16.3% 46.9%);
/* ── 次要 ── */
--ms-secondary: var(--secondary, 210 40% 96.1%);
--ms-secondary-foreground: var(--secondary-foreground, 222.2 47.4% 11.2%);
/* ── 强调 ── */
--ms-accent: var(--accent, 210 40% 96.1%);
--ms-accent-foreground: var(--accent-foreground, 222.2 47.4% 11.2%);
/* ── 主要操作 ── */
--ms-primary: var(--primary, 222.2 47.4% 11.2%);
--ms-primary-foreground: var(--primary-foreground, 210 40% 98%);
/* ── 危险 ── */
--ms-destructive: var(--destructive, 0 84.2% 60.2%);
--ms-destructive-foreground: var(--destructive-foreground, 210 40% 98%);
/* ── 边框 / Focus ── */
--ms-border: var(--border, 214.3 31.8% 91.4%);
--ms-ring: var(--ring, 222.2 84% 4.9%);
/* ── 弹出层 ── */
--ms-popover: var(--popover, 0 0% 100%);
--ms-popover-foreground: var(--popover-foreground, 222.2 84% 4.9%);
/* ── 圆角 ── */
--ms-radius: var(--radius, 0.5rem);
}
```
#### Dark
```css
.dark .markstream-vue,
.markstream-vue.dark {
--ms-background: var(--background, 222.2 84% 4.9%);
--ms-foreground: var(--foreground, 210 40% 98%);
--ms-muted: var(--muted, 217.2 32.6% 17.5%);
--ms-muted-foreground: var(--muted-foreground, 215 20.2% 65.1%);
--ms-secondary: var(--secondary, 217.2 32.6% 17.5%);
--ms-secondary-foreground: var(--secondary-foreground, 210 40% 98%);
--ms-accent: var(--accent, 217.2 32.6% 17.5%);
--ms-accent-foreground: var(--accent-foreground, 210 40% 98%);
--ms-primary: var(--primary, 210 40% 98%);
--ms-primary-foreground: var(--primary-foreground, 222.2 47.4% 11.2%);
--ms-destructive: var(--destructive, 0 62.8% 30.6%);
--ms-destructive-foreground: var(--destructive-foreground, 210 40% 98%);
--ms-border: var(--border, 217.2 32.6% 17.5%);
--ms-ring: var(--ring, 212.7 26.8% 83.9%);
--ms-popover: var(--popover, 222.2 84% 4.9%);
--ms-popover-foreground: var(--popover-foreground, 210 40% 98%);
}
```
**工作原理**:
- shadcn 宿主有 `.dark { --background: X }` → `var(--background, fallback)` 取 X → 宿主暗色穿透
- 独立使用,无 `--background` → 取 fallback 中的暗色值 → 内置暗色生效
### 1.2 Extension Tokens(本项目独有,无冲突风险)
> 这些变量名 shadcn 不存在,直接定义即可。
#### Light
```css
.markstream-vue {
/* ── 语义色 ── */
--ms-info: 217 91% 60%; /* 信息/备注 (#448aff) */
--ms-info-foreground: 210 40% 98%;
--ms-success: 168 100% 37.5%; /* 成功/提示 (#00bfa5) */
--ms-success-foreground: 210 40% 98%;
--ms-warning: 34 100% 50%; /* 警告 (#ff9100) */
--ms-warning-foreground: 0 0% 100%;
/* ── Diff ── */
--ms-diff-added: 174 60% 51%; /* 新增 (#14b8a6) */
--ms-diff-removed: 350 100% 60%; /* 删除 (#ff3658) */
/* ── 其他 ── */
--ms-highlight: 54 100% 62%; /* 文本高亮 */
--ms-highlight-foreground: 0 0% 0%;
--ms-link: 212 100% 42%; /* 链接色 (#0366d6) */
/* ── 排版 ── */
--ms-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--ms-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
```
#### Dark
```css
.dark .markstream-vue,
.markstream-vue.dark {
--ms-diff-added: 174 72% 70%; /* 暗色更亮 (#5eead4) */
--ms-diff-removed: 0 92% 82%; /* 暗色更柔 (#fda4af) */
--ms-highlight: 54 80% 42%; /* 暗色降低亮度 */
--ms-highlight-foreground: 0 0% 100%;
--ms-link: 213 94% 68%; /* 暗色更亮链接 */
/* info / success / warning 在暗色下保持不变,仍然醒目 */
}
```
### 1.3 圆角推算规则
```
sm = calc(var(--ms-radius) - 2px) → 6px
base = var(--ms-radius) → 8px (0.5rem)
md = calc(var(--ms-radius) + 2px) → 10px
lg = calc(var(--ms-radius) + 4px) → 12px
full = 9999px
```
---
## Layer 2: Semantic Tokens
全部引用 Layer 1 的 `--ms-*` 变量。Dark 模式下 Layer 1 变化 → Layer 2 自动跟随,无需重复定义。
```css
.markstream-vue {
/* ── 行内代码 ── */
--inline-code-bg: hsl(var(--ms-secondary));
--inline-code-fg: hsl(var(--ms-foreground));
--inline-code-border: hsl(var(--ms-border) / 0.9);
/* ── 代码块 ── */
--code-bg: hsl(var(--ms-background));
--code-fg: hsl(var(--ms-foreground));
--code-border: hsl(var(--ms-border));
--code-header-bg: hsl(var(--ms-muted));
--code-selection-bg: hsl(var(--ms-accent) / 0.3);
--code-line-number: hsl(var(--ms-muted-foreground));
/* ── 代码块操作栏 ── */
--code-action-fg: hsl(var(--ms-muted-foreground));
--code-action-hover-bg: hsl(var(--ms-accent));
--code-action-hover-fg: hsl(var(--ms-accent-foreground));
--code-action-active-bg: hsl(var(--ms-primary));
--code-action-active-fg: hsl(var(--ms-primary-foreground));
/* ── Diff ── */
--diff-added-fg: hsl(var(--ms-diff-added));
--diff-removed-fg: hsl(var(--ms-diff-removed));
--diff-added-bg: hsl(var(--ms-diff-added) / 0.1);
--diff-added-inline-bg: hsl(var(--ms-diff-added) / 0.2);
--diff-removed-bg: hsl(var(--ms-diff-removed) / 0.1);
--diff-removed-inline-bg: hsl(var(--ms-diff-removed) / 0.2);
/* ── 引用块 ── */
--blockquote-border: hsl(var(--ms-border));
--blockquote-fg: hsl(var(--ms-foreground));
/* ── 提示框 (Admonition) ── */
--admonition-bg: hsl(var(--ms-muted));
--admonition-border: hsl(var(--ms-border));
--admonition-fg: hsl(var(--ms-foreground));
--admonition-muted: hsl(var(--ms-muted-foreground));
--admonition-header-bg: hsl(var(--ms-muted) / 0.5);
--admonition-note: hsl(var(--ms-info));
--admonition-tip: hsl(var(--ms-success));
--admonition-warning: hsl(var(--ms-warning));
--admonition-danger: hsl(var(--ms-destructive));
/* ── 表格 ── */
--table-border: hsl(var(--ms-border));
--table-header-bg: hsl(var(--ms-muted));
--table-header-fg: hsl(var(--ms-foreground));
--table-row-hover-bg: hsl(var(--ms-accent) / 0.5);
/* ── 链接 ── */
--link-color: hsl(var(--ms-link));
/* ── 列表 ── */
--list-marker: hsl(var(--ms-border));
--list-counter-marker: hsl(var(--ms-muted-foreground));
/* ── 分割线 ── */
--hr-border: hsl(var(--ms-border));
/* ── 高亮 ── */
--highlight-bg: hsl(var(--ms-highlight));
--highlight-fg: hsl(var(--ms-highlight-foreground));
/* ── 脚注 ── */
--footnote-border: hsl(var(--ms-border));
/* ── Tooltip ── */
--tooltip-bg: hsl(var(--ms-popover));
--tooltip-fg: hsl(var(--ms-popover-foreground));
--tooltip-border: hsl(var(--ms-border));
/* ── Modal ── */
--modal-overlay: hsl(0 0% 0% / 0.7);
--modal-bg: hsl(var(--ms-popover));
--modal-fg: hsl(var(--ms-popover-foreground));
/* ── 图表容器 (Mermaid / D2 / Infographic) ── */
--diagram-bg: hsl(var(--ms-muted));
--diagram-border: hsl(var(--ms-border));
--diagram-header-bg: hsl(var(--ms-muted));
/* ── 数学公式 ── */
--math-fg: hsl(var(--ms-foreground));
/* ── 加载态 ── */
--loading-spinner: hsl(var(--ms-muted-foreground));
--loading-shimmer: hsl(var(--ms-muted) / 0.5);
/* ── 图片 ── */
--image-placeholder-bg: hsl(var(--ms-muted));
/* ── Focus ring ── */
--focus-ring: hsl(var(--ms-ring));
}
```
**Dark 模式下无需重复定义 Layer 2** — 因为 Layer 1 的 `--ms-*` 值变了,这里自动跟随。
---
## Layer 3: Component Styles
组件中只引用 Layer 2 语义变量:
```css
/* ❌ 之前 */
.blockquote { border-left: 0.25rem solid #e2e8f0; }
.admonition-note { border-left-color: #448aff; }
.highlight-node { background-color: #ffff00; }
/* ✅ 之后 */
.blockquote { border-left: 0.25rem solid var(--blockquote-border); }
.admonition-note { border-left-color: var(--admonition-note); }
.highlight-node { background: var(--highlight-bg); }
```
---
## 暗色模式统一方案
**去掉组件级 `.is-dark` CSS**,统一为:
```
之前:isDark prop → 每个组件 .is-dark → 30+ 组件各自覆盖 CSS 变量
之后:.dark 类 → Layer 1 bridge 回退值切换 → Layer 2 自动跟随 → 全部组件适配
```
**保留 `isDark` prop** 仅用于 JS 逻辑(不用于 CSS):
- Monaco Editor 主题名切换
- Mermaid 主题名切换
- `prefers-color-scheme` 检测兜底
### CodeBlockNode `.is-dark` 例外
CodeBlockNode 保留 `.is-dark` CSS 块。此处 `.is-dark` 的语义是**"编辑器表面为暗色"**
(由 Monaco 主题检测 `resolvedSurfaceIsDark` 决定),与页面级 `.dark` 无关。
- 一个亮色页面上可以出现暗色主题的代码块
- `.is-dark` 块包含 ~15 个 diff 视图的阴影/透明度/渐变差异值
- 这些值已全部引用 `--ms-*` 语义令牌,只是 opacity 系数不同
### Admonition header-bg 例外
提示框标题背景需要在 dark 下使用更高透明度(6% → 12%),因此
`--admonition-*-header-bg` 在 `.dark` 区块中重新定义。这是 Layer 2 中
唯一需要 dark 覆盖的语义令牌。
---
## shadcn 兼容矩阵
| 变量来源 | 我们的处理 | 用户如何定制 |
|---|---|---|
| shadcn 同名(`--background` 等 14 个) | `--ms-*` 桥接继承 + 回退 | 在 `:root` 设置 shadcn 变量,自动穿透 |
| 本项目独有(`--ms-info` 等 8 个) | 直接定义在 `.markstream-vue` | 在 `.markstream-vue` 上覆盖 |
| 组件语义(`--code-bg` 等 ~35 个) | 引用 `--ms-*`,定义一次 | 在 `.markstream-vue` 上覆盖 |
### shadcn 未采用(本项目不需要)
| Token | 原因 |
|---|---|
| `--card` / `--card-foreground` | 无 card 组件 |
| `--input` | 无输入框 |
| `--sidebar-*` | 无侧边栏 |
| `--chart-*` | Mermaid 自带主题 |
### 本项目扩展(shadcn 没有)
| Token | 用途 |
|---|---|
| `--ms-info` / `--ms-success` / `--ms-warning` | Admonition 语义色 |
| `--ms-diff-added` / `--ms-diff-removed` | Diff 视图 |
| `--ms-highlight` / `--ms-highlight-foreground` | `==高亮==` 语法 |
| `--ms-link` | 链接色 |
---
## 当前控件清单
| 控件 | 出现组件 | 涉及 tokens |
|---|---|---|
| 操作按钮(复制/折叠/全屏/导出) | CodeBlock, Mermaid, D2, Infographic, Admonition | `--code-action-*`, `--focus-ring` |
| 字号调节按钮组 | CodeBlock, MarkdownCodeBlock | `--code-action-*` |
| 模式切换(Preview/Source) | CodeBlock, Mermaid, D2, Infographic | `--code-action-active-*` |
| 缩放控制 | Mermaid, Infographic | `--code-action-*` |
| Tooltip | 全局 | `--tooltip-*` |
| Modal | CodeBlock, Mermaid, Infographic | `--modal-*` |
| Checkbox(只读) | CheckboxNode | `--ms-foreground` |
| Spinner | Image, Table, MathBlock | `--loading-spinner` |
| Shimmer 骨架屏 | CodeBlock, Table | `--loading-shimmer` |
| 错误态 | ImageNode | `--ms-destructive` |
| 可点击引用 | FootnoteRef, ReferenceNode | `--link-color` |
---
## 用户定制示例
### 场景 1:shadcn 项目(零配置接入)
```js
// 只需引入样式,shadcn 变量自动穿透
import 'markstream-vue/dist/index.css'
```
宿主 `:root` 上的 `--background`、`--foreground` 等自动继承进 `.markstream-vue`。
如需补充 Markdown 扩展色:
```css
.markstream-vue {
--ms-info: 217 91% 60%;
--ms-success: 142 71% 45%;
--ms-link: 221 83% 53%;
}
```
### 场景 2:独立项目,换整体配色
```css
/* 方式 A:覆盖 bridge 识别的 shadcn 变量名 */
:root {
--background: 0 0% 98%;
--foreground: 240 10% 10%;
--border: 240 5% 85%;
}
/* 方式 B:直接覆盖 --ms-* bridge token */
.markstream-vue {
--ms-background: 0 0% 98%;
--ms-foreground: 240 10% 10%;
--ms-border: 240 5% 85%;
}
```
### 场景 3:只改代码块(覆盖 semantic token)
```css
.markstream-vue {
--code-bg: #1e1e2e;
--code-fg: #cdd6f4;
--code-border: transparent;
}
```
### 场景 4:只改提示框色彩
```css
.markstream-vue {
--ms-info: 221 83% 53%;
--ms-success: 142 71% 45%;
--ms-warning: 38 92% 50%;
--ms-destructive: 0 72% 51%;
}
```
---
## 已知边界情况
**shadcn 宿主 + 仅对 markstream-vue 启用 dark**:
```html
<!-- 宿主全局 light,但 markstream-vue 想用 dark -->
<div class="markstream-vue dark">...</div>
```
此时宿主的 light `--background` 仍会继承进来,bridge 的 dark 回退值不会生效。
**解决**:shadcn 宿主应在文档级切换 `.dark`,或通过覆盖 semantic token 单独控制。
---
## 迁移策略
分步进行,每步可独立验证:
| 阶段 | 内容 | 影响范围 |
|---|---|---|
| 1 | 定义 Bridge Tokens + Extension Tokens(Layer 1) | 仅新增变量,不破坏现有 |
| 2 | 定义 Semantic Tokens(Layer 2),引用 `--ms-*` | 仅新增变量,不破坏现有 |
| 3 | 逐组件替换硬编码值为 Semantic Token 引用 | 逐组件改,可逐个验证 |
| 4 | 补全 Dark 的 Bridge Token 回退值 | 暗色模式完整 |
| 5 | 移除组件内 `.is-dark` CSS 覆盖 | 简化暗色逻辑 |
| 6 | 清理:删除废弃变量、统一动画变量 | 最终打磨 |
@@ -0,0 +1,347 @@
# CodeBlock 主题架构
> 核心决策:Shell(外壳)与内容渲染器**严格分离**。Shell 走 token,渲染器各自管理。
---
## 整体架构
### Shell + Renderer 分离
```
内置模式(自动套 Shell):
┌─ CodeBlockShell ──────────────────────────┐
│ header: 标题、语言标签、操作按钮 │ ← token 驱动
│ border / shadow / 圆角 │
│ 折叠/展开 │
│ 骨架屏 │
│ │
│ ┌─ Renderer(可替换) ─────────────────┐ │
│ │ Monaco / Shiki / <pre> │ │ ← 渲染器自治
│ └─────────────────────────────────────┘ │
└────────────────────────────────────────────┘
自定义模式(无 Shell,用户全权控制):
┌─ UserComponent ────────────────────────────┐
│ 用户自己实现全部 UI │
└────────────────────────────────────────────┘
```
### 三档内置渲染器
| 模式 | 渲染器 | Shell | 特点 | 选择方式 |
|---|---|---|---|---|
| Monaco | CodeBlockNode | **有** | 完整编辑器,语法高亮,diff | 默认 |
| Shiki | MarkdownCodeBlockNode | **有** | 轻量高亮,无编辑器,适合大量只读展示 | `setCustomComponents` 注册 |
| Pre | PreCodeNode | **无** | 纯 `<pre><code>`,无高亮,超轻量 | `renderCodeBlocksAsPre: true` |
CodeBlockNode 和 MarkdownCodeBlockNode 共享 `CodeBlockShell`(header、操作按钮、骨架屏、折叠)。
PreCodeNode 有意不套 Shell — 它是超轻量选项,不需要 header/按钮。需要这些功能应使用 Shiki 或 Monaco 模式。
### 自定义组件
通过 `setCustomComponents({ code_block: UserComponent })` 注册的组件:
- **不套 Shell**,直接渲染
- 用户全权控制 UI,库不干预
- 可选择通过 slot 或 composable 自行引入 Shell 能力(如果需要)
---
## Shell 与 Monaco 渲染器的边界
以下仅针对 Monaco 渲染器(CodeBlockNode)的内部架构:
```
┌─ Shell(token 驱动)─────────────────────────┐
│ header / 操作按钮 / 骨架屏 / diff 外框 │
│ │
│ ┌─ Monaco 渲染器(Monaco 主题自治)────────┐ │
│ │ 编辑器背景/前景、语法高亮 │ │
│ │ 行号、选区背景 │ │
│ │ diff 行背景、内联高亮、gutter 标记 │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
```
---
## 边界规则(每个元素只归一个区域)
### Shell 区域 — 页面主题 token
| 元素 | token | 说明 |
|---|---|---|
| 容器边框 | `var(--code-border)` | |
| **Header 背景** | `var(--code-header-bg)` | **改动:不再使用 `--vscode-editor-background`** |
| **Header 文字** | `var(--code-fg)` | **改动:不再使用 `--vscode-editor-foreground`** |
| 操作按钮默认色 | `var(--code-action-fg)` | |
| 操作按钮 hover | `var(--code-action-hover-bg/fg)` | **改动:不再使用 `--vscode-editor-selectionBackground`** |
| 操作按钮 active | `var(--code-action-active-bg/fg)` | 模式切换活跃态 |
| 骨架屏 | `var(--loading-shimmer)` | **改动:不再受 `.is-dark` 影响** |
| Diff 外框/阴影 | `var(--code-border)` 等 | |
| Diff 标题栏 | `var(--code-header-bg)` | |
**规则**:Shell 元素只用 `var(--code-*)` 和 `var(--ms-*)` token。不引用任何 `--vscode-*` 变量。
跟随页面 `.dark` 自动切换,无需 `.is-dark`。
### Monaco 区域 — Monaco 主题自治
| 元素 | 取值来源 | 说明 |
|---|---|---|
| 编辑器背景/前景 | Monaco theme | `--vscode-editor-background/foreground` |
| 语法高亮色 | Monaco theme | 完全由 Monaco token colors 决定 |
| 行号色 | Monaco theme | `--vscode-editorLineNumber-foreground` |
| 选区背景 | Monaco theme | `--vscode-editor-selectionBackground` |
| Diff 行背景 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
| Diff 内联高亮 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
| Gutter 标记 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
**规则**:Monaco 区域由 Monaco 主题 + `.is-dark` class 驱动。diff 基色(色相)仍来自全局
`--ms-diff-added` / `--ms-diff-removed`,但 opacity 和阴影深度由 `.is-dark` 控制。
---
## Monaco 主题 Props 设计
### 统一为单个 `theme` prop
取代现有的 `darkTheme` + `lightTheme` + 新增的 `forceTheme` 三 prop 设计,
合并为一个灵活的 `theme` prop:
```ts
type CodeBlockTheme = string | MonacoThemeObject
interface CodeBlockProps {
/**
* Monaco 主题配置。
*
* - 字符串 / 对象:固定主题,不随页面明暗切换
* theme="monokai"
* theme={...customThemeObject}
*
* - { light, dark } 配对:随页面明暗自动切换
* theme={{ light: 'github-light', dark: 'one-dark-pro' }}
*
* - 未指定:使用内置默认 light/dark 主题
*/
theme?: CodeBlockTheme | { light: CodeBlockTheme, dark: CodeBlockTheme }
}
```
### 切换逻辑
```ts
const activeTheme = computed(() => {
const t = props.theme
if (!t) {
// 未指定:内置默认
return isDark.value ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME
}
if (typeof t === 'string' || isThemeObject(t)) {
// 单主题:固定使用,不切换
return t
}
// 配对:按页面明暗切换
return isDark.value ? t.dark : t.light
})
```
### 用法示例
```html
<!-- 自动切换(大多数场景) -->
<CodeBlock :theme="{ light: 'github-light', dark: 'one-dark-pro' }" />
<!-- 固定主题(不随页面切换) -->
<CodeBlock theme="monokai" />
<!-- 默认(内置 light/dark) -->
<CodeBlock />
```
### 向后兼容
保留 `darkTheme` / `lightTheme` prop 作为 deprecated alias:
```ts
const resolvedThemeProp = computed(() => {
if (props.theme)
return props.theme
// 向后兼容
if (props.darkTheme || props.lightTheme) {
return { light: props.lightTheme ?? DEFAULT_LIGHT, dark: props.darkTheme ?? DEFAULT_DARK }
}
return undefined
})
```
---
## `.is-dark` 的角色
### 触发规则
```ts
const editorSurfaceIsDark = computed(() => {
const t = activeTheme.value
// 配对模式:跟随页面
if (!props.theme || isPairedTheme(props.theme))
return isDark.value
// 单主题模式:检测主题名/亮度
return detectThemeDarkness(t)
})
```
### 检测主题明暗
```ts
function detectThemeDarkness(theme: CodeBlockTheme): boolean {
if (typeof theme === 'string')
return themeLooksDark(theme)
// 主题对象:检测 background 色的亮度
if (theme.colors?.['editor.background']) {
return getLuminance(theme.colors['editor.background']) < 128
}
// 兜底
return isDark.value
}
```
### `.is-dark` 只作用于 Monaco 区域
`.is-dark` class **只影响**编辑器内部和 diff 行/gutter 的 opacity 系数。
Shell 元素(header、按钮、边框、骨架屏)**完全不受** `.is-dark` 影响。
---
## Diff stage 背景层
**决策:移除 `--markstream-diff-stage-bg` 渐变,直接使用编辑器背景。**
```css
/* 之前 */
.code-block-container.is-diff .code-editor-layer {
background: var(--markstream-diff-stage-bg); /* 复杂渐变 */
}
/* 之后 */
.code-block-container.is-diff .code-editor-layer {
background: transparent; /* Monaco 自己控制背景 */
}
```
简化层级,避免 Shell token 渐变和 Monaco 背景色叠加不协调。
---
## `--markstream-diff-*` 变量归属
### 归 Shell(迁移到 token / 删除)
| 变量 | 处理 |
|---|---|
| `--markstream-diff-frame-border` | → `var(--code-border)` |
| `--markstream-diff-frame-shadow` | → 基于 `hsl(var(--ms-foreground) / ...)` |
| `--markstream-diff-shell-*` | → token 化 |
| `--markstream-diff-header-border` | → `var(--code-border)` |
| `--markstream-diff-stage-bg` | → **删除**(stage 背景移除) |
| `--markstream-diff-focus` | → `var(--focus-ring)` |
| `--markstream-diff-action-hover` | → `var(--code-action-hover-bg)` |
| `--markstream-diff-panel-bg*` | → token 化 |
| `--markstream-diff-panel-border` | → token 化 |
| `--markstream-diff-pane-divider` | → token 化 |
| `--markstream-diff-widget-shadow` | → 基于 `hsl(var(--ms-foreground) / ...)` |
### 归 Monaco(保留组件内,跟随 `.is-dark`)
| 变量 | 原因 |
|---|---|
| `--markstream-diff-editor-bg/fg` | 匹配 Monaco 表面 |
| `--markstream-diff-added-fg/bg` | 匹配编辑器表面亮度 |
| `--markstream-diff-removed-fg/bg` | 匹配编辑器表面亮度 |
| `--markstream-diff-added/removed-inline` | 匹配编辑器表面亮度 |
| `--markstream-diff-added/removed-gutter` | 匹配编辑器表面亮度 |
| `--markstream-diff-line-number*` | 匹配 Monaco 行号色 |
| `--markstream-diff-unchanged-*` | 匹配编辑器表面 |
| `--markstream-diff-gutter-*` | 匹配编辑器表面 |
| `--markstream-diff-added/removed-line-fill` | 匹配编辑器表面亮度 |
Diff 基色仍引用全局 token:`--ms-diff-added` / `--ms-diff-removed`。
---
## MarkdownCodeBlockNode 处理
### 现状
- 默认不使用,仅在用户通过 `setCustomComponents` 注册时生效
- 用 Shiki 渲染语法高亮(比 Monaco 轻量),不加载编辑器
- Shell 部分(header、操作按钮、骨架屏)与 CodeBlockNode **大量重复**
- 已作为公开 API 导出
### 决策:保留,接入共享 Shell
Shiki 版本服务于**大量只读代码展示**场景(如文档站、博客、教程),
这类场景下加载 Monaco 编辑器不合理(包体积大、初始化慢、无需编辑功能)。
**重构方向**:所有内置渲染器接入共享 Shell
```
CodeBlockShell(共享)
├─ header(标题、语言图标、操作按钮)
├─ 边框 / 圆角 / 阴影
├─ 骨架屏
├─ 折叠/展开
└─ token 体系接入
└─ slot: 内容区域 → 由渲染器填充
三档内置渲染器:
Shell + CodeBlockNode(Monaco) ← 默认
Shell + MarkdownCodeBlockNode(Shiki) ← 轻量高亮
Shell + PreCodeNode(<pre>) ← 纯文本
自定义组件(setCustomComponents):
→ 不套 Shell,直接渲染,用户全权控制
```
步骤:
1. 抽取 Shell 为共享子组件(header、按钮、骨架屏、折叠逻辑)
2. CodeBlockNode、MarkdownCodeBlockNode、PreCodeNode 各自作为 Shell 的内容渲染器
3. 三者的 `theme` prop 设计保持一致(字符串 = 固定,`{ light, dark }` = 配对)
4. NodeRenderer 路由逻辑:内置组件走 Shell 包裹,自定义组件直接渲染
5. MarkdownCodeBlockNode 的 Shiki 主题切换与 CodeBlockNode 的 Monaco 主题切换对齐
---
## 已识别的边界问题及处理
| 问题 | 决策 |
|---|---|
| Header 当前用 `--vscode-*` 颜色 | **改为 token**。Header 归 Shell,用 `--code-header-bg/fg` |
| 操作按钮 hover 用 `--vscode-editor-selectionBackground` | **改为 token**。用 `--code-action-hover-bg` |
| 骨架屏受 `.is-dark` 影响 | **改为纯 token**。骨架屏归 Shell,不看 `.is-dark` |
| 折叠时 CSS 变量仍活跃 | **可接受**。`v-show` 隐藏但变量不影响其他元素 |
| `resolvedChromeIsDark` 命名 | **重命名为 `editorSurfaceIsDark`** |
| 主题名字符串检测不精确 | **增加对象主题亮度检测**(读取 `editor.background`) |
| `syncEditorCssVars` 跨边界同步 | **限制范围**。仅同步到 Monaco 容器内,不上浮到根元素 |
| Diff stage 渐变背景 | **删除**。让 Monaco 自己控制背景 |
---
## 实施步骤
| 阶段 | 内容 |
|---|---|
| 1 | **Header 归 Shell**:移除 `headerStyle` 中的 `--vscode-*` 引用,改用 token |
| 2 | **按钮归 Shell**:hover 改用 `--code-action-hover-bg`,移除 `--vscode-*` |
| 3 | **骨架屏归 Shell**:移除 `.is-dark .skeleton-line` 规则 |
| 4 | **Diff stage 简化**:移除 `--markstream-diff-stage-bg` 渐变 |
| 5 | **Shell diff 变量 token 化**:将 Shell 归属变量替换为 token 引用 |
| 6 | **限制 `syncEditorCssVars`**:不再将 `--vscode-*` 同步到根容器 |
| 7 | **实现 `theme` prop**:统一 API,向后兼容 `darkTheme`/`lightTheme` |
| 8 | **重命名 `.is-dark` 推导**:`resolvedChromeIsDark` → `editorSurfaceIsDark`,增加对象主题检测 |
| 9 | **抽取共享 Shell**:CodeBlockShell.vue — header / 按钮 / 骨架屏 / 折叠,内容区域用 slot |
| 10 | **CodeBlockNode + MarkdownCodeBlockNode 接入 Shell**:PreCodeNode 不套 Shell(超轻量设计意图) |
| 11 | NodeRenderer 路由不变 — Shell 内嵌在各组件中,自定义组件直接渲染 |
@@ -0,0 +1,398 @@
# Component Style Inventory
> 每个组件的当前样式详情,按功能分类。
> 来源:`src/components/` 下各 `.vue` 文件的 `<style>` 块。
---
## 1. 文本类组件
### 1.1 HeadingNode — 标题
**文件**:`src/components/HeadingNode/HeadingNode.vue`
| 级别 | 类名 | 字号 | 字重 | 行高 | 上下边距 |
|---|---|---|---|---|---|
| h1 | `.heading-1` | `text-4xl` | `extrabold` | `calc(10/9*1)` | `0` / `calc(8/9*1em)` |
| h2 | `.heading-2` | `text-2xl` | `bold` | `calc(4/3*1)` | `2rem` / `1rem` |
| h3 | `.heading-3` | `text-xl` | `semibold` | `calc(5/3*1)` | `calc(8/5*1em)` / `calc(3/5*1em)` |
| h4 | `.heading-4` | `text-base` | `semibold` | 默认 | `1.5rem` / `0.5rem` |
| h5 | `.heading-5` | `text-base` | 默认 | 默认 | `0` / `0` |
| h6 | `.heading-6` | `text-base` | 默认 | 默认 | `0` / `0` |
- 通用类 `.heading-node`:`font-medium leading-tight`
- 特殊规则:`hr + .heading-node` 取消 `margin-top`
---
### 1.2 ParagraphNode — 段落
**文件**:`src/components/ParagraphNode/ParagraphNode.vue`
- `.paragraph-node`:`margin: 1.25em 0`
- 列表内嵌套时:`li .paragraph-node { margin: 0 }`
---
### 1.3 StrongNode / EmphasisNode — 加粗与斜体
- 无自定义样式,继承 HTML 默认 `<strong>` / `<em>` 行为
---
### 1.4 StrikethroughNode — 删除线
**文件**:`src/components/StrikethroughNode/StrikethroughNode.vue`
- `.strikethrough-node`:`text-decoration: line-through`
---
### 1.5 HighlightNode — 高亮
**文件**:`src/components/HighlightNode/HighlightNode.vue`
- `.highlight-node`:`background-color: #ffff00`,`padding: 0 0.2rem`
- **待改进**:仅有亮黄色,无暗色模式适配
---
### 1.6 SubscriptNode / SuperscriptNode — 上下标
- 无自定义样式,使用 HTML 原生 `<sub>` / `<sup>`
---
### 1.7 InsertNode — 插入标记
- 使用 HTML 原生 `<ins>` 样式
---
### 1.8 EmojiNode — 表情
**文件**:`src/components/EmojiNode/EmojiNode.vue`
- `.emoji-node`:`display: inline-block`
---
## 2. 链接与引用
### 2.1 LinkNode — 链接
**文件**:`src/components/LinkNode/LinkNode.vue`
| 属性 | 值 |
|---|---|
| 颜色 | `var(--link-color, #0366d6)` |
| 装饰 | 默认无下划线,hover 显示 |
| hover 效果 | `text-decoration: underline`,`text-underline-offset: 0.2rem` |
| 加载态 | 脉冲下划线动画(`underlinePulse` keyframe) |
CSS 变量(可自定义):
```
--link-color, --underline-height, --underline-bottom,
--underline-opacity, --underline-rest-opacity,
--underline-duration, --underline-timing, --underline-iteration
```
---
### 2.2 FootnoteNode — 脚注
**文件**:`src/components/FootnoteNode/FootnoteNode.vue`
- 容器:`flex mt-2 mb-2 text-sm leading-relaxed`
- 上方分隔线:`border-t border-[#eaecef] pt-2`
### 2.3 FootnoteAnchorNode / FootnoteReferenceNode
- 上标链接样式,无复杂自定义
---
## 3. 代码类组件
### 3.1 InlineCodeNode — 行内代码
**文件**:`src/components/InlineCodeNode/InlineCodeNode.vue`
| 属性 | 值 |
|---|---|
| 字号 | `85%` |
| 字体 | `font-mono` |
| 颜色 | `hsl(var(--foreground))` |
| 背景 | `hsl(var(--secondary))` |
| 边框 | `1px solid hsl(var(--border) / 0.9)` |
| 内边距 | `px-1 py-0.5` |
| 圆角 | `rounded` |
| 换行 | `whitespace-normal break-words` |
流式更新动画:`inline-code-stream-update-fade-a/b`,duration 900ms
---
### 3.2 CodeBlockNode — 代码块
**文件**:`src/components/CodeBlockNode/CodeBlockNode.vue`(~2700 行)
**容器**:
- `contain: content`,`content-visibility: auto`
- `container-type: inline-size`(支持 Container Queries)
- `contain-intrinsic-size: 320px 180px`
**Header**:
- `gap: 16px`
- 标题:`font-size: 14px`,`font-weight: 650`,`letter-spacing: 0.01em`
- 说明:`font-size: 12px`,62% 透明度
- 操作按钮:`gap: 8px`,hover 态 `scale(0.98)`
**代码编辑器区域**:
- 使用 Monaco Editor / Shiki 高亮
- 主题可通过 `darkTheme` / `lightTheme` prop 切换
- 大量 `--markstream-code-*` 和 `--markstream-diff-*` CSS 变量(详见 tokens.md)
**加载骨架屏**:shimmer 动画,`linear-gradient(90deg, ...)` 背景滑动
**Diff 视图**:完整的增删行/内联 diff 颜色系统,含 Light/Dark 两套
---
### 3.3 PreCodeNode
**文件**:`src/components/PreCodeNode/PreCodeNode.vue`
- 简化版代码块,用于无高亮场景
---
### 3.4 MarkdownCodeBlockNode
- Markdown 渲染的代码块封装
---
## 4. 容器类组件
### 4.1 BlockquoteNode — 引用块
**文件**:`src/components/BlockquoteNode/BlockquoteNode.vue`
| 属性 | 值 |
|---|---|
| 字重 | `500` |
| 字体样式 | `italic` |
| 左边框 | `0.25rem solid var(--blockquote-border-color, #e2e8f0)` |
| 左内边距 | `1em` |
| 上下边距 | `1.6em` |
| 引号字符 | `"\201C" "\201D" "\2018" "\2019"` |
---
### 4.2 AdmonitionNode — 提示框
**文件**:`src/components/AdmonitionNode/AdmonitionNode.vue`
**基础样式**:
- 左边框:`4px solid`
- 圆角:`4px`
- 无 `overflow: hidden`
**类型色彩**:
| 类型 | 边框色 | 标题背景 |
|---|---|---|
| note / info | `#448aff` | `rgba(68, 138, 255, 0.06)` |
| tip | `#00bfa5` | `rgba(0, 191, 165, 0.06)` |
| warning / caution | `#ff9100` | `rgba(255, 145, 0, 0.06)` |
| danger / error | `#ff5252` | `rgba(255, 82, 82, 0.06)` |
**Dark 模式**:背景透明度从 6% 提升到 12%
**标题栏**:`padding: 0.5rem 1rem`,`font-weight: 600`,带图标
**内容区**:`padding: 0.5rem 1rem 1rem`
**折叠按钮**:`border-radius: 4px`,`focus: outline 2px`
---
### 4.3 VmrContainerNode — 通用容器
- 自定义容器节点封装
---
## 5. 列表类组件
### 5.1 ListNode — 列表
**文件**:`src/components/ListNode/ListNode.vue`
- `.list-node`:`my-5 pl-[calc(13/8*1em)]`
- `.list-decimal`:`list-style-type: decimal`
- `.list-disc`:`list-style-type: disc`
- 移动端:`max-lg:my-[calc(4/3*1em)] max-lg:pl-[calc(14/9*1em)]`
### 5.2 ListItemNode — 列表项
**文件**:`src/components/ListItemNode/ListItemNode.vue`
- `.list-item`:`pl-1.5 my-2`,`dir="auto"`
- 有序列表标记色:`var(--list-item-counter-marker, #64748b)`,`line-height: 1.6`
- 无序列表标记色:`var(--list-item-marker, #cbd5e1)`
### 5.3 CheckboxNode — 复选框
**文件**:`src/components/CheckboxNode/CheckboxNode.vue`
- `.checkbox-node`:`margin-right: 0.5rem`,`vertical-align: middle`
- `.checkbox-input`:`margin: 0`,`cursor: default`
---
### 5.4 DefinitionListNode — 定义列表
**文件**:`src/components/DefinitionListNode/DefinitionListNode.vue`
- `.definition-list`:`margin: 0 0 1rem`
- `.definition-term`:`font-weight: 600`,`margin-top: 0.5rem`
- `.definition-desc`:`margin-left: 1rem`,`margin-bottom: 0.5rem`
---
## 6. 表格组件
### 6.1 TableNode — 表格
**文件**:`src/components/TableNode/TableNode.vue`
**容器**:
- `.table-node-wrapper`:`max-width: 100%`,`overflow-x: auto`,`-webkit-overflow-scrolling: touch`,`scrollbar-gutter: stable`
**表格**:
- `table-layout: fixed`,`width: 100%`,`border-collapse: collapse`
- `my-8 text-sm`
- `th`:`font-semibold p-[calc(4/7*1em)]`
- `td`:`p-[calc(4/7*1em)]`
- 边框色:`var(--table-border, #cbd5e1)`
**加载态**:shimmer 骨架动画 + 旋转 spinner
**无障碍**:`.sr-only` 屏幕阅读器文本
---
## 7. 媒体类组件
### 7.1 ImageNode — 图片
**文件**:`src/components/ImageNode/ImageNode.vue`
- `.image-node__img`:`max-width: 24rem`
- 内联图片:`display: inline-block`,`vertical-align: middle`,无固定尺寸
- 切换动画:`opacity 220ms ease` + `translateY(6px)` 过渡
- 加载旋转器:`border: 2px solid`,`animate-spin`
---
### 7.2 MermaidBlockNode — Mermaid 图表
**文件**:`src/components/MermaidBlockNode/MermaidBlockNode.vue`
**容器**:`rounded-lg border shadow-sm overflow-hidden`
**Header**:
- Light:`bg-gray-50 border-gray-200`
- Dark:`bg-gray-800 border-gray-700/30`
- 模式切换按钮带 `shadow-sm`
**图表区域**:
- `.mermaid-container`:`min-h-[360px]`,`transition-[height] duration-150 ease-out`
- `content-visibility: auto`,`contain: content`,`contain-intrinsic-size: 360px 240px`
- 支持缩放、拖拽(`cursor-grab` / `cursor-grabbing`)
**全屏弹窗**:
- 遮罩:`bg-black/70`,`z-50`
- 面板:`shadow-lg`,入场 `scale(0.98)→1.0`
**操作按钮**:
- Light:`text-gray-600 hover:bg-gray-200`
- Dark:`text-gray-400 hover:bg-gray-700`
- Active:`scale(0.98)`
---
### 7.3 MathBlockNode / MathInlineNode — 数学公式
**文件**:`src/components/MathBlockNode/MathBlockNode.vue`
**块级**:
- `text-center overflow-x-auto min-h-[40px]`
- 加载遮罩:`backdrop-filter: blur(2px)`
- Spinner:`20px`,`border: 2px solid`,`0.8s linear spin`
- 渲染中:`opacity: 0.3`,`transition: opacity 0.2s ease`
- 切换:`opacity 0.3s ease`
---
### 7.4 D2BlockNode — D2 图表
- 类似 MermaidBlockNode,用于 D2 图表渲染
### 7.5 InfographicBlockNode — 信息图
- 信息图嵌入容器
---
## 8. 分隔与装饰组件
### 8.1 ThematicBreakNode — 分割线
**文件**:`src/components/ThematicBreakNode/ThematicBreakNode.vue`
- `.hr-node`:`border-t border-[var(--hr-border-color, #e2e8f0)] my-12`
---
## 9. HTML 嵌入
### 9.1 HtmlBlockNode / HtmlInlineNode
- 原始 HTML 渲染,样式由内容自身决定
---
## 10. 辅助组件
### 10.1 Tooltip — 提示浮层
**文件**:`src/components/Tooltip/Tooltip.vue`
| 属性 | Light | Dark |
|---|---|---|
| 背景 | `bg-white` | `bg-gray-900` |
| 文字 | `text-gray-900` | `text-white` |
| 边框 | `border-gray-200` | `border-gray-700` |
| 圆角 | `rounded-md` | 同 |
| 阴影 | `shadow-md` | 同 |
| 层级 | `z-[9999]` | 同 |
入场动画:`opacity 120ms linear`,`translateY(-6px) scale(0.98) → 0 scale(1)`
位移动画:`transform 220ms cubic-bezier(.16,1,.3,1)`
---
### 10.2 NodeRenderer — 渲染器
**文件**:`src/components/NodeRenderer/NodeRenderer.vue`
- 根容器传递 `isDark` prop 到所有子组件
- 使用 `.markstream-vue` 作为作用域根类
---
### 10.3 ReferenceNode — 引用
- 引用标记渲染
@@ -0,0 +1,270 @@
# Redesign Checklist
> 所有需要重新设计的元素清单,按优先级和类别组织。
> 架构方案:**带默认样式 + CSS 变量全量可定制**(详见 architecture.md)
---
## 0. Token 化改造(最高优先级,前置工作)
> 所有后续设计工作的基础。详细架构见 `architecture.md`。
- [ ] **定义 Base Tokens** — 在 `index.css` 中建立完整的基础变量集(色彩、排版、间距、圆角、阴影)
- [ ] **定义 Semantic Tokens** — 组件级语义变量,引用 Base Tokens(如 `--code-bg: var(--secondary)`)
- [ ] **逐组件替换硬编码值** — 把所有组件中的硬编码色值、间距替换为 Semantic Token 引用
- [ ] **补全 Dark Base Tokens** — `.dark` 下覆盖全部 Base Tokens,实现完整暗色
- [ ] **移除组件级 `.is-dark` CSS** — 暗色样式全部由 Base Token 覆盖驱动,组件不再单独处理
- [ ] **统一暗色触发方式** — 去掉 `.is-dark`,统一为根容器 `.dark` 类
---
## A. 全局体系(高优先级)
### A1. Design Tokens 统一
- [ ] **CSS 变量命名规范** — 当前全局用 HSL 裸值 (`--border: 214.3 31.8% 91.4%`),组件用 HEX (`#111827`)、RGB (`rgb(229 231 235)`)、rgba 混用。需统一为一套命名和格式体系
- [ ] **颜色格式统一** — 决定统一使用 HSL、HEX 还是 oklch,消除混用
- [ ] **语义色彩层级** — 目前仅 6 个全局变量,缺乏 `--accent`、`--destructive`、`--success`、`--warning` 等语义色
- [ ] **组件硬编码颜色提取** — 大量颜色硬编码在组件中:`#e2e8f0`、`#eaecef`、`#cbd5e1`、`#64748b`、`#0366d6`、`#ffff00` 等,需抽取为全局变量
### A2. 暗色模式补全
- [ ] **HighlightNode** — 高亮仅 `#ffff00`,暗色下刺眼且与暗色背景冲突
- [ ] **FootnoteNode** — 固定 `#eaecef` 边框色,无暗色适配
- [ ] **BlockquoteNode** — `--blockquote-border-color` 无暗色默认值
- [ ] **TableNode** — `--table-border` 无暗色默认值
- [ ] **ListItemNode** — `--list-item-marker` / `--list-item-counter-marker` 无暗色默认值
- [ ] **ThematicBreakNode** — `--hr-border-color` 无暗色默认值
- [ ] **ImageNode** — 无暗色背景适配
### A3. 暗色触发方式统一
- [ ] **统一暗色类名** — 全局用 `.dark`,组件用 `.is-dark`,MermaidBlockNode 用条件 Tailwind 类,需统一为一种方式
- [ ] **消除冗余判断** — `isDark` prop 传递链 + CSS class + media query 三层并存,简化为清晰的单一优先级链
---
## B. 色彩体系(高优先级)
### B1. 基础色板
- [ ] **重新定义色板** — 当前 Light/Dark 色彩对比度、和谐度需审查
- [ ] **灰阶系统** — 散落多个灰色系:Tailwind gray、slate、自定义 HEX,需统一
- [ ] **中性色梯度** — 建立从 50-950 的完整中性色阶(用于背景、边框、文字等)
### B2. 语义色彩
- [ ] **Admonition 色彩** — `#448aff`、`#00bfa5`、`#ff9100`、`#ff5252` 需要重新评估与整体色板的协调性
- [ ] **Diff 色彩** — Added/Removed 的 Light/Dark 配色需与新色板统一
- [ ] **Link 色** — `#0366d6` 来自 GitHub 风格,需要决定是否保留
- [ ] **Highlight 色** — `#ffff00` 过于刺眼,需要更精致的方案
### B3. 对比度与可读性
- [ ] **WCAG AA 达标检查** — 所有前景/背景色组合的对比度检查
- [ ] **暗色模式对比度** — 确保暗色下文字可读性
---
## C. 排版系统(中优先级)
### C1. 标题层级
- [ ] **h1-h6 字号梯度** — 当前 h4/h5/h6 都是 `text-base`,层级区分不足
- [ ] **标题间距** — 使用 calc 表达式(如 `calc(8/9*1em)`),可读性差,考虑简化
- [ ] **标题字重梯度** — extrabold → bold → semibold → semibold → 默认,h5/h6 缺乏区分
### C2. 正文排版
- [ ] **段落间距** — `1.25em` 是否合适需评估
- [ ] **行高系统** — 各标题用不同 calc 行高,缺乏统一的行高阶梯
- [ ] **字体栈** — 未显式定义,依赖 Tailwind 默认,考虑是否需要自定义
### C3. 代码排版
- [ ] **行内代码字号** — `85%` 与正文的视觉平衡
- [ ] **代码块字号** — `12px` 固定值,缺乏响应式
- [ ] **代码标题字号** — `14px` / `12px`,考虑是否与排版系统关联
---
## D. 间距系统(中优先级)
### D1. 垂直间距
- [ ] **统一间距阶梯** — 当前混用 rem、em、calc 表达式、Tailwind 间距值
- [ ] **块间距规范** — 段落 `1.25em`、引用块 `1.6em`、列表 `1.25rem`、表格 `2rem`、分割线 `3rem`,缺乏统一节奏
- [ ] **嵌套间距** — 列表内段落归零、其他嵌套场景需检查
### D2. 水平间距
- [ ] **列表缩进** — `calc(13/8*1em)` 难以理解,考虑使用更直观的值
- [ ] **提示框内边距** — `0.5rem 1rem` 是否与其他容器协调
- [ ] **表格单元格** — `calc(4/7*1em)` 不够直观
---
## E. 边框与装饰(中优先级)
### E1. 边框系统
- [ ] **边框色统一** — 至少 5 种边框色散落各处:`var(--border)`、`#eaecef`、`#e2e8f0`、`#cbd5e1`、`rgb(229 231 235)`
- [ ] **边框粗细** — `1px`、`0.25rem`、`4px` 混用
- [ ] **边框圆角** — `rounded`(4px)、`4px`、`rounded-md`(6px)、`rounded-lg`(8px) 混用,需建立圆角阶梯
### E2. 阴影系统
- [ ] **阴影层级** — `shadow-sm`、`shadow-md`、`shadow-lg` 加上自定义 Diff 阴影,需统一为设计令牌
- [ ] **暗色阴影** — 当前阴影在暗色下效果需检查
### E3. 分隔线
- [ ] **ThematicBreakNode** — 仅 `border-t`,考虑是否需要更多样式选项
- [ ] **FootnoteNode** — 使用独立边框色 `#eaecef`,应统一
---
## F. 组件逐一审查
### F1. 代码块(CodeBlockNode)— 复杂度最高
- [ ] Header 布局与视觉
- [ ] 操作按钮样式
- [ ] 加载骨架屏效果
- [ ] Diff 视图完整样式
- [ ] Diff 阴影和渐变(是否过度设计?)
- [ ] Monaco 主题集成
- [ ] Container Query 断点
- [ ] 行号区域样式
### F2. 提示框(AdmonitionNode)
- [ ] 整体视觉风格(左边框 vs 全边框 vs 背景色块)
- [ ] 图标设计
- [ ] 折叠交互
- [ ] 4 种语义类型的色彩协调
- [ ] 暗色模式效果
### F3. 表格(TableNode)
- [ ] 表头样式
- [ ] 斑马纹/分隔线选择
- [ ] 溢出横向滚动指示
- [ ] 加载态设计
- [ ] 对齐方式视觉
### F4. Mermaid 图表(MermaidBlockNode)
- [ ] 容器边框与阴影
- [ ] Header 栏布局
- [ ] 模式切换按钮组
- [ ] 缩放/拖拽交互反馈
- [ ] 全屏弹窗设计
- [ ] 操作按钮组样式
### F5. 数学公式(MathBlockNode)
- [ ] 加载态(blur 遮罩是否合适)
- [ ] 渲染过渡效果
- [ ] 块级/内联一致性
### F6. 图片(ImageNode)
- [ ] 最大宽度(`24rem` 是否合理)
- [ ] 占位与加载态
- [ ] 错误态设计
- [ ] 切换过渡效果
### F7. 链接(LinkNode)
- [ ] 链接色
- [ ] hover 效果
- [ ] 加载脉冲动画
- [ ] 外部链接指示
### F8. 引用块(BlockquoteNode)
- [ ] 左边框风格
- [ ] 斜体是否保留
- [ ] 嵌套引用视觉
### F9. 列表(ListNode / ListItemNode)
- [ ] 标记颜色与样式
- [ ] 嵌套缩进
- [ ] 任务列表(Checkbox)样式
### F10. Tooltip
- [ ] 背景/边框/阴影
- [ ] 入场出场动画
- [ ] 箭头指示器(当前无箭头)
### F11. 其他组件
- [ ] HighlightNode — 高亮色
- [ ] StrikethroughNode — 删除线样式
- [ ] SubscriptNode / SuperscriptNode — 上下标
- [ ] EmojiNode — 表情显示
- [ ] DefinitionListNode — 定义列表排版
- [ ] FootnoteNode — 脚注分隔与排版
- [ ] HtmlBlockNode / HtmlInlineNode — HTML 嵌入样式隔离
- [ ] D2BlockNode — D2 图表容器
- [ ] InfographicBlockNode — 信息图容器
---
## G. 动画与交互(低优先级)
### G1. 动画时长体系
- [ ] **建立统一时长阶梯** — 当前 120ms / 150ms / 180ms / 200ms / 220ms / 300ms / 900ms / 1600ms 过于分散
- [ ] **缓动函数规范** — ease / ease-out / ease-in-out / linear / cubic-bezier 各用各的
### G2. 交互反馈
- [ ] **按钮点击效果** — `scale(0.98)` 是否统一应用于所有可交互元素
- [ ] **Focus 状态** — 仅 Admonition 折叠按钮有 focus outline,其他按钮需补全
- [ ] **Hover 效果** — 各组件 hover 效果不统一
### G3. 加载态
- [ ] **Shimmer 动画** — CodeBlock 和 Table 各自实现,需抽取为复用
- [ ] **Spinner 样式** — MathBlock / Table / Image 三种不同 spinner,需统一
- [ ] **Blur 遮罩** — MathBlock 使用 `backdrop-filter: blur(2px)`,是否推广
### G4. 无障碍动效
- [ ] **`prefers-reduced-motion` 覆盖** — 仅 InlineCodeNode 和 LinkNode 支持,需全面覆盖
---
## H. 架构改进(低优先级)
### H1. CSS 架构
- [ ] **样式文件组织** — 当前仅一个 `index.css`,考虑拆分为 tokens / base / components
- [ ] **Tailwind @apply 过度使用** — 部分组件大量 `@apply`,考虑平衡
- [ ] **Scoped vs Global** — 部分样式需要全局穿透(`:deep()`),架构是否合理
### H2. 构建优化
- [ ] **CSS 产物体积** — 检查未使用的 Tailwind 类
- [ ] **双构建是否必要** — `index.css` + `index.tailwind.css` 的策略是否最优
- [ ] **PostCSS 插件链** — 是否需要增加 cssnano 等优化
---
## 统计摘要
| 类别 | 待设计项数 |
|---|---|
| A. 全局体系 | 10 |
| B. 色彩体系 | 9 |
| C. 排版系统 | 8 |
| D. 间距系统 | 6 |
| E. 边框与装饰 | 7 |
| F. 组件逐一审查 | 40+ |
| G. 动画与交互 | 10 |
| H. 架构改进 | 5 |
| **总计** | **~95 项** |
@@ -0,0 +1,90 @@
# 需求分析
> 来源:[Simon-He95/markstream-vue#343](https://github.com/Simon-He95/markstream-vue/issues/343)
> 标签:`enhancement` `help wanted` `pr welcome` `render`
---
## Issue 原文核心要求
**目标**:Make the streaming rendering UI look significantly better, component by component, without losing readability.
### 1. 视觉重设计(对方核心诉求)
> "the visual presentation during streaming still feels rough in places"
> "feel more intentional, attractive, and consistent"
- 重新思考流式内容的视觉设计
- 调整各组件的样式
- 改善层级(hierarchy)、间距(spacing)、节奏(rhythm)
- 整体美感提升
### 2. 流式渲染特有体验
> "Making partially streamed content feel smoother and more pleasant to read"
> "streaming-specific states or transitions that should look better"
- 部分渲染时的过渡效果
- 流式内容逐步呈现的视觉连贯性
- 打字机效果 / 骨架屏 / 渐入动画的打磨
### 3. 逐组件打磨
涉及所有渲染组件:
| 组件 | 需关注 |
|---|---|
| headings | 字号梯度、字重、间距 |
| paragraphs | 行高、段间距 |
| lists | 缩进、标记样式、嵌套 |
| blockquotes | 边框风格、斜体、配色 |
| links | 链接色、hover 效果 |
| inline code / code blocks | 背景、边框、字号、语法高亮主题 |
| tables | 表头、斑马纹、边框、溢出 |
| images | 占位、加载态、错误态 |
| separators | 分割线样式 |
### 4. 对方接受的贡献形式
- design direction proposals(设计方向提案)
- screenshots or mockups(截图或原型)
- style audits(样式审计)
- focused PRs for specific components(逐组件 PR)
- a broader visual refresh(整体视觉刷新)
---
## 我们的工作 vs 需求的映射
| 需求 | 我们的进展 | 状态 |
|---|---|---|
| 样式审计 | `design/tokens.md` + `design/components.md` + `design/themes.md` | ✅ 已完成 |
| 设计方向提案 | `design/architecture.md` token 体系 | ✅ 已完成 |
| 代码块专项设计 | `design/codeblock.md` Shell/Monaco 分离 | ✅ 已完成 |
| token 基础设施落地 | `src/index.css` Layer 1 + Layer 2,25 组件迁移 | ✅ 已完成 |
| 暗色模式修复 | Teleport dark、独立组件 dark | ✅ 已完成 |
| **视觉重设计** | 未开始 | ❌ **核心差距** |
| **流式渲染体验打磨** | 未开始 | ❌ **核心差距** |
| **逐组件视觉优化** | 未开始 | ❌ **核心差距** |
| **截图/对比** | 无 | ❌ 需要产出 |
---
## 下一步工作优先级
### P0 — 对方最想看到的
1. **逐组件视觉重设计** — 在 token 基础上调整每个组件的实际样式值(颜色搭配、间距节奏、圆角阴影、字号层级)
2. **流式渲染过渡效果** — 打字机淡入、骨架屏到内容的过渡、部分渲染时的视觉连贯性
3. **Before/After 截图** — 每个改进需要有视觉对比,这是 PR review 的关键材料
### P1 — 架构收尾
4. CodeBlock Shell/Monaco 分离实施(`design/codeblock.md` 阶段 1-11)
5. 共享 Shell 抽取(三档内置渲染器统一外壳)
### P2 — 长期改进
6. 动画时长体系统一
7. 无障碍(prefers-reduced-motion 全覆盖、focus 环统一)
8. 性能优化(content-visibility 策略审查)
@@ -0,0 +1,203 @@
# Theming System
> markstream-vue 的主题机制文档:Light/Dark 模式实现、CSS 变量架构、主题切换方式。
---
## 1. 主题架构概览
markstream-vue 采用 **混合主题方案**:
```
Props (isDark) → CSS Class (.is-dark / .dark) → CSS Variables Override → 组件渲染
```
三层机制互为补充:
| 层级 | 机制 | 说明 |
|---|---|---|
| 1. Props | `isDark: boolean` | 由宿主应用传入,最高优先级 |
| 2. CSS Class | `.dark` / `.is-dark` | 添加到根容器或组件上 |
| 3. Media Query | `prefers-color-scheme: dark` | 系统偏好兜底 |
---
## 2. 全局主题变量
### 2.1 挂载方式
```css
/* Light(默认) */
.markstream-vue {
--border: 214.3 31.8% 91.4%;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--secondary: 210 40% 96%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
}
/* Dark — 两种触发方式 */
.dark .markstream-vue, /* 父级添加 .dark 类 */
.markstream-vue.dark { /* 自身添加 .dark 类 */
--border: 217.2 32.6% 17.5%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
}
```
### 2.2 使用方式
在组件中通过 `hsl()` 引用:
```css
color: hsl(var(--foreground));
background-color: hsl(var(--secondary));
border: 1px solid hsl(var(--border) / 0.9);
```
Tailwind 配置中也引用这些变量(带回退值):
```js
const fallback = {
foreground: 'hsl(var(--foreground), 217.2 22.08% 19.5%)',
}
```
---
## 3. 组件级暗色模式
### 3.1 Props 传递链
```
NodeRenderer (isDark prop)
└─ 所有子组件 (isDark prop)
└─ :class="{ 'is-dark': props.isDark }"
```
### 3.2 CSS 类切换模式
各组件使用 `.is-dark` 类覆盖 CSS 变量:
```vue
<div class="code-block-container" :class="{ 'is-dark': props.isDark }">
<style scoped>
.code-block-container {
--markstream-code-fallback-bg: #ffffff;
}
.code-block-container.is-dark {
--markstream-code-fallback-bg: #111827;
}
</style>
```
### 3.3 各组件暗色实现方式
| 组件 | 暗色触发 | 方式 |
|---|---|---|
| 全局变量 | `.dark .markstream-vue` | CSS 变量覆盖 |
| CodeBlockNode | `.is-dark` class | ~30+ CSS 变量覆盖 |
| AdmonitionNode | `.is-dark` class | CSS 变量覆盖 + 透明度调整 |
| MermaidBlockNode | `:class` 条件 | Tailwind dark 类名切换 |
| Tooltip | `.is-dark` class | `bg-gray-900 text-white` |
| MathBlockNode | `.is-dark` class | spinner 颜色反转 |
| ImageNode | - | 无暗色适配 |
| HighlightNode | - | **无暗色适配**(固定 `#ffff00`) |
| BlockquoteNode | - | 仅通过全局 `--blockquote-border-color` |
| TableNode | - | 仅通过 `--table-border` |
| ListItemNode | - | 仅通过 `--list-item-marker` 等 |
| FootnoteNode | - | 固定 `#eaecef` 边框色,**无暗色适配** |
---
## 4. Monaco Editor 主题
代码块支持独立的编辑器主题系统:
```ts
interface Props {
darkTheme?: CodeBlockMonacoTheme // 暗色编辑器主题
lightTheme?: CodeBlockMonacoTheme // 亮色编辑器主题
}
// 运行时根据 isDark 切换
const activeTheme = props.isDark ? props.darkTheme : props.lightTheme
```
支持:
- 字符串形式:内置主题名
- 对象形式:`CodeBlockMonacoThemeObject` 自定义主题
VSCode 变量自动映射:
- `--vscode-editor-background`
- `--vscode-editor-foreground`
- `--vscode-editor-selectionBackground`
---
## 5. 媒体查询兜底
部分组件使用系统偏好作为兜底:
```css
@media (prefers-color-scheme: dark) {
.math-loading-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.6);
}
}
```
JavaScript 检测:
```js
window.matchMedia('(prefers-color-scheme: dark)').matches
```
---
## 6. Tailwind 作用域隔离
所有 Tailwind 工具类限定在 `.markstream-vue` 内生效,避免全局污染:
```js
// tailwind.config.js
important: '.markstream-vue'
```
按钮重置使用 `:where()` 保持低特异性:
```css
:where(.markstream-vue) button {
appearance: none;
background: transparent;
border: 0;
}
```
---
## 7. 双构建输出
| 构建 | 输出文件 | 说明 |
|---|---|---|
| 标准构建 | `dist/index.css` | 包含完整 Tailwind 工具类 |
| Tailwind 构建 | `dist/index.tailwind.css` | 不含 Tailwind,供宿主项目自行引入 |
---
## 8. 当前主题体系的问题
| 问题 | 详情 |
|---|---|
| 暗色覆盖不完整 | HighlightNode、FootnoteNode、ImageNode 等无暗色适配 |
| 变量命名不统一 | 全局用 HSL 裸值,组件用 HEX/RGB/rgba,缺乏统一规范 |
| 暗色触发方式不一致 | 全局用 `.dark`,组件用 `.is-dark`,Mermaid 用条件 class |
| 硬编码颜色散落 | `#e2e8f0`、`#eaecef`、`#cbd5e1` 等直接写在组件中 |
| 语义色彩未统一 | Admonition 语义色与全局体系分离 |
| CSS 变量缺乏 Dark 值 | `--blockquote-border-color`、`--table-border` 等无暗色默认值 |
@@ -0,0 +1,310 @@
# Token 扩展方案(v2)
> 状态:**经外部 review 修正后的版本**。
> 原则:保真优先,不硬归一。"基础刻度 + 语义 token + 组件私有常量"三层。
---
## 背景
当前 token 系统只覆盖了**颜色**和**圆角**。非颜色维度全部硬编码,且存在两类问题:
1. **CSS 硬编码** — 组件 `<style>` 中的固定值(间距、阴影、动画时长),无法通过 CSS 变量覆盖
2. **JS 硬编码** — props 默认值和 JS 中的 `style.transition = 'height 180ms'` 等,CSS 变量完全触达不到
> **关键约束**:JS 侧的硬编码(`360px`、`500px`、`180ms` 等)必须同步改为读取 CSS 变量
> 或 computed 值,否则密度主题只是 CSS 层面的假象,JS 控制的高度/动画不会跟随。
---
## 一、基础刻度(Scale)
只收项目里真实反复出现的值,包括半步值。
```css
.markstream-vue {
--ms-space-1: 0.25rem; /* 4px */
--ms-space-1_5: 0.375rem; /* 6px — ListItemNode pl */
--ms-space-2: 0.5rem; /* 8px */
--ms-space-2_5: 0.625rem; /* 10px — header py-2.5 */
--ms-space-3: 0.75rem; /* 12px */
--ms-space-4: 1rem; /* 16px */
--ms-space-5: 1.25rem; /* 20px — ListNode my-5 */
--ms-space-6: 1.5rem; /* 24px */
--ms-space-8: 2rem; /* 32px */
--ms-space-12: 3rem; /* 48px */
}
```
---
## 二、语义 Token — 文档流间距
保留原始值和单位,不做近似归并。
```css
.markstream-vue {
/* ── 块元素流 ── */
--ms-flow-paragraph-y: 1.25em; /* ParagraphNode margin */
--ms-flow-list-y: var(--ms-space-5); /* ListNode my-5 = 1.25rem */
--ms-flow-list-item-y: var(--ms-space-2); /* ListItemNode my-2 = 0.5rem */
--ms-flow-list-indent: calc(13 / 8 * 1em); /* ListNode pl */
--ms-flow-list-indent-mobile: calc(14 / 9 * 1em); /* ListNode max-lg:pl */
--ms-flow-table-y: var(--ms-space-8); /* TableNode my-8 = 2rem */
--ms-flow-table-cell: calc(4 / 7 * 1em); /* TableNode td/th padding */
--ms-flow-blockquote-y: 1.6em; /* BlockquoteNode margin-top/bottom */
--ms-flow-blockquote-indent: 1em; /* BlockquoteNode padding-left */
--ms-flow-admonition-y: var(--ms-space-4); /* AdmonitionNode margin = 1rem */
--ms-flow-footnote-y: var(--ms-space-2); /* FootnoteNode mt-2 mb-2 */
--ms-flow-hr-y: var(--ms-space-12); /* ThematicBreakNode my-12 = 3rem */
--ms-flow-diagram-y: var(--ms-space-4); /* Mermaid/D2/Infographic my-4 = 1rem */
--ms-flow-codeblock-y: var(--ms-space-4); /* CodeBlockNode my-4 = 1rem */
--ms-flow-definition-term-mt: var(--ms-space-2); /* DefinitionListNode term margin-top */
--ms-flow-definition-desc-ml: var(--ms-space-4); /* DefinitionListNode desc margin-left */
--ms-flow-definition-desc-mb: var(--ms-space-2); /* DefinitionListNode desc margin-bottom */
/* ── 标题节奏(每级独立,保留原始 calc)── */
--ms-flow-heading-1-mt: 0;
--ms-flow-heading-1-mb: calc(8 / 9 * 1em);
--ms-flow-heading-2-mt: var(--ms-space-8);
--ms-flow-heading-2-mb: var(--ms-space-4);
--ms-flow-heading-3-mt: calc(8 / 5 * 1em);
--ms-flow-heading-3-mb: calc(3 / 5 * 1em);
--ms-flow-heading-4-mt: var(--ms-space-6);
--ms-flow-heading-4-mb: var(--ms-space-2);
--ms-flow-heading-5-mt: 0;
--ms-flow-heading-5-mb: 0;
--ms-flow-heading-6-mt: 0;
--ms-flow-heading-6-mb: 0;
}
```
---
## 三、语义 Token — 容器内边距与布局间隙
```css
.markstream-vue {
/* ── 面板内边距 ── */
--ms-inset-panel-x: var(--ms-space-4); /* code/mermaid/d2/infographic header px-4 */
--ms-inset-panel-y: var(--ms-space-2_5); /* header py-2.5 = 0.625rem */
--ms-inset-panel-body-sm: var(--ms-space-2); /* admonition header padding = 0.5rem */
--ms-inset-panel-body: var(--ms-space-4); /* admonition content padding-x = 1rem */
--ms-inset-admonition-body-top: var(--ms-space-2); /* admonition content padding-top = 0.5rem */
--ms-inset-admonition-body-bottom: var(--ms-space-4); /* admonition content padding-bottom = 1rem */
/* ── 布局间隙 ── */
--ms-gap-header: var(--ms-space-4); /* code header gap = 16px */
--ms-gap-header-main: var(--ms-space-2_5); /* header main section gap = 10px */
--ms-gap-header-actions: var(--ms-space-2); /* header actions gap = 8px */
--ms-gap-toggle: var(--ms-space-1); /* mode toggle内gap = 4px */
--ms-gap-skeleton: var(--ms-space-3); /* skeleton line gap */
}
```
---
## 四、阴影 Token
按语义角色命名,不按 Tailwind 尺寸名。避免值混淆。
```css
.markstream-vue {
/* subtle: 卡片/容器的轻微立体感 */
--ms-shadow-subtle: 0 1px 2px 0 hsl(var(--ms-foreground) / 0.05);
/* popover: tooltip / 下拉 / 浮层 */
--ms-shadow-popover: 0 4px 6px -1px hsl(var(--ms-foreground) / 0.1),
0 2px 4px -2px hsl(var(--ms-foreground) / 0.1);
/* modal: 全屏弹窗 / dialog */
--ms-shadow-modal: 0 10px 15px -3px hsl(var(--ms-foreground) / 0.1),
0 4px 6px -4px hsl(var(--ms-foreground) / 0.1);
/* preview: HtmlPreviewFrame 专用,独立不混 */
--ms-shadow-preview: 0 10px 40px hsl(var(--ms-foreground) / 0.25);
}
```
### 映射
| 当前值 | 出现位置 | 映射到 |
|---|---|---|
| Tailwind `shadow-sm` | CodeBlock, D2, Infographic, MarkdownCodeBlock | `--ms-shadow-subtle` |
| Tailwind `shadow-md` | Tooltip | `--ms-shadow-popover` |
| Tailwind `shadow-lg` | Mermaid modal, Infographic modal | `--ms-shadow-modal` |
| `0 1px 2px 0 hsl(...)` | Mermaid/Infographic action bar | `--ms-shadow-subtle` |
| `0 10px 40px hsl(...)` | HtmlPreviewFrame | `--ms-shadow-preview` |
| Diff 专用阴影 | CodeBlockNode `.is-dark` 块 | 保留组件内(Monaco 区域) |
---
## 五、动画 Token
保留 5 档时长 + `linear` 纳入缓动表。
```css
.markstream-vue {
/* ── 时长:5 档,不混并 ── */
--ms-duration-fast: 120ms; /* tooltip fade */
--ms-duration-standard: 180ms; /* 高度过渡、表格淡入、代码块折叠 */
--ms-duration-overlay: 200ms; /* dialog enter/leave、MathBlock 渲染 */
--ms-duration-emphasis: 220ms; /* ImageNode 切换、Tooltip 位移 */
--ms-duration-slow: 300ms; /* MathBlock 切换 */
--ms-duration-stream: 900ms; /* 流式打字机淡入 */
/* ── 缓动 ── */
--ms-ease-linear: linear;
--ms-ease-standard: ease;
--ms-ease-out: ease-out;
--ms-ease-in-out: ease-in-out; /* LinkNode 脉冲 */
--ms-ease-spring: cubic-bezier(.16, 1, .3, 1); /* Tooltip 位移 */
}
```
### 映射
| 当前值 | 出现位置 | 映射到 |
|---|---|---|
| `120ms` | Tooltip enter/leave | `--ms-duration-fast` |
| `150ms` | Mermaid 容器高度 | `--ms-duration-fast`(最近档,或保留组件私有) |
| `180ms` | CodeBlock 高度、TableNode 淡入 | `--ms-duration-standard` |
| `200ms` | Mermaid/Infographic dialog、MathBlock 渲染 | `--ms-duration-overlay` |
| `220ms` | ImageNode 切换、Tooltip 位移 | `--ms-duration-emphasis` |
| `300ms` / `0.3s` | MathBlock 切换 | `--ms-duration-slow` |
| `900ms` | InlineCodeNode 流式淡入 | `--ms-duration-stream` |
| `1.2s` | Shimmer 动画 | 不 token 化(shimmer 专用) |
| `1.6s` | LinkNode 脉冲 | 不 token 化(脉冲专用) |
| `ease` | 多处 | `--ms-ease-standard` |
| `ease-out` | Mermaid 容器、打字机 | `--ms-ease-out` |
| `linear` | Spinner 旋转、Tooltip fade | `--ms-ease-linear` |
| `ease-in-out` | LinkNode 脉冲 | `--ms-ease-in-out` |
| `cubic-bezier(.16,1,.3,1)` | Tooltip 位移 | `--ms-ease-spring` |
---
## 六、边框与焦点 Token
```css
.markstream-vue {
--ms-border-width: 1px; /* 标准边框 */
--ms-border-width-strong: 4px; /* 强调左边框(admonition、blockquote、vmr-container) */
--ms-focus-ring-width: 2px; /* focus outline 宽度 */
--ms-focus-ring-offset: 2px; /* focus outline offset */
}
```
### 映射
| 当前值 | 出现位置 | 映射到 |
|---|---|---|
| `1px solid` | InlineCodeNode, 多处 `border` | `--ms-border-width` |
| `border-left: 4px solid` | AdmonitionNode | `--ms-border-width-strong` |
| `border-left: 0.25rem solid` | BlockquoteNode (= 4px) | `--ms-border-width-strong` |
| `border-left-width: 4px` | VmrContainerNode | `--ms-border-width-strong` |
| `outline: 2px solid` | AdmonitionNode toggle focus | `--ms-focus-ring-width` |
| `outline-offset: 2px` | AdmonitionNode toggle focus | `--ms-focus-ring-offset` |
| `border: 2px solid` | Spinner (MathBlock, Table, Image) | 不 token 化(spinner 专用) |
---
## 七、容器尺寸 Token
```css
.markstream-vue {
--ms-size-diagram-min-height: 360px; /* Mermaid / Infographic 最小高度 */
--ms-size-code-max-height: 500px; /* 代码块折叠最大高度 */
--ms-size-image-max-width: 24rem; /* 图片最大宽度 */
--ms-size-math-min-height: 40px; /* MathBlock 最小高度 */
--ms-size-skeleton-min-height: 120px; /* 骨架屏占位最小高度 */
}
```
### JS 硬编码问题
以下值**同时存在于 CSS 和 JS 中**,只改 CSS token 不够,JS 也必须同步:
| 值 | CSS 位置 | JS 位置 | 解决方案 |
|---|---|---|---|
| `360px` | `min-h-[360px]` | MermaidBlockNode props default、`contain-intrinsic-size` | JS 读取 `getComputedStyle` 或改为 CSS-only 控制 |
| `500px` | `max-height` | MarkdownCodeBlock/CodeBlock 折叠逻辑 | 同上 |
| `180ms` | `transition: height 180ms` | MermaidBlockNode `el.style.transition = 'height 180ms'` | JS 读取 `var(--ms-duration-standard)` |
| `120px` | skeleton 高度 | CodeBlockNode loading 占位 | 改用 CSS var |
---
## 八、不 token 化的项
| 项 | 原因 |
|---|---|
| 标题 calc 表达式(`calc(8/9*1em)` 等) | 已作为 `--ms-flow-heading-*` token 的值保留原始表达式 |
| z-index (`z-50`, `z-[9999]`) | 内部层叠实现,对外无意义 |
| icon 尺寸 (`w-4 h-4`, `w-3 h-3`) | 过于细碎,改了破坏对齐 |
| Spinner border-width (`2px`) | 极少定制需求 |
| Shimmer 时长 (`1.2s`) / 脉冲时长 (`1.6s`) | 单一用途,不值得抽象 |
| viewport 相对值 (`70vh`, `80vw`) | 运行时响应式,不适合固定 token |
| Tooltip offset (`8px`) / shift padding (`8px`) | Floating-UI 配置,非视觉 token |
---
## 九、密度主题示例
```css
/* 舒适模式(默认) */
.markstream-vue {
/* 使用上述所有默认值 */
}
/* 紧凑模式 */
.markstream-vue.compact {
--ms-flow-paragraph-y: 1em;
--ms-flow-list-y: var(--ms-space-4);
--ms-flow-list-item-y: var(--ms-space-1_5);
--ms-flow-table-y: var(--ms-space-6);
--ms-flow-admonition-y: 0.75rem;
--ms-flow-heading-2-mt: var(--ms-space-6);
--ms-flow-heading-2-mb: 0.75rem;
--ms-flow-heading-4-mt: var(--ms-space-4);
--ms-flow-heading-4-mb: 0.375rem;
--ms-inset-panel-x: 0.75rem;
--ms-inset-panel-y: 0.5rem;
--ms-inset-panel-body-sm: 0.5rem;
--ms-inset-panel-body: 0.75rem;
--ms-gap-header-main: 0.5rem;
--ms-gap-header-actions: 0.375rem;
--ms-size-diagram-min-height: 280px; /* 不要太激进,280 比 240 稳 */
--ms-size-code-max-height: 420px;
--ms-duration-standard: 160ms;
--ms-duration-overlay: 180ms;
}
```
---
## 十、实施注意事项
### "等值映射" vs "设计归一"
本文档的所有映射都是**严格等值**——token 的默认值就是当前代码里的实际值。
如果后续视觉重设计需要调整值(如 `1.6em` → `1.5rem`),应在单独的 PR 中进行,
并明确标注为"设计归一",不要混在 token 提取中。
### JS 硬编码必须同步
仅在 CSS 中定义 token 是不够的。所有在 JS 中硬编码的尺寸/时长值必须改为
通过 `getComputedStyle` 读取 CSS 变量值,或通过 props/computed 传递 token 值。
否则 compact 主题只影响 CSS 层面,JS 控制的高度/动画不会跟随。
### 保真优先
- `1.25rem`(ListNode my-5)和 `1.25em`(ParagraphNode margin)是**不同单位**,不要混成同一个 token
- `padding-left: 1em`(BlockquoteNode)不等于 `--ms-space-4`(1rem),单位不同
- `py-2.5 = 0.625rem` 不等于 `0.5rem`
- Admonition content 的 `padding: 0.5rem 1rem 1rem` 是三值(上 ≠ 下),不能和 header 共用
@@ -0,0 +1,259 @@
# Design Tokens
> markstream-vue 当前所有设计令牌(Design Tokens)清单。
> 来源:`src/index.css`、`tailwind.config.js`、各组件 `<style>` 块。
---
## 1. 全局 CSS 变量(Global CSS Variables)
定义在 `src/index.css`,挂载于 `.markstream-vue` 根选择器。
### 1.1 基础色彩(HSL 格式)
| 变量名 | Light 值 | Dark 值 | 用途 |
|---|---|---|---|
| `--border` | `214.3 31.8% 91.4%` | `217.2 32.6% 17.5%` | 边框色 |
| `--background` | `0 0% 100%` | `222.2 84% 4.9%` | 背景色 |
| `--foreground` | `222.2 84% 4.9%` | `210 40% 98%` | 前景/文字色 |
| `--secondary` | `210 40% 96%` | `217.2 32.6% 17.5%` | 次要背景(如行内代码底色) |
| `--muted` | `210 40% 96%` | `217.2 32.6% 17.5%` | 弱化背景 |
| `--muted-foreground` | `215.4 16.3% 46.9%` | `215 20.2% 65.1%` | 弱化文字 |
### 1.2 Tailwind 扩展色彩映射
定义在 `tailwind.config.js`,带回退值:
```js
const colors = {
'border': 'hsl(var(--border), 214.3 21.44% 93.4%)',
'background': 'hsl(var(--background), 0 0% 100%)',
'foreground': 'hsl(var(--foreground), 217.2 22.08% 19.5%)',
'secondary': 'hsl(var(--secondary), 214.3 21.44% 93.4%)',
'muted': 'hsl(var(--muted), 210 28% 98.1%)',
'muted-foreground': 'hsl(var(--muted-foreground), 215 12.16% 67.1%)',
}
```
---
## 2. 组件级 CSS 变量
### 2.1 代码块(CodeBlockNode)
| 变量名 | Light | Dark | 用途 |
|---|---|---|---|
| `--markstream-code-fallback-bg` | `#ffffff` | `#111827` | 代码区背景 |
| `--markstream-code-fallback-fg` | `#111827` | `#e5e7eb` | 代码区文字 |
| `--markstream-code-border-color` | `rgb(229 231 235)` | `rgb(55 65 81 / 0.3)` | 代码块边框 |
| `--markstream-code-fallback-selection-bg` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.08)` | 选区背景 |
| `--markstream-code-padding-y` | `8px` | - | 代码内上下边距 |
| `--markstream-code-padding-x` | `12px` | - | 代码内左右边距 |
| `--markstream-code-padding-left` | `52px` | - | 代码内左边距(含行号) |
| `--vscode-editor-font-size` | `12px` | - | 编辑器字号 |
| `--vscode-editor-background` | 来自 Monaco 主题 | - | Monaco 背景色 |
| `--vscode-editor-foreground` | 来自 Monaco 主题 | - | Monaco 前景色 |
### 2.2 Diff 视图变量
| 变量名 | Light | Dark |
|---|---|---|
| `--markstream-diff-frame-border` | `rgb(203 213 225 / 0.56)` | 深色变体 |
| `--markstream-diff-frame-shadow` | `0 16px 40px -32px rgb(15 23 42 / 0.18)` | 深色变体 |
| `--markstream-diff-shell-fg` | `#0f172a` | 深色变体 |
| `--markstream-diff-shell-muted` | `#64748b` | 深色变体 |
| `--markstream-diff-shell-border` | `rgb(148 163 184 / 0.18)` | 深色变体 |
| `--markstream-diff-editor-bg` | `#ffffff` | 深色变体 |
| `--markstream-diff-editor-fg` | `#435266` | 深色变体 |
| `--markstream-diff-added-fg` | `#14b8a6` | `#5eead4` |
| `--markstream-diff-removed-fg` | `#ff3658` | `#fda4af` |
| `--markstream-diff-added-line` | `rgb(232 249 245 / 0.98)` | 深色变体 |
| `--markstream-diff-removed-line` | `rgb(255 241 241 / 0.98)` | 深色变体 |
| `--markstream-diff-added-inline` | `rgb(197 245 219 / 0.96)` | 深色变体 |
| `--markstream-diff-removed-inline` | `rgb(255 215 217 / 0.92)` | 深色变体 |
### 2.3 提示框(AdmonitionNode)
| 变量名 | Light | Dark | 用途 |
|---|---|---|---|
| `--admonition-bg` | `#f8f8f8` | `#0b1220` | 提示框背景 |
| `--admonition-border` | `#eaecef` | `rgba(255,255,255,0.06)` | 提示框边框 |
| `--admonition-header-bg` | `rgba(0,0,0,0.03)` | `rgba(255,255,255,0.03)` | 标题栏背景 |
| `--admonition-text` | `#111827` | `#e6eef8` | 正文色 |
| `--admonition-muted` | `#374151` | `#cbd5e1` | 标题文字色 |
| `--admonition-note-color` | `#448aff` | 同 | 蓝色-提示 |
| `--admonition-tip-color` | `#00bfa5` | 同 | 绿色-技巧 |
| `--admonition-warning-color` | `#ff9100` | 同 | 橙色-警告 |
| `--admonition-danger-color` | `#ff5252` | 同 | 红色-危险 |
### 2.4 其他组件变量
| 变量名 | 默认值 | 组件 |
|---|---|---|
| `--blockquote-border-color` | `#e2e8f0` | BlockquoteNode |
| `--hr-border-color` | `#e2e8f0` | ThematicBreakNode |
| `--table-border` | `#cbd5e1` | TableNode |
| `--list-item-counter-marker` | `#64748b` | ListItemNode(有序列表数字) |
| `--list-item-marker` | `#cbd5e1` | ListItemNode(无序列表圆点) |
| `--link-color` | `#0366d6` | LinkNode |
| `--underline-height` | `2px` | LinkNode |
| `--underline-bottom` | `-3px` | LinkNode |
| `--underline-opacity` | `0.35` | LinkNode(动画最大透明度) |
| `--underline-rest-opacity` | `0.18` | LinkNode(静止态透明度) |
| `--underline-duration` | `1.6s` | LinkNode |
| `--underline-timing` | `ease-in-out` | LinkNode |
### 2.5 动画相关变量
| 变量名 | 默认值 | 用途 |
|---|---|---|
| `--typewriter-fade-duration` | `900ms` | 打字机淡入时长 |
| `--typewriter-fade-ease` | `ease-out` | 打字机缓动函数 |
| `--stream-update-fade-duration` | 同上 | 流式更新淡入时长 |
| `--stream-update-fade-ease` | 同上 | 流式更新缓动函数 |
---
## 3. 色彩体系(Color Palette)
### 3.1 语义色彩
| 语义 | 色值 | 用途 |
|---|---|---|
| Note / Info | `#448aff` | 信息提示 |
| Tip | `#00bfa5` | 技巧提示 |
| Warning | `#ff9100` | 警告提示 |
| Danger / Error | `#ff5252` | 危险/错误提示 |
| Link | `#0366d6` | 链接色 |
| Highlight | `#ffff00` | 文本高亮 |
| Diff Added | `#14b8a6` (light) / `#5eead4` (dark) | 新增行 |
| Diff Removed | `#ff3658` (light) / `#fda4af` (dark) | 删除行 |
### 3.2 灰阶色彩(散落于各组件)
| 色值 | 出现位置 |
|---|---|
| `#111827` (gray-900) | 代码区前景、提示框文字 |
| `#e5e7eb` (gray-200) | 代码区暗色前景 |
| `#e2e8f0` (slate-200) | 引用线、分割线 |
| `#cbd5e1` (slate-300) | 表格边框、列表标记 |
| `#64748b` (slate-500) | 有序列表标记 |
| `#374151` (gray-700) | 提示框标题文字 |
| `#eaecef` | 提示框边框、脚注边框 |
| `#0b1220` | 暗色提示框背景 |
---
## 4. 排版(Typography)
### 4.1 标题层级
| 等级 | 字号 | 字重 | 上边距 | 下边距 | 行高 |
|---|---|---|---|---|---|
| h1 | `text-4xl` (2.25rem) | `font-extrabold` (800) | `0` | `calc(8/9*1em)` | `calc(10/9*1)` |
| h2 | `text-2xl` (1.5rem) | `font-bold` (700) | `2rem` | `1rem` | `calc(4/3*1)` |
| h3 | `text-xl` (1.25rem) | `font-semibold` (600) | `calc(8/5*1em)` | `calc(3/5*1em)` | `calc(5/3*1)` |
| h4 | `text-base` (1rem) | `font-semibold` (600) | `1.5rem` | `0.5rem` | 默认 |
| h5 | `text-base` (1rem) | 默认 | `0` | `0` | 默认 |
| h6 | `text-base` (1rem) | 默认 | `0` | `0` | 默认 |
共有基类:`font-medium leading-tight`
### 4.2 正文
| 元素 | 字号 | 字重 | 行距 | 边距 |
|---|---|---|---|---|
| 段落 `<p>` | 继承 | 继承 | 继承 | `1.25em 0` |
| 行内代码 | `85%` | `font-mono` | - | `px-1 py-0.5` |
| 引用块 | 继承 | `500` | - | `mt-1.6em mb-1.6em pl-1em` |
| 定义列表 term | 继承 | `600` | - | `mt-0.5rem` |
| 定义列表 desc | 继承 | 继承 | - | `ml-1rem mb-0.5rem` |
| 脚注 | `text-sm` | - | `leading-relaxed` | `mt-2 mb-2` |
### 4.3 代码块
| 属性 | 值 |
|---|---|
| 字号 | `var(--vscode-editor-font-size, 12px)` |
| 字体 | monospace(系统等宽) |
| 标题字号 | `14px` |
| 标题字重 | `650` |
| 标题字间距 | `0.01em` |
| 说明文字字号 | `12px` |
---
## 5. 间距(Spacing)
### 5.1 块级元素垂直间距
| 元素 | margin-top | margin-bottom |
|---|---|---|
| 段落 | `1.25em` | `1.25em` |
| 标题 h1 | `0` | `calc(8/9*1em)` |
| 标题 h2 | `2rem` | `1rem` |
| 标题 h3 | `calc(8/5*1em)` | `calc(3/5*1em)` |
| 标题 h4 | `1.5rem` | `0.5rem` |
| 引用块 | `1.6em` | `1.6em` |
| 列表 | `1.25rem` | `1.25rem` |
| 列表项 | `0.5rem` | `0.5rem` |
| 提示框 | `1rem` | `1rem` |
| 表格 | `2rem` | `2rem` |
| 分割线 | `3rem` | `3rem` |
| Mermaid 图 | `1rem` | `1rem` |
| 定义列表 | `0` | `1rem` |
### 5.2 内边距(Padding)
| 元素 | padding |
|---|---|
| 行内代码 | `px-1 py-0.5` (4px 2px) |
| 提示框标题 | `0.5rem 1rem` |
| 提示框内容 | `0.5rem 1rem 1rem` |
| 引用块左侧 | `1em` |
| 列表缩进 | `calc(13/8*1em)` |
| 列表缩进(移动端) | `calc(14/9*1em)` |
| 列表项 | `pl-1.5` (6px) |
| 表格单元格 | `calc(4/7*1em)` |
| 代码块内容 | `8px 12px` (左侧 `52px` 含行号) |
---
## 6. 边框与圆角(Borders & Radius)
| 元素 | 边框 | 圆角 |
|---|---|---|
| 行内代码 | `1px solid hsl(var(--border) / 0.9)` | `rounded` (0.25rem) |
| 提示框 | `border-left: 4px solid` | `4px` |
| 引用块 | `border-left: 0.25rem solid` | - |
| 代码块 | `var(--markstream-code-border-color)` | 由 Monaco 控制 |
| 表格 | `var(--table-border, #cbd5e1)` | - |
| 分割线 | `border-t` | - |
| 脚注 | `border-t border-[#eaecef]` | - |
| Mermaid 图 | `border rounded-lg` | `0.5rem` |
| Tooltip | `border rounded-md` | `0.375rem` |
---
## 7. 阴影(Shadows)
| 元素 | 阴影值 |
|---|---|
| Mermaid 块 | `shadow-sm` |
| Tooltip | `shadow-md` |
| Diff 外框 | `0 16px 40px -32px rgb(15 23 42 / 0.18)` |
| Diff shell | `0 30px 70px -48px rgb(15 23 42 / 0.42)` |
| Mermaid 模态弹窗 | `shadow-lg` |
| Mermaid 激活按钮 | `shadow-sm` |
---
## 8. 性能相关令牌
| 属性 | 值 | 组件 |
|---|---|---|
| `content-visibility` | `auto` | markdown-renderer, CodeBlockNode, MermaidBlockNode |
| `contain` | `content` / `layout` | 多个组件 |
| `contain-intrinsic-size` | `320px 180px` | CodeBlockNode |
| `contain-intrinsic-size` | `360px 240px` | MermaidBlockNode |
| `container-type` | `inline-size` | CodeBlockNode |
@@ -0,0 +1,149 @@
# 排版调研:什么样的文本排版是舒适的
> 来源:2025-2026 主流产品分析 + 排版设计理论
---
## 核心发现
### 1. 舒适感来自"看不见的设计"
所有顶级产品(GitHub、Notion、Stripe、Linear)的排版有一个共同点:**你不会注意到它**。
用户从来不会说"Notion 的行高好舒服"——他们只会说"用 Notion 写东西很舒服"。
排版做对了,它就消失了。做错了,用户会感到莫名的不适但说不出为什么。
### 2. 字重比字号更重要
Apple HIG 的做法最能说明这一点:body 和 title 可以是同样的 17pt,
区分靠的是 Regular vs Semibold,而不是 16px vs 36px。
**行业趋势**:
- SemiBold (600) 正在取代 Bold (700) 成为标题首选字重——自信但不粗暴
- 极细字重(Ultralight/Thin)被 Apple 明确不推荐
- shadcn 的 h1 用 `extrabold`(800),h2 用 `semibold`(600)——标题之间也有字重梯度
**启示**:与其让 h4/h5/h6 都用 `text-base` 然后靠字重区分失败,不如用颜色、opacity、
大写字母这些维度来建立层级。Linear 用 dim(降低亮度)来区分次要信息。
### 3. 行宽是阅读舒适度的第一变量
| 来源 | 推荐行宽 |
|---|---|
| 排版理论 | 50-75 字符,66 字符最佳 |
| WCAG 无障碍 | 不超过 80 字符 |
| Baymard Institute | 超过 80 字符的内容被跳过率 +41% |
| CSS 实现 | `max-width: 65ch` 或 `30-40rem` |
**所有严肃的内容产品都限制行宽**:GitHub 980px,Notion 宽边距,Stripe 居中列。
没有一个做全宽正文。
### 4. 行高与行宽是联动的
| 场景 | 行高 |
|---|---|
| 正文(桌面) | 1.5-1.6x |
| 窄栏/移动端 | 1.3-1.45x |
| 标题 | 1.1-1.2x(紧凑,防止视觉碎片化) |
| UI 组件 | 1.2-1.3x |
**关键关系**:行宽越长,行高应该越大。短行配大行高会"散架",长行配小行高会"织成布"。
目标是"均匀的排版色彩"——看起来舒服、邀请阅读。
### 5. 标题缩放比例决定整体调性
| 比例 | 值 | 适用场景 | 感觉 |
|---|---|---|---|
| Major Second | 1.125 | 紧凑 UI、仪表盘 | 克制、高密度 |
| **Major Third** | **1.25** | **文档、内容应用** | **平衡、专业** |
| Perfect Fourth | 1.333 | 营销、多样内容 | 有层次、明快 |
| Perfect Fifth | 1.5 | 编辑排版、长文 | 戏剧性、正式 |
**Major Third (1.25) 是多数内容应用的最佳起点**。
### 6. 留白是奢侈品
感觉最"高级"的产品——Notion、Stripe、Linear——恰恰是留白最慷慨的。
留白传达品质感。但过多的留白会让内容感觉断裂、需要过多滚动。
**段落间距共识**:
- 段落之间:至少 1em,理想为行高的 1.5 倍
- 标题上方的空间应该是下方的 1.5 倍以上(接近原则:标题属于后面的内容)
- Wichita State 研究:合理的段落间白可以提升理解力 20%
### 7. 衬线字体正在回归
**Claude.ai 是旗舰案例**:AI 回复用衬线体,UI 用无衬线——这是对无衬线单一文化的声明。
驱动因素:
- 高分辨率屏幕让衬线清晰锐利
- 几何无衬线的审美疲劳
- 追求温暖感和人格化
- "过度抛光的 AI 审美正在造成视觉疲劳,设计师转向更有质感的字体"
**对渲染库的启示**:同时支持衬线和无衬线(通过 `--ms-font-sans` / `--ms-font-mono` token)
是前瞻性的。
### 8. 流式渲染需要系统字体
ChatGPT 主要用 `system-ui` 不是偷懒——是性能工程。
自定义字体意味着流式渲染时的布局跳动(layout shift),系统字体意味着即时稳定。
**对 markstream-vue 的启示**:
- 默认字体栈应该是 `system-ui` 优先(零加载、零跳动)
- 通过 `--ms-font-sans` token 允许用户切换到 Inter/Geist/衬线
- 字体加载策略本身就是排版决策
### 9. Dark 模式不是可选的
每一个调研的产品都支持。Claude、Linear、Cursor 默认暗色。
Tailwind 的 `prose-invert`、github-markdown-css 的 `prefers-color-scheme`
都说明这是基础能力。
### 10. 排版正在变成设计令牌系统
Vercel(Geist tokens)、Stripe(design tokens)、USWDS(font-size tokens)、
shadcn(CSS variables)。硬编码值在消亡,一切通过变量/token 流转。
这正是我们在做的。
---
## 主流产品排版定位
| 产品 | 感觉 | 关键特征 |
|---|---|---|
| **GitHub** | 功能性、隐形、实用 | 不引人注目的排版,让内容说话 |
| **Notion** | 干净、通透、冥想感 | 大量留白,内容漂浮在空间中 |
| **Linear** | 专业、克制、工程感 | 次要元素降亮度,只让重要内容突出 |
| **Stripe** | 精致、权威、可信赖 | 三栏布局,内容与代码对话 |
| **Vercel/Geist** | 瑞士现代、精确、开发者原生 | 三层类型系统(Heading/Label/Copy) |
| **ChatGPT** | 干净、机构感、高效 | 系统字体优先,零布局跳动 |
| **Claude** | 温暖、文学感、有人格 | **衬线体做 AI 回复**——反无衬线单一文化 |
---
## 对 markstream-vue 的建议
### 默认排版方向
不需要5种 preset,需要**1种好的默认**+ 通过 token 可调。
**推荐方向:Technical Clean + 一点 Notion 的呼吸感**
理由:
- markstream-vue 的主要场景是 AI 对话 + 开发文档 + 内容渲染
- 用户期望"专业但不冷淡"的感觉
- 需要同时服务代码密集和纯文字密集的场景
### 具体参数方向
| 维度 | 建议 | 依据 |
|---|---|---|
| 内容宽度 | `max-width: 65ch` | 行业共识 |
| 正文字号 | 16px (1rem) | 所有产品的最低线 |
| 正文行高 | 1.6 | 无障碍 + 舒适的交叉点 |
| 标题缩放 | Major Third (1.25) | 文档/内容应用的平衡点 |
| 标题字重 | h1: 700, h2: 600, h3-h4: 600, h5-h6: 500 | Apple + shadcn 的 weight > size 趋势 |
| 段落间距 | 1.5em | 行高的约 1x |
| 标题上下比 | 上 1.5x : 下 1x | 接近原则 |
| 默认字体 | `system-ui` → 可通过 token 切换 | 流式渲染零跳动 |
| h5/h6 区分 | opacity/color 降级,不是字号 | Linear/Apple 的做法 |
@@ -0,0 +1,192 @@
# 视觉设计评审
> 基于 `/example` 页面的 light + dark 截图,逐组件审视当前视觉状态。
> 标注"需重设计"的项是 issue #343 视觉重设计的具体工作。
---
## 需要重设计的元素
### 1. 标题层级 — 需重设计
**现状问题**:
- h4/h5/h6 视觉上几乎无法区分,都是同样大小的 `text-base`
- h1 和 h2 之间跳跃太大(4xl → 2xl)
- h5/h6 没有任何视觉区分度(无字重差异、无颜色差异、无间距差异)
**建议方向**:
- h4 可用 muted-foreground 或小号全大写字母来区分
- h5/h6 可用更小字号或不同字重
- 整体建立更平滑的字号梯度
---
### 2. 表格 — 需重设计
**现状问题**:
- 表头没有背景色,和正文行无法区分
- 无斑马纹,多行时视觉追踪困难
- 边框太淡(尤其 dark 模式下几乎不可见)
- 无 hover 行高亮
- 整体非常朴素,缺乏结构感
**建议方向**:
- 表头加 `--table-header-bg` 背景
- 偶数行加微弱背景色(斑马纹)
- 增强 dark 模式下边框可见度
- 增加 hover 行高亮
---
### 3. 引用块(Blockquote) — 需重设计
**现状问题**:
- 左边框颜色太淡,dark 模式下几乎不可见
- 整体过于朴素,没有背景色区分
- 与正文的视觉层级差异不够
**建议方向**:
- 增加微弱的背景色(如 `--ms-muted` 低透明度)
- 左边框加粗或加深颜色
- 可考虑使用 accent 色作为左边框
- Dark 模式下需要明显更亮的边框
---
### 4. 代码块 — 需重设计
**现状问题**:
- Header 背景和页面背景融为一体,没有区分
- 代码区域在 light 模式下是白色底,和页面白色底无边界感
- 整个容器的阴影太轻(`shadow-sm`),不够突出
- Dark 模式下代码块容器和页面背景的对比度不足
**建议方向**:
- Header 需要明确的 `--code-header-bg` 背景(当前 token 已定义但视觉太淡)
- 考虑加深容器边框或增加阴影
- Light 模式代码区域可用微灰背景(而非纯白)
- 代码块作为"嵌入内容"应该在视觉上与正文有明确的层级分离
---
### 5. 分割线(HR) — 需微调
**现状问题**:
- 太细太淡,dark 模式下几乎不可见
- 上下间距很大(`my-12`)但线本身视觉权重不够
**建议方向**:
- 增加线的粗细或颜色深度
- 或改为渐变消隐风格(从中心向两侧淡出)
---
### 6. 列表标记 — 需微调
**现状问题**:
- 无序列表圆点颜色太淡(`--list-marker` = border 色)
- 有序列表数字颜色也偏淡
- Task list 的 checkbox 太小,无自定义样式
**建议方向**:
- 标记色加深一档(用 `--ms-muted-foreground` 而非 `--ms-border`)
- Checkbox 可自定义样式(圆角、颜色、大小)
---
### 7. 定义列表 — 需重设计
**现状问题**:
- 纯文本渲染,术语和描述之间缺乏视觉层级
- 没有背景、边框或缩进的视觉区分
- 和普通段落看起来没区别
**建议方向**:
- 术语可用 accent 色或加背景标签
- 描述增加左缩进 + 左边框(类似引用块)
- 或使用交替背景色
---
### 8. 脚注 — 需微调
**现状问题**:
- 脚注区域和正文的分隔线太淡
- 脚注内容在页面底部容易被忽略
- 缺乏"脚注区域"的整体框架感
**建议方向**:
- 分隔线加深或改用双线
- 增加微弱背景色标示脚注区域
- 脚注编号可加 accent 色
---
### 9. 高亮文本(Highlight) — 需微调
**现状问题**:
- Light 模式下亮黄色 OK
- Dark 模式下黄色变暗了但可能和文字对比度不够
**建议方向**:
- Dark 模式下可用更亮的荧光色或调整透明度
- 确保 WCAG AA 对比度达标
---
## 当前表现良好的元素
### Admonitions — 良好
- 6 种类型颜色区分明显
- 左边框 + 标题栏 + 图标结构清晰
- Light/Dark 模式切换正常
- 有自定义标题功能
### 数学公式 — 良好
- 行内和块级都正确渲染
- KaTeX 排版质量好
### 链接 — 良好
- 蓝色链接色在两种模式下都清晰
- Hover 下划线效果正常
### 段落 & 内联元素 — 良好
- Bold、italic、strikethrough、inline code、subscript、superscript 都正常
- Inline code 有背景色和边框,辨识度高
### Mermaid 图 — 良好
- 容器有 header + 模式切换
- 边框和阴影提供了层级感
### 图片 — 良好
- 正确加载并显示
- 有 max-width 限制
---
## 整体节奏问题
### 间距不统一
- 段落 `1.25em`、引用块 `1.6em`、列表 `1.25rem`、表格 `2rem`、分割线 `3rem` — 缺乏统一节奏
- 代码块和表格的上下间距不协调
### Dark 模式对比度
- 多个元素在 dark 模式下对比度不足(分割线、表格边框、引用块边框、列表标记)
- 需要整体提升 dark 模式下的边框/分隔元素亮度
---
## 优先级排序
| 优先级 | 元素 | 原因 |
|---|---|---|
| P0 | 表格 | 当前最朴素,用户感知最强 |
| P0 | 代码块容器 | 高频组件,当前缺乏层级感 |
| P1 | 引用块 | dark 模式下几乎不可见 |
| P1 | 标题层级 | h4-h6 无法区分 |
| P1 | Dark 模式整体对比度 | 多个元素受影响 |
| P2 | 定义列表 | 缺乏视觉结构 |
| P2 | 分割线 | 视觉权重不够 |
| P2 | 列表标记 | 太淡 |
| P2 | 脚注 | 框架感不足 |
| P3 | 高亮文本 | dark 对比度 |