diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index 997bf9e8..b48c99e1 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -123,3 +123,4 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 未完成:`acceptDiskVersion`、`keepCurrentEditorVersion`、`writeMergedConflictResult`、document session lifecycle、secondary pane conflict smoke 仍未迁出或覆盖。 - 2026-05-25:Batch E 派发异常并终止:Worker B 任务被重复启动为两个 runner / ACP 进程(`reasonix-2026-05-24T18-25-26-844Z-33ea4d58`、`reasonix-2026-05-24T18-26-04-453Z-5ff62e40`),同一 worktree `/mnt/Data1T/mnote-worktrees/0525-e-worker-b-3-20-conflict-diff-dom` 出现并发写入风险;Codex 已终止相关 Reasonix 进程,未读取到 `final.md`、`result.json`、`process-handoff.md/json`。原始未验证 diff 不直接采纳;后续采纳必须经过 Codex 独立复核、修正和验证。 - 2026-05-25:Batch F 计划继续采用独立 worktree 候选 patch:Worker B 负责把 file tree 上传目标解析的纯 fallback / plan 归一化 helper 外置到 `filetree-runtime.js`;`layout.rs` 仍保留 async preflight 和完整 inline fallback,避免和当前 conflict panel 写集冲突。 +- 2026-05-25:Batch F Worker B 异常终止:run `reasonix-2026-05-24T18-39-34-442Z-d9e7ada1` 只产出 `prompt.md` / `memory-recall.json` / `reasonix-transcript.jsonl`,未产出 `final.md`、`result.json`、`process-handoff.md/json`,worktree 无 diff;transcript 停在 `ask_choice`,原因是 worker 误判任务书拼接了 Worker A / Worker B 两个任务并要求人工选择。Codex 已终止 runner / ACP,不采纳该 run。 diff --git a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md index fd33e93a..7f3a85cb 100644 --- a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md +++ b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md @@ -115,3 +115,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`。 - 未完成:`sync_editor_overlay_state` / `try_sync_editor_overlay_state`、block menu signal 和拖拽重排行为仍在 `lib.rs`,后续应继续按更窄 helper 拆分。 - 2026-05-25:Batch F 计划继续采用独立 worktree 候选 patch:Worker A 负责把 `hover_anchor_top`、`hover_anchor_transform`、`block_menu_layout` 或其几何计算部分迁入 `editor_runtime/block_menu_overlay.rs`,不迁拖拽 hit-test、signal 编排或 block menu 行为。 +- 2026-05-25:采纳并复核 Batch F Worker A 候选 patch:新增 `editor_runtime/block_menu_overlay.rs`,迁出 `hover_anchor_top`、`hover_anchor_transform`、`block_menu_layout` 的纯几何计算;`lib.rs` 仍保留 `editor_stage_element` DOM 查询、拖拽 hit-test、signal 编排和 block menu 行为。 + - Reasonix run:`reasonix-2026-05-24T18-39-34-366Z-631ed269`,已读取 Hindsight recall、`process-handoff.md/json`、`result.json`、diff 与验证摘要;Reasonix 自报 smoke 挂起,Codex 不采信该部分。 + - 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_overlay.rs`、`rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`、generated island wasm/js/d.ts。 + - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`。 + - 未完成:`pointer_in_handle_corridor`、`drop_indicator_from_target` / `drop_indicator_from_point`、`sync_editor_overlay_state` / `try_sync_editor_overlay_state` 仍在 `lib.rs`。 diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm index f10bed20..d6a81859 100644 Binary files a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm and b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm differ diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_overlay.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_overlay.rs new file mode 100644 index 00000000..4eabec6a --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_overlay.rs @@ -0,0 +1,100 @@ +//! Block menu hover anchor / layout helper。 +//! +//! 本模块承接 block menu 的锚点与面板布局计算,保持几何计算与 DOM 查询分离。 +//! 调用方(lib.rs)保留 DOM 查询(editor_stage_element),本模块只做纯几何计算。 +//! +//! 迁入自 lib.rs: +//! - `hover_anchor_top` +//! - `hover_anchor_transform` +//! - `block_menu_layout`(参数化版) + +use crate::editor_runtime::block_hover_state::{BlockMenuLayout, HoveredBlockState}; + +/// 多行块的最小高度阈值,超过此值视作多行。 +const HANDLE_MULTILINE_HEIGHT: f64 = 40.0; + +/// handle shell 菜单左侧最小限制。 +const HANDLE_MENU_LEFT_LIMIT: f64 = -320.0; + +/// 内容列水平内边距(与 lib.rs 中同名常量同值)。 +const CONTENT_COLUMN_HORIZONTAL_PADDING: f64 = 48.0; + +/// 内容列最大宽度(与 lib.rs 中同名常量同值)。 +const CONTENT_COLUMN_MAX_WIDTH: f64 = 708.0; + +/// 根据 stage 宽度计算实际内容列宽。 +fn content_column_width(stage_rect_width: f64) -> f64 { + let available = (stage_rect_width - (CONTENT_COLUMN_HORIZONTAL_PADDING * 2.0)).max(0.0); + available.min(CONTENT_COLUMN_MAX_WIDTH) +} + +/// 内容列左侧相对于 stage 的偏移。 +fn content_column_left(stage_rect_width: f64) -> f64 { + let column_width = content_column_width(stage_rect_width); + ((stage_rect_width - column_width) / 2.0).max(0.0) +} + +/// 内容文本区左侧相对于 stage 的偏移(列左内边距后)。 +fn content_text_left(stage_rect_width: f64) -> f64 { + content_column_left(stage_rect_width) + CONTENT_COLUMN_HORIZONTAL_PADDING +} + +/// 计算 handle shell 锚点在 stage 内的垂直位置。 +pub(crate) fn hover_anchor_top(block: &HoveredBlockState) -> f64 { + if block.height > HANDLE_MULTILINE_HEIGHT { + block.top + 12.0 + } else { + block.top + (block.height / 2.0) - 32.0 + } +} + +/// 计算 handle shell 锚点 transform 值(当前固定返回 "none")。 +pub(crate) fn hover_anchor_transform(_block: &HoveredBlockState) -> &'static str { + "none" +} + +/// 计算 block menu 面板布局。 +/// +/// 参数化版本:调用方提供 stage 的 bounding client rect 尺寸和位置, +/// 本函数做纯几何计算,不查询 DOM。 +pub(crate) fn block_menu_layout( + block: &HoveredBlockState, + stage_width: f64, + stage_height: f64, + stage_top: f64, + stage_left: f64, +) -> Option { + let shell_top = hover_anchor_top(block); + let stage_padding = 18.0; + let space_above = (shell_top - stage_padding).max(0.0); + let space_below = (stage_height - shell_top - stage_padding).max(0.0); + let open_upward = space_below < 340.0 && space_above > space_below; + let available_height = if open_upward { + space_above + } else { + space_below + }; + let max_height = if available_height >= 220.0 { + available_height + } else { + available_height.max(160.0) + }; + let menu_width = 242.0; + let menu_stage_left = + (content_text_left(stage_width) - menu_width - 132.0).max(HANDLE_MENU_LEFT_LIMIT); + let viewport_shell_top = stage_top + shell_top; + let top = if open_upward { + viewport_shell_top + 64.0 - max_height + } else { + viewport_shell_top + } + .max(8.0); + let left = (stage_left + menu_stage_left).max(8.0); + + Some(BlockMenuLayout { + max_height, + open_upward, + top, + left, + }) +} diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs index 0819f726..cdafb949 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod attachment_links; pub(crate) mod attachment_upload; pub(crate) mod block_hover_state; pub(crate) mod block_menu_document; +pub(crate) mod block_menu_overlay; pub(crate) mod block_menu_legacy_html; pub(crate) mod command_sync; pub(crate) mod dom_selection; diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index 52c506d1..0a9a05c6 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -21,6 +21,7 @@ use web_sys::{ }; use editor_runtime::attachment_upload::dispatch_editor_upload_request; +use editor_runtime::block_menu_overlay; use editor_runtime::block_menu_legacy_html::{ duplicate_top_level_block_html, reorder_top_level_block_html, }; @@ -47,7 +48,6 @@ use editor_runtime::persistence::persisted_document_storage_key; const EDITOR_STAGE_SELECTOR: &str = "[data-testid=\"mnote-leptos-tiptap-editor-stage\"]"; const EDITOR_ROOT_SELECTOR: &str = ".editor-surface .ProseMirror"; const HANDLE_SHELL_SELECTOR: &str = ".block-handle-shell"; -const HANDLE_MULTILINE_HEIGHT: f64 = 40.0; const CONTENT_COLUMN_MAX_WIDTH: f64 = 708.0; const CONTENT_COLUMN_HORIZONTAL_PADDING: f64 = 48.0; const HANDLE_STAGE_PADDING_LEFT: f64 = 18.0; @@ -55,7 +55,6 @@ const HANDLE_TRIGGER_WIDTH: f64 = 22.0; const HANDLE_TRIGGER_GAP: f64 = 4.0; const HANDLE_MENU_GAP: f64 = 0.0; const HANDLE_TEXT_ALIGN_OFFSET: f64 = 96.0; -const HANDLE_MENU_LEFT_LIMIT: f64 = -320.0; const RUNTIME_NAME: &str = "8123-leptos-tiptap-runtime"; const RUNTIME_VERSION: &str = "1.1.0"; const PROTOCOL: &str = "mnote.leptos_tiptap.bridge.v1"; @@ -5391,18 +5390,6 @@ fn image_element_from_target(target: web_sys::EventTarget) -> Option { .flatten() } -fn hover_anchor_top(block: &HoveredBlockState) -> f64 { - if block.height > HANDLE_MULTILINE_HEIGHT { - block.top + 12.0 - } else { - block.top + (block.height / 2.0) - 32.0 - } -} - -fn hover_anchor_transform(_block: &HoveredBlockState) -> &'static str { - "none" -} - fn pointer_in_handle_corridor(client_x: i32, client_y: i32, block: &HoveredBlockState) -> bool { let Some(stage) = editor_stage_element() else { return false; @@ -5445,44 +5432,6 @@ fn pointer_in_handle_corridor(client_x: i32, client_y: i32, block: &HoveredBlock x >= corridor_left && x <= corridor_right && y >= block_top - 18.0 && y <= block_bottom + 18.0 } -fn block_menu_layout(block: &HoveredBlockState) -> Option { - let stage = editor_stage_element()?; - let stage_rect = stage.get_bounding_client_rect(); - let shell_top = hover_anchor_top(block); - let stage_padding = 18.0; - let space_above = (shell_top - stage_padding).max(0.0); - let space_below = (stage_rect.height() - shell_top - stage_padding).max(0.0); - let open_upward = space_below < 340.0 && space_above > space_below; - let available_height = if open_upward { - space_above - } else { - space_below - }; - let max_height = if available_height >= 220.0 { - available_height - } else { - available_height.max(160.0) - }; - let menu_width = 242.0; - let menu_stage_left = - (content_text_left(stage_rect.width()) - menu_width - 132.0).max(HANDLE_MENU_LEFT_LIMIT); - let viewport_shell_top = stage_rect.top() + shell_top; - let top = if open_upward { - viewport_shell_top + 64.0 - max_height - } else { - viewport_shell_top - } - .max(8.0); - let left = (stage_rect.left() + menu_stage_left).max(8.0); - - Some(BlockMenuLayout { - max_height, - open_upward, - top, - left, - }) -} - fn drop_indicator_from_target( target: web_sys::EventTarget, client_y: i32, @@ -9714,8 +9663,8 @@ fn App(mount_options: MountOptions) -> impl IntoView { return ().into_any(); } - let anchor_top = format!("{}px", hover_anchor_top(&block)); - let anchor_transform = hover_anchor_transform(&block); + let anchor_top = format!("{}px", block_menu_overlay::hover_anchor_top(&block)); + let anchor_transform = block_menu_overlay::hover_anchor_transform(&block); let handle_left = editor_stage_element() .map(|stage| { let stage_rect = stage.get_bounding_client_rect(); @@ -9905,7 +9854,10 @@ fn App(mount_options: MountOptions) -> impl IntoView { { let current_duplicate_label = duplicate_block_label.clone(); let current_delete_label = delete_block_label.clone(); - let menu_layout = block_menu_layout(&block).unwrap_or(BlockMenuLayout { + let menu_layout = editor_stage_element().and_then(|stage| { + let rect = stage.get_bounding_client_rect(); + block_menu_overlay::block_menu_layout(&block, rect.width(), rect.height(), rect.top(), rect.left()) + }).unwrap_or(BlockMenuLayout { max_height: 360.0, open_upward: false, top: 80.0,