tailwind_rs_core/
theme_new.rs

1//! New theme system implementation according to API documentation
2
3use crate::color::Color;
4use std::collections::HashMap;
5
6/// Theme variant for different component styles
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ThemeVariant {
9    Primary,
10    Secondary,
11    Danger,
12    Success,
13    Warning,
14    Info,
15    Light,
16    Dark,
17}
18
19impl ThemeVariant {
20    /// Returns the associated color for the variant
21    pub fn color(&self) -> Color {
22        match self {
23            ThemeVariant::Primary => Color::Blue,
24            ThemeVariant::Secondary => Color::Gray,
25            ThemeVariant::Danger => Color::Red,
26            ThemeVariant::Success => Color::Green,
27            ThemeVariant::Warning => Color::Yellow,
28            ThemeVariant::Info => Color::Blue,
29            ThemeVariant::Light => Color::Gray,
30            ThemeVariant::Dark => Color::Gray,
31        }
32    }
33}
34
35/// Spacing size enum
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum SpacingSize {
38    Xs,
39    Sm,
40    Md,
41    Lg,
42    Xl,
43    Xxl,
44    Xxxl,
45}
46
47/// Spacing scale for consistent spacing values
48pub struct SpacingScale {
49    values: HashMap<SpacingSize, String>,
50}
51
52impl Default for SpacingScale {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl SpacingScale {
59    /// Creates a new spacing scale with default values
60    pub fn new() -> Self {
61        let mut values = HashMap::new();
62        values.insert(SpacingSize::Xs, "0.125rem".to_string());
63        values.insert(SpacingSize::Sm, "0.25rem".to_string());
64        values.insert(SpacingSize::Md, "1rem".to_string());
65        values.insert(SpacingSize::Lg, "1.5rem".to_string());
66        values.insert(SpacingSize::Xl, "2rem".to_string());
67        values.insert(SpacingSize::Xxl, "4rem".to_string());
68        values.insert(SpacingSize::Xxxl, "8rem".to_string());
69
70        Self { values }
71    }
72
73    /// Creates a custom spacing scale
74    pub fn custom(xs: &str, sm: &str, md: &str, lg: &str, xl: &str, xxl: &str, xxxl: &str) -> Self {
75        let mut values = HashMap::new();
76        values.insert(SpacingSize::Xs, xs.to_string());
77        values.insert(SpacingSize::Sm, sm.to_string());
78        values.insert(SpacingSize::Md, md.to_string());
79        values.insert(SpacingSize::Lg, lg.to_string());
80        values.insert(SpacingSize::Xl, xl.to_string());
81        values.insert(SpacingSize::Xxl, xxl.to_string());
82        values.insert(SpacingSize::Xxxl, xxxl.to_string());
83
84        Self { values }
85    }
86
87    /// Gets spacing value for a specific size
88    pub fn get(&self, size: SpacingSize) -> &str {
89        self.values.get(&size).map(|s| s.as_str()).unwrap_or("0rem")
90    }
91}
92
93/// Font family enum
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95pub enum FontFamily {
96    Sans,
97    Serif,
98    Mono,
99    Custom(String),
100}
101
102impl FontFamily {
103    /// Returns the CSS class for the font family
104    pub fn class(&self) -> &str {
105        match self {
106            FontFamily::Sans => "font-sans",
107            FontFamily::Serif => "font-serif",
108            FontFamily::Mono => "font-mono",
109            FontFamily::Custom(name) => name,
110        }
111    }
112}
113
114/// Font size scale
115pub struct FontSizeScale {
116    pub xs: String,    // 0.75rem
117    pub sm: String,    // 0.875rem
118    pub base: String,  // 1rem
119    pub lg: String,    // 1.125rem
120    pub xl: String,    // 1.25rem
121    pub xxl: String,   // 1.5rem
122    pub xxxl: String,  // 1.875rem
123    pub xxxxl: String, // 2.25rem
124}
125
126impl Default for FontSizeScale {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl FontSizeScale {
133    /// Creates a new font size scale with default values
134    pub fn new() -> Self {
135        Self {
136            xs: "0.75rem".to_string(),
137            sm: "0.875rem".to_string(),
138            base: "1rem".to_string(),
139            lg: "1.125rem".to_string(),
140            xl: "1.25rem".to_string(),
141            xxl: "1.5rem".to_string(),
142            xxxl: "1.875rem".to_string(),
143            xxxxl: "2.25rem".to_string(),
144        }
145    }
146}
147
148/// Font weight scale
149pub struct FontWeightScale {
150    pub thin: String,       // 100
151    pub extralight: String, // 200
152    pub light: String,      // 300
153    pub normal: String,     // 400
154    pub medium: String,     // 500
155    pub semibold: String,   // 600
156    pub bold: String,       // 700
157    pub extrabold: String,  // 800
158    pub black: String,      // 900
159}
160
161impl Default for FontWeightScale {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl FontWeightScale {
168    /// Creates a new font weight scale with default values
169    pub fn new() -> Self {
170        Self {
171            thin: "100".to_string(),
172            extralight: "200".to_string(),
173            light: "300".to_string(),
174            normal: "400".to_string(),
175            medium: "500".to_string(),
176            semibold: "600".to_string(),
177            bold: "700".to_string(),
178            extrabold: "800".to_string(),
179            black: "900".to_string(),
180        }
181    }
182}
183
184/// Line height scale
185pub struct LineHeightScale {
186    pub none: String,    // 1
187    pub tight: String,   // 1.25
188    pub snug: String,    // 1.375
189    pub normal: String,  // 1.5
190    pub relaxed: String, // 1.625
191    pub loose: String,   // 2
192}
193
194impl Default for LineHeightScale {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl LineHeightScale {
201    /// Creates a new line height scale with default values
202    pub fn new() -> Self {
203        Self {
204            none: "1".to_string(),
205            tight: "1.25".to_string(),
206            snug: "1.375".to_string(),
207            normal: "1.5".to_string(),
208            relaxed: "1.625".to_string(),
209            loose: "2".to_string(),
210        }
211    }
212}
213
214/// Letter spacing scale
215pub struct LetterSpacingScale {
216    pub tighter: String, // -0.05em
217    pub tight: String,   // -0.025em
218    pub normal: String,  // 0em
219    pub wide: String,    // 0.025em
220    pub wider: String,   // 0.05em
221    pub widest: String,  // 0.1em
222}
223
224impl Default for LetterSpacingScale {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230impl LetterSpacingScale {
231    /// Creates a new letter spacing scale with default values
232    pub fn new() -> Self {
233        Self {
234            tighter: "-0.05em".to_string(),
235            tight: "-0.025em".to_string(),
236            normal: "0em".to_string(),
237            wide: "0.025em".to_string(),
238            wider: "0.05em".to_string(),
239            widest: "0.1em".to_string(),
240        }
241    }
242}
243
244/// Typography scale for the theme
245pub struct TypographyScale {
246    pub font_family: FontFamily,
247    pub font_sizes: FontSizeScale,
248    pub font_weights: FontWeightScale,
249    pub line_heights: LineHeightScale,
250    pub letter_spacing: LetterSpacingScale,
251}
252
253impl Default for TypographyScale {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259impl TypographyScale {
260    /// Creates a new typography scale with default values
261    pub fn new() -> Self {
262        Self {
263            font_family: FontFamily::Sans,
264            font_sizes: FontSizeScale::new(),
265            font_weights: FontWeightScale::new(),
266            line_heights: LineHeightScale::new(),
267            letter_spacing: LetterSpacingScale::new(),
268        }
269    }
270
271    /// Sets the font family for the typography scale
272    pub fn font_family(self, family: FontFamily) -> Self {
273        Self {
274            font_family: family,
275            ..self
276        }
277    }
278}
279
280/// Shadow scale
281pub struct ShadowScale {
282    pub sm: String,
283    pub base: String,
284    pub md: String,
285    pub lg: String,
286    pub xl: String,
287    pub xxl: String,
288    pub inner: String,
289}
290
291impl Default for ShadowScale {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl ShadowScale {
298    /// Creates a new shadow scale with default values
299    pub fn new() -> Self {
300        Self {
301            sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)".to_string(),
302            base: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)".to_string(),
303            md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)".to_string(),
304            lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)".to_string(),
305            xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)".to_string(),
306            xxl: "0 25px 50px -12px rgb(0 0 0 / 0.25)".to_string(),
307            inner: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)".to_string(),
308        }
309    }
310}
311
312/// Border scale
313pub struct BorderScale {
314    pub none: String,
315    pub sm: String,
316    pub base: String,
317    pub md: String,
318    pub lg: String,
319    pub xl: String,
320}
321
322impl Default for BorderScale {
323    fn default() -> Self {
324        Self::new()
325    }
326}
327
328impl BorderScale {
329    /// Creates a new border scale with default values
330    pub fn new() -> Self {
331        Self {
332            none: "0px".to_string(),
333            sm: "1px".to_string(),
334            base: "2px".to_string(),
335            md: "4px".to_string(),
336            lg: "8px".to_string(),
337            xl: "16px".to_string(),
338        }
339    }
340}
341
342/// Animation scale
343pub struct AnimationScale {
344    pub none: String,
345    pub spin: String,
346    pub ping: String,
347    pub pulse: String,
348    pub bounce: String,
349}
350
351impl Default for AnimationScale {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357impl AnimationScale {
358    /// Creates a new animation scale with default values
359    pub fn new() -> Self {
360        Self {
361            none: "none".to_string(),
362            spin: "spin 1s linear infinite".to_string(),
363            ping: "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite".to_string(),
364            pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite".to_string(),
365            bounce: "bounce 1s infinite".to_string(),
366        }
367    }
368}
369
370/// Main theme structure according to API documentation
371pub struct Theme {
372    pub primary_color: Color,
373    pub secondary_color: Color,
374    pub accent_color: Color,
375    pub background_color: Color,
376    pub text_color: Color,
377    pub border_color: Color,
378    pub success_color: Color,
379    pub warning_color: Color,
380    pub error_color: Color,
381    pub info_color: Color,
382    pub spacing: SpacingScale,
383    pub typography: TypographyScale,
384    pub shadows: ShadowScale,
385    pub borders: BorderScale,
386    pub animations: AnimationScale,
387}
388
389impl Default for Theme {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395impl Theme {
396    /// Creates a new theme with default values
397    pub fn new() -> Self {
398        Self {
399            primary_color: Color::Blue,
400            secondary_color: Color::Gray,
401            accent_color: Color::Blue,
402            background_color: Color::Gray, // Using Gray as placeholder for White
403            text_color: Color::Gray,
404            border_color: Color::Gray,
405            success_color: Color::Green,
406            warning_color: Color::Yellow,
407            error_color: Color::Red,
408            info_color: Color::Blue,
409            spacing: SpacingScale::new(),
410            typography: TypographyScale::new(),
411            shadows: ShadowScale::new(),
412            borders: BorderScale::new(),
413            animations: AnimationScale::new(),
414        }
415    }
416
417    /// Sets the primary color for the theme
418    pub fn primary_color(self, color: Color) -> Self {
419        Self {
420            primary_color: color,
421            ..self
422        }
423    }
424
425    /// Sets the secondary color for the theme
426    pub fn secondary_color(self, color: Color) -> Self {
427        Self {
428            secondary_color: color,
429            ..self
430        }
431    }
432
433    /// Sets the accent color for the theme
434    pub fn accent_color(self, color: Color) -> Self {
435        Self {
436            accent_color: color,
437            ..self
438        }
439    }
440
441    /// Sets the background color for the theme
442    pub fn background_color(self, color: Color) -> Self {
443        Self {
444            background_color: color,
445            ..self
446        }
447    }
448
449    /// Sets the text color for the theme
450    pub fn text_color(self, color: Color) -> Self {
451        Self {
452            text_color: color,
453            ..self
454        }
455    }
456
457    /// Applies theme to a component
458    pub fn apply_to_component(&self, component: &dyn ThemedComponent) -> String {
459        component.apply_theme(self)
460    }
461}
462
463/// Trait for components that support theming
464pub trait ThemedComponent {
465    /// Returns the base classes for the component
466    fn base_classes(&self) -> &str;
467
468    /// Applies the theme to the component
469    fn apply_theme(&self, theme: &Theme) -> String;
470
471    /// Returns available theme variants for the component
472    fn theme_variants(&self) -> Vec<ThemeVariant> {
473        vec![
474            ThemeVariant::Primary,
475            ThemeVariant::Secondary,
476            ThemeVariant::Danger,
477            ThemeVariant::Success,
478        ]
479    }
480}
481
482/// Theme preset enum for predefined themes
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
484pub enum ThemePreset {
485    Light,
486    Dark,
487    Professional,
488    Minimal,
489    Vibrant,
490}
491
492impl ThemePreset {
493    /// Creates a theme from the preset
494    pub fn create(&self) -> Theme {
495        match self {
496            ThemePreset::Light => Theme::new()
497                .primary_color(Color::Blue)
498                .secondary_color(Color::Gray)
499                .background_color(Color::Gray) // Using Gray as placeholder for White
500                .text_color(Color::Gray),
501            ThemePreset::Dark => Theme::new()
502                .primary_color(Color::Blue)
503                .secondary_color(Color::Gray)
504                .background_color(Color::Gray) // Using Gray as placeholder for Black
505                .text_color(Color::Gray), // Using Gray as placeholder for White
506            ThemePreset::Professional => Theme::new()
507                .primary_color(Color::Blue)
508                .secondary_color(Color::Gray)
509                .accent_color(Color::Blue),
510            ThemePreset::Minimal => Theme::new()
511                .primary_color(Color::Gray)
512                .secondary_color(Color::Gray)
513                .accent_color(Color::Gray),
514            ThemePreset::Vibrant => Theme::new()
515                .primary_color(Color::Blue)
516                .secondary_color(Color::Green)
517                .accent_color(Color::Yellow),
518        }
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn test_theme_creation() {
528        let theme = Theme::new();
529        assert_eq!(theme.primary_color, Color::Blue);
530        assert_eq!(theme.secondary_color, Color::Gray);
531        assert_eq!(theme.accent_color, Color::Blue);
532    }
533
534    #[test]
535    fn test_theme_primary_color() {
536        let theme = Theme::new().primary_color(Color::Green);
537        assert_eq!(theme.primary_color, Color::Green);
538    }
539
540    #[test]
541    fn test_theme_secondary_color() {
542        let theme = Theme::new().secondary_color(Color::Purple);
543        assert_eq!(theme.secondary_color, Color::Purple);
544    }
545
546    #[test]
547    fn test_theme_accent_color() {
548        let theme = Theme::new().accent_color(Color::Orange);
549        assert_eq!(theme.accent_color, Color::Orange);
550    }
551
552    #[test]
553    fn test_theme_variant_color() {
554        assert_eq!(ThemeVariant::Primary.color(), Color::Blue);
555        assert_eq!(ThemeVariant::Secondary.color(), Color::Gray);
556        assert_eq!(ThemeVariant::Danger.color(), Color::Red);
557        assert_eq!(ThemeVariant::Success.color(), Color::Green);
558    }
559
560    #[test]
561    fn test_spacing_scale_new() {
562        let spacing = SpacingScale::new();
563        assert_eq!(spacing.get(SpacingSize::Xs), "0.125rem");
564        assert_eq!(spacing.get(SpacingSize::Sm), "0.25rem");
565        assert_eq!(spacing.get(SpacingSize::Md), "1rem");
566        assert_eq!(spacing.get(SpacingSize::Lg), "1.5rem");
567    }
568
569    #[test]
570    fn test_spacing_scale_custom() {
571        let spacing =
572            SpacingScale::custom("0.1rem", "0.2rem", "0.5rem", "1rem", "2rem", "4rem", "8rem");
573        assert_eq!(spacing.get(SpacingSize::Xs), "0.1rem");
574        assert_eq!(spacing.get(SpacingSize::Sm), "0.2rem");
575        assert_eq!(spacing.get(SpacingSize::Md), "0.5rem");
576    }
577
578    #[test]
579    fn test_font_family_class() {
580        assert_eq!(FontFamily::Sans.class(), "font-sans");
581        assert_eq!(FontFamily::Serif.class(), "font-serif");
582        assert_eq!(FontFamily::Mono.class(), "font-mono");
583        assert_eq!(
584            FontFamily::Custom("custom-font".to_string()).class(),
585            "custom-font"
586        );
587    }
588
589    #[test]
590    fn test_typography_scale_new() {
591        let typography = TypographyScale::new();
592        assert_eq!(typography.font_family, FontFamily::Sans);
593        assert_eq!(typography.font_sizes.xs, "0.75rem");
594        assert_eq!(typography.font_sizes.base, "1rem");
595    }
596
597    #[test]
598    fn test_typography_scale_font_family() {
599        let typography = TypographyScale::new().font_family(FontFamily::Serif);
600        assert_eq!(typography.font_family, FontFamily::Serif);
601    }
602
603    #[test]
604    fn test_theme_preset_light() {
605        let theme = ThemePreset::Light.create();
606        assert_eq!(theme.primary_color, Color::Blue);
607        assert_eq!(theme.background_color, Color::Gray); // Using Gray as placeholder for White
608        assert_eq!(theme.text_color, Color::Gray);
609    }
610
611    #[test]
612    fn test_theme_preset_dark() {
613        let theme = ThemePreset::Dark.create();
614        assert_eq!(theme.primary_color, Color::Blue);
615        assert_eq!(theme.background_color, Color::Gray); // Using Gray as placeholder for Black
616        assert_eq!(theme.text_color, Color::Gray); // Using Gray as placeholder for White
617    }
618
619    #[test]
620    fn test_theme_preset_professional() {
621        let theme = ThemePreset::Professional.create();
622        assert_eq!(theme.primary_color, Color::Blue);
623        assert_eq!(theme.secondary_color, Color::Gray);
624        assert_eq!(theme.accent_color, Color::Blue);
625    }
626
627    #[test]
628    fn test_theme_preset_minimal() {
629        let theme = ThemePreset::Minimal.create();
630        assert_eq!(theme.primary_color, Color::Gray);
631        assert_eq!(theme.secondary_color, Color::Gray);
632        assert_eq!(theme.accent_color, Color::Gray);
633    }
634
635    #[test]
636    fn test_theme_preset_vibrant() {
637        let theme = ThemePreset::Vibrant.create();
638        assert_eq!(theme.primary_color, Color::Blue);
639        assert_eq!(theme.secondary_color, Color::Green);
640        assert_eq!(theme.accent_color, Color::Yellow);
641    }
642
643    // Mock component for testing ThemedComponent trait
644    struct MockButton {
645        variant: ThemeVariant,
646    }
647
648    impl MockButton {
649        fn new(variant: ThemeVariant) -> Self {
650            Self { variant }
651        }
652    }
653
654    impl ThemedComponent for MockButton {
655        fn base_classes(&self) -> &str {
656            "px-4 py-2 rounded"
657        }
658
659        fn apply_theme(&self, theme: &Theme) -> String {
660            match self.variant {
661                ThemeVariant::Primary => {
662                    format!(
663                        "{} bg-{} text-white",
664                        self.base_classes(),
665                        theme.primary_color.name().to_lowercase()
666                    )
667                }
668                ThemeVariant::Secondary => {
669                    format!(
670                        "{} bg-{} text-{}",
671                        self.base_classes(),
672                        theme.secondary_color.name().to_lowercase(),
673                        theme.secondary_color.name().to_lowercase()
674                    )
675                }
676                _ => self.base_classes().to_string(),
677            }
678        }
679    }
680
681    #[test]
682    fn test_themed_component_primary() {
683        let theme = Theme::new().primary_color(Color::Blue);
684        let button = MockButton::new(ThemeVariant::Primary);
685        let classes = theme.apply_to_component(&button);
686        assert!(classes.contains("px-4 py-2 rounded"));
687        assert!(classes.contains("bg-blue"));
688        assert!(classes.contains("text-white"));
689    }
690
691    #[test]
692    fn test_themed_component_secondary() {
693        let theme = Theme::new().secondary_color(Color::Gray);
694        let button = MockButton::new(ThemeVariant::Secondary);
695        let classes = theme.apply_to_component(&button);
696        assert!(classes.contains("px-4 py-2 rounded"));
697        assert!(classes.contains("bg-gray"));
698        assert!(classes.contains("text-gray"));
699    }
700
701    #[test]
702    fn test_themed_component_variants() {
703        let button = MockButton::new(ThemeVariant::Primary);
704        let variants = button.theme_variants();
705        assert!(variants.contains(&ThemeVariant::Primary));
706        assert!(variants.contains(&ThemeVariant::Secondary));
707        assert!(variants.contains(&ThemeVariant::Danger));
708        assert!(variants.contains(&ThemeVariant::Success));
709    }
710}