Skip to main content

dioxus_ui_system/molecules/
alert.rs

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