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 0d6b3d24..8979c198 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 @@ -275,4 +275,15 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 2026-05-25:Batch AH 未派发 Reasonix worker;主控本地完成 table selection overlay style 小切片。 - 修改:`editor_runtime/overlays.rs` 新增 `table_selection_overlay_style(anchor, selection)`;`lib.rs` 删除同名实现并通过 import 继续调用。 - 边界:未迁 table selection signal、row/column click handler、overlay DOM view、`current_table_overlay_anchor` 之外的 table 控件编排或 listener;本批只外置 `TableOverlayAnchor + TableSelectionOverlayState -> CSS style` 的纯样式计算。 - - 已验证:`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`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3016 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3016 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3016 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3016 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3016 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + - 已验证:`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`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3015 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3015 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3015 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3015 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3015 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 +- 2026-05-25:Batch AI 派发 1 个只读 Reasonix worker 审计 `hovered_block_from_target` / `drop_indicator_from_target` 的下一刀边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-ai-worker-a-5-29-dnd-boundary-readonly`,run id `reasonix-2026-05-25T00-03-35-386Z-3c494bf2`,显式 `--no-memory-recall`。 + - Codex 已读取 Hindsight recall、worktree `process-handoff.md`,并核对 worktree `git diff --stat` / `git status --short` 仅有 handoff 文件;runner 未生成完整 `result.json` / `final.md` / `process-handoff.json`,因此不按完整 Reasonix handoff 采纳,只把只读结论作为审计输入。 + - 审计结论经主控复核后采纳:`hovered_block_from_target` / `drop_indicator_from_target` 的 listener 壳层与 signal 编排仍留在 `lib.rs`;下一刀优先迁 `block_state_from_index(index)`,因为它是 9 个调用方共享的纯 DOM/rect -> `HoveredBlockState` 投影逻辑。 + - Codex 主控完成最小机械切片:`editor_runtime/block_dnd.rs` 新增 `block_state_from_index(index)`,承接 root/stage/block rect 与 `block_label_for_element` 计算;`lib.rs` 的同名函数退化为薄 wrapper,继续供现有 hover/keyboard/mindmap 调用链使用。 + - 边界:未迁 `hovered_block_from_target`、`drop_indicator_from_target`、`drop_indicator_from_point`、drag/drop listener、keyboard signal 分支或 block reorder 行为;本批只外置共享 block state projection。 + - 已验证:`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`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3017 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3017 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3017 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3017 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3017 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 +- 2026-05-25:Batch AI 只派发 1 个只读 Reasonix worker 审计 `handle_lane_left` / block DnD 几何边界,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-ai-worker-a-5-29-handle-lane-readonly`,run id `reasonix-2026-05-25T00-04-47-885Z-b85d144f`。 + - Codex 已读取 Hindsight recall、`process-handoff.md/json`,并核对 worktree `git status --short` / `git diff --stat` 为空;过程中发现另一组未纳入本批的旧 `0525-ai-worker-a-5-29-dnd-boundary-readonly` runner / ACP 仍在运行,主控已终止,不采纳其任何输出。 + - 审计结论经 Codex 复核后采纳:`handle_lane_left(stage_rect_width)` 属于 editor layout helper,不应迁入 `editor_runtime/block_dnd.rs`;若继续推进 DnD 边界,下一刀应迁 `pointer_in_handle_corridor` 这一层 DOM 查询包装,而不是复制 `CONTENT_COLUMN_*` / `HANDLE_*` 常量。 + - Codex 主控完成后续小切片:`editor_runtime/block_dnd.rs` 新增 `pointer_in_handle_corridor(client_x, client_y, block, handle_selector, handle_left_fallback, handle_width)`,承接 block/stage/handle DOM 查询与 `HandleCorridorGeometry` 组装;`lib.rs` 删除同名 wrapper,调用点改为显式委托 `block_dnd` helper,并继续由壳层传入 `handle_lane_left(stage_rect.width())` fallback,不复制布局常量。 + - 已验证:`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`、临时当前源码服务 `MNOTE_WEB_BIND=127.0.0.1:3018 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3018 cargo run --manifest-path rust/Cargo.toml -p mnote-web`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task489-block-menu-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task488-local-attachment-link-delete-undo-smoke.js`、`MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3018 NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js index 839752a8..86518b31 100644 --- a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js +++ b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js @@ -1284,7 +1284,7 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 566, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 883, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf); return ret; }, 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 7766b9c1..8e33bed7 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_dnd.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs index ebe1c687..83039749 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_dnd.rs @@ -3,7 +3,12 @@ //! 本模块只承接拖拽命中所需的窄 helper,不处理 Leptos signal, //! 也不分发 editor command;调用方负责保留编辑器状态编排。 -use crate::editor_runtime::block_hover_state::{DropIndicatorState, DropPlacement}; +use crate::{ + editor_runtime::block_hover_state::{ + block_label_for_element, DropIndicatorState, DropPlacement, HoveredBlockState, + }, + editor_root_element, editor_stage_element, +}; use web_sys::{window, Element, EventTarget}; pub(crate) struct HandleCorridorGeometry { @@ -124,6 +129,78 @@ pub(crate) fn drop_indicator_from_block_rects( ) } +pub(crate) fn block_state_from_index(index: usize) -> Option { + let root = editor_root_element()?; + let stage = editor_stage_element()?; + let block = root.children().item(index as u32)?; + let block_rect = block.get_bounding_client_rect(); + let stage_rect = stage.get_bounding_client_rect(); + let block_height = if block_rect.height() > 0.0 { + block_rect.height() + } else { + 28.0 + }; + + Some(HoveredBlockState { + index, + label: block_label_for_element(&block), + top: block_rect.top() - stage_rect.top(), + height: block_height, + }) +} + +pub(crate) fn pointer_in_handle_corridor( + client_x: i32, + client_y: i32, + block: &HoveredBlockState, + handle_selector: &str, + handle_left_fallback: f64, + handle_width: f64, +) -> bool { + let Some(stage) = editor_stage_element() else { + return false; + }; + let Some(root) = editor_root_element() else { + return false; + }; + let Some(block_element) = root.children().item(block.index as u32) else { + return false; + }; + + let stage_rect = stage.get_bounding_client_rect(); + let block_rect = block_element.get_bounding_client_rect(); + let handle_rect = window() + .and_then(|win| win.document()) + .and_then(|document| document.query_selector(handle_selector).ok().flatten()) + .map(|handle| handle.get_bounding_client_rect()); + + let block_left = block_rect.left() - stage_rect.left(); + let block_top = block_rect.top() - stage_rect.top(); + let block_bottom = block_rect.bottom() - stage_rect.top(); + let handle_left = handle_rect + .as_ref() + .map(|rect| rect.left() - stage_rect.left()) + .unwrap_or(handle_left_fallback); + let handle_right = handle_rect + .as_ref() + .map(|rect| rect.right() - stage_rect.left()) + .unwrap_or_else(|| handle_left + handle_width); + + pointer_in_handle_corridor_geometry( + client_x, + client_y, + &HandleCorridorGeometry { + stage_left: stage_rect.left(), + stage_top: stage_rect.top(), + block_left, + block_top, + block_bottom, + handle_left, + handle_right, + }, + ) +} + pub(crate) fn top_level_block_index(root: &Element, block: &Element) -> Option { let children = root.children(); (0..children.length()).find_map(|index| { diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index f7b0583c..6c24c7a7 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -22,9 +22,9 @@ use web_sys::{ use editor_runtime::attachment_upload::dispatch_editor_upload_request; use editor_runtime::block_dnd::{ - direct_block_from_element, direct_block_index_from_element, - drop_indicator_from_block_rects, element_within_handle_shell, event_target_from_point, - pointer_in_handle_corridor_geometry, top_level_block_index, HandleCorridorGeometry, + block_state_from_index as block_state_from_index_runtime, direct_block_from_element, + direct_block_index_from_element, drop_indicator_from_block_rects, element_within_handle_shell, + event_target_from_point, top_level_block_index, }; use editor_runtime::block_menu_overlay; use editor_runtime::block_menu_document::{ @@ -34,8 +34,7 @@ use editor_runtime::block_menu_legacy_html::{ duplicate_top_level_block_html, reorder_top_level_block_html, }; use editor_runtime::block_hover_state::{ - block_label_for_element, BlockMenuLayout, DropIndicatorState, HoveredBlockState, - PendingDragState, + BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState, }; use editor_runtime::command_sync::{ read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command, @@ -4867,23 +4866,7 @@ fn hovered_block_from_selection() -> Option { } fn block_state_from_index(index: usize) -> Option { - let root = editor_root_element()?; - let stage = editor_stage_element()?; - let block = root.children().item(index as u32)?; - let block_rect = block.get_bounding_client_rect(); - let stage_rect = stage.get_bounding_client_rect(); - let block_height = if block_rect.height() > 0.0 { - block_rect.height() - } else { - 28.0 - }; - - Some(HoveredBlockState { - index, - label: block_label_for_element(&block), - top: block_rect.top() - stage_rect.top(), - height: block_height, - }) + block_state_from_index_runtime(index) } fn prosemirror_node_size(node: &Value) -> Option { @@ -5046,56 +5029,6 @@ fn image_element_from_target(target: web_sys::EventTarget) -> Option { .flatten() } -fn pointer_in_handle_corridor(client_x: i32, client_y: i32, block: &HoveredBlockState) -> bool { - let Some(stage) = editor_stage_element() else { - return false; - }; - let Some(root) = editor_root_element() else { - return false; - }; - let Some(block_element) = root.children().item(block.index as u32) else { - return false; - }; - - let stage_rect = stage.get_bounding_client_rect(); - let block_rect = block_element.get_bounding_client_rect(); - let handle_rect = window() - .and_then(|win| win.document()) - .and_then(|document| { - document - .query_selector(HANDLE_SHELL_SELECTOR) - .ok() - .flatten() - }) - .map(|handle| handle.get_bounding_client_rect()); - - let block_left = block_rect.left() - stage_rect.left(); - let block_top = block_rect.top() - stage_rect.top(); - let block_bottom = block_rect.bottom() - stage_rect.top(); - let handle_left = handle_rect - .as_ref() - .map(|rect| rect.left() - stage_rect.left()) - .unwrap_or_else(|| handle_lane_left(stage_rect.width())); - let handle_right = handle_rect - .as_ref() - .map(|rect| rect.right() - stage_rect.left()) - .unwrap_or_else(|| handle_left + HANDLE_TRIGGER_WIDTH); - - pointer_in_handle_corridor_geometry( - client_x, - client_y, - &HandleCorridorGeometry { - stage_left: stage_rect.left(), - stage_top: stage_rect.top(), - block_left, - block_top, - block_bottom, - handle_left, - handle_right, - }, - ) -} - fn drop_indicator_from_target( target: web_sys::EventTarget, client_y: i32, @@ -7794,7 +7727,18 @@ fn App(mount_options: MountOptions) -> impl IntoView { } if let Some(current_block) = hovered_block.get_untracked() { - if pointer_in_handle_corridor(event.client_x(), event.client_y(), ¤t_block) { + if editor_runtime::block_dnd::pointer_in_handle_corridor( + event.client_x(), + event.client_y(), + ¤t_block, + HANDLE_SHELL_SELECTOR, + handle_lane_left( + editor_stage_element() + .map(|stage| stage.get_bounding_client_rect().width()) + .unwrap_or(0.0), + ), + HANDLE_TRIGGER_WIDTH, + ) { return; } } @@ -7818,7 +7762,18 @@ fn App(mount_options: MountOptions) -> impl IntoView { return; } if let Some(current_block) = hovered_block.get_untracked() { - if pointer_in_handle_corridor(event.client_x(), event.client_y(), ¤t_block) { + if editor_runtime::block_dnd::pointer_in_handle_corridor( + event.client_x(), + event.client_y(), + ¤t_block, + HANDLE_SHELL_SELECTOR, + handle_lane_left( + editor_stage_element() + .map(|stage| stage.get_bounding_client_rect().width()) + .unwrap_or(0.0), + ), + HANDLE_TRIGGER_WIDTH, + ) { return; } }