Skip to main content

tailwind_rs_yew/
components.rs

1//! # Yew Components
2//!
3//! This module provides pre-built Yew components with Tailwind CSS styling.
4
5use tailwind_rs_core::ClassBuilder;
6use yew::prelude::*;
7
8/// Button component with Tailwind styling
9#[derive(Properties, PartialEq, Clone)]
10pub struct ButtonProps {
11    #[prop_or_default]
12    pub variant: ButtonVariant,
13    #[prop_or_default]
14    pub size: ButtonSize,
15    #[prop_or_default]
16    pub disabled: bool,
17    #[prop_or_default]
18    pub class: Option<String>,
19    pub children: Children,
20    #[prop_or_default]
21    pub onclick: Option<Callback<MouseEvent>>,
22}
23
24#[function_component]
25pub fn Button(props: &ButtonProps) -> Html {
26    let classes = use_memo(
27        (props.variant, props.size, props.class.clone()),
28        |(variant, size, class)| {
29            let mut builder = ClassBuilder::new()
30                .class("inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed");
31
32            // Add variant classes
33            match variant {
34                ButtonVariant::Primary => {
35                    builder = builder
36                        .class("bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500");
37                }
38                ButtonVariant::Secondary => {
39                    builder = builder
40                        .class("bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500");
41                }
42                ButtonVariant::Danger => {
43                    builder =
44                        builder.class("bg-red-600 text-white hover:bg-red-700 focus:ring-red-500");
45                }
46                ButtonVariant::Outline => {
47                    builder = builder.class("border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-gray-500");
48                }
49            }
50
51            // Add size classes
52            match size {
53                ButtonSize::Small => {
54                    builder = builder.class("px-3 py-1.5 text-sm rounded-md");
55                }
56                ButtonSize::Medium => {
57                    builder = builder.class("px-4 py-2 text-sm rounded-md");
58                }
59                ButtonSize::Large => {
60                    builder = builder.class("px-6 py-3 text-base rounded-lg");
61                }
62            }
63
64            // Add custom classes if provided
65            if let Some(custom_class) = class {
66                builder = builder.class(custom_class);
67            }
68
69            Classes::from(builder.build().to_css_classes())
70        },
71    );
72
73    html! {
74        <button
75            class={(*classes).clone()}
76            disabled={props.disabled}
77            onclick={props.onclick.clone()}
78        >
79            {props.children.clone()}
80        </button>
81    }
82}
83
84/// Button variant enumeration
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ButtonVariant {
87    Primary,
88    Secondary,
89    Danger,
90    Outline,
91}
92
93impl Default for ButtonVariant {
94    fn default() -> Self {
95        Self::Primary
96    }
97}
98
99/// Button size enumeration
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ButtonSize {
102    Small,
103    Medium,
104    Large,
105}
106
107impl Default for ButtonSize {
108    fn default() -> Self {
109        Self::Medium
110    }
111}
112
113/// Card component with Tailwind styling
114#[derive(Properties, PartialEq, Clone)]
115pub struct CardProps {
116    #[prop_or_default]
117    pub class: Option<String>,
118    pub children: Children,
119}
120
121#[function_component]
122pub fn Card(props: &CardProps) -> Html {
123    let classes = use_memo(props.class.clone(), |class| {
124        let mut builder =
125            ClassBuilder::new().class("bg-white rounded-lg shadow-md border border-gray-200");
126
127        if let Some(custom_class) = class {
128            builder = builder.class(custom_class);
129        }
130
131        Classes::from(builder.build().to_css_classes())
132    });
133
134    html! {
135        <div class={(*classes).clone()}>
136            {props.children.clone()}
137        </div>
138    }
139}
140
141/// Card header component
142#[derive(Properties, PartialEq, Clone)]
143pub struct CardHeaderProps {
144    #[prop_or_default]
145    pub class: Option<String>,
146    pub children: Children,
147}
148
149#[function_component]
150pub fn CardHeader(props: &CardHeaderProps) -> Html {
151    let classes = use_memo(props.class.clone(), |class| {
152        let mut builder = ClassBuilder::new().class("px-6 py-4 border-b border-gray-200");
153
154        if let Some(custom_class) = class {
155            builder = builder.class(custom_class);
156        }
157
158        Classes::from(builder.build().to_css_classes())
159    });
160
161    html! {
162        <div class={(*classes).clone()}>
163            {props.children.clone()}
164        </div>
165    }
166}
167
168/// Card body component
169#[derive(Properties, PartialEq, Clone)]
170pub struct CardBodyProps {
171    #[prop_or_default]
172    pub class: Option<String>,
173    pub children: Children,
174}
175
176#[function_component]
177pub fn CardBody(props: &CardBodyProps) -> Html {
178    let classes = use_memo(props.class.clone(), |class| {
179        let mut builder = ClassBuilder::new().class("px-6 py-4");
180
181        if let Some(custom_class) = class {
182            builder = builder.class(custom_class);
183        }
184
185        Classes::from(builder.build().to_css_classes())
186    });
187
188    html! {
189        <div class={(*classes).clone()}>
190            {props.children.clone()}
191        </div>
192    }
193}
194
195/// Input component with Tailwind styling
196#[derive(Properties, PartialEq, Clone)]
197pub struct InputProps {
198    #[prop_or_default]
199    pub input_type: Option<String>,
200    #[prop_or_default]
201    pub placeholder: Option<String>,
202    #[prop_or_default]
203    pub disabled: bool,
204    #[prop_or_default]
205    pub class: Option<String>,
206    #[prop_or_default]
207    pub value: Option<String>,
208    #[prop_or_default]
209    pub oninput: Option<Callback<InputEvent>>,
210}
211
212#[function_component]
213pub fn Input(props: &InputProps) -> Html {
214    let classes = use_memo(props.class.clone(), |class| {
215        let mut builder = ClassBuilder::new()
216                .class("block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-50 disabled:text-gray-500");
217
218        if let Some(custom_class) = class {
219            builder = builder.class(custom_class);
220        }
221
222        Classes::from(builder.build().to_css_classes())
223    });
224
225    html! {
226        <input
227            type={props.input_type.clone().unwrap_or_else(|| "text".to_string())}
228            class={(*classes).clone()}
229            placeholder={props.placeholder.clone()}
230            disabled={props.disabled}
231            value={props.value.clone()}
232            oninput={props.oninput.clone()}
233        />
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_button_variant_enum() {
243        assert_eq!(ButtonVariant::Primary, ButtonVariant::Primary);
244        assert_ne!(ButtonVariant::Primary, ButtonVariant::Secondary);
245    }
246
247    #[test]
248    fn test_button_size_enum() {
249        assert_eq!(ButtonSize::Medium, ButtonSize::Medium);
250        assert_ne!(ButtonSize::Small, ButtonSize::Large);
251    }
252
253    #[test]
254    fn test_class_builder_integration() {
255        let builder = ClassBuilder::new()
256            .class("px-4 py-2")
257            .class("bg-blue-600 text-white");
258
259        let result = builder.build();
260        let class_string = result.to_css_classes();
261        assert!(class_string.contains("px-4"));
262        assert!(class_string.contains("bg-blue-600"));
263    }
264}