重构基线

This commit is contained in:
Agent Board
2026-06-25 21:08:17 +08:00
parent cc4f975466
commit dabaf03bd7
42 changed files with 7720 additions and 138 deletions
@@ -0,0 +1,438 @@
# 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(保持对照参考)