Skip to main content

dioxus_ui_system/organisms/
confirmation_dialog.rs

1//! Confirmation Dialog organism component
2//!
3//! Critical decision dialogs with clear action labeling.
4
5use crate::molecules::Dialog;
6use crate::theme::use_theme;
7use dioxus::prelude::*;
8
9/// Confirmation dialog properties
10#[derive(Props, Clone, PartialEq)]
11pub struct ConfirmationDialogProps {
12    /// Whether the dialog is open
13    pub open: bool,
14    /// On close handler
15    pub on_close: EventHandler<()>,
16    /// Dialog title
17    pub title: String,
18    /// Confirmation message
19    pub message: String,
20    /// Confirm button text
21    #[props(default)]
22    pub confirm_text: Option<String>,
23    /// Cancel button text
24    #[props(default)]
25    pub cancel_text: Option<String>,
26    /// Confirm button variant
27    #[props(default = ConfirmVariant::Danger)]
28    pub variant: ConfirmVariant,
29    /// Icon
30    #[props(default)]
31    pub icon: Option<String>,
32    /// On confirm handler
33    pub on_confirm: EventHandler<()>,
34    /// Loading state
35    #[props(default = false)]
36    pub loading: bool,
37    /// Additional CSS classes
38    #[props(default)]
39    pub class: Option<String>,
40}
41
42/// Confirmation variant
43#[derive(Default, Clone, PartialEq, Debug)]
44pub enum ConfirmVariant {
45    #[default]
46    Danger,
47    Warning,
48    Info,
49    Success,
50}
51
52impl ConfirmVariant {
53    fn colors(&self) -> (&'static str, &'static str) {
54        match self {
55            ConfirmVariant::Danger => ("#ef4444", "#dc2626"),
56            ConfirmVariant::Warning => ("#f59e0b", "#d97706"),
57            ConfirmVariant::Info => ("#3b82f6", "#2563eb"),
58            ConfirmVariant::Success => ("#22c55e", "#16a34a"),
59        }
60    }
61
62    fn default_icon(&self) -> &'static str {
63        match self {
64            ConfirmVariant::Danger => "đŸ—‘ī¸",
65            ConfirmVariant::Warning => "âš ī¸",
66            ConfirmVariant::Info => "â„šī¸",
67            ConfirmVariant::Success => "✓",
68        }
69    }
70}
71
72/// Confirmation dialog component
73#[component]
74pub fn ConfirmationDialog(props: ConfirmationDialogProps) -> Element {
75    let theme = use_theme();
76
77    let class_css = props
78        .class
79        .as_ref()
80        .map(|c| format!(" {}", c))
81        .unwrap_or_default();
82
83    let (bg_color, _hover_color) = props.variant.colors();
84    let icon = props
85        .icon
86        .unwrap_or_else(|| props.variant.default_icon().to_string());
87
88    rsx! {
89        Dialog {
90            open: props.open,
91            on_close: props.on_close.clone(),
92            title: props.title.clone(),
93
94            div {
95                class: "confirmation-dialog{class_css}",
96                style: "text-align: center; padding: 24px 0;",
97
98                // Icon
99                div {
100                    class: "confirmation-dialog-icon",
101                    style: "width: 64px; height: 64px; margin: 0 auto 24px; border-radius: 50%; background: {bg_color}20; display: flex; align-items: center; justify-content: center; font-size: 32px;",
102                    "{icon}"
103                }
104
105                // Message
106                p {
107                    class: "confirmation-dialog-message",
108                    style: "margin: 0 0 32px 0; font-size: 16px; line-height: 1.6; color: {theme.tokens.read().colors.foreground.to_rgba()};",
109                    "{props.message}"
110                }
111
112                // Actions
113                div {
114                    class: "confirmation-dialog-actions",
115                    style: "display: flex; gap: 12px; justify-content: center;",
116
117                    button {
118                        type: "button",
119                        class: "confirmation-dialog-cancel",
120                        style: "padding: 12px 24px; font-size: 14px; font-weight: 500; color: {theme.tokens.read().colors.foreground.to_rgba()}; background: white; border: 1px solid {theme.tokens.read().colors.border.to_rgba()}; border-radius: 8px; cursor: pointer; transition: background 0.15s ease;",
121                        onclick: move |_| props.on_close.call(()),
122                        "{props.cancel_text.clone().unwrap_or_else(|| \"Cancel\".to_string())}"
123                    }
124
125                    button {
126                        type: "button",
127                        class: "confirmation-dialog-confirm",
128                        style: format!("padding: 12px 24px; font-size: 14px; font-weight: 500; color: white; background: {bg_color}; border: none; border-radius: 8px; cursor: pointer; transition: background 0.15s ease; opacity: {};", if props.loading { "0.7" } else { "1" }),
129                        disabled: props.loading,
130                        onclick: move |_| props.on_confirm.call(()),
131
132                        if props.loading {
133                            span {
134                                style: "display: inline-block; margin-right: 8px; animation: spin 1s linear infinite;",
135                                "âŸŗ"
136                            }
137                        }
138
139                        "{props.confirm_text.clone().unwrap_or_else(|| \"Confirm\".to_string())}"
140                    }
141                }
142            }
143        }
144
145
146    }
147}
148
149/// Delete confirmation dialog (convenience wrapper)
150#[derive(Props, Clone, PartialEq)]
151pub struct DeleteConfirmDialogProps {
152    pub open: bool,
153    pub on_close: EventHandler<()>,
154    #[props(default)]
155    pub title: Option<String>,
156    pub item_name: String,
157    pub on_confirm: EventHandler<()>,
158    #[props(default = false)]
159    pub loading: bool,
160}
161
162/// Delete confirmation dialog
163#[component]
164pub fn DeleteConfirmDialog(props: DeleteConfirmDialogProps) -> Element {
165    rsx! {
166        ConfirmationDialog {
167            open: props.open,
168            on_close: props.on_close,
169            title: props.title.unwrap_or_else(|| "Delete item".to_string()),
170            message: format!("Are you sure you want to delete \"{}\"? This action cannot be undone.", props.item_name),
171            confirm_text: Some("Delete".to_string()),
172            cancel_text: Some("Keep".to_string()),
173            variant: ConfirmVariant::Danger,
174            icon: Some("đŸ—‘ī¸".to_string()),
175            on_confirm: props.on_confirm,
176            loading: props.loading,
177        }
178    }
179}
180
181/// Unsaved changes dialog
182#[derive(Props, Clone, PartialEq)]
183pub struct UnsavedChangesDialogProps {
184    pub open: bool,
185    pub on_close: EventHandler<()>,
186    pub on_save: EventHandler<()>,
187    pub on_discard: EventHandler<()>,
188}
189
190/// Unsaved changes confirmation dialog
191#[component]
192pub fn UnsavedChangesDialog(props: UnsavedChangesDialogProps) -> Element {
193    let theme = use_theme();
194
195    rsx! {
196        Dialog {
197            open: props.open,
198            on_close: props.on_close,
199            title: "Unsaved changes",
200
201            div {
202                style: "text-align: center; padding: 24px 0;",
203
204                div {
205                    style: "width: 64px; height: 64px; margin: 0 auto 24px; border-radius: 50%; background: #f59e0b20; display: flex; align-items: center; justify-content: center; font-size: 32px;",
206                    "âš ī¸"
207                }
208
209                p {
210                    style: "margin: 0 0 32px 0; font-size: 16px; line-height: 1.6; color: {theme.tokens.read().colors.foreground.to_rgba()};",
211                    "You have unsaved changes. What would you like to do?"
212                }
213
214                div {
215                    style: "display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;",
216
217                    button {
218                        type: "button",
219                        style: "padding: 12px 24px; font-size: 14px; font-weight: 500; color: {theme.tokens.read().colors.foreground.to_rgba()}; background: white; border: 1px solid {theme.tokens.read().colors.border.to_rgba()}; border-radius: 8px; cursor: pointer;",
220                        onclick: move |_| props.on_close.call(()),
221                        "Keep editing"
222                    }
223
224                    button {
225                        type: "button",
226                        style: "padding: 12px 24px; font-size: 14px; font-weight: 500; color: white; background: #ef4444; border: none; border-radius: 8px; cursor: pointer;",
227                        onclick: move |_| props.on_discard.call(()),
228                        "Discard changes"
229                    }
230
231                    button {
232                        type: "button",
233                        style: "padding: 12px 24px; font-size: 14px; font-weight: 500; color: white; background: {theme.tokens.read().colors.primary.to_rgba()}; border: none; border-radius: 8px; cursor: pointer;",
234                        onclick: move |_| props.on_save.call(()),
235                        "Save changes"
236                    }
237                }
238            }
239        }
240    }
241}
242
243/// Sign out confirmation dialog
244#[derive(Props, Clone, PartialEq)]
245pub struct SignOutDialogProps {
246    pub open: bool,
247    pub on_close: EventHandler<()>,
248    pub on_confirm: EventHandler<()>,
249}
250
251/// Sign out confirmation dialog
252#[component]
253pub fn SignOutDialog(props: SignOutDialogProps) -> Element {
254    rsx! {
255        ConfirmationDialog {
256            open: props.open,
257            on_close: props.on_close,
258            title: "Sign out",
259            message: "Are you sure you want to sign out?",
260            confirm_text: "Sign out",
261            cancel_text: "Stay signed in",
262            variant: ConfirmVariant::Info,
263            icon: "👋",
264            on_confirm: props.on_confirm,
265        }
266    }
267}