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,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 | 清理:删除废弃变量、统一动画变量 | 最终打磨 |