use leptos::ev;
use leptos::mount::{mount_to, mount_to_body, UnmountHandle};
use leptos::prelude::*;
use leptos_dom::helpers::window_event_listener;
use leptos_tiptap::{
    TiptapCodeBlockAttributes, TiptapColorAttributes, TiptapContent, TiptapEditor,
    TiptapEditorHandle, TiptapExtension, TiptapHeadingLevel, TiptapHighlightAttributes,
    TiptapImageResource, TiptapInsertContentOptions, TiptapLinkResource, TiptapMarkName,
    TiptapNodeName, TiptapRange, TiptapSchemaTarget, TiptapSelectionState, TiptapTextAlign,
    TiptapTocNodeAttrs,
};
use send_wrapper::SendWrapper;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display};
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{
    window, CustomEvent, CustomEventInit, DragEvent, Element, Event, EventTarget, HtmlElement,
    HtmlInputElement, MouseEvent, Node, RequestInit, RequestMode, Response, WheelEvent,
};

use editor_runtime::attachment_upload::dispatch_editor_upload_request;
use editor_runtime::block_dnd::{
    direct_block_from_element, direct_block_index_from_element,
    drop_indicator_from_block_rects, element_within_handle_shell, event_target_from_point,
    pointer_in_handle_corridor_geometry, top_level_block_index, HandleCorridorGeometry,
};
use editor_runtime::block_menu_overlay;
use editor_runtime::block_menu_document::{
    collect_plain_text, mindmap_paragraph_node, paragraph_node,
};
use editor_runtime::block_menu_legacy_html::{
    duplicate_top_level_block_html, reorder_top_level_block_html,
};
use editor_runtime::block_hover_state::{
    block_label_for_element, BlockMenuLayout, DropIndicatorState, HoveredBlockState,
    PendingDragState,
};
use editor_runtime::command_sync::{
    read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command,
};
use editor_runtime::dom_events::{event_target_matches_selector, target_element};
use editor_runtime::dom_selection::{
    runtime_block_id_from_index, selection_payload, selection_summary, SelectionPayload,
};
use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus};
use editor_runtime::overlays::{
    close_editor_floating_overlays_if_escape, open_block_menu_overlay, open_image_toolbar_overlay,
    open_slash_menu_overlay, current_table_overlay_anchor, sync_overlays_on_selection_change,
    table_col_aux_style, table_overlay_anchor_from_table, table_row_aux_style,
    toolbar_overlay_locked, try_sync_editor_overlay_state, FloatingToolbarAnchor,
    ImageToolbarAnchor, TableOverlayAnchor, TableSelectionKind, TableSelectionOverlayState,
};
use editor_runtime::persistence::{
    load_persisted_document, normalize_identity_value, persist_document_state,
    persisted_document_identity, PersistedDocumentIdentity,
};
#[cfg(test)]
use editor_runtime::persistence::persisted_document_storage_key;

const EDITOR_STAGE_SELECTOR: &str = "[data-testid=\"mnote-leptos-tiptap-editor-stage\"]";
const EDITOR_ROOT_SELECTOR: &str = ".editor-surface .ProseMirror";
const HANDLE_SHELL_SELECTOR: &str = ".block-handle-shell";
const CONTENT_COLUMN_MAX_WIDTH: f64 = 708.0;
const CONTENT_COLUMN_HORIZONTAL_PADDING: f64 = 48.0;
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;
const RUNTIME_NAME: &str = "8123-leptos-tiptap-runtime";
const RUNTIME_VERSION: &str = "1.1.0";
const PROTOCOL: &str = "mnote.leptos_tiptap.bridge.v1";
const EVENT_PREFIX: &str = "mnote:leptos-tiptap-spike";
const READY_EVENT: &str = "mnote:leptos-tiptap-spike:ready";
const CHANGE_EVENT: &str = "mnote:leptos-tiptap-spike:change";
const STATE_EVENT: &str = "mnote:leptos-tiptap-spike:state";
const STATUS_EVENT: &str = "mnote:leptos-tiptap-spike:status";
const SELECTION_EVENT: &str = "mnote:leptos-tiptap-spike:selection";
const COMMAND_EVENT: &str = "mnote:leptos-tiptap-spike:command";
const BLOCK_DELTA_EVENT: &str = "mnote:editor:block-delta";
const HEIGHT_EVENT: &str = "mnote:leptos-tiptap-spike:height";
const MINDMAP_SHELL_ACTION_EVENT: &str = "mnote:mindmap-shell:action";
const MINDMAP_SHELL_PANEL_EVENT: &str = "mnote:mindmap-shell:panel";
const MINDMAP_SHELL_MINIMAP_EVENT: &str = "mnote:mindmap-shell:minimap";
const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom";
const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow";
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,
    },
];

const SPIKE_STYLE: &str = r#"
:root {
  --tt-color-text: #111827;
  --tt-color-text-gray: #6b7280;
  --tt-color-text-brown: #8b5e3c;
  --tt-color-text-orange: #c96b1f;
  --tt-color-text-yellow: #b7791f;
  --tt-color-text-green: #18794e;
  --tt-color-text-blue: #1d4ed8;
  --tt-color-text-purple: #7c3aed;
  --tt-color-text-pink: #be185d;
  --tt-color-text-red: #b91c1c;
  --tt-color-highlight-yellow: #fef3c7;
  --tt-color-highlight-green: #dcfce7;
  --tt-color-highlight-blue: #dbeafe;
  --tt-color-highlight-purple: #ede9fe;
  --tt-color-highlight-pink: #fce7f3;
  --tt-color-highlight-red: #fee2e2;
  --tt-color-highlight-orange: #ffedd5;
  --tt-color-highlight-gray: #e5e7eb;
}

.app-shell {
  max-width: 1180px;
  margin: 0 auto;
  padding: 28px 20px 72px;
}

.app-shell-embedded {
  max-width: none;
  margin: 0;
  padding: 0;
}

.app-shell-embedded .hero-bar,
.app-shell-embedded .title-input,
.app-shell-embedded .editor-subcopy,
.app-shell-embedded .footer-strip,
.app-shell-embedded .debug-drawer,
.app-shell-embedded .editor-crumbs:first-child,
.app-shell-embedded .editor-crumbs:last-child {
  display: none;
}

.app-shell-embedded .editor-card {
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  overflow: visible;
}

.app-shell-embedded .editor-topbar {
  display: none;
}

.app-shell-embedded .editor-stage {
  margin: 0;
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.hero-bar {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 18px;
  margin-bottom: 20px;
}

.hero-meta {
  display: grid;
  gap: 10px;
}

.hero-tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(15, 118, 110, 0.12);
  color: var(--accent-strong);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.hero-title {
  margin: 0;
  font-size: 40px;
  line-height: 1.04;
}

.hero-copy {
  margin: 0;
  max-width: 680px;
  color: var(--muted);
  line-height: 1.75;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-end;
}

.hero-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--ink);
  font-size: 13px;
  box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}

.hero-chip strong {
  color: var(--accent-strong);
}

.editor-card {
  position: relative;
  border: 1px solid rgba(100, 116, 139, 0.18);
  border-radius: 30px;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(254, 251, 246, 0.98) 100%);
  box-shadow: 0 28px 64px rgba(15, 23, 42, 0.08);
  overflow: hidden;
}

.editor-topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
  padding: 18px 22px 12px;
}

.editor-crumbs {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
  font-size: 13px;
}

.editor-crumbs strong {
  color: var(--ink);
}

.title-input {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-size: 48px;
  line-height: 1.05;
  font-weight: 700;
  color: #0f172a;
  padding: 8px 22px 6px;
}

.title-input::placeholder {
  color: #94a3b8;
}

.editor-subcopy {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  align-items: center;
  padding: 0 22px 18px;
  color: var(--muted);
  font-size: 14px;
}

.hint-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 999px;
  background: rgba(15, 118, 110, 0.09);
  color: var(--accent-strong);
  font-size: 13px;
}

.editor-stage {
  position: relative;
  width: min(100%, 900px);
  margin: 0 auto 18px;
  border: 1px solid rgba(100, 116, 139, 0.18);
  border-radius: 26px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}

.editor-stage[data-page-wide-layout="true"] {
  width: min(100%, 1120px);
}

.mnote-mindmap-placeholder {
  width: min(1280px, calc(100vw - 340px));
  margin: 10px 0;
  margin-left: 50%;
  transform: translateX(-50%);
}

.mnote-mindmap-editor-root {
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(148, 163, 184, 0.32);
  border-radius: 8px;
  background: #ffffff;
  color: #0f172a;
}

.mnote-leptos-mindmap-shell {
  position: relative;
  height: min(72vh, 720px);
  min-height: 560px;
  display: flex;
  flex-direction: column;
}

.mnote-mindmap-command-toolbar,
.mnote-mindmap-bottom-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px;
  border-bottom: 1px solid rgba(148, 163, 184, 0.26);
  background: #ffffff;
}

.mnote-mindmap-command-toolbar {
  flex-wrap: nowrap;
}

.mnote-mindmap-schema-toolbar {
  position: absolute;
  left: 50%;
  top: 22px;
  z-index: 4;
  width: max-content;
  max-width: min(980px, calc(100% - 184px));
  box-sizing: border-box;
  transform: translateX(-50%);
  gap: 8px;
  padding: 7px 12px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.95);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.1);
  backdrop-filter: blur(10px);
  overflow-x: visible;
  overflow-y: visible;
  scrollbar-width: none;
}

.mnote-mindmap-toolbar-primary,
.mnote-mindmap-toolbar-secondary {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
  flex-wrap: nowrap;
  min-width: 0;
}

.mnote-mindmap-toolbar-cluster {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
  min-width: 0;
  white-space: nowrap;
}

.mnote-mindmap-toolbar-cluster-file {
  padding-left: 8px;
  border-left: 1px solid rgba(148, 163, 184, 0.24);
}

.mnote-mindmap-toolbar-group {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding-right: 10px;
  border-right: 1px solid rgba(148, 163, 184, 0.22);
}

.mnote-mindmap-toolbar-group:last-child {
  border-right: 0;
}

.mnote-mindmap-toolbar-group-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  color: #64748b;
  font-size: 11px;
}

.mnote-mindmap-toolbar-primary input,
.mnote-mindmap-bottom-bar input {
  height: 28px;
  border: 1px solid rgba(148, 163, 184, 0.42);
  border-radius: 6px;
  padding: 0 8px;
  background: #fff;
  color: #0f172a;
  font-size: 12px;
}

.mnote-mindmap-tool,
.mnote-mindmap-bottom-bar button,
.mnote-mindmap-side-tab {
  height: 28px;
  border: 1px solid rgba(148, 163, 184, 0.34);
  border-radius: 6px;
  padding: 0 9px;
  background: #fff;
  color: #334155;
  font-size: 12px;
  cursor: pointer;
}

.mnote-mindmap-schema-toolbar .mnote-mindmap-tool {
  flex-direction: column;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 46px;
  height: 40px;
  min-width: 46px;
  padding: 3px 4px;
  color: #475569;
}

.mnote-mindmap-tool-icon {
  min-width: 16px;
  font-size: 15px;
  line-height: 1;
}

.mnote-mindmap-tool-label {
  max-width: 42px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  line-height: 14px;
}

.mnote-mindmap-tool:disabled,
.mnote-mindmap-bottom-bar button:disabled,
.mnote-mindmap-side-tab:disabled {
  cursor: default;
  opacity: 0.48;
}

.mnote-mindmap-toolbar-more {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
}

.mnote-mindmap-toolbar-more-button[data-open="true"] {
  border-color: rgba(37, 99, 235, 0.42);
  background: #eff6ff;
  color: #1d4ed8;
}

.mnote-mindmap-toolbar-more-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 8;
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px;
  min-width: 128px;
  padding: 8px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  border-radius: 6px;
  background: #ffffff;
  box-shadow: 0 16px 36px rgba(15, 23, 42, 0.16);
}

.mnote-mindmap-toolbar-more-menu .mnote-mindmap-tool {
  flex-direction: row;
  justify-content: flex-start;
  width: 100%;
  min-width: 112px;
  height: 30px;
  gap: 8px;
  padding: 0 8px;
}

.mnote-mindmap-toolbar-more-menu .mnote-mindmap-tool-label {
  max-width: none;
  font-size: 12px;
}

.mnote-mindmap-workspace {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-height: 460px;
}

.mnote-mindmap-workspace[data-layout="floating-overlay"] {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  min-height: 560px;
  overflow: hidden;
  background: #f7f8fa;
}

.mnote-mindmap-canvas-layer {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.mnote-mindmap-overlay-layer {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.mnote-mindmap-overlay-layer > * {
  pointer-events: auto;
}

.mnote-mindmap-rust-shell-mount,
.mnote-mindmap-rust-shell {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

.mnote-mindmap-rust-shell > * {
  pointer-events: auto;
}

.mnote-mindmap-workspace[data-debug-chrome="true"] {
  grid-template-columns: minmax(0, 1fr) 132px;
}

.mnote-mindmap-workspace[data-debug-chrome="false"] {
  grid-template-columns: minmax(0, 1fr) 172px;
}

.mnote-leptos-mindmap-runtime {
  position: relative;
  height: 460px;
  min-height: 460px;
  overflow: hidden;
  background: #ffffff;
}

.mnote-mindmap-workspace[data-layout="floating-overlay"] .mnote-leptos-mindmap-runtime {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-height: 100%;
  background: #f7f8fa;
}

.mnote-leptos-mindmap-runtime svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: auto;
}

.mnote-leptos-mindmap-runtime .smm-node {
  cursor: default;
}

.mnote-mindmap-side-panel {
  border-left: 1px solid rgba(148, 163, 184, 0.26);
  background: #fff;
  padding: 8px;
}

.mnote-mindmap-schema-sidebar {
  position: absolute;
  right: 18px;
  top: 50%;
  z-index: 4;
  width: auto;
  max-height: calc(100% - 130px);
  transform: translateY(-50%);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  backdrop-filter: none;
  padding: 0;
}

.mnote-mindmap-side-rail {
  width: 60px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 8px 8px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.1);
  backdrop-filter: blur(10px);
}

.mnote-mindmap-side-rail-handle,
.mnote-mindmap-side-restore-handle,
.mnote-mindmap-side-drawer-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(148, 163, 184, 0.24);
  background: rgba(248, 250, 252, 0.98);
  color: #475569;
}

.mnote-mindmap-side-rail-handle {
  width: 100%;
  min-height: 24px;
  border-radius: 8px;
}

.mnote-mindmap-side-restore-handle {
  width: 22px;
  min-height: 72px;
  margin-top: 64px;
  border-radius: 999px;
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.1);
}

.mnote-mindmap-side-rail-handle:hover,
.mnote-mindmap-side-restore-handle:hover,
.mnote-mindmap-side-drawer-close:hover {
  border-color: rgba(37, 99, 235, 0.36);
  background: #eff6ff;
  color: #1d4ed8;
}

.mnote-mindmap-side-tab {
  width: 100%;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  justify-content: center;
  margin-bottom: 0;
  min-height: 56px;
  padding: 8px 2px;
  text-align: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #475569;
  position: relative;
}

.mnote-mindmap-side-icon {
  width: 100%;
  overflow: hidden;
  color: #64748b;
  font-size: 13px;
  text-overflow: ellipsis;
}

.mnote-mindmap-side-tab-label {
  font-size: 11px;
  line-height: 1.2;
}

.mnote-mindmap-side-tab[data-active="true"] {
  background: #eff6ff;
  color: #1d4ed8;
}

.mnote-mindmap-side-tab[data-active="true"]::before {
  content: "";
  position: absolute;
  left: -8px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 999px;
  background: #2563eb;
}

.mnote-mindmap-side-body {
  position: absolute;
  right: 72px;
  top: 0;
  width: 300px;
  margin-top: 0;
  color: #64748b;
  font-size: 12px;
  padding: 16px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.1);
  backdrop-filter: blur(10px);
}

.mnote-mindmap-schema-sidebar .mnote-mindmap-side-body {
  padding-top: 16px;
}

.mnote-mindmap-side-body strong,
.mnote-mindmap-side-body span {
  display: block;
}

.mnote-mindmap-side-drawer-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.mnote-mindmap-side-drawer-heading {
  min-width: 0;
  flex: 1;
}

.mnote-mindmap-side-drawer-heading strong {
  color: #0f172a;
  font-size: 15px;
  line-height: 1.3;
}

.mnote-mindmap-side-drawer-heading span {
  margin-top: 4px;
  color: #64748b;
  font-size: 12px;
}

.mnote-mindmap-side-drawer-close {
  width: 28px;
  min-width: 28px;
  min-height: 28px;
  border-radius: 8px;
}

.mnote-mindmap-side-swatches {
  display: flex;
  gap: 5px;
  margin-top: 8px;
}

.mnote-mindmap-side-swatches i {
  width: 18px;
  height: 18px;
  border: 1px solid rgba(148, 163, 184, 0.38);
  border-radius: 4px;
  background: #f8fafc;
}

.mnote-mindmap-side-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-top: 16px;
}

.mnote-mindmap-side-option {
  min-height: 40px;
  padding: 10px;
  border: 1px solid rgba(148, 163, 184, 0.28);
  border-radius: 8px;
  background: #f8fafc;
  color: #334155;
  font-size: 12px;
  line-height: 1.3;
  text-align: left;
}

.mnote-mindmap-side-option:hover {
  border-color: rgba(37, 99, 235, 0.38);
  background: #eff6ff;
  color: #1d4ed8;
}

.mnote-mindmap-side-option-readonly {
  background: #fff;
}

.mnote-mindmap-side-option-label {
  color: inherit;
  font-weight: 500;
}

.mnote-mindmap-side-option-description {
  margin-top: 4px;
  color: #64748b;
  font-size: 11px;
}

.mnote-mindmap-side-option-swatch {
  display: flex;
  align-items: center;
  gap: 10px;
}

.mnote-mindmap-side-option-swatch-chip {
  width: 18px;
  height: 18px;
  min-width: 18px;
  border: 1px solid rgba(148, 163, 184, 0.28);
  border-radius: 999px;
}

.mnote-mindmap-side-option-layout-card {
  display: flex;
  min-height: 86px;
  flex-direction: column;
  justify-content: flex-start;
  gap: 8px;
}

.mnote-mindmap-side-option-preview-card {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 38px;
  border-radius: 6px;
  background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
  color: #1d4ed8;
  font-size: 11px;
  line-height: 1;
}

.mnote-mindmap-side-outline {
  max-height: 172px;
  margin: 16px 0 0;
  padding-left: 14px;
  overflow: auto;
  color: #334155;
  font-size: 11px;
  line-height: 1.45;
}

.mnote-mindmap-side-outline:empty {
  display: none;
}

.mnote-mindmap-side-swatches i:nth-child(2) {
  background: #eef2ff;
}

.mnote-mindmap-side-swatches i:nth-child(3) {
  background: #ecfeff;
}

.mnote-mindmap-bottom-bar {
  border-top: 1px solid rgba(148, 163, 184, 0.26);
  border-bottom: 0;
  font-size: 12px;
  color: #475569;
}

.mnote-mindmap-schema-navigator {
  position: absolute;
  right: 28px;
  bottom: 22px;
  z-index: 4;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-content: flex-end;
  min-height: 40px;
  padding: 6px 8px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
  backdrop-filter: blur(10px);
}

.mnote-mindmap-schema-navigator [data-testid="mindmap-schema-navigator-stats"] {
  margin-right: auto;
}

.mnote-mindmap-schema-navigator-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.mnote-mindmap-schema-navigator-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  min-width: 30px;
  padding: 0;
}

.mnote-mindmap-schema-navigator-button[data-active="true"] {
  border-color: rgba(37, 99, 235, 0.32);
  background: rgba(219, 234, 254, 0.94);
  color: #1d4ed8;
}

.mnote-mindmap-schema-navigator-zoom {
  width: 62px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.mnote-mindmap-schema-navigator-search-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.mnote-mindmap-schema-navigator-search-wrap input {
  width: 136px;
}

.mnote-mindmap-minimap {
  position: absolute;
  right: 28px;
  bottom: 76px;
  z-index: 4;
  width: 180px;
  height: 118px;
  border: 1px solid rgba(15, 23, 42, 0.12);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 2px 16px rgba(15, 23, 42, 0.08);
  pointer-events: auto;
}

.mnote-mindmap-minimap-map {
  position: absolute;
  inset: 12px;
}

.mnote-mindmap-minimap-map span {
  position: absolute;
  display: block;
  width: 42px;
  height: 14px;
  border-radius: 4px;
  background: #dbeafe;
}

.mnote-mindmap-minimap-map span:nth-child(1) {
  left: 54px;
  top: 8px;
  background: #bfdbfe;
}

.mnote-mindmap-minimap-map span:nth-child(2) {
  left: 22px;
  top: 52px;
  background: #dcfce7;
}

.mnote-mindmap-minimap-map span:nth-child(3) {
  right: 22px;
  top: 52px;
  background: #fde68a;
}

.mnote-mindmap-minimap-viewport {
  position: absolute;
  left: 38px;
  top: 30px;
  width: 104px;
  height: 62px;
  border: 2px solid rgba(37, 99, 235, 0.75);
  border-radius: 5px;
  background: rgba(37, 99, 235, 0.08);
}

.mnote-mindmap-context-menu {
  position: absolute;
  z-index: 8;
  min-width: 132px;
  padding: 6px;
  border: 1px solid rgba(15, 23, 42, 0.12);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.14);
  pointer-events: auto;
}

.mnote-mindmap-context-menu button {
  display: block;
  width: 100%;
  min-height: 26px;
  padding: 4px 8px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: #334155;
  font-size: 12px;
  text-align: left;
}

.mnote-mindmap-context-menu button:hover {
  background: #eff6ff;
  color: #1d4ed8;
}

.mnote-mindmap-count {
  position: absolute;
  left: 28px;
  bottom: 30px;
  z-index: 4;
  display: flex;
  gap: 18px;
  color: #64748b;
  font-size: 12px;
  pointer-events: auto;
}

.block-handle-shell {
  position: absolute;
  left: 12px;
  z-index: 80;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  width: 26px;
  padding: 0;
  background: transparent;
  pointer-events: none;
}

.block-handle-shell[data-dragging="true"] {
  pointer-events: none;
  opacity: 1;
}

.block-handle-insert,
.block-handle-trigger {
  position: relative;
  width: 22px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: #6b7280;
  cursor: pointer;
  pointer-events: auto;
  transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, color .12s ease, opacity .12s ease;
}

.block-handle-insert {
  opacity: 0;
}

.block-handle-shell[data-menu-open="true"] .block-handle-insert {
  opacity: 0;
  pointer-events: none;
}

.block-handle-trigger {
  height: 22px;
  border: 1px solid rgba(148, 163, 184, 0.42);
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.1);
  cursor: grab;
}

.block-handle-trigger:hover,
.block-handle-shell[data-keyboard-selected="true"] .block-handle-trigger,
.block-handle-shell[data-menu-open="true"] .block-handle-trigger {
  border-color: rgba(107, 114, 128, 0.5);
  background: #f8fafc;
  color: #4b5563;
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.14);
}

.block-handle-trigger:active {
  cursor: grabbing;
}

.block-handle-icon-dots {
  display: grid;
  grid-template-columns: repeat(2, 3px);
  grid-auto-rows: 3px;
  gap: 3px;
}

.block-handle-icon-dots span,
.block-handle-icon-lines span {
  width: 3px;
  height: 3px;
  border-radius: 999px;
  background: currentColor;
}

.block-handle-icon-lines {
  display: grid;
  gap: 3px;
}

.block-handle-icon-lines span {
  display: none;
  width: 12px;
  height: 1.5px;
  border-radius: 999px;
}

.block-handle-shell[data-menu-open="true"] .block-handle-icon-dots span {
  display: none;
}

.block-handle-shell[data-menu-open="true"] .block-handle-icon-lines span {
  display: block;
}

.block-insert-line {
  display: none;
}

.block-insert-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 1px solid rgba(148, 163, 184, 0.42);
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.96);
  color: #5f6773;
  font-size: 16px;
  line-height: 1;
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.1);
}

.block-handle-insert:hover,
.block-handle-insert:focus-visible {
  opacity: 1;
}

.block-handle-insert:hover .block-insert-plus,
.block-handle-insert:focus-visible .block-insert-plus {
  border-color: rgba(107, 114, 128, 0.5);
  background: #f8fafc;
  color: #4b5563;
}

.block-insert-tooltip {
  position: absolute;
  left: 26px;
  top: 50%;
  transform: translateY(-50%);
  min-width: 118px;
  display: none;
  gap: 5px;
  align-items: center;
  padding: 5px 7px;
  border-radius: 5px;
  background: rgba(17, 24, 39, 0.94);
  color: #fff;
  font-size: 12px;
  line-height: 1.2;
  white-space: nowrap;
  box-shadow: 0 8px 22px rgba(15, 23, 42, 0.2);
}

.block-insert-tooltip kbd {
  color: rgba(255, 255, 255, 0.72);
  font: inherit;
}

.block-handle-insert:hover .block-insert-tooltip,
.block-handle-insert:focus-visible .block-insert-tooltip {
  display: inline-flex;
}

.block-drag-menu {
  position: fixed;
  top: 0;
  left: 0;
  width: 242px;
  min-width: 242px;
  box-sizing: border-box;
  display: grid;
  gap: 0;
  padding: 10px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  background-image: linear-gradient(#fff, #fff);
  color: var(--ink);
  box-shadow: 0 18px 42px rgba(15, 23, 42, 0.18);
  z-index: 120;
  pointer-events: auto;
  isolation: isolate;
  overflow: visible;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}


.block-drag-menu::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: #fff;
}

.block-drag-menu > * {
  position: relative;
  z-index: 1;
}


.block-drag-menu-label,
.block-drag-menu-actions,
.block-drag-menu-section,
.block-drag-menu-item {
  background-color: #fff;
}

.block-drag-command-input {
  width: 100%;
  height: 34px;
  padding: 0 10px;
  border: 1px solid rgba(148, 163, 184, 0.24);
  border-radius: 5px;
  background: #fff;
  color: #9ca3af;
  font: inherit;
}

.block-drag-menu-label {
  display: grid;
  gap: 4px;
}

.block-drag-menu-label strong {
  font-size: 14px;
}

.block-drag-menu-label span {
  color: var(--muted);
  font-size: 12px;
}

.block-drag-menu-shortcut,
.block-drag-menu-arrow {
  margin-left: auto;
  color: #a1a1aa;
  font-size: 13px;
  white-space: nowrap;
}

.block-drag-menu-icon {
  min-width: 22px;
  color: #60646c;
  font-size: 16px;
  line-height: 1;
  text-align: center;
}

.block-drag-menu-switch {
  margin-left: auto;
  width: 28px;
  height: 16px;
  border-radius: 999px;
  background: #d4d4d8;
  position: relative;
}

.block-drag-menu-switch::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  left: 2px;
  top: 2px;
  border-radius: 999px;
  background: #fff;
}

.block-drag-submenu {
  position: absolute;
  left: calc(100% + 8px);
  top: 0;
  width: 206px;
  max-height: min(620px, 80vh);
  overflow-y: auto;
  display: grid;
  gap: 0;
  padding: 6px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 18px 42px rgba(15, 23, 42, 0.18);
  z-index: 130;
  pointer-events: auto;
}

.block-drag-tertiary-submenu {
  position: absolute;
  left: calc(100% + 8px);
  top: 0;
  width: 206px;
  display: grid;
  gap: 0;
  padding: 6px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 18px 42px rgba(15, 23, 42, 0.18);
  z-index: 140;
  pointer-events: auto;
}

.block-drag-menu-item[disabled] {
  color: #a1a1aa;
  cursor: default;
}

.block-drag-menu-item[disabled]:hover {
  background: transparent;
}

.block-drag-menu-back {
  width: 100%;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: none;
  background: transparent;
  color: var(--muted);
  padding: 2px 2px 6px;
  cursor: pointer;
}

.block-drag-menu-back:hover {
  color: var(--accent-strong);
}

.block-drag-menu-actions {
  display: grid;
  gap: 0;
}

.block-drag-menu-section {
  display: grid;
  gap: 0;
}

.block-drag-menu-divider {
  height: 1px;
  margin: 4px -10px;
  background: rgba(148, 163, 184, 0.22);
}

.block-drag-menu-item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  text-align: left;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  min-height: 31px;
  padding: 5px 8px;
  white-space: nowrap;
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}

.block-drag-menu-item:hover {
  background: rgba(17, 24, 39, 0.06);
  border-color: transparent;
}

.block-drag-menu-item > span:not(.block-drag-menu-icon):not(.block-drag-menu-shortcut):not(.block-drag-menu-arrow):not(.block-drag-menu-switch) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.block-drag-menu-footer {
  display: grid;
  gap: 2px;
  margin: 6px -2px -2px;
  padding-top: 8px;
  border-top: 1px solid rgba(148, 163, 184, 0.18);
  color: #a1a1aa;
  font-size: 12px;
  line-height: 1.35;
}


.block-drop-indicator {
  position: absolute;
  left: 56px;
  right: 34px;
  height: 2px;
  border-radius: 999px;
  background: rgba(15, 118, 110, 0.78);
  box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.12);
  z-index: 13;
}

.block-layout-outline {
  position: absolute;
  left: 56px;
  right: 34px;
  min-height: 30px;
  border: 1px dashed rgba(148, 163, 184, 0.95);
  border-radius: 4px;
  background: rgba(255, 71, 71, 0.06);
  pointer-events: none;
  z-index: 12;
}

.floating-toolbar {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, calc(-100% - 14px));
  z-index: 12;
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: nowrap;
  max-width: min(720px, calc(100% - 32px));
  padding: 3px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.98);
  color: #0f172a;
  box-shadow: 0 14px 34px rgba(15, 23, 42, 0.14);
  backdrop-filter: blur(16px);
}

.floating-toolbar-group {
  display: flex;
  align-items: center;
  gap: 2px;
}

.image-floating-toolbar {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, calc(-100% - 12px));
  z-index: 122;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.98);
  color: #111827;
  box-shadow: 0 14px 34px rgba(15, 23, 42, 0.16);
  backdrop-filter: blur(16px);
  pointer-events: auto;
}

.image-toolbar-btn {
  min-width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.image-toolbar-btn:hover,
.image-toolbar-btn:focus-visible,
.image-toolbar-btn[data-active="true"] {
  background: rgba(255, 229, 229, 0.95);
  outline: none;
}

.image-toolbar-btn:disabled {
  color: #a1a1aa;
  cursor: not-allowed;
  opacity: 0.55;
  background: transparent;
}

.image-toolbar-separator {
  width: 1px;
  align-self: stretch;
  min-height: 22px;
  background: rgba(148, 163, 184, 0.3);
}

.toolbar-popover-anchor {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.toolbar-separator {
  width: 1px;
  align-self: stretch;
  margin: 4px 3px;
  background: rgba(148, 163, 184, 0.28);
}

.toolbar-btn {
  min-width: 30px;
  height: 30px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  padding: 0 10px;
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease, transform .12s ease;
  font-size: 14px;
  font-weight: 600;
}

.toolbar-btn:hover {
  background: rgba(148, 163, 184, 0.16);
}

.toolbar-btn[data-active="true"] {
  background: rgba(15, 118, 110, 0.14);
  color: var(--accent-strong);
}

.toolbar-btn[data-wide="true"] {
  min-width: 56px;
  justify-content: space-between;
}

.turn-into-panel {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 220px;
  max-height: min(420px, calc(100vh - 32px));
  display: grid;
  gap: 6px;
  padding: 10px;
  border-radius: 18px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  background: rgba(255, 255, 255, 0.98);
  color: var(--ink);
  box-shadow: 0 20px 44px rgba(15, 23, 42, 0.16);
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 2;
}

.toolbar-popover-panel {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 248px;
  max-height: min(440px, calc(100vh - 32px));
  display: grid;
  gap: 10px;
  padding: 12px;
  border-radius: 18px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  background: rgba(255, 255, 255, 0.98);
  color: var(--ink);
  box-shadow: 0 20px 44px rgba(15, 23, 42, 0.16);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  z-index: 2;
}

.turn-into-item,
.slash-item,
.toolbar-menu-item {
  width: 100%;
  display: grid;
  gap: 4px;
  text-align: left;
  border: 1px solid transparent;
  border-radius: 14px;
  background: transparent;
  color: inherit;
  padding: 10px 12px;
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}

.turn-into-item:hover,
.slash-item:hover,
.slash-item[data-active="true"],
.toolbar-menu-item:hover,
.toolbar-menu-item[data-active="true"] {
  background: rgba(15, 118, 110, 0.08);
  border-color: rgba(15, 118, 110, 0.18);
}

.turn-into-item > span,
.slash-item-copy > span,
.toolbar-menu-item > span {
  color: var(--muted);
  font-size: 12px;
}

.slash-item-copy strong {
  color: #30343b;
  font-size: 14px;
  font-weight: 500;
}

.toolbar-popover-section {
  display: grid;
  gap: 8px;
}

.toolbar-popover-title {
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.toolbar-color-grid {
  display: grid;
  gap: 6px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.toolbar-color-item {
  display: flex;
  align-items: center;
  gap: 10px;
}

.toolbar-color-chip {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  border-radius: 999px;
  border: 1px solid rgba(15, 23, 42, 0.14);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
}

.toolbar-more-list {
  display: grid;
  gap: 6px;
}

.toolbar-more-item strong {
  font-size: 14px;
}

.slash-menu {
  position: fixed;
  top: 24px;
  left: 24px;
  z-index: 130;
  width: min(316px, calc(100vw - 16px));
  max-height: min(430px, calc(100vh - 16px));
  display: grid;
  gap: 0;
  padding: 8px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 16px 38px rgba(15, 23, 42, 0.18);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.slash-list {
  display: grid;
  gap: 2px;
}

.slash-section-label {
  padding: 10px 6px 5px;
  color: #9097a3;
  font-size: 12px;
}

.slash-item-row {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
}

.slash-item-icon {
  color: #6b7280;
  font-size: 14px;
}

.slash-item-copy {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.slash-item-shortcut {
  color: #a0a7b2;
  font-size: 12px;
}

.editor-surface {
  display: block;
  width: 100%;
  min-height: 580px;
  padding: 76px 0 32px;
}

.app-shell-embedded .editor-surface {
  min-height: 640px;
  padding: 18px 0 28vh;
}

.editor-surface .ProseMirror {
  outline: none;
  min-height: 472px;
  line-height: 1.78;
  font-size: 17px;
  color: #1f2937;
}

.editor-surface .ProseMirror img[src] {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 8px 0;
  border-radius: 6px;
}

.editor-surface .ProseMirror img[data-align="center"] {
  margin-left: auto;
  margin-right: auto;
}

.editor-surface .ProseMirror img[data-align="right"] {
  margin-left: auto;
  margin-right: 0;
}

.editor-surface .ProseMirror img.ProseMirror-selectednode {
  outline: 2px solid rgba(255, 71, 71, 0.78);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px rgba(255, 71, 71, 0.12);
}

.editor-surface .ProseMirror .tiptap-table-of-contents-node {
  display: block;
  margin: 12px 0;
  padding: 10px 12px;
  border: 1px solid rgba(229, 231, 235, 0.95);
  border-radius: 6px;
  background: #fff;
  color: #374151;
}

.editor-surface .ProseMirror .tiptap-table-of-contents-node.ProseMirror-selectednode {
  outline: 2px solid rgba(255, 71, 71, 0.62);
  outline-offset: 2px;
}

.editor-surface .ProseMirror [data-block-id]:target {
  outline: 2px solid #2563eb;
  outline-offset: 3px;
  background: rgba(37, 99, 235, 0.08);
  scroll-margin-top: 72px;
  transition: background 0.18s ease, outline-color 0.18s ease;
}

.tiptap-table-of-contents-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 24px;
  gap: 8px;
}

.tiptap-table-of-contents-title {
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
  color: #4b5563;
}

.tiptap-table-of-contents-title-toggle {
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: #9ca3af;
  font-size: 12px;
  cursor: pointer;
}

.tiptap-table-of-contents-title-toggle:hover {
  background: rgba(17, 24, 39, 0.06);
  color: #4b5563;
}

.tiptap-table-of-contents-list {
  display: grid;
  gap: 2px;
  margin-top: 6px;
}

.tiptap-table-of-contents-item {
  display: block;
  min-height: 24px;
  padding: 2px 6px;
  border-radius: 4px;
  color: #6b7280;
  font-size: 14px;
  line-height: 20px;
  text-decoration: none;
}

.tiptap-table-of-contents-item[data-depth="2"] { padding-left: 20px; }
.tiptap-table-of-contents-item[data-depth="3"] { padding-left: 34px; }
.tiptap-table-of-contents-item[data-depth="4"] { padding-left: 48px; }
.tiptap-table-of-contents-item[data-depth="5"] { padding-left: 62px; }
.tiptap-table-of-contents-item[data-depth="6"] { padding-left: 76px; }

.tiptap-table-of-contents-item:hover {
  background: rgba(17, 24, 39, 0.05);
  color: #374151;
}

.tiptap-table-of-contents-empty {
  margin-top: 4px;
  color: #9ca3af;
  font-size: 14px;
  line-height: 22px;
}

.editor-surface .ProseMirror .tableWrapper {
  width: 100%;
  overflow-x: auto;
  margin: 8px 0;
}

.editor-surface .ProseMirror table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  margin: 8px 0;
}

.editor-surface .ProseMirror .tableWrapper table {
  margin: 0;
}

.editor-surface .ProseMirror td,
.editor-surface .ProseMirror th {
  position: relative;
  min-width: 84px;
  height: 32px;
  padding: 5px 8px;
  border: 1px solid rgba(148, 163, 184, 0.72);
  vertical-align: top;
}

.editor-surface .ProseMirror th {
  background: rgba(248, 250, 252, 0.96);
  font-weight: 600;
}

.editor-surface .ProseMirror table[data-hidden-borders="true"] td,
.editor-surface .ProseMirror table[data-hidden-borders="true"] th {
  border-width: 0;
}

.editor-surface .ProseMirror .selectedCell::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(59, 130, 246, 0.12);
  pointer-events: none;
}

.editor-surface .ProseMirror .column-resize-handle {
  position: absolute;
  top: 0;
  right: -4px;
  bottom: -1px;
  width: 8px;
  background: rgba(239, 68, 68, 0.34);
  cursor: col-resize;
  pointer-events: none;
  z-index: 4;
}

.editor-surface .ProseMirror .column-resize-dragging {
  background: rgba(255, 71, 71, 0.08);
}

.editor-surface .ProseMirror.resize-cursor,
.editor-surface .ProseMirror.resize-cursor * {
  cursor: col-resize !important;
}

.table-controls {
  position: absolute;
  z-index: 118;
  pointer-events: none;
}

.table-aux-btn {
  position: absolute;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: rgba(148, 163, 184, 0.95);
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
}

.table-aux-btn:hover,
.table-aux-btn:focus-visible,
.table-aux-btn[data-active="true"] {
  background: rgba(255, 229, 229, 0.95);
  color: rgba(107, 114, 128, 0.96);
  outline: none;
}

.table-selection-overlay {
  position: absolute;
  border-radius: 4px;
  background: rgba(255, 71, 71, 0.12);
  outline: 1px solid rgba(255, 71, 71, 0.18);
  pointer-events: none;
}

.table-toolbar {
  position: absolute;
  top: 34px;
  right: 44px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  color: #111827;
  box-shadow: 0 14px 34px rgba(15, 23, 42, 0.16);
  z-index: 120;
  pointer-events: auto;
}

.table-toolbar-btn {
  min-width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: #111827;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.table-toolbar-btn:hover,
.table-toolbar-btn:focus-visible {
  background: rgba(241, 245, 249, 0.95);
  outline: none;
}

.table-toolbar-separator {
  align-self: stretch;
  width: 1px;
  min-height: 22px;
  background: rgba(148, 163, 184, 0.3);
}

.table-options-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 160px;
  display: grid;
  gap: 2px;
  padding: 6px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 8px;
  background: #fff;
  color: #111827;
  box-shadow: 0 16px 36px rgba(15, 23, 42, 0.16);
  z-index: 125;
}

.table-option-btn {
  width: 100%;
  min-height: 30px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 5px 8px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: #111827;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-align: left;
}

.table-option-btn:hover,
.table-option-btn:focus-visible {
  background: rgba(241, 245, 249, 0.95);
  outline: none;
}

.table-option-switch {
  position: relative;
  width: 28px;
  height: 16px;
  flex: 0 0 auto;
  border-radius: 999px;
  background: #d1d5db;
  transition: background .12s ease;
}

.table-option-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.2);
  transition: transform .12s ease;
}

.table-option-btn[aria-checked="true"] .table-option-switch {
  background: #9ca3af;
}

.table-option-btn[aria-checked="true"] .table-option-switch::after {
  transform: translateX(12px);
}

.app-shell-embedded .editor-surface .ProseMirror {
  min-height: 600px;
  width: min(100%, 708px);
  margin: 0 auto;
  padding: 2.25rem 3rem 30vh;
  font-family: "DM Sans", "Noto Sans SC", "PingFang SC", sans-serif;
}

.app-shell-embedded .editor-surface[data-small-text="true"] .ProseMirror,
.editor-surface[data-small-text="true"] .ProseMirror {
  font-size: 15px;
  line-height: 1.66;
}

.app-shell-embedded .editor-surface[data-layout-density="compact"] .ProseMirror,
.editor-surface[data-layout-density="compact"] .ProseMirror {
  line-height: 1.54;
}

.app-shell-embedded .editor-surface[data-layout-density="spacious"] .ProseMirror,
.editor-surface[data-layout-density="spacious"] .ProseMirror {
  line-height: 1.92;
}

.editor-surface .ProseMirror h1,
.editor-surface .ProseMirror h2,
.editor-surface .ProseMirror h3,
.editor-surface .ProseMirror h4 {
  line-height: 1.12;
  color: #0f172a;
  margin: 1.12em 0 0.46em;
  letter-spacing: -0.02em;
}

.editor-surface .ProseMirror h1 {
  font-size: 2.1em;
}

.editor-surface .ProseMirror h2 {
  font-size: 1.56em;
}

.editor-surface .ProseMirror h3 {
  font-size: 1.28em;
}

.editor-surface .ProseMirror h4 {
  font-size: 1.08em;
}

.editor-surface .ProseMirror ul,
.editor-surface .ProseMirror ol {
  padding-left: 1.5em;
}

.editor-surface .ProseMirror ul[data-type="taskList"] {
  list-style: none;
  padding-left: 0.25em;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li:not(:has(> p:first-child)) {
  list-style-type: none;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div > p,
.editor-surface .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div > p span {
  opacity: 0.5;
  text-decoration: line-through;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label {
  position: relative;
  padding-top: 0.375rem;
  padding-right: 0.5rem;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label span {
  display: block;
  width: 1em;
  height: 1em;
  border: 1px solid rgba(148, 163, 184, 0.7);
  border-radius: 0.25rem;
  position: relative;
  cursor: pointer;
  background-color: rgba(255, 255, 255, 0.96);
  transition:
    background-color 80ms ease-out,
    border-color 80ms ease-out;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label span::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 0.75em;
  height: 0.75em;
  background-color: #ffffff;
  opacity: 0;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E") center/contain no-repeat;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label input[type="checkbox"]:checked + span {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

.editor-surface .ProseMirror ul[data-type="taskList"] li label input[type="checkbox"]:checked + span::before {
  opacity: 1;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li > div {
  flex: 1 1 0%;
  min-width: 0;
}

.editor-surface .ProseMirror ul[data-type="taskList"] li > div > p {
  margin: 0;
}

.editor-surface .ProseMirror pre {
  background: #14222b;
  color: #f8fafc;
  padding: 16px 18px;
  border-radius: 18px;
  overflow: auto;
}

.editor-surface .ProseMirror blockquote {
  margin: 1.1em 0;
  padding-left: 16px;
  border-left: 3px solid rgba(15, 118, 110, 0.5);
  color: #334155;
}

.editor-surface .ProseMirror hr {
  margin: 1.6em 0;
  border: none;
  border-top: 1px solid rgba(100, 116, 139, 0.28);
}

.editor-surface .ProseMirror p.is-editor-empty:first-child::before,
.editor-surface .ProseMirror p.is-empty::before {
  color: #94a3b8;
  content: attr(data-placeholder);
  float: left;
  height: 0;
  pointer-events: none;
}

.editor-surface .ProseMirror p.is-editor-empty,
.editor-surface .ProseMirror p.is-empty {
  min-height: 1.6em;
}

.editor-surface .ProseMirror p:has(> a.mnote-page-block-link) {
  width: 100%;
  margin: 0 0 8px;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(255, 71, 71, 0.12);
}

.editor-surface .ProseMirror a.mnote-page-block-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 1px 4px;
  border-radius: 4px;
  color: #37352f;
  font-weight: 600;
  line-height: 1.45;
  text-decoration: none;
  cursor: pointer;
}

.editor-surface .ProseMirror a.mnote-page-block-link::before {
  content: "";
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  border: 1.4px solid #c8c5c0;
  border-radius: 3px;
  background: linear-gradient(180deg, #ffffff 0%, #f6f4f1 100%);
  box-shadow: inset 0 -2px 0 rgba(55, 53, 47, 0.08);
}

.editor-surface .ProseMirror a.mnote-page-block-link:hover {
  color: #37352f;
  background: rgba(55, 53, 47, 0.08);
  text-decoration: none;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-row {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  max-width: 100%;
  min-height: 28px;
  margin: 1px 0;
  padding: 2px 4px;
  border: 0;
  border-radius: 4px;
  color: #37352f;
  font-weight: 500;
  line-height: 1.4;
  text-decoration: none;
  background: transparent;
  box-shadow: none;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-row::before {
  content: "";
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  border-radius: 4px;
  background: var(--attachment-icon-bg, #e9ecef);
  border: 1px solid var(--attachment-icon-border, rgba(55, 53, 47, 0.14));
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-row::after {
  content: "◉";
  flex: 0 0 auto;
  margin-left: 4px;
  color: #9ca3af;
  font-size: 11px;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-row:hover {
  background: rgba(55, 53, 47, 0.06);
  text-decoration: none;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-word {
  --attachment-icon-bg: #4f82ff;
  --attachment-icon-border: #3366d6;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-ppt {
  --attachment-icon-bg: #ea581f;
  --attachment-icon-border: #cf4817;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-sheet {
  --attachment-icon-bg: #2f9e44;
  --attachment-icon-border: #25813a;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-pdf {
  --attachment-icon-bg: #ef4444;
  --attachment-icon-border: #dc2626;
}

.editor-surface .ProseMirror a.mnote-uploaded-attachment-file {
  --attachment-icon-bg: #9ca3af;
  --attachment-icon-border: #6b7280;
}

.footer-strip {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 0 22px 22px;
  color: var(--muted);
  font-size: 13px;
}

.footer-strip code {
  color: var(--accent-strong);
}

.debug-drawer {
  margin-top: 22px;
  border: 1px solid rgba(100, 116, 139, 0.18);
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.85);
  overflow: hidden;
}

.debug-drawer summary {
  cursor: pointer;
  list-style: none;
  padding: 18px 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 600;
}

.debug-drawer summary::-webkit-details-marker {
  display: none;
}

.debug-grid {
  display: grid;
  gap: 16px;
  padding: 0 20px 20px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.debug-card {
  border: 1px solid rgba(100, 116, 139, 0.18);
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.88);
  padding: 16px;
}

.debug-card h3 {
  margin: 0 0 10px;
  font-size: 14px;
}

.debug-card pre {
  margin: 0;
  max-height: 260px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  color: #1e293b;
}

@media (max-width: 920px) {
  .hero-bar,
  .editor-topbar,
  .footer-strip {
    flex-direction: column;
    align-items: flex-start;
  }

  .title-input {
    font-size: 34px;
  }

  .editor-surface {
    padding: 88px 0 24px;
    min-height: 460px;
  }

  .app-shell-embedded .editor-surface {
    padding: 12px 0 24vh;
  }

  .app-shell-embedded .editor-surface .ProseMirror {
    padding: 1.5rem 1.5rem 24vh;
  }

  .debug-grid {
    grid-template-columns: 1fr;
  }

  .floating-toolbar,
  .slash-menu,
  .block-drag-menu {
    left: 16px;
    right: 16px;
    width: auto;
    min-width: 0;
    transform: none;
  }

  .floating-toolbar {
    top: 12px;
  }
}

@media (max-width: 768px) {
  .editor-stage {
    width: 100%;
  }

  .mnote-mindmap-workspace {
    grid-template-columns: minmax(0, 1fr);
  }

  .mnote-mindmap-side-panel {
    border-left: 0;
    border-top: 1px solid rgba(148, 163, 184, 0.26);
  }

  .block-handle-shell,
  .block-drop-indicator {
    display: none;
  }

  .app-shell-embedded .editor-surface {
    padding-left: 0;
  }
}
"#;

#[derive(Clone, Copy, PartialEq, Eq)]
enum SlashActionKind {
    AiAssistant,
    AiWrite,
    ContinueWriting,
    Summarize,
    MoreAi,
    Paragraph,
    Heading1,
    Heading2,
    Heading3,
    Heading4,
    BulletList,
    OrderedList,
    Todo,
    AdvancedTodo,
    Quote,
    CodeBlock,
    Divider,
    SimpleTable,
    Mindmap,
    Image,
    UploadAttachment,
    Toc,
}

#[derive(Clone, Copy)]
struct SlashAction {
    kind: SlashActionKind,
    id: &'static str,
    category: &'static str,
    icon: &'static str,
    label: &'static str,
    description: &'static str,
    shortcut: &'static str,
}

#[derive(Clone, Copy)]
struct FoldedHeadingAction {
    level: u8,
    id: &'static str,
    icon: &'static str,
    label: &'static str,
}

const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
    FoldedHeadingAction {
        level: 1,
        id: "1",
        icon: "H1",
        label: "折叠主标题",
    },
    FoldedHeadingAction {
        level: 2,
        id: "2",
        icon: "H2",
        label: "折叠大标题",
    },
    FoldedHeadingAction {
        level: 3,
        id: "3",
        icon: "H3",
        label: "折叠中标题",
    },
    FoldedHeadingAction {
        level: 4,
        id: "4",
        icon: "H4",
        label: "折叠小标题",
    },
];

const SLASH_ACTIONS: [SlashAction; 22] = [
    SlashAction {
        kind: SlashActionKind::AiAssistant,
        id: "ai-assistant",
        category: "AI 助理",
        icon: "✦",
        label: "AI 助理",
        description: "按 Wolai 基线保留 AI 入口",
        shortcut: "/ai",
    },
    SlashAction {
        kind: SlashActionKind::AiWrite,
        id: "ai-write",
        category: "AI 助理",
        icon: "✎",
        label: "用 AI 写作",
        description: "唤起写作辅助入口",
        shortcut: "/yaixz",
    },
    SlashAction {
        kind: SlashActionKind::ContinueWriting,
        id: "continue-writing",
        category: "AI 助理",
        icon: "↪",
        label: "续写",
        description: "按当前上下文继续写作",
        shortcut: "/xx",
    },
    SlashAction {
        kind: SlashActionKind::Summarize,
        id: "summarize",
        category: "AI 助理",
        icon: "≡",
        label: "总结",
        description: "对当前内容生成摘要",
        shortcut: "/zj",
    },
    SlashAction {
        kind: SlashActionKind::MoreAi,
        id: "more-ai",
        category: "AI 助理",
        icon: "…",
        label: "更多",
        description: "更多 AI 命令",
        shortcut: "›",
    },
    SlashAction {
        kind: SlashActionKind::Paragraph,
        id: "paragraph",
        category: "基础块列表",
        icon: "Aa",
        label: "文本",
        description: "普通正文块",
        shortcut: "/wb",
    },
    SlashAction {
        kind: SlashActionKind::Todo,
        id: "todo",
        category: "基础块列表",
        icon: "☑",
        label: "待办列表",
        description: "创建可勾选任务",
        shortcut: "/dblb",
    },
    SlashAction {
        kind: SlashActionKind::AdvancedTodo,
        id: "advanced-todo",
        category: "基础块列表",
        icon: "☑",
        label: "高级待办列表",
        description: "保留 Wolai 高级待办入口",
        shortcut: "/gjdblb",
    },
    SlashAction {
        kind: SlashActionKind::Heading1,
        id: "heading-1",
        category: "基础块列表",
        icon: "H1",
        label: "主标题",
        description: "一级标题",
        shortcut: "/h1",
    },
    SlashAction {
        kind: SlashActionKind::Heading2,
        id: "heading-2",
        category: "基础块列表",
        icon: "H2",
        label: "大标题",
        description: "二级标题",
        shortcut: "/h2",
    },
    SlashAction {
        kind: SlashActionKind::Heading3,
        id: "heading-3",
        category: "基础块列表",
        icon: "H3",
        label: "中标题",
        description: "三级标题",
        shortcut: "/h3",
    },
    SlashAction {
        kind: SlashActionKind::Heading4,
        id: "heading-4",
        category: "基础块列表",
        icon: "H4",
        label: "小标题",
        description: "四级标题",
        shortcut: "/h4",
    },
    SlashAction {
        kind: SlashActionKind::BulletList,
        id: "bullet",
        category: "基础块列表",
        icon: "•",
        label: "列表",
        description: "记录普通要点",
        shortcut: "/lb",
    },
    SlashAction {
        kind: SlashActionKind::OrderedList,
        id: "ordered",
        category: "基础块列表",
        icon: "1.",
        label: "数字列表",
        description: "记录步骤顺序",
        shortcut: "/szlb",
    },
    SlashAction {
        kind: SlashActionKind::Quote,
        id: "quote",
        category: "基础块列表",
        icon: "❝",
        label: "引述文字",
        description: "包住注释与摘录",
        shortcut: "/ys",
    },
    SlashAction {
        kind: SlashActionKind::CodeBlock,
        id: "code-block",
        category: "基础块列表",
        icon: "<> ",
        label: "代码片段",
        description: "插入带语义的代码块",
        shortcut: "/dm",
    },
    SlashAction {
        kind: SlashActionKind::Divider,
        id: "divider",
        category: "基础块列表",
        icon: "—",
        label: "分割线",
        description: "插入分隔线",
        shortcut: "/fgx",
    },
    SlashAction {
        kind: SlashActionKind::SimpleTable,
        id: "simple-table",
        category: "进阶块列表",
        icon: "▦",
        label: "简单表格",
        description: "插入基础表格",
        shortcut: "/jdbg",
    },
    SlashAction {
        kind: SlashActionKind::Mindmap,
        id: "mindmap",
        category: "进阶块列表",
        icon: "⌘",
        label: "思维导图",
        description: "插入 kernel projection 导图块",
        shortcut: "/dt",
    },
    SlashAction {
        kind: SlashActionKind::Image,
        id: "image",
        category: "媒体与附件",
        icon: "▧",
        label: "图片",
        description: "上传并插入图片",
        shortcut: "/tp",
    },
    SlashAction {
        kind: SlashActionKind::UploadAttachment,
        id: "upload-attachment",
        category: "媒体与附件",
        icon: "↥",
        label: "上传附件",
        description: "上传 Office、PDF 或其他文件",
        shortcut: "/fj",
    },
    SlashAction {
        kind: SlashActionKind::Toc,
        id: "toc",
        category: "进阶块列表",
        icon: "☰",
        label: "页面目录",
        description: "根据当前标题生成目录",
        shortcut: "/toc",
    },
];

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BridgeEnvelope<T>
where
    T: Serialize,
{
    protocol: &'static str,
    runtime: &'static str,
    version: &'static str,
    source: &'static str,
    event: &'static str,
    payload: T,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BridgeSelectorsPayload {
    root: &'static str,
    stage: &'static str,
    editor: &'static str,
    toolbar: &'static str,
    slash_menu: &'static str,
    handle: &'static str,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HostStatusPayload {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    dirty_count: u32,
    selected_block_index: Option<usize>,
    current_block_id: Option<String>,
    editor_focused: bool,
    read_only: bool,
    slash_open: bool,
    toolbar_open: bool,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ReadyPayload {
    runtime_name: &'static str,
    selectors: BridgeSelectorsPayload,
    supported_commands: Vec<&'static str>,
    supports_embedded_mode: bool,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct StatePayload {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    dirty_count: u32,
    selected_block_index: Option<usize>,
    editor_focused: bool,
    slash_open: bool,
    toolbar_open: bool,
    read_only: bool,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ChangeMetaPayload {
    dirty_count: u32,
    editor_focused: bool,
    slash_open: bool,
    toolbar_open: bool,
    selected_block_index: Option<usize>,
    revision: Option<i64>,
    conflict_detection_key: Option<String>,
    read_only: bool,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ChangePayload {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: String,
    content: Value,
    meta: ChangeMetaPayload,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HeightPayload {
    height: f64,
}

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

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RuntimePageOptions {
    wide_layout: Option<bool>,
    small_text: Option<bool>,
    layout_density: Option<String>,
    show_heading_numbers: Option<bool>,
    embed_default_block_id: Option<String>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HostCommandPayload {
    command: Option<String>,
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: Option<String>,
    content: Option<Value>,
    editable: Option<bool>,
    page_options: Option<RuntimePageOptions>,
    block_id: Option<String>,
    block_index: Option<usize>,
    text: Option<String>,
    reference_document_id: Option<String>,
    reference_block_id: Option<String>,
    current_block_id: Option<String>,
    selection: Option<Value>,
    revision: Option<i64>,
    conflict_detection_key: Option<String>,
    read_only: Option<bool>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
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>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HostCommandKind {
    Undo,
    Redo,
    ReplaceContent,
    SetPageOptions,
    InsertInlineReference,
    InsertEmbedReference,
    RequestCurrentBlockId,
    SetEditable,
    Focus,
    Bootstrap,
}

impl HostCommandKind {
    fn from_event_name(event_name: &str) -> Option<Self> {
        match event_name {
            "undo" => Some(Self::Undo),
            "redo" => Some(Self::Redo),
            "replaceContent" | "replace-document" => Some(Self::ReplaceContent),
            "setPageOptions" | "set-page-options" => Some(Self::SetPageOptions),
            "insertInlineReference" => Some(Self::InsertInlineReference),
            "insertEmbedReference" => Some(Self::InsertEmbedReference),
            "requestCurrentBlockId" => Some(Self::RequestCurrentBlockId),
            "setEditable" | "set-editable" => Some(Self::SetEditable),
            "focus" => Some(Self::Focus),
            "bootstrap" => Some(Self::Bootstrap),
            _ => None,
        }
    }

    fn from_payload(payload: &HostCommandPayload) -> Option<Self> {
        payload.command.as_deref().and_then(Self::from_event_name)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct CurrentBlockInfo {
    index: Option<usize>,
    block_id: Option<String>,
}

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

    #[test]
    fn parses_command_aliases_and_canonical_names() {
        assert_eq!(
            HostCommandKind::from_event_name("replace-document"),
            Some(HostCommandKind::ReplaceContent)
        );
        assert_eq!(
            HostCommandKind::from_event_name("replaceContent"),
            Some(HostCommandKind::ReplaceContent)
        );
        assert_eq!(
            HostCommandKind::from_event_name("requestCurrentBlockId"),
            Some(HostCommandKind::RequestCurrentBlockId)
        );
    }

    #[test]
    fn reads_command_from_payload_field() {
        let payload = HostCommandPayload {
            command: Some("setEditable".to_string()),
            document_id: None,
            workspace_id: None,
            title: None,
            content: None,
            editable: Some(true),
            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,
        };

        assert_eq!(
            HostCommandKind::from_payload(&payload),
            Some(HostCommandKind::SetEditable)
        );
    }

    #[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!(STYLE.contains(".slash-menu"));
        assert!(STYLE.contains("position: fixed;"));
        assert!(STYLE.contains("z-index: 130;"));
        assert!(!STYLE.contains("z-index: 11;"));
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RuntimeDeliveryMode {
    Standalone,
    Embedded,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MountOptions {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: Option<String>,
    content: Option<Value>,
    html: Option<String>,
    editable: Option<bool>,
    read_only: Option<bool>,
    revision: Option<i64>,
    conflict_detection_key: Option<String>,
    page_options: Option<RuntimePageOptions>,
    #[serde(default)]
    standalone_object: Option<RuntimeStandaloneObject>,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RuntimeStandaloneObject {
    kind: Option<String>,
    document_id: Option<String>,
    mindmap_id: Option<String>,
}

struct RuntimeMountContext {
    id: u32,
    target: EventTarget,
    mode: RuntimeDeliveryMode,
}

#[derive(Clone, Debug)]
struct RuntimeMountOptions {
    options: MountOptions,
    mode: RuntimeDeliveryMode,
}

struct MountedRuntimeListener {
    target: EventTarget,
    listener: Closure<dyn FnMut(Event)>,
}

struct MountedRuntime {
    listeners: Vec<MountedRuntimeListener>,
    mount_handle: Box<dyn Any>,
}

struct MountedMindmapShell {
    mount_handle: Box<dyn Any>,
}

impl Drop for MountedRuntime {
    fn drop(&mut self) {
        for listener in &self.listeners {
            let _ = listener.target.remove_event_listener_with_callback(
                COMMAND_EVENT,
                listener.listener.as_ref().unchecked_ref(),
            );
        }
    }
}

thread_local! {
    static RUNTIME_MOUNT_CONTEXT: std::cell::RefCell<Option<RuntimeMountContext>> = const { std::cell::RefCell::new(None) };
    static RUNTIME_MOUNT_OPTIONS: std::cell::RefCell<Option<RuntimeMountOptions>> = const { std::cell::RefCell::new(None) };
    static MOUNTED_HANDLES: std::cell::RefCell<HashMap<u32, MountedRuntime>> = std::cell::RefCell::new(HashMap::new());
    static PENDING_RUNTIME_LISTENERS: std::cell::RefCell<HashMap<u32, Vec<MountedRuntimeListener>>> = std::cell::RefCell::new(HashMap::new());
    static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell<HashMap<u32, MountedMindmapShell>> = std::cell::RefCell::new(HashMap::new());
    static NEXT_MOUNT_ID: Cell<u32> = const { Cell::new(1) };
}

fn next_mount_id() -> u32 {
    NEXT_MOUNT_ID.with(|cell| {
        let next = cell.get();
        cell.set(next.saturating_add(1).max(1));
        next
    })
}

fn set_runtime_mount_context(context: Option<RuntimeMountContext>) {
    RUNTIME_MOUNT_CONTEXT.with(|cell| {
        *cell.borrow_mut() = context;
    });
}

fn runtime_mount_context() -> Option<(u32, EventTarget, RuntimeDeliveryMode)> {
    RUNTIME_MOUNT_CONTEXT.with(|cell| {
        cell.borrow()
            .as_ref()
            .map(|context| (context.id, context.target.clone(), context.mode))
    })
}

fn runtime_delivery_mode() -> RuntimeDeliveryMode {
    resolve_runtime_delivery_mode(
        runtime_mount_context().map(|(_, _, mode)| mode),
        url_requests_embedded_mode(),
    )
}

fn set_runtime_mount_options(options: Option<RuntimeMountOptions>) {
    RUNTIME_MOUNT_OPTIONS.with(|cell| {
        *cell.borrow_mut() = options;
    });
}

fn runtime_editor_instance_id(mount_id: Option<u32>) -> String {
    mount_id
        .filter(|value| *value > 0)
        .map(|value| format!("mnote-leptos-tiptap-spike-{value}"))
        .unwrap_or_else(|| "mnote-leptos-tiptap-spike-standalone".to_string())
}

fn runtime_event_target() -> Option<EventTarget> {
    if let Some((_, target, _)) = runtime_mount_context() {
        return Some(target);
    }

    window()
        .and_then(|win| win.document())
        .and_then(|document| document.body())
        .map(|body| body.into())
}

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();
}

fn dispatch_runtime_event<T>(event_name: &'static str, payload: &T)
where
    T: Serialize,
{
    let envelope = BridgeEnvelope {
        protocol: PROTOCOL,
        runtime: RUNTIME_NAME,
        version: RUNTIME_VERSION,
        source: EVENT_PREFIX,
        event: event_name,
        payload,
    };
    let Ok(detail_value) = serde_wasm_bindgen::to_value(&envelope) else {
        return;
    };

    let Some(target) = runtime_event_target() else {
        return;
    };

    let init = CustomEventInit::new();
    init.set_detail(&detail_value);
    if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) {
        let _ = target.dispatch_event(&event);
    }
}

fn dispatch_custom_event_to_target<T>(target: &EventTarget, event_name: &'static str, payload: &T)
where
    T: Serialize,
{
    let envelope = BridgeEnvelope {
        protocol: PROTOCOL,
        runtime: RUNTIME_NAME,
        version: RUNTIME_VERSION,
        source: EVENT_PREFIX,
        event: event_name,
        payload,
    };
    let Ok(detail_value) = serde_wasm_bindgen::to_value(&envelope) else {
        return;
    };

    let init = CustomEventInit::new();
    init.set_detail(&detail_value);
    init.set_bubbles(true);
    if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) {
        let _ = target.dispatch_event(&event);
    }
}

fn dispatch_ready_event_to_target(target: &EventTarget, payload: &ReadyPayload) {
    dispatch_custom_event_to_target(target, READY_EVENT, payload);
}

fn dispatch_change_event(payload: &ChangePayload) {
    dispatch_runtime_event(CHANGE_EVENT, payload);
}

fn dispatch_change_event_to_target(target: &EventTarget, payload: &ChangePayload) {
    dispatch_custom_event_to_target(target, CHANGE_EVENT, payload);
}

fn dispatch_state_event(payload: &StatePayload) {
    dispatch_runtime_event(STATE_EVENT, payload);
}

fn dispatch_state_event_to_target(target: &EventTarget, payload: &StatePayload) {
    dispatch_custom_event_to_target(target, STATE_EVENT, payload);
}

fn dispatch_status_event(payload: &HostStatusPayload) {
    dispatch_runtime_event(STATUS_EVENT, payload);
}

fn dispatch_status_event_to_target(target: &EventTarget, payload: &HostStatusPayload) {
    dispatch_custom_event_to_target(target, STATUS_EVENT, payload);
}

fn dispatch_selection_event_to_target(target: &EventTarget, payload: &SelectionPayload) {
    dispatch_custom_event_to_target(target, SELECTION_EVENT, payload);
}

fn register_unmount_handle<M: Any + leptos::prelude::Mountable + 'static>(
    id: u32,
    target: EventTarget,
    listener: Closure<dyn FnMut(Event)>,
    handle: UnmountHandle<M>,
) {
    let mut listeners = vec![MountedRuntimeListener { target, listener }];
    PENDING_RUNTIME_LISTENERS.with(|registry| {
        if let Some(mut pending) = registry.borrow_mut().remove(&id) {
            listeners.append(&mut pending);
        }
    });
    MOUNTED_HANDLES.with(|registry| {
        registry.borrow_mut().insert(
            id,
            MountedRuntime {
                listeners,
                mount_handle: Box::new(handle),
            },
        );
    });
}

fn register_runtime_listener(id: u32, target: EventTarget, listener: Closure<dyn FnMut(Event)>) {
    MOUNTED_HANDLES.with(|registry| {
        if let Some(runtime) = registry.borrow_mut().get_mut(&id) {
            runtime
                .listeners
                .push(MountedRuntimeListener { target, listener });
        } else {
            PENDING_RUNTIME_LISTENERS.with(|pending_registry| {
                pending_registry
                    .borrow_mut()
                    .entry(id)
                    .or_default()
                    .push(MountedRuntimeListener { target, listener });
            });
        }
    });
}

fn take_unmount_handle(id: u32) -> Option<MountedRuntime> {
    PENDING_RUNTIME_LISTENERS.with(|registry| {
        registry.borrow_mut().remove(&id);
    });
    MOUNTED_HANDLES.with(|registry| registry.borrow_mut().remove(&id))
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellAction {
    id: String,
    label: String,
    #[serde(default)]
    long_label: Option<String>,
    icon: String,
    #[serde(default)]
    icon_key: Option<String>,
    #[serde(default)]
    priority: Option<u32>,
    #[serde(default)]
    overflow_group: Option<String>,
    #[serde(default)]
    cluster: Option<String>,
    disabled: bool,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellToolbarGroup {
    id: String,
    label: String,
    actions: Vec<MindmapShellAction>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellSidebarPanel {
    id: String,
    kind: String,
    label: String,
    icon: String,
    active: bool,
    body_title: String,
    body_caption: String,
    #[serde(default)]
    options: Vec<MindmapShellSidebarOption>,
    #[serde(default)]
    body_items: Vec<String>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellSidebarOption {
    id: String,
    label: String,
    action_id: Option<String>,
    value: Value,
    control_type: String,
    #[serde(default)]
    preview: Option<String>,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    readonly: bool,
    compat_path: Option<String>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellNavigator {
    word_count: u32,
    node_count: u32,
    zoom_percent: u32,
    readonly: bool,
    #[serde(default)]
    minimap_open: bool,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellToolbarOverflowState {
    available_width: Option<f64>,
    #[serde(default)]
    visible_action_ids: Vec<String>,
    #[serde(default)]
    overflow_action_ids: Vec<String>,
    #[serde(default)]
    more_open: bool,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellFullscreenState {
    mode: String,
    is_fullscreen: bool,
    target: String,
    #[serde(default = "default_true")]
    api_available: bool,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellSidebarState {
    trigger_visible: bool,
    panel_open: bool,
    active_panel_id: Option<String>,
    drawer_width: u32,
    collapsed_by_toggle: bool,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellNavigatorState {
    search_open: bool,
    minimap_open: bool,
    readonly: bool,
    zoom_percent: u32,
    mouse_behavior: String,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellInteractionState {
    chrome_visibility: String,
    toolbar_overflow: MindmapShellToolbarOverflowState,
    fullscreen: MindmapShellFullscreenState,
    sidebar: MindmapShellSidebarState,
    navigator: MindmapShellNavigatorState,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellOptions {
    mindmap_id: String,
    toolbar_groups: Vec<MindmapShellToolbarGroup>,
    sidebar_panels: Vec<MindmapShellSidebarPanel>,
    navigator: MindmapShellNavigator,
    shell: MindmapShellInteractionState,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellActionPayload<'a> {
    action_id: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    value: Option<&'a Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    compat_path: Option<&'a str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    source: Option<&'a str>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellPanelPayload<'a> {
    event: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    panel_id: Option<&'a str>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellMinimapPayload {
    open: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellZoomPayload {
    percent: u32,
}

fn default_true() -> bool {
    true
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MindmapShellToolbarOverflowPayload {
    more_open: bool,
}

fn dispatch_mindmap_shell_event<T>(target: &EventTarget, event_name: &'static str, payload: &T)
where
    T: Serialize,
{
    let Ok(detail_value) = serde_wasm_bindgen::to_value(payload) else {
        return;
    };
    let init = CustomEventInit::new();
    init.set_detail(&detail_value);
    init.set_bubbles(true);
    if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) {
        let _ = target.dispatch_event(&event);
    }
}

fn render_mindmap_toolbar_action(
    action: MindmapShellAction,
    class_name: &'static str,
    source: &'static str,
    action_target: EventTarget,
) -> impl IntoView {
    let action_id = action.id.clone();
    let action_label_title = action
        .long_label
        .clone()
        .filter(|label| !label.is_empty())
        .unwrap_or_else(|| action.label.clone());
    let action_label_aria = action_label_title.clone();
    let action_label_text = action.label.clone();
    let action_icon_text = action.icon.clone();
    let action_icon_key = action.icon_key.clone().unwrap_or_default();
    let action_overflow_group = action.overflow_group.clone().unwrap_or_default();
    let action_cluster = action.cluster.clone().unwrap_or_default();
    let action_priority = action.priority.unwrap_or(u32::MAX).to_string();
    let action_disabled = action.disabled;

    view! {
        <button
            type="button"
            class=class_name
            data-testid=format!("mindmap-schema-toolbar-action-{}", action_id)
            data-mindmap-action-id=action.id.clone()
            data-icon-key=action_icon_key
            data-overflow-group=action_overflow_group
            data-toolbar-cluster=action_cluster
            data-priority=action_priority
            title=action_label_title
            aria-label=action_label_aria
            disabled=action_disabled
            on:click=move |_| {
                dispatch_mindmap_shell_event(
                    &action_target,
                    MINDMAP_SHELL_ACTION_EVENT,
                    &MindmapShellActionPayload {
                        action_id: &action_id,
                        value: None,
                        compat_path: None,
                        source: Some(source),
                    },
                );
            }
        >
            <span class="mnote-mindmap-tool-icon" aria-hidden="true">{action_icon_text}</span>
            <span class="mnote-mindmap-tool-label">{action_label_text}</span>
        </button>
    }
}

fn render_mindmap_sidebar_option(
    option: MindmapShellSidebarOption,
    action_target: EventTarget,
) -> impl IntoView {
    let option_id = option.id.clone();
    let option_label = option.label.clone();
    let option_control_type = option.control_type.clone();
    let action_id = option.action_id.clone();
    let option_value = option.value.clone();
    let compat_path = option.compat_path.clone();
    let option_preview = option.preview.clone().unwrap_or_default();
    let option_description = option.description.clone().unwrap_or_default();
    let option_readonly = option.readonly;
    let option_action_id_attr = action_id.clone().unwrap_or_default();

    if option_readonly || action_id.is_none() {
        return view! {
            <div
                class="mnote-mindmap-side-option mnote-mindmap-side-option-readonly"
                data-testid=format!("mindmap-schema-sidebar-option-{}", option_id)
                data-mindmap-sidebar-option-id=option.id
                data-control-type=option_control_type.clone()
                data-readonly="true"
            >
                <span class="mnote-mindmap-side-option-label">{option_label}</span>
                {if option_description.is_empty() {
                    ().into_any()
                } else {
                    view! { <span class="mnote-mindmap-side-option-description">{option_description}</span> }.into_any()
                }}
            </div>
        }
        .into_any();
    }

    let button_class = if option_control_type == "layoutCard" {
        "mnote-mindmap-side-option mnote-mindmap-side-option-layout-card"
    } else if option_control_type == "swatch" {
        "mnote-mindmap-side-option mnote-mindmap-side-option-swatch"
    } else if option_control_type == "treeItem" {
        "mnote-mindmap-side-option mnote-mindmap-side-option-tree-item"
    } else {
        "mnote-mindmap-side-option"
    };

    view! {
        <button
            type="button"
            class=button_class
            data-testid=format!("mindmap-schema-sidebar-option-{}", option_id)
            data-mindmap-sidebar-option-id=option.id
            data-mindmap-action-id=option_action_id_attr
            data-control-type=option_control_type.clone()
            on:click=move |_| {
                if let Some(action_id) = action_id.as_deref() {
                    dispatch_mindmap_shell_event(
                        &action_target,
                        MINDMAP_SHELL_ACTION_EVENT,
                        &MindmapShellActionPayload {
                            action_id,
                            value: Some(&option_value),
                            compat_path: compat_path.as_deref(),
                            source: Some("sidebar"),
                        },
                    );
                }
            }
        >
            {if option_control_type == "swatch" {
                view! {
                    <span class="mnote-mindmap-side-option-swatch-chip" style=format!("background:{};", option_preview.clone())></span>
                }
                .into_any()
            } else if option_control_type == "layoutCard" {
                view! {
                    <span class="mnote-mindmap-side-option-preview-card">{option_preview.clone()}</span>
                }
                .into_any()
            } else {
                ().into_any()
            }}
            <span class="mnote-mindmap-side-option-label">{option_label}</span>
            {if option_description.is_empty() {
                ().into_any()
            } else {
                view! { <span class="mnote-mindmap-side-option-description">{option_description}</span> }.into_any()
            }}
        </button>
    }
    .into_any()
}

#[component]
fn MindmapShell(options: MindmapShellOptions, event_target: EventTarget) -> impl IntoView {
    let toolbar_groups = options.toolbar_groups.clone();
    let sidebar_panels = options.sidebar_panels.clone();
    let navigator = options.navigator.clone();
    let shell = options.shell.clone();
    let minimap_open = shell.navigator.minimap_open;
    let search_open = shell.navigator.search_open;
    let sidebar_panel_open = shell.sidebar.panel_open;
    let sidebar_trigger_visible = shell.sidebar.trigger_visible;
    let active_panel_id = shell.sidebar.active_panel_id.clone();
    let sidebar_drawer_width = shell.sidebar.drawer_width.to_string();
    let chrome_visibility = shell.chrome_visibility.clone();
    let fullscreen_mode = shell.fullscreen.mode.clone();
    let fullscreen_target = shell.fullscreen.target.clone();
    let navigator_mouse_behavior = shell.navigator.mouse_behavior.clone();
    let toolbar_available_width = shell
        .toolbar_overflow
        .available_width
        .map(|width| width.round().to_string())
        .unwrap_or_else(|| "".to_string());
    let chrome_visible = shell.chrome_visibility == "visible";
    let toolbar_visible_actions = shell.toolbar_overflow.visible_action_ids.join(",");
    let toolbar_overflow_actions = shell.toolbar_overflow.overflow_action_ids.join(",");
    let sidebar_drawer_width_value = shell.sidebar.drawer_width;
    let navigator_right_offset = if chrome_visible && sidebar_panel_open && sidebar_trigger_visible
    {
        sidebar_drawer_width_value + 112
    } else if chrome_visible && sidebar_trigger_visible {
        108
    } else {
        28
    };
    let navigator_right_style = format!("right: {}px;", navigator_right_offset);
    let minimap_right_style = format!("right: {}px;", navigator_right_offset);
    let mut primary_toolbar_actions: Vec<MindmapShellAction> = Vec::new();
    let mut overflow_toolbar_actions: Vec<MindmapShellAction> = Vec::new();
    let mut file_toolbar_actions: Vec<MindmapShellAction> = Vec::new();

    for group in toolbar_groups.into_iter() {
        match group.id.as_str() {
            "overflow" => overflow_toolbar_actions.extend(group.actions),
            "file" => file_toolbar_actions.extend(group.actions),
            _ => primary_toolbar_actions.extend(group.actions),
        }
    }
    primary_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX));
    overflow_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX));
    file_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX));
    let has_toolbar_overflow = !overflow_toolbar_actions.is_empty();
    let toolbar_more_open = shell.toolbar_overflow.more_open && has_toolbar_overflow;

    let action_target = event_target.clone();
    let panel_target = event_target;

    view! {
        <div
            class="mnote-mindmap-rust-shell"
            data-testid="mindmap-rust-shell"
            data-ui-shell-source="leptos-rust-shell"
            data-mnote-mindmap-id=options.mindmap_id
            data-chrome-visibility=chrome_visibility
            data-toolbar-available-width=toolbar_available_width
            data-toolbar-visible-actions=toolbar_visible_actions
            data-toolbar-overflow-actions=toolbar_overflow_actions
            data-toolbar-more-open=toolbar_more_open.to_string()
            data-fullscreen-mode=fullscreen_mode
            data-fullscreen-active=shell.fullscreen.is_fullscreen.to_string()
            data-fullscreen-target=fullscreen_target
            data-fullscreen-api-available=shell.fullscreen.api_available.to_string()
            data-sidebar-trigger-visible=sidebar_trigger_visible.to_string()
            data-sidebar-panel-open=sidebar_panel_open.to_string()
            data-sidebar-active-panel=active_panel_id.unwrap_or_default()
            data-sidebar-drawer-width=sidebar_drawer_width
            data-sidebar-collapsed-by-toggle=shell.sidebar.collapsed_by_toggle.to_string()
            data-navigator-search-open=shell.navigator.search_open.to_string()
            data-navigator-minimap-open=shell.navigator.minimap_open.to_string()
            data-navigator-readonly=shell.navigator.readonly.to_string()
            data-navigator-zoom-percent=shell.navigator.zoom_percent.to_string()
            data-navigator-mouse-behavior=navigator_mouse_behavior
        >
            {if chrome_visible {
            view! { <div
                class="mnote-mindmap-command-toolbar mnote-mindmap-schema-toolbar"
                data-testid="mindmap-schema-toolbar"
                data-ui-source="leptos-rust-shell"
                data-schema-source="mindmapDefaultUiSchema"
            >
                <div class="mnote-mindmap-toolbar-primary" data-testid="mindmap-schema-toolbar-groups">
                    <div class="mnote-mindmap-toolbar-cluster mnote-mindmap-toolbar-cluster-main" data-testid="mindmap-schema-toolbar-cluster-main">
                        {primary_toolbar_actions
                            .into_iter()
                            .map(|action| render_mindmap_toolbar_action(action, "mnote-mindmap-tool", "toolbar", action_target.clone()))
                            .collect_view()}
                    </div>
                    {if has_toolbar_overflow {
                        let overflow_toggle_target = action_target.clone();
                        let overflow_menu_actions = overflow_toolbar_actions.clone();
                        view! {
                            <div class="mnote-mindmap-toolbar-more" data-testid="mindmap-schema-toolbar-more">
                                <button
                                    type="button"
                                    class="mnote-mindmap-tool mnote-mindmap-toolbar-more-button"
                                    data-testid="mindmap-schema-toolbar-action-more"
                                    data-mindmap-action-id="more"
                                    data-open=toolbar_more_open.to_string()
                                    aria-haspopup="menu"
                                    aria-expanded=toolbar_more_open.to_string()
                                    title="更多"
                                    aria-label="更多"
                                    on:click=move |_| {
                                        dispatch_mindmap_shell_event(
                                            &overflow_toggle_target,
                                            MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT,
                                            &MindmapShellToolbarOverflowPayload { more_open: !toolbar_more_open },
                                        );
                                    }
                                >
                                    <span class="mnote-mindmap-tool-icon" aria-hidden="true">"⋯"</span>
                                    <span class="mnote-mindmap-tool-label">"更多"</span>
                                </button>
                                {if toolbar_more_open {
                                    view! {
                                        <div class="mnote-mindmap-toolbar-more-menu" data-testid="mindmap-schema-toolbar-more-menu" role="menu">
                                            {overflow_menu_actions
                                                .into_iter()
                                                .map(|action| render_mindmap_toolbar_action(action, "mnote-mindmap-tool mnote-mindmap-toolbar-more-item", "toolbar", action_target.clone()))
                                                .collect_view()}
                                        </div>
                                    }.into_any()
                                } else {
                                    ().into_any()
                                }}
                            </div>
                        }.into_any()
                    } else {
                        ().into_any()
                    }}
                    <div class="mnote-mindmap-toolbar-cluster mnote-mindmap-toolbar-cluster-file" data-testid="mindmap-schema-toolbar-cluster-file">
                        {file_toolbar_actions
                            .into_iter()
                            .map(|action| render_mindmap_toolbar_action(action, "mnote-mindmap-tool", "toolbar", action_target.clone()))
                            .collect_view()}
                    </div>
                </div>
            </div> }.into_any()
            } else {
                ().into_any()
            }}
            {if chrome_visible {
            view! { <aside
                class="mnote-mindmap-side-panel mnote-mindmap-schema-sidebar"
                data-testid="mindmap-schema-sidebar"
                data-ui-source="leptos-rust-shell"
                data-schema-source="mindmapDefaultUiSchema"
                data-trigger-visible=sidebar_trigger_visible.to_string()
                data-panel-open=sidebar_panel_open.to_string()
                data-collapsed-by-toggle=shell.sidebar.collapsed_by_toggle.to_string()
            >
                {if sidebar_trigger_visible {
                    view! {
                        <div class="mnote-mindmap-side-rail" data-testid="mindmap-schema-sidebar-rail">
                            <button
                                type="button"
                                class="mnote-mindmap-side-rail-handle"
                                data-testid="mindmap-schema-sidebar-hide-handle"
                                aria-label="隐藏侧栏"
                                title="隐藏侧栏"
                                on:click={
                                    let panel_target = panel_target.clone();
                                    move |_| {
                                        dispatch_mindmap_shell_event(
                                            &panel_target,
                                            MINDMAP_SHELL_PANEL_EVENT,
                                            &MindmapShellPanelPayload {
                                                event: "hideTrigger",
                                                panel_id: None,
                                            },
                                        );
                                    }
                                }
                            >
                                "›"
                            </button>
                            {sidebar_panels
                                .iter()
                                .cloned()
                                .map(|panel| {
                                    let panel_id = panel.id.clone();
                                    let panel_kind = panel.kind.clone();
                                    let panel_target = panel_target.clone();
                                    view! {
                                        <button
                                            type="button"
                                            class="mnote-mindmap-side-tab"
                                            data-testid=format!("mindmap-schema-sidebar-tab-{}", panel_id)
                                            data-mindmap-sidebar-panel-id=panel.id.clone()
                                            data-sidebar-kind=panel_kind
                                            data-active=panel.active.to_string()
                                            on:click=move |_| {
                                                dispatch_mindmap_shell_event(
                                                    &panel_target,
                                                    MINDMAP_SHELL_PANEL_EVENT,
                                                    &MindmapShellPanelPayload {
                                                        event: "togglePanel",
                                                        panel_id: Some(&panel_id),
                                                    },
                                                );
                                            }
                                        >
                                            <span class="mnote-mindmap-side-icon">{panel.icon}</span>
                                            <span class="mnote-mindmap-side-tab-label">{panel.label}</span>
                                        </button>
                                    }
                                })
                                .collect_view()}
                        </div>
                    }
                    .into_any()
                } else {
                    view! {
                        <button
                            type="button"
                            class="mnote-mindmap-side-restore-handle"
                            data-testid="mindmap-schema-sidebar-restore-handle"
                            aria-label="恢复侧栏"
                            title="恢复侧栏"
                            on:click={
                                let panel_target = panel_target.clone();
                                move |_| {
                                    dispatch_mindmap_shell_event(
                                        &panel_target,
                                        MINDMAP_SHELL_PANEL_EVENT,
                                        &MindmapShellPanelPayload {
                                            event: "restoreTrigger",
                                            panel_id: None,
                                        },
                                    );
                                }
                            }
                        >
                            "‹"
                        </button>
                    }
                    .into_any()
                }}
                {if sidebar_panel_open {
                    sidebar_panels
                        .iter()
                        .find(|panel| panel.active)
                        .or_else(|| sidebar_panels.first())
                        .cloned()
                        .map(|panel| view! {
                        <div
                            class="mnote-mindmap-side-body mnote-mindmap-side-drawer"
                            data-testid="mindmap-schema-sidebar-drawer"
                            data-sidebar-panel-id=panel.id.clone()
                            data-sidebar-kind=panel.kind.clone()
                            style=format!("width: {}px;", sidebar_drawer_width_value)
                        >
                            <div class="mnote-mindmap-side-drawer-header">
                                <div class="mnote-mindmap-side-drawer-heading">
                                    <strong data-testid="mindmap-schema-sidebar-title">{panel.body_title}</strong>
                                    <span>{panel.body_caption}</span>
                                </div>
                                <button
                                    type="button"
                                    class="mnote-mindmap-side-drawer-close"
                                    data-testid="mindmap-schema-sidebar-close"
                                    aria-label="关闭侧栏"
                                    title="关闭侧栏"
                                    on:click={
                                        let panel_target = panel_target.clone();
                                        move |_| {
                                            dispatch_mindmap_shell_event(
                                                &panel_target,
                                                MINDMAP_SHELL_PANEL_EVENT,
                                                &MindmapShellPanelPayload {
                                                    event: "closeDrawer",
                                                    panel_id: None,
                                                },
                                            );
                                        }
                                    }
                                >
                                    "×"
                                </button>
                            </div>
                            <div class="mnote-mindmap-side-options" data-testid=format!("mindmap-schema-sidebar-options-{}", panel.id)>
                                {panel.options
                                    .into_iter()
                                    .map(|option| render_mindmap_sidebar_option(option, action_target.clone()))
                                    .collect_view()}
                            </div>
                            <ol class="mnote-mindmap-side-outline" data-testid=format!("mindmap-schema-sidebar-outline-{}", panel.id)>
                                {panel.body_items
                                    .into_iter()
                                    .map(|item| view! { <li>{item}</li> })
                                    .collect_view()}
                            </ol>
                        </div>
                        }.into_any())
                        .unwrap_or_else(|| ().into_any())
                } else {
                    ().into_any()
                }}
            </aside> }.into_any()
            } else {
                ().into_any()
            }}
            <div
                class="mnote-mindmap-count"
                data-testid="mindmap-schema-count"
                data-ui-source="leptos-rust-shell"
                data-schema-source="mindmapDefaultUiSchema"
            >
                <span data-testid="mindmap-schema-navigator-stats">{format!("字数 {}", navigator.word_count)}</span>
                <span>{format!("节点 {}", navigator.node_count)}</span>
            </div>
            {if chrome_visible {
            view! { <div
                class="mnote-mindmap-bottom-bar mnote-mindmap-schema-navigator"
                data-testid="mindmap-schema-navigator"
                data-ui-source="leptos-rust-shell"
                data-schema-source="mindmapDefaultUiSchema"
                style=navigator_right_style
            >
                <div class="mnote-mindmap-schema-navigator-group">
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-centerRoot" data-mindmap-action-id="centerRoot" title="回根节点" aria-label="回根节点" on:click={
                        let action_target = action_target.clone();
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                            action_id: "centerRoot",
                            value: None,
                            compat_path: None,
                            source: None,
                        })
                    }><span class="material-symbols-outlined" data-icon="home" aria-hidden="true"></span></button>
                    <div class="mnote-mindmap-schema-navigator-search-wrap">
                        <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-search" data-mindmap-action-id="search" data-active=search_open.to_string() title="搜索" aria-label="搜索" on:click={
                            let action_target = action_target.clone();
                            move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                                action_id: "search",
                                value: None,
                                compat_path: None,
                                source: None,
                            })
                        }><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
                        {if search_open {
                            view! {
                                <input
                                    data-testid="mindmap-schema-navigator-search"
                                    data-mindmap-action-id="search"
                                    placeholder="搜索"
                                    aria-label="搜索"
                                    on:input={
                                        let action_target = action_target.clone();
                                        move |event| {
                                            let value = event_target_value(&event);
                                            let value = json!(value);
                                            dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                                                action_id: "search",
                                                value: Some(&value),
                                                compat_path: None,
                                                source: None,
                                            })
                                        }
                                    }
                                    on:keydown={
                                        let action_target = action_target.clone();
                                        move |event: ev::KeyboardEvent| {
                                            if event.key() == "Escape" {
                                                let value = json!(false);
                                                dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                                                    action_id: "search",
                                                    value: Some(&value),
                                                    compat_path: None,
                                                    source: None,
                                                });
                                            }
                                        }
                                    }
                                />
                            }.into_any()
                        } else {
                            ().into_any()
                        }}
                    </div>
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-minimap" data-active=minimap_open.to_string() title="小地图" aria-label="小地图" on:click={
                        let action_target = action_target.clone();
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_MINIMAP_EVENT, &MindmapShellMinimapPayload { open: !minimap_open })
                    }><span class="material-symbols-outlined" data-icon="account_tree" aria-hidden="true"></span></button>
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-settings" data-active=(sidebar_panel_open && sidebar_trigger_visible).to_string() title="设置" aria-label="设置" on:click={
                        let action_target = action_target.clone();
                        let active_panel_id = active_panel_id.clone();
                        move |_| {
                            let event = if sidebar_trigger_visible { "togglePanel" } else { "restoreTrigger" };
                            dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_PANEL_EVENT, &MindmapShellPanelPayload {
                                event,
                                panel_id: active_panel_id.as_deref(),
                            })
                        }
                    }><span class="mnote-mindmap-tool-icon" aria-hidden="true">"⚙"</span></button>
                </div>
                <div class="mnote-mindmap-schema-navigator-group">
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-zoomOut" data-mindmap-action-id="zoomOut" title="缩小" aria-label="缩小" on:click={
                        let action_target = action_target.clone();
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                            action_id: "zoomOut",
                            value: None,
                            compat_path: None,
                            source: None,
                        })
                    }><span class="mnote-mindmap-tool-icon" aria-hidden="true">"－"</span></button>
                    <input
                        class="mnote-mindmap-schema-navigator-zoom"
                        data-testid="mindmap-schema-navigator-zoom-input"
                        value=format!("{}%", navigator.zoom_percent)
                        aria-label="缩放百分比"
                        on:change={
                            let action_target = action_target.clone();
                            move |event| {
                                let raw = event_target_value(&event);
                                let normalized = raw.replace('%', "").trim().to_string();
                                if let Ok(percent) = normalized.parse::<u32>() {
                                    dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ZOOM_EVENT, &MindmapShellZoomPayload { percent });
                                } else if let Some(target) = event.target() {
                                    if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
                                        input.set_value(&format!("{}%", navigator.zoom_percent));
                                    }
                                }
                            }
                        }
                        on:keydown=move |event: ev::KeyboardEvent| {
                            if event.key() == "Escape" {
                                if let Some(target) = event.target() {
                                    if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
                                        input.set_value(&format!("{}%", navigator.zoom_percent));
                                        input.blur().ok();
                                    }
                                }
                            }
                        }
                    />
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-zoomIn" data-mindmap-action-id="zoomIn" title="放大" aria-label="放大" on:click={
                        let action_target = action_target.clone();
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                            action_id: "zoomIn",
                            value: None,
                            compat_path: None,
                            source: None,
                        })
                    }><span class="material-symbols-outlined" data-icon="add" aria-hidden="true"></span></button>
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-fullscreenCanvas" data-mindmap-action-id="fullscreenCanvas" data-active=shell.fullscreen.is_fullscreen.to_string() data-disabled-reason=if shell.fullscreen.api_available { "" } else { "fullscreen-unavailable" } disabled=!shell.fullscreen.api_available title=if shell.fullscreen.is_fullscreen { "退出全屏" } else { "全屏查看" } aria-label=if shell.fullscreen.is_fullscreen { "退出全屏" } else { "全屏查看" } on:click={
                        let action_target = action_target.clone();
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                            action_id: if shell.fullscreen.is_fullscreen { "exitFullscreen" } else { "fullscreenCanvas" },
                            value: None,
                            compat_path: None,
                            source: None,
                        })
                    }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if shell.fullscreen.is_fullscreen { "⤫" } else { "⛶" }}</span></button>
                    <button type="button" class="mnote-mindmap-schema-navigator-button" data-testid="mindmap-schema-navigator-action-readonly" data-mindmap-action-id="readonly" data-active=navigator.readonly.to_string() title=if navigator.readonly { "切回编辑" } else { "切换只读" } aria-label=if navigator.readonly { "切回编辑" } else { "切换只读" } on:click={
                        let action_target = action_target;
                        move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
                            action_id: "readonly",
                            value: None,
                            compat_path: None,
                            source: None,
                        })
                    }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if navigator.readonly { "锁" } else { "开" }}</span></button>
                </div>
            </div> }.into_any()
            } else {
                ().into_any()
            }}
            {if minimap_open {
            view! {
                <div
                    class="mnote-mindmap-minimap"
                    data-testid="mindmap-schema-minimap"
                    data-ui-source="leptos-rust-shell"
                    style=minimap_right_style
                >
                        <div class="mnote-mindmap-minimap-map">
                            <span></span>
                            <span></span>
                            <span></span>
                        </div>
                        <div class="mnote-mindmap-minimap-viewport" data-testid="mindmap-schema-minimap-viewport"></div>
                    </div>
                }.into_any()
            } else {
                ().into_any()
            }}
        </div>
    }
}

fn take_mindmap_shell_handle(id: u32) -> Option<MountedMindmapShell> {
    MOUNTED_MINDMAP_SHELLS.with(|registry| registry.borrow_mut().remove(&id))
}

#[wasm_bindgen]
pub fn mount_mindmap_shell(container: Element, options: JsValue) -> Result<u32, JsValue> {
    console_error_panic_hook::set_once();
    let options = serde_wasm_bindgen::from_value::<MindmapShellOptions>(options)?;
    let target = container
        .dyn_into::<HtmlElement>()
        .map_err(|_| JsValue::from_str("mindmap shell mount 目标必须是 HTML 元素"))?;
    let mount_id = next_mount_id();
    let event_target: EventTarget = target.clone().into();
    let handle = mount_to(target, move || {
        view! {
            <MindmapShell options=options event_target=event_target />
        }
    });
    MOUNTED_MINDMAP_SHELLS.with(|registry| {
        registry.borrow_mut().insert(
            mount_id,
            MountedMindmapShell {
                mount_handle: Box::new(handle),
            },
        );
    });
    Ok(mount_id)
}

#[wasm_bindgen]
pub fn unmount_mindmap_shell(mount_id: u32) -> Result<(), JsValue> {
    if take_mindmap_shell_handle(mount_id).is_some() {
        Ok(())
    } else {
        Err(JsValue::from_str("找不到对应的 mindmap shell 挂载句柄"))
    }
}

#[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);
        }
        drop(handle);
        Ok(())
    } else {
        Err(JsValue::from_str("找不到对应的挂载句柄"))
    }
}

fn mount_app_into(target: HtmlElement, options: MountOptions, mode: RuntimeDeliveryMode) -> u32 {
    let mount_id = next_mount_id();
    let context_target: EventTarget = target.clone().into();
    let mount_options = RuntimeMountOptions {
        options: options.clone(),
        mode,
    };

    set_runtime_mount_options(Some(mount_options.clone()));
    set_runtime_mount_context(Some(RuntimeMountContext {
        id: mount_id,
        target: context_target.clone(),
        mode,
    }));

    let handle = mount_to(target, move || {
        view! {
            <App mount_options=options />
        }
    });

    let command_listener = build_command_listener(mount_id);
    let _ = context_target
        .add_event_listener_with_callback(COMMAND_EVENT, command_listener.as_ref().unchecked_ref());

    register_unmount_handle(mount_id, context_target, command_listener, handle);
    mount_id
}

fn build_command_listener(mount_id: u32) -> Closure<dyn FnMut(Event)> {
    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(payload) = envelope.payload else {
            return;
        };
        let Some(command_kind) = HostCommandKind::from_payload(&payload) else {
            return;
        };

        let _ = mount_id;
        let _ = command_kind;
    }))
}

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 url_requests_embedded_mode() -> bool {
    window()
        .and_then(|win| win.location().search().ok())
        .map(|search| search.contains("embedded=1"))
        .unwrap_or(false)
}

fn resolve_runtime_delivery_mode(
    explicit_mode: Option<RuntimeDeliveryMode>,
    url_embedded: bool,
) -> RuntimeDeliveryMode {
    if let Some(mode) = explicit_mode {
        return mode;
    }

    if url_embedded {
        RuntimeDeliveryMode::Embedded
    } else {
        RuntimeDeliveryMode::Standalone
    }
}

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 content_column_width(stage_rect_width: f64) -> f64 {
    let available = (stage_rect_width - (CONTENT_COLUMN_HORIZONTAL_PADDING * 2.0)).max(0.0);
    available.min(CONTENT_COLUMN_MAX_WIDTH)
}

fn content_column_left(stage_rect_width: f64) -> f64 {
    let column_width = content_column_width(stage_rect_width);
    ((stage_rect_width - column_width) / 2.0).max(0.0)
}

fn content_text_left(stage_rect_width: f64) -> f64 {
    content_column_left(stage_rect_width) + CONTENT_COLUMN_HORIZONTAL_PADDING
}

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 current_target_html_element(event: &WheelEvent) -> Option<HtmlElement> {
    event.current_target()?.dyn_into::<HtmlElement>().ok()
}

fn trap_scroll_inside_menu(event: &WheelEvent) {
    event.prevent_default();
    event.stop_propagation();

    let Some(menu) = current_target_html_element(event) else {
        return;
    };

    let current = f64::from(menu.scroll_top());
    let max_scroll = f64::from((menu.scroll_height() - menu.client_height()).max(0));
    let next = (current + event.delta_y()).clamp(0.0, max_scroll);
    menu.set_scroll_top(next.round() as i32);
}

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

fn hovered_block_from_selection() -> Option<HoveredBlockState> {
    let selection = window().and_then(|win| win.get_selection().ok().flatten())?;
    let anchor_node = selection.anchor_node()?;
    let element = anchor_node
        .dyn_ref::<Element>()
        .cloned()
        .or_else(|| anchor_node.parent_element())?;
    let root = editor_root_element()?;
    let block = direct_block_from_element(element, &root)?;
    let index = top_level_block_index(&root, &block)?;
    block_state_from_index(index)
}

fn block_state_from_index(index: usize) -> Option<HoveredBlockState> {
    let root = editor_root_element()?;
    let stage = editor_stage_element()?;
    let block = root.children().item(index as u32)?;
    let block_rect = block.get_bounding_client_rect();
    let stage_rect = stage.get_bounding_client_rect();
    let block_height = if block_rect.height() > 0.0 {
        block_rect.height()
    } else {
        28.0
    };

    Some(HoveredBlockState {
        index,
        label: block_label_for_element(&block),
        top: block_rect.top() - stage_rect.top(),
        height: block_height,
    })
}

fn table_selection_overlay_style(
    anchor: &TableOverlayAnchor,
    selection: &TableSelectionOverlayState,
) -> String {
    match selection.kind {
        TableSelectionKind::Row => {
            let row_height = anchor.height / anchor.rows.max(1) as f64;
            format!(
                "left:0;top:{:.1}px;width:{:.1}px;height:{:.1}px;",
                row_height * selection.index as f64,
                anchor.width,
                row_height
            )
        }
        TableSelectionKind::Column => {
            let col_width = anchor.width / anchor.cols.max(1) as f64;
            format!(
                "left:{:.1}px;top:0;width:{:.1}px;height:{:.1}px;",
                col_width * selection.index as f64,
                col_width,
                anchor.height
            )
        }
    }
}

fn prosemirror_node_size(node: &Value) -> Option<u32> {
    match node.get("type").and_then(Value::as_str) {
        Some("text") => Some(
            node.get("text")
                .and_then(Value::as_str)
                .map(|text| text.encode_utf16().count() as u32)
                .unwrap_or(0),
        ),
        Some("hardBreak") | Some("horizontalRule") => Some(1),
        _ => {
            let Some(children) = node.get("content").and_then(Value::as_array) else {
                return Some(match node.get("type").and_then(Value::as_str) {
                    Some("paragraph") | Some("heading") | Some("blockquote")
                    | Some("codeBlock") | Some("bulletList") | Some("orderedList")
                    | Some("taskList") | Some("listItem") | Some("taskItem") | Some("table")
                    | Some("tableRow") | Some("tableCell") | Some("tableHeader") => 2,
                    _ => 1,
                });
            };

            let content_size = children.iter().try_fold(0_u32, |acc, child| {
                prosemirror_node_size(child).map(|size| acc + size)
            })?;
            Some(content_size + 2)
        }
    }
}

fn nested_edge_child_size(node: &Value, at_start: bool) -> Option<u32> {
    let outer = node
        .get("content")
        .and_then(Value::as_array)
        .and_then(|children| {
            if at_start {
                children.first()
            } else {
                children.last()
            }
        })?;
    let inner = outer
        .get("content")
        .and_then(Value::as_array)
        .and_then(|children| {
            if at_start {
                children.first()
            } else {
                children.last()
            }
        })?;
    prosemirror_node_size(inner)
}

fn top_level_block_range(document: &Value, index: usize) -> Option<TiptapRange> {
    let content = document.get("content").and_then(Value::as_array)?;
    let mut position = 0_u32;

    for (current_index, node) in content.iter().enumerate() {
        let node_size = prosemirror_node_size(node)?;
        if current_index == index {
            let from = position + nested_edge_child_size(node, true).unwrap_or(1);
            let mut to = position
                + node_size.saturating_sub(nested_edge_child_size(node, false).unwrap_or(1));
            if to < from {
                to = from;
            }
            return Some(TiptapRange { from, to });
        }
        position += node_size;
    }

    None
}

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

    let viewport_width = window()
        .and_then(|win| win.inner_width().ok())
        .and_then(|value| value.as_f64())
        .unwrap_or(1024.0)
        .max(320.0);
    let viewport_height = window()
        .and_then(|win| win.inner_height().ok())
        .and_then(|value| value.as_f64())
        .unwrap_or(768.0)
        .max(240.0);

    let selection_rect = window()
        .and_then(|win| win.get_selection().ok().flatten())
        .and_then(|selection| selection.get_range_at(0).ok())
        .map(|range| range.get_bounding_client_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_left = raw_left.clamp(GAP, (viewport_width - MENU_WIDTH - GAP).max(GAP));
    let below_limit = viewport_height - GAP;
    let clamped_top = if raw_top + MENU_HEIGHT > below_limit {
        (raw_anchor_top - MENU_HEIGHT - GAP).clamp(GAP, below_limit - 120.0)
    } else {
        raw_top.clamp(GAP, below_limit - 120.0)
    };

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

fn image_toolbar_anchor_from_image(image: &Element) -> Option<ImageToolbarAnchor> {
    let stage = editor_stage_element()?;
    let rect = image.get_bounding_client_rect();
    if rect.width() <= 0.0 && rect.height() <= 0.0 {
        return None;
    }
    let stage_rect = stage.get_bounding_client_rect();
    let align = image
        .get_attribute("data-align")
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| "left".to_string());

    Some(ImageToolbarAnchor {
        top: (rect.top() - stage_rect.top()).max(60.0),
        left: rect.left() + (rect.width() / 2.0) - stage_rect.left(),
        align,
    })
}

fn image_element_from_target(target: web_sys::EventTarget) -> Option<Element> {
    let element = target_element(target)?;
    if element.tag_name().eq_ignore_ascii_case("img")
        && element
            .closest(".editor-surface .ProseMirror")
            .ok()
            .flatten()
            .is_some()
    {
        return Some(element);
    }

    element
        .closest(".editor-surface .ProseMirror img[src]")
        .ok()
        .flatten()
}

fn pointer_in_handle_corridor(client_x: i32, client_y: i32, block: &HoveredBlockState) -> bool {
    let Some(stage) = editor_stage_element() else {
        return false;
    };
    let Some(root) = editor_root_element() else {
        return false;
    };
    let Some(block_element) = root.children().item(block.index as u32) else {
        return false;
    };

    let stage_rect = stage.get_bounding_client_rect();
    let block_rect = block_element.get_bounding_client_rect();
    let handle_rect = window()
        .and_then(|win| win.document())
        .and_then(|document| {
            document
                .query_selector(HANDLE_SHELL_SELECTOR)
                .ok()
                .flatten()
        })
        .map(|handle| handle.get_bounding_client_rect());

    let block_left = block_rect.left() - stage_rect.left();
    let block_top = block_rect.top() - stage_rect.top();
    let block_bottom = block_rect.bottom() - stage_rect.top();
    let handle_left = handle_rect
        .as_ref()
        .map(|rect| rect.left() - stage_rect.left())
        .unwrap_or_else(|| handle_lane_left(stage_rect.width()));
    let handle_right = handle_rect
        .as_ref()
        .map(|rect| rect.right() - stage_rect.left())
        .unwrap_or_else(|| handle_left + HANDLE_TRIGGER_WIDTH);

    pointer_in_handle_corridor_geometry(
        client_x,
        client_y,
        &HandleCorridorGeometry {
            stage_left: stage_rect.left(),
            stage_top: stage_rect.top(),
            block_left,
            block_top,
            block_bottom,
            handle_left,
            handle_right,
        },
    )
}

fn drop_indicator_from_target(
    target: web_sys::EventTarget,
    client_y: i32,
) -> Option<DropIndicatorState> {
    let hovered = hovered_block_from_target(target)?;
    let root = editor_root_element()?;
    let block = root.children().item(hovered.index as u32)?;
    let stage = editor_stage_element()?;
    Some(drop_indicator_from_block_rects(
        hovered.index,
        &block,
        &stage,
        client_y,
    ))
}

fn drop_indicator_from_point(client_x: i32, client_y: i32) -> Option<DropIndicatorState> {
    let target = event_target_from_point(client_x, client_y)?;
    drop_indicator_from_target(target, client_y)
}

fn document_content_mut(document: &mut Value) -> Result<&mut Vec<Value>, String> {
    document
        .get_mut("content")
        .and_then(Value::as_array_mut)
        .ok_or_else(|| "文档 JSON 缺少顶层 content 数组".to_string())
}

fn next_mindmap_id() -> String {
    let now = js_sys::Date::new_0();
    format!(
        "思维导图{:02}{:02}{:02}.json",
        now.get_hours(),
        now.get_minutes(),
        now.get_seconds()
    )
}

fn apply_html_update(
    editor: TiptapEditorHandle,
    persisted_identity: &PersistedDocumentIdentity,
    next_html: String,
    document_id: ReadSignal<Option<String>>,
    workspace_id: ReadSignal<Option<String>>,
    dirty_count: ReadSignal<u32>,
    set_dirty_count: WriteSignal<u32>,
    set_html_output: WriteSignal<String>,
    set_document_json: WriteSignal<Value>,
    set_json_output: WriteSignal<String>,
    title: ReadSignal<String>,
    hovered_block: ReadSignal<Option<HoveredBlockState>>,
    editor_focused: ReadSignal<bool>,
    slash_open: ReadSignal<bool>,
    turn_into_open: ReadSignal<bool>,
    color_menu_open: ReadSignal<bool>,
    more_menu_open: ReadSignal<bool>,
    revision: ReadSignal<Option<i64>>,
    conflict_detection_key: ReadSignal<Option<String>>,
    read_only: ReadSignal<bool>,
    set_command_feedback: WriteSignal<String>,
    success_message: impl Into<String>,
) {
    let success_message = success_message.into();
    match editor.set_content(TiptapContent::html(next_html)) {
        Ok(()) => {
            set_dirty_count.update(|count| *count += 1);
            let (html, snapshot, json_text) = read_editor_snapshot(editor);
            set_html_output.set(html.clone());
            set_document_json.set(snapshot.clone());
            set_json_output.set(json_text);
            dispatch_change_event(&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(
                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(
                persisted_identity,
                &title.get_untracked(),
                &snapshot,
                Some(html),
            ) {
                Ok(()) => {
                    set_command_feedback.set(format!("{success_message}，并已写入本地草稿"));
                }
                Err(err) => {
                    set_command_feedback.set(format!("{success_message}，但本地保存失败：{err}"));
                }
            }
        }
        Err(err) => {
            set_command_feedback.set(format!("更新文档失败：{err}"));
        }
    }
}

fn top_level_block_boundary_position(document: &Value, index: usize, before: bool) -> Option<u32> {
    let content = document.get("content").and_then(Value::as_array)?;
    let mut position = 0_u32;

    for (current_index, node) in content.iter().enumerate() {
        let node_size = prosemirror_node_size(node)?;
        if current_index == index {
            return Some(if before {
                position
            } else {
                position + node_size
            });
        }
        position += node_size;
    }

    None
}

fn insert_editor_paragraph_relative_to_block(
    editor: TiptapEditorHandle,
    index: usize,
    before: bool,
) -> Result<usize, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let position = top_level_block_boundary_position(&document, index, before)
        .ok_or_else(|| format!("找不到第 {} 个块的插入位置", index + 1))?;
    editor
        .insert_content_at(
            position,
            TiptapContent::json(json!({ "type": "paragraph" })),
            Some(TiptapInsertContentOptions {
                update_selection: Some(true),
                ..Default::default()
            }),
        )
        .map_err(|err| format!("插入新块失败：{err}"))?;
    let new_index = if before { index } else { index + 1 };
    focus_top_level_block_start(editor, new_index)?;
    Ok(new_index)
}

fn focus_top_level_block_start(editor: TiptapEditorHandle, index: usize) -> Result<(), String> {
    editor
        .focus()
        .map_err(|err| format!("聚焦编辑器失败：{err}"))?;

    if let (Some(root), Some(document), Some(selection)) = (
        editor_root_element(),
        window().and_then(|win| win.document()),
        window().and_then(|win| win.get_selection().ok().flatten()),
    ) {
        if let Some(block) = root.children().item(index as u32) {
            let range = document
                .create_range()
                .map_err(|err| format!("创建新块光标失败：{err:?}"))?;
            range
                .select_node_contents(block.unchecked_ref::<Node>())
                .map_err(|err| format!("选择新块失败：{err:?}"))?;
            range.collapse_with_to_start(true);
            selection
                .remove_all_ranges()
                .map_err(|err| format!("清理旧选区失败：{err:?}"))?;
            selection
                .add_range(&range)
                .map_err(|err| format!("写入新块选区失败：{err:?}"))?;
            return Ok(());
        }
    }

    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", index + 1))?;
    editor
        .set_text_selection(range.from)
        .map_err(|err| format!("定位新块失败：{err}"))
}

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 current_block_info_from_index(index: Option<usize>) -> CurrentBlockInfo {
    index
        .map(|value| CurrentBlockInfo {
            index: Some(value),
            block_id: runtime_block_id_from_index(value),
        })
        .unwrap_or(CurrentBlockInfo {
            index: None,
            block_id: None,
        })
}

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(
    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(&state);
    dispatch_status_event(&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 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_text_color(editor: TiptapEditorHandle, color: &str) -> Result<&'static str, String> {
    editor
        .set_color(TiptapColorAttributes {
            color: color.to_string(),
        })
        .map(|_| "已更新文字颜色")
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn clear_text_color(editor: TiptapEditorHandle) -> Result<&'static str, String> {
    editor
        .unset_color()
        .map(|_| "已清除文字颜色")
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn apply_highlight_color(
    editor: TiptapEditorHandle,
    color: Option<&str>,
) -> Result<&'static str, String> {
    match color {
        Some(color) => editor
            .set_highlight(Some(TiptapHighlightAttributes {
                color: Some(color.to_string()),
            }))
            .map(|_| "已更新背景色")
            .map_err(|err| format!("命令执行失败：{err}")),
        None => editor
            .unset_highlight()
            .map(|_| "已清除背景色")
            .map_err(|err| format!("命令执行失败：{err}")),
    }
}

fn apply_text_align(
    editor: TiptapEditorHandle,
    alignment: TiptapTextAlign,
) -> Result<&'static str, String> {
    let message = match alignment {
        TiptapTextAlign::Left => "已切到左对齐",
        TiptapTextAlign::Center => "已切到居中",
        TiptapTextAlign::Right => "已切到右对齐",
        TiptapTextAlign::Justify => "已切到两端对齐",
    };
    editor
        .set_text_align(alignment)
        .map(|_| message)
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn apply_image_align(
    editor: TiptapEditorHandle,
    align: &'static str,
) -> Result<&'static str, String> {
    let _ = editor.focus();
    let mut attrs = leptos_tiptap::TiptapAttributes::new();
    attrs.insert("data-align", align);
    editor
        .update_attributes(TiptapSchemaTarget::Node(TiptapNodeName::Image), attrs)
        .map(|_| "已更新图片对齐")
        .map_err(|err| format!("命令执行失败：{err}"))
}

#[derive(Clone, Copy)]
enum TableToolbarAction {
    AddRowAfter,
    AddColumnAfter,
    DeleteRow,
    DeleteColumn,
    ClearCell,
    DeleteTable,
}

#[derive(Clone, Copy)]
enum TableOptionAction {
    ToggleHeaderRow,
    ToggleHeaderColumn,
    ToggleHiddenBorders,
}

impl TableToolbarAction {
    fn id(self) -> &'static str {
        match self {
            Self::AddRowAfter => "add-row-after",
            Self::AddColumnAfter => "add-column-after",
            Self::DeleteRow => "delete-row",
            Self::DeleteColumn => "delete-column",
            Self::ClearCell => "clear-cell",
            Self::DeleteTable => "delete-table",
        }
    }

    fn icon(self) -> &'static str {
        match self {
            Self::AddRowAfter => "+R",
            Self::AddColumnAfter => "+C",
            Self::DeleteRow => "-R",
            Self::DeleteColumn => "-C",
            Self::ClearCell => "⌫",
            Self::DeleteTable => "×",
        }
    }

    fn title(self) -> &'static str {
        match self {
            Self::AddRowAfter => "下方插入行",
            Self::AddColumnAfter => "右侧插入列",
            Self::DeleteRow => "删除当前行",
            Self::DeleteColumn => "删除当前列",
            Self::ClearCell => "清空当前单元格",
            Self::DeleteTable => "删除表格",
        }
    }
}

const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [
    TableToolbarAction::AddRowAfter,
    TableToolbarAction::AddColumnAfter,
    TableToolbarAction::ClearCell,
    TableToolbarAction::DeleteRow,
    TableToolbarAction::DeleteColumn,
    TableToolbarAction::DeleteTable,
];

impl TableOptionAction {
    fn id(self) -> &'static str {
        match self {
            Self::ToggleHeaderRow => "toggle-header-row",
            Self::ToggleHeaderColumn => "toggle-header-column",
            Self::ToggleHiddenBorders => "toggle-hidden-borders",
        }
    }

    fn title(self) -> &'static str {
        match self {
            Self::ToggleHeaderRow => "标题行",
            Self::ToggleHeaderColumn => "标题列",
            Self::ToggleHiddenBorders => "隐藏边框线",
        }
    }
}

const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [
    TableOptionAction::ToggleHeaderRow,
    TableOptionAction::ToggleHeaderColumn,
    TableOptionAction::ToggleHiddenBorders,
];

fn run_table_toolbar_action(
    editor: TiptapEditorHandle,
    action: TableToolbarAction,
) -> Result<&'static str, String> {
    let result = match action {
        TableToolbarAction::AddRowAfter => editor.add_table_row_after(),
        TableToolbarAction::AddColumnAfter => editor.add_table_column_after(),
        TableToolbarAction::DeleteRow => editor.delete_table_row(),
        TableToolbarAction::DeleteColumn => editor.delete_table_column(),
        TableToolbarAction::ClearCell => editor.clear_table_cell(),
        TableToolbarAction::DeleteTable => editor.delete_table(),
    };
    result
        .map(|_| action.title())
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn run_table_option_action(
    editor: TiptapEditorHandle,
    action: TableOptionAction,
) -> Result<&'static str, String> {
    let result = match action {
        TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(),
        TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(),
        TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(),
    };
    result
        .map(|_| action.title())
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn first_table_node(value: &Value) -> Option<&Value> {
    if value.get("type").and_then(Value::as_str) == Some("table") {
        return Some(value);
    }
    if let Some(table) = value
        .get("props")
        .and_then(|props| props.get("tiptapTable"))
        .and_then(first_table_node)
    {
        return Some(table);
    }
    for key in ["content", "children"] {
        if let Some(children) = value.get(key).and_then(Value::as_array) {
            if let Some(table) = children.iter().find_map(first_table_node) {
                return Some(table);
            }
        }
    }
    None
}

fn table_option_checked(document: &Value, action: TableOptionAction) -> bool {
    let Some(table) = first_table_node(document) else {
        return false;
    };
    match action {
        TableOptionAction::ToggleHiddenBorders => table
            .get("attrs")
            .and_then(|attrs| attrs.get("hiddenBorders"))
            .and_then(Value::as_bool)
            .unwrap_or(false),
        TableOptionAction::ToggleHeaderRow => table
            .get("content")
            .and_then(Value::as_array)
            .and_then(|rows| rows.first())
            .and_then(|row| row.get("content"))
            .and_then(Value::as_array)
            .map(|cells| {
                !cells.is_empty()
                    && cells
                        .iter()
                        .all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader"))
            })
            .unwrap_or(false),
        TableOptionAction::ToggleHeaderColumn => table
            .get("content")
            .and_then(Value::as_array)
            .map(|rows| {
                !rows.is_empty()
                    && rows.iter().all(|row| {
                        row.get("content")
                            .and_then(Value::as_array)
                            .and_then(|cells| cells.first())
                            .and_then(|cell| cell.get("type"))
                            .and_then(Value::as_str)
                            == Some("tableHeader")
                    })
            })
            .unwrap_or(false),
    }
}

fn run_slash_action(
    editor: TiptapEditorHandle,
    action: SlashActionKind,
) -> Result<&'static str, String> {
    let result = match action {
        SlashActionKind::AiAssistant
        | SlashActionKind::AiWrite
        | SlashActionKind::ContinueWriting
        | SlashActionKind::Summarize
        | SlashActionKind::MoreAi => return Ok("AI 命令入口已打开"),
        SlashActionKind::Paragraph => editor.set_paragraph(),
        SlashActionKind::Heading1 => editor.toggle_heading(TiptapHeadingLevel::H1),
        SlashActionKind::Heading2 => editor.toggle_heading(TiptapHeadingLevel::H2),
        SlashActionKind::Heading3 => editor.toggle_heading(TiptapHeadingLevel::H3),
        SlashActionKind::Heading4 => editor.toggle_heading(TiptapHeadingLevel::H4),
        SlashActionKind::BulletList => editor.toggle_bullet_list(),
        SlashActionKind::OrderedList => editor.toggle_ordered_list(),
        SlashActionKind::Todo | SlashActionKind::AdvancedTodo => editor.toggle_task_list(),
        SlashActionKind::Quote => editor.toggle_blockquote(),
        SlashActionKind::CodeBlock => editor.toggle_code_block(Some(TiptapCodeBlockAttributes {
            language: Some("rust".into()),
        })),
        SlashActionKind::Divider => editor.set_horizontal_rule(),
        SlashActionKind::SimpleTable => {
            let _ = editor.focus();
            editor.insert_table(4, 3, false)
        }
        SlashActionKind::Mindmap => {
            editor.insert_content(
                TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
                None,
            )
        }
        SlashActionKind::Image => {
            dispatch_editor_upload_request("image", "image/*")?;
            return Ok("已打开图片上传");
        }
        SlashActionKind::UploadAttachment => {
            dispatch_editor_upload_request("attachment", "")?;
            return Ok("已打开附件上传");
        }
        SlashActionKind::Toc => editor.insert_toc_node(TiptapTocNodeAttrs {
            top_offset: Some(0),
            max_show_count: Some(20),
            show_title: Some(true),
        }),
    };

    result
        .map(|_| match action {
            SlashActionKind::AiAssistant
            | SlashActionKind::AiWrite
            | SlashActionKind::ContinueWriting
            | SlashActionKind::Summarize
            | SlashActionKind::MoreAi => "AI 命令入口已打开",
            SlashActionKind::Paragraph => "已切到文本",
            SlashActionKind::Heading1 => "已切到一级标题",
            SlashActionKind::Heading2 => "已切到二级标题",
            SlashActionKind::Heading3 => "已切到三级标题",
            SlashActionKind::Heading4 => "已切到四级标题",
            SlashActionKind::BulletList => "已切到无序列表",
            SlashActionKind::OrderedList => "已切到有序列表",
            SlashActionKind::Todo | SlashActionKind::AdvancedTodo => "已切到待办列表",
            SlashActionKind::Quote => "已切到引述文字",
            SlashActionKind::CodeBlock => "已切到代码块",
            SlashActionKind::Divider => "已插入分割线",
            SlashActionKind::SimpleTable => "已插入简单表格",
            SlashActionKind::Mindmap => "已插入思维导图",
            SlashActionKind::Image => "已打开图片上传",
            SlashActionKind::UploadAttachment => "已打开附件上传",
            SlashActionKind::Toc => "已插入页面目录",
        })
        .map_err(|err| format!("命令执行失败：{err}"))
}

fn run_block_turn_into_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    action: SlashActionKind,
) -> Result<&'static str, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, block_index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;

    match action {
        SlashActionKind::SimpleTable => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .insert_table(4, 3, false)
                .map(|_| "已把当前块转成简单表格")
                .map_err(|err| format!("命令执行失败：{err}"))
        }
        SlashActionKind::Toc => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .insert_toc_node(TiptapTocNodeAttrs {
                    top_offset: Some(0),
                    max_show_count: Some(20),
                    show_title: Some(true),
                })
                .map(|_| "已把当前块转成页面目录")
                .map_err(|err| format!("命令执行失败：{err}"))
        }
        SlashActionKind::Divider => editor
            .insert_content_at(
                range,
                TiptapContent::json(json!({ "type": "horizontalRule" })),
                None,
            )
            .map(|_| "已把当前块转成分割线")
            .map_err(|err| format!("命令执行失败：{err}")),
        SlashActionKind::Mindmap => editor
            .insert_content_at(
                range,
                TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
                None,
            )
            .map(|_| "已把当前块转成思维导图")
            .map_err(|err| format!("命令执行失败：{err}")),
        _ => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .clear_nodes()
                .map_err(|err| format!("清理当前块样式失败：{err}"))?;

            let message = run_slash_action(editor, action)?;
            let _ = editor.focus();
            let _ = editor.select_textblock_end();
            Ok(message)
        }
    }
}

fn heading_level_from_u8(level: u8) -> TiptapHeadingLevel {
    match level {
        1 => TiptapHeadingLevel::H1,
        2 => TiptapHeadingLevel::H2,
        3 => TiptapHeadingLevel::H3,
        4 => TiptapHeadingLevel::H4,
        5 => TiptapHeadingLevel::H5,
        _ => TiptapHeadingLevel::H6,
    }
}

fn run_block_turn_into_folded_heading_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    level: u8,
) -> Result<&'static str, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, block_index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;

    editor
        .set_text_selection(range)
        .map_err(|err| format!("选中当前块失败：{err}"))?;
    editor
        .clear_nodes()
        .map_err(|err| format!("清理当前块样式失败：{err}"))?;
    editor
        .set_heading_with_collapsed(heading_level_from_u8(level), true)
        .map_err(|err| format!("命令执行失败：{err}"))?;
    let mut attrs = leptos_tiptap::TiptapAttributes::new();
    attrs.insert("collapsed", true);
    editor
        .update_attributes(TiptapSchemaTarget::Node(TiptapNodeName::Heading), attrs)
        .map_err(|err| format!("写入折叠属性失败：{err}"))?;
    let _ = editor.focus();
    let _ = editor.select_textblock_end();

    Ok(match level {
        1 => "已切到折叠主标题",
        2 => "已切到折叠大标题",
        3 => "已切到折叠中标题",
        4 => "已切到折叠小标题",
        _ => "已切到折叠标题",
    })
}

fn normalized_page_block_title(text: &str) -> String {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        "未命名页面".to_string()
    } else {
        trimmed.to_string()
    }
}

#[derive(Debug, Deserialize)]
struct TreeCommandHttpResponse {
    result: Option<TreeCommandResult>,
    message: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TreeCommandResult {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: Option<String>,
    parent_id: Option<String>,
}

fn dispatch_tree_local_command(body: &Value, result: &Value) {
    let Some(win) = window() else {
        return;
    };
    let Ok(detail) = serde_wasm_bindgen::to_value(&json!({
        "body": body,
        "result": result,
    })) else {
        return;
    };
    let init = CustomEventInit::new();
    init.set_detail(&detail);
    if let Ok(event) = CustomEvent::new_with_event_init_dict("tree:local-command", &init) {
        let _ = win.dispatch_event(&event);
    }
}

async fn create_child_page_via_tree_command(
    parent_id: String,
    workspace_id: Option<String>,
    title: String,
) -> Result<TreeCommandResult, String> {
    let body = json!({
        "action": "create",
        "workspaceId": workspace_id,
        "parentId": parent_id,
        "title": title,
        "accessScope": "private",
        "content": [],
    });
    let body_string =
        serde_json::to_string(&body).map_err(|err| format!("序列化创建子页面请求失败：{err}"))?;

    let request_init = RequestInit::new();
    request_init.set_method("POST");
    request_init.set_mode(RequestMode::SameOrigin);
    request_init.set_body(&JsValue::from_str(&body_string));
    let headers = js_sys::Object::new();
    js_sys::Reflect::set(
        &headers,
        &JsValue::from_str("content-type"),
        &JsValue::from_str("application/json"),
    )
    .map_err(|_| "设置创建子页面请求头失败".to_string())?;
    js_sys::Reflect::set(
        request_init.as_ref(),
        &JsValue::from_str("headers"),
        &headers,
    )
    .map_err(|_| "设置创建子页面请求头失败".to_string())?;

    let win = window().ok_or_else(|| "当前浏览器窗口不可用".to_string())?;
    let response_value =
        JsFuture::from(win.fetch_with_str_and_init("/api/tree/commands", &request_init))
            .await
            .map_err(|err| format!("创建子页面请求失败：{err:?}"))?;
    let response: Response = response_value
        .dyn_into()
        .map_err(|_| "创建子页面响应类型错误".to_string())?;
    let status = response.status();
    let json_value = JsFuture::from(
        response
            .json()
            .map_err(|_| "读取创建子页面响应失败".to_string())?,
    )
    .await
    .map_err(|err| format!("解析创建子页面响应失败：{err:?}"))?;
    let parsed: TreeCommandHttpResponse = serde_wasm_bindgen::from_value(json_value.clone())
        .map_err(|err| format!("创建子页面响应 JSON 不符合预期：{err}"))?;
    if !response.ok() {
        return Err(parsed
            .message
            .unwrap_or_else(|| format!("创建子页面失败：HTTP {status}")));
    }
    let result = parsed
        .result
        .ok_or_else(|| "创建子页面响应缺少 result".to_string())?;
    let document_id = result
        .document_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| "创建子页面响应缺少 documentId".to_string())?;
    if document_id == parent_id {
        return Err("创建子页面返回了当前页面 id".to_string());
    }
    if let Some(created_parent_id) = result
        .parent_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        if created_parent_id != parent_id {
            return Err(format!(
                "创建子页面父节点不符合预期：期望 {parent_id}，实际 {created_parent_id}"
            ));
        }
    }
    dispatch_tree_local_command(
        &body,
        &serde_wasm_bindgen::from_value(json_value).unwrap_or(Value::Null)["result"],
    );
    Ok(result)
}

fn block_plain_text_from_index(
    editor: TiptapEditorHandle,
    block_index: usize,
) -> Result<String, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let block = document
        .get("content")
        .and_then(Value::as_array)
        .and_then(|content| content.get(block_index))
        .ok_or_else(|| format!("找不到第 {} 个块", block_index + 1))?;
    Ok(normalized_page_block_title(&collect_plain_text(block)))
}

fn inline_has_page_block_link(node: &Value) -> bool {
    if node
        .get("marks")
        .and_then(Value::as_array)
        .map(|marks| {
            marks.iter().any(|mark| {
                mark.get("type").and_then(Value::as_str) == Some("link")
                    && mark
                        .get("attrs")
                        .and_then(|attrs| attrs.get("class"))
                        .and_then(Value::as_str)
                        .map(|class_name| {
                            class_name
                                .split_whitespace()
                                .any(|part| part == "mnote-page-block-link")
                        })
                        .unwrap_or(false)
            })
        })
        .unwrap_or(false)
    {
        return true;
    }

    node.get("content")
        .and_then(Value::as_array)
        .map(|children| children.iter().any(inline_has_page_block_link))
        .unwrap_or(false)
}

fn top_level_block_is_page(editor: TiptapEditorHandle, block_index: usize) -> bool {
    editor
        .get_json()
        .ok()
        .and_then(|document| {
            document
                .get("content")
                .and_then(Value::as_array)
                .and_then(|content| content.get(block_index).cloned())
        })
        .map(|block| inline_has_page_block_link(&block))
        .unwrap_or(false)
}

fn top_level_block_matches_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    action: SlashActionKind,
) -> bool {
    let Some(block) = editor.get_json().ok().and_then(|document| {
        document
            .get("content")
            .and_then(Value::as_array)
            .and_then(|content| content.get(block_index).cloned())
    }) else {
        return false;
    };

    let node_type = block
        .get("type")
        .and_then(Value::as_str)
        .unwrap_or_default();
    match action {
        SlashActionKind::Paragraph => {
            node_type == "paragraph" && !inline_has_page_block_link(&block)
        }
        SlashActionKind::Heading1
        | SlashActionKind::Heading2
        | SlashActionKind::Heading3
        | SlashActionKind::Heading4 => {
            let expected_level = match action {
                SlashActionKind::Heading1 => 1,
                SlashActionKind::Heading2 => 2,
                SlashActionKind::Heading3 => 3,
                SlashActionKind::Heading4 => 4,
                _ => 0,
            };
            node_type == "heading"
                && block
                    .get("attrs")
                    .and_then(|attrs| attrs.get("level"))
                    .and_then(Value::as_i64)
                    .map(|level| level == expected_level)
                    .unwrap_or(false)
        }
        SlashActionKind::BulletList => node_type == "bulletList",
        SlashActionKind::OrderedList => node_type == "orderedList",
        SlashActionKind::Todo | SlashActionKind::AdvancedTodo => node_type == "taskList",
        SlashActionKind::Quote => node_type == "blockquote",
        SlashActionKind::CodeBlock => node_type == "codeBlock",
        SlashActionKind::Divider => node_type == "horizontalRule",
        SlashActionKind::SimpleTable => node_type == "table",
        SlashActionKind::Mindmap => {
            node_type == "paragraph"
                && block
                    .get("attrs")
                    .and_then(|attrs| attrs.get("mnoteBlockType"))
                    .and_then(Value::as_str)
                    == Some("mindmap")
        }
        SlashActionKind::Image => node_type == "image",
        SlashActionKind::UploadAttachment => false,
        SlashActionKind::Toc => node_type == "tocNode",
        SlashActionKind::AiAssistant
        | SlashActionKind::AiWrite
        | SlashActionKind::ContinueWriting
        | SlashActionKind::Summarize
        | SlashActionKind::MoreAi => false,
    }
}

fn page_reference_paragraph_node(page_id: &str, workspace_id: Option<&str>, title: &str) -> Value {
    let href = if page_id.trim().is_empty() {
        "#".to_string()
    } else if let Some(workspace_id) = workspace_id
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        format!("/documents/{page_id}?workspaceId={workspace_id}")
    } else {
        format!("/documents/{page_id}")
    };
    paragraph_node(vec![json!({
        "type": "text",
        "text": normalized_page_block_title(title),
        "marks": [{
            "type": "link",
            "attrs": {
                "href": href,
                "target": "_self",
                "rel": "noopener noreferrer nofollow",
                "class": "mnote-page-block-link",
            }
        }]
    })])
}

fn replace_block_with_page_reference(
    editor: TiptapEditorHandle,
    block_index: usize,
    page_id: &str,
    workspace_id: Option<&str>,
    title: &str,
) -> Result<(), String> {
    let mut document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let content = document_content_mut(&mut document)?;
    if block_index >= content.len() {
        return Err(format!("找不到第 {} 个块", block_index + 1));
    }
    content[block_index] = page_reference_paragraph_node(page_id, workspace_id, title);
    let next_payload = document
        .get("content")
        .and_then(Value::as_array)
        .cloned()
        .map(Value::Array)
        .unwrap_or(document);
    editor
        .set_content(TiptapContent::json(next_payload))
        .map_err(|err| format!("转换为页面失败：{err}"))?;
    focus_top_level_block_start(editor, block_index)?;
    Ok(())
}

fn run_block_turn_into_page_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    document_id: ReadSignal<Option<String>>,
    workspace_id: ReadSignal<Option<String>>,
    title: ReadSignal<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>,
    set_block_menu_open: WriteSignal<bool>,
    set_block_menu_anchor: WriteSignal<Option<HoveredBlockState>>,
    set_block_turn_into_open: WriteSignal<bool>,
) {
    let page_title = match block_plain_text_from_index(editor, block_index) {
        Ok(value) => value,
        Err(err) => {
            set_command_feedback.set(err);
            return;
        }
    };
    let Some(parent_document_id) = document_id.get_untracked() else {
        set_command_feedback.set("转换为页面失败：当前页面缺少 documentId".to_string());
        return;
    };
    let workspace_id_value = workspace_id.get_untracked();
    set_command_feedback.set(format!("正在创建子页面：{page_title}"));
    spawn_local(async move {
        match create_child_page_via_tree_command(
            parent_document_id.clone(),
            workspace_id_value.clone(),
            page_title.clone(),
        )
        .await
        {
            Ok(created) => {
                let Some(child_page_id) = created.document_id.as_deref() else {
                    set_command_feedback.set("转换为页面失败：创建响应缺少子页面 id".to_string());
                    return;
                };
                let effective_workspace_id = created
                    .workspace_id
                    .as_deref()
                    .or(workspace_id_value.as_deref());
                let effective_title = created.title.as_deref().unwrap_or(&page_title);
                match replace_block_with_page_reference(
                    editor,
                    block_index,
                    child_page_id,
                    effective_workspace_id,
                    effective_title,
                ) {
                    Ok(()) => {
                        set_block_menu_open.set(false);
                        set_block_menu_anchor.set(None);
                        set_block_turn_into_open.set(false);
                        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!("已转换为子页面：{effective_title}"),
                        );
                    }
                    Err(err) => set_command_feedback.set(err),
                }
            }
            Err(err) => set_command_feedback.set(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_event_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_event_target.clone();
    let ready_event_target = runtime_event_target.clone();
    let change_event_target = runtime_event_target.clone();
    let selection_event_target = runtime_event_target.clone();
    let slash_change_event_target = runtime_event_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, &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_json_output = json_output;
            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 |_| {
            let toolbar_locked = toolbar_overlay_locked(
                turn_into_open.get_untracked(),
                color_menu_open.get_untracked(),
                more_menu_open.get_untracked(),
            );
            if ((!editor_focused.get() || !text_selection_active.get()) && !toolbar_locked)
                || 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.get_html() {
                        Ok(current_html) => match reorder_top_level_block_html(
                            &current_html,
                            source_index,
                            indicator.index,
                            indicator.placement,
                        ) {
                            Ok(next_html) => {
                                apply_html_update(
                                    editor,
                                    &persisted_identity,
                                    next_html,
                                    document_id,
                                    workspace_id,
                                    dirty_count,
                                    set_dirty_count,
                                    set_html_output,
                                    set_document_json,
                                    set_json_output,
                                    title,
                                    hovered_block,
                                    editor_focused,
                                    slash_open,
                                    turn_into_open,
                                    color_menu_open,
                                    more_menu_open,
                                    revision,
                                    conflict_detection_key,
                                    read_only,
                                    set_command_feedback,
                                    "已通过块手柄拖拽重排",
                                );
                            }
                            Err(err) => {
                                set_command_feedback.set(err);
                            }
                        },
                        Err(err) => {
                            let _ =
                                set_command_feedback.try_set(format!("读取当前 HTML 失败：{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()),
            }
        }
    };

    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 pointer_in_handle_corridor(event.client_x(), event.client_y(), &current_block) {
                                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 pointer_in_handle_corridor(event.client_x(), event.client_y(), &current_block) {
                                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.get_html() {
                            Ok(current_html) => match reorder_top_level_block_html(
                                &current_html,
                                source_index,
                                indicator.index,
                                indicator.placement,
                            ) {
                                Ok(next_html) => {
                                    apply_html_update(
                                        editor,
                                        &runtime_persisted_identity(document_id, workspace_id),
                                        next_html,
                                        document_id,
                                        workspace_id,
                                        dirty_count,
                                        set_dirty_count,
                                        set_html_output,
                                        set_document_json,
                                        set_json_output,
                                        title,
                                        hovered_block,
                                        editor_focused,
                                        slash_open,
                                        turn_into_open,
                                        color_menu_open,
                                        more_menu_open,
                                        revision,
                                        conflict_detection_key,
                                        read_only,
                                        set_command_feedback,
                                        "已通过块手柄拖拽重排",
                                    );
                                }
                                Err(err) => {
                                    set_command_feedback.set(err);
                                }
                            },
                            Err(err) => {
                                set_command_feedback.set(format!("读取当前 HTML 失败：{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);
                    }
                >
                    {move || {
                        if !table_toolbar_open.get() || slash_open.get() || block_menu_open.get() || dragging_block_index.get().is_some() {
                            return ().into_any();
                        }
                        let Some(anchor) = table_overlay_anchor.get() else {
                            return ().into_any();
                        };
                        let controls_style = format!(
                            "left:{:.1}px;top:{:.1}px;width:{:.1}px;height:{:.1}px;",
                            anchor.left,
                            anchor.top,
                            anchor.width,
                            anchor.height
                        );
                        view! {
                            <div
                                class="table-controls"
                                data-testid="mnote-leptos-tiptap-table-controls"
                                style=controls_style
                            >
                                {move || {
                                    table_selection_overlay.get().map(|selection| {
                                        view! {
                                            <div
                                                class="table-selection-overlay"
                                                data-testid="table-selection-overlay"
                                                data-selection-kind=selection.kind.id()
                                                data-selection-index=selection.index.to_string()
                                                style=table_selection_overlay_style(&anchor, &selection)
                                            ></div>
                                        }
                                    }).into_any()
                                }}
                                {(0..anchor.rows).map(|index| {
                                    let active = table_selection_overlay.get()
                                        .map(|selection| selection.kind == TableSelectionKind::Row && selection.index == index)
                                        .unwrap_or(false);
                                    view! {
                                        <button
                                            type="button"
                                            class="table-aux-btn table-row-aux"
                                            data-testid=format!("table-row-aux-{index}")
                                            data-active=active.to_string()
                                            style=table_row_aux_style(&anchor, index)
                                            title="点击插入一行"
                                            aria-label="点击插入一行"
                                            on:mousedown=move |event: MouseEvent| {
                                                event.prevent_default();
                                                event.stop_propagation();
                                            }
                                            on:click=move |event: MouseEvent| {
                                                event.prevent_default();
                                                event.stop_propagation();
                                                set_table_toolbar_open.set(true);
                                                set_table_options_open.set(false);
                                                match run_table_toolbar_action(editor, TableToolbarAction::AddRowAfter) {
                                                    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,
                                                        );
                                                        if let Err(err) = editor.select_table_row(index as u32) {
                                                            set_command_feedback.set(format!("行已插入，但表格行选区同步失败：{err}"));
                                                        }
                                                        set_table_overlay_anchor.set(current_table_overlay_anchor());
                                                        set_table_selection_overlay.set(Some(TableSelectionOverlayState { kind: TableSelectionKind::Row, index }));
                                                    }
                                                    Err(err) => set_command_feedback.set(err),
                                                }
                                            }
                                        >"+"</button>
                                    }
                                }).collect_view()}
                                {(0..anchor.cols).map(|index| {
                                    let active = table_selection_overlay.get()
                                        .map(|selection| selection.kind == TableSelectionKind::Column && selection.index == index)
                                        .unwrap_or(false);
                                    view! {
                                        <button
                                            type="button"
                                            class="table-aux-btn table-col-aux"
                                            data-testid=format!("table-col-aux-{index}")
                                            data-active=active.to_string()
                                            style=table_col_aux_style(&anchor, index)
                                            title="点击插入一列"
                                            aria-label="点击插入一列"
                                            on:mousedown=move |event: MouseEvent| {
                                                event.prevent_default();
                                                event.stop_propagation();
                                            }
                                            on:click=move |event: MouseEvent| {
                                                event.prevent_default();
                                                event.stop_propagation();
                                                set_table_toolbar_open.set(true);
                                                set_table_options_open.set(false);
                                                match run_table_toolbar_action(editor, TableToolbarAction::AddColumnAfter) {
                                                    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,
                                                        );
                                                        if let Err(err) = editor.select_table_column(index as u32) {
                                                            set_command_feedback.set(format!("列已插入，但表格列选区同步失败：{err}"));
                                                        }
                                                        set_table_overlay_anchor.set(current_table_overlay_anchor());
                                                        set_table_selection_overlay.set(Some(TableSelectionOverlayState { kind: TableSelectionKind::Column, index }));
                                                    }
                                                    Err(err) => set_command_feedback.set(err),
                                                }
                                            }
                                        >"+"</button>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                    {move || {
                        if !table_toolbar_open.get() || slash_open.get() || block_menu_open.get() || dragging_block_index.get().is_some() {
                            return ().into_any();
                        }
                        view! {
                            <div
                                class="table-toolbar"
                                data-testid="mnote-leptos-tiptap-table-toolbar"
                                on:mousedown=move |event: MouseEvent| {
                                    event.prevent_default();
                                    event.stop_propagation();
                                }
                                on:click=move |event: MouseEvent| {
                                    event.stop_propagation();
                                }
                            >
                                {TABLE_TOOLBAR_ACTIONS.iter().enumerate().map(|(index, action)| {
                                    let action = *action;
                                    let testid = format!("table-toolbar-{}", action.id());
                                    view! {
                                        <>
                                            {if index == 3 {
                                                view! { <span class="table-toolbar-separator" aria-hidden="true"></span> }.into_any()
                                            } else {
                                                ().into_any()
                                            }}
                                            <button
                                                type="button"
                                                class="table-toolbar-btn"
                                                data-testid=testid
                                                title=action.title()
                                                aria-label=action.title()
                                                on:click=move |_| {
                                                    match run_table_toolbar_action(editor, action) {
                                                        Ok(message) => {
                                                            if matches!(action, TableToolbarAction::DeleteTable) {
                                                                set_table_toolbar_open.set(false);
                                                                set_table_options_open.set(false);
                                                                set_table_overlay_anchor.set(None);
                                                                set_table_selection_overlay.set(None);
                                                            } else {
                                                                set_table_selection_overlay.set(None);
                                                            }
                                                            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,
                                                            );
                                                            if !matches!(action, TableToolbarAction::DeleteTable) {
                                                                set_table_overlay_anchor.set(current_table_overlay_anchor());
                                                            }
                                                        }
                                                        Err(err) => set_command_feedback.set(err),
                                                    }
                                                }
                                            >
                                                {action.icon()}
                                            </button>
                                        </>
                                    }
                                }).collect_view()}
                                <span class="table-toolbar-separator" aria-hidden="true"></span>
                                <button
                                    type="button"
                                    class="table-toolbar-btn"
                                    data-testid="table-toolbar-options"
                                    title="选项"
                                    aria-label="选项"
                                    aria-expanded=move || table_options_open.get().to_string()
                                    on:click=move |_| {
                                        set_table_options_open.update(|open| *open = !*open);
                                    }
                                >
                                    "选项"
                                </button>
                                {move || {
                                    if !table_options_open.get() {
                                        return ().into_any();
                                    }
                                    view! {
                                        <div class="table-options-menu" data-testid="table-toolbar-options-menu">
                                            {TABLE_OPTION_ACTIONS.iter().map(|action| {
                                                let action = *action;
                                                let testid = format!("table-option-{}", action.id());
                                                view! {
                                                    <button
                                                        type="button"
                                                        class="table-option-btn"
                                                        data-testid=testid
                                                        role="switch"
                                                        aria-checked=move || table_option_checked(&document_json.get(), action).to_string()
                                                        on:click=move |_| {
                                                            match run_table_option_action(editor, action) {
                                                                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,
                                                                    );
                                                                }
                                                                Err(err) => set_command_feedback.set(err),
                                                            }
                                                        }
                                                    >
                                                        <span>{action.title()}</span>
                                                        <span class="table-option-switch" aria-hidden="true"></span>
                                                    </button>
                                                }
                                            }).collect_view()}
                                        </div>
                                    }.into_any()
                                }}
                            </div>
                        }.into_any()
                    }}
                    {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 ((!editor_focused.get() || !text_selection_active.get()) && !toolbar_locked)
                                || 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 current_duplicate_label = duplicate_block_label.clone();
                                            let current_delete_label = delete_block_label.clone();
                                            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,
                                            });
                                            let menu_top = format!("{}px", menu_layout.top);
                                            let menu_bottom = "auto";
                                            let current_block_is_page = top_level_block_is_page(editor, block_index);
                                            let page_current_marker = if current_block_is_page { "✓" } else { "Ctrl+Shift+9" };
                                            view! {
                                                <div
                                                    class="block-drag-menu"
                                                    data-testid="block-drag-menu"
                                                    style:top=menu_top
                                                    style:bottom=menu_bottom
                                                    style:left=format!("{}px", menu_layout.left)
                                                    style:max-height=format!("{}px", menu_layout.max_height)
                                                    on:mousedown=move |event: MouseEvent| {
                                                        event.stop_propagation();
                                                    }
                                                    on:mouseup=move |event: MouseEvent| {
                                                        event.stop_propagation();
                                                    }
                                                    on:mousemove=move |event: MouseEvent| {
                                                        event.stop_propagation();
                                                    }
                                                    on:click=move |event: MouseEvent| {
                                                        event.stop_propagation();
                                                    }
                                                    on:wheel=move |event: WheelEvent| {
                                                        trap_scroll_inside_menu(&event);
                                                    }
                                                >
                                                    <input
                                                        class="block-drag-command-input"
                                                        data-testid="block-drag-command-input"
                                                        readonly=true
                                                        placeholder="请输入指令"
                                                    />
                                                    <div class="block-drag-menu-actions">
                                                        {move || {
                                                            let duplicate_feedback_label = current_duplicate_label.clone();
                                                            let delete_feedback_label = current_delete_label.clone();
                                                            view! {
                                                                <>
                                                                    <div class="block-drag-menu-section">
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-ai"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                request_ai_edit_bridge(
                                                                                    editor,
                                                                                    "ask_ai",
                                                                                    document_id,
                                                                                    workspace_id,
                                                                                    Some(block_index),
                                                                                    runtime_block_id_from_index(block_index),
                                                                                    selection_state,
                                                                                    set_ai_bridge_state,
                                                                                    set_ai_bridge_message,
                                                                                    set_dirty_count,
                                                                                    set_html_output,
                                                                                    set_document_json,
                                                                                    set_json_output,
                                                                                    title,
                                                                                    set_command_feedback,
                                                                                );
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"✦"</span>
                                                                            <span>"AI 助理"</span>
                                                                            <span class="block-drag-menu-shortcut">"Ctrl+J"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-turn-into"
                                                                            on:mouseenter=move |_| {
                                                                                set_block_turn_into_open.set(true);
                                                                            }
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                set_block_turn_into_open.set(true);
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"↻"</span>
                                                                            <span>"转换为"</span>
                                                                            <span class="block-drag-menu-arrow">"›"</span>
                                                                        </button>
                                                                    </div>
                                                                    <div class="block-drag-menu-divider"></div>
                                                                    <div class="block-drag-menu-section">
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-duplicate"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                match editor.get_html() {
                                                                                    Ok(current_html) => match duplicate_top_level_block_html(&current_html, block_index) {
                                                                                        Ok(next_html) => {
                                                                                            apply_html_update(
                                                                                                editor,
                                                                                                &runtime_persisted_identity(document_id, workspace_id),
                                                                                                next_html,
                                                                                                document_id,
                                                                                                workspace_id,
                                                                                                dirty_count,
                                                                                                set_dirty_count,
                                                                                                set_html_output,
                                                                                                set_document_json,
                                                                                                set_json_output,
                                                                                                title,
                                                                                                hovered_block,
                                                                                                editor_focused,
                                                                                                slash_open,
                                                                                                turn_into_open,
                                                                                                color_menu_open,
                                                                                                more_menu_open,
                                                                                                revision,
                                                                                                conflict_detection_key,
                                                                                                read_only,
                                                                                                set_command_feedback,
                                                                                                format!("已复制 {}", duplicate_feedback_label.clone()),
                                                                                            );
                                                                                            set_block_menu_open.set(false);
                                                                                            set_block_menu_anchor.set(None);
                                                                                        }
                                                                                        Err(err) => set_command_feedback.set(err),
                                                                                    },
                                                                                    Err(err) => set_command_feedback.set(format!("读取当前 HTML 失败：{err}")),
                                                                                }
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"▣"</span>
                                                                            <span>"拷贝副本"</span>
                                                                            <span class="block-drag-menu-shortcut">"Ctrl+D"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-delete"
                                                                            on:mousedown=move |event: MouseEvent| {
                                                                                event.prevent_default();
                                                                                event.stop_propagation();
                                                                            }
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                match editor_runtime::history_safe_commands::delete_top_level_block_with_history(&editor, block_index) {
                                                                                    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,
                                                                                            format!("已删除 {}", delete_feedback_label.clone()),
                                                                                        );
                                                                                        set_block_menu_open.set(false);
                                                                                        set_block_menu_anchor.set(None);
                                                                                        set_hovered_block.set(None);
                                                                                        schedule_editor_focus(editor);
                                                                                    }
                                                                                    Err(err) => set_command_feedback.set(err),
                                                                                }
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"⌫"</span>
                                                                            <span>"删除"</span>
                                                                            <span class="block-drag-menu-shortcut">"Del"</span>
                                                                        </button>
                                                                    </div>
                                                                    <div class="block-drag-menu-divider"></div>
                                                                    <div class="block-drag-menu-section">
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-copy-link"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                let Some(block_id) = runtime_block_id_from_index(block_index) else {
                                                                                    set_command_feedback.set("当前块缺少 Rust block id，无法复制锚点链接".to_string());
                                                                                    return;
                                                                                };
                                                                                let Some(anchor_url) = current_page_anchor_url(&block_id) else {
                                                                                    set_command_feedback.set("当前页面 URL 不可用，无法复制锚点链接".to_string());
                                                                                    return;
                                                                                };
                                                                                if let Some(block_element) = editor_block_by_id(&block_id) {
                                                                                    let _ = block_element.scroll_into_view_with_bool(true);
                                                                                }
                                                                                spawn_local(async move {
                                                                                    match write_mnote_text_to_clipboard(anchor_url.clone()).await {
                                                                                        Ok(value) if value.as_bool().unwrap_or(false) => {
                                                                                            set_command_feedback.set(format!("已复制块链接：{}", block_id));
                                                                                        }
                                                                                        _ => {
                                                                                            set_command_feedback.set(format!("块链接已生成但剪贴板写入失败：{}", anchor_url));
                                                                                        }
                                                                                    }
                                                                                });
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"🔗"</span>
                                                                            <span>"复制链接"</span>
                                                                            <span class="block-drag-menu-arrow">"›"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-move"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                set_command_feedback.set("移动/嵌入流程后续接 Page Aggregate 命令".to_string());
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"↗"</span>
                                                                            <span>"移动/嵌入到..."</span>
                                                                            <span class="block-drag-menu-shortcut">"Alt+Shift+M/G"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-history"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                set_command_feedback.set("块历史入口已记录，恢复流程后续接版本链路".to_string());
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"◴"</span>
                                                                            <span>"块历史..."</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-comment"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                set_command_feedback.set("评论入口已打开".to_string());
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"◉"</span>
                                                                            <span>"评论"</span>
                                                                            <span class="block-drag-menu-shortcut">"Ctrl+Alt+M"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-color"
                                                                            on:mouseenter=move |_| {
                                                                                set_command_feedback.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
                                                                            }
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                set_command_feedback.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"▧"</span>
                                                                            <span>"颜色"</span>
                                                                            <span class="block-drag-menu-arrow">"›"</span>
                                                                        </button>
                                                                        <button
                                                                            class="block-drag-menu-item"
                                                                            data-testid="block-drag-menu-item-align-center"
                                                                            on:click=move |event: MouseEvent| {
                                                                                event.stop_propagation();
                                                                                let result = (|| -> Result<(), String> {
                                                                                    let document = editor
                                                                                        .get_json()
                                                                                        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
                                                                                    let range = top_level_block_range(&document, block_index)
                                                                                        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;
                                                                                    editor
                                                                                        .set_text_selection(range)
                                                                                        .map_err(|err| format!("选中当前块失败：{err}"))?;
                                                                                    apply_text_align(editor, TiptapTextAlign::Center)?;
                                                                                    let (html, snapshot, json_text) = read_editor_snapshot(editor);
                                                                                    set_dirty_count.update(|count| *count += 1);
                                                                                    set_html_output.set(html.clone());
                                                                                    set_document_json.set(snapshot.clone());
                                                                                    set_json_output.set(json_text);
                                                                                    persist_document_state(
                                                                                        &runtime_persisted_identity(document_id, workspace_id),
                                                                                        &title.get_untracked(),
                                                                                        &snapshot,
                                                                                        Some(html),
                                                                                    )
                                                                                    .map_err(|err| format!("文字居中已执行，但本地保存失败：{err}"))?;
                                                                                    Ok(())
                                                                                })();
                                                                                match result {
                                                                                    Ok(()) => set_command_feedback.set("已切到居中，并已写入本地草稿".to_string()),
                                                                                    Err(err) => set_command_feedback.set(err),
                                                                                }
                                                                            }
                                                                        >
                                                                            <span class="block-drag-menu-icon">"≡"</span>
                                                                            <span>"文字居中"</span>
                                                                            <span class="block-drag-menu-switch"></span>
                                                                        </button>
                                                                        <button class="block-drag-menu-item" data-testid="block-drag-menu-item-translate" disabled=true>
                                                                            <span class="block-drag-menu-icon">"文"</span>
                                                                            <span>"文字翻译"</span>
                                                                            <span class="block-drag-menu-arrow">"›"</span>
                                                                        </button>
                                                                        <button class="block-drag-menu-item" data-testid="block-drag-menu-item-poster" disabled=true>
                                                                            <span class="block-drag-menu-icon">"▤"</span>
                                                                            <span>"生成海报"</span>
                                                                        </button>
                                                                    </div>
                                                                    <div class="block-drag-menu-footer" data-testid="block-drag-menu-footer">
                                                                        <span>"anonymous"</span>
                                                                        <span>"最后编辑于 刚刚"</span>
                                                                    </div>
                                                                    {move || {
                                                                        if block_turn_into_open.get() {
                                                                            view! {
                                                                                <div class="block-drag-submenu" data-testid="block-transform-submenu" data-e30-testid="block-turn-into-menu">
                                                                                    {SLASH_ACTIONS
                                                                                        .iter()
                                                                                        .filter(|action| !matches!(action.kind, SlashActionKind::Divider | SlashActionKind::SimpleTable | SlashActionKind::Image | SlashActionKind::UploadAttachment | SlashActionKind::Toc | SlashActionKind::AiAssistant | SlashActionKind::AiWrite | SlashActionKind::ContinueWriting | SlashActionKind::Summarize | SlashActionKind::MoreAi))
                                                                                        .map(|action| {
                                                                                            let kind = action.kind;
                                                                                            let label = action.label;
                                                                                            let icon = action.icon;
                                                                                            let is_current = top_level_block_matches_action(editor, block_index, action.kind);
                                                                                            let shortcut = if is_current {
                                                                                                "✓"
                                                                                            } else {
                                                                                                match action.kind {
                                                                                                    SlashActionKind::Paragraph => "Ctrl+Alt+0",
                                                                                                    SlashActionKind::Todo => "Ctrl+Shift+5",
                                                                                                    SlashActionKind::Heading1 => "Ctrl+Shift+1",
                                                                                                    SlashActionKind::Heading2 => "Ctrl+Shift+2",
                                                                                                    SlashActionKind::Heading3 => "Ctrl+Shift+3",
                                                                                                    SlashActionKind::BulletList => "Ctrl+Shift+6",
                                                                                                    SlashActionKind::OrderedList => "Ctrl+Shift+7",
                                                                                                    SlashActionKind::CodeBlock => "Ctrl+Alt+-",
                                                                                                    SlashActionKind::Mindmap => "/dt",
                                                                                                    _ => "",
                                                                                                }
                                                                                            };
                                                                                            let testid = format!("block-transform-item-{}", action.id);
                                                                                            let action_menu_label = "块".to_string();
                                                                                            view! {
                                                                                                <button
                                                                                                    class="block-drag-menu-item"
                                                                                                    data-testid=testid
                                                                                                    data-current=is_current.to_string()
                                                                                                    on:click=move |event: MouseEvent| {
                                                                                                        event.stop_propagation();
                                                                                                        match run_block_turn_into_action(editor, block_index, kind) {
                                                                                                            Ok(message) => {
                                                                                                                set_block_menu_open.set(false);
                                                                                                                set_block_menu_anchor.set(None);
                                                                                                                set_block_turn_into_open.set(false);
                                                                                                                set_block_folded_title_open.set(false);
                                                                                                                let (html, snapshot, json_text) = read_editor_snapshot(editor);
                                                                                                                set_dirty_count.update(|count| *count += 1);
                                                                                                                set_html_output.set(html.clone());
                                                                                                                set_document_json.set(snapshot.clone());
                                                                                                                set_json_output.set(json_text);
                                                                                                                match persist_document_state(
                                                                                                                    &runtime_persisted_identity(document_id, workspace_id),
                                                                                                                    &title.get_untracked(),
                                                                                                                    &snapshot,
                                                                                                                    Some(html),
                                                                                                                ) {
                                                                                                                    Ok(()) => set_command_feedback.set(format!(
                                                                                                                        "{}：{} -> {}，并已写入本地草稿",
                                                                                                                        message,
                                                                                                                        action_menu_label.clone(),
                                                                                                                        label,
                                                                                                                    )),
                                                                                                                    Err(err) => set_command_feedback.set(format!(
                                                                                                                        "{}：{} -> {}，但本地保存失败：{}",
                                                                                                                        message,
                                                                                                                        action_menu_label.clone(),
                                                                                                                        label,
                                                                                                                        err,
                                                                                                                    )),
                                                                                                                }
                                                                                                            }
                                                                                                            Err(err) => set_command_feedback.set(err),
                                                                                                        }
                                                                                                    }
                                                                                                >
                                                                                                    <span class="block-drag-menu-icon">{icon}</span>
                                                                                                    <span>{label}</span>
                                                                                                    <span class="block-drag-menu-shortcut">{shortcut}</span>
                                                                                                </button>
                                                                                            }
                                                                                        })
                                                                                        .collect_view()}
                                                                                    <button
                                                                                        class="block-drag-menu-item"
                                                                                        data-testid="block-transform-item-page"
                                                                                        data-current=current_block_is_page.to_string()
                                                                                        on:click=move |event: MouseEvent| {
                                                                                            event.stop_propagation();
                                                                                            run_block_turn_into_page_action(
                                                                                                editor,
                                                                                                block_index,
                                                                                                document_id,
                                                                                                workspace_id,
                                                                                                title,
                                                                                                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,
                                                                                               );
                                                                                           }
                                                                                       >
                                                                                        <span class="block-drag-menu-icon">"▣"</span>
                                                                                      <span>"页面"</span>
                                                                                      <span class="block-drag-menu-shortcut">{page_current_marker}</span>
                                                                                  </button>
                                                                                  <button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true><span class="block-drag-menu-icon">"≡"</span><span>"折叠列表"</span><span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span></button>
                                                                                  <button
                                                                                      class="block-drag-menu-item"
                                                                                      data-testid="block-transform-item-folded-title"
                                                                                      on:mouseenter=move |_| {
                                                                                          set_block_folded_title_open.set(true);
                                                                                      }
                                                                                      on:click=move |event: MouseEvent| {
                                                                                          event.stop_propagation();
                                                                                          set_block_folded_title_open.set(true);
                                                                                      }
                                                                                  >
                                                                                      <span class="block-drag-menu-icon">"H1"</span>
                                                                                      <span>"折叠标题"</span>
                                                                                      <span class="block-drag-menu-arrow">"›"</span>
                                                                                  </button>
                                                                                  <button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true><span class="block-drag-menu-icon">"☑"</span><span>"折叠待办"</span><span class="block-drag-menu-arrow">"›"</span></button>
                                                                                  <button class="block-drag-menu-item" data-testid="block-transform-item-emphasis" disabled=true><span class="block-drag-menu-icon">"Aa"</span><span>"着重文字"</span></button>
                                                                                  <button class="block-drag-menu-item" data-testid="block-transform-item-math" disabled=true><span class="block-drag-menu-icon">"f(x)"</span><span>"数学公式"</span><span class="block-drag-menu-shortcut">"Ctrl+Alt++"</span></button>
                                                                                  {move || {
                                                                                      if block_folded_title_open.get() {
                                                                                          view! {
                                                                                              <div class="block-drag-tertiary-submenu" data-testid="block-transform-folded-title-submenu">
                                                                                                  {FOLDED_HEADING_ACTIONS
                                                                                                      .iter()
                                                                                                      .map(|folded| {
                                                                                                          let action = *folded;
                                                                                                          let testid = format!("block-transform-folded-heading-{}", action.id);
                                                                                                          view! {
                                                                                                              <button
                                                                                                                  class="block-drag-menu-item"
                                                                                                                  data-testid=testid
                                                                                                                  on:click=move |event: MouseEvent| {
                                                                                                                      event.stop_propagation();
                                                                                                                      match run_block_turn_into_folded_heading_action(editor, block_index, action.level) {
                                                                                                                          Ok(message) => {
                                                                                                                              set_block_menu_open.set(false);
                                                                                                                              set_block_menu_anchor.set(None);
                                                                                                                              set_block_turn_into_open.set(false);
                                                                                                                              set_block_folded_title_open.set(false);
                                                                                                                              let (html, snapshot, json_text) = read_editor_snapshot(editor);
                                                                                                                              set_dirty_count.update(|count| *count += 1);
                                                                                                                              set_html_output.set(html.clone());
                                                                                                                              set_document_json.set(snapshot.clone());
                                                                                                                              set_json_output.set(json_text);
                                                                                                                              match persist_document_state(
                                                                                                                                  &runtime_persisted_identity(document_id, workspace_id),
                                                                                                                                  &title.get_untracked(),
                                                                                                                                  &snapshot,
                                                                                                                                  Some(html),
                                                                                                                              ) {
                                                                                                                                  Ok(()) => set_command_feedback.set(format!("{}，并已写入本地草稿", message)),
                                                                                                                                  Err(err) => set_command_feedback.set(format!("{}，但本地保存失败：{}", message, err)),
                                                                                                                              }
                                                                                                                          }
                                                                                                                          Err(err) => set_command_feedback.set(err),
                                                                                                                      }
                                                                                                                  }
                                                                                                              >
                                                                                                                  <span class="block-drag-menu-icon">{action.icon}</span>
                                                                                                                  <span>{action.label}</span>
                                                                                                              </button>
                                                                                                          }
                                                                                                      })
                                                                                                      .collect_view()}
                                                                                              </div>
                                                                                          }.into_any()
                                                                                      } else {
                                                                                          ().into_any()
                                                                                      }
                                                                                  }}
                                                                              </div>
                                                                          }.into_any()
                                                                        } else {
                                                                            ().into_any()
                                                                        }
                                                                    }}
                                                                </>
                                                            }
                                                        }}
                                                    </div>
                                                </div>
                                            }.into_any()
                                        } else {
                                            ().into_any()
                                        }
                                    }}
                                </div>
                            }.into_any()
                        } else {
                            ().into_any()
                        }
                    }}

                    {move || {
                        if slash_open.get() {
                            view! {
                                <div
                                    class="slash-menu"
                                    data-testid="mnote-leptos-tiptap-slash-menu"
                                    style=slash_menu_anchor_style()
                                >
                                    <div class="slash-list">
                                        {move || {
                                            let query = slash_query.get().trim().to_ascii_lowercase();
                                            let show_turn_into = !query.is_empty()
                                                && ("zhw".contains(query.as_str())
                                                    || "转换为".contains(query.as_str()));
                                            let show_page = !query.is_empty()
                                                && ("ym".contains(query.as_str())
                                                    || "页面".contains(query.as_str()));
                                            if show_turn_into || show_page {
                                                view! {
                                                    <>
                                                        {if show_turn_into {
                                                            view! {
                                                                <>
                                                                    <div class="slash-section-label">"转换为"</div>
                                                                    <button
                                                                        class="slash-item"
                                                                        data-active="false"
                                                                        data-testid="slash-item-turn-into"
                                                                        on:click=move |_| {
                                                                            set_command_feedback.set("转换为入口请通过块菜单或选区工具栏执行".to_string());
                                                                        }
                                                                    >
                                                                        <span class="slash-item-row">
                                                                            <span class="slash-item-icon">"↻"</span>
                                                                            <span class="slash-item-copy">
                                                                                <strong>"转换为"</strong>
                                                                                <span>"打开块类型转换入口"</span>
                                                                            </span>
                                                                            <span class="slash-item-shortcut">"/zhw"</span>
                                                                        </span>
                                                                    </button>
                                                                </>
                                                            }.into_any()
                                                        } else {
                                                            ().into_any()
                                                        }}
                                                        {if show_page {
                                                            view! {
                                                                <>
                                                                    <div class="slash-section-label">"基础块列表"</div>
                                                                    <button
                                                                        class="slash-item"
                                                                        data-active="false"
                                                                        data-testid="slash-item-page"
                                                                        on:click=move |_| {
                                                                            set_command_feedback.set("页面入口请通过转换为页面真源链路执行".to_string());
                                                                        }
                                                                    >
                                                                        <span class="slash-item-row">
                                                                            <span class="slash-item-icon">"▣"</span>
                                                                            <span class="slash-item-copy">
                                                                                <strong>"页面"</strong>
                                                                                <span>"创建或转换为页面块"</span>
                                                                            </span>
                                                                            <span class="slash-item-shortcut">"/ym"</span>
                                                                        </span>
                                                                    </button>
                                                                </>
                                                            }.into_any()
                                                        } else {
                                                            ().into_any()
                                                        }}
                                                    </>
                                                }.into_any()
                                            } else {
                                                ().into_any()
                                            }
                                        }}
                                        {SLASH_ACTIONS.iter().enumerate().map(|(index, action)| {
                                            let is_selected = move || slash_index.get() == index;
                                            let kind = action.kind;
                                            let label = action.label;
                                            let description = action.description;
                                            let shortcut = action.shortcut;
                                            let icon = action.icon;
                                            let category = action.category;
                                            let show_category = index == 0
                                                || SLASH_ACTIONS[index - 1].category != category;
                                            let testid = format!("slash-item-{}", action.id);
                                            let slash_change_event_target = slash_change_event_target.clone();
                                            view! {
                                                <>
                                                    {if show_category {
                                                        view! { <div class="slash-section-label">{category}</div> }.into_any()
                                                    } else {
                                                        ().into_any()
                                                    }}
                                                    <button
                                                        class="slash-item"
                                                        data-active=move || is_selected().to_string()
                                                        data-testid=testid
                                                        on:click=move |_| {
                                                            match run_slash_action(editor, kind) {
                                                                Ok(message) => {
                                                                    set_slash_open.set(false);
                                                                    set_dirty_count.update(|count| *count += 1);
                                                                    let (html, snapshot, json_text) = read_editor_snapshot(editor);
                                                                    set_html_output.set(html.clone());
                                                                    set_document_json.set(snapshot.clone());
                                                                    set_json_output.set(json_text);
                                                                    if let Some(target) = slash_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),
                                                                    ) {
                                                                        Ok(()) => set_command_feedback.set(format!("{message}，并已写入本地草稿")),
                                                                        Err(err) => set_command_feedback.set(format!("{message}，但本地保存失败：{err}")),
                                                                    }
                                                                }
                                                                Err(err) => {
                                                                    set_slash_open.set(false);
                                                                    set_command_feedback.set(format!("命令执行失败：{err}"));
                                                                }
                                                            }
                                                        }
                                                    >
                                                        <span class="slash-item-row">
                                                            <span class="slash-item-icon">{icon}</span>
                                                            <span class="slash-item-copy">
                                                                <strong>{label}</strong>
                                                                <span>{description}</span>
                                                            </span>
                                                            <span class="slash-item-shortcut">{shortcut}</span>
                                                        </span>
                                                    </button>
                                                </>
                                            }
                                        }).collect_view()}
                                    </div>
                                </div>
                            }
                                .into_any()
                        } else {
                            ().into_any()
                        }
                    }}

                    <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>
    }
}

fn initial_content() -> TiptapContent {
    TiptapContent::json(serde_json::json!({
        "type": "doc",
        "content": []
    }))
}

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

// ── Phase B：Block Delta apply ──────────────────────────────

/// 将 delta operations 应用到 Tiptap JSON 树（原地修改）。
/// 返回 true 表示树发生了变更。
fn apply_block_delta_to_json(content: &mut Value, operations: &[Value]) -> bool {
    let Some(content_arr) = content.get_mut("content").and_then(Value::as_array_mut) else {
        return false;
    };
    let mut changed = false;

    for op_value in operations {
        let Some(op) = op_value.get("op").and_then(Value::as_str) else {
            continue;
        };
        match op {
            "replace" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
                let block_type = op_value.get("block_type").and_then(Value::as_str);
                if let Some(bid) = block_id {
                    if apply_replace_block(content_arr, bid, text, block_type) {
                        changed = true;
                    }
                }
            }
            "insert_after" => {
                let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
                if let (Some(aid), Some(bid)) = (anchor, block_id) {
                    if apply_insert_block_after(content_arr, aid, bid, text) {
                        changed = true;
                    }
                }
            }
            "delete" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                if let Some(bid) = block_id {
                    if apply_delete_block(content_arr, bid) {
                        changed = true;
                    }
                }
            }
            "move_after" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
                if let (Some(bid), Some(aid)) = (block_id, anchor) {
                    if apply_move_block_after(content_arr, bid, aid) {
                        changed = true;
                    }
                }
            }
            _ => {}
        }
    }
    changed
}

fn find_block_index(blocks: &[Value], block_id: &str) -> Option<usize> {
    blocks.iter().position(|b| {
        b.get("attrs")
            .and_then(|a| a.get("block_id"))
            .and_then(Value::as_str)
            == Some(block_id)
    })
}

fn apply_replace_block(
    blocks: &mut Vec<Value>,
    block_id: &str,
    text: &str,
    block_type: Option<&str>,
) -> bool {
    let Some(idx) = find_block_index(blocks, block_id) else { return false; };
    let block = &mut blocks[idx];

    // 更新 block type
    if let Some(bt) = block_type {
        if let Some(b) = block.as_object_mut() {
            b.insert("type".into(), json!(bt));
        }
    }

    // 更新 text content
    let new_content: Value = if text.is_empty() {
        json!([])
    } else {
        json!([{ "type": "text", "text": text }])
    };

    if let Some(b) = block.as_object_mut() {
        b.insert("content".into(), new_content);
    }
    true
}

fn apply_insert_block_after(
    blocks: &mut Vec<Value>,
    anchor_block_id: &str,
    new_block_id: &str,
    text: &str,
) -> bool {
    let Some(idx) = find_block_index(blocks, anchor_block_id) else { return false; };
    let new_block = json!({
        "type": "paragraph",
        "attrs": { "block_id": new_block_id },
        "content": if text.is_empty() {
            json!([])
        } else {
            json!([{ "type": "text", "text": text }])
        }
    });
    blocks.insert(idx + 1, new_block);
    true
}

fn apply_delete_block(blocks: &mut Vec<Value>, block_id: &str) -> bool {
    let Some(idx) = find_block_index(blocks, block_id) else { return false; };
    blocks.remove(idx);
    true
}

fn apply_move_block_after(
    blocks: &mut Vec<Value>,
    block_id: &str,
    anchor_block_id: &str,
) -> bool {
    let Some(block_idx) = find_block_index(blocks, block_id) else { return false; };
    let Some(anchor_idx) = find_block_index(blocks, anchor_block_id) else { return false; };

    // Can't move to itself or anchor after block
    if block_idx == anchor_idx || block_idx == anchor_idx + 1 {
        return false;
    }

    let block = blocks.remove(block_idx);
    // After removal, anchor may have shifted if block was before anchor
    let adjusted_anchor = if block_idx < anchor_idx {
        anchor_idx - 1
    } else {
        anchor_idx
    };
    blocks.insert(adjusted_anchor + 1, block);
    true
}
