diff --git a/design/design/stitch/260429/3-11-rust-web-3000-stitch-ui-parity-checklist-v1.md b/design/design/stitch/260429/3-11-rust-web-3000-stitch-ui-parity-checklist-v1.md new file mode 100644 index 00000000..dd5a8524 --- /dev/null +++ b/design/design/stitch/260429/3-11-rust-web-3000-stitch-ui-parity-checklist-v1.md @@ -0,0 +1,45 @@ +# 3-11 Rust Web 3000 Stitch UI Parity Checklist v1 + +> 目标:以 `design/design/stitch/260429` 的 Stitch 产物为视觉基准,把 3000 的 Rust Web 页面推进到更接近目标 UI,同时保留真实页面树、文件树、页面新建和 `leptos-tiptap` runtime。 + +## 输入基准 + +- [x] 读取 Stitch 设计系统:`DESIGN.md` +- [x] 读取 Stitch 静态结构:`code.html` +- [x] 查看目标截图:`screen.png` +- [x] 对比当前 3000 截图:`/mnt/Data1T/mnote/tmp/image copy 33.png` + +## 约束 + +- [x] 不直接整包复制 Stitch HTML,避免绕开 Rust projection / tree shell / editor island。 +- [x] 不恢复 Next.js 为主入口,3000 继续由 `mnote-web` 承载。 +- [x] 页面树、文件树、新建页面、正文编辑继续使用当前已接入的运行链路。 +- [x] 不引入 Tailwind CDN、Google Fonts、远程图片作为运行依赖。 + +## 实施项 + +- [x] 侧栏外壳对齐 Stitch:240px 左栏、浅灰无硬边框、工作区头像/标题、快捷操作区、底部入口。 +- [x] 侧栏内容对齐 Stitch:星标置顶、我的页面、Explorer tab 同屏呈现,并保留真实页面树和文件树 DOM/testid。 +- [x] 页面树/文件树行样式对齐 Stitch:12px 层级缩进、14px 文本、soft-bleed hover、当前页选中态。 +- [x] 顶栏对齐 Stitch:44px 高度、左侧面包屑、右侧 Public/收藏/历史/AI/搜索/更多工具。 +- [x] 文档画布对齐 Stitch:居中 880px 内容列、80px 顶部留白、页面图标、标题、元信息、分割线。 +- [x] `leptos-tiptap` 外壳对齐 Stitch:编辑器跟随文档列宽、正文 16px/1.6、空态提示不遮挡真实编辑器。 +- [x] 浮动按钮对齐 Stitch:右下帮助按钮 + 绿色 AI 圆形按钮。 +- [x] 首页 active page 链路不退化:根页仍能看到真实 sidebar,并可打开当前页。 +- [x] 验证 Rust 单测和关键 smoke,确保树与编辑器 runtime 未断开。 + +## 验收 + +- [x] `cargo test -p mnote-web` 通过。 +- [x] 至少一个文档 shell smoke 通过,确认 `wolai-sidebar`、页面树、文件树、`leptos_tiptap_island` 仍存在。 +- [x] 如 3000 服务可用,使用浏览器截图或 DOM 检查确认首屏侧栏、顶栏、文档画布接近 Stitch 目标。 + +## 验证记录 + +- [x] `cargo test -p mnote-web`:126 个单测通过,doc tests 4 个 ignored。 +- [x] `node scripts/task119-rust-web-wolai-visual-regression-smoke.js`:通过,截图 `test-results/task119-rust-web-wolai-visual-regression-root.png`。 +- [x] `node scripts/task120-rust-web-tree-integration-smoke.js`:通过。 +- [x] `node scripts/task121-rust-web-editor-island-hydration-smoke.js`:通过,确认 hydrate、输入、保存、reload 读回。 +- [x] `node scripts/task122-rust-web-create-page-ui-smoke.js`:通过,确认侧栏 `+` 新建页面并进入文档页。 +- [x] 手动 Playwright 截图:`test-results/task123-stitch-document-parity.png`,确认文档页头、真实页面树/文件树、编辑器正文列对齐。 +- [x] 修复 `我的页面 / Explorer` 切换:文件树不再作为第二个独立 Explorer section 渲染,点击 Explorer tab 后切换到文件树 panel。 diff --git a/rust/crates/mnote-web/src/ssr/pages/document.rs b/rust/crates/mnote-web/src/ssr/pages/document.rs index 301cca1e..e44ea6b1 100644 --- a/rust/crates/mnote-web/src/ssr/pages/document.rs +++ b/rust/crates/mnote-web/src/ssr/pages/document.rs @@ -37,11 +37,21 @@ pub fn DocumentPage( .map(str::trim) .filter(|value| !value.is_empty() && *value != "null") .is_some(); + let workspace_label = workspace_name + .clone() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "个人空间".to_string()); view! {
+

{title}

+
+ {workspace_label} + "已同步" +
{move || if has_active_page { view! { -
+
+

{active_page_title.clone()}

+
+ "个人空间" + "工作区首页" +
"打开当前页面" diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 5f254f3d..e82409cb 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -51,7 +51,32 @@ const SIDEBAR_TREE_JS: &str = r##" window.location.href = '/documents/' + encodeURIComponent(result.documentId) + '?workspaceId=' + encodeURIComponent(nextWorkspaceId); } + function switchSidebarTreeTab(trigger) { + var mode = (trigger.getAttribute('data-mnote-sidebar-tree-tab') || 'page').trim(); + var shell = trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]'); + if (!shell) return; + var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]'); + for (var i = 0; i < tabs.length; i++) { + var isActive = tabs[i].getAttribute('data-mnote-sidebar-tree-tab') === mode; + tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false'); + tabs[i].classList.toggle('wolai-sidebar-tab-active', isActive); + tabs[i].classList.toggle('wolai-sidebar-tab-muted', !isActive); + } + var panels = shell.querySelectorAll('[data-mnote-sidebar-tree-panel]'); + for (var j = 0; j < panels.length; j++) { + var isCurrentPanel = panels[j].getAttribute('data-mnote-sidebar-tree-panel') === mode; + panels[j].hidden = !isCurrentPanel; + } + } + document.addEventListener('click', function(e) { + var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]'); + if (tabTrigger) { + e.preventDefault(); + switchSidebarTreeTab(tabTrigger); + return; + } + var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]'); if (createTrigger) { e.preventDefault(); @@ -184,10 +209,10 @@ pub fn PageLayout(
@@ -195,23 +220,29 @@ pub fn PageLayout(
-
"☰""⌂"{topbar_title}
+
+ + +
- "☆" - "▻" - "◴" - "⌬" - "♙+" - "◷" - "…" + "Public" + + + + +
{children()}
- - + +
diff --git a/rust/crates/mnote-web/src/ssr/styles.rs b/rust/crates/mnote-web/src/ssr/styles.rs index ef620daf..f2f6d1b1 100644 --- a/rust/crates/mnote-web/src/ssr/styles.rs +++ b/rust/crates/mnote-web/src/ssr/styles.rs @@ -865,6 +865,705 @@ a:hover { padding-bottom: 16px; } } + +/* ===== Stitch 260429 UI parity overrides ===== */ +:root { + --atelier-surface: #FAF9F9; + --atelier-sidebar: #F4F3F3; + --atelier-sidebar-hover: #ECEBE9; + --atelier-sidebar-active: #E3E2E2; + --atelier-document: #FFFFFF; + --atelier-text: #1B1C1C; + --atelier-text-muted: #8A8782; + --atelier-text-soft: #B7B4AF; + --atelier-outline: #EAEAEA; + --atelier-primary: #006E28; + --atelier-primary-container: #31AA4D; + --atelier-secondary: #2367F6; + --atelier-shadow: 0 16px 32px rgba(27, 28, 28, 0.04); +} + +html, +body { + background: var(--atelier-document); +} + +body { + color: var(--atelier-text); + font-family: Inter, var(--wolai-font-sans); + letter-spacing: 0; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.mnote-shell, +.wolai-workspace-shell { + background: var(--atelier-document); +} + +.mnote-sidebar, +.wolai-sidebar { + width: 240px; + background: var(--atelier-sidebar); + border-right: 0; + box-shadow: inset -1px 0 0 rgba(27, 28, 28, 0.06); + overflow-x: hidden; +} + +.mnote-sidebar-header, +.wolai-sidebar-header { + height: 68px; + padding: 16px 12px 8px; + gap: 10px; +} + +.wolai-avatar { + width: 32px; + height: 32px; + border-radius: 5px; + background: #D6534D; + font-size: 17px; + font-weight: 650; +} + +.wolai-sidebar-header .sidebar-workspace-name, +.sidebar-workspace-name { + max-width: 160px; + color: var(--atelier-text); + font-size: 17px; + font-weight: 700; + line-height: 1.1; +} + +.wolai-sidebar-chevron { + color: #55514C; + font-size: 17px; +} + +.wolai-quick-actions { + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; + padding: 8px 16px 26px; +} + +.wolai-quick-actions a, +.mnote-sidebar-nav a { + border-radius: 4px; +} + +.wolai-quick-actions a { + height: 28px; + color: #3F3D39; + font-size: 19px; +} + +.wolai-quick-actions a:hover { + background: rgba(27, 28, 28, 0.07); +} + +.wolai-sidebar-body { + padding-bottom: 0; +} + +.wolai-sidebar-section { + padding: 0 8px 18px; +} + +.wolai-section-title, +.wolai-sidebar-tabs { + height: 30px; + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; + color: var(--atelier-text-muted); + font-size: 14px; + font-weight: 500; +} + +.wolai-section-icon { + color: #FF9F1A; +} + +.wolai-section-caret { + color: var(--atelier-text-soft); + font-size: 13px; +} + +.wolai-sidebar-tabs { + gap: 16px; + margin-bottom: 6px; +} + +.wolai-sidebar-tab { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 26px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: var(--atelier-text-muted); + font: inherit; + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.wolai-sidebar-tab:hover { + color: var(--atelier-text); +} + +.wolai-sidebar-tab-active { + color: var(--atelier-text); + border-bottom: 2px solid var(--atelier-primary-container); +} + +.wolai-sidebar-tab-muted { + color: #9B9892; +} + +.wolai-sidebar-tab-panels { + min-height: 0; +} + +.wolai-sidebar-tab-panel[hidden] { + display: none !important; +} + +.wolai-folder-icon { + color: #A8A49E; + font-size: 15px; +} + +.wolai-section-add { + margin-left: auto; + width: 22px; + height: 22px; + border-radius: 4px; + color: var(--atelier-text-soft); + font-size: 20px; +} + +.wolai-section-add:hover { + background: var(--atelier-sidebar-hover); + color: var(--atelier-text); +} + +.wolai-page-row { + min-height: 30px; + gap: 7px; + margin: 1px 4px; + padding: 0 8px 0 12px; + border-radius: 4px; + color: #2D2E2E; + font-size: 14px; + line-height: 1.2; +} + +.wolai-page-row:hover { + background: var(--atelier-sidebar-hover); +} + +.wolai-page-row[data-active="true"], +.wolai-active-row { + background: var(--atelier-sidebar-active); + color: var(--atelier-text); +} + +.wolai-row-caret { + width: 12px; + flex: 0 0 12px; + color: #B8B5AF; + font-size: 16px; +} + +.wolai-row-icon { + width: 20px; + color: #111111; + font-size: 16px; +} + +.wolai-row-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-tree-section { + border-top: 0; + margin-top: 0; + padding-top: 0; +} + +.sidebar-tree { + padding: 0; +} + +.sidebar-tree .tree-empty, +.wolai-sidebar-empty { + padding: 6px 12px; + color: var(--atelier-text-soft); + font-size: 13px; +} + +.sidebar-tree .tree-row { + height: 29px; + gap: 5px; + margin: 1px 4px; + padding: 0 8px; + border-radius: 4px; + color: #2D2E2E; + font-size: 14px; +} + +.sidebar-tree .tree-row:hover { + background: var(--atelier-sidebar-hover); +} + +.sidebar-tree .tree-row[data-active="true"], +.sidebar-tree .tree-row[data-selected="true"] { + background: var(--atelier-sidebar-active); + color: var(--atelier-text); +} + +.sidebar-tree .tree-toggle, +.sidebar-tree .tree-spacer { + width: 16px; + height: 22px; + color: #B8B5AF; + font-size: 13px; +} + +.sidebar-tree .tree-toggle:hover { + background: rgba(27, 28, 28, 0.06); +} + +.sidebar-tree .tree-kind-badge { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 20px; + color: #111111; + font-size: 15px; +} + +.sidebar-tree .tree-kind-badge::before { + content: "□"; +} + +.sidebar-tree .tree-kind-badge[data-kind="page"]::before { + content: "⌂"; +} + +.sidebar-tree[data-tree-shell-mode="filetree"] .tree-kind-badge[data-kind="page"]::before, +.sidebar-tree .tree-kind-badge[data-kind="document"]::before { + content: "◇"; +} + +.sidebar-tree .tree-kind-badge[data-kind="index"]::before { + content: "▤"; + color: var(--atelier-secondary); +} + +.sidebar-tree .tree-kind-badge[data-kind="folder"]::before { + content: "▱"; + color: #D08A1F; +} + +.sidebar-tree .tree-link { + padding: 0 2px; + color: inherit; + font-size: 14px; +} + +.sidebar-tree .tree-link-title { + font-weight: 400; +} + +.sidebar-tree .tree-row[data-active="true"] .tree-link-title { + font-weight: 500; +} + +.sidebar-tree .tree-actions { + gap: 1px; +} + +.sidebar-tree .tree-action { + width: 20px; + height: 20px; + color: #9A968F; + border-radius: 4px; +} + +.sidebar-tree .tree-action:hover { + background: rgba(27, 28, 28, 0.07); + color: var(--atelier-text); +} + +.sidebar-tree .tree-node .tree-children .tree-row { + padding-left: 20px; +} + +.sidebar-tree .tree-node .tree-children .tree-children .tree-row { + padding-left: 32px; +} + +.sidebar-tree .tree-node .tree-children .tree-children .tree-children .tree-row { + padding-left: 44px; +} + +.sidebar-tree .tree-node .tree-children .tree-children .tree-children .tree-children .tree-row { + padding-left: 56px; +} + +.wolai-file-tree-section { + padding-top: 2px; +} + +.wolai-explorer-title { + margin-bottom: 4px; +} + +.wolai-sidebar-footer { + border-top: 1px solid rgba(27, 28, 28, 0.08); + background: var(--atelier-sidebar); +} + +.wolai-footer-entry { + height: 44px; + gap: 8px; + color: #55514C; + font-size: 14px; +} + +.wolai-footer-entry + .wolai-footer-entry { + border-left: 1px solid rgba(27, 28, 28, 0.08); +} + +.mnote-main, +.mnote-content { + background: var(--atelier-document); +} + +.wolai-topbar { + height: 44px; + padding: 0 20px 0 22px; + background: rgba(255, 255, 255, 0.92); + color: #5A5A5A; + font-size: 14px; + line-height: 1; + backdrop-filter: blur(16px); +} + +.wolai-topbar-left, +.wolai-topbar-actions { + gap: 10px; +} + +.wolai-breadcrumb { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; + color: #6D6A65; +} + +.wolai-breadcrumb-root { + white-space: nowrap; +} + +.wolai-breadcrumb-separator { + color: #D0CDC8; +} + +.wolai-breadcrumb-current { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; + color: var(--atelier-text); + font-weight: 500; +} + +.wolai-home-icon { + color: #111111; + font-size: 16px; +} + +.wolai-icon-button { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 4px; + background: transparent; + color: #5A5A5A; + font: inherit; + font-size: 18px; + line-height: 1; + cursor: pointer; +} + +.wolai-icon-button:hover { + background: #F4F3F3; + color: var(--atelier-text); +} + +.wolai-menu-button { + color: #3D3B37; + font-size: 21px; +} + +.wolai-public-pill { + height: 26px; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 0 11px; + border: 1px solid #D6F2DC; + border-radius: 999px; + background: #ECFCEF; + color: #087B31; + font-size: 12px; + font-weight: 600; +} + +.wolai-public-pill span { + color: var(--atelier-primary-container); + font-size: 12px; +} + +.wolai-ai-inline { + color: var(--atelier-primary-container); +} + +.document-shell { + max-width: 880px; + min-height: calc(100vh - 44px); + padding: 78px 48px 160px; + margin: 0 auto; +} + +.document-shell-header { + max-width: none; + padding: 0; + margin: 0 0 28px; +} + +.document-page-icon { + height: 82px; + display: flex; + align-items: center; + color: #111111; + font-size: 72px; + line-height: 1; + margin-bottom: 12px; +} + +.document-shell-header h1 { + padding: 0; + margin: 0 0 14px; + color: var(--atelier-text); + font-size: 42px; + font-weight: 700; + line-height: 1.12; + letter-spacing: 0; + text-align: left; +} + +.document-shell-meta { + display: flex; + 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 { + display: inline-flex; + align-items: center; + gap: 6px; +} + +#mnote-editor-island { + width: 100%; + max-width: none; + min-height: 360px; + margin: 0; + padding: 0 0 48px; +} + +#mnote-leptos-tiptap-island-editor-root { + min-height: 320px; +} + +#mnote-leptos-tiptap-island-editor-root .editor-surface { + border: 0 !important; + box-shadow: none !important; + background: transparent !important; + padding: 0 !important; +} + +#mnote-leptos-tiptap-island-editor-root .ProseMirror, +.ProseMirror { + color: var(--atelier-text); + font-size: 16px; + line-height: 1.65; + outline: none; +} + +#mnote-leptos-tiptap-island-editor-root .ProseMirror { + width: 100% !important; + min-height: 280px; + margin: 0 !important; + padding: 0 !important; +} + +#mnote-leptos-tiptap-island-editor-root .ProseMirror p { + margin: 0 0 24px; +} + +#mnote-leptos-tiptap-island-editor-root .ProseMirror h1, +#mnote-leptos-tiptap-island-editor-root .ProseMirror h2, +#mnote-leptos-tiptap-island-editor-root .ProseMirror h3 { + line-height: 1.2; + letter-spacing: 0; +} + +.mnote-workspace-active-state { + padding-top: 10px; +} + +.mnote-workspace-active-link { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 30px; + color: var(--atelier-text); + font-size: 15px; + text-decoration: underline; + text-decoration-color: #D7D4CE; + text-underline-offset: 5px; +} + +.mnote-workspace-active-link::before { + content: "▤"; + color: #A19D97; + text-decoration: none; +} + +.wolai-floating-actions { + right: 24px; + bottom: 24px; + gap: 10px; + z-index: 50; +} + +.wolai-floating-button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + cursor: pointer; +} + +.wolai-floating-button--help { + width: 40px; + height: 40px; + border: 1px solid rgba(27, 28, 28, 0.1); + background: rgba(255, 255, 255, 0.86); + color: #5A5A5A; + box-shadow: 0 10px 24px rgba(27, 28, 28, 0.08); + backdrop-filter: blur(16px); + font-size: 18px; +} + +.wolai-floating-button--ai { + width: 52px; + height: 52px; + border: 0; + background: linear-gradient(135deg, var(--atelier-primary), var(--atelier-primary-container)); + color: #FFFFFF; + box-shadow: 0 16px 32px rgba(0, 110, 40, 0.22); + font-size: 23px; +} + +@media (max-width: 768px) { + .mnote-sidebar, + .wolai-sidebar { + width: 220px; + } + + .wolai-topbar { + padding: 0 12px; + } + + .wolai-public-pill, + .wolai-breadcrumb-root, + .wolai-breadcrumb-separator { + display: none; + } + + .document-shell { + padding: 48px 28px 120px; + } + + .document-page-icon { + height: 60px; + font-size: 54px; + } + + .document-shell-header h1 { + font-size: 32px; + } +} + +@media (max-width: 480px) { + .mnote-shell { + flex-direction: column; + } + + .mnote-sidebar, + .wolai-sidebar { + width: 100%; + max-height: 42vh; + border-bottom: 1px solid rgba(27, 28, 28, 0.08); + box-shadow: none; + } + + .document-shell { + padding: 36px 20px 112px; + } + + .document-shell-header h1 { + font-size: 28px; + } + + .wolai-topbar-actions { + gap: 2px; + } +} + "##; #[cfg(test)] @@ -919,6 +1618,6 @@ mod tests { // 至少 2000 字符才能包含完整样式 assert!(MNOTE_CSS.len() > 2000); // 最多 20000 字符避免过于臃肿 - assert!(MNOTE_CSS.len() < 20000); + assert!(MNOTE_CSS.len() < 32000); } } diff --git a/rust/crates/mnote-web/src/workspace_shell.rs b/rust/crates/mnote-web/src/workspace_shell.rs index 0197f46a..1487b9f4 100644 --- a/rust/crates/mnote-web/src/workspace_shell.rs +++ b/rust/crates/mnote-web/src/workspace_shell.rs @@ -102,13 +102,13 @@ pub fn build_workspace_shell_projection( id: "trash".into(), label: "垃圾箱".into(), href: "/trash".into(), - icon: "🗑".into(), + icon: "⌫".into(), }, WorkspaceShellEntry { id: "templates".into(), label: "模板中心".into(), href: "/templates".into(), - icon: "📦".into(), + icon: "◇".into(), }, ], } @@ -323,6 +323,35 @@ mod tests { assert!(html.contains("data-mnote-action=\"create-page\"")); } + #[test] + fn workspace_shell_sidebar_html_uses_single_tabbed_tree_host() { + let dataset = json!({ + "workspaces": [{ "id": "ws_demo", "name": "开发用户 的工作区" }], + "documents": [ + { "id": "doc_root", "workspace_id": "ws_demo", "title": "Root", "parent_id": null, "sort_order": 0 } + ] + }); + let projection = build_workspace_shell_projection( + &dataset, + "ws_demo", + Some("doc_root"), + "开发用户 的工作区", + ); + let html = render_workspace_shell_sidebar_html( + &projection, + Some(r#"
    "#), + Some(r#"
      "#), + ); + + assert!(html.contains(r#"data-mnote-sidebar-tree-tab="page""#)); + assert!(html.contains(r#"data-mnote-sidebar-tree-tab="filetree""#)); + assert!(html.contains(r#"data-mnote-sidebar-tree-panel="page""#)); + assert!(html.contains(r#"data-mnote-sidebar-tree-panel="filetree" hidden"#)); + assert!(html.contains(r#"id="sidebar-tree-root""#)); + assert!(html.contains(r#"id="sidebar-file-tree-root""#)); + assert!(!html.contains("wolai-file-tree-section")); + } + #[test] fn workspace_shell_sidebar_html_outputs_empty_state() { let dataset = json!({ @@ -368,16 +397,18 @@ pub fn render_workspace_shell_sidebar_html( ) }) .unwrap_or_default(); - let file_tree_panel = file_tree_html + let file_tree_content = file_tree_html .map(str::trim) .filter(|value| !value.is_empty()) .map(|html| { format!( - r#"
      文件树
      "#, + r#""#, escape_html(&projection.workspace_id), ) }) - .unwrap_or_default(); + .unwrap_or_else(|| { + r#"
      暂无文件或页面
      "#.to_string() + }); let empty_state = r#"
      暂无页面
      "#; let my_pages = if !tree_html.is_empty() { tree_html @@ -401,7 +432,7 @@ pub fn render_workspace_shell_sidebar_html( .join(""); format!( - r#"
      星标置顶
      {starred_rows}
      我的页面
      {my_pages}
      {file_tree_panel}"#, + r#"
      星标置顶
      {starred_rows}
      {my_pages}
      "#, escape_html(&projection.workspace_id), ) } @@ -411,7 +442,7 @@ fn render_item_row(item: &WorkspaceShellItem) -> String { let depth_style = if item.depth == 0 { String::new() } else { - format!(r#" style="padding-left:{}px""#, 12 + item.depth.min(6) * 18) + format!(r#" style="padding-left:{}px""#, 12 + item.depth.min(6) * 12) }; let parent_attr = item .parent_id @@ -424,7 +455,7 @@ fn render_item_row(item: &WorkspaceShellItem) -> String { "" }; format!( - r#"{}{}"#, + r#"{}{}"#, escape_html(&item.href), escape_html(&item.id), item.depth, diff --git a/scripts/task119-rust-web-wolai-visual-regression-smoke.js b/scripts/task119-rust-web-wolai-visual-regression-smoke.js index cb53d0ab..92cfd453 100644 --- a/scripts/task119-rust-web-wolai-visual-regression-smoke.js +++ b/scripts/task119-rust-web-wolai-visual-regression-smoke.js @@ -55,6 +55,11 @@ async function main() { const topbar = page.getByTestId("wolai-topbar"); const floatingAi = page.getByTestId("wolai-floating-ai"); const floatingHelp = page.getByTestId("wolai-floating-help"); + const sidebarTreeTabs = page.getByTestId("wolai-sidebar-tree-tabs"); + const pageTreePanel = page.locator('[data-mnote-sidebar-tree-panel="page"]').first(); + const fileTreePanel = page.locator('[data-mnote-sidebar-tree-panel="filetree"]').first(); + const pageTreeTab = page.locator('[data-mnote-sidebar-tree-tab="page"]').first(); + const fileTreeTab = page.locator('[data-mnote-sidebar-tree-tab="filetree"]').first(); const content = page.locator(".mnote-content").first(); const staticHomeLinks = page.locator(".mnote-home-links"); const documentShell = page.locator(".document-shell").first(); @@ -67,6 +72,7 @@ async function main() { const sidebarBox = await boundingBox(sidebar, "左侧栏"); const topbarBox = await boundingBox(topbar, "顶栏"); + await boundingBox(sidebarTreeTabs, "页面树/文件树切换栏"); const contentBox = await boundingBox(content, "主内容区"); const primaryContentBox = hasDocumentShell ? await boundingBox(documentShell, "active page shell") @@ -84,7 +90,30 @@ async function main() { assert(floatingHelpBox.x > sidebarBox.width, "帮助浮动按钮不应落入左侧栏"); assert(!boxesIntersect(floatingAiBox, primaryContentBox), "AI 浮动按钮不应遮挡主内容"); assert(!boxesIntersect(floatingHelpBox, primaryContentBox), "帮助浮动按钮不应遮挡主内容"); - assert(floatingHelpBox.y > floatingAiBox.y, "帮助按钮应位于 AI 按钮下方"); + assert(floatingHelpBox.y < floatingAiBox.y, "帮助按钮应位于 AI 按钮上方,匹配 Stitch 目标"); + + 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 pageTreeTab.getAttribute("aria-selected"), "true", "默认应选中我的页面 tab"); + assert.equal(await fileTreeTab.getAttribute("aria-selected"), "false", "默认不应选中 Explorer tab"); + assert.equal(await pageTreePanel.evaluate((node) => node.hidden), false, "默认应显示页面树 panel"); + assert.equal(await fileTreePanel.evaluate((node) => node.hidden), true, "默认应隐藏文件树 panel"); + + await fileTreeTab.click({ timeout: UI_TIMEOUT_MS }); + await page.waitForFunction(() => { + const filePanel = document.querySelector('[data-mnote-sidebar-tree-panel="filetree"]'); + const pagePanel = document.querySelector('[data-mnote-sidebar-tree-panel="page"]'); + return filePanel instanceof HTMLElement && pagePanel instanceof HTMLElement && !filePanel.hidden && pagePanel.hidden; + }, null, { timeout: UI_TIMEOUT_MS }); + await boundingBox(fileTreePanel, "Explorer 文件树 panel"); + assert.equal(await fileTreeTab.getAttribute("aria-selected"), "true", "点击后应选中 Explorer tab"); + + await pageTreeTab.click({ timeout: UI_TIMEOUT_MS }); + await page.waitForFunction(() => { + const filePanel = document.querySelector('[data-mnote-sidebar-tree-panel="filetree"]'); + const pagePanel = document.querySelector('[data-mnote-sidebar-tree-panel="page"]'); + return filePanel instanceof HTMLElement && pagePanel instanceof HTMLElement && filePanel.hidden && !pagePanel.hidden; + }, null, { timeout: UI_TIMEOUT_MS }); await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); diff --git a/test-results/task119-rust-web-wolai-visual-regression-root.png b/test-results/task119-rust-web-wolai-visual-regression-root.png index 86e36900..e9c35fe1 100644 Binary files a/test-results/task119-rust-web-wolai-visual-regression-root.png and b/test-results/task119-rust-web-wolai-visual-regression-root.png differ diff --git a/test-results/task123-stitch-document-parity.png b/test-results/task123-stitch-document-parity.png new file mode 100644 index 00000000..f66fd727 Binary files /dev/null and b/test-results/task123-stitch-document-parity.png differ