tailwind_rs_core/
theme.rs

1//! Theme system for tailwind-rs
2
3use crate::error::{Result, TailwindError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::str::FromStr;
7
8/// Represents a color value in the theme system
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum Color {
11    /// Hex color value (e.g., "#3b82f6")
12    Hex(String),
13    /// RGB color value
14    Rgb { r: u8, g: u8, b: u8 },
15    /// RGBA color value
16    Rgba { r: u8, g: u8, b: u8, a: f32 },
17    /// HSL color value
18    Hsl { h: f32, s: f32, l: f32 },
19    /// HSLA color value
20    Hsla { h: f32, s: f32, l: f32, a: f32 },
21    /// Named color reference
22    Named(String),
23}
24
25impl Color {
26    /// Create a new hex color
27    pub fn hex(value: impl Into<String>) -> Self {
28        Self::Hex(value.into())
29    }
30
31    /// Create a new RGB color
32    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
33        Self::Rgb { r, g, b }
34    }
35
36    /// Create a new RGBA color
37    pub fn rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
38        Self::Rgba { r, g, b, a }
39    }
40
41    /// Create a new HSL color
42    pub fn hsl(h: f32, s: f32, l: f32) -> Self {
43        Self::Hsl { h, s, l }
44    }
45
46    /// Create a new HSLA color
47    pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
48        Self::Hsla { h, s, l, a }
49    }
50
51    /// Create a new named color
52    pub fn named(name: impl Into<String>) -> Self {
53        Self::Named(name.into())
54    }
55
56    /// Convert color to CSS string
57    pub fn to_css(&self) -> String {
58        match self {
59            Color::Hex(value) => value.clone(),
60            Color::Rgb { r, g, b } => format!("rgb({}, {}, {})", r, g, b),
61            Color::Rgba { r, g, b, a } => format!("rgba({}, {}, {}, {})", r, g, b, a),
62            Color::Hsl { h, s, l } => format!("hsl({}, {}%, {}%)", h, s * 100.0, l * 100.0),
63            Color::Hsla { h, s, l, a } => {
64                format!("hsla({}, {}%, {}%, {})", h, s * 100.0, l * 100.0, a)
65            }
66            Color::Named(name) => format!("var(--color-{})", name),
67        }
68    }
69}
70
71impl FromStr for Color {
72    type Err = TailwindError;
73
74    fn from_str(s: &str) -> Result<Self> {
75        let s = s.trim();
76        
77        if s.starts_with('#') {
78            Ok(Color::hex(s))
79        } else if s.starts_with("rgb(") {
80            // Parse rgb(r, g, b) format
81            let content = s.strip_prefix("rgb(")
82                .and_then(|s| s.strip_suffix(')'))
83                .ok_or_else(|| TailwindError::theme("Invalid RGB format"))?;
84            
85            let values: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
86            if values.len() != 3 {
87                return Err(TailwindError::theme("RGB must have 3 values"));
88            }
89            
90            let r = values[0].parse::<u8>()
91                .map_err(|_| TailwindError::theme("Invalid RGB red value"))?;
92            let g = values[1].parse::<u8>()
93                .map_err(|_| TailwindError::theme("Invalid RGB green value"))?;
94            let b = values[2].parse::<u8>()
95                .map_err(|_| TailwindError::theme("Invalid RGB blue value"))?;
96            
97            Ok(Color::rgb(r, g, b))
98        } else if s.starts_with("rgba(") {
99            // Parse rgba(r, g, b, a) format
100            let content = s.strip_prefix("rgba(")
101                .and_then(|s| s.strip_suffix(')'))
102                .ok_or_else(|| TailwindError::theme("Invalid RGBA format"))?;
103            
104            let values: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
105            if values.len() != 4 {
106                return Err(TailwindError::theme("RGBA must have 4 values"));
107            }
108            
109            let r = values[0].parse::<u8>()
110                .map_err(|_| TailwindError::theme("Invalid RGBA red value"))?;
111            let g = values[1].parse::<u8>()
112                .map_err(|_| TailwindError::theme("Invalid RGBA green value"))?;
113            let b = values[2].parse::<u8>()
114                .map_err(|_| TailwindError::theme("Invalid RGBA blue value"))?;
115            let a = values[3].parse::<f32>()
116                .map_err(|_| TailwindError::theme("Invalid RGBA alpha value"))?;
117            
118            Ok(Color::rgba(r, g, b, a))
119        } else {
120            // Named color
121            Ok(Color::named(s))
122        }
123    }
124}
125
126/// Represents a spacing value in the theme system
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum Spacing {
129    /// Pixel value
130    Px(f32),
131    /// Rem value
132    Rem(f32),
133    /// Em value
134    Em(f32),
135    /// Percentage value
136    Percent(f32),
137    /// Viewport width percentage
138    Vw(f32),
139    /// Viewport height percentage
140    Vh(f32),
141    /// Named spacing reference
142    Named(String),
143}
144
145impl Spacing {
146    /// Create a new pixel spacing
147    pub fn px(value: f32) -> Self {
148        Self::Px(value)
149    }
150
151    /// Create a new rem spacing
152    pub fn rem(value: f32) -> Self {
153        Self::Rem(value)
154    }
155
156    /// Create a new em spacing
157    pub fn em(value: f32) -> Self {
158        Self::Em(value)
159    }
160
161    /// Create a new percentage spacing
162    pub fn percent(value: f32) -> Self {
163        Self::Percent(value)
164    }
165
166    /// Create a new viewport width spacing
167    pub fn vw(value: f32) -> Self {
168        Self::Vw(value)
169    }
170
171    /// Create a new viewport height spacing
172    pub fn vh(value: f32) -> Self {
173        Self::Vh(value)
174    }
175
176    /// Create a new named spacing
177    pub fn named(name: impl Into<String>) -> Self {
178        Self::Named(name.into())
179    }
180
181    /// Convert spacing to CSS string
182    pub fn to_css(&self) -> String {
183        match self {
184            Spacing::Px(value) => format!("{}px", value),
185            Spacing::Rem(value) => format!("{}rem", value),
186            Spacing::Em(value) => format!("{}em", value),
187            Spacing::Percent(value) => format!("{}%", value),
188            Spacing::Vw(value) => format!("{}vw", value),
189            Spacing::Vh(value) => format!("{}vh", value),
190            Spacing::Named(name) => format!("var(--spacing-{})", name),
191        }
192    }
193}
194
195impl FromStr for Spacing {
196    type Err = TailwindError;
197
198    fn from_str(s: &str) -> Result<Self> {
199        let s = s.trim();
200        
201        if s.ends_with("px") {
202            let value = s.strip_suffix("px")
203                .ok_or_else(|| TailwindError::theme("Invalid pixel value"))?
204                .parse::<f32>()
205                .map_err(|_| TailwindError::theme("Invalid pixel value"))?;
206            Ok(Spacing::px(value))
207        } else if s.ends_with("rem") {
208            let value = s.strip_suffix("rem")
209                .ok_or_else(|| TailwindError::theme("Invalid rem value"))?
210                .parse::<f32>()
211                .map_err(|_| TailwindError::theme("Invalid rem value"))?;
212            Ok(Spacing::rem(value))
213        } else if s.ends_with("em") {
214            let value = s.strip_suffix("em")
215                .ok_or_else(|| TailwindError::theme("Invalid em value"))?
216                .parse::<f32>()
217                .map_err(|_| TailwindError::theme("Invalid em value"))?;
218            Ok(Spacing::em(value))
219        } else if s.ends_with('%') {
220            let value = s.strip_suffix('%')
221                .ok_or_else(|| TailwindError::theme("Invalid percentage value"))?
222                .parse::<f32>()
223                .map_err(|_| TailwindError::theme("Invalid percentage value"))?;
224            Ok(Spacing::percent(value))
225        } else if s.ends_with("vw") {
226            let value = s.strip_suffix("vw")
227                .ok_or_else(|| TailwindError::theme("Invalid vw value"))?
228                .parse::<f32>()
229                .map_err(|_| TailwindError::theme("Invalid vw value"))?;
230            Ok(Spacing::vw(value))
231        } else if s.ends_with("vh") {
232            let value = s.strip_suffix("vh")
233                .ok_or_else(|| TailwindError::theme("Invalid vh value"))?
234                .parse::<f32>()
235                .map_err(|_| TailwindError::theme("Invalid vh value"))?;
236            Ok(Spacing::vh(value))
237        } else {
238            // Try parsing as number (defaults to rem)
239            let value = s.parse::<f32>()
240                .map_err(|_| TailwindError::theme("Invalid spacing value"))?;
241            Ok(Spacing::rem(value))
242        }
243    }
244}
245
246/// Represents a border radius value in the theme system
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248pub enum BorderRadius {
249    /// Pixel value
250    Px(f32),
251    /// Rem value
252    Rem(f32),
253    /// Percentage value
254    Percent(f32),
255    /// Named border radius reference
256    Named(String),
257}
258
259impl BorderRadius {
260    /// Create a new pixel border radius
261    pub fn px(value: f32) -> Self {
262        Self::Px(value)
263    }
264
265    /// Create a new rem border radius
266    pub fn rem(value: f32) -> Self {
267        Self::Rem(value)
268    }
269
270    /// Create a new percentage border radius
271    pub fn percent(value: f32) -> Self {
272        Self::Percent(value)
273    }
274
275    /// Create a new named border radius
276    pub fn named(name: impl Into<String>) -> Self {
277        Self::Named(name.into())
278    }
279
280    /// Convert border radius to CSS string
281    pub fn to_css(&self) -> String {
282        match self {
283            BorderRadius::Px(value) => format!("{}px", value),
284            BorderRadius::Rem(value) => format!("{}rem", value),
285            BorderRadius::Percent(value) => format!("{}%", value),
286            BorderRadius::Named(name) => format!("var(--border-radius-{})", name),
287        }
288    }
289}
290
291impl FromStr for BorderRadius {
292    type Err = TailwindError;
293
294    fn from_str(s: &str) -> Result<Self> {
295        let s = s.trim();
296        
297        if s.ends_with("px") {
298            let value = s.strip_suffix("px")
299                .ok_or_else(|| TailwindError::theme("Invalid pixel value"))?
300                .parse::<f32>()
301                .map_err(|_| TailwindError::theme("Invalid pixel value"))?;
302            Ok(BorderRadius::px(value))
303        } else if s.ends_with("rem") {
304            let value = s.strip_suffix("rem")
305                .ok_or_else(|| TailwindError::theme("Invalid rem value"))?
306                .parse::<f32>()
307                .map_err(|_| TailwindError::theme("Invalid rem value"))?;
308            Ok(BorderRadius::rem(value))
309        } else if s.ends_with('%') {
310            let value = s.strip_suffix('%')
311                .ok_or_else(|| TailwindError::theme("Invalid percentage value"))?
312                .parse::<f32>()
313                .map_err(|_| TailwindError::theme("Invalid percentage value"))?;
314            Ok(BorderRadius::percent(value))
315        } else {
316            // Try parsing as number (defaults to rem)
317            let value = s.parse::<f32>()
318                .map_err(|_| TailwindError::theme("Invalid border radius value"))?;
319            Ok(BorderRadius::rem(value))
320        }
321    }
322}
323
324/// Represents a box shadow value in the theme system
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct BoxShadow {
327    pub offset_x: f32,
328    pub offset_y: f32,
329    pub blur_radius: f32,
330    pub spread_radius: f32,
331    pub color: Color,
332    pub inset: bool,
333}
334
335impl BoxShadow {
336    /// Create a new box shadow
337    pub fn new(
338        offset_x: f32,
339        offset_y: f32,
340        blur_radius: f32,
341        spread_radius: f32,
342        color: Color,
343        inset: bool,
344    ) -> Self {
345        Self {
346            offset_x,
347            offset_y,
348            blur_radius,
349            spread_radius,
350            color,
351            inset,
352        }
353    }
354
355    /// Convert box shadow to CSS string
356    pub fn to_css(&self) -> String {
357        let inset = if self.inset { "inset " } else { "" };
358        format!(
359            "{}box-shadow: {}px {}px {}px {}px {}",
360            inset,
361            self.offset_x,
362            self.offset_y,
363            self.blur_radius,
364            self.spread_radius,
365            self.color.to_css()
366        )
367    }
368}
369
370impl FromStr for BoxShadow {
371    type Err = TailwindError;
372
373    fn from_str(s: &str) -> Result<Self> {
374        // Parse box shadow string like "0 1px 3px rgba(0, 0, 0, 0.1)"
375        let parts: Vec<&str> = s.split_whitespace().collect();
376        
377        if parts.len() < 3 {
378            return Err(TailwindError::theme("Invalid box shadow format"));
379        }
380        
381        let offset_x = parts[0].parse::<f32>()
382            .map_err(|_| TailwindError::theme("Invalid box shadow offset x"))?;
383        let offset_y = parts[1].parse::<f32>()
384            .map_err(|_| TailwindError::theme("Invalid box shadow offset y"))?;
385        let blur_radius = parts[2].parse::<f32>()
386            .map_err(|_| TailwindError::theme("Invalid box shadow blur radius"))?;
387        
388        let spread_radius = if parts.len() > 3 && !parts[3].starts_with("rgba") && !parts[3].starts_with("rgb") {
389            parts[3].parse::<f32>()
390                .map_err(|_| TailwindError::theme("Invalid box shadow spread radius"))?
391        } else {
392            0.0
393        };
394        
395        let color_part = if parts.len() > 3 && (parts[3].starts_with("rgba") || parts[3].starts_with("rgb")) {
396            parts[3..].join(" ")
397        } else if parts.len() > 4 {
398            parts[4..].join(" ")
399        } else {
400            "rgba(0, 0, 0, 0.1)".to_string()
401        };
402        
403        let color = Color::from_str(&color_part)?;
404        
405        Ok(BoxShadow::new(offset_x, offset_y, blur_radius, spread_radius, color, false))
406    }
407}
408
409/// Represents a theme value that can be any of the theme types
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411pub enum ThemeValue {
412    Color(Color),
413    Spacing(Spacing),
414    BorderRadius(BorderRadius),
415    BoxShadow(BoxShadow),
416    String(String),
417    Number(f32),
418    Boolean(bool),
419}
420
421/// Main theme structure
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423pub struct Theme {
424    pub name: String,
425    pub colors: HashMap<String, Color>,
426    pub spacing: HashMap<String, Spacing>,
427    pub border_radius: HashMap<String, BorderRadius>,
428    pub box_shadows: HashMap<String, BoxShadow>,
429    pub custom: HashMap<String, ThemeValue>,
430}
431
432impl Theme {
433    /// Create a new theme
434    pub fn new(name: impl Into<String>) -> Self {
435        Self {
436            name: name.into(),
437            colors: HashMap::new(),
438            spacing: HashMap::new(),
439            border_radius: HashMap::new(),
440            box_shadows: HashMap::new(),
441            custom: HashMap::new(),
442        }
443    }
444
445    /// Add a color to the theme
446    pub fn add_color(&mut self, name: impl Into<String>, color: Color) {
447        self.colors.insert(name.into(), color);
448    }
449
450    /// Add spacing to the theme
451    pub fn add_spacing(&mut self, name: impl Into<String>, spacing: Spacing) {
452        self.spacing.insert(name.into(), spacing);
453    }
454
455    /// Add border radius to the theme
456    pub fn add_border_radius(&mut self, name: impl Into<String>, radius: BorderRadius) {
457        self.border_radius.insert(name.into(), radius);
458    }
459
460    /// Add box shadow to the theme
461    pub fn add_box_shadow(&mut self, name: impl Into<String>, shadow: BoxShadow) {
462        self.box_shadows.insert(name.into(), shadow);
463    }
464
465    /// Add custom value to the theme
466    pub fn add_custom(&mut self, name: impl Into<String>, value: ThemeValue) {
467        self.custom.insert(name.into(), value);
468    }
469
470    /// Get a color from the theme
471    pub fn get_color(&self, name: &str) -> Result<&Color> {
472        self.colors.get(name).ok_or_else(|| {
473            TailwindError::theme(format!(
474                "Color '{}' not found in theme '{}'",
475                name, self.name
476            ))
477        })
478    }
479
480    /// Get spacing from the theme
481    pub fn get_spacing(&self, name: &str) -> Result<&Spacing> {
482        self.spacing.get(name).ok_or_else(|| {
483            TailwindError::theme(format!(
484                "Spacing '{}' not found in theme '{}'",
485                name, self.name
486            ))
487        })
488    }
489
490    /// Get border radius from the theme
491    pub fn get_border_radius(&self, name: &str) -> Result<&BorderRadius> {
492        self.border_radius.get(name).ok_or_else(|| {
493            TailwindError::theme(format!(
494                "Border radius '{}' not found in theme '{}'",
495                name, self.name
496            ))
497        })
498    }
499
500    /// Get box shadow from the theme
501    pub fn get_box_shadow(&self, name: &str) -> Result<&BoxShadow> {
502        self.box_shadows.get(name).ok_or_else(|| {
503            TailwindError::theme(format!(
504                "Box shadow '{}' not found in theme '{}'",
505                name, self.name
506            ))
507        })
508    }
509
510    /// Get custom value from the theme
511    pub fn get_custom(&self, name: &str) -> Result<&ThemeValue> {
512        self.custom.get(name).ok_or_else(|| {
513            TailwindError::theme(format!(
514                "Custom value '{}' not found in theme '{}'",
515                name, self.name
516            ))
517        })
518    }
519
520    /// Validate the theme
521    pub fn validate(&self) -> Result<()> {
522        if self.name.is_empty() {
523            return Err(TailwindError::theme("Theme name cannot be empty".to_string()));
524        }
525
526        // Validate colors
527        for (name, color) in &self.colors {
528            match color {
529                Color::Hex(hex) => {
530                    if !hex.starts_with('#') || hex.len() != 7 {
531                        return Err(TailwindError::theme(format!("Invalid hex color '{}' for '{}'", hex, name)));
532                    }
533                }
534                    Color::Rgb { r: _, g: _, b: _ } => {
535                        // RGB values are already validated as u8, so they're always valid
536                    }
537                    Color::Rgba { r: _, g: _, b: _, a } => {
538                        if *a < 0.0 || *a > 1.0 {
539                            return Err(TailwindError::theme(format!("Invalid RGBA alpha value for '{}'", name)));
540                        }
541                    }
542                Color::Hsl { h, s, l } => {
543                    if *h < 0.0 || *h > 360.0 || *s < 0.0 || *s > 100.0 || *l < 0.0 || *l > 100.0 {
544                        return Err(TailwindError::theme(format!("Invalid HSL values for '{}'", name)));
545                    }
546                }
547                Color::Hsla { h, s, l, a } => {
548                    if *h < 0.0 || *h > 360.0 || *s < 0.0 || *s > 100.0 || *l < 0.0 || *l > 100.0 || *a < 0.0 || *a > 1.0 {
549                        return Err(TailwindError::theme(format!("Invalid HSLA values for '{}'", name)));
550                    }
551                }
552                Color::Named(_) => {} // Named colors are always valid
553            }
554        }
555
556        Ok(())
557    }
558}
559
560/// TOML representation of theme configuration
561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
562pub struct ThemeToml {
563    pub name: String,
564    pub colors: Option<HashMap<String, String>>,
565    pub spacing: Option<HashMap<String, String>>,
566    pub border_radius: Option<HashMap<String, String>>,
567    pub box_shadows: Option<HashMap<String, String>>,
568    pub custom: Option<HashMap<String, toml::Value>>,
569}
570
571impl From<Theme> for ThemeToml {
572    fn from(theme: Theme) -> Self {
573        Self {
574            name: theme.name,
575            colors: Some(theme.colors.into_iter().map(|(k, v)| (k, v.to_css())).collect()),
576            spacing: Some(theme.spacing.into_iter().map(|(k, v)| (k, v.to_css())).collect()),
577            border_radius: Some(theme.border_radius.into_iter().map(|(k, v)| (k, v.to_css())).collect()),
578            box_shadows: Some(theme.box_shadows.into_iter().map(|(k, v)| (k, v.to_css())).collect()),
579            custom: Some(theme.custom.into_iter().map(|(k, v)| {
580                let toml_value = match v {
581                    ThemeValue::String(s) => toml::Value::String(s),
582                    ThemeValue::Number(n) => toml::Value::Float(n as f64),
583                    ThemeValue::Boolean(b) => toml::Value::Boolean(b),
584                    ThemeValue::Color(c) => toml::Value::String(c.to_css()),
585                    ThemeValue::Spacing(s) => toml::Value::String(s.to_css()),
586                    ThemeValue::BorderRadius(br) => toml::Value::String(br.to_css()),
587                    ThemeValue::BoxShadow(bs) => toml::Value::String(bs.to_css()),
588                };
589                (k, toml_value)
590            }).collect()),
591        }
592    }
593}
594
595impl From<ThemeToml> for Theme {
596    fn from(toml_theme: ThemeToml) -> Self {
597        let mut theme = Theme::new(toml_theme.name);
598        
599        if let Some(colors) = toml_theme.colors {
600            for (name, color_str) in colors {
601                if let Ok(color) = Color::from_str(&color_str) {
602                    theme.add_color(name, color);
603                }
604            }
605        }
606        
607        if let Some(spacing) = toml_theme.spacing {
608            for (name, spacing_str) in spacing {
609                if let Ok(spacing_value) = Spacing::from_str(&spacing_str) {
610                    theme.add_spacing(name, spacing_value);
611                }
612            }
613        }
614        
615        if let Some(border_radius) = toml_theme.border_radius {
616            for (name, radius_str) in border_radius {
617                if let Ok(radius_value) = BorderRadius::from_str(&radius_str) {
618                    theme.add_border_radius(name, radius_value);
619                }
620            }
621        }
622        
623        if let Some(box_shadows) = toml_theme.box_shadows {
624            for (name, shadow_str) in box_shadows {
625                if let Ok(shadow_value) = BoxShadow::from_str(&shadow_str) {
626                    theme.add_box_shadow(name, shadow_value);
627                }
628            }
629        }
630        
631        theme
632    }
633}
634
635/// Create a default theme with common values
636pub fn create_default_theme() -> Theme {
637    let mut theme = Theme::new("default");
638
639    // Add default colors
640    theme.add_color("primary", Color::hex("#3b82f6"));
641    theme.add_color("secondary", Color::hex("#64748b"));
642    theme.add_color("success", Color::hex("#10b981"));
643    theme.add_color("warning", Color::hex("#f59e0b"));
644    theme.add_color("error", Color::hex("#ef4444"));
645    theme.add_color("white", Color::hex("#ffffff"));
646    theme.add_color("black", Color::hex("#000000"));
647    theme.add_color("gray-100", Color::hex("#f3f4f6"));
648    theme.add_color("gray-500", Color::hex("#6b7280"));
649    theme.add_color("gray-900", Color::hex("#111827"));
650
651    // Add default spacing
652    theme.add_spacing("xs", Spacing::rem(0.25));
653    theme.add_spacing("sm", Spacing::rem(0.5));
654    theme.add_spacing("md", Spacing::rem(1.0));
655    theme.add_spacing("lg", Spacing::rem(1.5));
656    theme.add_spacing("xl", Spacing::rem(2.0));
657    theme.add_spacing("2xl", Spacing::rem(3.0));
658
659    // Add default border radius
660    theme.add_border_radius("sm", BorderRadius::rem(0.125));
661    theme.add_border_radius("md", BorderRadius::rem(0.375));
662    theme.add_border_radius("lg", BorderRadius::rem(0.5));
663    theme.add_border_radius("xl", BorderRadius::rem(0.75));
664    theme.add_border_radius("full", BorderRadius::percent(50.0));
665
666    // Add default box shadows
667    theme.add_box_shadow(
668        "sm",
669        BoxShadow::new(0.0, 1.0, 2.0, 0.0, Color::hex("#000000"), false),
670    );
671    theme.add_box_shadow(
672        "md",
673        BoxShadow::new(0.0, 4.0, 6.0, -1.0, Color::hex("#000000"), false),
674    );
675    theme.add_box_shadow(
676        "lg",
677        BoxShadow::new(0.0, 10.0, 15.0, -3.0, Color::hex("#000000"), false),
678    );
679
680    theme
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn test_color_creation() {
689        let hex_color = Color::hex("#3b82f6");
690        assert_eq!(hex_color, Color::Hex("#3b82f6".to_string()));
691
692        let rgb_color = Color::rgb(59, 130, 246);
693        assert_eq!(
694            rgb_color,
695            Color::Rgb {
696                r: 59,
697                g: 130,
698                b: 246
699            }
700        );
701
702        let named_color = Color::named("primary");
703        assert_eq!(named_color, Color::Named("primary".to_string()));
704    }
705
706    #[test]
707    fn test_color_to_css() {
708        let hex_color = Color::hex("#3b82f6");
709        assert_eq!(hex_color.to_css(), "#3b82f6");
710
711        let rgb_color = Color::rgb(59, 130, 246);
712        assert_eq!(rgb_color.to_css(), "rgb(59, 130, 246)");
713
714        let named_color = Color::named("primary");
715        assert_eq!(named_color.to_css(), "var(--color-primary)");
716    }
717
718    #[test]
719    fn test_spacing_creation() {
720        let px_spacing = Spacing::px(16.0);
721        assert_eq!(px_spacing, Spacing::Px(16.0));
722
723        let rem_spacing = Spacing::rem(1.0);
724        assert_eq!(rem_spacing, Spacing::Rem(1.0));
725
726        let named_spacing = Spacing::named("md");
727        assert_eq!(named_spacing, Spacing::Named("md".to_string()));
728    }
729
730    #[test]
731    fn test_spacing_to_css() {
732        let px_spacing = Spacing::px(16.0);
733        assert_eq!(px_spacing.to_css(), "16px");
734
735        let rem_spacing = Spacing::rem(1.0);
736        assert_eq!(rem_spacing.to_css(), "1rem");
737
738        let named_spacing = Spacing::named("md");
739        assert_eq!(named_spacing.to_css(), "var(--spacing-md)");
740    }
741
742    #[test]
743    fn test_theme_creation() {
744        let mut theme = Theme::new("test");
745        assert_eq!(theme.name, "test");
746
747        theme.add_color("primary", Color::hex("#3b82f6"));
748        assert!(theme.colors.contains_key("primary"));
749
750        let color = theme.get_color("primary").unwrap();
751        assert_eq!(color, &Color::hex("#3b82f6"));
752    }
753
754    #[test]
755    fn test_theme_error_handling() {
756        let theme = Theme::new("test");
757        let result = theme.get_color("nonexistent");
758        assert!(result.is_err());
759
760        if let Err(TailwindError::Theme { message }) = result {
761            assert!(message.contains("Color 'nonexistent' not found"));
762        }
763    }
764
765    #[test]
766    fn test_default_theme() {
767        let theme = create_default_theme();
768        assert_eq!(theme.name, "default");
769
770        // Test that default colors exist
771        assert!(theme.get_color("primary").is_ok());
772        assert!(theme.get_color("secondary").is_ok());
773        assert!(theme.get_color("success").is_ok());
774
775        // Test that default spacing exists
776        assert!(theme.get_spacing("sm").is_ok());
777        assert!(theme.get_spacing("md").is_ok());
778        assert!(theme.get_spacing("lg").is_ok());
779
780        // Test that default border radius exists
781        assert!(theme.get_border_radius("sm").is_ok());
782        assert!(theme.get_border_radius("md").is_ok());
783        assert!(theme.get_border_radius("lg").is_ok());
784    }
785}