Skip to main content

dioxus_ui_system/molecules/
alert.rs

1//! Alert molecule component
2//!
3//! Displays a callout for user attention.
4
5use dioxus::prelude::*;
6use crate::theme::{use_theme, use_style};
7use crate::styles::Style;
8
9/// Alert variants
10#[derive(Default, Clone, PartialEq)]
11pub enum AlertVariant {
12    /// Default alert
13    #[default]
14    Default,
15    /// Destructive alert
16    Destructive,
17    /// Success alert
18    Success,
19    /// Warning alert
20    Warning,
21    /// Info alert
22    Info,
23}
24
25/// Alert properties
26#[derive(Props, Clone, PartialEq)]
27pub struct AlertProps {
28    /// Alert content
29    pub children: Element,
30    /// Alert variant
31    #[props(default)]
32    pub variant: AlertVariant,
33    /// Optional title
34    #[props(default)]
35    pub title: Option<String>,
36    /// Optional icon name
37    #[props(default)]
38    pub icon: Option<String>,
39    /// Whether alert is dismissible
40    #[props(default)]
41    pub dismissible: bool,
42    /// Callback when dismissed
43    #[props(default)]
44    pub on_dismiss: Option<EventHandler<()>>,
45    /// Custom inline styles
46    #[props(default)]
47    pub style: Option<String>,
48    /// Custom class name
49    #[props(default)]
50    pub class: Option<String>,
51}
52
53/// Alert molecule component
54#[component]
55pub fn Alert(props: AlertProps) -> Element {
56    let _theme = use_theme();
57    let mut is_visible = use_signal(|| true);
58    
59    if !is_visible() {
60        return rsx! {};
61    }
62    
63    let variant = props.variant.clone();
64    
65    let alert_style = use_style(move |t| {
66        let (bg_color, border_color, _text_color) = match variant {
67            AlertVariant::Default => (
68                &t.colors.background,
69                &t.colors.border,
70                &t.colors.foreground,
71            ),
72            AlertVariant::Destructive => (
73                &t.colors.destructive.lighten(0.9),
74                &t.colors.destructive,
75                &t.colors.destructive,
76            ),
77            AlertVariant::Success => (
78                &t.colors.success.lighten(0.9),
79                &t.colors.success,
80                &t.colors.success.darken(0.2),
81            ),
82            AlertVariant::Warning => (
83                &t.colors.warning.lighten(0.9),
84                &t.colors.warning,
85                &t.colors.warning.darken(0.2),
86            ),
87            AlertVariant::Info => (
88                &t.colors.primary.lighten(0.9),
89                &t.colors.primary,
90                &t.colors.primary,
91            ),
92        };
93        
94        Style::new()
95            .w_full()
96            .rounded(&t.radius, "lg")
97            .border(1, border_color)
98            .bg(bg_color)
99            .p(&t.spacing, "md")
100            .flex()
101            .gap(&t.spacing, "md")
102            .build()
103    });
104    
105    let icon_style = use_style(|_t| {
106        Style::new()
107            .w_px(20)
108            .h_px(20)
109            .flex_shrink(0)
110            .build()
111    });
112    
113    let mut handle_dismiss = move || {
114        is_visible.set(false);
115        if let Some(handler) = &props.on_dismiss {
116            handler.call(());
117        }
118    };
119    
120    // Default icons based on variant
121    let default_icon = match props.variant {
122        AlertVariant::Default => None,
123        AlertVariant::Destructive => Some("alert-triangle"),
124        AlertVariant::Success => Some("check-circle"),
125        AlertVariant::Warning => Some("alert-triangle"),
126        AlertVariant::Info => Some("info"),
127    };
128    
129    let icon_name = props.icon.as_deref().or(default_icon);
130    
131    rsx! {
132        div {
133            role: "alert",
134            style: "{alert_style} {props.style.clone().unwrap_or_default()}",
135            class: "{props.class.clone().unwrap_or_default()}",
136            
137            if let Some(icon) = icon_name {
138                AlertIcon { name: icon.to_string(), style: icon_style() }
139            }
140            
141            div {
142                style: "flex: 1;",
143                
144                if let Some(title) = props.title {
145                    h5 {
146                        style: "margin: 0 0 4px 0; font-size: 14px; font-weight: 600;",
147                        "{title}"
148                    }
149                }
150                
151                div {
152                    style: "font-size: 14px; line-height: 1.5;",
153                    {props.children}
154                }
155            }
156            
157            if props.dismissible {
158                button {
159                    style: "background: none; border: none; cursor: pointer; padding: 4px; opacity: 0.5; transition: opacity 150ms;",
160                    onmouseenter: move |e| e.stop_propagation(),
161                    onclick: move |_| handle_dismiss(),
162                    "✕"
163                }
164            }
165        }
166    }
167}
168
169#[derive(Props, Clone, PartialEq)]
170struct AlertIconProps {
171    name: String,
172    style: String,
173}
174
175#[component]
176fn AlertIcon(props: AlertIconProps) -> Element {
177    // Map icon names to SVG paths
178    let svg_content = match props.name.as_str() {
179        "alert-triangle" => rsx! {
180            svg {
181                view_box: "0 0 24 24",
182                fill: "none",
183                stroke: "currentColor",
184                stroke_width: "2",
185                stroke_linecap: "round",
186                stroke_linejoin: "round",
187                style: "{props.style}",
188                path { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }
189                line { x1: "12", y1: "9", x2: "12", y2: "13" }
190                line { x1: "12", y1: "17", x2: "12.01", y2: "17" }
191            }
192        },
193        "check-circle" => rsx! {
194            svg {
195                view_box: "0 0 24 24",
196                fill: "none",
197                stroke: "currentColor",
198                stroke_width: "2",
199                stroke_linecap: "round",
200                stroke_linejoin: "round",
201                style: "{props.style}",
202                path { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }
203                polyline { points: "22 4 12 14.01 9 11.01" }
204            }
205        },
206        "info" => rsx! {
207            svg {
208                view_box: "0 0 24 24",
209                fill: "none",
210                stroke: "currentColor",
211                stroke_width: "2",
212                stroke_linecap: "round",
213                stroke_linejoin: "round",
214                style: "{props.style}",
215                circle { cx: "12", cy: "12", r: "10" }
216                line { x1: "12", y1: "16", x2: "12", y2: "12" }
217                line { x1: "12", y1: "8", x2: "12.01", y2: "8" }
218            }
219        },
220        _ => rsx! {}
221    };
222    
223    svg_content
224}