1use crate::error::{Result, TailwindError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Color {
10 Hex(String),
12 Rgb { r: u8, g: u8, b: u8 },
14 Rgba { r: u8, g: u8, b: u8, a: f32 },
16 Hsl { h: f32, s: f32, l: f32 },
18 Hsla { h: f32, s: f32, l: f32, a: f32 },
20 Named(String),
22}
23
24impl Color {
25 pub fn hex(value: impl Into<String>) -> Self {
27 Self::Hex(value.into())
28 }
29
30 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
32 Self::Rgb { r, g, b }
33 }
34
35 pub fn rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
37 Self::Rgba { r, g, b, a }
38 }
39
40 pub fn hsl(h: f32, s: f32, l: f32) -> Self {
42 Self::Hsl { h, s, l }
43 }
44
45 pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
47 Self::Hsla { h, s, l, a }
48 }
49
50 pub fn named(name: impl Into<String>) -> Self {
52 Self::Named(name.into())
53 }
54
55 pub fn to_css(&self) -> String {
57 match self {
58 Color::Hex(value) => value.clone(),
59 Color::Rgb { r, g, b } => format!("rgb({}, {}, {})", r, g, b),
60 Color::Rgba { r, g, b, a } => format!("rgba({}, {}, {}, {})", r, g, b, a),
61 Color::Hsl { h, s, l } => format!("hsl({}, {}%, {}%)", h, s * 100.0, l * 100.0),
62 Color::Hsla { h, s, l, a } => {
63 format!("hsla({}, {}%, {}%, {})", h, s * 100.0, l * 100.0, a)
64 }
65 Color::Named(name) => format!("var(--color-{})", name),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub enum Spacing {
73 Px(f32),
75 Rem(f32),
77 Em(f32),
79 Percent(f32),
81 Vw(f32),
83 Vh(f32),
85 Named(String),
87}
88
89impl Spacing {
90 pub fn px(value: f32) -> Self {
92 Self::Px(value)
93 }
94
95 pub fn rem(value: f32) -> Self {
97 Self::Rem(value)
98 }
99
100 pub fn em(value: f32) -> Self {
102 Self::Em(value)
103 }
104
105 pub fn percent(value: f32) -> Self {
107 Self::Percent(value)
108 }
109
110 pub fn vw(value: f32) -> Self {
112 Self::Vw(value)
113 }
114
115 pub fn vh(value: f32) -> Self {
117 Self::Vh(value)
118 }
119
120 pub fn named(name: impl Into<String>) -> Self {
122 Self::Named(name.into())
123 }
124
125 pub fn to_css(&self) -> String {
127 match self {
128 Spacing::Px(value) => format!("{}px", value),
129 Spacing::Rem(value) => format!("{}rem", value),
130 Spacing::Em(value) => format!("{}em", value),
131 Spacing::Percent(value) => format!("{}%", value),
132 Spacing::Vw(value) => format!("{}vw", value),
133 Spacing::Vh(value) => format!("{}vh", value),
134 Spacing::Named(name) => format!("var(--spacing-{})", name),
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum BorderRadius {
142 Px(f32),
144 Rem(f32),
146 Percent(f32),
148 Named(String),
150}
151
152impl BorderRadius {
153 pub fn px(value: f32) -> Self {
155 Self::Px(value)
156 }
157
158 pub fn rem(value: f32) -> Self {
160 Self::Rem(value)
161 }
162
163 pub fn percent(value: f32) -> Self {
165 Self::Percent(value)
166 }
167
168 pub fn named(name: impl Into<String>) -> Self {
170 Self::Named(name.into())
171 }
172
173 pub fn to_css(&self) -> String {
175 match self {
176 BorderRadius::Px(value) => format!("{}px", value),
177 BorderRadius::Rem(value) => format!("{}rem", value),
178 BorderRadius::Percent(value) => format!("{}%", value),
179 BorderRadius::Named(name) => format!("var(--border-radius-{})", name),
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186pub struct BoxShadow {
187 pub offset_x: f32,
188 pub offset_y: f32,
189 pub blur_radius: f32,
190 pub spread_radius: f32,
191 pub color: Color,
192 pub inset: bool,
193}
194
195impl BoxShadow {
196 pub fn new(
198 offset_x: f32,
199 offset_y: f32,
200 blur_radius: f32,
201 spread_radius: f32,
202 color: Color,
203 inset: bool,
204 ) -> Self {
205 Self {
206 offset_x,
207 offset_y,
208 blur_radius,
209 spread_radius,
210 color,
211 inset,
212 }
213 }
214
215 pub fn to_css(&self) -> String {
217 let inset = if self.inset { "inset " } else { "" };
218 format!(
219 "{}box-shadow: {}px {}px {}px {}px {}",
220 inset,
221 self.offset_x,
222 self.offset_y,
223 self.blur_radius,
224 self.spread_radius,
225 self.color.to_css()
226 )
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub enum ThemeValue {
233 Color(Color),
234 Spacing(Spacing),
235 BorderRadius(BorderRadius),
236 BoxShadow(BoxShadow),
237 String(String),
238 Number(f32),
239 Boolean(bool),
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct Theme {
245 pub name: String,
246 pub colors: HashMap<String, Color>,
247 pub spacing: HashMap<String, Spacing>,
248 pub border_radius: HashMap<String, BorderRadius>,
249 pub box_shadows: HashMap<String, BoxShadow>,
250 pub custom: HashMap<String, ThemeValue>,
251}
252
253impl Theme {
254 pub fn new(name: impl Into<String>) -> Self {
256 Self {
257 name: name.into(),
258 colors: HashMap::new(),
259 spacing: HashMap::new(),
260 border_radius: HashMap::new(),
261 box_shadows: HashMap::new(),
262 custom: HashMap::new(),
263 }
264 }
265
266 pub fn add_color(&mut self, name: impl Into<String>, color: Color) {
268 self.colors.insert(name.into(), color);
269 }
270
271 pub fn add_spacing(&mut self, name: impl Into<String>, spacing: Spacing) {
273 self.spacing.insert(name.into(), spacing);
274 }
275
276 pub fn add_border_radius(&mut self, name: impl Into<String>, radius: BorderRadius) {
278 self.border_radius.insert(name.into(), radius);
279 }
280
281 pub fn add_box_shadow(&mut self, name: impl Into<String>, shadow: BoxShadow) {
283 self.box_shadows.insert(name.into(), shadow);
284 }
285
286 pub fn add_custom(&mut self, name: impl Into<String>, value: ThemeValue) {
288 self.custom.insert(name.into(), value);
289 }
290
291 pub fn get_color(&self, name: &str) -> Result<&Color> {
293 self.colors.get(name).ok_or_else(|| {
294 TailwindError::theme(format!(
295 "Color '{}' not found in theme '{}'",
296 name, self.name
297 ))
298 })
299 }
300
301 pub fn get_spacing(&self, name: &str) -> Result<&Spacing> {
303 self.spacing.get(name).ok_or_else(|| {
304 TailwindError::theme(format!(
305 "Spacing '{}' not found in theme '{}'",
306 name, self.name
307 ))
308 })
309 }
310
311 pub fn get_border_radius(&self, name: &str) -> Result<&BorderRadius> {
313 self.border_radius.get(name).ok_or_else(|| {
314 TailwindError::theme(format!(
315 "Border radius '{}' not found in theme '{}'",
316 name, self.name
317 ))
318 })
319 }
320
321 pub fn get_box_shadow(&self, name: &str) -> Result<&BoxShadow> {
323 self.box_shadows.get(name).ok_or_else(|| {
324 TailwindError::theme(format!(
325 "Box shadow '{}' not found in theme '{}'",
326 name, self.name
327 ))
328 })
329 }
330
331 pub fn get_custom(&self, name: &str) -> Result<&ThemeValue> {
333 self.custom.get(name).ok_or_else(|| {
334 TailwindError::theme(format!(
335 "Custom value '{}' not found in theme '{}'",
336 name, self.name
337 ))
338 })
339 }
340}
341
342pub fn create_default_theme() -> Theme {
344 let mut theme = Theme::new("default");
345
346 theme.add_color("primary", Color::hex("#3b82f6"));
348 theme.add_color("secondary", Color::hex("#64748b"));
349 theme.add_color("success", Color::hex("#10b981"));
350 theme.add_color("warning", Color::hex("#f59e0b"));
351 theme.add_color("error", Color::hex("#ef4444"));
352 theme.add_color("white", Color::hex("#ffffff"));
353 theme.add_color("black", Color::hex("#000000"));
354 theme.add_color("gray-100", Color::hex("#f3f4f6"));
355 theme.add_color("gray-500", Color::hex("#6b7280"));
356 theme.add_color("gray-900", Color::hex("#111827"));
357
358 theme.add_spacing("xs", Spacing::rem(0.25));
360 theme.add_spacing("sm", Spacing::rem(0.5));
361 theme.add_spacing("md", Spacing::rem(1.0));
362 theme.add_spacing("lg", Spacing::rem(1.5));
363 theme.add_spacing("xl", Spacing::rem(2.0));
364 theme.add_spacing("2xl", Spacing::rem(3.0));
365
366 theme.add_border_radius("sm", BorderRadius::rem(0.125));
368 theme.add_border_radius("md", BorderRadius::rem(0.375));
369 theme.add_border_radius("lg", BorderRadius::rem(0.5));
370 theme.add_border_radius("xl", BorderRadius::rem(0.75));
371 theme.add_border_radius("full", BorderRadius::percent(50.0));
372
373 theme.add_box_shadow(
375 "sm",
376 BoxShadow::new(0.0, 1.0, 2.0, 0.0, Color::hex("#000000"), false),
377 );
378 theme.add_box_shadow(
379 "md",
380 BoxShadow::new(0.0, 4.0, 6.0, -1.0, Color::hex("#000000"), false),
381 );
382 theme.add_box_shadow(
383 "lg",
384 BoxShadow::new(0.0, 10.0, 15.0, -3.0, Color::hex("#000000"), false),
385 );
386
387 theme
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_color_creation() {
396 let hex_color = Color::hex("#3b82f6");
397 assert_eq!(hex_color, Color::Hex("#3b82f6".to_string()));
398
399 let rgb_color = Color::rgb(59, 130, 246);
400 assert_eq!(
401 rgb_color,
402 Color::Rgb {
403 r: 59,
404 g: 130,
405 b: 246
406 }
407 );
408
409 let named_color = Color::named("primary");
410 assert_eq!(named_color, Color::Named("primary".to_string()));
411 }
412
413 #[test]
414 fn test_color_to_css() {
415 let hex_color = Color::hex("#3b82f6");
416 assert_eq!(hex_color.to_css(), "#3b82f6");
417
418 let rgb_color = Color::rgb(59, 130, 246);
419 assert_eq!(rgb_color.to_css(), "rgb(59, 130, 246)");
420
421 let named_color = Color::named("primary");
422 assert_eq!(named_color.to_css(), "var(--color-primary)");
423 }
424
425 #[test]
426 fn test_spacing_creation() {
427 let px_spacing = Spacing::px(16.0);
428 assert_eq!(px_spacing, Spacing::Px(16.0));
429
430 let rem_spacing = Spacing::rem(1.0);
431 assert_eq!(rem_spacing, Spacing::Rem(1.0));
432
433 let named_spacing = Spacing::named("md");
434 assert_eq!(named_spacing, Spacing::Named("md".to_string()));
435 }
436
437 #[test]
438 fn test_spacing_to_css() {
439 let px_spacing = Spacing::px(16.0);
440 assert_eq!(px_spacing.to_css(), "16px");
441
442 let rem_spacing = Spacing::rem(1.0);
443 assert_eq!(rem_spacing.to_css(), "1rem");
444
445 let named_spacing = Spacing::named("md");
446 assert_eq!(named_spacing.to_css(), "var(--spacing-md)");
447 }
448
449 #[test]
450 fn test_theme_creation() {
451 let mut theme = Theme::new("test");
452 assert_eq!(theme.name, "test");
453
454 theme.add_color("primary", Color::hex("#3b82f6"));
455 assert!(theme.colors.contains_key("primary"));
456
457 let color = theme.get_color("primary").unwrap();
458 assert_eq!(color, &Color::hex("#3b82f6"));
459 }
460
461 #[test]
462 fn test_theme_error_handling() {
463 let theme = Theme::new("test");
464 let result = theme.get_color("nonexistent");
465 assert!(result.is_err());
466
467 if let Err(TailwindError::Theme { message }) = result {
468 assert!(message.contains("Color 'nonexistent' not found"));
469 }
470 }
471
472 #[test]
473 fn test_default_theme() {
474 let theme = create_default_theme();
475 assert_eq!(theme.name, "default");
476
477 assert!(theme.get_color("primary").is_ok());
479 assert!(theme.get_color("secondary").is_ok());
480 assert!(theme.get_color("success").is_ok());
481
482 assert!(theme.get_spacing("sm").is_ok());
484 assert!(theme.get_spacing("md").is_ok());
485 assert!(theme.get_spacing("lg").is_ok());
486
487 assert!(theme.get_border_radius("sm").is_ok());
489 assert!(theme.get_border_radius("md").is_ok());
490 assert!(theme.get_border_radius("lg").is_ok());
491 }
492}