From e90b8cb6327ebb74f3f48d3a0238797bc2215c09 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Mon, 25 May 2026 22:53:15 +0800 Subject: [PATCH] refactor: split sidebar shell runtime --- ...ebar-tree-runtime-second-stage-split-v1.md | 95 +++++++++++ ...ebar-tree-runtime-second-stage-split-v1.md | 77 --------- ...cal-folder-resource-runtime-followup-v1.md | 33 ++++ ...-page-ai-sidebar-runtime-owner-split-v1.md | 32 ++++ .../browser/sidebar-shell-runtime.js | 148 ++++++++++++++++++ .../mnote-web/browser/sidebar-tree-runtime.js | 102 +++--------- rust/crates/mnote-web/src/routes/mod.rs | 8 + rust/crates/mnote-web/src/routes/web_shell.rs | 14 ++ rust/crates/mnote-web/src/ssr/pages/layout.rs | 22 ++- 9 files changed, 369 insertions(+), 162 deletions(-) create mode 100644 design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md delete mode 100644 design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md create mode 100644 design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md create mode 100644 design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md create mode 100644 rust/crates/mnote-web/browser/sidebar-shell-runtime.js diff --git a/design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md b/design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md new file mode 100644 index 00000000..f56fd62f --- /dev/null +++ b/design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md @@ -0,0 +1,95 @@ +# 3-22 Sidebar Tree Runtime 二阶段拆分 v1 + +> 创建时间:2026-05-25 +> 完成时间:2026-05-25 +> 状态:`done` +> 来源:承接 `3-19` / `3-20` / `3-21` 完成后的剩余规模问题。 + +## 1. 背景 + +`3-21` 已经解决原始痛点:CodeGraph 不能有效定位 `layout.rs` 内大型 Rust raw string。现在 `SIDEBAR_TREE_JS` 已外置为: + +```text +rust/crates/mnote-web/browser/sidebar-tree-runtime.js +``` + +这让 CodeGraph、IDE、浏览器 stack trace 都能把它当作一等 JavaScript 文件处理。但该文件仍约 1 万行,职责仍包含 sidebar workspace、page tree、file tree、Page AI panel、local folder/mindmap observer、resource open intent、secondary pane 目标、外部 drop 和 resize 等多条链路。 + +因此二阶段目标不是再解决 raw string,而是把一个可索引的 JS 巨文件继续拆成可审计、可派发、可测试的小模块。 + +## 2. 原则 + +- 保持 Rust kernel / mnote-web projection / tree command 语义不变。 +- 每一刀只迁一个边界清楚的 helper 或 runtime 簇。 +- 不重写 sidebar,不改变用户可见行为。 +- 不把 Page AI panel 混入 tree/filetree runtime;AI 相关大块应转入 `07-ai`。 +- `layout.rs` 继续只承担 SSR/bootstrap 和 asset script 注入。 + +## 3. 推荐拆分顺序 + +### Batch A:Page AI panel 边界审计 + +- [x] 只读审计 `sidebar-tree-runtime.js` 中 Page AI panel 状态、消息、profile、skills、permission requests、gateway health 等代码块。 +- [x] 判断是否新建 `design/07-ai/process/...` 承接,而不是继续放在 `03-rust-web`。 +- [x] 本批不迁代码,只输出 owner 和最小 smoke 建议。 + +执行结果:Page AI panel 已拆到 `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`,后续由 `07-ai` 承接。当前粗略搜索 `pageAi*` / `data-page-ai` / `PAGE_AI` 仍约 800 处,不能继续作为 tree/filetree runtime 的普通剩余项处理。 + +### Batch B:local folder / mindmap fetch observer + +- [x] 只读审计 `installMindmapAssetFetchObserver`、local mindmap source 参数、fetch hook、sidebar snapshot refresh 的边界。 +- [x] 只迁纯 URL/body/method/file name/helper 层以外仍适合外置的最小 runtime。 +- [x] 将后续实现和 `task455-local-folder-mindmap-clean-smoke.js` / `task479-local-folder-markdown-resource-lifecycle-smoke.js` 验证要求拆到新的 owner 文档。 + +执行结果:本批不在 `3-22` 中继续迁代码,后续拆到 `design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md`。原因是剩余边界仍强依赖 fetch hook、projection render、workspace/root/document 状态和 selection/focus 恢复,不能作为 sidebar shell helper 同批处理。 + +### Batch C:sidebar workspace / tab / resize shell helper + +- [x] 拆出 workspace sidebar collapse、tab restore、resize 相关的纯 DOM/state helper。 +- [x] 不迁 page tree / filetree projection 语义。 +- [x] 用轻量 DOM 单测或浏览器 smoke 证明侧栏首屏不回归。 + +执行结果:新增 `rust/crates/mnote-web/browser/sidebar-shell-runtime.js`,迁出 `toggleWorkspaceSidebar`、`installWorkspaceSidebarResizer`、`readStoredSidebarTreeMode`、`persistSidebarTreeMode`、`activeSidebarTreeMode`、`applySidebarTreeTab`、`switchSidebarTreeTab`、`restoreSidebarTreeTab`。`sidebar-tree-runtime.js` 仅保留薄 wrapper,未触碰 page tree / filetree projection 语义。 + +### Batch D:resource/filetree 编排剩余薄壳 + +- [x] 复核 `refreshLocalFolderSidebarSnapshot`、`appendUploadedAssetRow`、`resolveFileTreeUploadTarget` 目前有意保留在 shell 的原因是否仍成立。 +- [x] 只迁稳定的 DOM helper,不迁 fetch、projection render、selection/focus 编排。 +- [x] 继续用 `task479` 覆盖本地 Markdown 附件和文件树 drop 目标。 + +执行结果:后续拆到 `design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md`,本文件不继续承接 local folder/resource 编排。 + +## 4. 不建议的下一步 + +- 不建议直接整体重写 `sidebar-tree-runtime.js`。 +- 不建议把 `mnote-web --lib` 全量失败作为本文件前置阻塞;该问题已拆到 `bugs/03-rust-web/process/3-24-mnote-web-lib-test-failures-v1.md`。 +- 不建议为了 DRY 把 shell 业务编排强行迁入小 helper,尤其是需要 workspace/root/document/session 状态的函数。 + +## 5. 验收基线 + +每个实现批次至少选择相关命令: + +```bash +node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js +cargo fmt --manifest-path rust/Cargo.toml --all --check +cargo check --manifest-path rust/Cargo.toml -p mnote-web +NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js +NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task455-local-folder-mindmap-clean-smoke.js +``` + +涉及 AI panel 的批次应另走 `07-ai` 设计和页面 AI smoke。 + +## 6. 归档条件 + +- [x] `sidebar-tree-runtime.js` 的下一批高风险职责已有 owner,不再把 Page AI、local folder、filetree 编排混在一个无边界任务里。 +- [x] 至少一个二阶段实现批次落地,且 smoke 通过。 +- [x] 后续剩余项已经拆到对应 owner 文档,而不是继续堆在本文件。 + +## 7. 验证记录 + +- 2026-05-25:Batch C 落地 `sidebar-shell-runtime.js`。`sidebar-tree-runtime.js` 从 10,654 行降至 10,594 行,新模块 150 行。 +- 2026-05-25:已验证 `node --check rust/crates/mnote-web/browser/sidebar-shell-runtime.js` 与 `node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js`。 +- 2026-05-25:已验证 `cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`。 +- 2026-05-25:已验证专项 Rust 单测:`cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests:: -- --test-threads=1`,38/38 通过;`cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::tests::mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`,1/1 通过。 +- 2026-05-25:已启动当前源码临时服务 `MNOTE_WEB_BIND=127.0.0.1:3024 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3024 cargo run --manifest-path rust/Cargo.toml -p mnote-web`,并通过 `MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3024 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。结果 `ok:true`,截图:`/tmp/mnote-task479-lifecycle-MZEmnG/screenshots-1779720559336/final-state.png`。 +- 2026-05-25:Playwright 轻量断言通过:`window.__mnoteSidebarShellRuntime` 存在,页面同时加载 `sidebar-shell-runtime.js` 与 `sidebar-tree-runtime.js` module script,`data-mnote-sidebar-resizer-bound="true"`。 diff --git a/design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md b/design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md deleted file mode 100644 index 9af21030..00000000 --- a/design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md +++ /dev/null @@ -1,77 +0,0 @@ -# 3-22 Sidebar Tree Runtime 二阶段拆分 v1 - -> 创建时间:2026-05-25 -> 状态:`process` -> 来源:承接 `3-19` / `3-20` / `3-21` 完成后的剩余规模问题。 - -## 1. 背景 - -`3-21` 已经解决原始痛点:CodeGraph 不能有效定位 `layout.rs` 内大型 Rust raw string。现在 `SIDEBAR_TREE_JS` 已外置为: - -```text -rust/crates/mnote-web/browser/sidebar-tree-runtime.js -``` - -这让 CodeGraph、IDE、浏览器 stack trace 都能把它当作一等 JavaScript 文件处理。但该文件仍约 1 万行,职责仍包含 sidebar workspace、page tree、file tree、Page AI panel、local folder/mindmap observer、resource open intent、secondary pane 目标、外部 drop 和 resize 等多条链路。 - -因此二阶段目标不是再解决 raw string,而是把一个可索引的 JS 巨文件继续拆成可审计、可派发、可测试的小模块。 - -## 2. 原则 - -- 保持 Rust kernel / mnote-web projection / tree command 语义不变。 -- 每一刀只迁一个边界清楚的 helper 或 runtime 簇。 -- 不重写 sidebar,不改变用户可见行为。 -- 不把 Page AI panel 混入 tree/filetree runtime;AI 相关大块应转入 `07-ai`。 -- `layout.rs` 继续只承担 SSR/bootstrap 和 asset script 注入。 - -## 3. 推荐拆分顺序 - -### Batch A:Page AI panel 边界审计 - -- [ ] 只读审计 `sidebar-tree-runtime.js` 中 Page AI panel 状态、消息、profile、skills、permission requests、gateway health 等代码块。 -- [ ] 判断是否新建 `design/07-ai/process/...` 承接,而不是继续放在 `03-rust-web`。 -- [ ] 本批不迁代码,只输出 owner 和最小 smoke 建议。 - -### Batch B:local folder / mindmap fetch observer - -- [ ] 只读审计 `installMindmapAssetFetchObserver`、local mindmap source 参数、fetch hook、sidebar snapshot refresh 的边界。 -- [ ] 只迁纯 URL/body/method/file name/helper 层以外仍适合外置的最小 runtime。 -- [ ] 验证 `task455-local-folder-mindmap-clean-smoke.js` 和 `task479-local-folder-markdown-resource-lifecycle-smoke.js`。 - -### Batch C:sidebar workspace / tab / resize shell helper - -- [ ] 拆出 workspace sidebar collapse、tab restore、recent roots、resize 相关的纯 DOM/state helper。 -- [ ] 不迁 page tree / filetree projection 语义。 -- [ ] 用轻量 DOM 单测或浏览器 smoke 证明侧栏首屏不回归。 - -### Batch D:resource/filetree 编排剩余薄壳 - -- [ ] 复核 `refreshLocalFolderSidebarSnapshot`、`appendUploadedAssetRow`、`resolveFileTreeUploadTarget` 目前有意保留在 shell 的原因是否仍成立。 -- [ ] 只迁稳定的 DOM helper,不迁 fetch、projection render、selection/focus 编排。 -- [ ] 继续用 `task479` 覆盖本地 Markdown 附件和文件树 drop 目标。 - -## 4. 不建议的下一步 - -- 不建议直接整体重写 `sidebar-tree-runtime.js`。 -- 不建议把 `mnote-web --lib` 全量失败作为本文件前置阻塞;该问题已拆到 `bugs/03-rust-web/process/3-24-mnote-web-lib-test-failures-v1.md`。 -- 不建议为了 DRY 把 shell 业务编排强行迁入小 helper,尤其是需要 workspace/root/document/session 状态的函数。 - -## 5. 验收基线 - -每个实现批次至少选择相关命令: - -```bash -node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js -cargo fmt --manifest-path rust/Cargo.toml --all --check -cargo check --manifest-path rust/Cargo.toml -p mnote-web -NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js -NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task455-local-folder-mindmap-clean-smoke.js -``` - -涉及 AI panel 的批次应另走 `07-ai` 设计和页面 AI smoke。 - -## 6. 归档条件 - -- `sidebar-tree-runtime.js` 的下一批高风险职责已有 owner,不再把 Page AI、local folder、filetree 编排混在一个无边界任务里。 -- 至少一个二阶段实现批次落地,且 smoke 通过。 -- 后续剩余项已经拆到对应 owner 文档,而不是继续堆在本文件。 diff --git a/design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md b/design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md new file mode 100644 index 00000000..27eae2af --- /dev/null +++ b/design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md @@ -0,0 +1,33 @@ +# 3-23 Sidebar local folder / resource runtime follow-up v1 + +> 创建时间:2026-05-25 +> 状态:`process` +> 来源:`design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md` Batch B / Batch D。 + +## 1. 背景 + +`3-22` 已完成 sidebar shell helper 第一刀,但 `sidebar-tree-runtime.js` 中 local folder、mindmap fetch observer、resource/filetree 编排仍是下一批 03-rust-web owner。 + +这些代码仍涉及: + +- `installMindmapAssetFetchObserver` +- `withLocalMindmapSourceParams` +- `applyMindmapApiMutationToFileTree` +- `refreshLocalFolderSidebarSnapshot` +- `appendUploadedAssetRow` +- `resolveFileTreeUploadTarget` + +其中纯 URL/body/method/file name helper 已有一部分迁入 `filetree-runtime.js`。剩余函数多涉及 fetch hook、workspace/root/document 状态、projection render、selection/focus 恢复、watcher 标记和资源行 patch,因此不能整体迁出。 + +## 2. 下一批边界 + +- [ ] 只读审计 mindmap fetch observer,确认是否有新的纯 helper 可迁入 `filetree-runtime.js`。 +- [ ] 只读审计 `refreshLocalFolderSidebarSnapshot` 的 post-apply 副作用,避免重复拆已经确认应留在 shell 的 fetch/projection 编排。 +- [ ] 若实现,只迁稳定 DOM helper 或 payload normalize helper,不迁 fetch hook 与 projection render。 +- [ ] 验证 `task455-local-folder-mindmap-clean-smoke.js` 和 `task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + +## 3. 非目标 + +- 不改变 local folder 资源归属语义。 +- 不迁 Rust tree command / projection 语义到前端。 +- 不把 Page AI panel 混入本文件;AI owner 已拆到 `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md`。 diff --git a/design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md b/design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md new file mode 100644 index 00000000..a1aa3e95 --- /dev/null +++ b/design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md @@ -0,0 +1,32 @@ +# 7-38 Page AI sidebar runtime owner split v1 + +> 创建时间:2026-05-25 +> 状态:`process` +> 来源:`design/03-rust-web/process/3-22-sidebar-tree-runtime-second-stage-split-v1.md` Batch A。 + +## 1. 背景 + +`rust/crates/mnote-web/browser/sidebar-tree-runtime.js` 中仍有大量 Page AI panel 代码:会话、消息、profile、skills、gateway health、ACP runtime、permission dialog、plan/status event、session search/resume 等。当前粗略搜索 `pageAi*` / `data-page-ai` / `PAGE_AI` 命中约 800 处。 + +这些代码不是 tree/filetree runtime 的长期 owner。继续把 Page AI 面板放在 `03-rust-web` 的 sidebar runtime 拆分里,会让 tree shell、local folder、AI session 三条边界继续混在一起。 + +## 2. Owner 判断 + +- owner:`07-ai` +- 运行位置:当前仍可挂在 sidebar UI,但 runtime 模块应独立于 tree/filetree runtime。 +- 目标模块候选:`rust/crates/mnote-web/browser/page-ai-panel-runtime.js` +- 主壳职责:只加载 asset、提供当前 document/workspace/root/bootstrap。 + +## 3. 后续执行建议 + +- [ ] 只读审计 Page AI panel 的状态入口:`pageUiState.pageAi*`、storage、session 列表、profile/skills、permission dialog。 +- [ ] 新增 `page-ai-panel-runtime.js`,先迁纯数据 normalize / storage helper,不迁事件流和 DOM 面板。 +- [ ] 第二刀迁 permission dialog DOM helper,保持 `/api/hermes/client/runs/*/resolve-permission` 业务调用不变。 +- [ ] 第三刀迁 session list/search/resume DOM helper。 +- [ ] 浏览器验证使用页面 AI / ACP smoke,而不是 tree/filetree smoke 替代。 + +## 4. 非目标 + +- 不在 03-rust-web 的 sidebar runtime 二阶段里继续扩大 Page AI 实现。 +- 不改变 Hermes / Reasonix ACP runtime 协议。 +- 不把 local-first agent 文件编辑链路改回粗粒度 `mnote.page.save`。 diff --git a/rust/crates/mnote-web/browser/sidebar-shell-runtime.js b/rust/crates/mnote-web/browser/sidebar-shell-runtime.js new file mode 100644 index 00000000..ee33a91b --- /dev/null +++ b/rust/crates/mnote-web/browser/sidebar-shell-runtime.js @@ -0,0 +1,148 @@ +var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode'; +var MNOTE_SIDEBAR_WIDTH_STORAGE_KEY = 'mnote.workspace.sidebarWidth.v1'; +var MNOTE_SIDEBAR_MIN_WIDTH = 220; +var MNOTE_SIDEBAR_MAX_WIDTH = 520; +var MNOTE_SIDEBAR_DEFAULT_WIDTH = 248; + +function normalizeSidebarTreeModeForShell(mode) { + return mode === 'filetree' ? 'filetree' : 'page'; +} + +function readStoredSidebarTreeMode() { + var params = new URLSearchParams(window.location.search); + var fromUrl = normalizeSidebarTreeModeForShell(params.get('treeView') || params.get('sidebarTree')); + if (params.has('treeView') || params.has('sidebarTree')) return fromUrl; + try { + var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : ''; + return normalizeSidebarTreeModeForShell(stored); + } catch (_) { + return 'page'; + } +} + +function persistSidebarTreeMode(mode) { + var normalized = normalizeSidebarTreeModeForShell(mode); + document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized); + try { + if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized); + } catch (_) {} + return normalized; +} + +function activeSidebarTreeMode() { + var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]'); + if (active instanceof HTMLElement) { + return normalizeSidebarTreeModeForShell(active.getAttribute('data-mnote-sidebar-tree-tab')); + } + return readStoredSidebarTreeMode(); +} + +function toggleWorkspaceSidebar(trigger) { + var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell'); + if (!(shell instanceof HTMLElement)) return; + var collapsed = shell.getAttribute('data-mnote-sidebar-collapsed') === 'true'; + var next = !collapsed; + shell.setAttribute('data-mnote-sidebar-collapsed', String(next)); + document.documentElement.setAttribute('data-mnote-sidebar-collapsed', String(next)); + document.documentElement.setAttribute('data-mnote-sidebar-toggle-applied', 'true'); + if (trigger instanceof HTMLElement) { + trigger.setAttribute('aria-pressed', String(next)); + trigger.setAttribute('title', next ? '展开侧栏' : '切换侧栏'); + } +} + +function clampSidebarWidth(value) { + var width = Number(value); + if (!Number.isFinite(width)) return MNOTE_SIDEBAR_DEFAULT_WIDTH; + return Math.max(MNOTE_SIDEBAR_MIN_WIDTH, Math.min(MNOTE_SIDEBAR_MAX_WIDTH, width)); +} + +function installWorkspaceSidebarResizer() { + var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell'); + var sidebar = document.querySelector('[data-testid="wolai-sidebar"]'); + var resizer = document.querySelector('[data-mnote-sidebar-resizer="true"]'); + if (!(shell instanceof HTMLElement) || !(sidebar instanceof HTMLElement) || !(resizer instanceof HTMLElement)) return; + if (resizer.getAttribute('data-mnote-sidebar-resizer-bound') === 'true') return; + resizer.setAttribute('data-mnote-sidebar-resizer-bound', 'true'); + + function applyWidth(value) { + var width = clampSidebarWidth(value); + shell.style.setProperty('--mnote-sidebar-width', width + 'px'); + document.documentElement.setAttribute('data-mnote-sidebar-width', String(width)); + return width; + } + + try { + var stored = Number(window.localStorage.getItem(MNOTE_SIDEBAR_WIDTH_STORAGE_KEY) || ''); + if (stored) applyWidth(stored); + } catch (_) {} + + resizer.addEventListener('wheel', function(event) { + event.preventDefault(); + event.stopPropagation(); + }, { passive: false }); + resizer.addEventListener('pointerdown', function(event) { + if (event.button !== 0) return; + event.preventDefault(); + var startX = event.clientX; + var startWidth = sidebar.getBoundingClientRect().width || clampSidebarWidth(0); + resizer.setPointerCapture(event.pointerId); + document.documentElement.setAttribute('data-mnote-sidebar-resizing', 'true'); + function onMove(moveEvent) { + applyWidth(startWidth + moveEvent.clientX - startX); + } + function onEnd(endEvent) { + resizer.removeEventListener('pointermove', onMove); + resizer.removeEventListener('pointerup', onEnd); + resizer.removeEventListener('pointercancel', onEnd); + try { resizer.releasePointerCapture(endEvent.pointerId); } catch (_) {} + document.documentElement.removeAttribute('data-mnote-sidebar-resizing'); + try { + window.localStorage.setItem(MNOTE_SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(sidebar.getBoundingClientRect().width))); + } catch (_) {} + } + resizer.addEventListener('pointermove', onMove); + resizer.addEventListener('pointerup', onEnd); + resizer.addEventListener('pointercancel', onEnd); + }); +} + +function applySidebarTreeTab(mode, shell) { + mode = persistSidebarTreeMode(mode); + shell = shell || document.querySelector('[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; + } +} + +function switchSidebarTreeTab(trigger) { + var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page'; + applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null); +} + +function restoreSidebarTreeTab() { + applySidebarTreeTab(readStoredSidebarTreeMode(), null); +} + +if (!window.__mnoteSidebarShellRuntime) { + window.__mnoteSidebarShellRuntime = { + activeSidebarTreeMode: activeSidebarTreeMode, + applySidebarTreeTab: applySidebarTreeTab, + installWorkspaceSidebarResizer: installWorkspaceSidebarResizer, + persistSidebarTreeMode: persistSidebarTreeMode, + readStoredSidebarTreeMode: readStoredSidebarTreeMode, + restoreSidebarTreeTab: restoreSidebarTreeTab, + switchSidebarTreeTab: switchSidebarTreeTab, + toggleWorkspaceSidebar: toggleWorkspaceSidebar + }; +} diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index edcc5cef..82dea3d2 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -77,73 +77,19 @@ return node && typeof node.closest === 'function' ? node.closest(selector) : null; } + function sidebarShellRuntimeFunction(name) { + var runtime = window.__mnoteSidebarShellRuntime; + return runtime && typeof runtime[name] === 'function' ? runtime[name] : null; + } + function toggleWorkspaceSidebar(trigger) { - var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell'); - if (!(shell instanceof HTMLElement)) return; - var collapsed = shell.getAttribute('data-mnote-sidebar-collapsed') === 'true'; - var next = !collapsed; - shell.setAttribute('data-mnote-sidebar-collapsed', String(next)); - document.documentElement.setAttribute('data-mnote-sidebar-collapsed', String(next)); - document.documentElement.setAttribute('data-mnote-sidebar-toggle-applied', 'true'); - if (trigger instanceof HTMLElement) { - trigger.setAttribute('aria-pressed', String(next)); - trigger.setAttribute('title', next ? '展开侧栏' : '切换侧栏'); - } + var runtimeFn = sidebarShellRuntimeFunction('toggleWorkspaceSidebar'); + if (runtimeFn) return runtimeFn(trigger); } function installWorkspaceSidebarResizer() { - var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell'); - var sidebar = document.querySelector('[data-testid="wolai-sidebar"]'); - var resizer = document.querySelector('[data-mnote-sidebar-resizer="true"]'); - if (!(shell instanceof HTMLElement) || !(sidebar instanceof HTMLElement) || !(resizer instanceof HTMLElement)) return; - if (resizer.getAttribute('data-mnote-sidebar-resizer-bound') === 'true') return; - resizer.setAttribute('data-mnote-sidebar-resizer-bound', 'true'); - var storageKey = 'mnote.workspace.sidebarWidth.v1'; - var minWidth = 220; - var maxWidth = 520; - function clampWidth(value) { - var width = Number(value); - if (!Number.isFinite(width)) return 248; - return Math.max(minWidth, Math.min(maxWidth, width)); - } - function applyWidth(value) { - var width = clampWidth(value); - shell.style.setProperty('--mnote-sidebar-width', width + 'px'); - document.documentElement.setAttribute('data-mnote-sidebar-width', String(width)); - return width; - } - try { - var stored = Number(window.localStorage.getItem(storageKey) || ''); - if (stored) applyWidth(stored); - } catch (_) {} - resizer.addEventListener('wheel', function(event) { - event.preventDefault(); - event.stopPropagation(); - }, { passive: false }); - resizer.addEventListener('pointerdown', function(event) { - if (event.button !== 0) return; - event.preventDefault(); - var startX = event.clientX; - var startWidth = sidebar.getBoundingClientRect().width || clampWidth(0); - resizer.setPointerCapture(event.pointerId); - document.documentElement.setAttribute('data-mnote-sidebar-resizing', 'true'); - function onMove(moveEvent) { - applyWidth(startWidth + moveEvent.clientX - startX); - } - function onEnd(endEvent) { - resizer.removeEventListener('pointermove', onMove); - resizer.removeEventListener('pointerup', onEnd); - resizer.removeEventListener('pointercancel', onEnd); - try { resizer.releasePointerCapture(endEvent.pointerId); } catch (_) {} - document.documentElement.removeAttribute('data-mnote-sidebar-resizing'); - try { - window.localStorage.setItem(storageKey, String(Math.round(sidebar.getBoundingClientRect().width))); - } catch (_) {} - } - resizer.addEventListener('pointermove', onMove); - resizer.addEventListener('pointerup', onEnd); - resizer.addEventListener('pointercancel', onEnd); - }); + var runtimeFn = sidebarShellRuntimeFunction('installWorkspaceSidebarResizer'); + if (runtimeFn) return runtimeFn(); } function escapeHtml(value) { @@ -680,6 +626,8 @@ } function readStoredSidebarTreeMode() { + var runtimeFn = sidebarShellRuntimeFunction('readStoredSidebarTreeMode'); + if (runtimeFn) return runtimeFn(); var params = new URLSearchParams(window.location.search); var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree')); if (params.has('treeView') || params.has('sidebarTree')) return fromUrl; @@ -692,6 +640,8 @@ } function persistSidebarTreeMode(mode) { + var runtimeFn = sidebarShellRuntimeFunction('persistSidebarTreeMode'); + if (runtimeFn) return runtimeFn(mode); var normalized = normalizeSidebarTreeMode(mode); document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized); try { @@ -701,6 +651,8 @@ } function activeSidebarTreeMode() { + var runtimeFn = sidebarShellRuntimeFunction('activeSidebarTreeMode'); + if (runtimeFn) return runtimeFn(); var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]'); if (active instanceof HTMLElement) { return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab')); @@ -1782,30 +1734,18 @@ } function applySidebarTreeTab(mode, shell) { - mode = persistSidebarTreeMode(mode); - shell = shell || document.querySelector('[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; - } + var runtimeFn = sidebarShellRuntimeFunction('applySidebarTreeTab'); + if (runtimeFn) return runtimeFn(mode, shell); } function switchSidebarTreeTab(trigger) { - var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page'; - applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null); + var runtimeFn = sidebarShellRuntimeFunction('switchSidebarTreeTab'); + if (runtimeFn) return runtimeFn(trigger); } function restoreSidebarTreeTab() { - applySidebarTreeTab(readStoredSidebarTreeMode(), null); + var runtimeFn = sidebarShellRuntimeFunction('restoreSidebarTreeTab'); + if (runtimeFn) return runtimeFn(); } function updateTitleEverywhere(documentId, title) { diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index a8c7bfbe..1cb1af40 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -122,6 +122,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/filetree-keyboard-runtime.js", get(web_shell::filetree_keyboard_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/sidebar-shell-runtime.js", + get(web_shell::sidebar_shell_runtime_asset), + ) .route( "/api/mnote-browser-runtime/sidebar-tree-runtime.js", get(web_shell::sidebar_tree_runtime_asset), @@ -544,6 +548,10 @@ mod tests { "/api/mnote-browser-runtime/filetree-runtime.js", "/api/mnote-browser-runtime/filetree-selection-runtime.js", "/api/mnote-browser-runtime/filetree-context-menu-runtime.js", + "/api/mnote-browser-runtime/filetree-dnd-runtime.js", + "/api/mnote-browser-runtime/filetree-keyboard-runtime.js", + "/api/mnote-browser-runtime/sidebar-shell-runtime.js", + "/api/mnote-browser-runtime/sidebar-tree-runtime.js", "/api/mnote-browser-runtime/tree-live-controller.js", "/api/mnote-browser-runtime/tree-shell-runtime.js", "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 785db20a..fa1e5523 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -4516,6 +4516,20 @@ pub async fn sidebar_tree_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn sidebar_shell_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js"); + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-store") + .header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .body(Body::from(JS)) + .unwrap_or_else(|_| Response::new(Body::empty())) +} + pub async fn filetree_keyboard_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js"); Response::builder() diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index e660dc51..b0730101 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -157,6 +157,7 @@ pub fn PageLayout( + @@ -198,6 +199,8 @@ pub fn PageLayout( #[cfg(test)] mod tests { const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js"); + const SIDEBAR_SHELL_RUNTIME_JS: &str = + include_str!("../../../browser/sidebar-shell-runtime.js"); const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str = include_str!("../../../browser/filetree-context-menu-runtime.js"); const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js"); @@ -430,7 +433,12 @@ mod tests { #[test] fn page_layout_sidebar_toggle_is_wired_to_shell_state() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function toggleWorkspaceSidebar")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-sidebar-collapsed")); + assert!(SIDEBAR_TREE_RUNTIME_JS + .contains("sidebarShellRuntimeFunction('toggleWorkspaceSidebar')")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function toggleWorkspaceSidebar")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-collapsed")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function installWorkspaceSidebarResizer")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function restoreSidebarTreeTab")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-mnote-action=\"toggle-sidebar\"]")); } @@ -805,9 +813,15 @@ mod tests { #[test] fn sidebar_runtime_supports_resizable_filetree_and_hover_titles() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function installWorkspaceSidebarResizer()")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-sidebar-resizer")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote.workspace.sidebarWidth.v1")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shell.style.setProperty('--mnote-sidebar-width'")); + assert!(SIDEBAR_TREE_RUNTIME_JS + .contains("sidebarShellRuntimeFunction('installWorkspaceSidebarResizer')")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-resizer")); + assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("mnote.workspace.sidebarWidth.v1")); + assert!( + SIDEBAR_SHELL_RUNTIME_JS.contains("shell.style.setProperty('--mnote-sidebar-width'") + ); + assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime")); + assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#)); assert!(SIDEBAR_TREE_RUNTIME_JS