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