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.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 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 Ok(Color::named(s))
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum Spacing {
129 Px(f32),
131 Rem(f32),
133 Em(f32),
135 Percent(f32),
137 Vw(f32),
139 Vh(f32),
141 Named(String),
143}
144
145impl Spacing {
146 pub fn px(value: f32) -> Self {
148 Self::Px(value)
149 }
150
151 pub fn rem(value: f32) -> Self {
153 Self::Rem(value)
154 }
155
156 pub fn em(value: f32) -> Self {
158 Self::Em(value)
159 }
160
161 pub fn percent(value: f32) -> Self {
163 Self::Percent(value)
164 }
165
166 pub fn vw(value: f32) -> Self {
168 Self::Vw(value)
169 }
170
171 pub fn vh(value: f32) -> Self {
173 Self::Vh(value)
174 }
175
176 pub fn named(name: impl Into<String>) -> Self {
178 Self::Named(name.into())
179 }
180
181 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 let value = s.parse::<f32>()
240 .map_err(|_| TailwindError::theme("Invalid spacing value"))?;
241 Ok(Spacing::rem(value))
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248pub enum BorderRadius {
249 Px(f32),
251 Rem(f32),
253 Percent(f32),
255 Named(String),
257}
258
259impl BorderRadius {
260 pub fn px(value: f32) -> Self {
262 Self::Px(value)
263 }
264
265 pub fn rem(value: f32) -> Self {
267 Self::Rem(value)
268 }
269
270 pub fn percent(value: f32) -> Self {
272 Self::Percent(value)
273 }
274
275 pub fn named(name: impl Into<String>) -> Self {
277 Self::Named(name.into())
278 }
279
280 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 let value = s.parse::<f32>()
318 .map_err(|_| TailwindError::theme("Invalid border radius value"))?;
319 Ok(BorderRadius::rem(value))
320 }
321 }
322}
323
324#[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 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 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 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#[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#[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 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 pub fn add_color(&mut self, name: impl Into<String>, color: Color) {
447 self.colors.insert(name.into(), color);
448 }
449
450 pub fn add_spacing(&mut self, name: impl Into<String>, spacing: Spacing) {
452 self.spacing.insert(name.into(), spacing);
453 }
454
455 pub fn add_border_radius(&mut self, name: impl Into<String>, radius: BorderRadius) {
457 self.border_radius.insert(name.into(), radius);
458 }
459
460 pub fn add_box_shadow(&mut self, name: impl Into<String>, shadow: BoxShadow) {
462 self.box_shadows.insert(name.into(), shadow);
463 }
464
465 pub fn add_custom(&mut self, name: impl Into<String>, value: ThemeValue) {
467 self.custom.insert(name.into(), value);
468 }
469
470 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 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 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 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 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 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 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 }
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(_) => {} }
554 }
555
556 Ok(())
557 }
558}
559
560#[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
635pub fn create_default_theme() -> Theme {
637 let mut theme = Theme::new("default");
638
639 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 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 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 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 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 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 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}