1use dioxus::prelude::*;
2use std::collections::HashMap;
3
4#[cfg(feature = "alert")]
5use crate::alert::AlertVariant;
6#[cfg(feature = "badge")]
7use crate::badge::BadgeVariant;
8#[cfg(feature = "button")]
9use crate::button::ButtonVariant;
10#[cfg(feature = "card")]
11use crate::card::CardShadow;
12
13#[derive(Clone, PartialEq, Debug, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum PrimaryColor {
17 #[default]
18 Violet,
19 Obsidian,
20 Crimson,
21 Cyan,
22 Emerald,
23 Custom {
24 name: String,
25 hex: String,
26 css_prefix: Option<String>,
27 },
28}
29
30impl PrimaryColor {
31 pub fn name(&self) -> &str {
32 match self {
33 PrimaryColor::Violet => "Violet",
34 PrimaryColor::Obsidian => "Obsidian",
35 PrimaryColor::Crimson => "Crimson",
36 PrimaryColor::Cyan => "Cyan",
37 PrimaryColor::Emerald => "Emerald",
38 PrimaryColor::Custom { name, .. } => name.as_str(),
39 }
40 }
41
42 pub fn hex(&self) -> &str {
43 match self {
44 PrimaryColor::Violet => "#3b0764",
45 PrimaryColor::Obsidian => "#ffffff",
46 PrimaryColor::Crimson => "#ff1744",
47 PrimaryColor::Cyan => "#00f0ff",
48 PrimaryColor::Emerald => "#00ff9d",
49 PrimaryColor::Custom { hex, .. } => hex.as_str(),
50 }
51 }
52
53 pub fn css_prefix(&self) -> String {
55 match self {
56 PrimaryColor::Violet => "violet".to_string(),
57 PrimaryColor::Obsidian => "obsidian".to_string(),
58 PrimaryColor::Crimson => "crimson".to_string(),
59 PrimaryColor::Cyan => "cyan".to_string(),
60 PrimaryColor::Emerald => "emerald".to_string(),
61 PrimaryColor::Custom { name, css_prefix, .. } => {
62 if let Some(prefix) = css_prefix {
63 prefix.clone()
64 } else {
65 name.to_lowercase()
66 .chars()
67 .map(|c| if c.is_alphanumeric() { c } else { '-' })
68 .collect::<String>()
69 .trim_matches('-')
70 .to_string()
71 }
72 }
73 }
74 }
75
76 pub fn custom(name: impl Into<String>, hex: impl Into<String>) -> Self {
77 Self::Custom {
78 name: name.into(),
79 hex: hex.into(),
80 css_prefix: None,
81 }
82 }
83
84 pub fn custom_with_prefix(
85 name: impl Into<String>,
86 hex: impl Into<String>,
87 css_prefix: impl Into<String>,
88 ) -> Self {
89 Self::Custom {
90 name: name.into(),
91 hex: hex.into(),
92 css_prefix: Some(css_prefix.into()),
93 }
94 }
95}
96
97#[derive(Clone, Copy, PartialEq, Debug, Default)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub enum RadiusScale {
101 Sharp,
102 #[default]
103 Medium,
104 Soft,
105}
106
107impl RadiusScale {
108 pub fn name(&self) -> &'static str {
109 match self {
110 RadiusScale::Sharp => "Sharp (2px)",
111 RadiusScale::Medium => "Medium (8px)",
112 RadiusScale::Soft => "Soft (14px)",
113 }
114 }
115
116 pub fn sm(&self) -> &'static str {
117 match self {
118 RadiusScale::Sharp => "sd-radius-sharp",
119 RadiusScale::Medium => "rounded-md",
120 RadiusScale::Soft => "rounded-lg",
121 }
122 }
123
124 pub fn md(&self) -> &'static str {
125 match self {
126 RadiusScale::Sharp => "sd-radius-sharp",
127 RadiusScale::Medium => "rounded-lg",
128 RadiusScale::Soft => "rounded-xl",
129 }
130 }
131
132 pub fn lg(&self) -> &'static str {
133 match self {
134 RadiusScale::Sharp => "sd-radius-sharp",
135 RadiusScale::Medium => "rounded-xl",
136 RadiusScale::Soft => "rounded-2xl",
137 }
138 }
139}
140
141#[derive(Clone, Copy, PartialEq, Debug, Default)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub enum GlowMode {
145 #[default]
146 Subtle,
147 Expressive,
148 Off,
149}
150
151#[derive(Clone, Copy, PartialEq, Debug, Default)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub enum ElevationStyle {
155 #[default]
156 NeonGlow, HardBlock, SoftElevated, Flat, }
161
162#[derive(Clone, PartialEq, Debug, Default)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
165pub struct ComponentOverride {
166 pub base_class: Option<String>,
168 pub variant_classes: HashMap<String, String>,
170 pub size_classes: HashMap<String, String>,
172 pub shadow_class: Option<String>,
174 pub class_modifiers: HashMap<String, String>,
176}
177
178impl ComponentOverride {
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 pub fn with_base_class(mut self, base: impl Into<String>) -> Self {
184 self.base_class = Some(base.into());
185 self
186 }
187
188 pub fn with_variant(mut self, variant: impl Into<String>, class: impl Into<String>) -> Self {
189 self.variant_classes.insert(variant.into(), class.into());
190 self
191 }
192
193 pub fn with_size(mut self, size: impl Into<String>, class: impl Into<String>) -> Self {
194 self.size_classes.insert(size.into(), class.into());
195 self
196 }
197
198 pub fn with_shadow(mut self, shadow: impl Into<String>) -> Self {
199 self.shadow_class = Some(shadow.into());
200 self
201 }
202
203 pub fn with_class_modifier(
204 mut self,
205 target_class: impl Into<String>,
206 modifier: impl Into<String>,
207 ) -> Self {
208 self.class_modifiers
209 .insert(target_class.into(), modifier.into());
210 self
211 }
212}
213
214#[derive(Clone, PartialEq, Debug)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218pub struct Theme {
219 pub name: String,
220 pub primary_color: PrimaryColor,
221 pub radius_scale: RadiusScale,
222 pub elevation_style: ElevationStyle,
223 pub glow_mode: GlowMode,
224
225 pub surface_0: String,
227 pub surface_1: String,
228 pub surface_2: String,
229 pub surface_3: String,
230
231 pub root: String,
233 pub header: String,
234 pub sidebar: String,
235 pub sidebar_link: String,
236 pub sidebar_link_active: String,
237 pub content: String,
238 pub surface: String,
239 pub panel: String,
240 pub banner: String,
241
242 pub border: String,
244 pub border_subtle: String,
245 pub border_accent: String,
246 pub text_accent: String,
247 pub text_accent_bright: String,
248 pub glow: String,
249
250 pub glow_button: String,
252 pub glow_card: String,
253 pub glow_input: String,
254 pub glow_badge: String,
255 pub glow_modal: String,
256 pub glow_alert: String,
257 pub glow_dropdown: String,
258 pub glow_notification: String,
259 pub glow_switch: String,
260 pub glow_checkbox: String,
261
262 pub button_primary: String,
264 pub button_secondary: String,
265 pub button_outline: String,
266 pub button_ghost: String,
267 pub button_danger: String,
268 pub button_success: String,
269
270 pub card_base: String,
271 pub card_header: String,
272 pub card_footer: String,
273 pub card_shadow_none: String,
274 pub card_shadow_sm: String,
275 pub card_shadow_md: String,
276 pub card_shadow_lg: String,
277 pub card_shadow_xl: String,
278
279 pub input_base: String,
280 pub input_normal_border: String,
281 pub input_error_border: String,
282
283 pub badge_default: String,
284 pub badge_primary: String,
285 pub badge_success: String,
286 pub badge_warning: String,
287 pub badge_danger: String,
288 pub badge_outline: String,
289
290 pub navbar_base: String,
291
292 pub modal_backdrop: String,
293 pub modal_dialog: String,
294 pub modal_header: String,
295 pub modal_close: String,
296
297 pub alert_info: String,
298 pub alert_success: String,
299 pub alert_warning: String,
300 pub alert_danger: String,
301 pub alert_title_info: String,
302 pub alert_title_success: String,
303 pub alert_title_warning: String,
304 pub alert_title_danger: String,
305
306 pub components: HashMap<String, ComponentOverride>,
308 pub class_overrides: HashMap<String, String>,
309}
310
311impl Default for Theme {
312 fn default() -> Self {
313 Self::shadow()
314 }
315}
316
317impl Theme {
318 pub fn builder() -> ThemeBuilder {
320 ThemeBuilder::default()
321 }
322
323 pub fn resolve_variant_class(
326 &self,
327 component: &str,
328 variant: &str,
329 fallback_class: &str,
330 ) -> String {
331 if let Some(comp_override) = self.components.get(component) {
332 if let Some(custom) = comp_override.variant_classes.get(variant) {
333 return custom.clone();
334 }
335 }
336 fallback_class.to_string()
337 }
338
339 pub fn resolve_size_class(&self, component: &str, size: &str, fallback_class: &str) -> String {
341 if let Some(comp_override) = self.components.get(component) {
342 if let Some(custom) = comp_override.size_classes.get(size) {
343 return custom.clone();
344 }
345 }
346 fallback_class.to_string()
347 }
348
349 pub fn resolve_component_base(&self, component: &str) -> Option<&str> {
351 self.components
352 .get(component)
353 .and_then(|c| c.base_class.as_deref())
354 }
355
356 pub fn resolve_class_modifiers(&self, component: &str, instance_classes: &str) -> String {
358 let mut modifiers = Vec::new();
359
360 if let Some(comp) = self.components.get(component) {
362 for word in instance_classes.split_whitespace() {
363 if let Some(m) = comp.class_modifiers.get(word) {
364 modifiers.push(m.as_str());
365 }
366 }
367 }
368
369 for word in instance_classes.split_whitespace() {
371 if let Some(m) = self.class_overrides.get(word) {
372 modifiers.push(m.as_str());
373 }
374 }
375
376 modifiers.join(" ")
377 }
378
379 pub fn resolve_elevation_class(&self, component: &str, override_glow: Option<bool>) -> String {
381 if override_glow == Some(false) {
382 return String::new();
383 }
384
385 if let Some(comp) = self.components.get(component) {
387 if let Some(custom_shadow) = &comp.shadow_class {
388 return custom_shadow.clone();
389 }
390 }
391
392 match self.elevation_style {
393 ElevationStyle::Flat => String::new(),
394 ElevationStyle::HardBlock => match component {
395 "button" => "sd-comic-shadow-sm".to_string(),
396 "card" => "sd-comic-shadow-lg".to_string(),
397 "modal" => "sd-comic-shadow-xl".to_string(),
398 "dropdown" => "sd-comic-shadow-md".to_string(),
399 "badge" => "sd-comic-shadow-xs".to_string(),
400 "alert" => "sd-comic-shadow-md".to_string(),
401 _ => "sd-comic-shadow-sm".to_string(),
402 },
403 ElevationStyle::SoftElevated => match component {
404 "button" => "shadow-sm".to_string(),
405 "card" => "shadow-md".to_string(),
406 "modal" => "shadow-xl".to_string(),
407 _ => "shadow".to_string(),
408 },
409 ElevationStyle::NeonGlow => {
410 if self.is_component_glow_enabled(component, override_glow) {
411 self.component_glow(component).to_string()
412 } else {
413 String::new()
414 }
415 }
416 }
417 }
418
419 pub fn to_css_variables(&self) -> String {
421 format!(
422 ":root {{ \
423 --sd-primary-color: {primary}; \
424 --sd-surface-0: {s0}; \
425 --sd-surface-1: {s1}; \
426 --sd-surface-2: {s2}; \
427 --sd-surface-3: {s3}; \
428 }}",
429 primary = self.primary_color.hex(),
430 s0 = self.surface_0,
431 s1 = self.surface_1,
432 s2 = self.surface_2,
433 s3 = self.surface_3,
434 )
435 }
436
437 fn calculate_glows(
439 color: &PrimaryColor,
440 mode: GlowMode,
441 ) -> (
442 String,
443 String,
444 String,
445 String,
446 String,
447 String,
448 String,
449 String,
450 String,
451 String,
452 String,
453 ) {
454 if mode == GlowMode::Off {
455 return (
456 String::new(),
457 String::new(),
458 String::new(),
459 String::new(),
460 String::new(),
461 String::new(),
462 String::new(),
463 String::new(),
464 String::new(),
465 String::new(),
466 String::new(),
467 );
468 }
469
470 match color {
471 PrimaryColor::Violet => (
472 "glow-violet".to_string(),
473 "sd-btn--glow".to_string(),
474 "sd-card--hover-glow".to_string(),
475 "".to_string(),
476 "sd-badge--glow".to_string(),
477 "sd-modal-dialog--glow".to_string(),
478 "sd-alert--glow-info".to_string(),
479 "sd-dropdown-menu--glow".to_string(),
480 "sd-toast--glow-info".to_string(),
481 "sd-switch__track--glow".to_string(),
482 "sd-checkbox__box--glow".to_string(),
483 ),
484 PrimaryColor::Obsidian => (
485 "".to_string(),
486 "sd-btn--glow-obsidian".to_string(),
487 "sd-card--hover-glow".to_string(),
488 "".to_string(),
489 "sd-badge--glow".to_string(),
490 "sd-modal-dialog--glow".to_string(),
491 "sd-alert--glow-info".to_string(),
492 "sd-dropdown-menu--glow".to_string(),
493 "sd-toast--glow-info".to_string(),
494 "sd-switch__track--glow-obsidian".to_string(),
495 "sd-checkbox__box--glow".to_string(),
496 ),
497 PrimaryColor::Crimson => (
498 "glow-crimson".to_string(),
499 "sd-btn--glow-crimson".to_string(),
500 "sd-card--hover-glow".to_string(),
501 "".to_string(),
502 "sd-badge--glow".to_string(),
503 "sd-modal-dialog--glow".to_string(),
504 "sd-alert--glow-danger".to_string(),
505 "sd-dropdown-menu--glow".to_string(),
506 "sd-toast--glow-danger".to_string(),
507 "sd-switch__track--glow-crimson".to_string(),
508 "sd-checkbox__box--glow".to_string(),
509 ),
510 PrimaryColor::Cyan => (
511 "glow-cyan".to_string(),
512 "sd-btn--glow-cyan".to_string(),
513 "sd-card--hover-glow".to_string(),
514 "".to_string(),
515 "sd-badge--glow".to_string(),
516 "sd-modal-dialog--glow".to_string(),
517 "sd-alert--glow-info".to_string(),
518 "sd-dropdown-menu--glow".to_string(),
519 "sd-toast--glow-info".to_string(),
520 "sd-switch__track--glow-cyan".to_string(),
521 "sd-checkbox__box--glow".to_string(),
522 ),
523 PrimaryColor::Emerald => (
524 "glow-emerald".to_string(),
525 "sd-btn--glow-emerald".to_string(),
526 "sd-card--hover-glow".to_string(),
527 "".to_string(),
528 "sd-badge--glow".to_string(),
529 "sd-modal-dialog--glow".to_string(),
530 "sd-alert--glow-success".to_string(),
531 "sd-dropdown-menu--glow".to_string(),
532 "sd-toast--glow-success".to_string(),
533 "sd-switch__track--glow-emerald".to_string(),
534 "sd-checkbox__box--glow".to_string(),
535 ),
536 PrimaryColor::Custom { .. } => {
537 let prefix = color.css_prefix();
538 (
539 format!("glow-{}", prefix),
540 format!("sd-btn--glow-{}", prefix),
541 format!("sd-card--glow-{}", prefix),
542 "".to_string(),
543 format!("sd-badge--glow-{}", prefix),
544 format!("sd-modal-dialog--glow-{}", prefix),
545 "sd-alert--glow-info".to_string(),
546 "sd-dropdown-menu--glow".to_string(),
547 "sd-toast--glow-info".to_string(),
548 format!("sd-switch__track--glow-{}", prefix),
549 "sd-checkbox__box--glow".to_string(),
550 )
551 }
552 }
553 }
554
555 pub fn shadow() -> Self {
557 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
558 Self::calculate_glows(&PrimaryColor::Violet, GlowMode::Subtle);
559
560 Self {
561 name: "Ultraviolet Shadow".to_string(),
562 primary_color: PrimaryColor::Violet,
563 radius_scale: RadiusScale::Medium,
564 elevation_style: ElevationStyle::NeonGlow,
565 glow_mode: GlowMode::Subtle,
566
567 surface_0: "#030305".to_string(),
568 surface_1: "#09090d".to_string(),
569 surface_2: "#111118".to_string(),
570 surface_3: "#181824".to_string(),
571
572 root: "sd-root".to_string(),
573 header: "sd-header".to_string(),
574 sidebar: "sd-sidebar".to_string(),
575 sidebar_link: "sd-sidebar-link".to_string(),
576 sidebar_link_active: "sd-sidebar-link--active".to_string(),
577 content: "sd-content".to_string(),
578 surface: "sd-surface".to_string(),
579 panel: "sd-panel".to_string(),
580 banner: "sd-banner".to_string(),
581
582 border: "border-purple-500/25".to_string(),
583 border_subtle: "border-purple-500/10".to_string(),
584 border_accent: "border-purple-500/40".to_string(),
585 text_accent: "text-purple-400".to_string(),
586 text_accent_bright: "text-purple-300".to_string(),
587 glow: g,
588
589 glow_button: gb,
590 glow_card: gc,
591 glow_input: gi,
592 glow_badge: gbd,
593 glow_modal: gm,
594 glow_alert: ga,
595 glow_dropdown: gd,
596 glow_notification: gn,
597 glow_switch: gs,
598 glow_checkbox: gcb,
599
600 button_primary: "sd-btn--primary".to_string(),
601 button_secondary: "sd-btn--secondary".to_string(),
602 button_outline: "sd-btn--outline".to_string(),
603 button_ghost: "sd-btn--ghost".to_string(),
604 button_danger: "sd-btn--danger".to_string(),
605 button_success: "sd-btn--success".to_string(),
606
607 card_base: "sd-card".to_string(),
608 card_header: "sd-card__header".to_string(),
609 card_footer: "sd-card__footer".to_string(),
610 card_shadow_none: "sd-card--shadow-none".to_string(),
611 card_shadow_sm: "sd-card--shadow-sm".to_string(),
612 card_shadow_md: "sd-card--shadow-md".to_string(),
613 card_shadow_lg: "sd-card--shadow-lg".to_string(),
614 card_shadow_xl: "sd-card--shadow-xl".to_string(),
615
616 input_base: "sd-input".to_string(),
617 input_normal_border: "".to_string(),
618 input_error_border: "sd-input--error".to_string(),
619
620 badge_default: "sd-badge--default".to_string(),
621 badge_primary: "sd-badge--primary".to_string(),
622 badge_success: "sd-badge--success".to_string(),
623 badge_warning: "sd-badge--warning".to_string(),
624 badge_danger: "sd-badge--danger".to_string(),
625 badge_outline: "sd-badge--outline".to_string(),
626
627 navbar_base: "sd-navbar".to_string(),
628
629 modal_backdrop: "sd-modal-backdrop".to_string(),
630 modal_dialog: "sd-modal-dialog".to_string(),
631 modal_header: "sd-modal-dialog__header".to_string(),
632 modal_close: "sd-modal-dialog__close".to_string(),
633
634 alert_info: "sd-alert--info".to_string(),
635 alert_success: "sd-alert--success".to_string(),
636 alert_warning: "sd-alert--warning".to_string(),
637 alert_danger: "sd-alert--danger".to_string(),
638 alert_title_info: "sd-alert__title--info".to_string(),
639 alert_title_success: "sd-alert__title--success".to_string(),
640 alert_title_warning: "sd-alert__title--warning".to_string(),
641 alert_title_danger: "sd-alert__title--danger".to_string(),
642
643 components: HashMap::new(),
644 class_overrides: HashMap::new(),
645 }
646 }
647
648 pub fn obsidian() -> Self {
650 let mut t = Self::shadow();
651 t.name = "Obsidian Titanium".to_string();
652 t.primary_color = PrimaryColor::Obsidian;
653 t.sidebar_link_active = "sd-sidebar-link--active-obsidian".to_string();
654 t.border_accent = "border-zinc-500/40".to_string();
655 t.text_accent = "text-zinc-300".to_string();
656 t.text_accent_bright = "text-white".to_string();
657 t.button_primary = "sd-btn--primary-obsidian".to_string();
658 t.badge_primary = "sd-badge--default".to_string();
659 t.banner = "sd-banner sd-banner--obsidian".to_string();
660
661 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
662 Self::calculate_glows(&PrimaryColor::Obsidian, t.glow_mode);
663 t.glow = g;
664 t.glow_button = gb;
665 t.glow_card = gc;
666 t.glow_input = gi;
667 t.glow_badge = gbd;
668 t.glow_modal = gm;
669 t.glow_alert = ga;
670 t.glow_dropdown = gd;
671 t.glow_notification = gn;
672 t.glow_switch = gs;
673 t.glow_checkbox = gcb;
674 t
675 }
676
677 pub fn crimson() -> Self {
679 let mut t = Self::shadow();
680 t.name = "Crimson Phoenix".to_string();
681 t.primary_color = PrimaryColor::Crimson;
682 t.sidebar_link_active = "sd-sidebar-link--active-crimson".to_string();
683 t.border_accent = "border-rose-500/40".to_string();
684 t.text_accent = "text-rose-400".to_string();
685 t.text_accent_bright = "text-rose-300".to_string();
686 t.button_primary = "sd-btn--primary-crimson".to_string();
687 t.badge_primary = "sd-badge--danger".to_string();
688 t.banner = "sd-banner sd-banner--crimson".to_string();
689
690 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
691 Self::calculate_glows(&PrimaryColor::Crimson, t.glow_mode);
692 t.glow = g;
693 t.glow_button = gb;
694 t.glow_card = gc;
695 t.glow_input = gi;
696 t.glow_badge = gbd;
697 t.glow_modal = gm;
698 t.glow_alert = ga;
699 t.glow_dropdown = gd;
700 t.glow_notification = gn;
701 t.glow_switch = gs;
702 t.glow_checkbox = gcb;
703 t
704 }
705
706 pub fn cyan() -> Self {
708 let mut t = Self::shadow();
709 t.name = "Cyber Cyan".to_string();
710 t.primary_color = PrimaryColor::Cyan;
711 t.sidebar_link_active = "sd-sidebar-link--active-cyan".to_string();
712 t.border_accent = "border-cyan-500/40".to_string();
713 t.text_accent = "text-cyan-400".to_string();
714 t.text_accent_bright = "text-cyan-300".to_string();
715 t.button_primary = "sd-btn--primary-cyan".to_string();
716 t.badge_primary = "sd-badge--primary".to_string();
717 t.banner = "sd-banner sd-banner--cyan".to_string();
718
719 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
720 Self::calculate_glows(&PrimaryColor::Cyan, t.glow_mode);
721 t.glow = g;
722 t.glow_button = gb;
723 t.glow_card = gc;
724 t.glow_input = gi;
725 t.glow_badge = gbd;
726 t.glow_modal = gm;
727 t.glow_alert = ga;
728 t.glow_dropdown = gd;
729 t.glow_notification = gn;
730 t.glow_switch = gs;
731 t.glow_checkbox = gcb;
732 t
733 }
734
735 pub fn emerald() -> Self {
737 let mut t = Self::shadow();
738 t.name = "Emerald Matrix".to_string();
739 t.primary_color = PrimaryColor::Emerald;
740 t.sidebar_link_active = "sd-sidebar-link--active-emerald".to_string();
741 t.border_accent = "border-emerald-500/40".to_string();
742 t.text_accent = "text-emerald-400".to_string();
743 t.text_accent_bright = "text-emerald-300".to_string();
744 t.button_primary = "sd-btn--primary-emerald".to_string();
745 t.badge_primary = "sd-badge--success".to_string();
746 t.banner = "sd-banner sd-banner--emerald".to_string();
747
748 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
749 Self::calculate_glows(&PrimaryColor::Emerald, t.glow_mode);
750 t.glow = g;
751 t.glow_button = gb;
752 t.glow_card = gc;
753 t.glow_input = gi;
754 t.glow_badge = gbd;
755 t.glow_modal = gm;
756 t.glow_alert = ga;
757 t.glow_dropdown = gd;
758 t.glow_notification = gn;
759 t.glow_switch = gs;
760 t.glow_checkbox = gcb;
761 t
762 }
763
764 pub fn cartoon() -> Self {
766 let mut t = Self::shadow();
767 t.name = "Cartoon Pop".to_string();
768 t.primary_color = PrimaryColor::custom_with_prefix("Cartoon Yellow", "#FFCC00", "cartoon");
769 t.radius_scale = RadiusScale::Sharp;
770 t.elevation_style = ElevationStyle::HardBlock;
771 t.glow_mode = GlowMode::Off;
772
773 t.surface_0 = "#FBFAF3".to_string();
774 t.surface_1 = "#FFFFFF".to_string();
775 t.surface_2 = "#F4F4F4".to_string();
776 t.surface_3 = "#EAE6DF".to_string();
777
778 t.root = "sd-root--cartoon".to_string();
779 t.header = "sd-header--cartoon".to_string();
780 t.border = "border-3 border-black".to_string();
781 t.border_accent = "border-4 border-black".to_string();
782 t.text_accent = "text-[#FF3333]".to_string();
783 t.text_accent_bright = "text-[#FFCC00]".to_string();
784
785 t.button_primary = "sd-btn--primary-cartoon".to_string();
786 t.button_secondary = "sd-btn--secondary-cartoon".to_string();
787 t.card_base = "sd-card--comic".to_string();
788 t.badge_primary = "sd-badge--primary-cartoon".to_string();
789
790 let mut btn_override = ComponentOverride::new();
792 btn_override.base_class = Some("sd-btn--comic".to_string());
793 btn_override
794 .variant_classes
795 .insert("primary".to_string(), "sd-btn--primary-cartoon".to_string());
796 btn_override
797 .variant_classes
798 .insert("secondary".to_string(), "sd-btn--secondary-cartoon".to_string());
799 t.components.insert("button".to_string(), btn_override);
800
801 let mut card_override = ComponentOverride::new();
802 card_override.base_class = Some("sd-card--comic".to_string());
803 t.components.insert("card".to_string(), card_override);
804
805 let mut badge_override = ComponentOverride::new();
806 badge_override.base_class = Some("sd-badge--comic".to_string());
807 t.components.insert("badge".to_string(), badge_override);
808
809 t
810 }
811
812 pub fn apply_primary_color(&mut self, color: PrimaryColor) {
814 let current_radius = self.radius_scale;
815 let current_elevation = self.elevation_style;
816 let current_glow = self.glow_mode;
817 let mut updated = match &color {
818 PrimaryColor::Violet => Self::shadow(),
819 PrimaryColor::Obsidian => Self::obsidian(),
820 PrimaryColor::Crimson => Self::crimson(),
821 PrimaryColor::Cyan => Self::cyan(),
822 PrimaryColor::Emerald => Self::emerald(),
823 PrimaryColor::Custom { .. } => {
824 let mut custom_t = Self::shadow();
825 custom_t.name = color.name().to_string();
826 custom_t.primary_color = color.clone();
827 let prefix = color.css_prefix();
828 custom_t.button_primary = format!("sd-btn--primary-{}", prefix);
829 custom_t
830 }
831 };
832 updated.radius_scale = current_radius;
833 updated.elevation_style = current_elevation;
834 updated.apply_glow_mode(current_glow);
835 *self = updated;
836 }
837
838 pub fn apply_radius_scale(&mut self, radius: RadiusScale) {
840 self.radius_scale = radius;
841 }
842
843 pub fn apply_elevation_style(&mut self, elevation: ElevationStyle) {
845 self.elevation_style = elevation;
846 }
847
848 pub fn apply_glow_mode(&mut self, glow: GlowMode) {
850 self.glow_mode = glow;
851 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
852 Self::calculate_glows(&self.primary_color, glow);
853 self.glow = g;
854 self.glow_button = gb;
855 self.glow_card = gc;
856 self.glow_input = gi;
857 self.glow_badge = gbd;
858 self.glow_modal = gm;
859 self.glow_alert = ga;
860 self.glow_dropdown = gd;
861 self.glow_notification = gn;
862 self.glow_switch = gs;
863 self.glow_checkbox = gcb;
864 }
865
866 pub fn component_glow(&self, component: &str) -> &str {
868 match component {
869 "button" => &self.glow_button,
870 "card" => &self.glow_card,
871 "input" => &self.glow_input,
872 "badge" => &self.glow_badge,
873 "modal" => &self.glow_modal,
874 "alert" => &self.glow_alert,
875 "dropdown" => &self.glow_dropdown,
876 "notification" => &self.glow_notification,
877 "switch" => &self.glow_switch,
878 "checkbox" => &self.glow_checkbox,
879 _ => &self.glow,
880 }
881 }
882
883 pub fn is_component_glow_enabled(&self, component: &str, override_glow: Option<bool>) -> bool {
885 match override_glow {
886 Some(enabled) => enabled,
887 None => !self.component_glow(component).is_empty(),
888 }
889 }
890
891 pub fn with_name(mut self, name: impl Into<String>) -> Self {
893 self.name = name.into();
894 self
895 }
896
897 pub fn with_primary(mut self, button_primary: impl Into<String>) -> Self {
898 self.button_primary = button_primary.into();
899 self
900 }
901
902 pub fn with_card_base(mut self, card_base: impl Into<String>) -> Self {
903 self.card_base = card_base.into();
904 self
905 }
906
907 pub fn with_glow(mut self, glow: impl Into<String>) -> Self {
908 self.glow = glow.into();
909 self
910 }
911
912 pub fn with_elevation(mut self, elevation: ElevationStyle) -> Self {
913 self.elevation_style = elevation;
914 self
915 }
916
917 pub fn with_component_override(
918 mut self,
919 component: impl Into<String>,
920 component_override: ComponentOverride,
921 ) -> Self {
922 self.components.insert(component.into(), component_override);
923 self
924 }
925
926 pub fn with_class_override(
927 mut self,
928 target_class: impl Into<String>,
929 replacement_or_extra: impl Into<String>,
930 ) -> Self {
931 self.class_overrides
932 .insert(target_class.into(), replacement_or_extra.into());
933 self
934 }
935
936 #[cfg(feature = "button")]
938 pub fn button_variant_classes(&self, variant: ButtonVariant) -> &str {
939 match variant {
940 ButtonVariant::Primary => &self.button_primary,
941 ButtonVariant::Secondary => &self.button_secondary,
942 ButtonVariant::Outline => &self.button_outline,
943 ButtonVariant::Ghost => &self.button_ghost,
944 ButtonVariant::Danger => &self.button_danger,
945 ButtonVariant::Success => &self.button_success,
946 }
947 }
948
949 #[cfg(feature = "card")]
950 pub fn card_shadow_classes(&self, shadow: CardShadow) -> &str {
951 match shadow {
952 CardShadow::None => &self.card_shadow_none,
953 CardShadow::Sm => &self.card_shadow_sm,
954 CardShadow::Md => &self.card_shadow_md,
955 CardShadow::Lg => &self.card_shadow_lg,
956 CardShadow::Xl => &self.card_shadow_xl,
957 }
958 }
959
960 #[cfg(feature = "badge")]
961 pub fn badge_variant_classes(&self, variant: BadgeVariant) -> &str {
962 match variant {
963 BadgeVariant::Default => &self.badge_default,
964 BadgeVariant::Primary => &self.badge_primary,
965 BadgeVariant::Success => &self.badge_success,
966 BadgeVariant::Warning => &self.badge_warning,
967 BadgeVariant::Danger => &self.badge_danger,
968 BadgeVariant::Outline => &self.badge_outline,
969 }
970 }
971
972 #[cfg(feature = "alert")]
973 pub fn alert_variant_classes(&self, variant: AlertVariant) -> &str {
974 match variant {
975 AlertVariant::Info => &self.alert_info,
976 AlertVariant::Success => &self.alert_success,
977 AlertVariant::Warning => &self.alert_warning,
978 AlertVariant::Danger => &self.alert_danger,
979 }
980 }
981
982 #[cfg(feature = "alert")]
983 pub fn alert_title_classes(&self, variant: AlertVariant) -> &str {
984 match variant {
985 AlertVariant::Info => &self.alert_title_info,
986 AlertVariant::Success => &self.alert_title_success,
987 AlertVariant::Warning => &self.alert_title_warning,
988 AlertVariant::Danger => &self.alert_title_danger,
989 }
990 }
991}
992
993#[derive(Clone, Default)]
995pub struct ThemeBuilder {
996 theme: Theme,
997}
998
999impl ThemeBuilder {
1000 pub fn new() -> Self {
1001 Self::default()
1002 }
1003
1004 pub fn name(mut self, name: impl Into<String>) -> Self {
1005 self.theme.name = name.into();
1006 self
1007 }
1008
1009 pub fn primary_color(mut self, color: PrimaryColor) -> Self {
1010 self.theme.apply_primary_color(color);
1011 self
1012 }
1013
1014 pub fn radius_scale(mut self, radius: RadiusScale) -> Self {
1015 self.theme.apply_radius_scale(radius);
1016 self
1017 }
1018
1019 pub fn elevation_style(mut self, elevation: ElevationStyle) -> Self {
1020 self.theme.apply_elevation_style(elevation);
1021 self
1022 }
1023
1024 pub fn glow_mode(mut self, glow: GlowMode) -> Self {
1025 self.theme.apply_glow_mode(glow);
1026 self
1027 }
1028
1029 pub fn surface_0(mut self, val: impl Into<String>) -> Self {
1030 self.theme.surface_0 = val.into();
1031 self
1032 }
1033
1034 pub fn surface_1(mut self, val: impl Into<String>) -> Self {
1035 self.theme.surface_1 = val.into();
1036 self
1037 }
1038
1039 pub fn surface_2(mut self, val: impl Into<String>) -> Self {
1040 self.theme.surface_2 = val.into();
1041 self
1042 }
1043
1044 pub fn surface_3(mut self, val: impl Into<String>) -> Self {
1045 self.theme.surface_3 = val.into();
1046 self
1047 }
1048
1049 pub fn override_component(
1051 mut self,
1052 component: impl Into<String>,
1053 config_fn: impl FnOnce(ComponentOverride) -> ComponentOverride,
1054 ) -> Self {
1055 let key = component.into();
1056 let existing = self.theme.components.remove(&key).unwrap_or_default();
1057 let updated = config_fn(existing);
1058 self.theme.components.insert(key, updated);
1059 self
1060 }
1061
1062 pub fn override_class(
1064 mut self,
1065 target_class: impl Into<String>,
1066 modifier: impl Into<String>,
1067 ) -> Self {
1068 self.theme
1069 .class_overrides
1070 .insert(target_class.into(), modifier.into());
1071 self
1072 }
1073
1074 pub fn build(self) -> Theme {
1075 self.theme
1076 }
1077}
1078
1079#[component]
1081pub fn ThemeProvider(#[props(default)] theme: Option<Signal<Theme>>, children: Element) -> Element {
1082 let current = theme.unwrap_or_else(|| use_signal(Theme::default));
1083 use_context_provider(|| current);
1084 rsx! { {children} }
1085}
1086
1087pub fn use_theme() -> Signal<Theme> {
1089 match try_use_context::<Signal<Theme>>() {
1090 Some(s) => s,
1091 None => use_context_provider(|| Signal::new(Theme::default())),
1092 }
1093}