1use dioxus::prelude::*;
7use crate::theme::{use_theme, use_style};
8use crate::styles::Style;
9use crate::theme::tokens::Color;
10
11#[derive(Default, Clone, PartialEq, Debug)]
13pub enum ButtonVariant {
14 #[default]
16 Primary,
17 Secondary,
19 Ghost,
21 Destructive,
23 Link,
25}
26
27#[derive(Default, Clone, PartialEq, Debug)]
29pub enum ButtonSize {
30 Sm,
32 #[default]
34 Md,
35 Lg,
37 Icon,
39}
40
41#[derive(Props, Clone, PartialEq)]
43pub struct ButtonProps {
44 pub children: Element,
46 #[props(default)]
48 pub variant: ButtonVariant,
49 #[props(default)]
51 pub size: ButtonSize,
52 #[props(default)]
54 pub disabled: bool,
55 #[props(default)]
57 pub full_width: bool,
58 #[props(default)]
60 pub onclick: Option<EventHandler<MouseEvent>>,
61 #[props(default)]
63 pub style: Option<String>,
64 #[props(default)]
66 pub class: Option<String>,
67 #[props(default)]
69 pub button_type: ButtonType,
70}
71
72#[derive(Default, Clone, PartialEq)]
74pub enum ButtonType {
75 #[default]
76 Button,
77 Submit,
78 Reset,
79}
80
81impl ButtonType {
82 pub fn as_str(&self) -> &'static str {
83 match self {
84 ButtonType::Button => "button",
85 ButtonType::Submit => "submit",
86 ButtonType::Reset => "reset",
87 }
88 }
89}
90
91#[component]
108pub fn Button(props: ButtonProps) -> Element {
109 let _theme = use_theme();
110
111 let variant = props.variant.clone();
112 let size = props.size.clone();
113 let disabled = props.disabled;
114 let full_width = props.full_width;
115
116 let mut is_hovered = use_signal(|| false);
118 let mut is_pressed = use_signal(|| false);
119 let mut is_focused = use_signal(|| false);
120
121 let style = use_style(move |t| {
123 let base = Style::new()
124 .inline_flex()
125 .items_center()
126 .justify_center()
127 .gap(&t.spacing, "sm")
128 .rounded(&t.radius, "md")
129 .text(&t.typography, "sm")
130 .font_weight(500)
131 .line_height(1.0)
132 .transition("all 150ms cubic-bezier(0.4, 0, 0.2, 1)")
133 .select_none()
134 .whitespace_nowrap()
135 .cursor(if disabled { "not-allowed" } else { "pointer" });
136
137 let base = if disabled {
139 base.opacity(0.5)
140 } else {
141 base.opacity(1.0)
142 };
143
144 let base = if full_width {
146 base.w_full()
147 } else {
148 base
149 };
150
151 let sized = match size {
153 ButtonSize::Sm => base.p(&t.spacing, "sm").h_px(32),
154 ButtonSize::Md => base.p(&t.spacing, "md").h_px(40),
155 ButtonSize::Lg => base.p(&t.spacing, "lg").h_px(48),
156 ButtonSize::Icon => base.p(&t.spacing, "md").rounded(&t.radius, "md"),
157 };
158
159 let (bg, fg, border, shadow) = match variant {
161 ButtonVariant::Primary => {
162 let bg = if is_hovered() && !disabled {
163 t.colors.primary.darken(0.1)
164 } else {
165 t.colors.primary.clone()
166 };
167 let shadow = if is_focused() && !disabled {
168 format!("0 0 0 2px {}", t.colors.background.to_rgba())
169 } else {
170 String::new()
171 };
172 (bg, t.colors.primary_foreground.clone(), None, shadow)
173 }
174 ButtonVariant::Secondary => {
175 let bg = if is_hovered() && !disabled {
176 t.colors.secondary.darken(0.05)
177 } else {
178 t.colors.secondary.clone()
179 };
180 (bg, t.colors.secondary_foreground.clone(), None, String::new())
181 }
182 ButtonVariant::Ghost => {
183 let bg = if is_hovered() && !disabled {
184 t.colors.accent.clone()
185 } else {
186 Color::new_rgba(0, 0, 0, 0.0)
187 };
188 let border = if is_focused() && !disabled {
189 Some(format!("1px solid {}", t.colors.ring.to_rgba()))
190 } else {
191 None
192 };
193 (bg, t.colors.foreground.clone(), border, String::new())
194 }
195 ButtonVariant::Destructive => {
196 let bg = if is_hovered() && !disabled {
197 t.colors.destructive.darken(0.1)
198 } else {
199 t.colors.destructive.clone()
200 };
201 (bg, Color::new(255, 255, 255), None, String::new())
202 }
203 ButtonVariant::Link => {
204 let fg = if is_hovered() && !disabled {
205 t.colors.primary.darken(0.1)
206 } else {
207 t.colors.primary.clone()
208 };
209 (Color::new_rgba(0, 0, 0, 0.0), fg, None, String::new())
210 }
211 };
212
213 let mut final_style = sized
214 .bg(&bg)
215 .text_color(&fg);
216
217 if let Some(b) = border {
218 final_style = Style {
219 border: Some(b),
220 ..final_style
221 };
222 }
223
224 if !shadow.is_empty() {
225 final_style = Style {
226 box_shadow: Some(shadow),
227 ..final_style
228 };
229 }
230
231 final_style.build()
232 });
233
234 let transform = if is_pressed() && !disabled {
236 "transform: scale(0.98);"
237 } else {
238 ""
239 };
240
241 let final_style = if let Some(custom) = &props.style {
243 format!("{} {}{}", style(), custom, transform)
244 } else {
245 format!("{}{}", style(), transform)
246 };
247
248 let class = props.class.clone().unwrap_or_default();
249
250 rsx! {
251 button {
252 r#type: props.button_type.as_str(),
253 style: "{final_style}",
254 class: "{class}",
255 disabled: disabled,
256 onmouseenter: move |_| if !disabled { is_hovered.set(true) },
257 onmouseleave: move |_| { is_hovered.set(false); is_pressed.set(false); },
258 onmousedown: move |_| if !disabled { is_pressed.set(true) },
259 onmouseup: move |_| if !disabled { is_pressed.set(false) },
260 onfocus: move |_| is_focused.set(true),
261 onblur: move |_| is_focused.set(false),
262 onclick: move |e| {
263 if let Some(handler) = &props.onclick {
264 if !disabled {
265 handler.call(e);
266 }
267 }
268 },
269 {props.children}
270 }
271 }
272}
273
274#[component]
276pub fn IconButton(
277 icon: Element,
278 #[props(default)]
279 variant: ButtonVariant,
280 #[props(default)]
281 size: ButtonSize,
282 #[props(default)]
283 disabled: bool,
284 #[props(default)]
285 onclick: Option<EventHandler<MouseEvent>>,
286) -> Element {
287 rsx! {
288 Button {
289 variant: variant,
290 size: ButtonSize::Icon,
291 disabled: disabled,
292 onclick: onclick,
293 {icon}
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_button_variant_equality() {
304 assert_eq!(ButtonVariant::Primary, ButtonVariant::Primary);
305 assert_ne!(ButtonVariant::Primary, ButtonVariant::Secondary);
306 }
307
308 #[test]
309 fn test_button_size_equality() {
310 assert_eq!(ButtonSize::Md, ButtonSize::Md);
311 assert_ne!(ButtonSize::Sm, ButtonSize::Lg);
312 }
313}