dioxus_ui_system/molecules/
dialog.rs1use dioxus::prelude::*;
6use crate::theme::{use_theme, use_style};
7use crate::styles::Style;
8use crate::atoms::{Button, ButtonVariant};
9
10#[derive(Props, Clone, PartialEq)]
12pub struct DialogProps {
13 pub open: bool,
15 pub on_close: EventHandler<()>,
17 pub children: Element,
19 #[props(default)]
21 pub title: Option<String>,
22 #[props(default)]
24 pub description: Option<String>,
25 #[props(default = true)]
27 pub show_close_button: bool,
28 #[props(default = true)]
30 pub close_on_overlay_click: bool,
31 #[props(default)]
33 pub content_style: Option<String>,
34}
35
36#[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 rsx! {
81 div {
82 style: "{overlay_style}",
83 onclick: handle_overlay_click,
84
85 div {
86 style: "{content_style} {props.content_style.clone().unwrap_or_default()}",
87 onclick: move |e| e.stop_propagation(),
88
89 if props.title.is_some() || props.show_close_button {
91 DialogHeader {
92 title: props.title.clone(),
93 show_close_button: props.show_close_button,
94 on_close: props.on_close.clone(),
95 }
96 }
97
98 if let Some(description) = props.description.clone() {
100 DialogDescription { description: description }
101 }
102
103 div {
105 style: "padding: 0 24px 24px 24px; overflow-y: auto;",
106 {props.children}
107 }
108 }
109 }
110 }
111}
112
113use crate::theme::tokens::Color;
114
115#[derive(Props, Clone, PartialEq)]
116struct DialogHeaderProps {
117 title: Option<String>,
118 show_close_button: bool,
119 on_close: EventHandler<()>,
120}
121
122#[component]
123fn DialogHeader(props: DialogHeaderProps) -> Element {
124 let _theme = use_theme();
125
126 let header_style = use_style(|t| {
127 Style::new()
128 .flex()
129 .items_center()
130 .justify_between()
131 .p(&t.spacing, "lg")
132 .border_bottom(1, &t.colors.border)
133 .build()
134 });
135
136 rsx! {
137 div {
138 style: "{header_style}",
139
140 if let Some(title) = props.title {
141 h2 {
142 style: "margin: 0; font-size: 18px; font-weight: 600;",
143 "{title}"
144 }
145 } else {
146 div {}
147 }
148
149 if props.show_close_button {
150 Button {
151 variant: ButtonVariant::Ghost,
152 onclick: move |_| props.on_close.call(()),
153 "✕"
154 }
155 }
156 }
157 }
158}
159
160#[derive(Props, Clone, PartialEq)]
161struct DialogDescriptionProps {
162 description: String,
163}
164
165#[component]
166fn DialogDescription(props: DialogDescriptionProps) -> Element {
167 rsx! {
168 p {
169 style: "margin: 0; padding: 16px 24px 0 24px; font-size: 14px; color: #64748b;",
170 "{props.description}"
171 }
172 }
173}
174
175#[derive(Props, Clone, PartialEq)]
177pub struct DialogFooterProps {
178 pub children: Element,
180 #[props(default)]
182 pub align: DialogFooterAlign,
183}
184
185#[derive(Default, Clone, PartialEq)]
187pub enum DialogFooterAlign {
188 #[default]
190 Start,
191 Center,
193 End,
195 Between,
197}
198
199#[component]
201pub fn DialogFooter(props: DialogFooterProps) -> Element {
202 let _theme = use_theme();
203
204 let justify = match props.align {
205 DialogFooterAlign::Start => "flex-start",
206 DialogFooterAlign::Center => "center",
207 DialogFooterAlign::End => "flex-end",
208 DialogFooterAlign::Between => "space-between",
209 };
210
211 let footer_style = use_style(|t| {
212 Style::new()
213 .flex()
214 .items_center()
215 .gap(&t.spacing, "sm")
216 .p(&t.spacing, "lg")
217 .border_top(1, &t.colors.border)
218 .build()
219 });
220
221 rsx! {
222 div {
223 style: "{footer_style} justify-content: {justify};",
224 {props.children}
225 }
226 }
227}
228
229#[derive(Props, Clone, PartialEq)]
231pub struct AlertDialogProps {
232 pub open: bool,
234 pub on_close: EventHandler<()>,
236 pub title: String,
238 pub description: String,
240 #[props(default = "Cancel".to_string())]
242 pub cancel_text: String,
243 #[props(default = "Confirm".to_string())]
245 pub confirm_text: String,
246 pub on_confirm: EventHandler<()>,
248 #[props(default)]
250 pub destructive: bool,
251}
252
253#[component]
255pub fn AlertDialog(props: AlertDialogProps) -> Element {
256 let confirm_variant = if props.destructive {
257 ButtonVariant::Destructive
258 } else {
259 ButtonVariant::Primary
260 };
261
262 rsx! {
263 Dialog {
264 open: props.open,
265 on_close: props.on_close.clone(),
266 title: props.title.clone(),
267 show_close_button: false,
268 close_on_overlay_click: false,
269
270 p {
271 style: "margin: 0 0 24px 0; font-size: 14px; color: #64748b; line-height: 1.5;",
272 "{props.description}"
273 }
274
275 DialogFooter {
276 align: DialogFooterAlign::End,
277
278 Button {
279 variant: ButtonVariant::Ghost,
280 onclick: move |_| props.on_close.call(()),
281 "{props.cancel_text}"
282 }
283
284 Button {
285 variant: confirm_variant,
286 onclick: move |_| props.on_confirm.call(()),
287 "{props.confirm_text}"
288 }
289 }
290 }
291 }
292}