use leptos::ev;
use leptos::mount::mount_to_body;
use leptos::prelude::*;
use leptos_dom::helpers::window_event_listener;
use leptos_tiptap::{
    TiptapContent, TiptapEditor, TiptapEditorHandle, TiptapExtension, TiptapLinkResource,
    TiptapMarkName, TiptapSelectionState, TiptapTextAlign,
};
use send_wrapper::SendWrapper;
use serde::Deserialize;
use serde_json::{json, Value};
use std::fmt::Display;
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
use web_sys::{
    window, CustomEvent, DragEvent, Element, Event, EventTarget, HtmlElement, MouseEvent,
    WheelEvent,
};

use editor_runtime::block_dnd::{
    block_state_from_index as block_state_from_index_runtime,
    drop_indicator_from_point as drop_indicator_from_point_runtime,
    drop_indicator_from_target as drop_indicator_from_target_runtime, element_within_handle_shell,
    hovered_block_from_target as hovered_block_from_target_runtime,
};
use editor_runtime::block_hover_state::{
    BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
};
use editor_runtime::block_handle_menu_view::{
    block_handle_menu_view, BlockHandleMenuSignals, BlockHandleMenuViewProps,
};
use editor_runtime::block_menu_overlay;
use editor_runtime::block_transform::{
    apply_block_delta_to_json, apply_highlight_color, apply_image_align, apply_text_align,
    apply_text_color, clear_text_color, insert_editor_paragraph_relative_to_block,
    prosemirror_node_size, run_slash_action, top_level_block_boundary_position,
};
use editor_runtime::bridge_dispatch::HostCommandKind;
use editor_runtime::bridge_events::{
    BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload, HostCommandEnvelope,
    HostCommandPayload, HostStatusPayload, ReadyPayload, StatePayload, BLOCK_DELTA_EVENT,
    COMMAND_EVENT, HEIGHT_EVENT, PROTOCOL, RUNTIME_NAME,
};
use editor_runtime::command_sync::{sync_editor_outputs, sync_persisted_editor_command};
use editor_runtime::content_layout::{content_column_left, content_text_left};
use editor_runtime::dom_events::{
    event_target_matches_selector, target_element, trap_scroll_inside_menu,
};
use editor_runtime::dom_selection::{
    block_index_from_selection, current_block_info_from_index, runtime_block_id_from_index,
    selection_payload, selection_summary,
};
use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus};
use editor_runtime::mindmap_node_view::{mount_mindmap_shell_impl, unmount_mindmap_shell_impl};
use editor_runtime::mount::{
    initial_content, mount_app_into, runtime_delivery_mode, runtime_editor_instance_id,
    runtime_mount_context, set_runtime_mount_context, set_runtime_mount_options, MountOptions,
    RuntimeDeliveryMode, RuntimeStandaloneObject,
};
#[cfg(test)]
use editor_runtime::mount::resolve_runtime_delivery_mode;
use editor_runtime::overlays::{
    clamp_overlay_anchor, close_editor_floating_overlays_if_escape, image_element_from_target,
    image_toolbar_anchor_from_image, open_block_menu_overlay, open_image_toolbar_overlay,
    open_slash_menu_overlay, selection_bounding_rect, should_auto_close_toolbar_overlays,
    sync_overlays_on_selection_change, table_overlay_anchor_from_table, toolbar_overlay_locked,
    try_sync_editor_overlay_state, FloatingToolbarAnchor, ImageToolbarAnchor, TableOverlayAnchor,
    TableSelectionOverlayState,
};
#[cfg(test)]
use editor_runtime::persistence::persisted_document_storage_key;
use editor_runtime::persistence::{
    load_persisted_document, normalize_identity_value, persist_document_state,
    persisted_document_identity, PersistedDocumentIdentity,
};
use editor_runtime::runtime_bridge::{
    dispatch_change_event_to_target, dispatch_ready_event_to_target,
    dispatch_runtime_event, dispatch_selection_event_to_target, dispatch_state_event_to_target,
    dispatch_status_event_to_target, register_runtime_listener, take_unmount_handle,
};
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
use editor_runtime::slash_menu_view::{
    slash_menu_view, SlashMenuRuntimeContext, SlashMenuSignals, SlashMenuViewProps,
};
use editor_runtime::style::SPIKE_STYLE;
use editor_runtime::table_toolbar_view::table_toolbar_view;

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_STAGE_PADDING_LEFT: f64 = 18.0;
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;
mod editor_runtime;

#[wasm_bindgen(inline_js = r#"
export function write_mnote_text_to_clipboard(text) {
  try {
    if (navigator?.clipboard?.writeText) {
      return navigator.clipboard.writeText(String(text)).then(() => true, () => false);
    }
  } catch (_error) {}
  return Promise.resolve(false);
}

export function find_mnote_block_anchor(blockId) {
  const wanted = String(blockId || '');
  if (!wanted) return null;
  const root = document.querySelector('.editor-surface .ProseMirror');
  if (!root) return null;
  return Array.from(root.querySelectorAll('[data-block-id]')).find((node) => node.getAttribute('data-block-id') === wanted) || null;
}

export function select_mnote_image_node(image) {
  try {
    const root = image?.closest?.('.ProseMirror');
    const editor = root?.editor;
    const view = editor?.view;
    if (!editor || !view || typeof view.posAtDOM !== 'function') return false;
    const pos = view.posAtDOM(image, 0);
    if (!Number.isFinite(pos)) return false;
    return editor.chain().focus().setNodeSelection(pos).run() === true;
  } catch (error) {
    console.warn('mnote image NodeSelection failed', error);
    return false;
  }
}

function mnoteImageExtension(url, contentType) {
  const urlMatch = String(url || '').match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
  if (urlMatch?.[1]) return `.${urlMatch[1].toLowerCase()}`;
  const mimeMap = {
    'image/jpeg': '.jpg',
    'image/jpg': '.jpg',
    'image/png': '.png',
    'image/gif': '.gif',
    'image/webp': '.webp',
    'image/svg+xml': '.svg',
    'image/bmp': '.bmp',
  };
  return mimeMap[String(contentType || '').toLowerCase()] || '.jpg';
}

function mnoteImageFilename(image, url) {
  const raw = image.getAttribute('title') || image.getAttribute('alt') || `image-${Date.now()}`;
  const base = raw.replace(/[\/:*?"<>|]+/g, '-').trim() || `image-${Date.now()}`;
  return /\.[a-zA-Z0-9]+$/.test(base) ? base : `${base}${mnoteImageExtension(url)}`;
}

export function download_mnote_selected_image() {
  try {
    const image = document.querySelector('.editor-surface .ProseMirror img.ProseMirror-selectednode[src]');
    if (!(image instanceof HTMLImageElement)) return false;
    const src = image.getAttribute('src');
    if (!src) return false;
    const url = new URL(src, window.location.href);
    if (!(url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'data:')) return false;
    const link = document.createElement('a');
    link.href = url.href;
    link.download = mnoteImageFilename(image, url.href);
    link.style.display = 'none';
    document.body.appendChild(link);
    link.click();
    link.remove();
    return true;
  } catch (error) {
    console.warn('mnote image download failed', error);
    return false;
  }
}

export function bump_mnote_e27_ai_bridge_request_count() {
  try {
    const key = '__MNOTE_E27_AI_BRIDGE_REQUEST_COUNT__';
    window[key] = Number(window[key] || 0) + 1;
    return window[key];
  } catch (_error) {
    return 0;
  }
}

export async function post_mnote_document_save(body) {
  const response = await fetch('/api/documents/save', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: String(body || '{}'),
  });
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`document save failed: HTTP ${response.status}; ${text.slice(0, 1200)}`);
  }
  return text || '{}';
}
"#)]
extern "C" {
    #[wasm_bindgen(catch)]
    async fn write_mnote_text_to_clipboard(text: String) -> Result<JsValue, JsValue>;
    fn find_mnote_block_anchor(block_id: &str) -> Option<Element>;
    fn select_mnote_image_node(image: &Element) -> bool;
    fn download_mnote_selected_image() -> bool;
    fn bump_mnote_e27_ai_bridge_request_count() -> u32;
    #[wasm_bindgen(catch)]
    async fn post_mnote_document_save(body: String) -> Result<JsValue, JsValue>;
}

#[derive(Clone, Copy)]
struct ToolbarColorOption {
    id: &'static str,
    label: &'static str,
    value: &'static str,
}

#[derive(Clone, Copy)]
struct ToolbarAlignOption {
    id: &'static str,
    label: &'static str,
    alignment: TiptapTextAlign,
}

const TOOLBAR_TEXT_COLORS: [ToolbarColorOption; 10] = [
    ToolbarColorOption {
        id: "default",
        label: "默认文字",
        value: "#111827",
    },
    ToolbarColorOption {
        id: "gray",
        label: "灰色",
        value: "#6b7280",
    },
    ToolbarColorOption {
        id: "brown",
        label: "棕色",
        value: "#8b5e3c",
    },
    ToolbarColorOption {
        id: "orange",
        label: "橙色",
        value: "#c96b1f",
    },
    ToolbarColorOption {
        id: "yellow",
        label: "黄色",
        value: "#b7791f",
    },
    ToolbarColorOption {
        id: "green",
        label: "绿色",
        value: "#18794e",
    },
    ToolbarColorOption {
        id: "blue",
        label: "蓝色",
        value: "#1d4ed8",
    },
    ToolbarColorOption {
        id: "purple",
        label: "紫色",
        value: "#7c3aed",
    },
    ToolbarColorOption {
        id: "pink",
        label: "粉色",
        value: "#be185d",
    },
    ToolbarColorOption {
        id: "red",
        label: "红色",
        value: "#b91c1c",
    },
];

const TOOLBAR_HIGHLIGHT_COLORS: [ToolbarColorOption; 8] = [
    ToolbarColorOption {
        id: "yellow",
        label: "黄色背景",
        value: "#fef3c7",
    },
    ToolbarColorOption {
        id: "green",
        label: "绿色背景",
        value: "#dcfce7",
    },
    ToolbarColorOption {
        id: "blue",
        label: "蓝色背景",
        value: "#dbeafe",
    },
    ToolbarColorOption {
        id: "purple",
        label: "紫色背景",
        value: "#ede9fe",
    },
    ToolbarColorOption {
        id: "pink",
        label: "粉色背景",
        value: "#fce7f3",
    },
    ToolbarColorOption {
        id: "red",
        label: "红色背景",
        value: "#fee2e2",
    },
    ToolbarColorOption {
        id: "orange",
        label: "橙色背景",
        value: "#ffedd5",
    },
    ToolbarColorOption {
        id: "gray",
        label: "灰色背景",
        value: "#e5e7eb",
    },
];

const TOOLBAR_ALIGN_OPTIONS: [ToolbarAlignOption; 4] = [
    ToolbarAlignOption {
        id: "left",
        label: "左对齐",
        alignment: TiptapTextAlign::Left,
    },
    ToolbarAlignOption {
        id: "center",
        label: "居中",
        alignment: TiptapTextAlign::Center,
    },
    ToolbarAlignOption {
        id: "right",
        label: "右对齐",
        alignment: TiptapTextAlign::Right,
    },
    ToolbarAlignOption {
        id: "justify",
        label: "两端对齐",
        alignment: TiptapTextAlign::Justify,
    },
];

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct LegacyHostEnvelope {
    protocol: Option<String>,
    runtime: Option<String>,
    version: Option<String>,
    source: Option<String>,
    event: Option<String>,
    payload: Option<HostDocumentPayload>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HostDocumentPayload {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: Option<String>,
    content: Option<Value>,
    revision: Option<i64>,
    conflict_detection_key: Option<String>,
    read_only: Option<bool>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn resolves_embedded_mode_from_explicit_runtime_mode_first() {
        assert_eq!(
            resolve_runtime_delivery_mode(Some(RuntimeDeliveryMode::Embedded), false),
            RuntimeDeliveryMode::Embedded
        );
        assert_eq!(
            resolve_runtime_delivery_mode(Some(RuntimeDeliveryMode::Standalone), true),
            RuntimeDeliveryMode::Standalone
        );
    }

    #[test]
    fn resolves_embedded_mode_from_url_when_runtime_mode_absent() {
        assert_eq!(
            resolve_runtime_delivery_mode(None, true),
            RuntimeDeliveryMode::Embedded
        );
        assert_eq!(
            resolve_runtime_delivery_mode(None, false),
            RuntimeDeliveryMode::Standalone
        );
    }

    #[test]
    fn runtime_editor_instance_id_uses_mount_id_when_available() {
        assert_eq!(
            runtime_editor_instance_id(Some(7)),
            "mnote-leptos-tiptap-spike-7"
        );
    }

    #[test]
    fn runtime_editor_instance_id_falls_back_for_standalone_mode() {
        assert_eq!(
            runtime_editor_instance_id(None),
            "mnote-leptos-tiptap-spike-standalone"
        );
    }

    #[test]
    fn persisted_document_storage_key_isolated_per_document_identity() {
        let doc_a =
            persisted_document_identity(Some("doc-a".to_string()), Some("ws-1".to_string()));
        let doc_b =
            persisted_document_identity(Some("doc-b".to_string()), Some("ws-1".to_string()));
        let doc_without_workspace = persisted_document_identity(Some("doc-a".to_string()), None);

        assert_eq!(
            persisted_document_storage_key(&doc_a),
            "mnote.leptos-tiptap-spike.document:ws-1:doc-a"
        );
        assert_eq!(
            persisted_document_storage_key(&doc_b),
            "mnote.leptos-tiptap-spike.document:ws-1:doc-b"
        );
        assert_eq!(
            persisted_document_storage_key(&doc_without_workspace),
            "mnote.leptos-tiptap-spike.document:doc-a"
        );
        assert_ne!(
            persisted_document_storage_key(&doc_a),
            persisted_document_storage_key(&doc_b)
        );
    }

    #[test]
    fn standalone_mindmap_object_uses_isolated_draft_identity() {
        let object = RuntimeStandaloneObject {
            kind: Some("mindmap".to_string()),
            document_id: Some("doc-a".to_string()),
            mindmap_id: Some("mind-a".to_string()),
        };
        let identity = persisted_mindmap_object_identity(Some(&object)).expect("identity");

        assert_eq!(
            persisted_document_storage_key(&identity),
            "mnote.leptos-tiptap-spike.document:mindmap-object:doc-a:mind-a"
        );
    }

    #[test]
    fn mindmap_empty_paragraph_has_textblock_boundary_size() {
        let node = json!({
            "type": "paragraph",
            "attrs": {
                "mnoteBlockType": "mindmap",
                "mindmapId": "思维导图123456.json",
                "rootNodeId": "root",
                "projectionVersion": 1
            }
        });
        assert_eq!(prosemirror_node_size(&node), Some(2));

        let document = json!({
            "type": "doc",
            "content": [
                {"type": "paragraph", "content": [{"type": "text", "text": "上"}]},
                node,
                {"type": "paragraph", "content": [{"type": "text", "text": "下"}]}
            ]
        });

        assert_eq!(
            top_level_block_boundary_position(&document, 1, true),
            Some(3)
        );
        assert_eq!(
            top_level_block_boundary_position(&document, 1, false),
            Some(5)
        );
    }

    #[test]
    fn slash_menu_css_uses_viewport_overlay_layer() {
        assert!(SPIKE_STYLE.contains(".slash-menu"));
        assert!(SPIKE_STYLE.contains("position: fixed;"));
        assert!(SPIKE_STYLE.contains("z-index: 130;"));
        assert!(!SPIKE_STYLE.contains("z-index: 11;"));
    }
}

fn editor_block_by_id(block_id: &str) -> Option<Element> {
    find_mnote_block_anchor(block_id)
}

fn current_page_anchor_url(block_id: &str) -> Option<String> {
    let win = window()?;
    let location = win.location();
    let origin = location.origin().ok()?;
    let pathname = location.pathname().ok()?;
    let search = location.search().ok().unwrap_or_default();
    Some(format!("{}{}{}#{}", origin, pathname, search, block_id))
}

fn scroll_mnote_block_anchor(block_id: &str) -> bool {
    let Some(block) = editor_block_by_id(block_id) else {
        return false;
    };
    let _ = block.scroll_into_view_with_bool(true);
    true
}

fn current_location_hash_block_id() -> Option<String> {
    window()
        .and_then(|win| win.location().hash().ok())
        .map(|hash| hash.trim_start_matches('#').trim().to_string())
        .filter(|hash| !hash.is_empty())
}

fn scroll_mnote_block_anchor_from_hash() -> bool {
    current_location_hash_block_id()
        .as_deref()
        .map(scroll_mnote_block_anchor)
        .unwrap_or(false)
}

fn schedule_scroll_mnote_block_anchor_from_hash() {
    schedule_scroll_mnote_block_anchor_from_hash_retry(60);
}

fn schedule_scroll_mnote_block_anchor_from_hash_retry(remaining: u8) {
    let Some(win) = window() else {
        return;
    };
    let callback = Closure::<dyn FnMut()>::new(move || {
        let _ = scroll_mnote_block_anchor_from_hash();
        if remaining > 1 {
            schedule_scroll_mnote_block_anchor_from_hash_retry(remaining - 1);
        }
    });
    let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(
        callback.as_ref().unchecked_ref(),
        120,
    );
    callback.forget();
}

fn current_viewport_scroll() -> Option<(f64, f64)> {
    let win = window()?;
    let x = win.scroll_x().ok()?;
    let y = win.scroll_y().ok()?;
    Some((x, y))
}

fn restore_viewport_scroll(x: f64, y: f64) {
    if let Some(win) = window() {
        win.scroll_to_with_x_and_y(x, y);
    }
}

fn schedule_restore_viewport_scroll(x: f64, y: f64, remaining: u8) {
    restore_viewport_scroll(x, y);
    let Some(win) = window() else {
        return;
    };
    if remaining == 0 {
        return;
    }
    let callback = Closure::<dyn FnMut()>::new(move || {
        restore_viewport_scroll(x, y);
        if remaining > 1 {
            schedule_restore_viewport_scroll(x, y, remaining - 1);
        }
    });
    let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(
        callback.as_ref().unchecked_ref(),
        80,
    );
    callback.forget();
}

#[wasm_bindgen]
pub fn mount_mindmap_shell(container: Element, options: JsValue) -> Result<u32, JsValue> {
    console_error_panic_hook::set_once();
    mount_mindmap_shell_impl(container.into(), options)
}

#[wasm_bindgen]
pub fn unmount_mindmap_shell(mount_id: u32) -> Result<(), JsValue> {
    unmount_mindmap_shell_impl(mount_id)
}

#[wasm_bindgen]
pub fn mount(container: Element, options: JsValue) -> Result<u32, JsValue> {
    console_error_panic_hook::set_once();
    let options = if options.is_undefined() || options.is_null() {
        MountOptions::default()
    } else {
        serde_wasm_bindgen::from_value(options)?
    };
    let target = container
        .dyn_into::<HtmlElement>()
        .map_err(|_| JsValue::from_str("mount 目标必须是 HTML 元素"))?;
    let mount_id = mount_app_into(target, options, RuntimeDeliveryMode::Embedded);
    Ok(mount_id)
}

#[wasm_bindgen]
pub fn unmount(mount_id: u32) -> Result<(), JsValue> {
    if let Some(handle) = take_unmount_handle(mount_id) {
        if runtime_mount_context().map(|(id, _, _)| id) == Some(mount_id) {
            set_runtime_mount_context(None);
            set_runtime_mount_options(None, RuntimeDeliveryMode::Embedded);
        }
        drop(handle);
        Ok(())
    } else {
        Err(JsValue::from_str("找不到对应的挂载句柄"))
    }
}

fn default_title() -> String {
    "Leptos Tiptap 主编辑器 P0".to_string()
}

fn current_query_param(key: &str) -> Option<String> {
    window()
        .and_then(|win| win.location().search().ok())
        .and_then(|search| {
            let query = search.trim_start_matches('?');
            query.split('&').find_map(|pair| {
                let (candidate_key, candidate_value) = pair.split_once('=')?;
                if candidate_key == key && !candidate_value.trim().is_empty() {
                    Some(candidate_value.trim().to_string())
                } else {
                    None
                }
            })
        })
}

fn resolve_persisted_document_identity(
    explicit_document_id: Option<String>,
    explicit_workspace_id: Option<String>,
) -> PersistedDocumentIdentity {
    persisted_document_identity(
        explicit_document_id.or_else(|| current_query_param("documentId")),
        explicit_workspace_id.or_else(|| current_query_param("workspaceId")),
    )
}

fn persisted_mindmap_object_identity(
    object: Option<&RuntimeStandaloneObject>,
) -> Option<PersistedDocumentIdentity> {
    let object = object?;
    if object.kind.as_deref() != Some("mindmap") {
        return None;
    }
    let document_id = normalize_identity_value(object.document_id.clone())?;
    let mindmap_id = normalize_identity_value(object.mindmap_id.clone())?;
    Some(persisted_document_identity(
        Some(format!("mindmap-object:{document_id}:{mindmap_id}")),
        None,
    ))
}

fn runtime_persisted_identity(
    document_id: ReadSignal<Option<String>>,
    workspace_id: ReadSignal<Option<String>>,
) -> PersistedDocumentIdentity {
    persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked())
}

fn is_embedded_mode() -> bool {
    runtime_delivery_mode() == RuntimeDeliveryMode::Embedded
}

fn current_document_id() -> Option<String> {
    current_query_param("documentId")
}

fn current_workspace_id() -> Option<String> {
    current_query_param("workspaceId")
}

pub(crate) fn editor_root_element() -> Option<Element> {
    window()
        .and_then(|win| win.document())
        .and_then(|document| document.query_selector(EDITOR_ROOT_SELECTOR).ok().flatten())
}

pub(crate) fn editor_stage_element() -> Option<Element> {
    window()
        .and_then(|win| win.document())
        .and_then(|document| {
            document
                .query_selector(EDITOR_STAGE_SELECTOR)
                .ok()
                .flatten()
        })
}

fn handle_lane_left(stage_rect_width: f64) -> f64 {
    (content_text_left(stage_rect_width) + HANDLE_TEXT_ALIGN_OFFSET
        - HANDLE_TRIGGER_WIDTH
        - HANDLE_TRIGGER_GAP
        - HANDLE_MENU_GAP)
        .max(HANDLE_STAGE_PADDING_LEFT)
}

fn hovered_block_from_target(target: web_sys::EventTarget) -> Option<HoveredBlockState> {
    let root = editor_root_element()?;
    hovered_block_from_target_runtime(target, &root, HANDLE_SHELL_SELECTOR)
}

fn hovered_block_from_selection() -> Option<HoveredBlockState> {
    block_index_from_selection().and_then(block_state_from_index)
}

fn block_state_from_index(index: usize) -> Option<HoveredBlockState> {
    block_state_from_index_runtime(index)
}

fn slash_menu_anchor_style() -> String {
    const MENU_WIDTH: f64 = 316.0;
    const MENU_HEIGHT: f64 = 430.0;
    const GAP: f64 = 8.0;

    let selection_rect = selection_bounding_rect()
        .filter(|rect| rect.top() > 0.0 || rect.left() > 0.0 || rect.height() > 0.0);

    let (raw_top, raw_left, raw_anchor_top) = if let Some(rect) = selection_rect {
        (rect.bottom() + GAP, rect.left(), rect.top())
    } else if let Some(block) = hovered_block_from_selection() {
        if let Some(stage) = editor_stage_element() {
            let stage_rect = stage.get_bounding_client_rect();
            (
                stage_rect.top() + block.top + block.height + GAP,
                stage_rect.left() + content_text_left(stage_rect.width()),
                stage_rect.top() + block.top,
            )
        } else {
            (24.0, 24.0, 24.0)
        }
    } else {
        (24.0, 24.0, 24.0)
    };

    let (clamped_top, clamped_left) = clamp_overlay_anchor(
        raw_top,
        raw_left,
        raw_anchor_top,
        MENU_WIDTH,
        MENU_HEIGHT,
        GAP,
    );

    format!("top:{clamped_top:.1}px;left:{clamped_left:.1}px;")
}

fn drop_indicator_from_target(
    target: web_sys::EventTarget,
    client_y: i32,
) -> Option<DropIndicatorState> {
    drop_indicator_from_target_runtime(target, client_y, HANDLE_SHELL_SELECTOR)
}

fn drop_indicator_from_point(client_x: i32, client_y: i32) -> Option<DropIndicatorState> {
    drop_indicator_from_point_runtime(client_x, client_y, HANDLE_SHELL_SELECTOR)
}

fn p0_extensions() -> Vec<TiptapExtension> {
    vec![
        TiptapExtension::Document,
        TiptapExtension::Dropcursor,
        TiptapExtension::Gapcursor,
        TiptapExtension::Text,
        TiptapExtension::Paragraph,
        TiptapExtension::Heading,
        TiptapExtension::Bold,
        TiptapExtension::Italic,
        TiptapExtension::Strike,
        TiptapExtension::Code,
        TiptapExtension::Blockquote,
        TiptapExtension::BulletList,
        TiptapExtension::OrderedList,
        TiptapExtension::ListItem,
        TiptapExtension::TaskItem,
        TiptapExtension::TaskList,
        TiptapExtension::CodeBlock,
        TiptapExtension::HorizontalRule,
        TiptapExtension::Image,
        TiptapExtension::Table,
        TiptapExtension::TableRow,
        TiptapExtension::TableCell,
        TiptapExtension::TableHeader,
        TiptapExtension::TocNode,
        TiptapExtension::History,
        TiptapExtension::Underline,
        TiptapExtension::TextStyle,
        TiptapExtension::TextAlign,
        TiptapExtension::Highlight,
        TiptapExtension::Link,
        TiptapExtension::Placeholder,
    ]
}

fn state_payload(
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    dirty_count: u32,
    hovered_block: Option<HoveredBlockState>,
    editor_focused: bool,
    slash_open: bool,
    turn_into_open: bool,
    color_menu_open: bool,
    more_menu_open: bool,
    read_only: bool,
) -> StatePayload {
    let toolbar_open = toolbar_overlay_locked(turn_into_open, color_menu_open, more_menu_open);
    StatePayload {
        document_id,
        workspace_id,
        title,
        dirty_count,
        selected_block_index: hovered_block.as_ref().map(|block| block.index),
        editor_focused,
        slash_open,
        toolbar_open,
        read_only,
    }
}

fn status_payload(
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    dirty_count: u32,
    hovered_block: Option<HoveredBlockState>,
    editor_focused: bool,
    slash_open: bool,
    turn_into_open: bool,
    color_menu_open: bool,
    more_menu_open: bool,
    read_only: bool,
) -> HostStatusPayload {
    let current_block_index = hovered_block.as_ref().map(|block| block.index);
    let current_block_id = current_block_index.and_then(runtime_block_id_from_index);
    HostStatusPayload {
        document_id,
        workspace_id,
        title,
        dirty_count,
        selected_block_index: current_block_index,
        current_block_id,
        editor_focused,
        read_only,
        slash_open,
        toolbar_open: toolbar_overlay_locked(turn_into_open, color_menu_open, more_menu_open),
    }
}

fn dispatch_runtime_state_to_target(
    target: &EventTarget,
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    dirty_count: u32,
    hovered_block: Option<HoveredBlockState>,
    editor_focused: bool,
    slash_open: bool,
    turn_into_open: bool,
    color_menu_open: bool,
    more_menu_open: bool,
    read_only: bool,
) {
    let state = state_payload(
        document_id.clone(),
        workspace_id.clone(),
        title.clone(),
        dirty_count,
        hovered_block.clone(),
        editor_focused,
        slash_open,
        turn_into_open,
        color_menu_open,
        more_menu_open,
        read_only,
    );
    dispatch_state_event_to_target(target, &state);
    dispatch_status_event_to_target(
        target,
        &status_payload(
            document_id,
            workspace_id,
            title,
            dirty_count,
            hovered_block,
            editor_focused,
            slash_open,
            turn_into_open,
            color_menu_open,
            more_menu_open,
            read_only,
        ),
    );
}

fn send_selection_state_to_target(
    target: &EventTarget,
    selection: &TiptapSelectionState,
    editor_focused: bool,
    current_block_index: Option<usize>,
) {
    dispatch_selection_event_to_target(
        target,
        &selection_payload(selection, editor_focused, current_block_index),
    );
}

fn apply_host_document_payload(
    editor: TiptapEditorHandle,
    payload: HostDocumentPayload,
    set_document_id: WriteSignal<Option<String>>,
    set_workspace_id: WriteSignal<Option<String>>,
    set_title: WriteSignal<String>,
    set_read_only: WriteSignal<bool>,
    set_revision: WriteSignal<Option<i64>>,
    set_conflict_detection_key: WriteSignal<Option<String>>,
    set_dirty_count: WriteSignal<u32>,
    set_html_output: WriteSignal<String>,
    set_document_json: WriteSignal<Value>,
    set_json_output: WriteSignal<String>,
    set_command_feedback: WriteSignal<String>,
) {
    let next_document_id = payload.document_id.clone();
    let next_workspace_id = payload.workspace_id.clone();
    let next_title = payload.title.clone();
    let next_revision = payload.revision;
    let next_conflict_detection_key = payload.conflict_detection_key.clone();
    let next_read_only = payload.read_only.unwrap_or(false);

    set_document_id.set(next_document_id);
    set_workspace_id.set(next_workspace_id);
    if let Some(next_title_value) = next_title {
        set_title.set(next_title_value);
    }
    set_read_only.set(next_read_only);
    set_revision.set(next_revision);
    set_conflict_detection_key.set(next_conflict_detection_key.clone());

    if let Some(content) = payload.content {
        let viewport_scroll = current_viewport_scroll();
        let next_content = TiptapContent::json(content);
        match editor.set_content(next_content) {
            Ok(()) => {
                let _snapshot = sync_editor_outputs(
                    editor,
                    set_html_output,
                    set_document_json,
                    set_json_output,
                );
                set_dirty_count.set(0);
                set_command_feedback.set("宿主文档已同步到编辑器".to_string());
                if let Some((x, y)) = viewport_scroll {
                    schedule_restore_viewport_scroll(x, y, 10);
                }
            }
            Err(err) => {
                set_command_feedback.set(format!("宿主文档同步失败：{err}"));
            }
        }
    }
}

fn apply_feedback_result<E>(setter: WriteSignal<String>, result: Result<&'static str, E>)
where
    E: Display,
{
    match result {
        Ok(message) => setter.set(message.to_string()),
        Err(err) => setter.set(format!("命令执行失败：{err}")),
    }
}

fn apply_ok_feedback<E>(
    setter: WriteSignal<String>,
    result: Result<(), E>,
    ok_message: &'static str,
) where
    E: Display,
{
    apply_feedback_result(setter, result.map(|_| ok_message));
}

fn open_hermes_page_ai_drawer() {
    let Some(win) = window() else {
        return;
    };
    let Some(document) = win.document() else {
        return;
    };
    if let Ok(Some(trigger)) = document.query_selector("[data-mnote-action=\"open-page-ai\"]") {
        if let Some(button) = trigger.dyn_ref::<HtmlElement>() {
            button.click();
        }
    }
}

fn request_ai_edit_bridge(
    editor: TiptapEditorHandle,
    action: &'static str,
    document_id: ReadSignal<Option<String>>,
    workspace_id: ReadSignal<Option<String>>,
    selected_block_index: Option<usize>,
    selected_block_id: Option<String>,
    selection_state: ReadSignal<TiptapSelectionState>,
    set_ai_bridge_state: WriteSignal<String>,
    set_ai_bridge_message: WriteSignal<String>,
    set_dirty_count: WriteSignal<u32>,
    set_html_output: WriteSignal<String>,
    set_document_json: WriteSignal<Value>,
    set_json_output: WriteSignal<String>,
    title: ReadSignal<String>,
    set_command_feedback: WriteSignal<String>,
) {
    let _ = (
        editor,
        action,
        document_id,
        workspace_id,
        selected_block_index,
        selected_block_id,
        selection_state,
        set_dirty_count,
        set_html_output,
        set_document_json,
        set_json_output,
        title,
    );
    let _ = (
        set_ai_bridge_state,
        set_ai_bridge_message,
        set_command_feedback,
    );
    open_hermes_page_ai_drawer();
}

fn normalize_layout_density(value: Option<String>) -> String {
    match value.as_deref() {
        Some("compact") => "compact".to_string(),
        Some("spacious") => "spacious".to_string(),
        _ => "normal".to_string(),
    }
}

#[component]
fn App(mount_options: MountOptions) -> impl IntoView {
    let editor = TiptapEditorHandle::new();
    let current_mount_id = runtime_mount_context().map(|(id, _, _)| id);
    let editor_instance_id = runtime_editor_instance_id(current_mount_id);
    let editor_stage_id = format!("{editor_instance_id}-stage");
    let runtime_target = runtime_mount_context()
        .map(|(_, target, _)| target)
        .or_else(|| {
            window()
                .and_then(|win| win.document())
                .map(|document| document.into())
        });
    let initial_document_id = mount_options
        .document_id
        .clone()
        .or_else(current_document_id);
    let initial_workspace_id = mount_options
        .workspace_id
        .clone()
        .or_else(current_workspace_id);
    let persisted_identity = persisted_mindmap_object_identity(
        mount_options.standalone_object.as_ref(),
    )
    .unwrap_or_else(|| {
        resolve_persisted_document_identity(
            initial_document_id.clone(),
            initial_workspace_id.clone(),
        )
    });
    let persisted = load_persisted_document(&persisted_identity);
    let has_explicit_bootstrap_content =
        mount_options.html.is_some() || mount_options.content.is_some();
    let restored_from_storage = persisted.is_some() && !has_explicit_bootstrap_content;
    let restored_html = if restored_from_storage {
        persisted
            .as_ref()
            .and_then(|document| document.html.clone())
    } else {
        None
    };
    let initial_title_value = mount_options
        .title
        .clone()
        .or_else(|| {
            restored_from_storage
                .then(|| persisted.as_ref().map(|document| document.title.clone()))
                .flatten()
        })
        .unwrap_or_else(default_title);
    let initial_editor_content = mount_options
        .html
        .clone()
        .map(TiptapContent::html)
        .or_else(|| mount_options.content.clone().map(TiptapContent::json))
        .or_else(|| {
            if restored_from_storage {
                persisted.as_ref().map(|document| {
                    document
                        .html
                        .clone()
                        .map(TiptapContent::html)
                        .unwrap_or_else(|| TiptapContent::json(document.content.clone()))
                })
            } else {
                None
            }
        })
        .unwrap_or_else(initial_content);

    let (document_id, set_document_id) = signal(initial_document_id);
    let (workspace_id, set_workspace_id) = signal(initial_workspace_id);
    let (title, set_title) = signal(initial_title_value);
    let (read_only, set_read_only) = signal(mount_options.read_only.unwrap_or(false));
    let (revision, set_revision) = signal(mount_options.revision);
    let (conflict_detection_key, set_conflict_detection_key) =
        signal(mount_options.conflict_detection_key.clone());
    let (html_output, set_html_output) = signal(String::new());
    let (document_json, set_document_json) =
        signal(mount_options.content.clone().unwrap_or_else(|| {
            if restored_from_storage {
                persisted
                    .as_ref()
                    .map(|document| document.content.clone())
                    .unwrap_or_else(|| json!({"type": "doc", "content": []}))
            } else {
                json!({"type": "doc", "content": []})
            }
        }));
    let (json_output, set_json_output) = signal(String::new());
    let (selection_state, set_selection_state) = signal(TiptapSelectionState::default());
    let (dirty_count, set_dirty_count) = signal(0_u32);
    let (editor_focused, set_editor_focused) = signal(false);
    let (text_selection_active, set_text_selection_active) = signal(false);
    let (table_toolbar_open, set_table_toolbar_open) = signal(false);
    let (table_options_open, set_table_options_open) = signal(false);
    let (table_overlay_anchor, set_table_overlay_anchor) = signal(None::<TableOverlayAnchor>);
    let (table_selection_overlay, set_table_selection_overlay) =
        signal(None::<TableSelectionOverlayState>);
    let (floating_toolbar_anchor, set_floating_toolbar_anchor) =
        signal(None::<FloatingToolbarAnchor>);
    let (image_toolbar_anchor, set_image_toolbar_anchor) = signal(None::<ImageToolbarAnchor>);
    let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::<FloatingToolbarAnchor>);
    let (slash_open, set_slash_open) = signal(false);
    let (slash_index, set_slash_index) = signal(0_usize);
    let (slash_query, set_slash_query) = signal(String::new());
    let (turn_into_open, set_turn_into_open) = signal(false);
    let (color_menu_open, set_color_menu_open) = signal(false);
    let (more_menu_open, set_more_menu_open) = signal(false);
    let (hovered_block, set_hovered_block) = signal(None::<HoveredBlockState>);
    let (block_keyboard_mode, set_block_keyboard_mode) = signal(false);
    let (block_menu_anchor, set_block_menu_anchor) = signal(None::<HoveredBlockState>);
    let (block_menu_open, set_block_menu_open) = signal(false);
    let (block_turn_into_open, set_block_turn_into_open) = signal(false);
    let (block_folded_title_open, set_block_folded_title_open) = signal(false);
    let (_ai_bridge_state, set_ai_bridge_state) = signal("idle".to_string());
    let (_ai_bridge_message, set_ai_bridge_message) = signal("".to_string());
    let (pending_drag, set_pending_drag) = signal(None::<PendingDragState>);
    let (dragging_block_index, set_dragging_block_index) = signal(None::<usize>);
    let (dragging_block_anchor, set_dragging_block_anchor) = signal(None::<HoveredBlockState>);
    let (drop_indicator, set_drop_indicator) = signal(None::<DropIndicatorState>);
    let (suppress_handle_click, set_suppress_handle_click) = signal(false);
    let (command_feedback, set_command_feedback) = signal("等待第一次编辑".to_string());
    let initial_editable = mount_options.editable.unwrap_or(!read_only.get_untracked());
    let (editor_editable, set_editor_editable) = signal(initial_editable);
    let initial_page_options = mount_options.page_options.clone();
    let (wide_layout, set_wide_layout) = signal(
        initial_page_options
            .as_ref()
            .and_then(|opts| opts.wide_layout)
            .unwrap_or(false),
    );
    let (small_text, set_small_text) = signal(
        initial_page_options
            .as_ref()
            .and_then(|opts| opts.small_text)
            .unwrap_or(false),
    );
    let (layout_density, set_layout_density) = signal(normalize_layout_density(
        initial_page_options
            .as_ref()
            .and_then(|opts| opts.layout_density.clone()),
    ));
    let (show_heading_numbers, set_show_heading_numbers) = signal(
        initial_page_options
            .as_ref()
            .and_then(|opts| opts.show_heading_numbers)
            .unwrap_or(true),
    );
    let (embed_default_block_id, set_embed_default_block_id) = signal(
        initial_page_options
            .as_ref()
            .and_then(|opts| opts.embed_default_block_id.clone()),
    );
    let command_event_target = runtime_target.clone();
    let ready_event_target = runtime_target.clone();
    let change_event_target = runtime_target.clone();
    let selection_event_target = runtime_target.clone();
    let slash_change_event_target = runtime_target.clone();
    let height_event_target = runtime_target.clone();

    {
        let block_menu_open = block_menu_open;
        Effect::new(move |_| {
            let Some(document) = window().and_then(|win| win.document()) else {
                return;
            };
            let Some(body) = document.body() else {
                return;
            };

            if block_menu_open.get() {
                let _ = body.style().set_property("overflow", "hidden");
            } else {
                let _ = body.style().remove_property("overflow");
            }
        });
    }

    {
        Effect::new(move |_| {
            let Some(stage) = editor_stage_element() else {
                return;
            };
            let height = stage.get_bounding_client_rect().height();
            if height > 0.0 {
                dispatch_runtime_event(
                    height_event_target.clone(),
                    HEIGHT_EVENT,
                    &HeightPayload { height },
                );
            }
        });
    }

    {
        let block_menu_open = block_menu_open;
        let set_block_turn_into_open = set_block_turn_into_open;
        let set_block_folded_title_open = set_block_folded_title_open;
        Effect::new(move |_| {
            if !block_menu_open.get() {
                set_block_turn_into_open.set(false);
                set_block_folded_title_open.set(false);
            }
        });
    }

    Effect::new({
        let editor = editor;
        let set_document_id = set_document_id;
        let set_workspace_id = set_workspace_id;
        let set_title = set_title;
        let set_read_only = set_read_only;
        let set_revision = set_revision;
        let set_conflict_detection_key = set_conflict_detection_key;
        let set_dirty_count = set_dirty_count;
        let set_html_output = set_html_output;
        let set_document_json = set_document_json;
        let set_json_output = set_json_output;
        let set_command_feedback = set_command_feedback;
        let set_editor_editable = set_editor_editable;
        let set_wide_layout = set_wide_layout;
        let set_small_text = set_small_text;
        let set_layout_density = set_layout_density;
        let set_show_heading_numbers = set_show_heading_numbers;
        let set_embed_default_block_id = set_embed_default_block_id;
        move |_| {
            let command_event_target = command_event_target.clone();
            let command_listener =
                Closure::<dyn FnMut(Event)>::wrap(Box::new(move |event: Event| {
                    let Some(custom_event) = event.dyn_ref::<CustomEvent>() else {
                        return;
                    };
                    let detail = custom_event.detail();
                    let Ok(envelope) =
                        serde_wasm_bindgen::from_value::<HostCommandEnvelope>(detail)
                    else {
                        return;
                    };
                    if envelope.protocol.as_deref() != Some(PROTOCOL) {
                        return;
                    }

                    let Some(command_kind) = HostCommandKind::from_payload(
                        envelope.payload.as_ref().unwrap_or(&HostCommandPayload {
                            command: None,
                            document_id: None,
                            workspace_id: None,
                            title: None,
                            content: None,
                            editable: None,
                            page_options: None,
                            block_id: None,
                            block_index: None,
                            text: None,
                            reference_document_id: None,
                            reference_block_id: None,
                            current_block_id: None,
                            selection: None,
                            revision: None,
                            conflict_detection_key: None,
                            read_only: None,
                        }),
                    ) else {
                        return;
                    };

                    let Some(payload) = envelope.payload else {
                        return;
                    };

                    match command_kind {
                        HostCommandKind::Undo => {
                            let _ = editor.undo();
                        }
                        HostCommandKind::Redo => {
                            let _ = editor.redo();
                        }
                        HostCommandKind::ReplaceContent | HostCommandKind::Bootstrap => {
                            apply_host_document_payload(
                                editor,
                                HostDocumentPayload {
                                    document_id: payload.document_id,
                                    workspace_id: payload.workspace_id,
                                    title: payload.title,
                                    content: payload.content,
                                    revision: payload.revision,
                                    conflict_detection_key: payload.conflict_detection_key,
                                    read_only: payload
                                        .read_only
                                        .or(payload.editable.map(|editable| !editable)),
                                },
                                set_document_id,
                                set_workspace_id,
                                set_title,
                                set_read_only,
                                set_revision,
                                set_conflict_detection_key,
                                set_dirty_count,
                                set_html_output,
                                set_document_json,
                                set_json_output,
                                set_command_feedback,
                            );
                        }
                        HostCommandKind::SetEditable => {
                            let editable = payload.editable.unwrap_or(true);
                            set_editor_editable.set(editable);
                            set_read_only.set(!editable);
                            if let Some(target) = command_event_target.as_ref() {
                                dispatch_runtime_state_to_target(
                                    target,
                                    document_id.get_untracked(),
                                    workspace_id.get_untracked(),
                                    title.get_untracked(),
                                    dirty_count.get_untracked(),
                                    hovered_block.get_untracked(),
                                    editor_focused.get_untracked(),
                                    slash_open.get_untracked(),
                                    turn_into_open.get_untracked(),
                                    color_menu_open.get_untracked(),
                                    more_menu_open.get_untracked(),
                                    read_only.get_untracked(),
                                );
                            }
                        }
                        HostCommandKind::SetPageOptions => {
                            if let Some(page_options) = payload.page_options {
                                if let Some(value) = page_options.wide_layout {
                                    set_wide_layout.set(value);
                                }
                                if let Some(value) = page_options.small_text {
                                    set_small_text.set(value);
                                }
                                set_layout_density
                                    .set(normalize_layout_density(page_options.layout_density));
                                if let Some(value) = page_options.show_heading_numbers {
                                    set_show_heading_numbers.set(value);
                                }
                                set_embed_default_block_id.set(page_options.embed_default_block_id);
                                set_command_feedback.set("页面布局选项已同步".to_string());
                            }
                        }
                        HostCommandKind::Focus => {
                            let _ = editor.focus();
                        }
                        HostCommandKind::RequestCurrentBlockId => {
                            let current_block = current_block_info_from_index(
                                hovered_block.get_untracked().map(|block| block.index),
                            );
                            if let Some(target) = command_event_target.as_ref() {
                                dispatch_status_event_to_target(
                                    target,
                                    &HostStatusPayload {
                                        document_id: document_id.get_untracked(),
                                        workspace_id: workspace_id.get_untracked(),
                                        title: title.get_untracked(),
                                        dirty_count: dirty_count.get_untracked(),
                                        selected_block_index: current_block.index,
                                        current_block_id: current_block.block_id,
                                        editor_focused: editor_focused.get_untracked(),
                                        read_only: read_only.get_untracked(),
                                        slash_open: slash_open.get_untracked(),
                                        toolbar_open: toolbar_overlay_locked(
                                            turn_into_open.get_untracked(),
                                            color_menu_open.get_untracked(),
                                            more_menu_open.get_untracked(),
                                        ),
                                    },
                                );
                            }
                        }
                        HostCommandKind::InsertInlineReference
                        | HostCommandKind::InsertEmbedReference => {
                            let reference_text = match (
                                payload.reference_document_id.or(payload.document_id),
                                payload.reference_block_id.or(payload.block_id),
                            ) {
                                (Some(document_id), Some(block_id)) => {
                                    format!("[[引用 {}:{}]]", document_id, block_id)
                                }
                                (Some(document_id), None) => format!("[[引用 {}]]", document_id),
                                _ => "[[引用]]".to_string(),
                            };
                            let node =
                                if matches!(command_kind, HostCommandKind::InsertEmbedReference) {
                                    json!({
                                        "type": "blockquote",
                                        "content": [
                                            {
                                                "type": "paragraph",
                                                "content": [
                                                    { "type": "text", "text": reference_text }
                                                ]
                                            }
                                        ]
                                    })
                                } else {
                                    json!({
                                        "type": "paragraph",
                                        "content": [
                                            { "type": "text", "text": reference_text }
                                        ]
                                    })
                                };
                            let _ = editor.insert_content(TiptapContent::json(node), None);
                        }
                    }
                }));

            if let Some(document) = window().and_then(|win| win.document()) {
                let listener_ref = command_listener.as_ref().unchecked_ref();
                let _ = document.add_event_listener_with_callback(COMMAND_EVENT, listener_ref);
                if let Some((mount_id, _, _)) = runtime_mount_context() {
                    let target: EventTarget = document.into();
                    register_runtime_listener(mount_id, target, command_listener);
                }
            }

            // ── Block-delta listener（Phase B：AI 写入增量通道） ──
            let delta_editor = editor;
            let delta_set_document_json = set_document_json;
            let delta_set_json_output = set_json_output;
            let delta_set_dirty_count = set_dirty_count;
            let delta_set_html_output = set_html_output;
            let delta_listener =
                Closure::<dyn FnMut(Event)>::wrap(Box::new(move |event: Event| {
                    let Some(custom_event) = event.dyn_ref::<CustomEvent>() else {
                        return;
                    };
                    let detail = custom_event.detail();
                    let Ok(delta): Result<Value, _> = serde_wasm_bindgen::from_value(detail) else {
                        return;
                    };
                    let Some(operations) = delta.get("operations").and_then(Value::as_array) else {
                        return;
                    };
                    if operations.is_empty() {
                        return;
                    }
                    // Editor instance maybe unavailable while loading
                    let Some(instance) = delta_editor.instance_untracked() else {
                        return;
                    };
                    // Read current content
                    let Ok(mut content) = instance.get_json() else {
                        return;
                    };
                    // Apply delta operations to the Tiptap JSON tree
                    let changed = apply_block_delta_to_json(&mut content, operations);
                    if !changed {
                        return;
                    }
                    // Write back
                    if instance
                        .set_content(TiptapContent::json(content.clone()))
                        .is_ok()
                    {
                        // Update reactive state
                        let html = instance.get_html().unwrap_or_default();
                        let json_text = serde_json::to_string(&content).unwrap_or_default();
                        delta_set_dirty_count.update(|c| *c += 1);
                        delta_set_html_output.set(html);
                        delta_set_json_output.set(json_text);
                        delta_set_document_json.set(content);
                    }
                }));

            if let Some(document) = window().and_then(|win| win.document()) {
                let delta_ref = delta_listener.as_ref().unchecked_ref();
                let _ = document.add_event_listener_with_callback(BLOCK_DELTA_EVENT, delta_ref);
                if let Some((mount_id, _, _)) = runtime_mount_context() {
                    let target: EventTarget = document.into();
                    register_runtime_listener(mount_id, target, delta_listener);
                }
            }
        }
    });

    {
        let turn_into_open = turn_into_open;
        let color_menu_open = color_menu_open;
        let more_menu_open = more_menu_open;
        let set_locked_toolbar_anchor = set_locked_toolbar_anchor;
        Effect::new(move |_| {
            if !(turn_into_open.get() || color_menu_open.get() || more_menu_open.get()) {
                set_locked_toolbar_anchor.set(None);
            }
        });
    }

    {
        let editor_focused = editor_focused;
        let text_selection_active = text_selection_active;
        let slash_open = slash_open;
        let block_menu_open = block_menu_open;
        let turn_into_open = turn_into_open;
        let color_menu_open = color_menu_open;
        let more_menu_open = more_menu_open;
        let set_turn_into_open = set_turn_into_open;
        let set_color_menu_open = set_color_menu_open;
        let set_more_menu_open = set_more_menu_open;
        Effect::new(move |_| {
            if should_auto_close_toolbar_overlays(
                editor_focused.get(),
                text_selection_active.get(),
                turn_into_open.get_untracked(),
                color_menu_open.get_untracked(),
                more_menu_open.get_untracked(),
                slash_open.get(),
                block_menu_open.get(),
            ) {
                set_turn_into_open.set(false);
                set_color_menu_open.set(false);
                set_more_menu_open.set(false);
            }
        });
    }

    {
        let set_editor_focused = set_editor_focused;
        let set_text_selection_active = set_text_selection_active;
        let set_table_toolbar_open = set_table_toolbar_open;
        let set_table_options_open = set_table_options_open;
        let set_table_overlay_anchor = set_table_overlay_anchor;
        let set_table_selection_overlay = set_table_selection_overlay;
        let set_floating_toolbar_anchor = set_floating_toolbar_anchor;
        let set_image_toolbar_anchor = set_image_toolbar_anchor;
        let set_slash_open = set_slash_open;
        let set_turn_into_open = set_turn_into_open;
        let set_hovered_block = set_hovered_block;
        let set_block_menu_anchor = set_block_menu_anchor;
        let set_block_menu_open = set_block_menu_open;
        let set_pending_drag = set_pending_drag;
        let set_dragging_block_index = set_dragging_block_index;
        let set_dragging_block_anchor = set_dragging_block_anchor;
        let set_drop_indicator = set_drop_indicator;
        let set_command_feedback = set_command_feedback;
        let set_suppress_handle_click = set_suppress_handle_click;
        let pending_drag = pending_drag;
        let dragging_block_index = dragging_block_index;
        let mouseup_handle = window_event_listener(ev::mouseup, move |event: MouseEvent| {
            if let Some(source_index) = dragging_block_index.try_get_untracked().flatten() {
                let indicator = drop_indicator_from_point(event.client_x(), event.client_y());
                if let Some(indicator) = indicator {
                    match editor_runtime::history_safe_commands::move_top_level_block_with_history(
                        &editor,
                        source_index,
                        indicator.index,
                        indicator.placement,
                    ) {
                        Ok(()) => {
                            sync_persisted_editor_command(
                                editor,
                                &persisted_identity,
                                set_dirty_count,
                                set_html_output,
                                set_document_json,
                                set_json_output,
                                title,
                                set_command_feedback,
                                "已通过块手柄拖拽重排",
                            );
                        }
                        Err(err) => {
                            set_command_feedback.set(err);
                        }
                    }
                } else {
                    let _ = set_command_feedback.try_set("块拖拽已取消".to_string());
                }
                let _ = set_dragging_block_index.try_set(None);
                let _ = set_dragging_block_anchor.try_set(None);
                let _ = set_drop_indicator.try_set(None);
                let _ = set_pending_drag.try_set(None);
                let _ = set_block_menu_open.try_set(false);
                let _ = set_block_menu_anchor.try_set(None);
                let _ = set_hovered_block.try_set(None);
                let _ = set_suppress_handle_click.try_set(true);
            } else if pending_drag.try_get_untracked().flatten().is_some() {
                let _ = set_pending_drag.try_set(None);
            }

            if let Some(target) = event.target() {
                if let Some(element) = target_element(target) {
                    if element
                        .closest(&format!("{HANDLE_SHELL_SELECTOR}, .floating-toolbar, .image-floating-toolbar, .table-toolbar, .table-controls"))
                        .ok()
                        .flatten()
                        .is_some()
                    {
                        return;
                    }
                    let in_image = element
                        .closest(".editor-surface .ProseMirror img[src]")
                        .ok()
                        .flatten()
                        .is_some();
                    if !in_image {
                        let _ = set_image_toolbar_anchor.try_set(None);
                    }
                    let _ = set_table_options_open.try_set(false);
                    let table = element
                        .closest(".editor-surface .ProseMirror table")
                        .ok()
                        .flatten();
                    let in_table = table.is_some();
                    let _ = set_table_toolbar_open.try_set(in_table);
                    if let Some(table) = table {
                        let _ = set_table_overlay_anchor
                            .try_set(table_overlay_anchor_from_table(&table));
                    } else {
                        let _ = set_table_overlay_anchor.try_set(None);
                        let _ = set_table_selection_overlay.try_set(None);
                    }
                }
            }
            let _ = try_sync_editor_overlay_state(
                set_editor_focused,
                set_text_selection_active,
                set_floating_toolbar_anchor,
                set_slash_open,
                set_turn_into_open,
                set_hovered_block,
                set_block_menu_anchor,
                set_block_menu_open,
            );
        });
        let keyup_handle = window_event_listener(ev::keyup, move |_| {
            let _ = try_sync_editor_overlay_state(
                set_editor_focused,
                set_text_selection_active,
                set_floating_toolbar_anchor,
                set_slash_open,
                set_turn_into_open,
                set_hovered_block,
                set_block_menu_anchor,
                set_block_menu_open,
            );
        });

        on_cleanup(move || {
            drop(mouseup_handle);
            drop(keyup_handle);
        });
    }

    {
        let set_editor_focused = set_editor_focused;
        let set_slash_open = set_slash_open;
        let set_turn_into_open = set_turn_into_open;
        let set_hovered_block = set_hovered_block;
        let set_block_menu_anchor = set_block_menu_anchor;
        let set_block_menu_open = set_block_menu_open;
        let set_pending_drag = set_pending_drag;
        let set_dragging_block_index = set_dragging_block_index;
        let set_dragging_block_anchor = set_dragging_block_anchor;
        let set_drop_indicator = set_drop_indicator;
        let set_command_feedback = set_command_feedback;
        let pending_drag = pending_drag;
        let dragging_block_index = dragging_block_index;
        let dragging_block_anchor = dragging_block_anchor;
        let mousemove_handle = window_event_listener(ev::mousemove, move |event: MouseEvent| {
            if dragging_block_index.try_get_untracked().flatten().is_some() {
                let _ = set_editor_focused.try_set(true);
                let _ = set_drop_indicator.try_set(drop_indicator_from_point(
                    event.client_x(),
                    event.client_y(),
                ));
                return;
            }

            let Some(pending) = pending_drag.try_get_untracked().flatten() else {
                return;
            };

            if event.buttons() & 1 == 0 {
                let _ = set_pending_drag.try_set(None);
                return;
            }

            let dx = (event.client_x() - pending.start_x).abs();
            let dy = (event.client_y() - pending.start_y).abs();
            if dx < 4 && dy < 4 {
                return;
            }

            let _ = set_editor_focused.try_set(true);
            let _ = set_slash_open.try_set(false);
            let _ = set_turn_into_open.try_set(false);
            let _ = set_block_menu_open.try_set(false);
            let _ = set_block_menu_anchor.try_set(None);
            let _ = set_hovered_block.try_set(Some(pending.anchor.clone()));
            let _ = set_dragging_block_index.try_set(Some(pending.index));
            let next_anchor = dragging_block_anchor
                .try_get_untracked()
                .flatten()
                .unwrap_or_else(|| pending.anchor.clone());
            let _ = set_dragging_block_anchor.try_set(Some(next_anchor));
            let _ = set_pending_drag.try_set(None);
            let _ = set_drop_indicator.try_set(drop_indicator_from_point(
                event.client_x(),
                event.client_y(),
            ));
            let _ = set_command_feedback.try_set(format!("开始拖拽：{}", pending.anchor.label));
        });

        on_cleanup(move || drop(mousemove_handle));
    }

    {
        let editor = editor;
        let document_id = document_id;
        let workspace_id = workspace_id;
        let title = title;
        let set_dirty_count = set_dirty_count;
        let set_html_output = set_html_output;
        let set_document_json = set_document_json;
        let set_json_output = set_json_output;
        let hovered_block = hovered_block;
        let set_hovered_block = set_hovered_block;
        let block_keyboard_mode = block_keyboard_mode;
        let set_block_keyboard_mode = set_block_keyboard_mode;
        let set_slash_open = set_slash_open;
        let slash_open = slash_open;
        let slash_index = slash_index;
        let set_slash_index = set_slash_index;
        let slash_query = slash_query;
        let set_slash_query = set_slash_query;
        let turn_into_open = turn_into_open;
        let set_turn_into_open = set_turn_into_open;
        let color_menu_open = color_menu_open;
        let set_color_menu_open = set_color_menu_open;
        let more_menu_open = more_menu_open;
        let set_more_menu_open = set_more_menu_open;
        let set_locked_toolbar_anchor = set_locked_toolbar_anchor;
        let set_text_selection_active = set_text_selection_active;
        let set_floating_toolbar_anchor = set_floating_toolbar_anchor;
        let image_toolbar_anchor = image_toolbar_anchor;
        let set_image_toolbar_anchor = set_image_toolbar_anchor;
        let table_toolbar_open = table_toolbar_open;
        let set_table_toolbar_open = set_table_toolbar_open;
        let table_options_open = table_options_open;
        let set_table_options_open = set_table_options_open;
        let set_table_overlay_anchor = set_table_overlay_anchor;
        let set_table_selection_overlay = set_table_selection_overlay;
        let set_block_menu_open = set_block_menu_open;
        let block_menu_open = block_menu_open;
        let set_block_menu_anchor = set_block_menu_anchor;
        let set_block_turn_into_open = set_block_turn_into_open;
        let set_block_folded_title_open = set_block_folded_title_open;
        let set_command_feedback = set_command_feedback;
        let keydown_capture_closure = SendWrapper::new(Closure::wrap(Box::new(
            move |event: ev::KeyboardEvent| {
                if !active_editor_stage()
                    || event.ctrl_key()
                    || event.meta_key()
                    || event.alt_key()
                    || !matches!(event.key().as_str(), "Backspace" | "Delete")
                    || !editor_runtime::attachment_links::is_local_attachment_link_selected()
                {
                    return;
                }

                event.prevent_default();
                event.stop_immediate_propagation();
                match editor_runtime::history_safe_commands::delete_selected_attachment_link_with_history(
                        &editor,
                    ) {
                    Ok(()) => {
                        sync_persisted_editor_command(
                            editor,
                            &runtime_persisted_identity(document_id, workspace_id),
                            set_dirty_count,
                            set_html_output,
                            set_document_json,
                            set_json_output,
                            title,
                            set_command_feedback,
                            "已删除附件引用",
                        );
                        let _ = set_hovered_block.try_set(None);
                        let _ = set_block_menu_open.try_set(false);
                        let _ = set_block_menu_anchor.try_set(None);
                    }
                    Err(err) => {
                        let _ = set_command_feedback.try_set(format!("删除附件引用失败：{err}"));
                    }
                }
            },
        )
            as Box<dyn FnMut(_)>));
        if let Some(win) = window() {
            let _ = win.add_event_listener_with_callback_and_bool(
                "keydown",
                keydown_capture_closure.as_ref().unchecked_ref(),
                true,
            );
        }
        on_cleanup(move || {
            if let Some(win) = window() {
                let _ = win.remove_event_listener_with_callback_and_bool(
                    "keydown",
                    keydown_capture_closure.as_ref().unchecked_ref(),
                    true,
                );
            }
            drop(keydown_capture_closure);
        });
        let keydown_handle = window_event_listener(ev::keydown, move |event| {
            let focused = active_editor_stage();
            if !focused {
                if body_has_focus() && (event.ctrl_key() || event.meta_key()) && !event.alt_key() {
                    let key = event.key();
                    let undo_shortcut = !event.shift_key() && key.eq_ignore_ascii_case("z");
                    let redo_shortcut = key.eq_ignore_ascii_case("y")
                        || (event.shift_key() && key.eq_ignore_ascii_case("z"));
                    if undo_shortcut || redo_shortcut {
                        event.prevent_default();
                        if redo_shortcut {
                            let _ = editor.redo();
                        } else {
                            let _ = editor.undo();
                        }
                        schedule_editor_focus(editor);
                        return;
                    }
                }
                return;
            }

            if (event.ctrl_key() || event.meta_key())
                && event.shift_key()
                && event.key().eq_ignore_ascii_case("u")
            {
                event.prevent_default();
                let Some(block) = hovered_block_from_selection()
                    .or_else(|| hovered_block.try_get_untracked().flatten())
                    .or_else(|| block_state_from_index(0))
                else {
                    let _ =
                        set_command_feedback.try_set("没有可进入布局选中态的当前块".to_string());
                    return;
                };
                let _ = set_hovered_block.try_set(Some(block.clone()));
                let _ = set_block_keyboard_mode.try_set(true);
                let _ = set_slash_open.try_set(false);
                let _ = set_slash_query.try_set(String::new());
                let _ = set_turn_into_open.try_set(false);
                let _ = set_block_menu_open.try_set(false);
                let _ = set_block_menu_anchor.try_set(None);
                let _ =
                    set_command_feedback.try_set(format!("已进入块布局选中态：{}", block.label));
                return;
            }

            if (event.ctrl_key() || event.meta_key()) && event.shift_key() && !event.alt_key() {
                let action = match event.key().as_str() {
                    "1" | "!" => Some(SlashActionKind::Heading1),
                    "2" | "@" => Some(SlashActionKind::Heading2),
                    "3" | "#" => Some(SlashActionKind::Heading3),
                    "5" | "%" => Some(SlashActionKind::Todo),
                    "6" | "^" => Some(SlashActionKind::BulletList),
                    "7" | "&" => Some(SlashActionKind::OrderedList),
                    _ => None,
                };
                if let Some(action) = action {
                    event.prevent_default();
                    let result = run_slash_action(editor, action);
                    let message = match result {
                        Ok(message) => {
                            sync_persisted_editor_command(
                                editor,
                                &runtime_persisted_identity(document_id, workspace_id),
                                set_dirty_count,
                                set_html_output,
                                set_document_json,
                                set_json_output,
                                title,
                                set_command_feedback,
                                message,
                            );
                            None
                        }
                        Err(err) => Some(err),
                    };
                    if let Some(message) = message {
                        let _ = set_command_feedback.try_set(message);
                    }
                    let _ = set_slash_open.try_set(false);
                    let _ = set_slash_query.try_set(String::new());
                    let _ = set_turn_into_open.try_set(false);
                    let _ = set_block_menu_open.try_set(false);
                    let _ = set_block_menu_anchor.try_set(None);
                    return;
                }
            }

            if event.key() == "Escape" {
                event.prevent_default();
                if close_editor_floating_overlays_if_escape(
                    slash_open,
                    turn_into_open,
                    color_menu_open,
                    more_menu_open,
                    image_toolbar_anchor,
                    table_toolbar_open,
                    table_options_open,
                    block_menu_open,
                    set_slash_open,
                    set_slash_index,
                    set_slash_query,
                    set_turn_into_open,
                    set_color_menu_open,
                    set_more_menu_open,
                    set_locked_toolbar_anchor,
                    set_text_selection_active,
                    set_floating_toolbar_anchor,
                    set_image_toolbar_anchor,
                    set_table_toolbar_open,
                    set_table_options_open,
                    set_table_overlay_anchor,
                    set_table_selection_overlay,
                    set_block_menu_open,
                    set_block_menu_anchor,
                    set_block_turn_into_open,
                    set_block_folded_title_open,
                ) {
                    let _ = set_block_keyboard_mode.try_set(false);
                    return;
                }

                if let Some(block) = hovered_block_from_selection()
                    .or_else(|| hovered_block.try_get_untracked().flatten())
                    .or_else(|| block_state_from_index(0))
                {
                    let _ = set_hovered_block.try_set(Some(block.clone()));
                    let _ = set_block_keyboard_mode.try_set(true);
                    let _ = set_command_feedback
                        .try_set(format!("已选中块：{}，按 a/b 可在上/下插入", block.label));
                }
                return;
            }

            if block_keyboard_mode.try_get_untracked().unwrap_or(false)
                && !event.ctrl_key()
                && !event.meta_key()
                && !event.alt_key()
                && matches!(event.key().as_str(), "a" | "A" | "b" | "B")
            {
                event.prevent_default();
                let before = matches!(event.key().as_str(), "a" | "A");
                let Some(block) = hovered_block
                    .try_get_untracked()
                    .flatten()
                    .or_else(|| hovered_block_from_selection())
                    .or_else(|| block_state_from_index(0))
                else {
                    let _ = set_command_feedback.try_set("没有可插入的当前块".to_string());
                    return;
                };
                match insert_editor_paragraph_relative_to_block(editor, block.index, before) {
                    Ok(new_index) => {
                        sync_persisted_editor_command(
                            editor,
                            &runtime_persisted_identity(document_id, workspace_id),
                            set_dirty_count,
                            set_html_output,
                            set_document_json,
                            set_json_output,
                            title,
                            set_command_feedback,
                            if before {
                                "已通过 Esc,a 在上方插入新块"
                            } else {
                                "已通过 Esc,b 在下方插入新块"
                            },
                        );
                        let _ = set_hovered_block.try_set(block_state_from_index(new_index));
                        let _ = set_block_keyboard_mode.try_set(false);
                    }
                    Err(err) => {
                        let _ = set_command_feedback.try_set(err);
                    }
                }
                return;
            }

            if event.key() == "/" && !event.ctrl_key() && !event.meta_key() && !event.alt_key() {
                event.prevent_default();
                open_slash_menu_overlay(
                    set_slash_open,
                    set_slash_index,
                    set_slash_query,
                    set_turn_into_open,
                    set_color_menu_open,
                    set_more_menu_open,
                    set_locked_toolbar_anchor,
                    set_text_selection_active,
                    set_floating_toolbar_anchor,
                    set_image_toolbar_anchor,
                    set_table_toolbar_open,
                    set_table_options_open,
                    set_table_overlay_anchor,
                    set_table_selection_overlay,
                    set_block_menu_open,
                    set_block_menu_anchor,
                    set_block_turn_into_open,
                    set_block_folded_title_open,
                );
                let _ = set_block_keyboard_mode.try_set(false);
                let _ = set_command_feedback.try_set("Slash 菜单已打开".to_string());
                return;
            }

            if !slash_open.try_get_untracked().unwrap_or(false) {
                return;
            }

            match event.key().as_str() {
                "ArrowDown" => {
                    event.prevent_default();
                    let next =
                        (slash_index.try_get_untracked().unwrap_or(0) + 1) % SLASH_ACTIONS.len();
                    let _ = set_slash_index.try_set(next);
                }
                "ArrowUp" => {
                    event.prevent_default();
                    let current = slash_index.try_get_untracked().unwrap_or(0);
                    let next = if current == 0 {
                        SLASH_ACTIONS.len() - 1
                    } else {
                        current - 1
                    };
                    let _ = set_slash_index.try_set(next);
                }
                "Backspace" => {
                    if !slash_query
                        .try_get_untracked()
                        .unwrap_or_default()
                        .is_empty()
                    {
                        event.prevent_default();
                        let _ = set_slash_query.try_update(|query| {
                            query.pop();
                        });
                        let _ = set_slash_index.try_set(0);
                    }
                }
                "Enter" => {
                    event.prevent_default();
                    let action = SLASH_ACTIONS[slash_index.try_get_untracked().unwrap_or(0)].kind;
                    match run_slash_action(editor, action) {
                        Ok(message) => {
                            let _ = set_command_feedback.try_set(message.to_string());
                        }
                        Err(message) => {
                            let _ = set_command_feedback.try_set(message);
                        }
                    }
                    let _ = set_slash_open.try_set(false);
                }
                _ => {
                    let key = event.key();
                    if key.chars().count() == 1
                        && !event.ctrl_key()
                        && !event.meta_key()
                        && !event.alt_key()
                    {
                        event.prevent_default();
                        let _ = set_slash_query.try_update(|query| query.push_str(&key));
                        let _ = set_slash_index.try_set(0);
                    }
                }
            }
        });
        on_cleanup(move || drop(keydown_handle));
    }

    {
        let click_handle = window_event_listener(ev::click, move |event: MouseEvent| {
            let Some(element) = event.target().and_then(target_element) else {
                return;
            };
            let Some(anchor) = element.closest("a.mnote-page-block-link").ok().flatten() else {
                return;
            };
            let Some(href) = anchor
                .get_attribute("href")
                .map(|value| value.trim().to_string())
                .filter(|value| !value.is_empty() && value != "#")
            else {
                return;
            };
            event.prevent_default();
            event.stop_propagation();
            if let Some(win) = window() {
                let _ = win.location().assign(&href);
            }
        });
        on_cleanup(move || drop(click_handle));
    }

    {
        schedule_scroll_mnote_block_anchor_from_hash();
        let hashchange_handle = window_event_listener(ev::hashchange, move |_| {
            schedule_scroll_mnote_block_anchor_from_hash();
        });
        on_cleanup(move || drop(hashchange_handle));
    }

    let has_task_schema = move || {
        let snapshot = json_output.get();
        snapshot.contains("\"taskList\"") && snapshot.contains("\"taskItem\"")
    };

    let selection_text = move || selection_summary(&selection_state.get());
    let is_embedded = is_embedded_mode();

    let on_link_click = {
        let editor = editor;
        let selection_state = selection_state;
        let set_command_feedback = set_command_feedback;
        move |_| {
            if selection_state.get().link {
                apply_feedback_result(
                    set_command_feedback,
                    editor.unset_link().map(|_| "已移除链接"),
                );
                return;
            }

            let href = window()
                .and_then(|win| win.prompt_with_message("输入链接地址").ok().flatten())
                .filter(|value| !value.trim().is_empty());

            match href {
                Some(value) => apply_feedback_result(
                    set_command_feedback,
                    editor
                        .set_link(TiptapLinkResource {
                            href: value,
                            target: Some("_blank".into()),
                            rel: Some("noopener noreferrer".into()),
                            class: None,
                        })
                        .map(|_| "已插入链接"),
                ),
                None => set_command_feedback.set("链接操作已取消".to_string()),
            }
        }
    };

    let slash_menu_runtime = SlashMenuRuntimeContext {
        change_event_target: slash_change_event_target.clone(),
        dispatch_runtime_state: std::sync::Arc::new(move |target: &EventTarget| {
            dispatch_runtime_state_to_target(
                target,
                document_id.get_untracked(),
                workspace_id.get_untracked(),
                title.get_untracked(),
                dirty_count.get_untracked(),
                hovered_block.get_untracked(),
                editor_focused.get_untracked(),
                slash_open.get_untracked(),
                turn_into_open.get_untracked(),
                color_menu_open.get_untracked(),
                more_menu_open.get_untracked(),
                read_only.get_untracked(),
            );
        }),
    };
    let slash_menu_props = SlashMenuViewProps {
        editor,
        signals: SlashMenuSignals {
            slash_open,
            set_slash_open,
            slash_index,
            slash_query,
            dirty_count,
            set_dirty_count,
            document_json: set_document_json,
            html_output: set_html_output,
            json_output: set_json_output,
            document_id,
            workspace_id,
            title,
            editor_focused,
            hovered_block,
            turn_into_open,
            color_menu_open,
            more_menu_open,
            revision,
            conflict_detection_key,
            read_only,
            command_feedback: set_command_feedback,
        },
        runtime: slash_menu_runtime,
        anchor_style: std::sync::Arc::new(slash_menu_anchor_style),
    };

    view! {
        <style>{SPIKE_STYLE}</style>
        <main
            class=move || if is_embedded { "app-shell app-shell-embedded" } else { "app-shell" }
            data-testid="mnote-leptos-tiptap-host"
            data-mnote-runtime=RUNTIME_NAME
            data-mnote-runtime-bridge="true"
        >
            {move || {
                if is_embedded {
                    ().into_any()
                } else {
                    view! {
                        <section class="hero-bar">
                            <div class="hero-meta">
                                <span class="hero-tag">"P0 / Tiptap-like 主编辑器体验"</span>
                                <h1 class="hero-title">"把 Spike 收敛成能检验的最小产品页壳"</h1>
                                <p class="hero-copy">
                                    "这里继续只做 "
                                    <code>"8123"</code>
                                    " 的 Leptos + Tiptap P0 体验验证，不挪动正式 "
                                    <code>"/documents/[id]"</code>
                                    " 主链。目标是先把 slash、toolbar、todo schema、块手柄和 reload 做成可直接看得见、点得动、能刷新保留结构的网页。"
                                </p>
                            </div>
                            <div class="hero-actions">
                                <div class="hero-chip">
                                    <span>"变更计数"</span>
                                    <strong>{move || dirty_count.get().to_string()}</strong>
                                </div>
                                <div class="hero-chip" id="todo-schema-status">
                                    <span>"Todo Schema"</span>
                                    <strong>{move || if has_task_schema() { "ready" } else { "pending" }}</strong>
                                </div>
                                <div class="hero-chip" id="selection-summary-chip">
                                    <span>"Selection"</span>
                                    <strong>{selection_text}</strong>
                                </div>
                            </div>
                        </section>
                    }.into_any()
                }
            }}

            <section class="editor-card">
                {move || {
                    if is_embedded {
                        ().into_any()
                    } else {
                        view! {
                            <>
                                <div class="editor-topbar">
                                    <div class="editor-crumbs">
                                        <span>"空间 / 主文档 /"</span>
                                        <strong>"Editor Baseline Reset"</strong>
                                    </div>
                                    <div class="editor-crumbs">
                                        <span>"反馈："</span>
                                        <strong id="command-feedback">{move || command_feedback.get()}</strong>
                                    </div>
                                </div>

                                <input
                                    class="title-input"
                                    prop:value=move || title.get()
                                    on:input=move |event| {
                                        let next_title = event_target_value(&event);
                                        set_title.set(next_title.clone());
                                        match persist_document_state(
                                            &runtime_persisted_identity(document_id, workspace_id),
                                            &next_title,
                                            &document_json.get_untracked(),
                                            Some(html_output.get_untracked()),
                                        ) {
                                            Ok(()) => set_command_feedback.set("标题已写入本地草稿".to_string()),
                                            Err(err) => set_command_feedback.set(format!("标题保存失败：{err}")),
                                        }
                                    }
                                    placeholder="给这篇页面起个标题"
                                    aria-label="Spike 页面标题"
                                />

                                <div class="editor-subcopy">
                                    <span class="hint-pill">"输入 “/” 打开真正的最小 slash 菜单"</span>
                                    <span class="hint-pill">"选中文本时会出现最小浮动工具条"</span>
                                    <span class="hint-pill">"Todo 现在走 taskList / taskItem，而不是占位 HTML"</span>
                                    <span class="hint-pill">"把鼠标移到块左侧，点击 ⠿ 打开块菜单并拖拽重排"</span>
                                    <span class="hint-pill">"本页会把 JSON 草稿写入 localStorage，刷新后不丢结构"</span>
                                </div>
                            </>
                        }.into_any()
                    }
                }}

                <div
                    id=editor_stage_id.clone()
                    class="editor-stage"
                    data-testid="mnote-leptos-tiptap-editor-stage"
                    data-page-wide-layout=move || wide_layout.get().to_string()
                    data-page-show-heading-numbers=move || show_heading_numbers.get().to_string()
                    data-page-embed-default-block-id=move || embed_default_block_id.get().unwrap_or_default()
                    on:click=move |event: MouseEvent| {
                        let Some(target) = event.target() else {
                            return;
                        };

                        if let Some(element) = target_element(target.clone()) {
                            if element_within_handle_shell(&element, HANDLE_SHELL_SELECTOR) {
                                return;
                            }
                        }

                                        if let Some(image) = image_element_from_target(target.clone()) {
                            let _ = select_mnote_image_node(&image);
                            set_editor_focused.set(true);
                            open_image_toolbar_overlay(
                                image_toolbar_anchor_from_image(&image),
                                set_block_keyboard_mode,
                                set_hovered_block,
                                set_slash_open,
                                set_slash_index,
                                set_slash_query,
                                set_turn_into_open,
                                set_color_menu_open,
                                set_more_menu_open,
                                set_locked_toolbar_anchor,
                                set_text_selection_active,
                                set_floating_toolbar_anchor,
                                set_image_toolbar_anchor,
                                set_table_toolbar_open,
                                set_table_options_open,
                                set_table_overlay_anchor,
                                set_table_selection_overlay,
                                set_block_menu_open,
                                set_block_menu_anchor,
                                set_block_turn_into_open,
                                set_block_folded_title_open,
                            );
                            return;
                        }

                        set_image_toolbar_anchor.set(None);
                        set_editor_focused.set(true);
                        set_block_keyboard_mode.set(false);
                        set_block_menu_open.set(false);
                        set_block_menu_anchor.set(None);
                        set_turn_into_open.set(false);
                        set_hovered_block.set(hovered_block_from_target(target));
                    }
                    on:mousemove=move |event: MouseEvent| {
                        if block_menu_open.get_untracked() || dragging_block_index.get_untracked().is_some() {
                            return;
                        }

                        let Some(target) = event.target() else {
                            return;
                        };

                        if let Some(element) = target_element(target.clone()) {
                            if element_within_handle_shell(&element, HANDLE_SHELL_SELECTOR) {
                                return;
                            }
                        }

                        set_editor_focused.set(true);
                        if let Some(block) = hovered_block_from_target(target) {
                            set_hovered_block.set(Some(block));
                            return;
                        }

                        if let Some(current_block) = hovered_block.get_untracked() {
                            if editor_runtime::block_dnd::pointer_in_handle_corridor(
                                event.client_x(),
                                event.client_y(),
                                &current_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;
                            }
                        }

                        set_hovered_block.set(None);
                    }
                    on:mouseover=move |event: MouseEvent| {
                        if block_menu_open.get_untracked() || dragging_block_index.get_untracked().is_some() {
                            return;
                        }
                        let Some(target) = event.target() else {
                            return;
                        };
                        if let Some(block) = hovered_block_from_target(target) {
                            set_editor_focused.set(true);
                            set_hovered_block.set(Some(block));
                        }
                    }
                    on:mouseleave=move |event: MouseEvent| {
                        if block_menu_open.get_untracked() || dragging_block_index.get_untracked().is_some() {
                            return;
                        }
                        if let Some(current_block) = hovered_block.get_untracked() {
                            if editor_runtime::block_dnd::pointer_in_handle_corridor(
                                event.client_x(),
                                event.client_y(),
                                &current_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;
                            }
                        }
                        set_hovered_block.set(None);
                    }
                    on:dragover=move |event: DragEvent| {
                        if dragging_block_index.get_untracked().is_none() {
                            return;
                        }

                        event.prevent_default();
                        let Some(target) = event.target() else {
                            return;
                        };

                        if let Some(element) = target_element(target.clone()) {
                            if element_within_handle_shell(&element, HANDLE_SHELL_SELECTOR) {
                                return;
                            }
                        }

                        set_drop_indicator.set(drop_indicator_from_target(target, event.client_y()));
                    }
                    on:drop=move |event: DragEvent| {
                        event.prevent_default();
                        let Some(source_index) = dragging_block_index.get_untracked() else {
                            return;
                        };

                        let Some(target) = event.target() else {
                            set_dragging_block_index.set(None);
                            set_dragging_block_anchor.set(None);
                            set_drop_indicator.set(None);
                            return;
                        };

                        let Some(indicator) = drop_indicator_from_target(target, event.client_y()) else {
                            set_dragging_block_index.set(None);
                            set_dragging_block_anchor.set(None);
                            set_drop_indicator.set(None);
                            return;
                        };

                        match editor_runtime::history_safe_commands::move_top_level_block_with_history(
                                &editor,
                                source_index,
                                indicator.index,
                                indicator.placement,
                            ) {
                                Ok(()) => {
                                    sync_persisted_editor_command(
                                        editor,
                                        &runtime_persisted_identity(document_id, workspace_id),
                                        set_dirty_count,
                                        set_html_output,
                                        set_document_json,
                                        set_json_output,
                                        title,
                                        set_command_feedback,
                                        "已通过块手柄拖拽重排",
                                    );
                                }
                                Err(err) => {
                                    set_command_feedback.set(err);
                                }
                            }

                        set_dragging_block_index.set(None);
                        set_dragging_block_anchor.set(None);
                        set_drop_indicator.set(None);
                        set_block_menu_open.set(false);
                        set_block_menu_anchor.set(None);
                        set_hovered_block.set(None);
                    }
                >
                    {table_toolbar_view(
                        editor,
                        table_toolbar_open,
                        set_table_toolbar_open,
                        table_options_open,
                        set_table_options_open,
                        table_overlay_anchor,
                        set_table_overlay_anchor,
                        table_selection_overlay,
                        set_table_selection_overlay,
                        slash_open,
                        block_menu_open,
                        dragging_block_index,
                        document_json,
                        set_document_json,
                        set_dirty_count,
                        set_html_output,
                        set_json_output,
                        title,
                        document_id,
                        workspace_id,
                        set_command_feedback,
                    )}
                    {move || {
                        let Some(anchor) = image_toolbar_anchor.get() else {
                            return ().into_any();
                        };
                        if slash_open.get() || block_menu_open.get() || dragging_block_index.get().is_some() {
                            return ().into_any();
                        }
                        view! {
                            <div
                                class="image-floating-toolbar"
                                data-testid="image-floating-toolbar"
                                data-align=anchor.align.clone()
                                style:top=format!("{}px", anchor.top)
                                style:left=format!("{}px", anchor.left)
                                on:mousedown=move |event: MouseEvent| {
                                    event.prevent_default();
                                    event.stop_propagation();
                                }
                                on:click=move |event: MouseEvent| {
                                    event.stop_propagation();
                                }
                            >
                                <button
                                    type="button"
                                    class="image-toolbar-btn"
                                    data-testid="image-align-left"
                                    data-active=move || image_toolbar_anchor.get().map(|anchor| anchor.align == "left").unwrap_or(true).to_string()
                                    title="左对齐"
                                    aria-label="左对齐"
                                    on:click=move |_| {
                                        match apply_image_align(editor, "left") {
                                            Ok(message) => {
                                                set_image_toolbar_anchor.update(|state| {
                                                    if let Some(anchor) = state.as_mut() {
                                                        anchor.align = "left".to_string();
                                                    }
                                                });
                                                sync_persisted_editor_command(
                                                    editor,
                                                    &runtime_persisted_identity(document_id, workspace_id),
                                                    set_dirty_count,
                                                    set_html_output,
                                                    set_document_json,
                                                    set_json_output,
                                                    title,
                                                    set_command_feedback,
                                                    message,
                                                );
                                            }
                                            Err(err) => set_command_feedback.set(err),
                                        }
                                    }
                                >"左"</button>
                                <button
                                    type="button"
                                    class="image-toolbar-btn"
                                    data-testid="image-align-center"
                                    data-active=move || image_toolbar_anchor.get().map(|anchor| anchor.align == "center").unwrap_or(false).to_string()
                                    title="居中"
                                    aria-label="居中"
                                    on:click=move |_| {
                                        match apply_image_align(editor, "center") {
                                            Ok(message) => {
                                                set_image_toolbar_anchor.update(|state| {
                                                    if let Some(anchor) = state.as_mut() {
                                                        anchor.align = "center".to_string();
                                                    }
                                                });
                                                sync_persisted_editor_command(
                                                    editor,
                                                    &runtime_persisted_identity(document_id, workspace_id),
                                                    set_dirty_count,
                                                    set_html_output,
                                                    set_document_json,
                                                    set_json_output,
                                                    title,
                                                    set_command_feedback,
                                                    message,
                                                );
                                            }
                                            Err(err) => set_command_feedback.set(err),
                                        }
                                    }
                                >"中"</button>
                                <button
                                    type="button"
                                    class="image-toolbar-btn"
                                    data-testid="image-align-right"
                                    data-active=move || image_toolbar_anchor.get().map(|anchor| anchor.align == "right").unwrap_or(false).to_string()
                                    title="右对齐"
                                    aria-label="右对齐"
                                    on:click=move |_| {
                                        match apply_image_align(editor, "right") {
                                            Ok(message) => {
                                                set_image_toolbar_anchor.update(|state| {
                                                    if let Some(anchor) = state.as_mut() {
                                                        anchor.align = "right".to_string();
                                                    }
                                                });
                                                sync_persisted_editor_command(
                                                    editor,
                                                    &runtime_persisted_identity(document_id, workspace_id),
                                                    set_dirty_count,
                                                    set_html_output,
                                                    set_document_json,
                                                    set_json_output,
                                                    title,
                                                    set_command_feedback,
                                                    message,
                                                );
                                            }
                                            Err(err) => set_command_feedback.set(err),
                                        }
                                    }
                                >"右"</button>
                                <span class="image-toolbar-separator" aria-hidden="true"></span>
                                <button
                                    type="button"
                                    class="image-toolbar-btn"
                                    data-testid="image-download"
                                    title="下载图片"
                                    aria-label="下载图片"
                                    on:click=move |_| {
                                        if download_mnote_selected_image() {
                                            set_command_feedback.set("已开始下载图片".to_string());
                                        } else {
                                            set_command_feedback.set("图片下载失败".to_string());
                                        }
                                    }
                                >"下"</button>
                                <button
                                    type="button"
                                    class="image-toolbar-btn"
                                    data-testid="image-delete"
                                    title="删除图片"
                                    aria-label="删除图片"
                                    disabled=true
                                >"删"</button>
                            </div>
                        }.into_any()
                    }}

                    {move || {
                        let toolbar_locked = toolbar_overlay_locked(
                            turn_into_open.get(),
                            color_menu_open.get(),
                            more_menu_open.get(),
                        );
                        let toolbar_anchor = if toolbar_locked {
                            locked_toolbar_anchor
                                .get()
                                .or_else(|| floating_toolbar_anchor.get())
                        } else {
                            floating_toolbar_anchor.get()
                        };
                        if let Some(anchor) = toolbar_anchor {
                            if should_auto_close_toolbar_overlays(
                                editor_focused.get(),
                                text_selection_active.get(),
                                turn_into_open.get(),
                                color_menu_open.get(),
                                more_menu_open.get(),
                                slash_open.get(),
                                block_menu_open.get(),
                            )
                                || dragging_block_index.get().is_some()
                            {
                                return ().into_any();
                            }
                            view! {
                                <div
                                    class="floating-toolbar"
                                    data-testid="mnote-leptos-tiptap-toolbar"
                                    style:top=format!("{}px", anchor.top)
                                    style:left=format!("{}px", anchor.left)
                                    on:mousedown=move |event: MouseEvent| {
                                        if event_target_matches_selector(
                                            event.target(),
                                            ".toolbar-btn, .toolbar-menu-item, .turn-into-item",
                                        ) {
                                            event.prevent_default();
                                        }
                                        event.stop_propagation();
                                    }
                                    on:click=move |event: MouseEvent| {
                                        event.stop_propagation();
                                    }
                                >
                                    <div class="floating-toolbar-group">
                                        <div class="toolbar-popover-anchor">
                                            <button
                                                class="toolbar-btn"
                                                data-wide="true"
                                                data-active=move || turn_into_open.get().to_string()
                                                data-testid="toolbar-turn-into"
                                                on:click=move |_| {
                                                    let next_open = !turn_into_open.get_untracked();
                                                    if next_open {
                                                        set_locked_toolbar_anchor
                                                            .set(floating_toolbar_anchor.get_untracked());
                                                    }
                                                    set_turn_into_open.set(next_open);
                                                    set_block_menu_open.set(false);
                                                }
                                            >
                                                "文本"
                                            </button>
                                            <div
                                                class="turn-into-panel"
                                                data-testid="turn-into-panel"
                                                style:display=move || if turn_into_open.get() { "grid" } else { "none" }
                                                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);
                                                }
                                            >
                                                {SLASH_ACTIONS.iter().map(|action| {
                                                    let kind = action.kind;
                                                    let label = action.label;
                                                    let description = action.description;
                                                    let testid = format!("turn-into-{}", action.id);
                                                    view! {
                                                        <button
                                                            class="turn-into-item"
                                                            data-testid=testid
                                                            on:click=move |_| {
                                                                apply_feedback_result(set_command_feedback, run_slash_action(editor, kind));
                                                                set_turn_into_open.set(false);
                                                            }
                                                        >
                                                            {label}
                                                            <span>{description}</span>
                                                        </button>
                                                    }
                                                }).collect_view()}
                                            </div>
                                        </div>
                                    </div>

                                    <div class="toolbar-separator"></div>

                                    <div class="floating-toolbar-group">
                                        <button
                                            class="toolbar-btn"
                                            data-active=move || selection_state.get().bold.to_string()
                                            data-testid="toolbar-bold"
                                            on:click=move |_| apply_ok_feedback(set_command_feedback, editor.toggle_bold(), "已更新行内样式")
                                        >
                                            "B"
                                        </button>
                                        <button
                                            class="toolbar-btn"
                                            data-active=move || selection_state.get().italic.to_string()
                                            data-testid="toolbar-italic"
                                            on:click=move |_| apply_ok_feedback(set_command_feedback, editor.toggle_italic(), "已更新行内样式")
                                        >
                                            "I"
                                        </button>
                                        <button
                                            class="toolbar-btn"
                                            data-active=move || selection_state.get().underline.to_string()
                                            data-testid="toolbar-underline"
                                            on:click=move |_| apply_ok_feedback(
                                                set_command_feedback,
                                                editor.toggle_mark(TiptapMarkName::Underline, None, None),
                                                "已更新行内样式",
                                            )
                                        >
                                            "U"
                                        </button>
                                        <button
                                            class="toolbar-btn"
                                            data-active=move || selection_state.get().strike.to_string()
                                            data-testid="toolbar-strike"
                                            on:click=move |_| apply_ok_feedback(set_command_feedback, editor.toggle_strike(), "已更新行内样式")
                                        >
                                            "S"
                                        </button>
                                        <button
                                            class="toolbar-btn"
                                            data-active="false"
                                            data-testid="toolbar-code"
                                            on:click=move |_| apply_ok_feedback(set_command_feedback, editor.toggle_code(), "已更新行内样式")
                                        >
                                            "</>"
                                        </button>
                                        <button
                                            class="toolbar-btn"
                                            data-active=move || selection_state.get().link.to_string()
                                            data-testid="toolbar-link"
                                            on:click=on_link_click
                                        >
                                            "链接"
                                        </button>
                                        <div class="toolbar-popover-anchor">
                                            <button
                                                class="toolbar-btn"
                                                data-wide="true"
                                                data-active=move || (selection_state.get().text_style
                                                    || selection_state.get().highlight
                                                    || color_menu_open.get()).to_string()
                                                data-testid="toolbar-color"
                                                on:click=move |_| {
                                                    let next_open = !color_menu_open.get_untracked();
                                                    if next_open {
                                                        set_locked_toolbar_anchor
                                                            .set(floating_toolbar_anchor.get_untracked());
                                                    }
                                                    set_color_menu_open.set(next_open);
                                                    set_more_menu_open.set(false);
                                                    set_turn_into_open.set(false);
                                                }
                                            >
                                                "颜色"
                                            </button>
                                            <div
                                                class="toolbar-popover-panel"
                                                data-testid="toolbar-color-panel"
                                                style:display=move || if color_menu_open.get() { "grid" } else { "none" }
                                                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">
                                                        {TOOLBAR_TEXT_COLORS.iter().map(|option| {
                                                            let value = option.value;
                                                            let label = option.label;
                                                            let testid = format!("toolbar-text-color-{}", option.id);
                                                            view! {
                                                                <button
                                                                    class="toolbar-menu-item toolbar-color-item"
                                                                    data-testid=testid
                                                                    on:click=move |_| {
                                                                        apply_feedback_result(
                                                                            set_command_feedback,
                                                                            apply_text_color(editor, value),
                                                                        );
                                                                        set_color_menu_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">
                                                        {TOOLBAR_HIGHLIGHT_COLORS.iter().map(|option| {
                                                            let value = option.value;
                                                            let label = option.label;
                                                            let testid = format!("toolbar-highlight-color-{}", option.id);
                                                            view! {
                                                                <button
                                                                    class="toolbar-menu-item toolbar-color-item"
                                                                    data-testid=testid
                                                                    on:click=move |_| {
                                                                        apply_feedback_result(
                                                                            set_command_feedback,
                                                                            apply_highlight_color(editor, Some(value)),
                                                                        );
                                                                        set_color_menu_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="toolbar-clear-color"
                                                        on:click=move |_| {
                                                            apply_feedback_result(
                                                                set_command_feedback,
                                                                clear_text_color(editor),
                                                            );
                                                            let _ = apply_highlight_color(editor, None);
                                                            set_color_menu_open.set(false);
                                                        }
                                                    >
                                                        <strong>"清除颜色"</strong>
                                                        <span>"移除文字色与背景色"</span>
                                                    </button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>

                                    <div class="toolbar-separator"></div>

                                    <div class="floating-toolbar-group">
                                        <div class="toolbar-popover-anchor">
                                            <button
                                                class="toolbar-btn"
                                                data-wide="true"
                                                data-active=move || (selection_state.get().align_center
                                                    || selection_state.get().align_right
                                                    || selection_state.get().align_justify
                                                    || more_menu_open.get()).to_string()
                                                data-testid="toolbar-more"
                                                on:click=move |_| {
                                                    let next_open = !more_menu_open.get_untracked();
                                                    if next_open {
                                                        set_locked_toolbar_anchor
                                                            .set(floating_toolbar_anchor.get_untracked());
                                                    }
                                                    set_more_menu_open.set(next_open);
                                                    set_color_menu_open.set(false);
                                                    set_turn_into_open.set(false);
                                                }
                                            >
                                                "更多"
                                            </button>
                                            <div
                                                class="toolbar-popover-panel"
                                                data-testid="toolbar-more-panel"
                                                style:display=move || if more_menu_open.get() { "grid" } else { "none" }
                                                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-more-list">
                                                        {TOOLBAR_ALIGN_OPTIONS.iter().map(|option| {
                                                            let label = option.label;
                                                            let alignment = option.alignment;
                                                            let testid = format!("toolbar-align-{}", option.id);
                                                            let active = match option.alignment {
                                                                TiptapTextAlign::Left => selection_state.get().align_left,
                                                                TiptapTextAlign::Center => selection_state.get().align_center,
                                                                TiptapTextAlign::Right => selection_state.get().align_right,
                                                                TiptapTextAlign::Justify => selection_state.get().align_justify,
                                                            };
                                                            view! {
                                                                <button
                                                                    class="toolbar-menu-item toolbar-more-item"
                                                                    data-active=active.to_string()
                                                                    data-testid=testid
                                                                    on:click=move |_| {
                                                                        apply_feedback_result(
                                                                            set_command_feedback,
                                                                            apply_text_align(editor, alignment),
                                                                        );
                                                                        set_more_menu_open.set(false);
                                                                    }
                                                                >
                                                                    <strong>{label}</strong>
                                                                    <span>"按官方浮动工具条对当前块对齐"</span>
                                                                </button>
                                                            }
                                                        }).collect_view()}
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            }.into_any()
                        } else {
                            ().into_any()
                        }
                    }}

                    {move || {
                        if let Some(indicator) = drop_indicator.get() {
                            let drop_indicator_left = editor_stage_element()
                                .map(|stage| {
                                    let stage_rect = stage.get_bounding_client_rect();
                                    format!("{}px", content_column_left(stage_rect.width()))
                                })
                                .unwrap_or_else(|| "56px".to_string());
                            let drop_indicator_right = editor_stage_element()
                                .map(|stage| {
                                    let stage_rect = stage.get_bounding_client_rect();
                                    let content_left = content_column_left(stage_rect.width());
                                    format!("{}px", content_left)
                                })
                                .unwrap_or_else(|| "56px".to_string());
                            view! {
                                <div
                                    class="block-drop-indicator"
                                    data-testid="block-drop-indicator"
                                    style:top=format!("{}px", indicator.top)
                                    style:left=drop_indicator_left
                                    style:right=drop_indicator_right
                                ></div>
                            }.into_any()
                        } else {
                            ().into_any()
                        }
                    }}

                    {move || {
                        if block_keyboard_mode.get()
                            && editor_focused.get()
                            && !text_selection_active.get()
                            && !slash_open.get()
                            && !block_menu_open.get()
                        {
                            if let Some(block) = hovered_block.get().or_else(hovered_block_from_selection) {
                                let outline_top = format!("{}px", (block.top - 2.0).max(0.0));
                                let outline_height = format!("{}px", (block.height + 4.0).max(30.0));
                                return view! {
                                    <div
                                        class="block-layout-outline"
                                        data-testid="block-layout-outline"
                                        style:top=outline_top
                                        style:min-height=outline_height
                                    ></div>
                                }.into_any();
                            }
                        }
                        ().into_any()
                    }}

                    {move || {
                        let active_block = if block_menu_open.get() {
                            block_menu_anchor.get()
                        } else if dragging_block_index.get().is_some() {
                            dragging_block_anchor.get()
                        } else {
                            hovered_block.get().or_else(|| {
                                if block_keyboard_mode.get()
                                    && editor_focused.get()
                                    && !text_selection_active.get()
                                    && !slash_open.get()
                                {
                                    hovered_block_from_selection()
                                } else {
                                    None
                                }
                            })
                        };

                        if let Some(block) = active_block {
                            let dragging_index = dragging_block_index.get();
                            let is_dragging = dragging_index
                                .map(|active| active == block.index)
                                .unwrap_or(false);

                            if !editor_focused.get()
                                || text_selection_active.get()
                                || slash_open.get()
                                || (dragging_index.is_some() && !is_dragging)
                            {
                                return ().into_any();
                            }

                            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();
                                    format!("{}px", handle_lane_left(stage_rect.width()))
                                })
                                .unwrap_or_else(|| "12px".to_string());
                            let block_index = block.index;
                            let block_label = block.label.clone();
                            let click_block_label = block_label.clone();
                            let duplicate_block_label = block_label.clone();
                            let delete_block_label = block_label.clone();
                            let insert_before_block_label = block_label.clone();
                            let insert_after_block_label = block_label.clone();
                            let click_anchor_block = block.clone();
                            let drag_anchor_block = block.clone();

                            view! {
                                <div
                                    class="block-handle-shell"
                                    data-testid="mnote-leptos-tiptap-handle"
                                    data-dragging=move || if is_dragging { "true" } else { "false" }
                                    data-keyboard-selected=move || block_keyboard_mode.get().to_string()
                                    data-menu-open=move || if block_menu_open.get()
                                        && block_menu_anchor
                                            .get()
                                            .map(|current| current.index == block_index)
                                            .unwrap_or(false) { "true" } else { "false" }
                                    style:left=handle_left
                                    style:top=anchor_top
                                    style:transform=anchor_transform
                                >
                                    <button
                                        class="block-handle-insert block-handle-insert-before"
                                        data-testid="block-insert-before-trigger"
                                        aria-label="在上方插入块"
                                        title="在上方插入块 · Esc, a"
                                        on:click=move |event: MouseEvent| {
                                            event.prevent_default();
                                            event.stop_propagation();
                                            set_turn_into_open.set(false);
                                            set_block_menu_open.set(false);
                                            set_block_menu_anchor.set(None);
                                            set_block_keyboard_mode.set(false);
                                            match insert_editor_paragraph_relative_to_block(editor, block_index, true) {
                                                Ok(new_index) => {
                                                    sync_persisted_editor_command(
                                                        editor,
                                                        &runtime_persisted_identity(document_id, workspace_id),
                                                        set_dirty_count,
                                                        set_html_output,
                                                        set_document_json,
                                                        set_json_output,
                                                        title,
                                                        set_command_feedback,
                                                        format!("已在 {} 上方插入新块", insert_before_block_label),
                                                    );
                                                    set_slash_open.set(false);
                                                    set_slash_index.set(0);
                                                    set_hovered_block.set(block_state_from_index(new_index));
                                                }
                                                Err(err) => {
                                                    set_command_feedback.set(err);
                                                }
                                            }
                                        }
                                    >
                                        <span class="block-insert-line"></span>
                                        <span class="block-insert-plus">"+"</span>
                                        <span class="block-insert-tooltip">"在上方插入块" <kbd>"Esc, a"</kbd></span>
                                    </button>

                                    <button
                                        class="block-handle-trigger"
                                        data-testid="block-drag-handle-trigger"
                                        aria-label="Open block menu"
                                        title="Open block menu"
                                        on:mousedown=move |event: MouseEvent| {
                                            if event.button() != 0 {
                                                return;
                                            }
                                            event.stop_propagation();
                                            set_pending_drag.set(Some(PendingDragState {
                                                index: block_index,
                                                anchor: drag_anchor_block.clone(),
                                                start_x: event.client_x(),
                                                start_y: event.client_y(),
                                            }));
                                            set_suppress_handle_click.set(false);
                                        }
                                        on:click=move |event: MouseEvent| {
                                            event.prevent_default();
                                            event.stop_propagation();
                                            if suppress_handle_click.get_untracked() {
                                                set_suppress_handle_click.set(false);
                                                return;
                                            }
                                            let same_anchor = block_menu_anchor
                                                .get_untracked()
                                                .map(|current| current.index == block_index)
                                                .unwrap_or(false);
                                            open_block_menu_overlay(
                                                click_anchor_block.clone(),
                                                same_anchor,
                                                block_menu_open,
                                                set_slash_open,
                                                set_slash_index,
                                                set_slash_query,
                                                set_turn_into_open,
                                                set_color_menu_open,
                                                set_more_menu_open,
                                                set_locked_toolbar_anchor,
                                                set_text_selection_active,
                                                set_floating_toolbar_anchor,
                                                set_image_toolbar_anchor,
                                                set_table_toolbar_open,
                                                set_table_options_open,
                                                set_table_overlay_anchor,
                                                set_table_selection_overlay,
                                                set_block_menu_open,
                                                set_block_menu_anchor,
                                                set_block_turn_into_open,
                                                set_block_folded_title_open,
                                            );
                                            set_command_feedback.set(format!("已定位块菜单：{}", click_block_label));
                                        }
                                    >
                                        <span class="block-handle-icon block-handle-icon-dots" aria-hidden="true">
                                            <span data-testid="block-handle-dot"></span>
                                            <span data-testid="block-handle-dot"></span>
                                            <span data-testid="block-handle-dot"></span>
                                            <span data-testid="block-handle-dot"></span>
                                        </span>
                                        <span class="block-handle-icon block-handle-icon-lines" aria-hidden="true">
                                            <span data-testid="block-handle-line"></span>
                                            <span data-testid="block-handle-line"></span>
                                            <span data-testid="block-handle-line"></span>
                                        </span>
                                    </button>

                                    <button
                                        class="block-handle-insert block-handle-insert-after"
                                        data-testid="block-insert-after-trigger"
                                        aria-label="在下方插入块"
                                        title="在下方插入块 · Esc, b"
                                        on:click=move |event: MouseEvent| {
                                            event.prevent_default();
                                            event.stop_propagation();
                                            set_turn_into_open.set(false);
                                            set_block_menu_open.set(false);
                                            set_block_menu_anchor.set(None);
                                            set_block_keyboard_mode.set(false);
                                            match insert_editor_paragraph_relative_to_block(editor, block_index, false) {
                                                Ok(new_index) => {
                                                    sync_persisted_editor_command(
                                                        editor,
                                                        &runtime_persisted_identity(document_id, workspace_id),
                                                        set_dirty_count,
                                                        set_html_output,
                                                        set_document_json,
                                                        set_json_output,
                                                        title,
                                                        set_command_feedback,
                                                        format!("已在 {} 下方插入新块", insert_after_block_label),
                                                    );
                                                    set_slash_open.set(false);
                                                    set_slash_index.set(0);
                                                    set_hovered_block.set(block_state_from_index(new_index));
                                                }
                                                Err(err) => {
                                                    set_command_feedback.set(err);
                                                }
                                            }
                                        }
                                    >
                                        <span class="block-insert-line"></span>
                                        <span class="block-insert-plus">"+"</span>
                                        <span class="block-insert-tooltip">"在下方插入块" <kbd>"Esc, b"</kbd></span>
                                    </button>


                                    {move || {
                                        if block_menu_open.get()
                                            && block_menu_anchor
                                                .get()
                                                .map(|current| current.index == block_index)
                                                .unwrap_or(false)
                                        {
                                            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,
                                                left: 80.0,
                                            });
                                            block_handle_menu_view(BlockHandleMenuViewProps {
                                                editor,
                                                block: block.clone(),
                                                duplicate_block_label: duplicate_block_label.clone(),
                                                delete_block_label: delete_block_label.clone(),
                                                menu_layout,
                                                signals: BlockHandleMenuSignals {
                                                    document_id,
                                                    workspace_id,
                                                    title,
                                                    selection_state,
                                                    block_turn_into_open,
                                                    block_folded_title_open,
                                                    set_ai_bridge_state,
                                                    set_ai_bridge_message,
                                                    set_dirty_count,
                                                    set_html_output,
                                                    set_document_json,
                                                    set_json_output,
                                                    set_command_feedback,
                                                    set_block_menu_open,
                                                    set_block_menu_anchor,
                                                    set_block_turn_into_open,
                                                    set_block_folded_title_open,
                                                    set_hovered_block,
                                                },
                                            }).into_any()
                                        } else {
                                            ().into_any()
                                        }
                                    }}
                                </div>
                            }.into_any()
                        } else {
                            ().into_any()
                        }
                    }}

                    {slash_menu_view(slash_menu_props)}

                    <TiptapEditor
                        id=editor_instance_id.clone()
                        editor=editor
                        initial_content=initial_editor_content.clone()
                        placeholder="输入 “/” 打开命令菜单；试试 heading / list / todo / quote / code block / divider"
                        disabled=move || !editor_editable.get()
                        extensions=p0_extensions()
                        on_ready=move |_| {
                            if restored_from_storage {
                                let restored_content = restored_html
                                    .clone()
                                    .map(TiptapContent::html)
                                    .unwrap_or_else(|| {
                                        let restored_json = document_json.get_untracked();
                                        let payload = restored_json
                                            .get("content")
                                            .and_then(Value::as_array)
                                            .cloned()
                                            .map(Value::Array)
                                            .unwrap_or(restored_json);
                                        TiptapContent::json(payload)
                                    });
                                if let Err(err) = editor.set_content(restored_content) {
                                    set_command_feedback.set(format!("恢复本地草稿失败：{err}"));
                                    return;
                                }
                            }
                            let snapshot =
                                sync_editor_outputs(editor, set_html_output, set_document_json, set_json_output);
                            schedule_scroll_mnote_block_anchor_from_hash();
                            if let Some(target) = ready_event_target.as_ref() {
                                dispatch_ready_event_to_target(
                                    target,
                                    &ReadyPayload {
                                        runtime_name: RUNTIME_NAME,
                                        selectors: BridgeSelectorsPayload {
                                            root: "[data-testid=\"mnote-leptos-tiptap-host\"]",
                                            stage: "[data-testid=\"mnote-leptos-tiptap-editor-stage\"]",
                                            editor: "[data-testid=\"mnote-leptos-tiptap-editor-root\"]",
                                            toolbar: "[data-testid=\"mnote-leptos-tiptap-toolbar\"]",
                                            slash_menu: "[data-testid=\"mnote-leptos-tiptap-slash-menu\"]",
                                            handle: "[data-testid=\"mnote-leptos-tiptap-handle\"]",
                                        },
                                        supported_commands: vec![
                                            "replaceContent",
                                            "setEditable",
                                            "undo",
                                            "redo",
                                            "focus",
                                            "requestCurrentBlockId",
                                        ],
                                        supports_embedded_mode: true,
                                    },
                                );
                                dispatch_runtime_state_to_target(
                                    target,
                                    document_id.get_untracked(),
                                    workspace_id.get_untracked(),
                                    title.get_untracked(),
                                    dirty_count.get_untracked(),
                                    hovered_block.get_untracked(),
                                    editor_focused.get_untracked(),
                                    slash_open.get_untracked(),
                                    turn_into_open.get_untracked(),
                                    color_menu_open.get_untracked(),
                                    more_menu_open.get_untracked(),
                                    read_only.get_untracked(),
                                );
                            }
                            match persist_document_state(
                                &runtime_persisted_identity(document_id, workspace_id),
                                &title.get_untracked(),
                                &snapshot,
                                Some(html_output.get_untracked()),
                            ) {
                                Ok(()) => {
                                    if restored_from_storage {
                                        set_command_feedback.set("编辑器已准备就绪，并已恢复本地草稿".to_string());
                                    } else {
                                        set_command_feedback.set("编辑器已准备就绪，当前内容已写入本地草稿".to_string());
                                    }
                                }
                                Err(err) => {
                                    set_command_feedback.set(format!("编辑器已准备就绪，但草稿初始化失败：{err}"));
                                }
                            }
                        }
                        on_change=move |_| {
                            set_dirty_count.update(|count| *count += 1);
                            let snapshot =
                                sync_editor_outputs(editor, set_html_output, set_document_json, set_json_output);
                            if let Some(target) = change_event_target.as_ref() {
                                dispatch_change_event_to_target(
                                    target,
                                    &ChangePayload {
                                        document_id: document_id.get_untracked(),
                                        workspace_id: workspace_id.get_untracked(),
                                        title: title.get_untracked(),
                                        content: snapshot.clone(),
                                        meta: ChangeMetaPayload {
                                            dirty_count: dirty_count.get_untracked(),
                                            editor_focused: editor_focused.get_untracked(),
                                            slash_open: slash_open.get_untracked(),
                                            toolbar_open: toolbar_overlay_locked(
                                                turn_into_open.get_untracked(),
                                                color_menu_open.get_untracked(),
                                                more_menu_open.get_untracked(),
                                            ),
                                            selected_block_index: hovered_block.get_untracked().map(|block| block.index),
                                            revision: revision.get_untracked(),
                                            conflict_detection_key: conflict_detection_key.get_untracked(),
                                            read_only: read_only.get_untracked(),
                                        },
                                    },
                                );
                                dispatch_runtime_state_to_target(
                                    target,
                                    document_id.get_untracked(),
                                    workspace_id.get_untracked(),
                                    title.get_untracked(),
                                    dirty_count.get_untracked(),
                                    hovered_block.get_untracked(),
                                    editor_focused.get_untracked(),
                                    slash_open.get_untracked(),
                                    turn_into_open.get_untracked(),
                                    color_menu_open.get_untracked(),
                                    more_menu_open.get_untracked(),
                                    read_only.get_untracked(),
                                );
                            }
                            match persist_document_state(
                                &runtime_persisted_identity(document_id, workspace_id),
                                &title.get_untracked(),
                                &snapshot,
                                Some(html_output.get_untracked()),
                            ) {
                                Ok(()) => {
                                    set_command_feedback.set("内容已变更，已同步观察值并写入本地草稿".to_string());
                                }
                                Err(err) => {
                                    set_command_feedback.set(format!("内容已变更，但本地保存失败：{err}"));
                                }
                            }
                            schedule_scroll_mnote_block_anchor_from_hash();
                        }
                        on_selection_change=move |selection: TiptapSelectionState| {
                            let selection_clone = selection.clone();
                            set_selection_state.set(selection_clone.clone());
                            sync_overlays_on_selection_change(
                                turn_into_open,
                                color_menu_open,
                                more_menu_open,
                                text_selection_active,
                                set_text_selection_active,
                                set_floating_toolbar_anchor,
                                set_image_toolbar_anchor,
                                set_turn_into_open,
                                set_block_menu_open,
                                set_block_menu_anchor,
                                set_hovered_block,
                            );
                            if let Some(target) = selection_event_target.as_ref() {
                                send_selection_state_to_target(
                                    target,
                                    &selection_clone,
                                    editor_focused.get_untracked(),
                                    hovered_block.get_untracked().map(|block| block.index),
                                );
                            }
                        }
                        attr:class="editor-surface"
                        attr:data-testid="mnote-leptos-tiptap-editor-root"
                        attr:data-small-text=move || small_text.get().to_string()
                        attr:data-layout-density=move || layout_density.get()
                        attr:data-show-heading-numbers=move || show_heading_numbers.get().to_string()
                        attr:data-embed-default-block-id=move || embed_default_block_id.get().unwrap_or_default()
                    />
                </div>

                {move || {
                    if is_embedded {
                        ().into_any()
                    } else {
                        view! {
                            <div class="footer-strip">
                                <span>
                                    "当前 P0 边界："
                                    <code>"paragraph / heading1-3 / bullet / ordered / todo / quote / code block / divider"</code>
                                </span>
                                <span>
                                    "运行时扩展不再走 "
                                    <code>"TiptapExtension::all_enabled()"</code>
                                    "，这里已冻结为最小集合；当前 reload 走 "
                                    <code>"localStorage JSON"</code>
                                    "，块菜单支持复制、删除、turn into 与顶层拖拽。"
                                </span>
                            </div>
                        }.into_any()
                    }
                }}
            </section>

            {move || {
                if is_embedded {
                    ().into_any()
                } else {
                    view! {
                        <details class="debug-drawer">
                            <summary>
                                <span>"调试抽屉：保留 HTML / JSON / Selection 观测，但不再主导页面"</span>
                                <span>"展开 / 收起"</span>
                            </summary>
                            <div class="debug-grid">
                                <section class="debug-card">
                                    <h3>"Selection"</h3>
                                    <pre id="editor-selection-summary">{selection_text}</pre>
                                </section>
                                <section class="debug-card">
                                    <h3>"HTML"</h3>
                                    <pre id="editor-html-output">{move || html_output.get()}</pre>
                                </section>
                                <section class="debug-card">
                                    <h3>"JSON"</h3>
                                    <pre id="editor-json-output">{move || json_output.get()}</pre>
                                </section>
                            </div>
                        </details>
                    }.into_any()
                }
            }}
        </main>
    }
}

pub fn standalone_main() {
    console_error_panic_hook::set_once();
    mount_to_body(|| {
        view! { <App mount_options=MountOptions::default()/> }
    });
}
