Skip to main content

dioxus_ui_system/molecules/
dialog.rs

1//! Dialog molecule component
2//!
3//! A window overlaid on either the primary window or another dialog window.
4
5use crate::atoms::{Box, Button, ButtonVariant};
6use crate::styles::Style;
7use crate::theme::{use_style, use_theme};
8use dioxus::prelude::*;
9
10/// Dialog properties
11#[derive(Props, Clone, PartialEq)]
12pub struct DialogProps {
13    /// Whether the dialog is open
14    pub open: bool,
15    /// Callback when dialog should close
16    pub on_close: EventHandler<()>,
17    /// Dialog content
18    pub children: Element,
19    /// Dialog title
20    #[props(default)]
21    pub title: Option<String>,
22    /// Dialog description
23    #[props(default)]
24    pub description: Option<String>,
25    /// Whether to show close button
26    #[props(default = true)]
27    pub show_close_button: bool,
28    /// Whether clicking overlay closes the dialog
29    #[props(default = true)]
30    pub close_on_overlay_click: bool,
31    /// Custom inline styles for the content
32    #[props(default)]
33    pub content_style: Option<String>,
34}
35
36/// Dialog molecule component
37#[component]
38pub fn Dialog(props: DialogProps) -> Element {
39    let _theme = use_theme();
40
41    if !props.open {
42        return rsx! {};
43    }
44
45    let overlay_style = use_style(|_t| {
46        Style::new()
47            .fixed()
48            .top("0")
49            .left("0")
50            .w_full()
51            .h_full()
52            .bg(&Color::new_rgba(0, 0, 0, 0.5))
53            .z_index(100)
54            .flex()
55            .items_center()
56            .justify_center()
57            .build()
58    });
59
60    let content_style = use_style(|t| {
61        Style::new()
62            .w_full()
63            .max_w_px(512)
64            .max_h_px(600)
65            .rounded(&t.radius, "lg")
66            .bg(&t.colors.background)
67            .shadow(&t.shadows.xl)
68            .overflow_hidden()
69            .flex()
70            .flex_col()
71            .build()
72    });
73
74    let handle_overlay_click = move |_| {
75        if props.close_on_overlay_click {
76            props.on_close.call(());
77        }
78    };
79
80    let custom_content_style = props.content_style.clone().unwrap_or_default();
81
82    rsx! {
83        div {
84            style: "{overlay_style}",
85            onclick: handle_overlay_click,
86
87            div {
88                style: "{content_style} {custom_content_style}",
89                onclick: move |e| e.stop_propagation(),
90
91                // Header
92                if props.title.is_some() || props.show_close_button {
93                    DialogHeader {
94                        title: props.title.clone(),
95                        show_close_button: props.show_close_button,
96                        on_close: props.on_close.clone(),
97                    }
98                }
99
100                // Description
101                if let Some(description) = props.description.clone() {
102                    DialogDescription { description: description }
103                }
104
105                // Content
106                Box {
107                    style: "padding: 0 24px 24px 24px; overflow-y: auto;",
108                    {props.children}
109                }
110            }
111        }
112    }
113}
114
115use crate::theme::tokens::Color;
116
117#[derive(Props, Clone, PartialEq)]
118struct DialogHeaderProps {
119    title: Option<String>,
120    show_close_button: bool,
121    on_close: EventHandler<()>,
122}
123
124#[component]
125fn DialogHeader(props: DialogHeaderProps) -> Element {
126    let _theme = use_theme();
127
128    let header_style = use_style(|t| {
129        Style::new()
130            .flex()
131            .items_center()
132            .justify_between()
133            .p(&t.spacing, "lg")
134            .border_bottom(1, &t.colors.border)
135            .build()
136    });
137
138    rsx! {
139        div {
140            style: "{header_style}",
141
142            if let Some(title) = props.title {
143                h2 {
144                    style: "margin: 0; font-size: 18px; font-weight: 600;",
145                    "{title}"
146                }
147            } else {
148                div {}
149            }
150
151            if props.show_close_button {
152                Button {
153                    variant: ButtonVariant::Ghost,
154                    onclick: move |_| props.on_close.call(()),
155                    "✕"
156                }
157            }
158        }
159    }
160}
161
162#[derive(Props, Clone, PartialEq)]
163struct DialogDescriptionProps {
164    description: String,
165}
166
167#[component]
168fn DialogDescription(props: DialogDescriptionProps) -> Element {
169    rsx! {
170        p {
171            style: "margin: 0; padding: 16px 24px 0 24px; font-size: 14px; color: #64748b;",
172            "{props.description}"
173        }
174    }
175}
176
177/// Dialog footer component for action buttons
178#[derive(Props, Clone, PartialEq)]
179pub struct DialogFooterProps {
180    /// Footer content (usually buttons)
181    pub children: Element,
182    /// Align content
183    #[props(default)]
184    pub align: DialogFooterAlign,
185}
186
187/// Dialog footer alignment
188#[derive(Default, Clone, PartialEq)]
189pub enum DialogFooterAlign {
190    /// Start alignment
191    #[default]
192    Start,
193    /// Center alignment
194    Center,
195    /// End alignment
196    End,
197    /// Space between
198    Between,
199}
200
201/// Dialog footer component
202#[component]
203pub fn DialogFooter(props: DialogFooterProps) -> Element {
204    let _theme = use_theme();
205
206    let justify = match props.align {
207        DialogFooterAlign::Start => "flex-start",
208        DialogFooterAlign::Center => "center",
209        DialogFooterAlign::End => "flex-end",
210        DialogFooterAlign::Between => "space-between",
211    };
212
213    let footer_style = use_style(|t| {
214        Style::new()
215            .flex()
216            .items_center()
217            .gap(&t.spacing, "sm")
218            .p(&t.spacing, "lg")
219            .border_top(1, &t.colors.border)
220            .build()
221    });
222
223    rsx! {
224        div {
225            style: "{footer_style} justify-content: {justify};",
226            {props.children}
227        }
228    }
229}
230
231/// Alert dialog for important confirmations
232#[derive(Props, Clone, PartialEq)]
233pub struct AlertDialogProps {
234    /// Whether the dialog is open
235    pub open: bool,
236    /// Callback when dialog should close
237    pub on_close: EventHandler<()>,
238    /// Dialog title
239    pub title: String,
240    /// Dialog description
241    pub description: String,
242    /// Cancel button text
243    #[props(default = "Cancel".to_string())]
244    pub cancel_text: String,
245    /// Confirm button text
246    #[props(default = "Confirm".to_string())]
247    pub confirm_text: String,
248    /// Callback when confirmed
249    pub on_confirm: EventHandler<()>,
250    /// Whether the action is destructive
251    #[props(default)]
252    pub destructive: bool,
253}
254
255/// Alert dialog component
256#[component]
257pub fn AlertDialog(props: AlertDialogProps) -> Element {
258    let confirm_variant = if props.destructive {
259        ButtonVariant::Destructive
260    } else {
261        ButtonVariant::Primary
262    };
263
264    rsx! {
265        Dialog {
266            open: props.open,
267            on_close: props.on_close.clone(),
268            title: props.title.clone(),
269            show_close_button: false,
270            close_on_overlay_click: false,
271
272            p {
273                style: "margin: 0 0 24px 0; font-size: 14px; color: #64748b; line-height: 1.5;",
274                "{props.description}"
275            }
276
277            DialogFooter {
278                align: DialogFooterAlign::End,
279
280                Button {
281                    variant: ButtonVariant::Ghost,
282                    onclick: move |_| props.on_close.call(()),
283                    "{props.cancel_text}"
284                }
285
286                Button {
287                    variant: confirm_variant,
288                    onclick: move |_| props.on_confirm.call(()),
289                    "{props.confirm_text}"
290                }
291            }
292        }
293    }
294}