chore: land tree view-state, vault, Pi module split, and repo hygiene

Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
This commit is contained in:
Agent Board
2026-07-21 05:13:05 +08:00
parent 6f9c7d3b58
commit b798f628ee
264 changed files with 17480 additions and 17314 deletions
@@ -1,7 +1,8 @@
use crate::editor_runtime::{
block_hover_state::{BlockMenuLayout, HoveredBlockState},
block_transform::{
apply_text_align, run_block_turn_into_action, run_block_turn_into_folded_heading_action,
apply_highlight_color, apply_text_align, apply_text_color, clear_text_color,
run_block_turn_into_action, run_block_turn_into_folded_heading_action,
run_block_turn_into_page_action, top_level_block_is_page, top_level_block_matches_action,
top_level_block_range,
},
@@ -19,9 +20,9 @@ use crate::{
};
use leptos::prelude::*;
use leptos_tiptap::{TiptapEditorHandle, TiptapSelectionState, TiptapTextAlign};
use serde_json::Value;
use serde_json::{json, Value};
use wasm_bindgen_futures::spawn_local;
use web_sys::{MouseEvent, WheelEvent};
use web_sys::{window, CustomEvent, CustomEventInit, MouseEvent, WheelEvent};
#[derive(Clone, Copy)]
struct FoldedHeadingAction {
@@ -58,6 +59,167 @@ const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
},
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg(test)]
enum BlockMenuSecondarySurface {
InlineFeedback,
Dialog,
Submenu,
}
#[derive(Clone, Copy)]
#[cfg(test)]
struct BlockMenuSecondaryActionSpec {
test_id: &'static str,
surface: BlockMenuSecondarySurface,
}
#[cfg(test)]
const fn block_menu_secondary_action_specs() -> [BlockMenuSecondaryActionSpec; 5] {
[
BlockMenuSecondaryActionSpec {
test_id: "block-drag-menu-item-copy-link",
surface: BlockMenuSecondarySurface::InlineFeedback,
},
BlockMenuSecondaryActionSpec {
test_id: "block-drag-menu-item-move",
surface: BlockMenuSecondarySurface::Dialog,
},
BlockMenuSecondaryActionSpec {
test_id: "block-drag-menu-item-history",
surface: BlockMenuSecondarySurface::Dialog,
},
BlockMenuSecondaryActionSpec {
test_id: "block-drag-menu-item-comment",
surface: BlockMenuSecondarySurface::Dialog,
},
BlockMenuSecondaryActionSpec {
test_id: "block-drag-menu-item-color",
surface: BlockMenuSecondarySurface::Submenu,
},
]
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BlockAuxPanel {
Move,
History,
Comment,
}
#[derive(Clone, Copy)]
struct BlockColorOption {
id: &'static str,
label: &'static str,
value: &'static str,
}
const BLOCK_TEXT_COLORS: [BlockColorOption; 5] = [
BlockColorOption {
id: "default",
label: "默认",
value: "#37352f",
},
BlockColorOption {
id: "gray",
label: "灰色",
value: "#787774",
},
BlockColorOption {
id: "red",
label: "红色",
value: "#e03e3e",
},
BlockColorOption {
id: "blue",
label: "蓝色",
value: "#337ea9",
},
BlockColorOption {
id: "green",
label: "绿色",
value: "#448361",
},
];
const BLOCK_HIGHLIGHT_COLORS: [BlockColorOption; 5] = [
BlockColorOption {
id: "gray",
label: "灰底",
value: "#f1f1ef",
},
BlockColorOption {
id: "red",
label: "红底",
value: "#fdebec",
},
BlockColorOption {
id: "yellow",
label: "黄底",
value: "#fbf3db",
},
BlockColorOption {
id: "blue",
label: "蓝底",
value: "#e7f3f8",
},
BlockColorOption {
id: "green",
label: "绿底",
value: "#edf3ec",
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secondary_block_menu_entries_are_real_surfaces_not_placeholder_feedback() {
let specs = block_menu_secondary_action_specs();
assert_eq!(
specs
.iter()
.find(|spec| spec.test_id == "block-drag-menu-item-copy-link")
.unwrap()
.surface,
BlockMenuSecondarySurface::InlineFeedback
);
assert_eq!(
specs
.iter()
.find(|spec| spec.test_id == "block-drag-menu-item-move")
.unwrap()
.surface,
BlockMenuSecondarySurface::Dialog
);
assert_eq!(
specs
.iter()
.find(|spec| spec.test_id == "block-drag-menu-item-history")
.unwrap()
.surface,
BlockMenuSecondarySurface::Dialog
);
assert_eq!(
specs
.iter()
.find(|spec| spec.test_id == "block-drag-menu-item-comment")
.unwrap()
.surface,
BlockMenuSecondarySurface::Dialog
);
assert_eq!(
specs
.iter()
.find(|spec| spec.test_id == "block-drag-menu-item-color")
.unwrap()
.surface,
BlockMenuSecondarySurface::Submenu
);
}
}
pub(crate) struct BlockHandleMenuViewProps {
pub(crate) editor: TiptapEditorHandle,
pub(crate) block: HoveredBlockState,
@@ -115,6 +277,106 @@ fn close_block_transform_menus(signals: BlockHandleMenuSignals) {
signals.set_block_folded_title_open.set(false);
}
fn dispatch_mnote_toast(message: &str, kind: &str) {
let Some(win) = window() else {
return;
};
let Ok(detail) = serde_wasm_bindgen::to_value(&json!({
"message": message,
"kind": kind,
})) else {
return;
};
let init = CustomEventInit::new();
init.set_detail(&detail);
if let Ok(event) = CustomEvent::new_with_event_init_dict("mnote:toast", &init) {
let _ = win.dispatch_event(&event);
}
}
fn set_current_block_selection(
editor: TiptapEditorHandle,
block_index: usize,
) -> Result<(), String> {
let document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let range = top_level_block_range(&document, block_index)
.ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;
editor
.set_text_selection(range)
.map_err(|err| format!("选中当前块失败:{err}"))
}
fn persist_block_menu_command(
editor: TiptapEditorHandle,
signals: BlockHandleMenuSignals,
success_message: &str,
) {
let (html, snapshot, json_text) = read_editor_snapshot(editor);
signals.set_dirty_count.update(|count| *count += 1);
signals.set_html_output.set(html.clone());
signals.set_document_json.set(snapshot.clone());
signals.set_json_output.set(json_text);
match persist_document_state(
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
&signals.title.get_untracked(),
&snapshot,
Some(html),
) {
Ok(()) => signals
.set_command_feedback
.set(format!("{success_message},并已写入本地草稿")),
Err(err) => signals
.set_command_feedback
.set(format!("{success_message},但本地保存失败:{err}")),
}
}
fn handle_block_text_color_click(
editor: TiptapEditorHandle,
block_index: usize,
value: &'static str,
signals: BlockHandleMenuSignals,
) {
let result = set_current_block_selection(editor, block_index)
.and_then(|_| apply_text_color(editor, value).map(str::to_string));
match result {
Ok(message) => persist_block_menu_command(editor, signals, &message),
Err(err) => signals.set_command_feedback.set(err),
}
}
fn handle_block_highlight_color_click(
editor: TiptapEditorHandle,
block_index: usize,
value: &'static str,
signals: BlockHandleMenuSignals,
) {
let result = set_current_block_selection(editor, block_index)
.and_then(|_| apply_highlight_color(editor, Some(value)).map(str::to_string));
match result {
Ok(message) => persist_block_menu_command(editor, signals, &message),
Err(err) => signals.set_command_feedback.set(err),
}
}
fn handle_block_clear_color_click(
editor: TiptapEditorHandle,
block_index: usize,
signals: BlockHandleMenuSignals,
) {
let result = set_current_block_selection(editor, block_index).and_then(|_| {
let message = clear_text_color(editor)?;
let _ = apply_highlight_color(editor, None)?;
Ok(message.to_string())
});
match result {
Ok(message) => persist_block_menu_command(editor, signals, &message),
Err(err) => signals.set_command_feedback.set(err),
}
}
fn handle_turn_into_action_click(
editor: TiptapEditorHandle,
block_index: usize,
@@ -358,6 +620,8 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
let block_index = block.index;
let menu_top = format!("{}px", menu_layout.top);
let menu_bottom = "auto";
let (block_aux_panel, set_block_aux_panel) = signal::<Option<BlockAuxPanel>>(None);
let (block_color_open, set_block_color_open) = signal(false);
view! {
<div
@@ -509,16 +773,18 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
data-testid="block-drag-menu-item-copy-link"
on:click=move |event: MouseEvent| {
event.stop_propagation();
set_block_aux_panel.set(None);
set_block_color_open.set(false);
let Some(block_id) = runtime_block_id_from_index(block_index) else {
signals
.set_command_feedback
.set("当前块缺少 Rust block id,无法复制锚点链接".to_string());
let message = "当前块缺少 Rust block id,无法复制锚点链接";
signals.set_command_feedback.set(message.to_string());
dispatch_mnote_toast(message, "warning");
return;
};
let Some(anchor_url) = current_page_anchor_url(&block_id) else {
signals
.set_command_feedback
.set("当前页面 URL 不可用,无法复制锚点链接".to_string());
let message = "当前页面 URL 不可用,无法复制锚点链接";
signals.set_command_feedback.set(message.to_string());
dispatch_mnote_toast(message, "warning");
return;
};
if let Some(block_element) = editor_block_by_id(&block_id) {
@@ -527,12 +793,14 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
spawn_local(async move {
match write_mnote_text_to_clipboard(anchor_url.clone()).await {
Ok(value) if value.as_bool().unwrap_or(false) => {
signals.set_command_feedback.set(format!("已复制块链接:{}", block_id));
let message = format!("已复制块链接:{}", block_id);
signals.set_command_feedback.set(message.clone());
dispatch_mnote_toast(&message, "success");
}
_ => {
signals
.set_command_feedback
.set(format!("块链接已生成但剪贴板写入失败:{}", anchor_url));
let message = format!("块链接已生成但剪贴板写入失败:{}", anchor_url);
signals.set_command_feedback.set(message.clone());
dispatch_mnote_toast(&message, "warning");
}
}
});
@@ -547,9 +815,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
data-testid="block-drag-menu-item-move"
on:click=move |event: MouseEvent| {
event.stop_propagation();
signals
.set_command_feedback
.set("移动/嵌入流程后续接 Page Aggregate 命令".to_string());
set_block_color_open.set(false);
set_block_aux_panel.set(Some(BlockAuxPanel::Move));
signals.set_command_feedback.set("已打开移动/嵌入面板".to_string());
}
>
<span class="block-drag-menu-icon">{material_icon("arrow_outward")}</span>
@@ -561,9 +829,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
data-testid="block-drag-menu-item-history"
on:click=move |event: MouseEvent| {
event.stop_propagation();
signals
.set_command_feedback
.set("块历史入口已记录,恢复流程后续接版本链路".to_string());
set_block_color_open.set(false);
set_block_aux_panel.set(Some(BlockAuxPanel::History));
signals.set_command_feedback.set("已打开块历史面板".to_string());
}
>
<span class="block-drag-menu-icon">{material_icon("history")}</span>
@@ -574,7 +842,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
data-testid="block-drag-menu-item-comment"
on:click=move |event: MouseEvent| {
event.stop_propagation();
signals.set_command_feedback.set("评论入口已打开".to_string());
set_block_color_open.set(false);
set_block_aux_panel.set(Some(BlockAuxPanel::Comment));
signals.set_command_feedback.set("已打开评论面板".to_string());
}
>
<span class="block-drag-menu-icon">{material_icon("chat")}</span>
@@ -585,15 +855,13 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
class="block-drag-menu-item"
data-testid="block-drag-menu-item-color"
on:mouseenter=move |_| {
signals
.set_command_feedback
.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
set_block_aux_panel.set(None);
set_block_color_open.set(true);
}
on:click=move |event: MouseEvent| {
event.stop_propagation();
signals
.set_command_feedback
.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
set_block_aux_panel.set(None);
set_block_color_open.update(|open| *open = !*open);
}
>
<span class="block-drag-menu-icon">{material_icon("palette")}</span>
@@ -603,17 +871,13 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
<button
class="block-drag-menu-item"
data-testid="block-drag-menu-item-align-center"
data-active=move || signals.selection_state.get().align_center.to_string()
on:click=move |event: MouseEvent| {
event.stop_propagation();
set_block_aux_panel.set(None);
set_block_color_open.set(false);
let result = (|| -> Result<(), String> {
let document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let range = top_level_block_range(&document, block_index)
.ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;
editor
.set_text_selection(range)
.map_err(|err| format!("选中当前块失败:{err}"))?;
set_current_block_selection(editor, block_index)?;
apply_text_align(editor, TiptapTextAlign::Center)?;
let (html, snapshot, json_text) = read_editor_snapshot(editor);
signals.set_dirty_count.update(|count| *count += 1);
@@ -662,6 +926,112 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
().into_any()
}
}}
{move || {
if block_color_open.get() {
view! {
<div
class="block-drag-submenu block-color-panel"
data-testid="block-color-panel"
on:mousedown=move |event: MouseEvent| {
event.stop_propagation();
}
on:click=move |event: MouseEvent| {
event.stop_propagation();
}
on:wheel=move |event: WheelEvent| {
trap_scroll_inside_menu(&event);
}
>
<div class="toolbar-popover-section">
<div class="toolbar-popover-title">"文字颜色"</div>
<div class="toolbar-color-grid">
{BLOCK_TEXT_COLORS.iter().map(|option| {
let value = option.value;
let label = option.label;
let testid = format!("block-text-color-{}", option.id);
view! {
<button
class="toolbar-menu-item toolbar-color-item"
data-testid=testid
on:click=move |_| {
handle_block_text_color_click(editor, block_index, value, signals);
set_block_color_open.set(false);
}
>
<span class="toolbar-color-chip" style:background=value></span>
<strong>{label}</strong>
</button>
}
}).collect_view()}
</div>
</div>
<div class="toolbar-popover-section">
<div class="toolbar-popover-title">"背景颜色"</div>
<div class="toolbar-color-grid">
{BLOCK_HIGHLIGHT_COLORS.iter().map(|option| {
let value = option.value;
let label = option.label;
let testid = format!("block-highlight-color-{}", option.id);
view! {
<button
class="toolbar-menu-item toolbar-color-item"
data-testid=testid
on:click=move |_| {
handle_block_highlight_color_click(editor, block_index, value, signals);
set_block_color_open.set(false);
}
>
<span class="toolbar-color-chip" style:background=value></span>
<strong>{label}</strong>
</button>
}
}).collect_view()}
</div>
</div>
<div class="toolbar-popover-section">
<button
class="toolbar-menu-item"
data-testid="block-clear-color"
on:click=move |_| {
handle_block_clear_color_click(editor, block_index, signals);
set_block_color_open.set(false);
}
>
<strong>"清除颜色"</strong>
<span>"移除当前块文字色与背景色"</span>
</button>
</div>
</div>
}.into_any()
} else {
().into_any()
}
}}
{move || {
match block_aux_panel.get() {
Some(BlockAuxPanel::Move) => view! {
<div class="block-drag-aux-panel" data-testid="block-move-panel" role="dialog" aria-label="移动或嵌入当前块">
<strong>"移动/嵌入到"</strong>
<span>"当前 MVP 仅开放块内编辑;跨页移动和嵌入将接入 Page Aggregate 命令。"</span>
<button class="block-drag-menu-item" disabled=true>"选择目标页面(开发中)"</button>
</div>
}.into_any(),
Some(BlockAuxPanel::History) => view! {
<div class="block-drag-aux-panel" data-testid="block-history-panel" role="dialog" aria-label="块历史">
<strong>"块历史"</strong>
<span>"当前块尚未接入版本链路;这里先显示明确面板,避免误判为点击失效。"</span>
</div>
}.into_any(),
Some(BlockAuxPanel::Comment) => view! {
<div class="block-drag-aux-panel" data-testid="block-comment-panel" role="dialog" aria-label="评论">
<strong>"评论"</strong>
<span>"评论数据层尚未开放写入;后续会在这里显示评论线程。"</span>
<button class="block-drag-menu-item" disabled=true>"添加评论(开发中)"</button>
</div>
}.into_any(),
None => ().into_any(),
}
}}
</div>
</div>
}
@@ -10,6 +10,7 @@ use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{window, CustomEvent, CustomEventInit, Node, RequestInit, RequestMode, Response};
use crate::editor_root_element;
use crate::editor_runtime::attachment_upload::dispatch_editor_upload_request;
use crate::editor_runtime::block_hover_state::HoveredBlockState;
use crate::editor_runtime::block_menu_document::{
@@ -18,7 +19,6 @@ use crate::editor_runtime::block_menu_document::{
use crate::editor_runtime::command_sync::sync_persisted_editor_command;
use crate::editor_runtime::persistence::persisted_document_identity;
use crate::editor_runtime::slash_actions::SlashActionKind;
use crate::editor_root_element;
pub(crate) fn prosemirror_node_size(node: &Value) -> Option<u32> {
match node.get("type").and_then(Value::as_str) {
@@ -282,9 +282,10 @@ pub(crate) fn run_slash_action(
let _ = editor.focus();
editor.insert_table(4, 3, false)
}
SlashActionKind::Mindmap => {
editor.insert_content(TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)), None)
}
SlashActionKind::Mindmap => editor.insert_content(
TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
None,
),
SlashActionKind::Image => {
dispatch_editor_upload_request("image", "image/*")?;
return Ok("已打开图片上传");
@@ -441,10 +442,7 @@ pub(crate) fn run_block_turn_into_folded_heading_action(
})
}
pub(crate) fn top_level_block_is_page(
editor: TiptapEditorHandle,
block_index: usize,
) -> bool {
pub(crate) fn top_level_block_is_page(editor: TiptapEditorHandle, block_index: usize) -> bool {
editor
.get_json()
.ok()
@@ -3,18 +3,26 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub(crate) const RUNTIME_NAME: &str = "8123-leptos-tiptap-runtime";
pub(crate) const RUNTIME_VERSION: &str = "1.1.0";
pub(crate) const RUNTIME_NAME: &str = "mnote-tiptap-island-runtime";
pub(crate) const RUNTIME_VERSION: &str = "1.2.0";
pub(crate) const PROTOCOL: &str = "mnote.leptos_tiptap.bridge.v1";
pub(crate) const BLOCK_DELTA_EVENT: &str = "mnote:editor:block-delta";
/// 稳定产品事件前缀(Phase 3);spike 前缀短期双发兼容。
pub(crate) const STABLE_EVENT_PREFIX: &str = "mnote:tiptap-island";
pub(crate) const EVENT_PREFIX: &str = "mnote:leptos-tiptap-spike";
pub(crate) const COMMAND_EVENT: &str = "mnote:leptos-tiptap-spike:command";
pub(crate) const HEIGHT_EVENT: &str = "mnote:leptos-tiptap-spike:height";
pub(crate) const EVENT_PREFIX: &str = "mnote:leptos-tiptap-spike";
pub(crate) const READY_EVENT: &str = "mnote:leptos-tiptap-spike:ready";
pub(crate) const CHANGE_EVENT: &str = "mnote:leptos-tiptap-spike:change";
pub(crate) const STATE_EVENT: &str = "mnote:leptos-tiptap-spike:state";
pub(crate) const STATUS_EVENT: &str = "mnote:leptos-tiptap-spike:status";
pub(crate) const SELECTION_EVENT: &str = "mnote:leptos-tiptap-spike:selection";
pub(crate) const STABLE_COMMAND_EVENT: &str = "mnote:tiptap-island:command";
pub(crate) const STABLE_READY_EVENT: &str = "mnote:tiptap-island:ready";
pub(crate) const STABLE_CHANGE_EVENT: &str = "mnote:tiptap-island:change";
pub(crate) const STABLE_STATE_EVENT: &str = "mnote:tiptap-island:state";
pub(crate) const STABLE_STATUS_EVENT: &str = "mnote:tiptap-island:status";
pub(crate) const STABLE_SELECTION_EVENT: &str = "mnote:tiptap-island:selection";
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -14,8 +14,8 @@
//! - B7: 块菜单按钮(popover trigger + focus
//! - B8: WASM gzipped 增量验证(本模块自身极小,不引入新 dep)
use leptos::prelude::*;
use leptos::ev;
use leptos::prelude::*;
/// 按钮语义变体
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -20,8 +20,8 @@ use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{CustomEvent, CustomEventInit, EventTarget, HtmlElement, HtmlInputElement};
use crate::editor_runtime::mount::next_mount_id;
use crate::editor_runtime::icons::material_icon;
use crate::editor_runtime::mount::next_mount_id;
thread_local! {
static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell<HashMap<u32, MountedMindmapShell>> = std::cell::RefCell::new(HashMap::new());
@@ -1,22 +1,22 @@
pub(crate) mod attachment_links;
pub(crate) mod attachment_upload;
pub(crate) mod block_dnd;
pub(crate) mod button;
pub(crate) mod block_hover_state;
pub(crate) mod icons;
pub(crate) mod block_handle_menu_view;
pub(crate) mod block_hover_state;
pub(crate) mod block_menu_document;
pub(crate) mod block_menu_overlay;
pub(crate) mod block_transform;
pub(crate) mod bridge_dispatch;
pub(crate) mod bridge_events;
pub(crate) mod button;
pub(crate) mod command_sync;
pub(crate) mod dismiss;
pub(crate) mod content_layout;
pub(crate) mod dismiss;
pub(crate) mod dom_events;
pub(crate) mod dom_selection;
pub(crate) mod editor_focus;
pub(crate) mod history_safe_commands;
pub(crate) mod icons;
pub(crate) mod mindmap_node_view;
pub(crate) mod mount;
pub(crate) mod overlays;
@@ -74,11 +74,8 @@ pub(crate) fn next_mount_id() -> u32 {
pub(crate) fn set_runtime_mount_context(context: Option<(u32, EventTarget, RuntimeDeliveryMode)>) {
RUNTIME_MOUNT_CONTEXT.with(|cell| {
*cell.borrow_mut() = context.map(|(id, target, mode)| RuntimeMountContext {
id,
target,
mode,
});
*cell.borrow_mut() =
context.map(|(id, target, mode)| RuntimeMountContext { id, target, mode });
});
}
@@ -622,7 +622,6 @@ pub(crate) fn try_close_block_menu_overlays(
true
}
/// 在 overlay 打开时注册 focus-trap 与 dismiss 层,在关闭时自动清理。
/// 这是 B7/B8 的 radix 化收口:将手写的 Escape / click-outside / tab 循环统一到此函数。
///
@@ -13,7 +13,9 @@ use crate::editor_runtime::bridge_dispatch::HostCommandKind;
use crate::editor_runtime::bridge_events::{
BridgeEnvelope, ChangePayload, HostCommandEnvelope, HostStatusPayload, ReadyPayload,
StatePayload, CHANGE_EVENT, COMMAND_EVENT, EVENT_PREFIX, PROTOCOL, READY_EVENT, RUNTIME_NAME,
RUNTIME_VERSION, SELECTION_EVENT, STATE_EVENT, STATUS_EVENT,
RUNTIME_VERSION, SELECTION_EVENT, STABLE_CHANGE_EVENT, STABLE_COMMAND_EVENT,
STABLE_READY_EVENT, STABLE_SELECTION_EVENT, STABLE_STATE_EVENT,
STABLE_STATUS_EVENT, STATE_EVENT, STATUS_EVENT,
};
use crate::editor_runtime::dom_selection::SelectionPayload;
@@ -100,22 +102,27 @@ where
pub(crate) fn dispatch_ready_event_to_target(target: &EventTarget, payload: &ReadyPayload) {
dispatch_custom_event_to_target(target, READY_EVENT, payload);
dispatch_custom_event_to_target(target, STABLE_READY_EVENT, payload);
}
pub(crate) fn dispatch_change_event_to_target(target: &EventTarget, payload: &ChangePayload) {
dispatch_custom_event_to_target(target, CHANGE_EVENT, payload);
dispatch_custom_event_to_target(target, STABLE_CHANGE_EVENT, payload);
}
pub(crate) fn dispatch_state_event_to_target(target: &EventTarget, payload: &StatePayload) {
dispatch_custom_event_to_target(target, STATE_EVENT, payload);
dispatch_custom_event_to_target(target, STABLE_STATE_EVENT, payload);
}
pub(crate) fn dispatch_status_event_to_target(target: &EventTarget, payload: &HostStatusPayload) {
dispatch_custom_event_to_target(target, STATUS_EVENT, payload);
dispatch_custom_event_to_target(target, STABLE_STATUS_EVENT, payload);
}
pub(crate) fn dispatch_selection_event_to_target(target: &EventTarget, payload: &SelectionPayload) {
dispatch_custom_event_to_target(target, SELECTION_EVENT, payload);
dispatch_custom_event_to_target(target, STABLE_SELECTION_EVENT, payload);
}
pub(crate) fn register_unmount_handle<M: Any + leptos::prelude::Mountable + 'static>(
@@ -1321,6 +1321,34 @@ pub(crate) const SPIKE_STYLE: &str = r#"
pointer-events: auto;
}
.block-drag-aux-panel {
position: absolute;
left: calc(100% + 8px);
top: 112px;
width: 236px;
display: grid;
gap: 8px;
padding: 12px;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 10px;
background: #fff;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.18);
z-index: 132;
color: #37352f;
pointer-events: auto;
}
.block-drag-aux-panel > strong {
font-size: 13px;
line-height: 1.35;
}
.block-drag-aux-panel > span {
color: #6b7280;
font-size: 12px;
line-height: 1.45;
}
.block-drag-menu-item[disabled] {
color: #a1a1aa;
cursor: default;
@@ -2637,11 +2665,28 @@ pub(crate) const SPIKE_STYLE: &str = r#"
border-top: 1px solid rgba(148, 163, 184, 0.26);
}
.block-handle-shell,
.block-drop-indicator {
display: none;
}
.block-handle-shell {
left: 4px;
width: 44px;
min-height: 44px;
padding: 2px;
}
.block-handle-insert {
display: none;
}
.block-handle-trigger {
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
}
.app-shell-embedded .editor-surface {
padding-left: 0;
}
+8 -6
View File
@@ -2338,12 +2338,14 @@ fn App(mount_options: MountOptions) -> impl IntoView {
}
});
});
let preview_mouseup_handle = window_event_listener(ev::mouseup, move |event: MouseEvent| {
if event.button() == 1 || image_preview_pan.try_get_untracked().flatten().is_some() {
event.prevent_default();
let _ = set_image_preview_pan.try_set(None);
}
});
let preview_mouseup_handle =
window_event_listener(ev::mouseup, move |event: MouseEvent| {
if event.button() == 1 || image_preview_pan.try_get_untracked().flatten().is_some()
{
event.prevent_default();
let _ = set_image_preview_pan.try_set(None);
}
});
on_cleanup(move || {
drop(keydown_handle);
drop(preview_mousemove_handle);