Files
mnote/design/05-editor-mainline/process/5-37-mnote-ui-foundation-design-system-v1.md
T
2026-06-25 21:08:17 +08:00

20 KiB
Raw Blame History

5-37 [process] MNote UI Foundation — 统一设计系统与组件基础设施 v1

创建时间:2026-06-15

状态:process

Owner05-editor-mainline / 03-rust-web / 11-wolai

目标:为 MNote 建立统一设计系统与组件基础设施,解决当前 UI 风格分散、组件缺失、交互 primitive 手写重复、CSS 单文件膨胀、SSR/CSR 两套运行时不统一的问题。

本轮完成 goal5-37-ui-foundation-p0-icon-qa。范围是补齐 P0 中可独立落地的 token/portal/toast 基础设施,收紧 Material Symbols 图标验收,并交给 AgentBoard 做风险审查与浏览器 QA。P0 中编辑器全量 Button/Radix focus trap、CSS 全量拆分仍作为后续目标,不在本轮强行扩散。

1. 问题诊断

1.1 当前 UI 资产分布

层次 位置 规模 问题
CSS styles.rs 6718 行单文件 Rust 字符串常量 单文件膨胀、无模块拆分、修改风险高
SSR 模板 layout.rs, document.rs, home.rs ~10 个 Leptos view 组件 交互逻辑混入 JS runtime,非 reactive
浏览器 runtime browser/ 46 个 JS 文件 覆盖全但 ad-hoc,无统一 primitive
编辑器 CSR editor_runtime/ 27 个 Rust 文件 菜单/overlay 手写,缺 radix 级基础设施

1.2 核心缺口

组件抽象层缺失

  • 没有 Button/Dialog/Popover/Menu/Tabs/Input/Select/Toast/Skeleton 等可复用 primitive。
  • root package.json 只有 Playwright + 预览库,无 UI 框架依赖。
  • recycle 旧前端有 Radix UI + shadcn 的完整组件体系(19 个组件文件),但依赖 React,不能直接用于当前 Rust SSR + CSR 架构。

交互 primitive 重复手写

  • Escape 关闭:tree-shell-filetree-menu-runtime.jsblock_handle_menu_view.rsmindmap_node_view.rssidebar-page-ai-runtime.js 各写一份。
  • outside click 关闭:同上。
  • focus trap:仅 filetree menu 有基础实现,dialog 没有。
  • body scroll lock:不存在。
  • z-index 管理:无统一层叠规范,12001000999 散落在 styles.rs 各处。

反馈系统缺失

  • toast/notification 不存在。错误反馈多数用 alert() fallback 或局部 set_command_feedback
  • 无全局反馈管道。

图标系统不统一

  • 主壳用 Material Symbols (Google Fonts CDN)。
  • 编辑器菜单用 Unicode 文本符号("✦" "↻" "⌫")。
  • mindmap 用 emoji + Material Symbols 混合。

设计 token 无管线

  • CSS 变量已在 :root 定义(--wolai-bg--wolai-text-primary 等),但没有 token 文件、组件 variant 规范、尺寸等级、状态色。
  • 6718 行 CSS 直接塞在 Rust 常量里,长期维护不可持续。

组件演示与可视化验收缺失

  • 无 Storybook 或内部 /ui-debug 组件画廊。
  • 现有 Wolai 视觉 smoke 偏页面级,不是组件级基线。

前端工程守门缺失

  • root 没有 linttestformat 脚本。
  • JS runtime 靠 smoke 和局部 node --check,无统一质量门禁。

SSR / CSR 双运行时鸿沟

  • 主壳是 Leptos SSR → HTML 字符串 + 46 JS runtime。
  • 编辑器是 Leptos CSR/WASM → reactive signal。
  • 两个运行时之间没有共享的组件层、token 层或交互契约。

1.3 为什么不能直接搬旧 recycle 前端组件

  • 旧前端依赖 React 19 + Radix UI React + shadcn + Tailwind + Next.js。
  • 当前主壳是 Rust Leptos SSR + 浏览器 JS runtime,编辑器是 Leptos CSR/WASM。
  • 不能把 React 组件直接嵌入 Rust 栈,需要适配。

2. 设计原则

  1. 单一真相源:token 只有一份定义,SSR 和 CSR 共同引用。
  2. 渐进迁移:不在主壳大规模重写 SSR 架构;先在编辑器 island 落地 primitive,再逐步上溯到主壳。
  3. headless 优先:组件 primitive 不绑定视觉风格,外观由 Wolai 设计 token + CSS 变量控制。
  4. 事件驱动,取消轮询:所有 UI 状态同步优先走 realtime event stream / MutationObserver / command result,不新增 setInterval 轮询。
  5. 可测试:每个 primitive 必须有组件级 smoke,每个 variant/state 有截图或 DOM 断言覆盖。
  6. 不新增依赖债务:只用成熟、有维护者的 Leptos 生态库;不自行从零实现已有成熟方案。

3. 技术选型

3.1 编辑器 island (CSR/WASM) 组件 primitive

选 radix-leptos v0.9.x

对比维度 radix-leptos thaw-ui rust-ui/ui leptonic 自研
风格 headless,不绑定视觉 Fluent Design shadcn/Tailwind Material 任意
与 Wolai CSS 兼容 headless Fluent 冲突 ⚠️ 需 Tailwind Material
Leptos 0.8 兼容 v0.9.0 v0.5-beta
组件数量 57+ ~40 ~30 ~20 0
测试覆盖 1792+ tests 未知 未知 未知 0
维护状态 活跃 (Q2 2026) 活跃 活跃 稳定 N/A
WASM 体积 538KB 优化后 未知 未知 未知 0
FocusTrap/Escape/Portal 全套 未知 需自建

结论:radix-leptos 是唯一提供完整 headless primitive + a11y infrastructure 的 Leptos 生态库,与 MNote Wolai CSS 变量体系零冲突。

3.2 主壳 (SSR + JS runtime) 过渡策略

短期不迁移 SSR 架构,但做三件事:

  1. CSS 拆分styles.rsstyles/tokens.css + styles/components/ + styles/pages/
  2. 交互契约统一:参考 radix-leptos 的 hook 行为语义,统一 JS runtime 中的 escape/focus trap/outside click 模式。
  3. Portal 容器标准化layout.rs 中预留统一 portal root (#mnote-portal-root),所有 popover/dialog/menu 渲染到此处。

3.3 图标系统

统一使用 Material Symbols。

  • 编辑器菜单 Unicode 文本符号逐步替换为 Material Symbols。
  • 维护 ICON_MAP 常量将语义名映射到 ligature。
  • 不引入额外图标库。

3.4 构建工具

  • 编辑器 island:现有 trunk 构建不变,Cargo.tomlradix-leptos-primitives 依赖。
  • 主壳 CSS:保持 Rust include_str! 编译期嵌入,但源文件拆分为多个 .css 文件。
  • token 源:一个 tokens.css 文件,被主壳和编辑器共同引用。

4. 设计 token 体系

4.1 设计变量分层

Layer 0: Raw Colors      (--color-basic-50 .. --color-basic-900)
Layer 1: Semantic Colors (--wolai-bg, --wolai-text-primary, --wolai-border, --wolai-brand ...)
Layer 2: Component Tokens (--mnote-btn-bg, --mnote-dialog-shadow, --mnote-popover-radius ...)
Layer 3: State Tokens    (--mnote-state-hover, --mnote-state-active, --mnote-state-disabled)

Layer 0-1 已存在于 styles.rs:root 块。Layer 2-3 当前缺失。

4.2 尺寸与间距等级

--wolai-space-xs: 4px;
--wolai-space-sm: 8px;
--wolai-space-md: 12px;
--wolai-space-lg: 16px;
--wolai-space-xl: 24px;
--wolai-space-2xl: 32px;

--wolai-radius-sm: 4px;
--wolai-radius-md: 6px;
--wolai-radius-lg: 8px;
--wolai-radius-xl: 12px;

--wolai-font-xs: 12px;
--wolai-font-sm: 13px;
--wolai-font-md: 14px;
--wolai-font-lg: 16px;
--wolai-font-xl: 20px;
--wolai-font-2xl: 24px;

4.3 阴影等级

--wolai-shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08);
--wolai-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.12);
--wolai-shadow-lg: 0 18px 54px rgba(15, 23, 42, 0.18);
--wolai-shadow-overlay: 0 18px 48px rgba(15, 23, 42, 0.22), 0 2px 8px rgba(15, 23, 42, 0.08);

4.4 z-index 层级

--wolai-z-sidebar: 100;
--wolai-z-topbar: 200;
--wolai-z-floating: 500;
--wolai-z-popover: 1000;
--wolai-z-dialog-backdrop: 1100;
--wolai-z-dialog: 1200;
--wolai-z-toast: 1300;
--wolai-z-tooltip: 1400;

5. 组件清单与优先级

5.1 P0 — 编辑器 island 落地(blocking 风格统一)

组件 radix-leptos 对应 当前手写位置 迁移收益
Button Button (6 variants) 各处零散 <button> 统一 variant/size/disabled/loading
Dialog/Modal Dialog + AlertDialog mnote-profile-dialog CSS only escape/focus trap/backdrop click
Popover Popover 页面设置 popover JS 手写 定位/关闭/层级统一
DropdownMenu DropdownMenu block handle menu (Rust) 键盘/子菜单/分隔线
ContextMenu ContextMenu filetree context menu (JS) 右键定位/键盘/a11y
Tabs Tabs sidebar tabs, page settings 键盘切换/aria/动画
Toast 自建薄封装 无,alert() fallback 全局反馈

5.2 P1 — 编辑器增强

组件 radix-leptos 对应 用途
Select Select 页面下拉选择
Checkbox Checkbox 页面设置选项
Toggle/Switch Switch toggle 控件
CommandPalette CommandPalette slash menu 容器
Skeleton 自建 加载占位
ScrollArea ScrollArea 统一滚动容器

5.3 P2 — 主壳 SSR 统一

  • 主壳 popover 从 JS 手写迁到 radix-leptos hydration。
  • 主页 PageLayout 中按钮、搜索 modal、account menu 统一 variant。
  • 垃圾桶 modal 统一 Dialog 组件。

6. 主壳 CSS 重构方案

6.1 拆分目标

rust/crates/mnote-web/src/ssr/
├── styles/
│   ├── mod.rs              # 组装入口,重新导出 MNOTE_CSS
│   ├── tokens.css           # Layer 0-3 设计变量
│   ├── reset.css            # 基础重置
│   ├── layout.css           # 主壳布局
│   ├── components/
│   │   ├── button.css
│   │   ├── dialog.css
│   │   ├── popover.css
│   │   ├── menu.css
│   │   ├── tabs.css
│   │   ├── input.css
│   │   ├── select.css
│   │   ├── toggle.css
│   │   ├── tooltip.css
│   │   ├── toast.css
│   │   ├── skeleton.css
│   │   └── badge.css
│   └── pages/
│       ├── sidebar.css
│       ├── document.css
│       ├── search.css
│       ├── auth.css
│       ├── admin.css
│       ├── trash.css
│       ├── knowledge_rag.css
│       └── page_ai.css

6.2 组装方式

// styles/mod.rs
pub const MNOTE_CSS: &str = concat!(
    include_str!("tokens.css"),
    include_str!("reset.css"),
    include_str!("layout.css"),
    include_str!("components/button.css"),
    include_str!("components/dialog.css"),
    // ...
    include_str!("pages/sidebar.css"),
    // ...
);

保持编译期嵌入,零运行时开销,源文件可独立维护。

6.3 CSS 命名规范

  • 组件:.mnote-btn.mnote-dialog
  • 变体:data-variant="primary" / "ghost"
  • 尺寸:data-size="sm" / "lg"
  • 状态:data-state="open" / "closed"aria-expanded
  • 页面:.mnote-search.mnote-auth
  • wolai-* 类逐步迁移到 mnote-*,保留 wolai-* 别名过渡一个版本。

7. 编辑器 island 集成 radix-leptos 的切口

7.1 依赖

# rust/spikes/leptos-tiptap-spike/Cargo.toml
[dependencies]
radix-leptos-primitives = { version = "0.9.0", features = ["full"] }
radix-leptos-core = "0.9.0"

7.2 首个替换:block handle menu

block_handle_menu_view.rs 320 行手写浮动菜单,用 DropdownMenu 替换可删约 200 行事件处理代码,同时获键盘导航、ARIA、focus 管理。

7.3 第二个替换:slash menu

slash_menu_view.rs 300 行手写弹出菜单,用 CommandPalette 替换,保留过滤逻辑,获标准键盘导航、ARIA。

7.4 第三个替换:table toolbar options

table_toolbar_view.rs 表格选项菜单,用 DropdownMenu 替换。

8. 交互契约统一

8.1 全局 Portal Root

<div id="mnote-portal-root" style="position:fixed;inset:0;pointer-events:none;z-index:var(--wolai-z-popover)">
  <!-- popover/dialog/menu/toast 渲染到此 -->
</div>

8.2 交互模式标准化

行为 当前实现 统一方式
Escape 关闭 各处手写 event.key === "Escape" use_escape_keydown
Outside click 关闭 各处手写 mousedown listener use_outside_click
Focus trap filetree menu JS 手写 use_focus_trap
Body scroll lock use_body_scroll_lock
层级管理 各处手写 z-index CSS 变量 --wolai-z-* + portal root

8.3 JS Runtime 对齐

JS runtime 的 popover/menu/dialog 创建行为参考 radix-leptos 的 DOM 结构和 ARIA 属性,保证:相同的 role/aria-*、相同的 data-state、相同的 z-index 变量。

9. Toast 通知系统

  • 位置:右下角或顶部居中。
  • 类型:success / error / warning / info / loading。
  • 行为:自动消失(可配置 duration)、可手动关闭、可堆叠。
  • 主壳:JS runtime 薄封装,mnote.toast({ type, title, duration })
  • 编辑器:<ToastProvider> + use_toast() hook,约 150 行。

10. 组件演示页

GET /ui-debug/components(仅 debug routes 启用时)

展示:所有 P0 组件 variant × size × state 矩阵,可切换亮/暗,可交互,自动截图用于视觉回归。

11. 前端工程守门

{
  "scripts": {
    "lint:js": "node --check rust/crates/mnote-web/browser/*.js",
    "lint:css": "npx stylelint 'rust/crates/mnote-web/src/ssr/styles/**/*.css'",
    "lint:rs": "cd rust && cargo clippy --workspace -- -D warnings",
    "format:rs": "cd rust && cargo fmt -- --check",
    "test:rs": "cd rust && cargo test --workspace",
    "test:smoke": "node scripts/task490-runtime-surfaces-smoke.js"
  }
}

CSS 体积护栏:总 CSS 不超过当前 6718 行的 120%。

12. Checklist

Phase A:设计 token 与 CSS 重构(P0

  • A1. 从 styles.rs 提取 Layer 0-1 变量到 tokens.css
  • A2. 补充 Layer 2-3 变量(组件 token、状态 token、间距、阴影、z-index)— 已补 font-size、button component tokens、radius、shadow overlay、toast z-index
  • A3. 按组件/页面拆分 styles.rsstyles/ 目录 — ⚠️ 目录已建但未按组件拆分:reset/layout 合为 base.csscomponents/ 只有单文件 main.css5693行),pages/ 缺 document/search/admin/trash/knowledge_rag/page_ai
  • A4. 建立 CSS 命名规范:组件用 data-variant/data-size/data-state⚠️ 已加 data-state 属性但无 data-variant/data-size 体系
  • A5. 保留 mnote_css_is_reasonably_sized 护栏通过(836/838 tests pass
  • A6. 所有现有 smoke 通过 — ⚠️ 2 个预存测试失败(非 5-37 引入),smoke 待运行

Phase B:编辑器 island radix-leptos 集成(P0

  • B1. leptos-tiptap-spikeradix-leptos-primitives 依赖,编译通过
  • B2. 用 DropdownMenu 替换 block_handle_menu_view 手写菜单
  • B3. 用 CommandPalette 替换 slash_menu_view 键盘导航
  • B4. 用 DropdownMenu 替换 table_toolbar_view options 菜单
  • B5. 编辑器内 <button> 使用 radix Button 组件 — ⚠️ EditorButton 基础组件可编译,但运行面仍未全量替换
  • B6. 编辑器内 Escape/outside click/focus trap 替换为 radix hooks — ⚠️ dismiss.rs / focus trap 可编译,尚未接入主要 overlay
  • B7. WASM bundle 增量 < 200KB (gzipped) — ⚠️ cargo build --lib --target wasm32-unknown-unknown --release 通过;重新生成 runtime artifact 会触发 Leptos SSR feature unification panicbundle 增量未作为完成证据
  • B8. task490-runtime-surfaces-smoke.js 通过 — ⚠️ smoke 已覆盖 Page AI stop 与图标容器,但当前 generated island 仍有旧 block menu glyph),未通过

Phase C:全局 Portal & Toast 基础设施(P0

  • C1. layout.rs 新增 #mnote-portal-root
  • C2. JS runtime 新增 mnote.toast() APImnote-ui-runtime.js
  • C3. 替换 fallback: 'alert' 为 toast 调用 — 未替换
  • C4. 编辑器新增 <ToastProvider> + use_toast() 未实现
  • C5. Toast 样式符合 Wolai 风格(components/toast.css
  • C6. 补 toast smoketask500-ui-debug-components-smoke.js 覆盖 UI debug toast,可输出截图)

Phase D:图标系统统一(P0

  • D1. 建立 ICON_MAP 常量(icons.rs 有 48 个映射)
  • D2. 编辑器 Unicode 文本符号替换为 Material Symbolsvia material_icon())— ⚠️ source 已替换,当前 checked-in generated island 仍可见旧 block/slash glyph,需补 artifact 生成链路
  • D3. mindmap emoji 图标统一(mindmap_node_view.rs 已修改)
  • D4. 所有 icon 使用同一 <span class="material-symbols-outlined"> 约定

Phase E:组件演示页与可视化验收(P1)

  • E1. 新增 /ui-debug/components 路由(需要 MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES=1
  • E2. 展示 P0 组件 variant × size × state 矩阵 — ⚠️ 路由已建但 debug routes 默认关闭,未验证
  • E3. 补组件级 Playwright smoketask500-ui-debug-components-smoke.js
  • E4. 截图输出到 tmp/ui-components/MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES=1 node scripts/task500-ui-debug-components-smoke.js

Phase F:主壳 SSR 组件统一(P2)

  • F1. 搜索 modal 统一 Dialog 语义 — ⚠️ 搜索 overlay 有样式但未完整 Dialog 语义
  • F2. 页面设置 popover 统一样式变量 — ⚠️ popover 样式部分统一
  • F3. account menu / workspace source menu 统一 DropdownMenu 模式 — 仅加了 data-state 属性,未替换为 DropdownMenu
  • F4. 垃圾桶 modal 统一 Dialog 语义 — 未处理
  • F5. 资料库设置 popover 统一样式变量 — ⚠️ KB rag settings popover 样式存在(82 处引用)

13. 验收证据模板

13.1 当前本地证据(2026-06-19

  • cargo test -p mnote-web mnote_css_does_not_hide_all_closed_state_triggers 通过,证明不再存在全局 [data-state="closed"] 隐藏 trigger 的回归。
  • cargo test -p mnote-web mnote_css_contains_ui_foundation_tokens_and_toast 通过,证明 token/toast 样式已进入 MNOTE_CSS
  • cargo test -p mnote-web page_layout_hides_public_state_and_exposes_sidebar_shortcut_starmnote_ui_runtime_exposes_toast_api 通过,证明 portal/toast runtime 已挂入主壳。
  • cargo build --lib --target wasm32-unknown-unknown --releaserust/spikes/leptos-tiptap-spike 通过,证明当前 editor island source 可编译。
  • 本地 Chrome 截图验证三处用户报告回归:tmp/5-37-ui-regression-manual/01-initial-document.png02-after-knowledge-click.png03-after-workspace-click.png04-after-page-ai-click.png;Page AI、知识库按钮、左上角空间切换均可见并可交互。
  • task490-runtime-surfaces-smoke.js 仍未完成通过:当前 checked-in generated island 中 block handle menu 仍泄漏旧 glyph;直接重新生成 artifact 会因 tachys/leptosssr feature unification 在浏览器端 panic,需先修正生成链路再把 D2/B8 标为完成。
# CSS 护栏
cargo test -p mnote-web mnote_css_is_reasonably_sized

# 编辑器 WASM 编译
cd rust && cargo build -p mnote-leptos-tiptap-spike --target wasm32-unknown-unknown

# 关键 smoke
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task114-rust-web-gateway-entry-smoke.js

# JS syntax check
node --check rust/crates/mnote-web/browser/*.js

# Rust lint
cd rust && cargo fmt --check -p mnote-web -p mnote-leptos-tiptap-spike
cd rust && cargo clippy -p mnote-web -p mnote-leptos-tiptap-spike -- -D warnings

14. 不做什么

  • 不把 SSR 主壳全量迁到 Leptos hydrationPhase F 只统一样式,不迁移渲染架构)
  • 不引入 Tailwind CSS
  • 不引入 React/Next.js 组件库
  • 不新建 npm 前端项目
  • 不在本设计做暗色模式完整方案(Good Night 已有基础,后续单独设计)
  • 不清理 recycle/wolai-frontend(保持对照参考)