Files
boos e0a5f57793 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.
2026-06-22 11:32:57 +08:00

13 KiB
Raw Permalink Blame History

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:

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 }
}

切换逻辑

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
})

用法示例

<!-- 自动切换(大多数场景) -->
<CodeBlock :theme="{ light: 'github-light', dark: 'one-dark-pro' }" />

<!-- 固定主题(不随页面切换) -->
<CodeBlock theme="monokai" />

<!-- 默认(内置 light/dark) -->
<CodeBlock />

向后兼容

保留 darkTheme / lightTheme prop 作为 deprecated alias:

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 的角色

触发规则

const editorSurfaceIsDark = computed(() => {
  const t = activeTheme.value
  // 配对模式:跟随页面
  if (!props.theme || isPairedTheme(props.theme))
    return isDark.value
  // 单主题模式:检测主题名/亮度
  return detectThemeDarkness(t)
})

检测主题明暗

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 渐变,直接使用编辑器背景。

/* 之前 */
.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 内嵌在各组件中,自定义组件直接渲染