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