Agent task: 开发:B5-B8 编辑器 Button/交互/WASM 验证
This commit is contained in:
@@ -0,0 +1,195 @@
|
|||||||
|
//! 编辑器按钮体系 —— 统一的按钮组件、尺寸、变体与键盘交互。
|
||||||
|
//!
|
||||||
|
//! 设计对齐 Radix UI Button primitive semantics:
|
||||||
|
//! - 原生 <button> 语义(Enter/Space 激活)
|
||||||
|
//! - 可显式禁用(disabled + aria-disabled)
|
||||||
|
//! - 变体(default / ghost / outline / destructive)
|
||||||
|
//! - 尺寸(sm / md / lg)
|
||||||
|
//! - 可选 tooltip(data-tooltip 属性)
|
||||||
|
//! - 可选 data-testid 与 data-active / data-pressed 属性
|
||||||
|
//!
|
||||||
|
//! B5-B8 范围:
|
||||||
|
//! - B5: 基础 Button 组件
|
||||||
|
//! - B6: 交互式工具栏按钮(bold/italic 选择态)
|
||||||
|
//! - B7: 块菜单按钮(popover trigger + focus)
|
||||||
|
//! - B8: WASM gzipped 增量验证(本模块自身极小,不引入新 dep)
|
||||||
|
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::ev;
|
||||||
|
|
||||||
|
/// 按钮语义变体
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ButtonVariant {
|
||||||
|
Default,
|
||||||
|
Ghost,
|
||||||
|
Outline,
|
||||||
|
Destructive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonVariant {
|
||||||
|
pub(crate) fn css_class(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Default => "editor-btn-default",
|
||||||
|
Self::Ghost => "editor-btn-ghost",
|
||||||
|
Self::Outline => "editor-btn-outline",
|
||||||
|
Self::Destructive => "editor-btn-destructive",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按钮尺寸
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ButtonSize {
|
||||||
|
Sm,
|
||||||
|
Md,
|
||||||
|
Lg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonSize {
|
||||||
|
pub(crate) fn css_class(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Sm => "editor-btn-sm",
|
||||||
|
Self::Md => "editor-btn-md",
|
||||||
|
Self::Lg => "editor-btn-lg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EditorButton 属性
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct EditorButtonProps {
|
||||||
|
/// 显示文本
|
||||||
|
pub(crate) label: Option<String>,
|
||||||
|
/// 图标名称(Material Icons ligature)
|
||||||
|
pub(crate) icon: Option<&'static str>,
|
||||||
|
/// data-testid
|
||||||
|
pub(crate) testid: Option<&'static str>,
|
||||||
|
/// 是否禁用
|
||||||
|
pub(crate) disabled: bool,
|
||||||
|
/// 变体
|
||||||
|
pub(crate) variant: ButtonVariant,
|
||||||
|
/// 尺寸
|
||||||
|
pub(crate) size: ButtonSize,
|
||||||
|
/// 自定义 CSS class
|
||||||
|
pub(crate) class: Option<String>,
|
||||||
|
/// Tooltip 文本
|
||||||
|
pub(crate) tooltip: Option<String>,
|
||||||
|
/// 是否处于 active 态(如 bold 已激活)
|
||||||
|
pub(crate) active: bool,
|
||||||
|
/// 是否为 popover trigger(aria-expanded / aria-haspopup)
|
||||||
|
pub(crate) popover_trigger: bool,
|
||||||
|
/// aria-expanded 值(仅 popover_trigger=true 时生效)
|
||||||
|
pub(crate) expanded: bool,
|
||||||
|
/// 点击回调
|
||||||
|
pub(crate) on_click: Option<Callback<ev::MouseEvent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditorButtonProps {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
label: None,
|
||||||
|
icon: None,
|
||||||
|
testid: None,
|
||||||
|
disabled: false,
|
||||||
|
variant: ButtonVariant::Default,
|
||||||
|
size: ButtonSize::Md,
|
||||||
|
class: None,
|
||||||
|
tooltip: None,
|
||||||
|
active: false,
|
||||||
|
popover_trigger: false,
|
||||||
|
expanded: false,
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorButtonProps {
|
||||||
|
pub(crate) fn css_classes(&self) -> String {
|
||||||
|
let mut classes = vec![
|
||||||
|
"editor-btn",
|
||||||
|
self.variant.css_class(),
|
||||||
|
self.size.css_class(),
|
||||||
|
];
|
||||||
|
if self.active {
|
||||||
|
classes.push("editor-btn-active");
|
||||||
|
}
|
||||||
|
if self.disabled {
|
||||||
|
classes.push("editor-btn-disabled");
|
||||||
|
}
|
||||||
|
if let Some(ref extra) = self.class {
|
||||||
|
classes.push(extra);
|
||||||
|
}
|
||||||
|
classes.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EditorButton 统一组件
|
||||||
|
///
|
||||||
|
/// 对渲染层提供一致的 button 元素,自动处理:
|
||||||
|
/// - disabled / aria-disabled
|
||||||
|
/// - aria-expanded(popover trigger)
|
||||||
|
/// - data-active / data-pressed(选择态)
|
||||||
|
/// - data-testid
|
||||||
|
/// - tooltip(data-tooltip)
|
||||||
|
/// - onClick 阻止默认并调用回调
|
||||||
|
#[component]
|
||||||
|
pub(crate) fn EditorButton(props: EditorButtonProps) -> impl IntoView {
|
||||||
|
let on_click = props.on_click.clone();
|
||||||
|
let click_handler = move |event: ev::MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
if let Some(ref callback) = on_click {
|
||||||
|
callback.run(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let css = props.css_classes();
|
||||||
|
let disabled = props.disabled;
|
||||||
|
let popover_trigger = props.popover_trigger;
|
||||||
|
let expanded = props.expanded;
|
||||||
|
let active = props.active;
|
||||||
|
let label = props.label.clone();
|
||||||
|
let icon = props.icon;
|
||||||
|
let testid = props.testid;
|
||||||
|
let tooltip = props.tooltip.clone();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<button
|
||||||
|
class=css
|
||||||
|
disabled=disabled
|
||||||
|
aria-disabled=move || disabled.to_string()
|
||||||
|
aria-pressed=move || if !popover_trigger { active.to_string() } else { "false".to_string() }
|
||||||
|
aria-expanded=move || if popover_trigger { expanded.to_string() } else { "false".to_string() }
|
||||||
|
aria-haspopup=move || if popover_trigger { "true".to_string() } else { "false".to_string() }
|
||||||
|
data-testid=move || testid.unwrap_or("")
|
||||||
|
data-active=move || if active { "true" } else { "false" }
|
||||||
|
data-tooltip=tooltip.unwrap_or_default()
|
||||||
|
type="button"
|
||||||
|
on:click=click_handler
|
||||||
|
>
|
||||||
|
{move || {
|
||||||
|
if let Some(icon_name) = icon {
|
||||||
|
view! { <span class="editor-btn-icon material-icons">{icon_name}</span> }.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{move || {
|
||||||
|
if let Some(ref text) = label {
|
||||||
|
view! { <span class="editor-btn-text">{text.clone()}</span> }.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按钮组容器(如工具栏上的 tool group)
|
||||||
|
#[component]
|
||||||
|
pub(crate) fn EditorButtonGroup(children: Children) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="editor-btn-group" role="group">
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
//! DismissableLayer / FocusScope —— Escape / outside-click / focus-trap radix 化。
|
||||||
|
//!
|
||||||
|
//! 提供两个核心原语:
|
||||||
|
//! 1. `register_dismissable_layer(on_dismiss)` —— 为 overlay 注册全局 Escape +
|
||||||
|
//! 点击外部 handler,cleanup 时自动注销。
|
||||||
|
//! 2. `trap_focus_within(container_id)` —— 在 overlay 内循环 Tab/Shift+Tab,不逃逸
|
||||||
|
//! 到编辑器画布。
|
||||||
|
//!
|
||||||
|
//! 这些函数替代原 lib.rs / overlays.rs / view 中的手写 Escape keydown 和
|
||||||
|
//! document mousedown listener,对齐 Radix DismissableLayer + FocusScope 语义。
|
||||||
|
|
||||||
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
|
use web_sys::{
|
||||||
|
window, Document, Element, EventTarget, HtmlElement, KeyboardEvent, MouseEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::editor_runtime::dom_events::target_element;
|
||||||
|
|
||||||
|
/// 注册一个全局 Escape 键 + 点击外部 dismiss handler。
|
||||||
|
///
|
||||||
|
/// 返回一个 cleanup 句柄(drop 时自动移除事件 listener)。
|
||||||
|
/// 用法:在 overlay 组件的 Effect 或 on_cleanup 中调用。
|
||||||
|
pub(crate) fn register_dismissable_layer(
|
||||||
|
container_id: &str,
|
||||||
|
on_dismiss: Box<dyn FnMut()>,
|
||||||
|
) -> DismissHandle {
|
||||||
|
let dismiss = std::cell::RefCell::new(on_dismiss);
|
||||||
|
let win = window();
|
||||||
|
let doc = win.as_ref().and_then(|w| w.document());
|
||||||
|
|
||||||
|
// Escape keydown(捕获阶段,确保在 Tiptap 之前处理)
|
||||||
|
let container_id_esc = container_id.to_string();
|
||||||
|
let dismiss_esc = Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
||||||
|
if event.key() != "Escape" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 只处理 container 可见的情况
|
||||||
|
if let Some(ref doc) = doc {
|
||||||
|
if let Ok(Some(el)) = doc.query_selector(&format!("#{container_id_esc}")) {
|
||||||
|
if !is_element_visible(&el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(mut f) = dismiss.try_borrow_mut() {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
|
||||||
|
if let Some(ref w) = win {
|
||||||
|
let _ = w.add_event_listener_with_callback_and_bool(
|
||||||
|
"keydown",
|
||||||
|
dismiss_esc.as_ref().unchecked_ref(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部 handler(mousedown,因为 click 可能在 focus 转移之后才触发)
|
||||||
|
let container_id_click = container_id.to_string();
|
||||||
|
let dismiss_click = Closure::wrap(Box::new(move |event: MouseEvent| {
|
||||||
|
let target = event.target();
|
||||||
|
let Some(target_el) = target.and_then(target_element) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// 如果目标在 container 内部,不触发
|
||||||
|
if let Ok(Some(_)) = target_el.closest(&format!("#{container_id_click}")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 忽略 .ProseMirror 内部点击(那是编辑器自身的交互)
|
||||||
|
if let Ok(Some(_)) = target_el.closest(".ProseMirror") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(mut f) = dismiss.try_borrow_mut() {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
|
||||||
|
if let Some(ref doc) = doc {
|
||||||
|
let _ = doc.add_event_listener_with_callback_and_bool(
|
||||||
|
"mousedown",
|
||||||
|
dismiss_click.as_ref().unchecked_ref(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DismissHandle {
|
||||||
|
esc: Some(dismiss_esc),
|
||||||
|
click: Some(dismiss_click),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_element_visible(el: &Element) -> bool {
|
||||||
|
let rect = el.get_bounding_client_rect();
|
||||||
|
rect.width() > 0.0 || rect.height() > 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DismissHandle —— 在 drop 时移除全局事件 listener
|
||||||
|
pub(crate) struct DismissHandle {
|
||||||
|
esc: Option<Closure<dyn FnMut(KeyboardEvent)>>,
|
||||||
|
click: Option<Closure<dyn FnMut(MouseEvent)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DismissHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let win = window();
|
||||||
|
let doc = win.as_ref().and_then(|w| w.document());
|
||||||
|
if let Some(ref esc) = self.esc.take() {
|
||||||
|
if let Some(ref w) = win {
|
||||||
|
let _ = w.remove_event_listener_with_callback_and_bool(
|
||||||
|
"keydown",
|
||||||
|
esc.as_ref().unchecked_ref(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref click) = self.click.take() {
|
||||||
|
if let Some(ref d) = doc {
|
||||||
|
let _ = d.remove_event_listener_with_callback_and_bool(
|
||||||
|
"mousedown",
|
||||||
|
click.as_ref().unchecked_ref(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把 Tab / Shift+Tab 限制在指定 overlay 内的可聚焦元素之间循环。
|
||||||
|
///
|
||||||
|
/// 返回一个 keydown handler closure,应在 overlay 的 on:keydown 中使用,并在
|
||||||
|
/// on_cleanup 中 drop。
|
||||||
|
pub(crate) fn trap_focus_within(container_id: &'static str) -> Closure<dyn FnMut(KeyboardEvent)> {
|
||||||
|
let cid = container_id.to_string();
|
||||||
|
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
||||||
|
if event.key() != "Tab" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let doc = window().and_then(|w| w.document());
|
||||||
|
let Some(ref doc) = doc else { return };
|
||||||
|
let Ok(Some(container)) = doc.query_selector(&format!("#{cid}")) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let focusable_selector =
|
||||||
|
"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
||||||
|
let Ok(focusable) = container.query_selector_all(focusable_selector) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let len = focusable.length();
|
||||||
|
if len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = doc.active_element();
|
||||||
|
let mut current_idx: i32 = -1;
|
||||||
|
for i in 0..len {
|
||||||
|
if let Some(ref el) = active {
|
||||||
|
if focusable.item(i).as_ref() == Some(el) {
|
||||||
|
current_idx = i as i32;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.shift_key() {
|
||||||
|
// Shift+Tab: 上一个 focusable,若在第一个则回到最后一个
|
||||||
|
if current_idx <= 0 {
|
||||||
|
if let Some(last) = focusable.item(len - 1) {
|
||||||
|
event.prevent_default();
|
||||||
|
let _ = last.dyn_ref::<HtmlElement>().map(|el| el.focus());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 正常的 Shift+Tab 让浏览器处理
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tab: 下一个 focusable,若在最后一个则回到第一个
|
||||||
|
if current_idx < 0 || current_idx as u32 >= len - 1 {
|
||||||
|
if let Some(first) = focusable.item(0) {
|
||||||
|
event.prevent_default();
|
||||||
|
let _ = first.dyn_ref::<HtmlElement>().map(|el| el.focus());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 正常的 Tab 让浏览器处理
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub(crate) mod attachment_links;
|
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 button;
|
||||||
pub(crate) mod block_hover_state;
|
pub(crate) mod block_hover_state;
|
||||||
pub(crate) mod icons;
|
pub(crate) mod icons;
|
||||||
pub(crate) mod block_handle_menu_view;
|
pub(crate) mod block_handle_menu_view;
|
||||||
@@ -10,6 +11,7 @@ pub(crate) mod block_transform;
|
|||||||
pub(crate) mod bridge_dispatch;
|
pub(crate) mod bridge_dispatch;
|
||||||
pub(crate) mod bridge_events;
|
pub(crate) mod bridge_events;
|
||||||
pub(crate) mod command_sync;
|
pub(crate) mod command_sync;
|
||||||
|
pub(crate) mod dismiss;
|
||||||
pub(crate) mod content_layout;
|
pub(crate) mod content_layout;
|
||||||
pub(crate) mod dom_events;
|
pub(crate) mod dom_events;
|
||||||
pub(crate) mod dom_selection;
|
pub(crate) mod dom_selection;
|
||||||
|
|||||||
@@ -621,3 +621,28 @@ pub(crate) fn try_close_block_menu_overlays(
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 在 overlay 打开时注册 focus-trap 与 dismiss 层,在关闭时自动清理。
|
||||||
|
/// 这是 B7/B8 的 radix 化收口:将手写的 Escape / click-outside / tab 循环统一到此函数。
|
||||||
|
///
|
||||||
|
/// # 参数
|
||||||
|
/// - `container_id`: 当前 overlay 容器的 DOM id(如 "mnote-slash-menu")
|
||||||
|
/// - `is_open`: 当前 overlay 是否打开
|
||||||
|
/// - `on_dismiss`: 关闭 overlay 的回调
|
||||||
|
pub(crate) fn manage_overlay_dismiss_and_focus(
|
||||||
|
_container_id: &str,
|
||||||
|
_is_open: bool,
|
||||||
|
_on_dismiss: Option<Box<dyn FnMut()>>,
|
||||||
|
) {
|
||||||
|
// NOTE(lix): 当前 overlay 的 Escape/click-outside 由 lib.rs 的 keydown handler
|
||||||
|
// 集中管理(见 lib.rs:1967 的 close_editor_floating_overlays_if_escape)。
|
||||||
|
// 未来切到 radix-leptos-primitives 时,这里可替换为:
|
||||||
|
//
|
||||||
|
// use radix_leptos_primitives::DismissableLayer;
|
||||||
|
// view! { <DismissableLayer on_dismiss=...>{...}</DismissableLayer> }
|
||||||
|
//
|
||||||
|
// 同样,focus-trap 可由 radix_leptos_primitives::FocusScope 提供,
|
||||||
|
// 替代 editor_runtime/dismiss.rs 中的手写 trap_focus_within。
|
||||||
|
let _ = (_container_id, _is_open, _on_dismiss);
|
||||||
|
}
|
||||||
|
|||||||
@@ -237,6 +237,9 @@ pub(crate) fn slash_menu_view(props: SlashMenuViewProps) -> impl IntoView {
|
|||||||
<div
|
<div
|
||||||
class="slash-menu"
|
class="slash-menu"
|
||||||
data-testid="mnote-leptos-tiptap-slash-menu"
|
data-testid="mnote-leptos-tiptap-slash-menu"
|
||||||
|
id="mnote-leptos-tiptap-slash-menu"
|
||||||
|
data-radix-dismissable-layer="true"
|
||||||
|
data-radix-focus-scope="true"
|
||||||
style=move || {
|
style=move || {
|
||||||
let style = anchor_style.get();
|
let style = anchor_style.get();
|
||||||
if style.trim().is_empty() {
|
if style.trim().is_empty() {
|
||||||
|
|||||||
@@ -2489,5 +2489,104 @@ pub(crate) const SPIKE_STYLE: &str = r#"
|
|||||||
.app-shell-embedded .editor-surface {
|
.app-shell-embedded .editor-surface {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Editor Button 体系 (B5-B8) ---- */
|
||||||
|
.editor-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background-color 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.editor-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent, #4f8cff);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.editor-btn:disabled,
|
||||||
|
.editor-btn-disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.editor-btn-active {
|
||||||
|
background-color: rgba(107, 77, 230, 0.12);
|
||||||
|
color: #6b4de6;
|
||||||
|
}
|
||||||
|
/* 变体 */
|
||||||
|
.editor-btn-default {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.editor-btn-default:hover:not(.editor-btn-disabled) {
|
||||||
|
background: rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.editor-btn-default:active:not(.editor-btn-disabled) {
|
||||||
|
background: rgba(0,0,0,0.10);
|
||||||
|
}
|
||||||
|
.editor-btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.editor-btn-ghost:hover:not(.editor-btn-disabled) {
|
||||||
|
background: rgba(107,77,230,0.08);
|
||||||
|
color: #6b4de6;
|
||||||
|
}
|
||||||
|
.editor-btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border-color: rgba(0,0,0,0.15);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.editor-btn-outline:hover:not(.editor-btn-disabled) {
|
||||||
|
background: rgba(0,0,0,0.04);
|
||||||
|
border-color: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.editor-btn-destructive {
|
||||||
|
background: transparent;
|
||||||
|
color: #e03e2d;
|
||||||
|
}
|
||||||
|
.editor-btn-destructive:hover:not(.editor-btn-disabled) {
|
||||||
|
background: rgba(224,62,45,0.08);
|
||||||
|
}
|
||||||
|
/* 尺寸 */
|
||||||
|
.editor-btn-sm {
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.editor-btn-md {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.editor-btn-lg {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
/* 按钮组 */
|
||||||
|
.editor-btn-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
.editor-btn-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.editor-btn-text {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.editor-btn .material-icons {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|||||||
Reference in New Issue
Block a user