对齐 Wolai 编辑器搜索与文档画布

This commit is contained in:
lix-2026
2026-04-30 18:36:50 +08:00
parent afb2a5b8a0
commit 2b88ecb4b9
14 changed files with 317 additions and 138 deletions
@@ -208,32 +208,118 @@ RED / GREEN
| ID | 状态 | 任务 | 验收要点 | | ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| C1 | TODO | 顶栏高度与按钮密度 | 顶栏约 40px;按钮轻量 hover;右侧 icon/text 序列不重 | | C1 | PARITY | 顶栏高度与按钮密度 | `task135` 覆盖顶栏约 40px、右侧按钮组无搜索入口、icon 不再渲染成黑色方块;`task119` 复核 topbarHeight=40 |
| C2 | TODO | breadcrumb | 图标、分隔符、弱色文字、截断方式对齐;切页后与页面树同步 | | C2 | PARITY | breadcrumb | `task135` 覆盖 breadcrumb 当前标题绑定、弱色分隔符改为 Wolai chevron;真实父路径数据仍依赖后续 Page Aggregate / tree projection |
| C3 | TODO | owner/published 按钮分型 | owner 与 published 的按钮组合分开;不把 published 文案硬塞 owner 态 | | C3 | PARITY | owner/published 按钮分型 | `task135` 覆盖 owner 顶栏绿色 `全网公开`,右侧按钮组从本地 AI/搜索收敛为 Wolai owner 态常见按钮序列 |
| C4 | TODO | 顶栏搜索入口 | 点击打开全局搜索 modal,URL 不变,焦点进入输入框 | | C4 | PARITY | 顶栏搜索入口基线纠偏 | 2026-04-30 Wolai owner 当前 viewport 顶栏无搜索入口;本地已移除顶栏搜索按钮,搜索统一走侧栏入口 / `Ctrl+P` |
| C5 | TODO | 侧栏搜索入口 | 打开同一个全局搜索 modal 或明确的树过滤入口;不得误跳 `/search` | | C5 | PARITY | 侧栏搜索入口 | `task135` 覆盖侧栏左上搜索打开同一个全局搜索 modal,URL 不变;modal 打开时再次点击侧栏搜索会关闭 |
| C6 | TODO | 搜索 modal 默认状态 | `仅匹配标题``页面内搜索` 为 switch 且默认开启;不是普通 pill button | | C6 | PARITY | 搜索 modal 默认状态 | `task135` 按最新 Wolai 实测修正:空 query 先显示最近浏览,不直接显示筛选;输入后 switch 出现,`仅匹配标题` / `页面内搜索` 为 switch 且默认关闭 |
| C7 | TODO | 搜索 modal 关闭路径 | Esc、遮罩、再次 `Ctrl+P`、入口 toggle 按 Wolai 实测对齐 | | C7 | PARITY | 搜索 modal 关闭路径 | `task135` 覆盖 Esc、主区域遮罩、再次 `Ctrl+P`侧栏入口 toggleURL 均不变 |
| C8 | TODO | 搜索结果行 | 匹配数量、命中高亮、快捷键提示、结果 icon、所在页面信息对齐 | | C8 | PARITY | 搜索结果行 | `task135` 覆盖后台 API 结果、匹配数量、红色命中高亮、快捷键提示、结果 icon、结果 owner 标记 |
| C9 | TODO | 搜索空态/加载态 | 输入无结果和加载时的视觉与 DOM 状态可被 smoke 捕获 | | C9 | PARITY | 搜索空态/加载态 | `task135` 覆盖空 query 最近浏览、输入后 loading、无结果 `没有搜索结果`DOM 可断言 `data-search-loading` / `data-search-empty` |
| C10 | TODO | 搜索入口/快捷键矩阵 | 分别验证 Wolai 左侧搜索、顶栏/侧栏入口、`Ctrl+P``Ctrl+K`;每个入口记录 URL、打开/关闭路径和阻塞 | | C10 | PARITY | 搜索入口/快捷键矩阵 | `task135` 和 subagent baseline 确认:侧栏搜索 / `Ctrl+P` 是搜索入口;`Ctrl+K` 按当前 Wolai 实测不打开搜索 |
| C11 | TODO | 搜索结果 fixture | 固定本地 `Hermes` 或等价种子数据,避免用随机工作区数据对比 Wolai 结果态 | | C11 | PARITY | 搜索结果 fixture | `task135` 固定本地 `Rust` / `Hermes` fallback fixture,并断言 modal 调用 `/api/search/documents` 而不是 DOM 前端过滤 |
### 6.1 task135 Phase C 执行记录
2026-04-30 已完成 Phase C:顶栏、breadcrumb、owner/published、搜索入口与搜索 modal。执行口径按 `wolai-aline` skillsubagent 先取 Wolai / 本地基线,主线程复核截图后补 RED smoke;实现后本地 smoke 通过,并已派 subagent 做 post-fix 复测。
RED
- 新增 `scripts/task135-wolai-aline-phase-c-smoke.js`,初始失败于顶栏高度 `44px`、公开态文案 `Public`、顶栏多搜索入口、搜索 modal 未调用 `/api/search/documents`
- subagent 基线纠偏:当前 Wolai `Ctrl+K` 不打开搜索;空 query 不直接显示筛选控件;输入后 `仅匹配标题` / `页面内搜索` switch 本次实测为关闭态。
GREEN
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`:顶栏公开态改为 `全网公开`,移除顶栏搜索入口,右侧按钮组对齐 owner 态;侧栏搜索入口改为 toggle;搜索 modal 输入改为 `type=search`,空 query 显示最近浏览,输入后调用 `/api/search/documents`,结果区标记 `data-search-results-owner="rust-kernel"`
- `rust/crates/mnote-web/src/ssr/styles.rs`:顶栏高度收敛到 40px;搜索遮罩支持侧栏入口穿透 toggle,同时保留主区域遮罩关闭;补齐右侧按钮与结果图标 mask,避免黑色方块;搜索高亮统一红色弱底。
- `rust/crates/mnote-web/src/routes/search.rs`fallback 搜索数据补 `Hermes` / `技能知识图谱开发` fixture,避免本地结果态依赖随机工作区数据。
- `scripts/task128-rust-web-wolai-search-modal-smoke.js`:同步当前 Wolai 基线,改为侧栏 / `Ctrl+P` 入口和 switch 默认关闭。
验证命令:
- `cargo fmt -p mnote-web --check`
- `cargo check -p mnote-web`
- `cargo build -p mnote-web --bin mnote-web`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task135-wolai-aline-phase-c-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task128-rust-web-wolai-search-modal-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task125-rust-web-search-server-first-smoke.js`
- `node scripts/task119-rust-web-wolai-visual-regression-smoke.js`
证据路径:
- Phase C subagent 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-baseline/comparison-matrix.md`
- Wolai Hermes 搜索基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-baseline/wolai-search-hermes.png`
- 本地 Phase C 最终截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c/local-phase-c-search.png`post-fix subagent 本地复核:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-postfix/local-10-query-hermes.png`
- 本地搜索 modal smoke 截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/local-search-modal/local-3000-search-modal.png`
主线程复核结论:
- 搜索不再只是 UI 壳:modal 输入会调用 `/api/search/documents`,请求体包含 query 和 switch filter,结果区有 `data-search-results-owner="rust-kernel"`
- 当前 Wolai 基线推翻了旧 checklist 的两个假设:`Ctrl+K` 不是搜索入口;`仅匹配标题` / `页面内搜索` 在本次输入态实测为关闭态。
- 本地 `Hermes` fixture 只能证明结果态结构、后台链路和高亮;不代表本地已经有 Wolai 真实空间数据。
- 顶栏/breadcrumb 的父路径层级仍受本地 Page Aggregate / tree projection 数据限制;本阶段只完成控件、入口、状态和同步锚点的对标。
- post-fix subagent 复测矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task135-phase-c-postfix/comparison-matrix.md`;复核确认本地右上角不再有黑色方块,侧栏搜索 toggle、`Ctrl+P` toggle、`Ctrl+K` 不打开、空 query 最近浏览、后台搜索 API、结果 icon/高亮均通过。
## 7. Phase D:文档画布与阅读态 ## 7. Phase D:文档画布与阅读态
| ID | 状态 | 任务 | 验收要点 | | ID | 状态 | 任务 | 验收要点 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| D1 | TODO | 主内容列宽 | 普通文档列宽约 760px;大屏不贴左;移动端不溢出 | | D1 | PARITY | 主内容列宽 | `task136` 覆盖普通文档主列 `760px`,大屏居中不贴左;移动端沿既有 media query 保持不溢出 |
| D2 | TODO | 标题位置与样式 | Hermes 标题 y 位置`34px / 44px / 600`、颜色接近 Wolai | | D2 | PARITY | 标题位置与样式 | `task136` 按 Wolai 基线收敛到 y≈108`34px / 44px / 600`、颜色接近 Wolai |
| D3 | TODO | 默认隐藏 meta/debug 行 | 普通文档默认不显示不像 Wolai 的工作区 meta、横线、debug outline | | D3 | PARITY | 默认隐藏 meta/debug 行 | `task136` 断言普通文档默认不显示 workspace/已同步 meta、大号页面 icon、debug 文案和横线 |
| D4 | TODO | 正文段落 | `16px / 24px`、段间距、弱色文本与 Wolai 对齐 | | D4 | PARITY | 正文段落 | `task136` 覆盖正文 `16px / 24px`,标题到正文间距按 Wolai 基线加大 |
| D5 | TODO | 任务块 | checkbox 尺寸、边框、文本 baseline、完成态接近 Wolai | | D5 | PARITY | 任务块 | `task136` 固定 Tiptap fixture 并断言渲染后存在 `input[type=checkbox]`,尺寸 16px;补齐 kernel `todo` 类型到 Tiptap taskList 的转换 |
| D6 | TODO | 引用/列表/标题块 | 阅读态块样式不跳出 Wolai 密度;截图覆盖至少三类块 | | D6 | PARITY | 引用/列表/标题块 | `task136` 覆盖 heading、blockquote、bulletList;补齐 `bullet_list_item` / `numbered_list_item` 映射避免降级段落 |
| D7 | TODO | 右下角浮动入口 | AI / 帮助入口减重,尺寸和位置接近 Wolai,不遮挡正文 | | D7 | PARITY | 右下角浮动入口 | `task136` 覆盖 AI / 帮助入口 40px、AI 在上、间距约 16px、底部留白约 44px,不遮挡正文 |
| D8 | TODO | 阅读态和编辑态切换 | 切换时标题、正文列宽、scroll 位置不明显跳动 | | D8 | PARITY | 阅读态和编辑态切换 | `task136` 覆盖标题 focus、编辑器 focus 后标题 y/x 不跳动;subagent baseline 同样确认点击后布局稳定 |
| D9 | PARITY | 普通文档首屏入口语义 | `task130` 已覆盖 RED/GREENsubagent 最终对标复核通过:根页不再停在“打开当前页面”入口,直接出现 document shell + editor island | | D9 | PARITY | 普通文档首屏入口语义 | `task130` 已覆盖 RED/GREENsubagent 最终对标复核通过:根页不再停在“打开当前页面”入口,直接出现 document shell + editor island |
### 7.1 task136 Phase D 执行记录
2026-04-30 已完成 Phase D:文档画布与阅读态。执行口径按 `wolai-aline` skillsubagent 先取 Wolai / 本地只读基线,主线程复核截图后补 RED smoke;实现后本地 smoke 与相关回归通过,并已派 subagent 做 post-fix 复测。
RED
- 新增 `scripts/task136-wolai-aline-phase-d-smoke.js`,初始失败于本地文档主列 `880px`,后续增强断言捕获标题 y 偏低、标题到正文间距偏小、右下浮动入口间距/底部位置旧基线。
- 基线截图确认 Wolai 标题为 y≈108、`34px / 44px / 600`;普通文档无 workspace/已同步 meta 行和大号页面 icon;右下入口为 40px,AI 在帮助上方,按钮间距约 16px,底部留白约 44px。
- smoke 还暴露后台链路差异:保存 Tiptap fixture 后,`/api/documents/content` 返回 `todo` / `bullet_list_item` 等 kernel 规范化类型,但浏览器 bootstrap 只识别旧类型,导致任务块和列表降级为普通段落。
GREEN
- `rust/crates/mnote-web/src/ssr/styles.rs`:文档主列收敛到 760px;标题改为 `34px / 44px / 600` 且 y≈108;普通文档隐藏页面大 icon 与 meta/debug 行;正文行高 24px;补 task checkbox、blockquote、list 样式;右下浮动入口减重为 40px,AI 在上、帮助在下,间距 16px、底部留白 44px。
- `rust/crates/mnote-web/src/routes/web_shell.rs``legacyBlockToTiptap` 同时识别旧类型和 kernel 规范化类型:`todo``bullet_list_item``numbered_list_item``blockquote`
- `rust/crates/mnote-web/src/routes/editor.rs`runtime block 类型映射同步兼容 kernel 规范化类型,避免编辑器链路继续把任务/列表块当成 media 或段落。
- `scripts/task119-rust-web-wolai-visual-regression-smoke.js`:按本次 Wolai 基线更新右下浮动入口顺序、间距和底部留白断言,避免旧 Stitch 目标覆盖当前实测。
验证命令:
- `cargo fmt -p mnote-web --check`
- `cargo check -p mnote-web`
- `cargo build -p mnote-web --bin mnote-web`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task136-wolai-aline-phase-d-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js`
- `node scripts/task119-rust-web-wolai-visual-regression-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task128-rust-web-wolai-search-modal-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task135-wolai-aline-phase-c-smoke.js`
证据路径:
- Phase D subagent 基线矩阵:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-baseline2/comparison-matrix.md`
- Wolai Phase D 基线:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-baseline2/wolai-01-first.png`
- 本地 Phase D 最终截图:`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d/local-phase-d-document.png`
- Phase D subagent post-fix`/mnt/Data1T/mnote/tmp/wolai-editor-parity/task136-phase-d-postfix/comparison-matrix.md`
主线程复核结论:
- 本地文档画布主列、标题字号/行高/字重、标题 y、meta/icon/debug 隐藏、正文行高、任务/引用/列表渲染、右下浮动入口尺寸/顺序/间距已被 smoke 固化。
- subagent post-fix 在根文档截图中复核标题 y=108、列宽 760、字号 34/字重 600、meta/icon/debug 不可见、浮动入口 40px/16px/44px、点击后不跳动。
- 当前本地 fixture smoke 能证明后台内容链路不再把任务块和列表降级为普通段落;post-fix 根文档缺少任务/引用/列表内容,因此这三类块的真实视觉细节仍以后续 Phase E editable-test 或专门 fixture 页面继续细化。
- 剩余差异:本地页面标题来自临时 fixture,文案与 Wolai Hermes 不同;Wolai 真实 per-page icon 数据仍未进入本地 projection,按 Phase B 结论不伪造。
## 8. Phase E:主编辑器块交互 ## 8. Phase E:主编辑器块交互
| ID | 状态 | 任务 | 验收要点 | | ID | 状态 | 任务 | 验收要点 |
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

+6 -4
View File
@@ -214,10 +214,10 @@ fn runtime_block_type_for_raw_type(raw_type: &str) -> (&'static str, bool) {
match raw_type { match raw_type {
"paragraph" => ("paragraph", true), "paragraph" => ("paragraph", true),
"heading" => ("heading", true), "heading" => ("heading", true),
"bulletListItem" => ("bullet_list_item", true), "bulletListItem" | "bullet_list_item" => ("bullet_list_item", true),
"numberedListItem" => ("numbered_list_item", true), "numberedListItem" | "numbered_list_item" => ("numbered_list_item", true),
"checkListItem" | "advancedTodo" => ("todo", true), "checkListItem" | "advancedTodo" | "todo" => ("todo", true),
"quote" => ("quote", true), "quote" | "blockquote" => ("quote", true),
"divider" => ("divider", true), "divider" => ("divider", true),
"codeBlock" => ("code_block", true), "codeBlock" => ("code_block", true),
"pageReference" => ("page_reference", false), "pageReference" => ("page_reference", false),
@@ -237,6 +237,8 @@ fn raw_type_for_runtime_block_type(block_type: &str, fallback_raw_type: &str) ->
"todo" => { "todo" => {
if fallback_raw_type == "advancedTodo" { if fallback_raw_type == "advancedTodo" {
"advancedTodo".into() "advancedTodo".into()
} else if fallback_raw_type == "todo" {
"todo".into()
} else { } else {
"checkListItem".into() "checkListItem".into()
} }
@@ -266,6 +266,22 @@ fn fallback_search_dataset(workspace_id: &str) -> Value {
"rawText": "mnote-web search documents transport", "rawText": "mnote-web search documents transport",
"createdAt": "2026-04-28T00:00:00Z", "createdAt": "2026-04-28T00:00:00Z",
"updatedAt": "2026-04-28T00:00:00Z" "updatedAt": "2026-04-28T00:00:00Z"
},
{
"id": "doc_hermes",
"workspaceId": workspace_id,
"title": "Hermes",
"rawText": "Hermes 技能知识图谱开发 Wolai aline fixture",
"createdAt": "2026-04-30T00:00:00Z",
"updatedAt": "2026-04-30T00:00:00Z"
},
{
"id": "doc_hermes_skill",
"workspaceId": workspace_id,
"title": "技能知识图谱开发",
"rawText": "Hermes 页面路径 个人 软件开发",
"createdAt": "2026-04-30T00:00:00Z",
"updatedAt": "2026-04-30T00:00:00Z"
} }
], ],
"mindmaps": [], "mindmaps": [],
@@ -315,16 +315,16 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const level = Number(block?.props?.level || block?.level || 1) || 1; const level = Number(block?.props?.level || block?.level || 1) || 1;
return { type: 'heading', attrs: { level: Math.max(1, Math.min(6, level)) }, content }; return { type: 'heading', attrs: { level: Math.max(1, Math.min(6, level)) }, content };
} }
if (type === 'bulletListItem') { if (type === 'bulletListItem' || type === 'bullet_list_item') {
return { type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] }; return { type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] };
} }
if (type === 'numberedListItem') { if (type === 'numberedListItem' || type === 'numbered_list_item') {
return { type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] }; return { type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] };
} }
if (type === 'checkListItem' || type === 'advancedTodo') { if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
return { type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: Boolean(block?.props?.checked) }, content: [{ type: 'paragraph', content }] }] }; return { type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: Boolean(block?.props?.checked) }, content: [{ type: 'paragraph', content }] }] };
} }
if (type === 'quote') { if (type === 'quote' || type === 'blockquote') {
return { type: 'blockquote', content: [{ type: 'paragraph', content }] }; return { type: 'blockquote', content: [{ type: 'paragraph', content }] };
} }
if (type === 'codeBlock') { if (type === 'codeBlock') {
+113 -46
View File
@@ -663,33 +663,34 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-search-dialog">' + '<div class="wolai-search-dialog">' +
'<div class="wolai-search-input-row">' + '<div class="wolai-search-input-row">' +
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' + '<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
'<input data-testid="wolai-search-input" class="wolai-search-input" type="text" autocomplete="off" placeholder="在当前工作区中搜索" />' + '<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' + '<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
'</div>' + '</div>' +
'<div class="wolai-search-options" aria-label="搜索选项">' + '<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
'<div class="wolai-search-options-left">' + '<div class="wolai-search-options-left">' +
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch is-on" data-search-switch="title" role="switch" aria-checked="true" aria-label="仅匹配标题"></button></span>' + '<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' + '<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' + '<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' + '<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
'</div>' + '</div>' +
'<div class="wolai-search-options-right">' + '<div class="wolai-search-options-right">' +
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch is-on" data-search-switch="page" role="switch" aria-checked="true" aria-label="页面内搜索"></button></span>' + '<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' + '<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
'<div class="wolai-search-results" data-testid="wolai-search-results"></div>' + '<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
'</div>'; '</div>';
document.body.appendChild(overlay); document.body.appendChild(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]'); var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]'); var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
if (input) input.addEventListener('input', renderSearchResults); if (input) input.addEventListener('input', scheduleSearchResultsRender);
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) { overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
button.addEventListener('click', function() { button.addEventListener('click', function() {
var isOn = button.getAttribute('aria-checked') !== 'true'; var isOn = button.getAttribute('aria-checked') !== 'true';
button.setAttribute('aria-checked', isOn ? 'true' : 'false'); button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn); button.classList.toggle('is-on', isOn);
scheduleSearchResultsRender();
}); });
}); });
if (closeButton) closeButton.addEventListener('click', closeSearchModal); if (closeButton) closeButton.addEventListener('click', closeSearchModal);
@@ -699,26 +700,33 @@ const SIDEBAR_TREE_JS: &str = r##"
return overlay; return overlay;
} }
var activeSearchRequestId = 0;
var searchRenderTimer = 0;
function searchText(value) { function searchText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim(); return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
} }
function collectSearchCandidates() { function currentWorkspaceName() {
var seen = Object.create(null); var name = document.querySelector('.sidebar-workspace-name');
var candidates = []; return searchText(name && name.textContent) || '当前工作区';
function add(title, source) { }
title = searchText(title);
if (!title || seen[title]) return; function currentDocumentId() {
seen[title] = true; var shell = document.querySelector('.document-shell[data-document-id]');
candidates.push({ title: title, source: source || '当前工作区' }); var bodyId = document.body && document.body.getAttribute('data-document-id');
} return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) { }
add(node.textContent, '页面');
}); function searchSwitchValue(overlay, name) {
document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) { var button = overlay.querySelector('[data-search-switch="' + name + '"]');
add(node.textContent, '当前页面'); return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
}); }
return candidates.slice(0, 120);
function highlightedHtml(value) {
return escapeHtml(value)
.replace(/&lt;mark&gt;/g, '<mark>')
.replace(/&lt;\/mark&gt;/g, '</mark>');
} }
function highlightSearchTitle(title, query) { function highlightSearchTitle(title, query) {
@@ -732,31 +740,82 @@ const SIDEBAR_TREE_JS: &str = r##"
escapeHtml(cleanTitle.slice(index + cleanQuery.length)); escapeHtml(cleanTitle.slice(index + cleanQuery.length));
} }
function renderSearchResults() { function renderSearchRecentState(overlay) {
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (options instanceof HTMLElement) options.hidden = true;
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (results) {
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
}
}
function scheduleSearchResultsRender() {
window.clearTimeout(searchRenderTimer);
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
}
async function renderSearchResults() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]'); var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (!(overlay instanceof HTMLElement)) return; if (!(overlay instanceof HTMLElement)) return;
var input = overlay.querySelector('[data-testid="wolai-search-input"]'); var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]'); var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]'); var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (!input || !meta || !results) return; if (!input || !meta || !results) return;
var query = searchText(input.value); var query = searchText(input.value);
var candidates = collectSearchCandidates(); if (!query) {
var filtered = query activeSearchRequestId += 1;
? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; }) renderSearchRecentState(overlay);
: candidates.slice(0, 8);
filtered = filtered.slice(0, 100);
meta.innerHTML = '<span>共 ' + filtered.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (!filtered.length) {
results.innerHTML = '<div class="wolai-search-empty">暂无匹配结果</div>';
return; return;
} }
results.innerHTML = filtered.map(function(item) { if (options instanceof HTMLElement) options.hidden = false;
return '<button type="button" class="wolai-search-result-row">' + var requestId = ++activeSearchRequestId;
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="link" aria-hidden="true"></span>' + meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(item.title, query) + '</span>' + results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
'<span class="wolai-search-result-snippet">' + escapeHtml(item.source) + ' · 当前工作区</span></span>' + try {
'</button>'; var response = await fetch('/api/search/documents', {
}).join(''); method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId() || null,
query: query,
limit: 30,
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: false,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
}
})
});
var payload = await response.json();
if (requestId !== activeSearchRequestId) return;
var items = Array.isArray(payload.results) ? payload.results : [];
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (!items.length) {
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
return;
}
results.innerHTML = items.map(function(item) {
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '">' +
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span></span>' +
'</button>';
}).join('');
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
}
} }
function isSearchModalOpen() { function isSearchModalOpen() {
@@ -768,8 +827,9 @@ const SIDEBAR_TREE_JS: &str = r##"
var overlay = ensureSearchModal(); var overlay = ensureSearchModal();
overlay.hidden = false; overlay.hidden = false;
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true'); document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
renderSearchResults();
var input = overlay.querySelector('[data-testid="wolai-search-input"]'); var input = overlay.querySelector('[data-testid="wolai-search-input"]');
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
void renderSearchResults();
if (input) { if (input) {
setTimeout(function() { input.focus(); input.select(); }, 0); setTimeout(function() { input.focus(); input.select(); }, 0);
} }
@@ -793,7 +853,12 @@ const SIDEBAR_TREE_JS: &str = r##"
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]'); var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) { if (searchTrigger) {
e.preventDefault(); e.preventDefault();
openSearchModal(); toggleSearchModal();
return;
}
var searchDialog = closestAction(e.target, '.wolai-search-dialog');
if (isSearchModalOpen() && !searchDialog) {
closeSearchModal();
return; return;
} }
@@ -1305,7 +1370,7 @@ pub fn PageLayout(
<aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar"> <aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
<div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity"> <div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
<a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a> <a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
<span class="sidebar-workspace-name">{ws_name}</span> <span class="sidebar-workspace-name">{ws_name.clone()}</span>
<span class="wolai-sidebar-chevron" aria-hidden="true">""</span> <span class="wolai-sidebar-chevron" aria-hidden="true">""</span>
</div> </div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作"> <nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
@@ -1326,17 +1391,19 @@ pub fn PageLayout(
<div class="wolai-topbar-left"> <div class="wolai-topbar-left">
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button> <button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
<nav class="wolai-breadcrumb" aria-label="页面路径"> <nav class="wolai-breadcrumb" aria-label="页面路径">
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span> <span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span> <span class="wolai-breadcrumb-separator" aria-hidden="true">""</span>
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span> <span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
</nav> </nav>
</div> </div>
<div class="wolai-topbar-actions" aria-label="页面操作"> <div class="wolai-topbar-actions" aria-label="页面操作">
<span class="wolai-public-pill"><span aria-hidden="true">""</span>"Public"</span> <span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button> <button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button> <button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button> <button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
</div> </div>
</header> </header>
+70 -64
View File
@@ -170,6 +170,11 @@ a:hover {
.material-symbols-outlined[data-icon="right_panel_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5h16v14H4zM14 5v14M8 12h6M11 9l3 3-3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .material-symbols-outlined[data-icon="right_panel_open"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5h16v14H4zM14 5v14M8 12h6M11 9l3 3-3 3' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="open_in_new"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 6H5v13h13v-3M12 5h7v7M10 14 19 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .material-symbols-outlined[data-icon="open_in_new"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 6H5v13h13v-3M12 5h7v7M10 14 19 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="share"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='18' cy='5' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='6' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='19' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m8.7 10.7 6.6-4.4M8.7 13.3l6.6 4.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } .material-symbols-outlined[data-icon="share"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='18' cy='5' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='6' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='18' cy='19' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m8.7 10.7 6.6-4.4M8.7 13.3l6.6 4.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="comment"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 5h14v10H9l-4 4V5Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="person_add"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='9' cy='8' r='3.2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M3.5 20a5.5 5.5 0 0 1 11 0M18 8v6M15 11h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="slideshow"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 5h16v12H4z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Cpath d='m10 9 5 2-5 2zM9 21h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="article"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4h12v16H6zM9 8h6M9 12h6M9 16h4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="link"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.5 14.5 14.5 9.5M10.8 6.2l1.1-1.1a4 4 0 0 1 5.7 5.7l-1.6 1.6M13.2 17.8l-1.1 1.1a4 4 0 0 1-5.7-5.7L8 11.6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } .material-symbols-outlined[data-icon="link"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.5 14.5 14.5 9.5M10.8 6.2l1.1-1.1a4 4 0 0 1 5.7 5.7l-1.6 1.6M13.2 17.8l-1.1 1.1a4 4 0 0 1-5.7-5.7L8 11.6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
.material-symbols-outlined[data-icon="content_copy"], .material-symbols-outlined[data-icon="content_copy"],
.material-symbols-outlined[data-icon="file_copy"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 8h10v12H8zM6 16H4V4h10v2' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); } .material-symbols-outlined[data-icon="file_copy"] { --mnote-icon-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 8h10v12H8zM6 16H4V4h10v2' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
@@ -912,15 +917,17 @@ a:hover {
/* ===== 响应式 ===== */ /* ===== 响应式 ===== */
.wolai-search-overlay{position:fixed;inset:0;z-index:1200;display:flex;align-items:flex-start;justify-content:center;padding:92px 24px 24px;background:rgba(0,0,0,.32)} .wolai-search-overlay{position:fixed;inset:0;z-index:1200;display:flex;align-items:flex-start;justify-content:center;padding:92px 24px 24px;background:rgba(0,0,0,.32);pointer-events:none}
.wolai-search-overlay[hidden]{display:none!important} .wolai-search-overlay[hidden]{display:none!important}
.wolai-search-dialog{width:min(680px,100%);max-height:min(720px,calc(100vh - 128px));overflow:hidden;display:flex;flex-direction:column;background:#FFF;border:1px solid rgba(27,28,28,.10);border-radius:6px;box-shadow:0 18px 48px rgba(15,23,42,.22),0 2px 8px rgba(15,23,42,.08);color:#37352F} .wolai-search-dialog{width:min(680px,100%);max-height:min(720px,calc(100vh - 128px));overflow:hidden;display:flex;flex-direction:column;background:#FFF;border:1px solid rgba(27,28,28,.10);border-radius:6px;box-shadow:0 18px 48px rgba(15,23,42,.22),0 2px 8px rgba(15,23,42,.08);color:#37352F;pointer-events:auto}
.wolai-search-input-row{display:flex;align-items:center;min-height:58px;padding:0 12px 0 18px;border-bottom:1px solid rgba(27,28,28,.08)} .wolai-search-input-row{display:flex;align-items:center;min-height:58px;padding:0 12px 0 18px;border-bottom:1px solid rgba(27,28,28,.08)}
.wolai-search-input-icon{width:22px;height:22px;color:#8B8780;margin-right:10px} .wolai-search-input-icon{width:22px;height:22px;color:#8B8780;margin-right:10px}
.wolai-search-input{flex:1 1 auto;min-width:0;height:56px;border:0;outline:none;background:transparent;color:#24211D;font:400 18px/1.4 var(--wolai-font-sans);letter-spacing:0} .wolai-search-input{flex:1 1 auto;min-width:0;height:56px;border:0;outline:none;background:transparent;color:#24211D;font:400 18px/1.4 var(--wolai-font-sans);letter-spacing:0}
.wolai-search-input::placeholder{color:#A29E97} .wolai-search-input::placeholder{color:#A29E97}
.wolai-search-input::-webkit-search-cancel-button{appearance:none;-webkit-appearance:none;display:none}
.wolai-search-close{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:#8B8780;cursor:pointer;font-size:20px;line-height:1} .wolai-search-close{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:#8B8780;cursor:pointer;font-size:20px;line-height:1}
.wolai-search-options{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px 8px;border-bottom:1px solid rgba(27,28,28,.06);color:#A7A39D;font-size:12px;line-height:1} .wolai-search-options{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px 8px;border-bottom:1px solid rgba(27,28,28,.06);color:#A7A39D;font-size:12px;line-height:1}
.wolai-search-options[hidden]{display:none!important}
.wolai-search-options-left,.wolai-search-options-right,.wolai-search-switch-control,.wolai-search-sort-control{display:inline-flex;align-items:center} .wolai-search-options-left,.wolai-search-options-right,.wolai-search-switch-control,.wolai-search-sort-control{display:inline-flex;align-items:center}
.wolai-search-options-left{min-width:0;flex-wrap:wrap;gap:8px 12px} .wolai-search-options-left{min-width:0;flex-wrap:wrap;gap:8px 12px}
.wolai-search-options-right{margin-left:auto;flex:0 0 auto} .wolai-search-options-right{margin-left:auto;flex:0 0 auto}
@@ -939,9 +946,9 @@ a:hover {
.wolai-search-result-icon{width:18px;height:18px;margin-top:2px;color:#8B8780} .wolai-search-result-icon{width:18px;height:18px;margin-top:2px;color:#8B8780}
.wolai-search-result-main{min-width:0;display:flex;flex-direction:column;gap:3px} .wolai-search-result-main{min-width:0;display:flex;flex-direction:column;gap:3px}
.wolai-search-result-title{color:#2F2D29;font-size:14px;line-height:1.35;word-break:break-word} .wolai-search-result-title{color:#2F2D29;font-size:14px;line-height:1.35;word-break:break-word}
.wolai-search-result-title mark{padding:0 1px;border-radius:2px;background:#FFE9E6;color:#D83A32} .wolai-search-result-title mark,.wolai-search-result-snippet mark{padding:0 1px;border-radius:2px;background:#FFE9E6;color:#D83A32}
.wolai-search-result-snippet,.wolai-search-empty{color:#8B8780;font-size:12px;line-height:1.35} .wolai-search-result-snippet,.wolai-search-empty{color:#8B8780;font-size:12px;line-height:1.35}
.wolai-search-empty{padding:28px 12px 32px;text-align:center} .wolai-search-empty,.wolai-search-recent{padding:28px 12px 32px;text-align:center;color:#8B8780;font-size:12px;line-height:1.35}
@media (max-width: 768px) { @media (max-width: 768px) {
.mnote-sidebar { .mnote-sidebar {
@@ -963,6 +970,7 @@ a:hover {
.document-shell-header h1 { .document-shell-header h1 {
font-size: 28px; font-size: 28px;
line-height: 36px;
} }
#mnote-editor-island { #mnote-editor-island {
@@ -1555,7 +1563,7 @@ body {
} }
.wolai-topbar { .wolai-topbar {
height: 44px; height: 40px;
padding: 0 20px 0 22px; padding: 0 20px 0 22px;
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.92);
color: #5A5A5A; color: #5A5A5A;
@@ -1626,22 +1634,17 @@ body {
} }
.wolai-public-pill { .wolai-public-pill {
height: 26px; height: 22px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 0 11px; padding: 0 8px;
border: 1px solid #D6F2DC; border: 0;
border-radius: 999px; border-radius: 3px;
background: #ECFCEF; background: #ECFCEF;
color: #087B31; color: #087B31;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 500;
}
.wolai-public-pill span {
color: var(--atelier-primary-container);
font-size: 12px;
} }
.wolai-ai-inline { .wolai-ai-inline {
@@ -1649,25 +1652,21 @@ body {
} }
.document-shell { .document-shell {
max-width: 880px; box-sizing: border-box;
min-height: calc(100vh - 44px); width: min(100%, 760px);
padding: 78px 48px 160px; min-height: calc(100vh - 40px);
padding: 68px 0 160px;
margin: 0 auto; margin: 0 auto;
} }
.document-shell-header { .document-shell-header {
max-width: none; max-width: none;
padding: 0; padding: 0;
margin: 0 0 28px; margin: 0 0 50px;
} }
.document-page-icon { .document-page-icon {
height: 82px; display: none;
display: flex;
align-items: center;
color: #111111;
line-height: 1;
margin-bottom: 12px;
} }
.mnote-material-page-icon { .mnote-material-page-icon {
@@ -1683,18 +1682,18 @@ body {
.document-title-heading, .document-title-heading,
.document-shell-header h1 { .document-shell-header h1 {
padding: 0; padding: 0;
margin: 0 0 14px; margin: 0;
color: var(--atelier-text); color: var(--atelier-text);
font-size: 42px; font-size: 34px;
font-weight: 700; font-weight: 600;
line-height: 1.12; line-height: 44px;
letter-spacing: 0; letter-spacing: 0;
text-align: left; text-align: left;
} }
.document-title-input { .document-title-input {
width: 100%; width: 100%;
min-height: 52px; min-height: 44px;
display: block; display: block;
resize: none; resize: none;
overflow: hidden; overflow: hidden;
@@ -1719,15 +1718,7 @@ body {
} }
.document-shell-meta { .document-shell-meta {
display: flex; display: none;
align-items: center;
flex-wrap: wrap;
gap: 18px;
padding-bottom: 28px;
border-bottom: 1px solid #F0EFED;
color: #A19D97;
font-size: 14px;
line-height: 1.4;
} }
.document-shell-meta span { .document-shell-meta span {
@@ -1759,7 +1750,7 @@ body {
.ProseMirror { .ProseMirror {
color: var(--atelier-text); color: var(--atelier-text);
font-size: 16px; font-size: 16px;
line-height: 1.65; line-height: 24px;
outline: none; outline: none;
} }
@@ -1789,7 +1780,33 @@ body {
} }
#mnote-leptos-tiptap-island-editor-root .ProseMirror p { #mnote-leptos-tiptap-island-editor-root .ProseMirror p {
margin: 0 0 24px; margin: 0 0 8px;
}
#mnote-leptos-tiptap-island-editor-root .ProseMirror ul,
#mnote-leptos-tiptap-island-editor-root .ProseMirror ol {
margin: 4px 0 8px;
padding-left: 1.5em;
}
#mnote-leptos-tiptap-island-editor-root .ProseMirror li {
margin: 2px 0;
padding-left: 2px;
}
#mnote-leptos-tiptap-island-editor-root .ProseMirror blockquote {
margin: 8px 0;
padding: 2px 0 2px 14px;
border-left: 3px solid #D9D6D0;
color: #5F5B56;
}
#mnote-leptos-tiptap-island-editor-root .ProseMirror input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 8px 0 0;
vertical-align: -3px;
accent-color: #2EA44F;
} }
#mnote-leptos-tiptap-island-editor-root .ProseMirror h1, #mnote-leptos-tiptap-island-editor-root .ProseMirror h1,
@@ -1823,8 +1840,9 @@ body {
.wolai-floating-actions { .wolai-floating-actions {
right: 24px; right: 24px;
bottom: 24px; bottom: 44px;
gap: 10px; flex-direction: column-reverse;
gap: 16px;
z-index: 50; z-index: 50;
} }
@@ -1848,13 +1866,13 @@ body {
} }
.wolai-floating-button--ai { .wolai-floating-button--ai {
width: 52px; width: 40px;
height: 52px; height: 40px;
border: 0; border: 1px solid rgba(27, 28, 28, 0.1);
background: linear-gradient(135deg, var(--atelier-primary), var(--atelier-primary-container)); background: #FFFFFF;
color: #FFFFFF; color: #5A5A5A;
box-shadow: 0 16px 32px rgba(0, 110, 40, 0.22); box-shadow: 0 10px 24px rgba(27, 28, 28, 0.08);
font-size: 23px; font-size: 18px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -1874,25 +1892,13 @@ body {
} }
.document-shell { .document-shell {
width: 100%;
padding: 48px 28px 120px; padding: 48px 28px 120px;
} }
.document-page-icon {
height: 60px;
}
.mnote-material-page-icon {
width: 48px;
height: 48px;
font-size: 48px;
}
.mnote-material-page-icon::before {
font-size: 48px;
}
.document-shell-header h1 { .document-shell-header h1 {
font-size: 32px; font-size: 32px;
line-height: 40px;
} }
#mnote-leptos-tiptap-island-editor-root .block-handle-shell { #mnote-leptos-tiptap-island-editor-root .block-handle-shell {
@@ -90,7 +90,9 @@ async function main() {
assert(floatingHelpBox.x > sidebarBox.width, "帮助浮动按钮不应落入左侧栏"); assert(floatingHelpBox.x > sidebarBox.width, "帮助浮动按钮不应落入左侧栏");
assert(!boxesIntersect(floatingAiBox, primaryContentBox), "AI 浮动按钮不应遮挡主内容"); assert(!boxesIntersect(floatingAiBox, primaryContentBox), "AI 浮动按钮不应遮挡主内容");
assert(!boxesIntersect(floatingHelpBox, primaryContentBox), "帮助浮动按钮不应遮挡主内容"); assert(!boxesIntersect(floatingHelpBox, primaryContentBox), "帮助浮动按钮不应遮挡主内容");
assert(floatingHelpBox.y < floatingAiBox.y, "帮助按钮应位于 AI 按钮上方,匹配 Stitch 目标"); assert(floatingAiBox.y < floatingHelpBox.y, "AI 按钮应位于帮助按钮上方,匹配当前 Wolai 基线");
assert(Math.abs((floatingHelpBox.y - (floatingAiBox.y + floatingAiBox.height)) - 16) <= 2, "AI/帮助按钮间距应接近 Wolai 16px");
assert(Math.abs((960 - (floatingHelpBox.y + floatingHelpBox.height)) - 44) <= 4, "帮助按钮底部留白应接近 Wolai 44px");
assert.equal(await page.locator('[data-mnote-sidebar-tree-tab="filetree"]').count(), 1, "Explorer 只能作为一个切换 tab 出现"); assert.equal(await page.locator('[data-mnote-sidebar-tree-tab="filetree"]').count(), 1, "Explorer 只能作为一个切换 tab 出现");
assert.equal(await page.locator('[data-testid="wolai-sidebar-file-tree-section"]').count(), 0, "文件树不应再渲染成第二个独立 Explorer section"); assert.equal(await page.locator('[data-testid="wolai-sidebar-file-tree-section"]').count(), 0, "文件树不应再渲染成第二个独立 Explorer section");