Merge branch 'group/d7066a76/1-phase-b1-d-radix-leptos-tiptap'

This commit is contained in:
Agent Board
2026-06-16 01:27:46 +08:00
9 changed files with 191 additions and 79 deletions
@@ -15,6 +15,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
radix-leptos-primitives = "0.9.0"
leptos = { version = "0.8.19", features = ["csr"] } leptos = { version = "0.8.19", features = ["csr"] }
leptos_dom = "0.8.8" leptos_dom = "0.8.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -13,6 +13,7 @@ use crate::editor_runtime::{
dom_events::trap_scroll_inside_menu, dom_events::trap_scroll_inside_menu,
dom_selection::runtime_block_id_from_index, dom_selection::runtime_block_id_from_index,
editor_focus::schedule_editor_focus, editor_focus::schedule_editor_focus,
icons::material_icon,
persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity}, persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity},
slash_actions::{SlashActionKind, SLASH_ACTIONS}, slash_actions::{SlashActionKind, SLASH_ACTIONS},
}; };
@@ -34,25 +35,25 @@ const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
FoldedHeadingAction { FoldedHeadingAction {
level: 1, level: 1,
id: "1", id: "1",
icon: "H1", icon: "looks_one",
label: "折叠主标题", label: "折叠主标题",
}, },
FoldedHeadingAction { FoldedHeadingAction {
level: 2, level: 2,
id: "2", id: "2",
icon: "H2", icon: "looks_two",
label: "折叠大标题", label: "折叠大标题",
}, },
FoldedHeadingAction { FoldedHeadingAction {
level: 3, level: 3,
id: "3", id: "3",
icon: "H3", icon: "looks_3",
label: "折叠中标题", label: "折叠中标题",
}, },
FoldedHeadingAction { FoldedHeadingAction {
level: 4, level: 4,
id: "4", id: "4",
icon: "H4", icon: "looks_4",
label: "折叠小标题", label: "折叠小标题",
}, },
]; ];
@@ -244,7 +245,7 @@ fn block_transform_submenu_view(
handle_turn_into_action_click(editor, block_index, kind, label, signals); handle_turn_into_action_click(editor, block_index, kind, label, signals);
} }
> >
<span class="block-drag-menu-icon">{icon}</span> <span class="block-drag-menu-icon material-symbols-outlined">{icon}</span>
<span>{label}</span> <span>{label}</span>
<span class="block-drag-menu-shortcut">{shortcut}</span> <span class="block-drag-menu-shortcut">{shortcut}</span>
</button> </button>
@@ -274,12 +275,12 @@ fn block_transform_submenu_view(
); );
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("description")}</span>
<span>"页面"</span> <span>"页面"</span>
<span class="block-drag-menu-shortcut">{page_current_marker}</span> <span class="block-drag-menu-shortcut">{page_current_marker}</span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true> <button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true>
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("format_list_bulleted")}</span>
<span>"折叠列表"</span> <span>"折叠列表"</span>
<span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span> <span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span>
</button> </button>
@@ -294,21 +295,21 @@ fn block_transform_submenu_view(
signals.set_block_folded_title_open.set(true); signals.set_block_folded_title_open.set(true);
} }
> >
<span class="block-drag-menu-icon">"H1"</span> <span class="block-drag-menu-icon">{material_icon("looks_one")}</span>
<span>"折叠标题"</span> <span>"折叠标题"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true> <button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true>
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("checklist")}</span>
<span>"折叠待办"</span> <span>"折叠待办"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-transform-item-emphasis" disabled=true> <button class="block-drag-menu-item" data-testid="block-transform-item-emphasis" disabled=true>
<span class="block-drag-menu-icon">"Aa"</span> <span class="block-drag-menu-icon">{material_icon("text_fields")}</span>
<span>"着重文字"</span> <span>"着重文字"</span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-transform-item-math" disabled=true> <button class="block-drag-menu-item" data-testid="block-transform-item-math" disabled=true>
<span class="block-drag-menu-icon">"f(x)"</span> <span class="block-drag-menu-icon">{material_icon("function")}</span>
<span>"数学公式"</span> <span>"数学公式"</span>
<span class="block-drag-menu-shortcut">"Ctrl+Alt++"</span> <span class="block-drag-menu-shortcut">"Ctrl+Alt++"</span>
</button> </button>
@@ -330,7 +331,7 @@ fn block_transform_submenu_view(
handle_folded_heading_click(editor, block_index, action, signals); handle_folded_heading_click(editor, block_index, action, signals);
} }
> >
<span class="block-drag-menu-icon">{action.icon}</span> <span class="block-drag-menu-icon material-symbols-outlined">{action.icon}</span>
<span>{action.label}</span> <span>{action.label}</span>
</button> </button>
} }
@@ -415,7 +416,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
); );
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("auto_awesome")}</span>
<span>"AI 助理"</span> <span>"AI 助理"</span>
<span class="block-drag-menu-shortcut">"Ctrl+J"</span> <span class="block-drag-menu-shortcut">"Ctrl+J"</span>
</button> </button>
@@ -430,9 +431,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
signals.set_block_turn_into_open.set(true); signals.set_block_turn_into_open.set(true);
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("autorenew")}</span>
<span>"转换为"</span> <span>"转换为"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
</div> </div>
<div class="block-drag-menu-divider"></div> <div class="block-drag-menu-divider"></div>
@@ -462,7 +463,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
} }
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("content_copy")}</span>
<span>"拷贝副本"</span> <span>"拷贝副本"</span>
<span class="block-drag-menu-shortcut">"Ctrl+D"</span> <span class="block-drag-menu-shortcut">"Ctrl+D"</span>
</button> </button>
@@ -497,7 +498,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
} }
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("delete")}</span>
<span>"删除"</span> <span>"删除"</span>
<span class="block-drag-menu-shortcut">"Del"</span> <span class="block-drag-menu-shortcut">"Del"</span>
</button> </button>
@@ -538,9 +539,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
}); });
} }
> >
<span class="block-drag-menu-icon">"🔗"</span> <span class="block-drag-menu-icon">{material_icon("link")}</span>
<span>"复制链接"</span> <span>"复制链接"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
<button <button
class="block-drag-menu-item" class="block-drag-menu-item"
@@ -552,7 +553,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
.set("移动/嵌入流程后续接 Page Aggregate 命令".to_string()); .set("移动/嵌入流程后续接 Page Aggregate 命令".to_string());
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("arrow_outward")}</span>
<span>"移动/嵌入到..."</span> <span>"移动/嵌入到..."</span>
<span class="block-drag-menu-shortcut">"Alt+Shift+M/G"</span> <span class="block-drag-menu-shortcut">"Alt+Shift+M/G"</span>
</button> </button>
@@ -566,7 +567,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
.set("块历史入口已记录,恢复流程后续接版本链路".to_string()); .set("块历史入口已记录,恢复流程后续接版本链路".to_string());
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("history")}</span>
<span>"块历史..."</span> <span>"块历史..."</span>
</button> </button>
<button <button
@@ -577,7 +578,7 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
signals.set_command_feedback.set("评论入口已打开".to_string()); signals.set_command_feedback.set("评论入口已打开".to_string());
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("chat")}</span>
<span>"评论"</span> <span>"评论"</span>
<span class="block-drag-menu-shortcut">"Ctrl+Alt+M"</span> <span class="block-drag-menu-shortcut">"Ctrl+Alt+M"</span>
</button> </button>
@@ -596,9 +597,9 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
.set("颜色子菜单后续按 Wolai 继续补齐".to_string()); .set("颜色子菜单后续按 Wolai 继续补齐".to_string());
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("palette")}</span>
<span>"颜色"</span> <span>"颜色"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
<button <button
class="block-drag-menu-item" class="block-drag-menu-item"
@@ -637,17 +638,17 @@ pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl In
} }
} }
> >
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("format_align_center")}</span>
<span>"文字居中"</span> <span>"文字居中"</span>
<span class="block-drag-menu-switch"></span> <span class="block-drag-menu-switch"></span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-translate" disabled=true> <button class="block-drag-menu-item" data-testid="block-drag-menu-item-translate" disabled=true>
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("translate")}</span>
<span>"文字翻译"</span> <span>"文字翻译"</span>
<span class="block-drag-menu-arrow">""</span> <span class="block-drag-menu-arrow">{material_icon("chevron_right")}</span>
</button> </button>
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-poster" disabled=true> <button class="block-drag-menu-item" data-testid="block-drag-menu-item-poster" disabled=true>
<span class="block-drag-menu-icon">""</span> <span class="block-drag-menu-icon">{material_icon("dashboard")}</span>
<span>"生成海报"</span> <span>"生成海报"</span>
</button> </button>
</div> </div>
@@ -0,0 +1,83 @@
//! Unified icon helpers — Material Symbols ligatures for all editor runtime icons.
//!
//! Every icon rendered in the leptos-tiptap editor shell should flow through
//! `material_icon()` or consume `ICON_MAP` so that the set of Material Symbols
//! ligatures stays centralised and we can swap font providers later.
use leptos::prelude::*;
/// Semantic name → Material Symbols ligature.
///
/// Keys are stable semantic identifiers; values are the literal text content
/// that `material-symbols-outlined` will turn into a glyph via ligature.
pub(crate) const ICON_MAP: &[(&str, &str)] = &[
("heading_1", "looks_one"),
("heading_2", "looks_two"),
("heading_3", "looks_3"),
("heading_4", "looks_4"),
("chevron_right", "chevron_right"),
("chevron_left", "chevron_left"),
("check", "check"),
("text_fields", "text_fields"),
("auto_awesome", "auto_awesome"),
("edit", "edit"),
("redo", "redo"),
("summarize", "auto_awesome"),
("more_horiz", "more_horiz"),
("checklist", "checklist"),
("format_list_bulleted", "format_list_bulleted"),
("format_list_numbered", "format_list_numbered"),
("format_quote", "format_quote"),
("code", "code"),
("horizontal_rule", "horizontal_rule"),
("table_chart", "table_chart"),
("account_tree", "account_tree"),
("image", "image"),
("upload_file", "upload_file"),
("toc", "toc"),
("playlist_add", "playlist_add"),
("view_column", "view_column"),
("delete", "delete"),
("backspace", "backspace"),
("close", "close"),
("settings", "settings"),
("remove", "remove"),
("fullscreen_exit", "fullscreen_exit"),
("fullscreen", "fullscreen"),
("lock", "lock"),
("lock_open", "lock_open"),
("autorenew", "autorenew"),
("link", "link"),
("arrow_outward", "arrow_outward"),
("history", "history"),
("chat", "chat"),
("palette", "palette"),
("format_align_center", "format_align_center"),
("translate", "translate"),
("function", "function"),
("description", "description"),
("content_copy", "content_copy"),
("format_bold", "format_bold"),
("dashboard", "dashboard"),
("home", "home"),
("search", "search"),
("add", "add"),
];
/// Look up a Material Symbols ligature by semantic name.
pub(crate) fn icon_ligature(name: &str) -> Option<&'static str> {
ICON_MAP
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| *value)
}
/// Render a `<span class="material-symbols-outlined">` icon.
///
/// The `name` parameter must be a Material Symbols ligature name
/// (e.g. `"auto_awesome"`, `"chevron_right"`, `"looks_one"`).
pub(crate) fn material_icon(name: &str) -> impl IntoView {
view! {
<span class="material-symbols-outlined" aria-hidden="true">{name}</span>
}
}
@@ -21,6 +21,7 @@ use wasm_bindgen::{JsCast, JsValue};
use web_sys::{CustomEvent, CustomEventInit, EventTarget, HtmlElement, HtmlInputElement}; use web_sys::{CustomEvent, CustomEventInit, EventTarget, HtmlElement, HtmlInputElement};
use crate::editor_runtime::mount::next_mount_id; use crate::editor_runtime::mount::next_mount_id;
use crate::editor_runtime::icons::material_icon;
thread_local! { thread_local! {
static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell<HashMap<u32, MountedMindmapShell>> = std::cell::RefCell::new(HashMap::new()); static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell<HashMap<u32, MountedMindmapShell>> = std::cell::RefCell::new(HashMap::new());
@@ -493,7 +494,7 @@ pub(crate) fn MindmapShell(
); );
} }
> >
<span class="mnote-mindmap-tool-icon" aria-hidden="true">""</span> <span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("more_horiz")}</span>
<span class="mnote-mindmap-tool-label">"更多"</span> <span class="mnote-mindmap-tool-label">"更多"</span>
</button> </button>
{if toolbar_more_open { {if toolbar_more_open {
@@ -557,7 +558,7 @@ pub(crate) fn MindmapShell(
} }
} }
> >
"" {material_icon("chevron_right")}
</button> </button>
{sidebar_panels {sidebar_panels
.iter() .iter()
@@ -616,7 +617,7 @@ pub(crate) fn MindmapShell(
} }
} }
> >
"" {material_icon("chevron_left")}
</button> </button>
} }
.into_any() .into_any()
@@ -660,7 +661,7 @@ pub(crate) fn MindmapShell(
} }
} }
> >
"×" {material_icon("close")}
</button> </button>
</div> </div>
<div class="mnote-mindmap-side-options" data-testid=format!("mindmap-schema-sidebar-options-{}", panel.id)> <div class="mnote-mindmap-side-options" data-testid=format!("mindmap-schema-sidebar-options-{}", panel.id)>
@@ -711,7 +712,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="material-symbols-outlined" data-icon="home" aria-hidden="true"></span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("home")}</span></button>
<div class="mnote-mindmap-schema-navigator-search-wrap"> <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={ <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(); let action_target = action_target.clone();
@@ -721,7 +722,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("search")}</span></button>
{if search_open { {if search_open {
view! { view! {
<input <input
@@ -765,7 +766,7 @@ pub(crate) fn MindmapShell(
<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={ <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(); let action_target = action_target.clone();
move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_MINIMAP_EVENT, &MindmapShellMinimapPayload { open: !minimap_open }) 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> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("account_tree")}</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={ <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 action_target = action_target.clone();
let active_panel_id = active_panel_id.clone(); let active_panel_id = active_panel_id.clone();
@@ -776,7 +777,7 @@ pub(crate) fn MindmapShell(
panel_id: active_panel_id.as_deref(), panel_id: active_panel_id.as_deref(),
}) })
} }
}><span class="mnote-mindmap-tool-icon" aria-hidden="true">""</span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("settings")}</span></button>
</div> </div>
<div class="mnote-mindmap-schema-navigator-group"> <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={ <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={
@@ -787,7 +788,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="mnote-mindmap-tool-icon" aria-hidden="true">""</span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("remove")}</span></button>
<input <input
class="mnote-mindmap-schema-navigator-zoom" class="mnote-mindmap-schema-navigator-zoom"
data-testid="mindmap-schema-navigator-zoom-input" data-testid="mindmap-schema-navigator-zoom-input"
@@ -826,7 +827,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="material-symbols-outlined" data-icon="add" aria-hidden="true"></span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{material_icon("add")}</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={ <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(); let action_target = action_target.clone();
move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload { move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
@@ -835,7 +836,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if shell.fullscreen.is_fullscreen { "" } else { "" }}</span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if shell.fullscreen.is_fullscreen { material_icon("fullscreen_exit") } else { material_icon("fullscreen") }}</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={ <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; let action_target = action_target;
move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload { move |_| dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ACTION_EVENT, &MindmapShellActionPayload {
@@ -844,7 +845,7 @@ pub(crate) fn MindmapShell(
compat_path: None, compat_path: None,
source: None, source: None,
}) })
}><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if navigator.readonly { "" } else { "" }}</span></button> }><span class="mnote-mindmap-tool-icon" aria-hidden="true">{if navigator.readonly { material_icon("lock") } else { material_icon("lock_open") }}</span></button>
</div> </div>
</div> }.into_any() </div> }.into_any()
} else { } else {
@@ -2,6 +2,7 @@ pub(crate) mod attachment_links;
pub(crate) mod attachment_upload; pub(crate) mod attachment_upload;
pub(crate) mod block_dnd; pub(crate) mod block_dnd;
pub(crate) mod block_hover_state; pub(crate) mod block_hover_state;
pub(crate) mod icons;
pub(crate) mod block_handle_menu_view; pub(crate) mod block_handle_menu_view;
pub(crate) mod block_menu_document; pub(crate) mod block_menu_document;
pub(crate) mod block_menu_overlay; pub(crate) mod block_menu_overlay;
@@ -40,7 +40,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::AiAssistant, kind: SlashActionKind::AiAssistant,
id: "ai-assistant", id: "ai-assistant",
category: "AI 助理", category: "AI 助理",
icon: "", icon: "auto_awesome",
label: "AI 助理", label: "AI 助理",
description: "按 Wolai 基线保留 AI 入口", description: "按 Wolai 基线保留 AI 入口",
shortcut: "/ai", shortcut: "/ai",
@@ -49,7 +49,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::AiWrite, kind: SlashActionKind::AiWrite,
id: "ai-write", id: "ai-write",
category: "AI 助理", category: "AI 助理",
icon: "", icon: "edit",
label: "用 AI 写作", label: "用 AI 写作",
description: "唤起写作辅助入口", description: "唤起写作辅助入口",
shortcut: "/yaixz", shortcut: "/yaixz",
@@ -58,7 +58,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::ContinueWriting, kind: SlashActionKind::ContinueWriting,
id: "continue-writing", id: "continue-writing",
category: "AI 助理", category: "AI 助理",
icon: "", icon: "redo",
label: "续写", label: "续写",
description: "按当前上下文继续写作", description: "按当前上下文继续写作",
shortcut: "/xx", shortcut: "/xx",
@@ -67,7 +67,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Summarize, kind: SlashActionKind::Summarize,
id: "summarize", id: "summarize",
category: "AI 助理", category: "AI 助理",
icon: "", icon: "auto_awesome",
label: "总结", label: "总结",
description: "对当前内容生成摘要", description: "对当前内容生成摘要",
shortcut: "/zj", shortcut: "/zj",
@@ -76,7 +76,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::MoreAi, kind: SlashActionKind::MoreAi,
id: "more-ai", id: "more-ai",
category: "AI 助理", category: "AI 助理",
icon: "", icon: "more_horiz",
label: "更多", label: "更多",
description: "更多 AI 命令", description: "更多 AI 命令",
shortcut: "", shortcut: "",
@@ -85,7 +85,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Paragraph, kind: SlashActionKind::Paragraph,
id: "paragraph", id: "paragraph",
category: "基础块列表", category: "基础块列表",
icon: "Aa", icon: "text_fields",
label: "文本", label: "文本",
description: "普通正文块", description: "普通正文块",
shortcut: "/wb", shortcut: "/wb",
@@ -94,7 +94,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Todo, kind: SlashActionKind::Todo,
id: "todo", id: "todo",
category: "基础块列表", category: "基础块列表",
icon: "", icon: "checklist",
label: "待办列表", label: "待办列表",
description: "创建可勾选任务", description: "创建可勾选任务",
shortcut: "/dblb", shortcut: "/dblb",
@@ -103,7 +103,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::AdvancedTodo, kind: SlashActionKind::AdvancedTodo,
id: "advanced-todo", id: "advanced-todo",
category: "基础块列表", category: "基础块列表",
icon: "", icon: "checklist",
label: "高级待办列表", label: "高级待办列表",
description: "保留 Wolai 高级待办入口", description: "保留 Wolai 高级待办入口",
shortcut: "/gjdblb", shortcut: "/gjdblb",
@@ -112,7 +112,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Heading1, kind: SlashActionKind::Heading1,
id: "heading-1", id: "heading-1",
category: "基础块列表", category: "基础块列表",
icon: "H1", icon: "looks_one",
label: "主标题", label: "主标题",
description: "一级标题", description: "一级标题",
shortcut: "/h1", shortcut: "/h1",
@@ -121,7 +121,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Heading2, kind: SlashActionKind::Heading2,
id: "heading-2", id: "heading-2",
category: "基础块列表", category: "基础块列表",
icon: "H2", icon: "looks_two",
label: "大标题", label: "大标题",
description: "二级标题", description: "二级标题",
shortcut: "/h2", shortcut: "/h2",
@@ -130,7 +130,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Heading3, kind: SlashActionKind::Heading3,
id: "heading-3", id: "heading-3",
category: "基础块列表", category: "基础块列表",
icon: "H3", icon: "looks_3",
label: "中标题", label: "中标题",
description: "三级标题", description: "三级标题",
shortcut: "/h3", shortcut: "/h3",
@@ -139,7 +139,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Heading4, kind: SlashActionKind::Heading4,
id: "heading-4", id: "heading-4",
category: "基础块列表", category: "基础块列表",
icon: "H4", icon: "looks_4",
label: "小标题", label: "小标题",
description: "四级标题", description: "四级标题",
shortcut: "/h4", shortcut: "/h4",
@@ -148,7 +148,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::BulletList, kind: SlashActionKind::BulletList,
id: "bullet", id: "bullet",
category: "基础块列表", category: "基础块列表",
icon: "", icon: "format_list_bulleted",
label: "列表", label: "列表",
description: "记录普通要点", description: "记录普通要点",
shortcut: "/lb", shortcut: "/lb",
@@ -157,7 +157,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::OrderedList, kind: SlashActionKind::OrderedList,
id: "ordered", id: "ordered",
category: "基础块列表", category: "基础块列表",
icon: "1.", icon: "format_list_numbered",
label: "数字列表", label: "数字列表",
description: "记录步骤顺序", description: "记录步骤顺序",
shortcut: "/szlb", shortcut: "/szlb",
@@ -166,7 +166,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Quote, kind: SlashActionKind::Quote,
id: "quote", id: "quote",
category: "基础块列表", category: "基础块列表",
icon: "", icon: "format_quote",
label: "引述文字", label: "引述文字",
description: "包住注释与摘录", description: "包住注释与摘录",
shortcut: "/ys", shortcut: "/ys",
@@ -175,7 +175,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::CodeBlock, kind: SlashActionKind::CodeBlock,
id: "code-block", id: "code-block",
category: "基础块列表", category: "基础块列表",
icon: "<> ", icon: "code",
label: "代码片段", label: "代码片段",
description: "插入带语义的代码块", description: "插入带语义的代码块",
shortcut: "/dm", shortcut: "/dm",
@@ -184,7 +184,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Divider, kind: SlashActionKind::Divider,
id: "divider", id: "divider",
category: "基础块列表", category: "基础块列表",
icon: "", icon: "horizontal_rule",
label: "分割线", label: "分割线",
description: "插入分隔线", description: "插入分隔线",
shortcut: "/fgx", shortcut: "/fgx",
@@ -193,7 +193,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::SimpleTable, kind: SlashActionKind::SimpleTable,
id: "simple-table", id: "simple-table",
category: "进阶块列表", category: "进阶块列表",
icon: "", icon: "table_chart",
label: "简单表格", label: "简单表格",
description: "插入基础表格", description: "插入基础表格",
shortcut: "/jdbg", shortcut: "/jdbg",
@@ -202,7 +202,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Mindmap, kind: SlashActionKind::Mindmap,
id: "mindmap", id: "mindmap",
category: "进阶块列表", category: "进阶块列表",
icon: "", icon: "account_tree",
label: "思维导图", label: "思维导图",
description: "插入 kernel projection 导图块", description: "插入 kernel projection 导图块",
shortcut: "/dt", shortcut: "/dt",
@@ -211,7 +211,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Image, kind: SlashActionKind::Image,
id: "image", id: "image",
category: "媒体与附件", category: "媒体与附件",
icon: "", icon: "image",
label: "图片", label: "图片",
description: "上传并插入图片", description: "上传并插入图片",
shortcut: "/tp", shortcut: "/tp",
@@ -220,7 +220,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::UploadAttachment, kind: SlashActionKind::UploadAttachment,
id: "upload-attachment", id: "upload-attachment",
category: "媒体与附件", category: "媒体与附件",
icon: "", icon: "upload_file",
label: "上传附件", label: "上传附件",
description: "上传 Office、PDF 或其他文件", description: "上传 Office、PDF 或其他文件",
shortcut: "/fj", shortcut: "/fj",
@@ -229,7 +229,7 @@ pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
kind: SlashActionKind::Toc, kind: SlashActionKind::Toc,
id: "toc", id: "toc",
category: "进阶块列表", category: "进阶块列表",
icon: "", icon: "toc",
label: "页面目录", label: "页面目录",
description: "根据当前标题生成目录", description: "根据当前标题生成目录",
shortcut: "/toc", shortcut: "/toc",
@@ -3,6 +3,7 @@ use crate::editor_runtime::{
block_transform::run_slash_action, block_transform::run_slash_action,
bridge_events::{ChangeMetaPayload, ChangePayload}, bridge_events::{ChangeMetaPayload, ChangePayload},
command_sync::read_editor_snapshot, command_sync::read_editor_snapshot,
icons::material_icon,
overlays::toolbar_overlay_locked, overlays::toolbar_overlay_locked,
persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity}, persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity},
runtime_bridge::dispatch_change_event_to_target, runtime_bridge::dispatch_change_event_to_target,
@@ -59,6 +60,29 @@ fn runtime_persisted_identity(
persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked()) persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked())
} }
fn render_slash_shortcut(shortcut: &str) -> impl IntoView {
// Hardcoded Unicode→Material Symbols mappings for shortcuts that use
// non-text glyphs (chevron, arrows, triangles, etc.).
match shortcut {
"" => view! {
<span class="slash-item-shortcut">{material_icon("chevron_right")}</span>
}
.into_any(),
"" => view! {
<span class="slash-item-shortcut">{material_icon("autorenew")}</span>
}
.into_any(),
"" => view! {
<span class="slash-item-shortcut">{material_icon("arrow_drop_down")}</span>
}
.into_any(),
_ => view! {
<span class="slash-item-shortcut">{shortcut}</span>
}
.into_any(),
}
}
fn filtered_slash_intro_view( fn filtered_slash_intro_view(
query: &str, query: &str,
command_feedback: WriteSignal<String>, command_feedback: WriteSignal<String>,
@@ -83,12 +107,12 @@ fn filtered_slash_intro_view(
} }
> >
<span class="slash-item-row"> <span class="slash-item-row">
<span class="slash-item-icon">""</span> <span class="slash-item-icon">{material_icon("autorenew")}</span>
<span class="slash-item-copy"> <span class="slash-item-copy">
<strong>"转换为"</strong> <strong>"转换为"</strong>
<span>"打开块类型转换入口"</span> <span>"打开块类型转换入口"</span>
</span> </span>
<span class="slash-item-shortcut">"/zhw"</span> <span class="slash-item-shortcut">{render_slash_shortcut("/zhw")}</span>
</span> </span>
</button> </button>
</> </>
@@ -109,12 +133,12 @@ fn filtered_slash_intro_view(
} }
> >
<span class="slash-item-row"> <span class="slash-item-row">
<span class="slash-item-icon">""</span> <span class="slash-item-icon">{material_icon("description")}</span>
<span class="slash-item-copy"> <span class="slash-item-copy">
<strong>"页面"</strong> <strong>"页面"</strong>
<span>"创建或转换为页面块"</span> <span>"创建或转换为页面块"</span>
</span> </span>
<span class="slash-item-shortcut">"/ym"</span> <span class="slash-item-shortcut">{render_slash_shortcut("/ym")}</span>
</span> </span>
</button> </button>
</> </>
@@ -255,12 +279,12 @@ pub(crate) fn slash_menu_view(props: SlashMenuViewProps) -> impl IntoView {
} }
> >
<span class="slash-item-row"> <span class="slash-item-row">
<span class="slash-item-icon">{icon}</span> <span class="slash-item-icon material-symbols-outlined">{icon}</span>
<span class="slash-item-copy"> <span class="slash-item-copy">
<strong>{label}</strong> <strong>{label}</strong>
<span>{description}</span> <span>{description}</span>
</span> </span>
<span class="slash-item-shortcut">{shortcut}</span> <span class="slash-item-shortcut">{render_slash_shortcut(shortcut)}</span>
</span> </span>
</button> </button>
</> </>
@@ -32,12 +32,12 @@ impl TableToolbarAction {
pub(crate) fn icon(self) -> &'static str { pub(crate) fn icon(self) -> &'static str {
match self { match self {
Self::AddRowAfter => "+R", Self::AddRowAfter => "playlist_add",
Self::AddColumnAfter => "+C", Self::AddColumnAfter => "view_column",
Self::DeleteRow => "-R", Self::DeleteRow => "delete",
Self::DeleteColumn => "-C", Self::DeleteColumn => "delete",
Self::ClearCell => "", Self::ClearCell => "backspace",
Self::DeleteTable => "×", Self::DeleteTable => "close",
} }
} }
@@ -1,5 +1,6 @@
use crate::editor_runtime::{ use crate::editor_runtime::{
command_sync::sync_persisted_editor_command, command_sync::sync_persisted_editor_command,
icons::material_icon,
overlays::{ overlays::{
current_table_overlay_anchor, table_col_aux_style, table_row_aux_style, current_table_overlay_anchor, table_col_aux_style, table_row_aux_style,
table_selection_overlay_style, TableOverlayAnchor, TableSelectionKind, table_selection_overlay_style, TableOverlayAnchor, TableSelectionKind,
@@ -141,7 +142,7 @@ pub(crate) fn table_toolbar_view(
Err(err) => set_command_feedback.set(err), Err(err) => set_command_feedback.set(err),
} }
} }
>"+"</button> >{material_icon("add")}</button>
} }
}).collect_view()} }).collect_view()}
{(0..anchor.cols).map(|index| { {(0..anchor.cols).map(|index| {
@@ -203,7 +204,7 @@ pub(crate) fn table_toolbar_view(
Err(err) => set_command_feedback.set(err), Err(err) => set_command_feedback.set(err),
} }
} }
>"+"</button> >{material_icon("add")}</button>
} }
}).collect_view()} }).collect_view()}
</div> </div>
@@ -287,7 +288,7 @@ pub(crate) fn table_toolbar_view(
} }
} }
> >
{action.icon()} {material_icon(action.icon())}
</button> </button>
</> </>
} }