feat: add message component

This commit is contained in:
luoxiao 2023-10-19 17:16:57 +08:00
parent 92165edebf
commit 05bd1f3e5f
12 changed files with 316 additions and 35 deletions

View file

@ -29,6 +29,7 @@ icondata = { version = "0.1.0", features = [
"AiMinusOutlined",
] }
icondata_core = "0.0.2"
uuid = { version = "1.5.0", features = ["v4"] }
[workspace]
members = ["demo"]

View file

@ -1,43 +1,47 @@
use crate::pages::*;
use leptos::*;
use leptos_router::*;
use melt_ui::*;
#[component]
pub fn App() -> impl IntoView {
view! {
<Router base="/melt-ui">
<Routes base="/melt-ui".to_string()>
<Route path="/" view=Home/>
<Route path="/components" view=ComponentsPage>
<Route path="/menu" view=MenuPage/>
<Route path="/slider" view=SliderPage/>
<Route path="/tabbar" view=TabbarPage/>
<Route path="/nav-bar" view=NavBarPage/>
<Route path="/input" view=InputPage/>
<Route path="/image" view=ImagePage/>
<Route path="/modal" view=ModalPage/>
<Route path="/button" view=ButtonPage/>
<Route path="/checkbox" view=CheckboxPage/>
<Route path="/toast" view=ToastPage/>
<Route path="/tabs" view=TabsPage/>
<Route path="/select" view=SelectPage/>
<Route path="/space" view=SpacePage/>
<Route path="/table" view=TablePage/>
<Route path="/color-picker" view=ColorPickerPage/>
<Route path="/alert" view=AlertPage/>
<Route path="/grid" view=GridPage/>
<Route path="/auto-complete" view=AutoCompletePage/>
<Route path="/avatar" view=AvatarPage/>
<Route path="/badge" view=BadgePage/>
<Route path="/card" view=CardPage/>
<Route path="/divider" view=DividerPage/>
<Route path="/input-number" view=InputNumberPage/>
<Route path="/icon" view=IconPage/>
</Route>
<Route path="/mobile/tabbar" view=TabbarDemoPage/>
<Route path="/mobile/nav-bar" view=NavBarDemoPage/>
<Route path="/mobile/toast" view=ToastDemoPage/>
</Routes>
</Router>
<MessageProvider>
<Router base="/melt-ui">
<Routes base="/melt-ui".to_string()>
<Route path="/" view=Home/>
<Route path="/components" view=ComponentsPage>
<Route path="/menu" view=MenuPage/>
<Route path="/slider" view=SliderPage/>
<Route path="/tabbar" view=TabbarPage/>
<Route path="/nav-bar" view=NavBarPage/>
<Route path="/input" view=InputPage/>
<Route path="/image" view=ImagePage/>
<Route path="/modal" view=ModalPage/>
<Route path="/button" view=ButtonPage/>
<Route path="/checkbox" view=CheckboxPage/>
<Route path="/toast" view=ToastPage/>
<Route path="/tabs" view=TabsPage/>
<Route path="/select" view=SelectPage/>
<Route path="/space" view=SpacePage/>
<Route path="/table" view=TablePage/>
<Route path="/color-picker" view=ColorPickerPage/>
<Route path="/alert" view=AlertPage/>
<Route path="/grid" view=GridPage/>
<Route path="/auto-complete" view=AutoCompletePage/>
<Route path="/avatar" view=AvatarPage/>
<Route path="/badge" view=BadgePage/>
<Route path="/card" view=CardPage/>
<Route path="/divider" view=DividerPage/>
<Route path="/input-number" view=InputNumberPage/>
<Route path="/icon" view=IconPage/>
<Route path="/message" view=MessagePage/>
</Route>
<Route path="/mobile/tabbar" view=TabbarDemoPage/>
<Route path="/mobile/nav-bar" view=NavBarDemoPage/>
<Route path="/mobile/toast" view=ToastDemoPage/>
</Routes>
</Router>
</MessageProvider>
}
}

View file

@ -172,6 +172,10 @@ fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "badge".into(),
label: "Badge".into(),
},
MenuItemOption {
value: "message".into(),
label: "Message".into(),
},
MenuItemOption {
value: "modal".into(),
label: "Modal".into(),

View file

@ -0,0 +1,77 @@
use crate::components::{Demo, DemoCode};
use leptos::*;
use melt_ui::*;
use prisms::highlight_str;
#[component]
pub fn MessagePage() -> impl IntoView {
let message = use_message();
let success = move |_| {
message.create(
"Success".into(),
MessageVariant::Success,
Default::default(),
);
};
let warning = move |_| {
message.create(
"Warning".into(),
MessageVariant::Warning,
Default::default(),
);
};
let error = move |_| {
message.create("Error".into(), MessageVariant::Error, Default::default());
};
view! {
<div style="width: 896px; margin: 0 auto;">
<h1>"Message"</h1>
<Alert variant=AlertVariant::Warning title="Prerequisite">
"If you want to use message, you need to wrap the component where you call related methods inside MessageProvider and use use_message to get the API."
</Alert>
<Demo>
<Space>
<Button on:click=success>"Success"</Button>
<Button on:click=warning>"Warning"</Button>
<Button on:click=error>"Error"</Button>
</Space>
<DemoCode
slot
html=highlight_str!(
r#"
let message = use_message();
let success = move |_| {
message.create(
"Success".into(),
MessageVariant::Success,
Default::default(),
);
};
let warning = move |_| {
message.create(
"Warning".into(),
MessageVariant::Warning,
Default::default(),
);
};
let error = move |_| {
message.create("Error".into(), MessageVariant::Error, Default::default());
};
view! {
<Space>
<Button on:click=success>"Success"</Button>
<Button on:click=warning>"Warning"</Button>
<Button on:click=error>"Error"</Button>
</Space>
}
"#,
"rust"
)
>
""
</DemoCode>
</Demo>
</div>
}
}

View file

@ -15,6 +15,7 @@ mod image;
mod input;
mod input_number;
mod menu;
mod message;
mod mobile;
mod modal;
mod nav_bar;
@ -43,6 +44,7 @@ pub use image::*;
pub use input::*;
pub use input_number::*;
pub use menu::*;
pub use message::*;
pub use mobile::*;
pub use modal::*;
pub use nav_bar::*;

View file

@ -1,6 +1,6 @@
// copy https://github.com/Carlosted/leptos-icons
// leptos updated version causes leptos_icons error
pub use icondata::*;
pub(crate) use icondata::*;
use leptos::*;
/// The Icon component.

View file

@ -16,6 +16,7 @@ mod input;
mod input_number;
mod layout;
mod menu;
mod message;
pub mod mobile;
mod modal;
mod progress;
@ -46,6 +47,7 @@ pub use input::*;
pub use input_number::*;
pub use layout::*;
pub use menu::*;
pub use message::*;
pub use modal::*;
pub use progress::*;
pub use select::*;

44
src/message/message.css Normal file
View file

@ -0,0 +1,44 @@
.melt-message-container {
z-index: 6000;
position: fixed;
top: 12px;
left: 0;
right: 0;
height: 0;
overflow: visible;
display: flex;
flex-direction: column;
align-items: center;
}
.melt-message-wrapper {
margin-bottom: 8px;
}
.melt-message {
box-sizing: border-box;
display: flex;
align-items: center;
flex-wrap: nowrap;
overflow: hidden;
padding: 10px 20px;
max-width: 75vh;
background: #fff;
font-size: 14px;
border-radius: 3px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
.melt-message__icon {
width: 20px;
height: 20px;
margin-right: 10px;
font-size: 20px;
}
.melt-message__content {
line-height: 1.6;
}

44
src/message/message.rs Normal file
View file

@ -0,0 +1,44 @@
use crate::{theme::use_theme, Icon, Theme};
use icondata::*;
use leptos::*;
#[derive(Default, Clone)]
pub enum MessageVariant {
#[default]
Success,
Warning,
Error,
}
impl MessageVariant {
fn icon(&self) -> Icon {
match self {
MessageVariant::Success => icondata::Icon::Ai(AiCloseCircleFilled),
MessageVariant::Warning => icondata::Icon::Ai(AiExclamationCircleFilled),
MessageVariant::Error => icondata::Icon::Ai(AiCheckCircleFilled),
}
}
fn theme_color(&self, theme: &Theme) -> String {
match self {
MessageVariant::Success => theme.common.color_success.clone(),
MessageVariant::Warning => theme.common.color_warning.clone(),
MessageVariant::Error => theme.common.color_error.clone(),
}
}
}
#[component]
pub(crate) fn Message(variant: MessageVariant, content: String) -> impl IntoView {
let theme = use_theme(Theme::light);
let style = theme.with_untracked(|theme| format!("color: {};", variant.theme_color(theme)));
view! {
<div class="melt-message-wrapper">
<div class="melt-message">
<div class="melt-message__icon">
<Icon icon=variant.icon() style/>
</div>
<div class="melt-message__content">{content}</div>
</div>
</div>
}
}

View file

@ -0,0 +1,19 @@
use super::{message::Message, message_provider::MessageType};
use leptos::*;
use uuid::Uuid;
#[component]
pub fn MessageEnvironment(
message: MessageType,
#[prop(into)] on_internal_after_leave: Callback<Uuid>,
) -> impl IntoView {
let (id, content, variant, options) = message;
set_timeout(
move || {
on_internal_after_leave.call(id);
},
options.duration,
);
view! { <Message content variant/> }
}

View file

@ -0,0 +1,78 @@
use std::time::Duration;
use super::{message::MessageVariant, message_environment::MessageEnvironment};
use crate::{mount_style, teleport::Teleport};
use leptos::*;
use uuid::Uuid;
#[component]
pub fn MessageProvider(children: Children) -> impl IntoView {
mount_style("message", include_str!("./message.css"));
let message_list = create_rw_signal::<Vec<MessageType>>(vec![]);
provide_context(MessageInjection::new(message_list));
let handle_after_leave = move |id| {
message_list.update(move |message_list| {
let Some(index) = message_list.iter().position(|message| id == message.0) else {
return;
};
message_list.remove(index);
});
};
view! {
{children()}
<Teleport>
<div class="melt-message-container">
<For
each=move || message_list.get()
key=|message| message.0
children=move |message| {
view! {
<MessageEnvironment message on_internal_after_leave=handle_after_leave/>
}
}
/>
</div>
</Teleport>
}
}
pub(crate) type MessageType = (Uuid, String, MessageVariant, MessageOptions);
#[derive(Clone)]
pub struct MessageOptions {
pub duration: Duration,
}
impl Default for MessageOptions {
fn default() -> Self {
Self {
duration: Duration::from_secs(3),
}
}
}
#[derive(Clone)]
pub struct MessageInjection {
message_list: RwSignal<Vec<MessageType>>,
}
impl Copy for MessageInjection {}
impl MessageInjection {
fn new(message_list: RwSignal<Vec<MessageType>>) -> Self {
Self { message_list }
}
pub fn create(&self, content: String, variant: MessageVariant, options: MessageOptions) {
self.message_list.update(move |message_list| {
let id = uuid::Uuid::new_v4();
message_list.push((id, content, variant, options));
});
}
}
pub fn use_message() -> MessageInjection {
expect_context::<MessageInjection>()
}

6
src/message/mod.rs Normal file
View file

@ -0,0 +1,6 @@
mod message;
mod message_environment;
mod message_provider;
pub use message::*;
pub use message_provider::*;