skill_web/components/
button.rs

1//! Button component with variants
2
3use yew::prelude::*;
4
5/// Button variant styles
6#[derive(Clone, PartialEq, Default)]
7pub enum ButtonVariant {
8    #[default]
9    Primary,
10    Secondary,
11    Ghost,
12    Danger,
13}
14
15/// Button size options
16#[derive(Clone, PartialEq, Default)]
17pub enum ButtonSize {
18    Small,
19    #[default]
20    Medium,
21    Large,
22}
23
24/// Button component props
25#[derive(Properties, PartialEq)]
26pub struct ButtonProps {
27    #[prop_or_default]
28    pub children: Children,
29    #[prop_or_default]
30    pub variant: ButtonVariant,
31    #[prop_or_default]
32    pub size: ButtonSize,
33    #[prop_or_default]
34    pub class: Classes,
35    #[prop_or_default]
36    pub disabled: bool,
37    #[prop_or_default]
38    pub loading: bool,
39    #[prop_or_default]
40    pub onclick: Callback<MouseEvent>,
41    #[prop_or_default]
42    pub r#type: Option<AttrValue>,
43}
44
45/// Button component
46#[function_component(Button)]
47pub fn button(props: &ButtonProps) -> Html {
48    let variant_class = match props.variant {
49        ButtonVariant::Primary => "btn-primary",
50        ButtonVariant::Secondary => "btn-secondary",
51        ButtonVariant::Ghost => "btn-ghost",
52        ButtonVariant::Danger => "btn-danger",
53    };
54
55    let size_class = match props.size {
56        ButtonSize::Small => "px-3 py-1.5 text-xs",
57        ButtonSize::Medium => "px-4 py-2 text-sm",
58        ButtonSize::Large => "px-6 py-3 text-base",
59    };
60
61    let button_type = props.r#type.clone().unwrap_or_else(|| "button".into());
62
63    html! {
64        <button
65            type={button_type}
66            class={classes!("btn", variant_class, size_class, props.class.clone())}
67            disabled={props.disabled || props.loading}
68            onclick={props.onclick.clone()}
69        >
70            if props.loading {
71                <svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
72                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
73                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
74                </svg>
75            }
76            { for props.children.iter() }
77        </button>
78    }
79}
80
81/// Icon button component for toolbar actions
82#[derive(Properties, PartialEq)]
83pub struct IconButtonProps {
84    pub children: Children,
85    #[prop_or_default]
86    pub class: Classes,
87    #[prop_or_default]
88    pub disabled: bool,
89    #[prop_or_default]
90    pub onclick: Callback<MouseEvent>,
91    #[prop_or_default]
92    pub title: Option<AttrValue>,
93}
94
95#[function_component(IconButton)]
96pub fn icon_button(props: &IconButtonProps) -> Html {
97    html! {
98        <button
99            type="button"
100            class={classes!(
101                "p-2", "rounded-lg", "text-gray-500", "hover:text-gray-700", "hover:bg-gray-100",
102                "dark:text-gray-400", "dark:hover:text-gray-200", "dark:hover:bg-gray-700",
103                "transition-colors", "disabled:opacity-50", "disabled:cursor-not-allowed",
104                props.class.clone()
105            )}
106            disabled={props.disabled}
107            onclick={props.onclick.clone()}
108            title={props.title.clone()}
109        >
110            { for props.children.iter() }
111        </button>
112    }
113}