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

439 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 两套运行时不统一的问题。
> 本轮完成 goal`5-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.js``block_handle_menu_view.rs``mindmap_node_view.rs``sidebar-page-ai-runtime.js` 各写一份。
- outside click 关闭:同上。
- focus trap:仅 filetree menu 有基础实现,dialog 没有。
- body scroll lock:不存在。
- z-index 管理:无统一层叠规范,`1200``1000``999` 散落在 `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 没有 `lint``test``format` 脚本。
- 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.rs``styles/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.toml``radix-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 尺寸与间距等级
```css
--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 阴影等级
```css
--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 层级
```css
--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 组装方式
```rust
// 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 依赖
```toml
# 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
```html
<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. 前端工程守门
```json
{
"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
- [x] A1. 从 `styles.rs` 提取 Layer 0-1 变量到 `tokens.css`
- [x] A2. 补充 Layer 2-3 变量(组件 token、状态 token、间距、阴影、z-index)— 已补 font-size、button component tokens、radius、shadow overlay、toast z-index
- [ ] A3. 按组件/页面拆分 `styles.rs``styles/` 目录 — ⚠️ 目录已建但未按组件拆分: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` 体系
- [x] A5. 保留 `mnote_css_is_reasonably_sized` 护栏通过(836/838 tests pass
- [ ] A6. 所有现有 smoke 通过 — ⚠️ 2 个预存测试失败(非 5-37 引入),smoke 待运行
### Phase B:编辑器 island radix-leptos 集成(P0
- [x] B1. `leptos-tiptap-spike``radix-leptos-primitives` 依赖,编译通过
- [x] B2. 用 `DropdownMenu` 替换 `block_handle_menu_view` 手写菜单
- [x] B3. 用 `CommandPalette` 替换 `slash_menu_view` 键盘导航
- [x] 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
- [x] C1. `layout.rs` 新增 `#mnote-portal-root`
- [x] C2. JS runtime 新增 `mnote.toast()` API`mnote-ui-runtime.js`
- [ ] C3. 替换 `fallback: 'alert'` 为 toast 调用 — ❌ 未替换
- [ ] C4. 编辑器新增 `<ToastProvider>` + `use_toast()` — ❌ 未实现
- [x] C5. Toast 样式符合 Wolai 风格(`components/toast.css`
- [x] C6. 补 toast smoke`task500-ui-debug-components-smoke.js` 覆盖 UI debug toast,可输出截图)
### Phase D:图标系统统一(P0
- [x] D1. 建立 `ICON_MAP` 常量(`icons.rs` 有 48 个映射)
- [ ] D2. 编辑器 Unicode 文本符号替换为 Material Symbolsvia `material_icon()`)— ⚠️ source 已替换,当前 checked-in generated island 仍可见旧 block/slash glyph,需补 artifact 生成链路
- [x] D3. mindmap emoji 图标统一(`mindmap_node_view.rs` 已修改)
- [x] D4. 所有 icon 使用同一 `<span class="material-symbols-outlined">` 约定
### Phase E:组件演示页与可视化验收(P1)
- [x] E1. 新增 `/ui-debug/components` 路由(需要 `MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES=1`
- [ ] E2. 展示 P0 组件 variant × size × state 矩阵 — ⚠️ 路由已建但 debug routes 默认关闭,未验证
- [x] E3. 补组件级 Playwright smoke`task500-ui-debug-components-smoke.js`
- [x] 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_star``mnote_ui_runtime_exposes_toast_api` 通过,证明 portal/toast runtime 已挂入主壳。
- `cargo build --lib --target wasm32-unknown-unknown --release``rust/spikes/leptos-tiptap-spike` 通过,证明当前 editor island source 可编译。
- 本地 Chrome 截图验证三处用户报告回归:`tmp/5-37-ui-regression-manual/01-initial-document.png``02-after-knowledge-click.png``03-after-workspace-click.png``04-after-page-ai-click.png`;Page AI、知识库按钮、左上角空间切换均可见并可交互。
- `task490-runtime-surfaces-smoke.js` 仍未完成通过:当前 checked-in generated island 中 block handle menu 仍泄漏旧 `↻` glyph;直接重新生成 artifact 会因 `tachys`/`leptos``ssr` feature unification 在浏览器端 panic,需先修正生成链路再把 D2/B8 标为完成。
```bash
# 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(保持对照参考)