tailwind_rs_yew/
components.rs1use tailwind_rs_core::ClassBuilder;
6use yew::prelude::*;
7
8#[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 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 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 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#[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#[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#[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#[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#[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#[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}