1use crate::styles::Style;
7use crate::theme::tokens::Color;
8use crate::theme::{use_style, use_theme};
9use dioxus::prelude::*;
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 { base.w_full() } else { base };
146
147 let sized = match size {
149 ButtonSize::Sm => base.p(&t.spacing, "sm").h_px(32),
150 ButtonSize::Md => base.p(&t.spacing, "md").h_px(40),
151 ButtonSize::Lg => base.p(&t.spacing, "lg").h_px(48),
152 ButtonSize::Icon => base.p(&t.spacing, "md").rounded(&t.radius, "md"),
153 };
154
155 let (bg, fg, border, shadow) = match variant {
157 ButtonVariant::Primary => {
158 let bg = if is_hovered() && !disabled {
159 t.colors.primary.darken(0.1)
160 } else {
161 t.colors.primary.clone()
162 };
163 let shadow = if is_focused() && !disabled {
164 format!("0 0 0 2px {}", t.colors.background.to_rgba())
165 } else {
166 String::new()
167 };
168 (bg, t.colors.primary_foreground.clone(), None, shadow)
169 }
170 ButtonVariant::Secondary => {
171 let bg = if is_hovered() && !disabled {
172 t.colors.secondary.darken(0.05)
173 } else {
174 t.colors.secondary.clone()
175 };
176 (
177 bg,
178 t.colors.secondary_foreground.clone(),
179 None,
180 String::new(),
181 )
182 }
183 ButtonVariant::Ghost => {
184 let bg = if is_hovered() && !disabled {
185 t.colors.accent.clone()
186 } else {
187 Color::new_rgba(0, 0, 0, 0.0)
188 };
189 let border = if is_focused() && !disabled {
190 Some(format!("1px solid {}", t.colors.ring.to_rgba()))
191 } else {
192 None
193 };
194 (bg, t.colors.foreground.clone(), border, String::new())
195 }
196 ButtonVariant::Destructive => {
197 let bg = if is_hovered() && !disabled {
198 t.colors.destructive.darken(0.1)
199 } else {
200 t.colors.destructive.clone()
201 };
202 (bg, Color::new(255, 255, 255), None, String::new())
203 }
204 ButtonVariant::Link => {
205 let fg = if is_hovered() && !disabled {
206 t.colors.primary.darken(0.1)
207 } else {
208 t.colors.primary.clone()
209 };
210 (Color::new_rgba(0, 0, 0, 0.0), fg, None, String::new())
211 }
212 };
213
214 let mut final_style = sized.bg(&bg).text_color(&fg);
215
216 if let Some(b) = border {
217 final_style = Style {
218 border: Some(b),
219 ..final_style
220 };
221 }
222
223 if !shadow.is_empty() {
224 final_style = Style {
225 box_shadow: Some(shadow),
226 ..final_style
227 };
228 }
229
230 final_style.build()
231 });
232
233 let transform = if is_pressed() && !disabled {
235 "transform: scale(0.98);"
236 } else {
237 ""
238 };
239
240 let final_style = if let Some(custom) = &props.style {
242 format!("{} {}{}", style(), custom, transform)
243 } else {
244 format!("{}{}", style(), transform)
245 };
246
247 let class = props.class.clone().unwrap_or_default();
248
249 rsx! {
250 button {
251 r#type: props.button_type.as_str(),
252 style: "{final_style}",
253 class: "{class}",
254 disabled: disabled,
255 onmouseenter: move |_| if !disabled { is_hovered.set(true) },
256 onmouseleave: move |_| { is_hovered.set(false); is_pressed.set(false); },
257 onmousedown: move |_| if !disabled { is_pressed.set(true) },
258 onmouseup: move |_| if !disabled { is_pressed.set(false) },
259 onfocus: move |_| is_focused.set(true),
260 onblur: move |_| is_focused.set(false),
261 onclick: move |e| {
262 if let Some(handler) = &props.onclick {
263 if !disabled {
264 handler.call(e);
265 }
266 }
267 },
268 {props.children}
269 }
270 }
271}
272
273#[component]
275pub fn IconButton(
276 icon: Element,
277 #[props(default)] variant: ButtonVariant,
278 #[props(default)] size: ButtonSize,
279 #[props(default)] disabled: bool,
280 #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
281) -> Element {
282 rsx! {
283 Button {
284 variant: variant,
285 size: ButtonSize::Icon,
286 disabled: disabled,
287 onclick: onclick,
288 {icon}
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_button_variant_equality() {
299 assert_eq!(ButtonVariant::Primary, ButtonVariant::Primary);
300 assert_ne!(ButtonVariant::Primary, ButtonVariant::Secondary);
301 }
302
303 #[test]
304 fn test_button_size_equality() {
305 assert_eq!(ButtonSize::Md, ButtonSize::Md);
306 assert_ne!(ButtonSize::Sm, ButtonSize::Lg);
307 }
308}