1use dioxus::prelude::*;
2
3#[cfg(feature = "alert")]
4use crate::alert::AlertVariant;
5#[cfg(feature = "badge")]
6use crate::badge::BadgeVariant;
7#[cfg(feature = "button")]
8use crate::button::ButtonVariant;
9#[cfg(feature = "card")]
10use crate::card::CardShadow;
11
12#[derive(Clone, Copy, PartialEq, Debug, Default)]
14pub enum PrimaryColor {
15 #[default]
16 Violet,
17 Obsidian,
18 Crimson,
19 Cyan,
20 Emerald,
21}
22
23impl PrimaryColor {
24 pub fn name(&self) -> &'static str {
25 match self {
26 PrimaryColor::Violet => "Violet",
27 PrimaryColor::Obsidian => "Obsidian",
28 PrimaryColor::Crimson => "Crimson",
29 PrimaryColor::Cyan => "Cyan",
30 PrimaryColor::Emerald => "Emerald",
31 }
32 }
33
34 pub fn hex(&self) -> &'static str {
35 match self {
36 PrimaryColor::Violet => "#3b0764",
37 PrimaryColor::Obsidian => "#ffffff",
38 PrimaryColor::Crimson => "#ff1744",
39 PrimaryColor::Cyan => "#00f0ff",
40 PrimaryColor::Emerald => "#00ff9d",
41 }
42 }
43}
44
45#[derive(Clone, Copy, PartialEq, Debug, Default)]
47pub enum RadiusScale {
48 Sharp,
49 #[default]
50 Medium,
51 Soft,
52}
53
54impl RadiusScale {
55 pub fn name(&self) -> &'static str {
56 match self {
57 RadiusScale::Sharp => "Sharp (2px)",
58 RadiusScale::Medium => "Medium (8px)",
59 RadiusScale::Soft => "Soft (14px)",
60 }
61 }
62
63 pub fn sm(&self) -> &'static str {
64 match self {
65 RadiusScale::Sharp => "sd-radius-sharp",
66 RadiusScale::Medium => "rounded-md",
67 RadiusScale::Soft => "rounded-lg",
68 }
69 }
70
71 pub fn md(&self) -> &'static str {
72 match self {
73 RadiusScale::Sharp => "sd-radius-sharp",
74 RadiusScale::Medium => "rounded-lg",
75 RadiusScale::Soft => "rounded-xl",
76 }
77 }
78
79 pub fn lg(&self) -> &'static str {
80 match self {
81 RadiusScale::Sharp => "sd-radius-sharp",
82 RadiusScale::Medium => "rounded-xl",
83 RadiusScale::Soft => "rounded-2xl",
84 }
85 }
86}
87
88#[derive(Clone, Copy, PartialEq, Debug, Default)]
90pub enum GlowMode {
91 #[default]
92 Subtle,
93 Expressive,
94 Off,
95}
96
97#[derive(Clone, PartialEq, Debug)]
99pub struct Theme {
100 pub name: String,
101 pub primary_color: PrimaryColor,
102 pub radius_scale: RadiusScale,
103 pub glow_mode: GlowMode,
104
105 pub surface_0: String,
107 pub surface_1: String,
108 pub surface_2: String,
109 pub surface_3: String,
110
111 pub root: String,
113 pub header: String,
114 pub sidebar: String,
115 pub sidebar_link: String,
116 pub sidebar_link_active: String,
117 pub content: String,
118 pub surface: String,
119 pub panel: String,
120 pub banner: String,
121
122 pub border: String,
124 pub border_subtle: String,
125 pub border_accent: String,
126 pub text_accent: String,
127 pub text_accent_bright: String,
128 pub glow: String,
129
130 pub glow_button: String,
132 pub glow_card: String,
133 pub glow_input: String,
134 pub glow_badge: String,
135 pub glow_modal: String,
136 pub glow_alert: String,
137 pub glow_dropdown: String,
138 pub glow_notification: String,
139 pub glow_switch: String,
140 pub glow_checkbox: String,
141
142 pub button_primary: String,
144 pub button_secondary: String,
145 pub button_outline: String,
146 pub button_ghost: String,
147 pub button_danger: String,
148 pub button_success: String,
149
150 pub card_base: String,
152 pub card_header: String,
153 pub card_footer: String,
154 pub card_shadow_none: String,
155 pub card_shadow_sm: String,
156 pub card_shadow_md: String,
157 pub card_shadow_lg: String,
158 pub card_shadow_xl: String,
159
160 pub input_base: String,
162 pub input_normal_border: String,
163 pub input_error_border: String,
164
165 pub badge_default: String,
167 pub badge_primary: String,
168 pub badge_success: String,
169 pub badge_warning: String,
170 pub badge_danger: String,
171 pub badge_outline: String,
172
173 pub navbar_base: String,
175
176 pub modal_backdrop: String,
178 pub modal_dialog: String,
179 pub modal_header: String,
180 pub modal_close: String,
181
182 pub alert_info: String,
184 pub alert_success: String,
185 pub alert_warning: String,
186 pub alert_danger: String,
187 pub alert_title_info: String,
188 pub alert_title_success: String,
189 pub alert_title_warning: String,
190 pub alert_title_danger: String,
191}
192
193impl Default for Theme {
194 fn default() -> Self {
195 Self::shadow()
196 }
197}
198
199impl Theme {
200 fn calculate_glows(
202 color: PrimaryColor,
203 mode: GlowMode,
204 ) -> (
205 String,
206 String,
207 String,
208 String,
209 String,
210 String,
211 String,
212 String,
213 String,
214 String,
215 String,
216 ) {
217 if mode == GlowMode::Off {
218 return (
219 String::new(),
220 String::new(),
221 String::new(),
222 String::new(),
223 String::new(),
224 String::new(),
225 String::new(),
226 String::new(),
227 String::new(),
228 String::new(),
229 String::new(),
230 );
231 }
232
233 match color {
234 PrimaryColor::Violet => (
235 "glow-violet".to_string(),
236 "sd-btn--glow".to_string(),
237 "sd-card--hover-glow".to_string(),
238 "".to_string(),
239 "sd-badge--glow".to_string(),
240 "sd-modal-dialog--glow".to_string(),
241 "sd-alert--glow-info".to_string(),
242 "sd-dropdown-menu--glow".to_string(),
243 "sd-toast--glow-info".to_string(),
244 "sd-switch__track--glow".to_string(),
245 "sd-checkbox__box--glow".to_string(),
246 ),
247 PrimaryColor::Obsidian => (
248 "".to_string(),
249 "sd-btn--glow-obsidian".to_string(),
250 "sd-card--hover-glow".to_string(),
251 "".to_string(),
252 "sd-badge--glow".to_string(),
253 "sd-modal-dialog--glow".to_string(),
254 "sd-alert--glow-info".to_string(),
255 "sd-dropdown-menu--glow".to_string(),
256 "sd-toast--glow-info".to_string(),
257 "sd-switch__track--glow-obsidian".to_string(),
258 "sd-checkbox__box--glow".to_string(),
259 ),
260 PrimaryColor::Crimson => (
261 "glow-crimson".to_string(),
262 "sd-btn--glow-crimson".to_string(),
263 "sd-card--hover-glow".to_string(),
264 "".to_string(),
265 "sd-badge--glow".to_string(),
266 "sd-modal-dialog--glow".to_string(),
267 "sd-alert--glow-danger".to_string(),
268 "sd-dropdown-menu--glow".to_string(),
269 "sd-toast--glow-danger".to_string(),
270 "sd-switch__track--glow-crimson".to_string(),
271 "sd-checkbox__box--glow".to_string(),
272 ),
273 PrimaryColor::Cyan => (
274 "glow-cyan".to_string(),
275 "sd-btn--glow-cyan".to_string(),
276 "sd-card--hover-glow".to_string(),
277 "".to_string(),
278 "sd-badge--glow".to_string(),
279 "sd-modal-dialog--glow".to_string(),
280 "sd-alert--glow-info".to_string(),
281 "sd-dropdown-menu--glow".to_string(),
282 "sd-toast--glow-info".to_string(),
283 "sd-switch__track--glow-cyan".to_string(),
284 "sd-checkbox__box--glow".to_string(),
285 ),
286 PrimaryColor::Emerald => (
287 "glow-emerald".to_string(),
288 "sd-btn--glow-emerald".to_string(),
289 "sd-card--hover-glow".to_string(),
290 "".to_string(),
291 "sd-badge--glow".to_string(),
292 "sd-modal-dialog--glow".to_string(),
293 "sd-alert--glow-success".to_string(),
294 "sd-dropdown-menu--glow".to_string(),
295 "sd-toast--glow-success".to_string(),
296 "sd-switch__track--glow-emerald".to_string(),
297 "sd-checkbox__box--glow".to_string(),
298 ),
299 }
300 }
301
302 pub fn shadow() -> Self {
304 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
305 Self::calculate_glows(PrimaryColor::Violet, GlowMode::Subtle);
306
307 Self {
308 name: "Ultraviolet Shadow".to_string(),
309 primary_color: PrimaryColor::Violet,
310 radius_scale: RadiusScale::Medium,
311 glow_mode: GlowMode::Subtle,
312
313 surface_0: "#030305".to_string(),
314 surface_1: "#09090d".to_string(),
315 surface_2: "#111118".to_string(),
316 surface_3: "#181824".to_string(),
317
318 root: "sd-root".to_string(),
319 header: "sd-header".to_string(),
320 sidebar: "sd-sidebar".to_string(),
321 sidebar_link: "sd-sidebar-link".to_string(),
322 sidebar_link_active: "sd-sidebar-link--active".to_string(),
323 content: "sd-content".to_string(),
324 surface: "sd-surface".to_string(),
325 panel: "sd-panel".to_string(),
326 banner: "sd-banner".to_string(),
327
328 border: "border-white/[0.08]".to_string(),
329 border_subtle: "border-white/[0.05]".to_string(),
330 border_accent: "border-purple-500/40".to_string(),
331 text_accent: "text-purple-400".to_string(),
332 text_accent_bright: "text-purple-300".to_string(),
333 glow: g,
334
335 glow_button: gb,
336 glow_card: gc,
337 glow_input: gi,
338 glow_badge: gbd,
339 glow_modal: gm,
340 glow_alert: ga,
341 glow_dropdown: gd,
342 glow_notification: gn,
343 glow_switch: gs,
344 glow_checkbox: gcb,
345
346 button_primary: "sd-btn--primary".to_string(),
347 button_secondary: "sd-btn--secondary".to_string(),
348 button_outline: "sd-btn--outline".to_string(),
349 button_ghost: "sd-btn--ghost".to_string(),
350 button_danger: "sd-btn--danger".to_string(),
351 button_success: "sd-btn--success".to_string(),
352
353 card_base: "sd-card".to_string(),
354 card_header: "sd-card__header".to_string(),
355 card_footer: "sd-card__footer".to_string(),
356 card_shadow_none: "sd-card--shadow-none".to_string(),
357 card_shadow_sm: "sd-card--shadow-sm".to_string(),
358 card_shadow_md: "sd-card--shadow-md".to_string(),
359 card_shadow_lg: "sd-card--shadow-lg".to_string(),
360 card_shadow_xl: "sd-card--shadow-xl".to_string(),
361
362 input_base: "sd-input".to_string(),
363 input_normal_border: "".to_string(),
364 input_error_border: "sd-input--error".to_string(),
365
366 badge_default: "sd-badge--default".to_string(),
367 badge_primary: "sd-badge--primary".to_string(),
368 badge_success: "sd-badge--success".to_string(),
369 badge_warning: "sd-badge--warning".to_string(),
370 badge_danger: "sd-badge--danger".to_string(),
371 badge_outline: "sd-badge--outline".to_string(),
372
373 navbar_base: "sd-navbar".to_string(),
374
375 modal_backdrop: "sd-modal-backdrop".to_string(),
376 modal_dialog: "sd-modal-dialog".to_string(),
377 modal_header: "sd-modal-dialog__header".to_string(),
378 modal_close: "sd-modal-dialog__close".to_string(),
379
380 alert_info: "sd-alert--info".to_string(),
381 alert_success: "sd-alert--success".to_string(),
382 alert_warning: "sd-alert--warning".to_string(),
383 alert_danger: "sd-alert--danger".to_string(),
384 alert_title_info: "sd-alert__title--info".to_string(),
385 alert_title_success: "sd-alert__title--success".to_string(),
386 alert_title_warning: "sd-alert__title--warning".to_string(),
387 alert_title_danger: "sd-alert__title--danger".to_string(),
388 }
389 }
390
391 pub fn obsidian() -> Self {
393 let mut t = Self::shadow();
394 t.name = "Obsidian Titanium".to_string();
395 t.primary_color = PrimaryColor::Obsidian;
396 t.sidebar_link_active = "sd-sidebar-link--active-obsidian".to_string();
397 t.border_accent = "border-zinc-500/40".to_string();
398 t.text_accent = "text-zinc-300".to_string();
399 t.text_accent_bright = "text-white".to_string();
400 t.button_primary = "sd-btn--primary-obsidian".to_string();
401 t.badge_primary = "sd-badge--default".to_string();
402 t.banner = "sd-banner sd-banner--obsidian".to_string();
403
404 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
405 Self::calculate_glows(PrimaryColor::Obsidian, t.glow_mode);
406 t.glow = g;
407 t.glow_button = gb;
408 t.glow_card = gc;
409 t.glow_input = gi;
410 t.glow_badge = gbd;
411 t.glow_modal = gm;
412 t.glow_alert = ga;
413 t.glow_dropdown = gd;
414 t.glow_notification = gn;
415 t.glow_switch = gs;
416 t.glow_checkbox = gcb;
417 t
418 }
419
420 pub fn crimson() -> Self {
422 let mut t = Self::shadow();
423 t.name = "Crimson Phoenix".to_string();
424 t.primary_color = PrimaryColor::Crimson;
425 t.sidebar_link_active = "sd-sidebar-link--active-crimson".to_string();
426 t.border_accent = "border-rose-500/40".to_string();
427 t.text_accent = "text-rose-400".to_string();
428 t.text_accent_bright = "text-rose-300".to_string();
429 t.button_primary = "sd-btn--primary-crimson".to_string();
430 t.badge_primary = "sd-badge--danger".to_string();
431 t.banner = "sd-banner sd-banner--crimson".to_string();
432
433 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
434 Self::calculate_glows(PrimaryColor::Crimson, t.glow_mode);
435 t.glow = g;
436 t.glow_button = gb;
437 t.glow_card = gc;
438 t.glow_input = gi;
439 t.glow_badge = gbd;
440 t.glow_modal = gm;
441 t.glow_alert = ga;
442 t.glow_dropdown = gd;
443 t.glow_notification = gn;
444 t.glow_switch = gs;
445 t.glow_checkbox = gcb;
446 t
447 }
448
449 pub fn cyan() -> Self {
451 let mut t = Self::shadow();
452 t.name = "Cyber Cyan".to_string();
453 t.primary_color = PrimaryColor::Cyan;
454 t.sidebar_link_active = "sd-sidebar-link--active-cyan".to_string();
455 t.border_accent = "border-cyan-500/40".to_string();
456 t.text_accent = "text-cyan-400".to_string();
457 t.text_accent_bright = "text-cyan-300".to_string();
458 t.button_primary = "sd-btn--primary-cyan".to_string();
459 t.badge_primary = "sd-badge--primary".to_string();
460 t.banner = "sd-banner sd-banner--cyan".to_string();
461
462 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
463 Self::calculate_glows(PrimaryColor::Cyan, t.glow_mode);
464 t.glow = g;
465 t.glow_button = gb;
466 t.glow_card = gc;
467 t.glow_input = gi;
468 t.glow_badge = gbd;
469 t.glow_modal = gm;
470 t.glow_alert = ga;
471 t.glow_dropdown = gd;
472 t.glow_notification = gn;
473 t.glow_switch = gs;
474 t.glow_checkbox = gcb;
475 t
476 }
477
478 pub fn emerald() -> Self {
480 let mut t = Self::shadow();
481 t.name = "Emerald Matrix".to_string();
482 t.primary_color = PrimaryColor::Emerald;
483 t.sidebar_link_active = "sd-sidebar-link--active-emerald".to_string();
484 t.border_accent = "border-emerald-500/40".to_string();
485 t.text_accent = "text-emerald-400".to_string();
486 t.text_accent_bright = "text-emerald-300".to_string();
487 t.button_primary = "sd-btn--primary-emerald".to_string();
488 t.badge_primary = "sd-badge--success".to_string();
489 t.banner = "sd-banner sd-banner--emerald".to_string();
490
491 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
492 Self::calculate_glows(PrimaryColor::Emerald, t.glow_mode);
493 t.glow = g;
494 t.glow_button = gb;
495 t.glow_card = gc;
496 t.glow_input = gi;
497 t.glow_badge = gbd;
498 t.glow_modal = gm;
499 t.glow_alert = ga;
500 t.glow_dropdown = gd;
501 t.glow_notification = gn;
502 t.glow_switch = gs;
503 t.glow_checkbox = gcb;
504 t
505 }
506
507 pub fn apply_primary_color(&mut self, color: PrimaryColor) {
509 let current_radius = self.radius_scale;
510 let current_glow = self.glow_mode;
511 let mut updated = match color {
512 PrimaryColor::Violet => Self::shadow(),
513 PrimaryColor::Obsidian => Self::obsidian(),
514 PrimaryColor::Crimson => Self::crimson(),
515 PrimaryColor::Cyan => Self::cyan(),
516 PrimaryColor::Emerald => Self::emerald(),
517 };
518 updated.radius_scale = current_radius;
519 updated.apply_glow_mode(current_glow);
520 *self = updated;
521 }
522
523 pub fn apply_radius_scale(&mut self, radius: RadiusScale) {
525 self.radius_scale = radius;
526 }
527
528 pub fn apply_glow_mode(&mut self, glow: GlowMode) {
530 self.glow_mode = glow;
531 let (g, gb, gc, gi, gbd, gm, ga, gd, gn, gs, gcb) =
532 Self::calculate_glows(self.primary_color, glow);
533 self.glow = g;
534 self.glow_button = gb;
535 self.glow_card = gc;
536 self.glow_input = gi;
537 self.glow_badge = gbd;
538 self.glow_modal = gm;
539 self.glow_alert = ga;
540 self.glow_dropdown = gd;
541 self.glow_notification = gn;
542 self.glow_switch = gs;
543 self.glow_checkbox = gcb;
544 }
545
546 pub fn component_glow(&self, component: &str) -> &str {
548 match component {
549 "button" => &self.glow_button,
550 "card" => &self.glow_card,
551 "input" => &self.glow_input,
552 "badge" => &self.glow_badge,
553 "modal" => &self.glow_modal,
554 "alert" => &self.glow_alert,
555 "dropdown" => &self.glow_dropdown,
556 "notification" => &self.glow_notification,
557 "switch" => &self.glow_switch,
558 "checkbox" => &self.glow_checkbox,
559 _ => &self.glow,
560 }
561 }
562
563 pub fn is_component_glow_enabled(&self, component: &str, override_glow: Option<bool>) -> bool {
565 match override_glow {
566 Some(enabled) => enabled,
567 None => !self.component_glow(component).is_empty(),
568 }
569 }
570
571 pub fn with_name(mut self, name: impl Into<String>) -> Self {
573 self.name = name.into();
574 self
575 }
576
577 pub fn with_primary(mut self, button_primary: impl Into<String>) -> Self {
578 self.button_primary = button_primary.into();
579 self
580 }
581
582 pub fn with_card_base(mut self, card_base: impl Into<String>) -> Self {
583 self.card_base = card_base.into();
584 self
585 }
586
587 pub fn with_glow(mut self, glow: impl Into<String>) -> Self {
588 self.glow = glow.into();
589 self
590 }
591
592 pub fn with_component_glow(mut self, component: &str, glow: impl Into<String>) -> Self {
593 let val = glow.into();
594 match component {
595 "button" => self.glow_button = val,
596 "card" => self.glow_card = val,
597 "input" => self.glow_input = val,
598 "badge" => self.glow_badge = val,
599 "modal" => self.glow_modal = val,
600 "alert" => self.glow_alert = val,
601 "dropdown" => self.glow_dropdown = val,
602 "notification" => self.glow_notification = val,
603 "switch" => self.glow_switch = val,
604 "checkbox" => self.glow_checkbox = val,
605 _ => self.glow = val,
606 }
607 self
608 }
609
610 #[cfg(feature = "button")]
612 pub fn button_variant_classes(&self, variant: ButtonVariant) -> &str {
613 match variant {
614 ButtonVariant::Primary => &self.button_primary,
615 ButtonVariant::Secondary => &self.button_secondary,
616 ButtonVariant::Outline => &self.button_outline,
617 ButtonVariant::Ghost => &self.button_ghost,
618 ButtonVariant::Danger => &self.button_danger,
619 ButtonVariant::Success => &self.button_success,
620 }
621 }
622
623 #[cfg(feature = "card")]
624 pub fn card_shadow_classes(&self, shadow: CardShadow) -> &str {
625 match shadow {
626 CardShadow::None => &self.card_shadow_none,
627 CardShadow::Sm => &self.card_shadow_sm,
628 CardShadow::Md => &self.card_shadow_md,
629 CardShadow::Lg => &self.card_shadow_lg,
630 CardShadow::Xl => &self.card_shadow_xl,
631 }
632 }
633
634 #[cfg(feature = "badge")]
635 pub fn badge_variant_classes(&self, variant: BadgeVariant) -> &str {
636 match variant {
637 BadgeVariant::Default => &self.badge_default,
638 BadgeVariant::Primary => &self.badge_primary,
639 BadgeVariant::Success => &self.badge_success,
640 BadgeVariant::Warning => &self.badge_warning,
641 BadgeVariant::Danger => &self.badge_danger,
642 BadgeVariant::Outline => &self.badge_outline,
643 }
644 }
645
646 #[cfg(feature = "alert")]
647 pub fn alert_variant_classes(&self, variant: AlertVariant) -> &str {
648 match variant {
649 AlertVariant::Info => &self.alert_info,
650 AlertVariant::Success => &self.alert_success,
651 AlertVariant::Warning => &self.alert_warning,
652 AlertVariant::Danger => &self.alert_danger,
653 }
654 }
655
656 #[cfg(feature = "alert")]
657 pub fn alert_title_classes(&self, variant: AlertVariant) -> &str {
658 match variant {
659 AlertVariant::Info => &self.alert_title_info,
660 AlertVariant::Success => &self.alert_title_success,
661 AlertVariant::Warning => &self.alert_title_warning,
662 AlertVariant::Danger => &self.alert_title_danger,
663 }
664 }
665}
666
667#[component]
669pub fn ThemeProvider(#[props(default)] theme: Option<Signal<Theme>>, children: Element) -> Element {
670 let current = theme.unwrap_or_else(|| use_signal(Theme::default));
671 use_context_provider(|| current);
672 rsx! { {children} }
673}
674
675pub fn use_theme() -> Signal<Theme> {
677 match try_use_context::<Signal<Theme>>() {
678 Some(s) => s,
679 None => use_context_provider(|| Signal::new(Theme::default())),
680 }
681}