mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
feat: optimize Toast component
This commit is contained in:
parent
0839db8ec4
commit
1d7237e6de
10 changed files with 320 additions and 66 deletions
|
@ -1,14 +1,64 @@
|
|||
# Toast
|
||||
|
||||
```rust demo
|
||||
let toaster = ToasterInjection::use_();
|
||||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
let on_click = move |_| {
|
||||
toaster.dispatch_toast(view! { <span>"Hello"</span> }.into_any(), Default::default());
|
||||
toaster.dispatch_toast(view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
"This is a toast body"
|
||||
<ToastBodySubtitle slot>
|
||||
"Subtitle"
|
||||
</ToastBodySubtitle>
|
||||
</ToastBody>
|
||||
<ToastFooter>
|
||||
"Footer"
|
||||
// <Link>Action</Link>
|
||||
// <Link>Action</Link>
|
||||
</ToastFooter>
|
||||
</Toast>
|
||||
}.into_any(), Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
<Button on_click=on_click>"Make toast"</Button>
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
### Toast Positions
|
||||
|
||||
```rust demo
|
||||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
let dispatch_toast = Callback::new(move |position| {
|
||||
toaster.dispatch_toast(view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
"This is a toast body"
|
||||
<ToastBodySubtitle slot>
|
||||
"Subtitle"
|
||||
</ToastBodySubtitle>
|
||||
</ToastBody>
|
||||
<ToastFooter>
|
||||
"Footer"
|
||||
// <Link>Action</Link>
|
||||
// <Link>Action</Link>
|
||||
</ToastFooter>
|
||||
</Toast>
|
||||
}.into_any(), ToastOptions::default().with_position(position));
|
||||
});
|
||||
|
||||
view! {
|
||||
<Space>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::Bottom)>"Bottom"</Button>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::BottomStart)>"BottomStart"</Button>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::BottomEnd)>"BottomEnd"</Button>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::Top)>"Top"</Button>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::TopStart)>"Topstart"</Button>
|
||||
<Button on_click=move |_| dispatch_toast.call(ToastPosition::TopEnd)>"TopEnd"</Button>
|
||||
</Space>
|
||||
}
|
||||
```
|
||||
|
|
|
@ -71,6 +71,10 @@ impl ConfigInjection {
|
|||
pub fn use_() -> ConfigInjection {
|
||||
expect_context()
|
||||
}
|
||||
|
||||
pub fn expect_context() -> Self {
|
||||
expect_context()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -109,6 +109,7 @@ pub struct ColorTheme {
|
|||
pub color_transparent_stroke: String,
|
||||
|
||||
pub shadow4: String,
|
||||
pub shadow8: String,
|
||||
pub shadow16: String,
|
||||
}
|
||||
|
||||
|
@ -223,6 +224,7 @@ impl ColorTheme {
|
|||
color_transparent_stroke: "transparent".into(),
|
||||
|
||||
shadow4: "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)".into(),
|
||||
shadow8: "0 0 2px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.14)".into(),
|
||||
shadow16: "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)".into(),
|
||||
}
|
||||
}
|
||||
|
@ -337,6 +339,7 @@ impl ColorTheme {
|
|||
color_transparent_stroke: "transparent".into(),
|
||||
|
||||
shadow4: "0 0 2px rgba(0,0,0,0.24), 0 2px 4px rgba(0,0,0,0.28)".into(),
|
||||
shadow8: "0 0 2px rgba(0,0,0,0.24), 0 4px 8px rgba(0,0,0,0.28)".into(),
|
||||
shadow16: "0 0 2px rgba(0,0,0,0.24), 0 8px 16px rgba(0,0,0,0.28)".into(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ pub struct ToasterInjection {
|
|||
}
|
||||
|
||||
impl ToasterInjection {
|
||||
pub fn use_() -> Self {
|
||||
pub fn expect_context() -> Self {
|
||||
expect_context()
|
||||
}
|
||||
|
||||
|
|
|
@ -25,20 +25,29 @@ impl ToastPosition {
|
|||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Top => "top",
|
||||
Self::TopStart => "top-left",
|
||||
Self::TopEnd => "top-right",
|
||||
Self::TopStart => "top-start",
|
||||
Self::TopEnd => "top-dnc",
|
||||
Self::Bottom => "bottom",
|
||||
Self::BottomStart => "bottom-left",
|
||||
Self::BottomEnd => "bottom-right",
|
||||
Self::BottomStart => "bottom-start",
|
||||
Self::BottomEnd => "bottom-end",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToastIntent {
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToastOptions {
|
||||
pub(crate) id: uuid::Uuid,
|
||||
pub(crate) position: Option<ToastPosition>,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
pub(crate) intent: Option<ToastIntent>,
|
||||
}
|
||||
|
||||
impl Default for ToastOptions {
|
||||
|
@ -47,6 +56,24 @@ impl Default for ToastOptions {
|
|||
id: uuid::Uuid::new_v4(),
|
||||
position: None,
|
||||
timeout: None,
|
||||
intent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToastOptions {
|
||||
pub fn with_position(mut self, position: ToastPosition) -> Self {
|
||||
self.position = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_intent(mut self, intent: ToastIntent) -> Self {
|
||||
self.intent = Some(intent);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,13 @@ pub fn ToastBody(
|
|||
children: Children,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
{children()}
|
||||
<div class="thaw-toast-body">
|
||||
{children()}
|
||||
</div>
|
||||
<OptionComp value=toast_body_subtitle let:subtitle>
|
||||
{(subtitle.children)()}
|
||||
<div class="thaw-toast-body__subtitle">
|
||||
{(subtitle.children)()}
|
||||
</div>
|
||||
</OptionComp>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,5 +2,9 @@ use leptos::prelude::*;
|
|||
|
||||
#[component]
|
||||
pub fn ToastFooter(children: Children) -> impl IntoView {
|
||||
children()
|
||||
view! {
|
||||
<div class="thaw-toast-footer">
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos::prelude::*;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use thaw_components::OptionComp;
|
||||
|
||||
#[component]
|
||||
|
@ -9,13 +9,25 @@ pub fn ToastTitle(
|
|||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="thaw-toast-title__media">
|
||||
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
|
||||
<path d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
{
|
||||
if let Some(media) = toast_title_media {
|
||||
Either::Left((media.children)())
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
|
||||
<path d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
})
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="thaw-toast-title">
|
||||
{children()}
|
||||
</div>
|
||||
{children()}
|
||||
<OptionComp value=toast_title_action let:action>
|
||||
{(action.children)()}
|
||||
<div class="thaw-toast-title__action">
|
||||
{(action.children)()}
|
||||
</div>
|
||||
</OptionComp>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
div.thaw-toaster {
|
||||
div.thaw-toaster-wrapper {
|
||||
z-index: 1000000;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
|
@ -13,15 +13,51 @@ div.thaw-toaster {
|
|||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.thaw-toaster-container {
|
||||
bottom: 16px;
|
||||
right: 20px;
|
||||
|
||||
.thaw-toaster {
|
||||
position: fixed;
|
||||
width: 292px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.thaw-toaster--top {
|
||||
top: 16px;
|
||||
left: calc(50% + 20px);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.thaw-toaster--top-start {
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.thaw-toaster--top-end {
|
||||
top: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.thaw-toaster--bottom {
|
||||
bottom: 16px;
|
||||
left: calc(50% + 20px);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.thaw-toaster--bottom-start {
|
||||
bottom: 16px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.thaw-toaster--bottom-end {
|
||||
bottom: 16px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.thaw-toaster-container {
|
||||
box-sizing: border-box;
|
||||
margin-top: 16px;
|
||||
pointer-events: all;
|
||||
border-radius: var(--borderRadiusMedium);
|
||||
}
|
||||
|
||||
.thaw-toaster-container.fade-in-height-expand-transition-leave-from,
|
||||
.thaw-toaster-container.fade-in-height-expand-transition-enter-to {
|
||||
transform: scale(1);
|
||||
|
@ -95,3 +131,33 @@ div.thaw-toaster {
|
|||
grid-column-end: -1;
|
||||
color: var(--colorBrandForeground1);
|
||||
}
|
||||
|
||||
.thaw-toast-body {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 3;
|
||||
padding-top: 6px;
|
||||
font-size: var(--fontSizeBase300);
|
||||
line-height: var(--fontSizeBase300);
|
||||
font-weight: var(--fontWeightRegular);
|
||||
color: var(--colorNeutralForeground1);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.thaw-toast-body__subtitle {
|
||||
padding-top: 4px;
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 3;
|
||||
font-size: var(--fontSizeBase200);
|
||||
line-height: var(--fontSizeBase200);
|
||||
font-weight: var(--fontWeightRegular);
|
||||
color: var(--colorNeutralForeground2);
|
||||
}
|
||||
|
||||
.thaw-toast-footer {
|
||||
padding-top: 16px;
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::{ToastOptions, ToastPosition, ToasterReceiver};
|
||||
use crate::ConfigInjection;
|
||||
use leptos::{either::Either, html, prelude::*, tachys::view::any_view::AnyView};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
@ -9,14 +10,29 @@ use thaw_utils::mount_style;
|
|||
pub fn Toaster(
|
||||
receiver: ToasterReceiver,
|
||||
#[prop(optional)] position: ToastPosition,
|
||||
#[prop(default = Duration::from_secs(3))] timeout: Duration,
|
||||
#[prop(default = Duration::from_secs(3000))] timeout: Duration,
|
||||
) -> impl IntoView {
|
||||
mount_style("toaster", include_str!("./toaster.css"));
|
||||
let config_provider = ConfigInjection::expect_context();
|
||||
let top_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let top_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let top_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let bottom_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let bottom_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let toasts = StoredValue::<HashMap<uuid::Uuid, (SendWrapper<AnyView<Dom>>, ToastOptions)>>::new(
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
let id_list = move |position: &ToastPosition| match position {
|
||||
ToastPosition::Top => top_id_list,
|
||||
ToastPosition::TopStart => top_start_id_list,
|
||||
ToastPosition::TopEnd => top_end_id_list,
|
||||
ToastPosition::Bottom => bottom_id_list,
|
||||
ToastPosition::BottomStart => bottom_start_id_list,
|
||||
ToastPosition::BottomEnd => bottom_end_id_list,
|
||||
};
|
||||
|
||||
Effect::new(move |_| {
|
||||
for (view, mut options) in receiver.try_recv() {
|
||||
if options.position.is_none() {
|
||||
|
@ -25,57 +41,124 @@ pub fn Toaster(
|
|||
if options.timeout.is_none() {
|
||||
options.timeout = Some(timeout);
|
||||
}
|
||||
match options.position.unwrap() {
|
||||
ToastPosition::Top => todo!(),
|
||||
ToastPosition::TopStart => todo!(),
|
||||
ToastPosition::TopEnd => todo!(),
|
||||
ToastPosition::Bottom => todo!(),
|
||||
ToastPosition::BottomStart => {
|
||||
let id = options.id;
|
||||
toasts.update_value(|map| {
|
||||
map.insert(id, (SendWrapper::new(view), options));
|
||||
});
|
||||
bottom_start_id_list.update(|list| {
|
||||
list.push(id);
|
||||
});
|
||||
}
|
||||
ToastPosition::BottomEnd => todo!(),
|
||||
}
|
||||
|
||||
let list = id_list(&options.position.unwrap());
|
||||
let id = options.id;
|
||||
toasts.update_value(|map| {
|
||||
map.insert(id, (SendWrapper::new(view), options));
|
||||
});
|
||||
list.update(|list| {
|
||||
list.push(id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let on_close = move |(id, position)| match position {
|
||||
ToastPosition::Top => todo!(),
|
||||
ToastPosition::TopStart => todo!(),
|
||||
ToastPosition::TopEnd => todo!(),
|
||||
ToastPosition::Bottom => todo!(),
|
||||
ToastPosition::BottomStart => {
|
||||
bottom_start_id_list.update(move |list| {
|
||||
let Some(index) = list.iter().position(|item_id| &id == item_id) else {
|
||||
return;
|
||||
};
|
||||
list.remove(index);
|
||||
});
|
||||
}
|
||||
ToastPosition::BottomEnd => todo!(),
|
||||
let on_close = move |(id, position)| {
|
||||
let list = id_list(&position);
|
||||
list.update(move |list| {
|
||||
let Some(index) = list.iter().position(|item_id| &id == item_id) else {
|
||||
return;
|
||||
};
|
||||
list.remove(index);
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<Teleport>
|
||||
<div class="thaw-config-provider thaw-toaster">
|
||||
<For
|
||||
each=move || bottom_start_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
<div
|
||||
class="thaw-config-provider thaw-toaster-wrapper"
|
||||
data-thaw-id=config_provider.id().clone()
|
||||
>
|
||||
<div class="thaw-toaster thaw-toaster--top">
|
||||
<For
|
||||
each=move || top_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</For>
|
||||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--top-start">
|
||||
<For
|
||||
each=move || top_start_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--top-end">
|
||||
<For
|
||||
each=move || top_end_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom">
|
||||
<For
|
||||
each=move || bottom_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom-start">
|
||||
<For
|
||||
each=move || bottom_start_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom-end">
|
||||
<For
|
||||
each=move || bottom_end_id_list.get()
|
||||
key=|id| id.clone()
|
||||
let:id
|
||||
>
|
||||
{
|
||||
if let Some((view, options)) = toasts.try_update_value(|map| { map.remove(&id) }).flatten() {
|
||||
Either::Left(view! { <ToasterContainer on_close view=view.take() options/> })
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
}
|
||||
|
@ -93,6 +176,7 @@ fn ToasterContainer(
|
|||
id,
|
||||
timeout,
|
||||
position,
|
||||
..
|
||||
} = options;
|
||||
let timeout = timeout.unwrap();
|
||||
let position = position.unwrap();
|
||||
|
|
Loading…
Add table
Reference in a new issue