refactor: extract block menu overlay layout
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
BIN
Binary file not shown.
@@ -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<BlockMenuLayout> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Element> {
|
||||
.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<BlockMenuLayout> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user