1use crate::components::overlay::{OverlayHandle, OverlayKey, OverlayKind, OverlayMeta};
2use dioxus::prelude::*;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum NotificationType {
7 Info,
8 Success,
9 Warning,
10 Error,
11}
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum NotificationPlacement {
16 #[default]
17 TopRight,
18 TopLeft,
19 BottomRight,
20 BottomLeft,
21}
22
23impl NotificationPlacement {
24 fn as_style(&self) -> &'static str {
25 match self {
26 NotificationPlacement::TopRight => "top: 24px; right: 24px;",
27 NotificationPlacement::TopLeft => "top: 24px; left: 24px;",
28 NotificationPlacement::BottomRight => "bottom: 24px; right: 24px;",
29 NotificationPlacement::BottomLeft => "bottom: 24px; left: 24px;",
30 }
31 }
32}
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct NotificationConfig {
37 pub title: String,
38 pub description: Option<String>,
39 pub r#type: NotificationType,
40 pub placement: NotificationPlacement,
41 pub duration: f32,
43 pub icon: Option<Element>,
45 pub class: Option<String>,
47 pub style: Option<String>,
49 pub on_click: Option<EventHandler<()>>,
51 pub key: Option<String>,
53}
54
55impl Default for NotificationConfig {
56 fn default() -> Self {
57 Self {
58 title: String::new(),
59 description: None,
60 r#type: NotificationType::Info,
61 placement: NotificationPlacement::TopRight,
62 duration: 4.5,
63 icon: None,
64 class: None,
65 style: None,
66 on_click: None,
67 key: None,
68 }
69 }
70}
71
72#[derive(Clone, Debug, PartialEq)]
73pub struct NotificationEntry {
74 pub key: OverlayKey,
75 pub meta: OverlayMeta,
76 pub config: NotificationConfig,
77}
78
79pub type NotificationEntriesSignal = Signal<Vec<NotificationEntry>>;
80
81pub fn use_notification_entries_provider() -> NotificationEntriesSignal {
82 let signal: NotificationEntriesSignal = use_context_provider(|| Signal::new(Vec::new()));
83 signal
84}
85
86pub fn use_notification_entries() -> NotificationEntriesSignal {
87 use_context::<NotificationEntriesSignal>()
88}
89
90#[derive(Clone)]
91pub struct NotificationApi {
92 overlay: OverlayHandle,
93 entries: NotificationEntriesSignal,
94}
95
96impl NotificationApi {
97 pub fn new(overlay: OverlayHandle, entries: NotificationEntriesSignal) -> Self {
98 Self { overlay, entries }
99 }
100
101 pub fn open(&self, config: NotificationConfig) -> OverlayKey {
102 let (key, meta) = self.overlay.open(OverlayKind::Notification, false);
103 let mut entries = self.entries;
104 entries.write().push(NotificationEntry {
105 key,
106 meta,
107 config: config.clone(),
108 });
109 schedule_notification_dismiss(key, self.entries, self.overlay.clone(), config.duration);
110 key
111 }
112
113 fn open_with_type(
114 &self,
115 title: impl Into<String>,
116 description: Option<String>,
117 placement: NotificationPlacement,
118 kind: NotificationType,
119 ) -> OverlayKey {
120 let cfg = NotificationConfig {
121 title: title.into(),
122 description,
123 r#type: kind,
124 placement,
125 duration: 4.5,
126 icon: None,
127 class: None,
128 style: None,
129 on_click: None,
130 key: None,
131 };
132 self.open(cfg)
133 }
134
135 pub fn info(&self, title: impl Into<String>, description: Option<String>) -> OverlayKey {
136 self.open_with_type(
137 title,
138 description,
139 NotificationPlacement::TopRight,
140 NotificationType::Info,
141 )
142 }
143
144 pub fn success(&self, title: impl Into<String>, description: Option<String>) -> OverlayKey {
145 self.open_with_type(
146 title,
147 description,
148 NotificationPlacement::TopRight,
149 NotificationType::Success,
150 )
151 }
152
153 pub fn warning(&self, title: impl Into<String>, description: Option<String>) -> OverlayKey {
154 self.open_with_type(
155 title,
156 description,
157 NotificationPlacement::TopRight,
158 NotificationType::Warning,
159 )
160 }
161
162 pub fn error(&self, title: impl Into<String>, description: Option<String>) -> OverlayKey {
163 self.open_with_type(
164 title,
165 description,
166 NotificationPlacement::TopRight,
167 NotificationType::Error,
168 )
169 }
170
171 pub fn close(&self, key: OverlayKey) {
172 let mut entries = self.entries;
173 entries.write().retain(|e| e.key != key);
174 self.overlay.close(key);
175 }
176
177 pub fn destroy(&self) {
178 let mut entries = self.entries;
179 let current: Vec<_> = entries.read().iter().map(|e| e.key).collect();
180 entries.write().clear();
181 for k in current {
182 self.overlay.close(k);
183 }
184 }
185}
186
187#[component]
188pub fn NotificationHost() -> Element {
189 let entries_signal = use_notification_entries();
190 let entries = entries_signal.read().clone();
191
192 if entries.is_empty() {
193 return rsx! {};
194 }
195
196 let top_right: Vec<_> = entries
197 .iter()
198 .filter(|e| e.config.placement == NotificationPlacement::TopRight)
199 .cloned()
200 .collect();
201 let bottom_right: Vec<_> = entries
202 .iter()
203 .filter(|e| e.config.placement == NotificationPlacement::BottomRight)
204 .cloned()
205 .collect();
206
207 rsx! {
208 if !top_right.is_empty() {
210 div {
211 class: "adui-notification-root adui-notification-top-right",
212 style: "position: fixed; top: 24px; inset-inline-end: 0; z-index: 1000; display: flex; flex-direction: column; gap: 8px; padding-inline-end: 24px;",
213 {top_right.iter().map(|entry| {
214 let key = entry.key;
215 let z = entry.meta.z_index;
216 let title = entry.config.title.clone();
217 let desc = entry.config.description.clone();
218 let kind_class = match entry.config.r#type {
219 NotificationType::Info => "adui-notification-info",
220 NotificationType::Success => "adui-notification-success",
221 NotificationType::Warning => "adui-notification-warning",
222 NotificationType::Error => "adui-notification-error",
223 };
224 rsx! {
225 div {
226 key: "notice-{key:?}",
227 class: "adui-notification {kind_class}",
228 style: "pointer-events: auto; z-index: {z}; min-width: 280px; max-width: 480px; padding: 12px 16px; border-radius: 4px; background: var(--adui-color-bg-container); box-shadow: var(--adui-shadow); color: var(--adui-color-text); border: 1px solid var(--adui-color-border);",
229 div {
230 style: "font-weight: 500; margin-bottom: 4px;",
231 "{title}"
232 }
233 if let Some(text) = desc {
234 div { style: "font-size: 13px; color: var(--adui-color-text-secondary);", "{text}" }
235 }
236 button {
237 style: "margin-left: 8px; background: none; border: none; cursor: pointer; color: var(--adui-color-text-secondary); float: right;",
238 onclick: move |_| {
239 let mut entries = entries_signal;
240 entries.write().retain(|e| e.key != key);
241 },
242 "×"
243 }
244 }
245 }
246 })}
247 }
248 }
249
250 if !bottom_right.is_empty() {
252 div {
253 class: "adui-notification-root adui-notification-bottom-right",
254 style: "position: fixed; bottom: 24px; inset-inline-end: 0; z-index: 1000; display: flex; flex-direction: column; gap: 8px; padding-inline-end: 24px;",
255 {bottom_right.iter().map(|entry| {
256 let key = entry.key;
257 let z = entry.meta.z_index;
258 let title = entry.config.title.clone();
259 let desc = entry.config.description.clone();
260 let kind_class = match entry.config.r#type {
261 NotificationType::Info => "adui-notification-info",
262 NotificationType::Success => "adui-notification-success",
263 NotificationType::Warning => "adui-notification-warning",
264 NotificationType::Error => "adui-notification-error",
265 };
266 rsx! {
267 div {
268 key: "notice-bottom-{key:?}",
269 class: "adui-notification {kind_class}",
270 style: "pointer-events: auto; z-index: {z}; min-width: 280px; max-width: 480px; padding: 12px 16px; border-radius: 4px; background: var(--adui-color-bg-container); box-shadow: var(--adui-shadow); color: var(--adui-color-text); border: 1px solid var(--adui-color-border);",
271 div {
272 style: "font-weight: 500; margin-bottom: 4px;",
273 "{title}"
274 }
275 if let Some(text) = desc {
276 div { style: "font-size: 13px; color: var(--adui-color-text-secondary);", "{text}" }
277 }
278 button {
279 style: "margin-left: 8px; background: none; border: none; cursor: pointer; color: var(--adui-color-text-secondary); float: right;",
280 onclick: move |_| {
281 let mut entries = entries_signal;
282 entries.write().retain(|e| e.key != key);
283 },
284 "×"
285 }
286 }
287 }
288 })}
289 }
290 }
291 }
292}
293
294#[cfg(target_arch = "wasm32")]
295fn schedule_notification_dismiss(
296 key: OverlayKey,
297 entries: NotificationEntriesSignal,
298 overlay: OverlayHandle,
299 duration_secs: f32,
300) {
301 use wasm_bindgen::{JsCast, closure::Closure};
302
303 if duration_secs <= 0.0 {
304 return;
305 }
306
307 if let Some(window) = web_sys::window() {
308 let delay_ms = (duration_secs * 1000.0) as i32;
309 let mut entries_signal = entries;
310 let overlay_clone = overlay.clone();
311 let callback = Closure::once(move || {
312 entries_signal.write().retain(|e| e.key != key);
313 overlay_clone.close(key);
314 });
315 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
316 callback.as_ref().unchecked_ref(),
317 delay_ms,
318 );
319 callback.forget();
320 }
321}
322
323#[cfg(not(target_arch = "wasm32"))]
324fn schedule_notification_dismiss(
325 _key: OverlayKey,
326 _entries: NotificationEntriesSignal,
327 _overlay: OverlayHandle,
328 _duration_secs: f32,
329) {
330}