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