1use crate::error::{Result, TailwindError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum Color {
11 Hex(String),
13 Rgb { r: u8, g: u8, b: u8 },
15 Rgba { r: u8, g: u8, b: u8, a: f32 },
17 Hsl { h: f32, s: f32, l: f32 },
19 Hsla { h: f32, s: f32, l: f32, a: f32 },
21 Named(String),
23}
24
25impl Color {
26 pub fn hex(value: impl Into<String>) -> Self {
28 Self::Hex(value.into())
29 }
30
31 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
33 Self::Rgb { r, g, b }
34 }
35
36 pub fn rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
38 Self::Rgba { r, g, b, a }
39 }
40
41 pub fn hsl(h: f32, s: f32, l: f32) -> Self {
43 Self::Hsl { h, s, l }
44 }
45
46 pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
48 Self::Hsla { h, s, l, a }
49 }
50
51 pub fn named(name: impl Into<String>) -> Self {
53 Self::Named(name.into())
54 }
55
56 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 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 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 Ok(Color::named(s))
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub enum Spacing {
138 Px(f32),
140 Rem(f32),
142 Em(f32),
144 Percent(f32),
146 Vw(f32),
148 Vh(f32),
150 Named(String),
152}
153
154impl Spacing {
155 pub fn px(value: f32) -> Self {
157 Self::Px(value)
158 }
159
160 pub fn rem(value: f32) -> Self {
162 Self::Rem(value)
163 }
164
165 pub fn em(value: f32) -> Self {
167 Self::Em(value)
168 }
169
170 pub fn percent(value: f32) -> Self {
172 Self::Percent(value)
173 }
174
175 pub fn vw(value: f32) -> Self {
177 Self::Vw(value)
178 }
179
180 pub fn vh(value: f32) -> Self {
182 Self::Vh(value)
183 }
184
185 pub fn named(name: impl Into<String>) -> Self {
187 Self::Named(name.into())
188 }
189
190 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 let value = s
255 .parse::<f32>()
256 .map_err(|_| TailwindError::theme("Invalid spacing value"))?;
257 Ok(Spacing::rem(value))
258 }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
264pub enum BorderRadius {
265 Px(f32),
267 Rem(f32),
269 Percent(f32),
271 Named(String),
273}
274
275impl BorderRadius {
276 pub fn px(value: f32) -> Self {
278 Self::Px(value)
279 }
280
281 pub fn rem(value: f32) -> Self {
283 Self::Rem(value)
284 }
285
286 pub fn percent(value: f32) -> Self {
288 Self::Percent(value)
289 }
290
291 pub fn named(name: impl Into<String>) -> Self {
293 Self::Named(name.into())
294 }
295
296 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 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#[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 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 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 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#[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#[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 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 pub fn add_color(&mut self, name: impl Into<String>, color: Color) {
480 self.colors.insert(name.into(), color);
481 }
482
483 pub fn add_spacing(&mut self, name: impl Into<String>, spacing: Spacing) {
485 self.spacing.insert(name.into(), spacing);
486 }
487
488 pub fn add_border_radius(&mut self, name: impl Into<String>, radius: BorderRadius) {
490 self.border_radius.insert(name.into(), radius);
491 }
492
493 pub fn add_box_shadow(&mut self, name: impl Into<String>, shadow: BoxShadow) {
495 self.box_shadows.insert(name.into(), shadow);
496 }
497
498 pub fn add_custom(&mut self, name: impl Into<String>, value: ThemeValue) {
500 self.custom.insert(name.into(), value);
501 }
502
503 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 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 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 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 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 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 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 }
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(_) => {} }
614 }
615
616 Ok(())
617 }
618}
619
620#[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
725pub fn create_default_theme() -> Theme {
727 let mut theme = Theme::new("default");
728
729 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 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 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 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 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 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 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}