Skip to main content

shadow_design/
notification.rs

1use crate::icon::{Icon, IconKind};
2use dioxus::prelude::*;
3
4#[derive(Clone, Copy, PartialEq, Debug, Default)]
5pub enum NotificationVariant {
6    #[default]
7    Info,
8    Success,
9    Warning,
10    Danger,
11}
12
13impl NotificationVariant {
14    pub fn scss_class(&self) -> &'static str {
15        match self {
16            NotificationVariant::Info => "sd-toast--info",
17            NotificationVariant::Success => "sd-toast--success",
18            NotificationVariant::Warning => "sd-toast--warning",
19            NotificationVariant::Danger => "sd-toast--danger",
20        }
21    }
22
23    pub fn glow_class(&self) -> &'static str {
24        match self {
25            NotificationVariant::Info => "sd-toast--glow-info",
26            NotificationVariant::Success => "sd-toast--glow-success",
27            NotificationVariant::Warning => "sd-toast--glow-warning",
28            NotificationVariant::Danger => "sd-toast--glow-danger",
29        }
30    }
31
32    pub fn icon_kind(&self) -> IconKind {
33        match self {
34            NotificationVariant::Info => IconKind::Info,
35            NotificationVariant::Success => IconKind::CheckCircle,
36            NotificationVariant::Warning => IconKind::AlertTriangle,
37            NotificationVariant::Danger => IconKind::XCircle,
38        }
39    }
40
41    pub fn icon_color_class(&self) -> &'static str {
42        match self {
43            NotificationVariant::Info => "sd-toast__icon--info",
44            NotificationVariant::Success => "sd-toast__icon--success",
45            NotificationVariant::Warning => "sd-toast__icon--warning",
46            NotificationVariant::Danger => "sd-toast__icon--danger",
47        }
48    }
49}
50
51#[derive(Clone, PartialEq, Debug)]
52pub struct NotificationItem {
53    pub id: usize,
54    pub title: Option<String>,
55    pub message: String,
56    pub variant: NotificationVariant,
57}
58
59#[derive(Clone, Copy, PartialEq)]
60pub struct NotificationHandle {
61    items: Signal<Vec<NotificationItem>>,
62    next_id: Signal<usize>,
63}
64
65impl NotificationHandle {
66    pub fn info(&mut self, message: impl Into<String>) -> usize {
67        self.show(message, NotificationVariant::Info)
68    }
69
70    pub fn success(&mut self, message: impl Into<String>) -> usize {
71        self.show(message, NotificationVariant::Success)
72    }
73
74    pub fn warning(&mut self, message: impl Into<String>) -> usize {
75        self.show(message, NotificationVariant::Warning)
76    }
77
78    pub fn danger(&mut self, message: impl Into<String>) -> usize {
79        self.show(message, NotificationVariant::Danger)
80    }
81
82    pub fn show(&mut self, message: impl Into<String>, variant: NotificationVariant) -> usize {
83        let id = *self.next_id.read();
84        *self.next_id.write() += 1;
85
86        let item = NotificationItem {
87            id,
88            title: None,
89            message: message.into(),
90            variant,
91        };
92
93        self.items.write().push(item);
94        id
95    }
96
97    pub fn show_with_title(
98        &mut self,
99        title: impl Into<String>,
100        message: impl Into<String>,
101        variant: NotificationVariant,
102    ) -> usize {
103        let id = *self.next_id.read();
104        *self.next_id.write() += 1;
105
106        let item = NotificationItem {
107            id,
108            title: Some(title.into()),
109            message: message.into(),
110            variant,
111        };
112
113        self.items.write().push(item);
114        id
115    }
116
117    pub fn dismiss(&mut self, id: usize) {
118        self.items.write().retain(|item| item.id != id);
119    }
120
121    pub fn clear(&mut self) {
122        self.items.write().clear();
123    }
124}
125
126#[component]
127pub fn NotificationProvider(children: Element) -> Element {
128    let items = use_signal(Vec::<NotificationItem>::new);
129    let next_id = use_signal(|| 1usize);
130    let handle = NotificationHandle { items, next_id };
131    use_context_provider(|| handle);
132
133    rsx! {
134        {children}
135        NotificationContainer { items }
136    }
137}
138
139pub fn use_notification() -> NotificationHandle {
140    match try_use_context::<NotificationHandle>() {
141        Some(handle) => handle,
142        None => {
143            let items = use_signal(Vec::<NotificationItem>::new);
144            let next_id = use_signal(|| 1usize);
145            let handle = NotificationHandle { items, next_id };
146            use_context_provider(|| handle)
147        }
148    }
149}
150
151#[component]
152fn NotificationContainer(mut items: Signal<Vec<NotificationItem>>) -> Element {
153    rsx! {
154        div {
155            class: "sd-notification-container",
156            for item in items.read().iter().cloned() {
157                NotificationToast {
158                    key: "{item.id}",
159                    item: item.clone(),
160                    on_dismiss: move |_| {
161                        items.write().retain(|i| i.id != item.id);
162                    }
163                }
164            }
165        }
166    }
167}
168
169#[component]
170fn NotificationToast(item: NotificationItem, on_dismiss: EventHandler<MouseEvent>) -> Element {
171    let variant_class = item.variant.scss_class();
172    let icon_color_class = item.variant.icon_color_class();
173    let icon_kind = item.variant.icon_kind();
174
175    #[cfg(feature = "theme")]
176    let theme = crate::theme::use_theme();
177    #[cfg(feature = "theme")]
178    let has_glow = theme().is_component_glow_enabled("notification", None);
179    #[cfg(not(feature = "theme"))]
180    let has_glow = true;
181
182    let glow_class = if has_glow {
183        item.variant.glow_class()
184    } else {
185        ""
186    };
187
188    #[cfg(feature = "animation")]
189    let anim_class = crate::animation::Animations::TOAST_ENTER;
190    #[cfg(not(feature = "animation"))]
191    let anim_class = "animate-toast-enter";
192
193    rsx! {
194        div {
195            class: "sd-toast {variant_class} {glow_class} {anim_class}",
196            span { class: "sd-toast__icon {icon_color_class}",
197                Icon { kind: icon_kind, size: 16 }
198            }
199            div { class: "sd-toast__content",
200                if let Some(ref title) = item.title {
201                    h5 { class: "sd-toast__title", "{title}" }
202                }
203                p { class: "sd-toast__body", "{item.message}" }
204            }
205            button {
206                r#type: "button",
207                class: "sd-toast__close",
208                onclick: move |e| on_dismiss.call(e),
209                Icon { kind: IconKind::X, size: 13 }
210            }
211        }
212    }
213}