20 KiB
5-37 [process] MNote UI Foundation — 统一设计系统与组件基础设施 v1
创建时间:2026-06-15
状态:process
Owner:05-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. 设计原则
- 单一真相源:token 只有一份定义,SSR 和 CSR 共同引用。
- 渐进迁移:不在主壳大规模重写 SSR 架构;先在编辑器 island 落地 primitive,再逐步上溯到主壳。
- headless 优先:组件 primitive 不绑定视觉风格,外观由 Wolai 设计 token + CSS 变量控制。
- 事件驱动,取消轮询:所有 UI 状态同步优先走 realtime event stream / MutationObserver / command result,不新增
setInterval轮询。 - 可测试:每个 primitive 必须有组件级 smoke,每个 variant/state 有截图或 DOM 断言覆盖。
- 不新增依赖债务:只用成熟、有维护者的 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 架构,但做三件事:
- CSS 拆分:
styles.rs→styles/tokens.css+styles/components/+styles/pages/。 - 交互契约统一:参考 radix-leptos 的 hook 行为语义,统一 JS runtime 中的 escape/focus trap/outside click 模式。
- 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 尺寸与间距等级
--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.rs为styles/目录 — ⚠️ 目录已建但未按组件拆分:reset/layout 合为 base.css,components/ 只有单文件 main.css(5693行),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-spike加radix-leptos-primitives依赖,编译通过 - B2. 用
DropdownMenu替换block_handle_menu_view手写菜单 - B3. 用
CommandPalette替换slash_menu_view键盘导航 - B4. 用
DropdownMenu替换table_toolbar_viewoptions 菜单 - B5. 编辑器内
<button>使用 radixButton组件 — ⚠️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 panic,bundle 增量未作为完成证据 - 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()API(mnote-ui-runtime.js) - C3. 替换
fallback: 'alert'为 toast 调用 — ❌ 未替换 - C4. 编辑器新增
<ToastProvider>+use_toast()— ❌ 未实现 - C5. Toast 样式符合 Wolai 风格(
components/toast.css) - C6. 补 toast smoke(
task500-ui-debug-components-smoke.js覆盖 UI debug toast,可输出截图)
Phase D:图标系统统一(P0)
- D1. 建立
ICON_MAP常量(icons.rs有 48 个映射) - D2. 编辑器 Unicode 文本符号替换为 Material Symbols(via
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 smoke(
task500-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_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的ssrfeature 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 hydration(Phase F 只统一样式,不迁移渲染架构)
- 不引入 Tailwind CSS
- 不引入 React/Next.js 组件库
- 不新建 npm 前端项目
- 不在本设计做暗色模式完整方案(Good Night 已有基础,后续单独设计)
- 不清理 recycle/wolai-frontend(保持对照参考)