1use crate::color::Color;
4use std::collections::HashMap;
5
6#[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 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#[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
47pub 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 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 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 pub fn get(&self, size: SpacingSize) -> &str {
89 self.values.get(&size).map(|s| s.as_str()).unwrap_or("0rem")
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95pub enum FontFamily {
96 Sans,
97 Serif,
98 Mono,
99 Custom(String),
100}
101
102impl FontFamily {
103 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
114pub struct FontSizeScale {
116 pub xs: String, pub sm: String, pub base: String, pub lg: String, pub xl: String, pub xxl: String, pub xxxl: String, pub xxxxl: String, }
125
126impl Default for FontSizeScale {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl FontSizeScale {
133 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
148pub struct FontWeightScale {
150 pub thin: String, pub extralight: String, pub light: String, pub normal: String, pub medium: String, pub semibold: String, pub bold: String, pub extrabold: String, pub black: String, }
160
161impl Default for FontWeightScale {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl FontWeightScale {
168 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
184pub struct LineHeightScale {
186 pub none: String, pub tight: String, pub snug: String, pub normal: String, pub relaxed: String, pub loose: String, }
193
194impl Default for LineHeightScale {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl LineHeightScale {
201 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
214pub struct LetterSpacingScale {
216 pub tighter: String, pub tight: String, pub normal: String, pub wide: String, pub wider: String, pub widest: String, }
223
224impl Default for LetterSpacingScale {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl LetterSpacingScale {
231 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
244pub 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 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 pub fn font_family(self, family: FontFamily) -> Self {
273 Self {
274 font_family: family,
275 ..self
276 }
277 }
278}
279
280pub 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 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
312pub 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 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
342pub 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 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
370pub 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 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, 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 pub fn primary_color(self, color: Color) -> Self {
419 Self {
420 primary_color: color,
421 ..self
422 }
423 }
424
425 pub fn secondary_color(self, color: Color) -> Self {
427 Self {
428 secondary_color: color,
429 ..self
430 }
431 }
432
433 pub fn accent_color(self, color: Color) -> Self {
435 Self {
436 accent_color: color,
437 ..self
438 }
439 }
440
441 pub fn background_color(self, color: Color) -> Self {
443 Self {
444 background_color: color,
445 ..self
446 }
447 }
448
449 pub fn text_color(self, color: Color) -> Self {
451 Self {
452 text_color: color,
453 ..self
454 }
455 }
456
457 pub fn apply_to_component(&self, component: &dyn ThemedComponent) -> String {
459 component.apply_theme(self)
460 }
461}
462
463pub trait ThemedComponent {
465 fn base_classes(&self) -> &str;
467
468 fn apply_theme(&self, theme: &Theme) -> String;
470
471 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#[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 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) .text_color(Color::Gray),
501 ThemePreset::Dark => Theme::new()
502 .primary_color(Color::Blue)
503 .secondary_color(Color::Gray)
504 .background_color(Color::Gray) .text_color(Color::Gray), 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); 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); assert_eq!(theme.text_color, Color::Gray); }
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 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}