1pub use fission_ir::op::{BoxShadow, Color, Fill, LineCap, LineJoin, Stroke};
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub enum DesignMode {
21 #[default]
22 Light,
23 Dark,
24}
25
26#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
27pub struct DesignSystemInfo {
28 pub name: String,
29 pub version: String,
30 pub description: String,
31 pub source: String,
32}
33
34pub trait DesignSystem {
35 fn info() -> &'static DesignSystemInfo;
36 fn tokens() -> &'static DesignTokenSet;
37 fn components() -> &'static [DesignComponentSpec];
38 fn patterns() -> &'static [DesignPatternSpec];
39 fn assets() -> &'static DesignAssetManifest;
40 fn theme_ref(mode: DesignMode) -> &'static Theme;
41
42 fn theme(mode: DesignMode) -> Theme {
43 Self::theme_ref(mode).clone()
44 }
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
48pub struct ResolvedDesignSystem {
49 pub mode: DesignMode,
50 pub info: DesignSystemInfo,
51 pub tokens: DesignTokenSet,
52 pub components: Vec<DesignComponentSpec>,
53 pub patterns: Vec<DesignPatternSpec>,
54 pub assets: DesignAssetManifest,
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
58pub struct DesignTokenSet {
59 pub tokens: Vec<DesignToken>,
60}
61
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63pub struct DesignToken {
64 pub path: String,
65 pub kind: String,
66 pub value: DesignValue,
67}
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub enum DesignValue {
71 None,
72 Bool(bool),
73 Number(f32),
74 Dimension(f32),
75 DurationMs(u64),
76 Text(String),
77 Color(Color),
78 Shadow(Vec<ShadowLayer>),
79 Easing(EasingCurve),
80 Object(Vec<DesignProperty>),
81 List(Vec<DesignValue>),
82}
83
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct DesignProperty {
86 pub name: String,
87 pub value: DesignValue,
88}
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91pub struct ShadowLayer {
92 pub color: Color,
93 pub offset: (f32, f32),
94 pub blur_radius: f32,
95 pub spread_radius: f32,
96 pub inset: bool,
97}
98
99impl ShadowLayer {
100 pub fn to_box_shadow(&self) -> BoxShadow {
101 BoxShadow {
102 color: self.color,
103 offset: self.offset,
104 blur_radius: self.blur_radius,
105 }
106 }
107}
108
109fn shadow_layer_from_box(shadow: BoxShadow) -> ShadowLayer {
110 ShadowLayer {
111 color: shadow.color,
112 offset: shadow.offset,
113 blur_radius: shadow.blur_radius,
114 spread_radius: 0.0,
115 inset: false,
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
120pub enum EasingCurve {
121 Linear,
122 Ease,
123 CubicBezier(f32, f32, f32, f32),
124 Named(String),
125}
126
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128pub struct DesignComponentSpec {
129 pub name: String,
130 pub description: String,
131 pub anatomy: Vec<String>,
132 pub properties: Vec<DesignProperty>,
133}
134
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
136pub struct DesignPatternSpec {
137 pub name: String,
138 pub description: String,
139 pub properties: Vec<DesignProperty>,
140}
141
142#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
143pub struct DesignAssetManifest {
144 pub logos: Vec<DesignAsset>,
145 pub fonts: Vec<DesignAsset>,
146}
147
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
149pub struct DesignAsset {
150 pub id: String,
151 pub path: String,
152 pub format: String,
153}
154
155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
156pub enum ComponentSize {
157 Sm,
158 #[default]
159 Md,
160 Lg,
161 Xl,
162}
163
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
165pub enum ComponentState {
166 #[default]
167 Default,
168 Hover,
169 Active,
170 Focus,
171 Disabled,
172 Error,
173 Selected,
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
177pub enum ButtonHierarchy {
178 #[default]
179 Primary,
180 SecondaryColor,
181 SecondaryGray,
182 TertiaryColor,
183 TertiaryGray,
184 LinkColor,
185 LinkGray,
186 Destructive,
187}
188
189#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
190pub enum BadgeTone {
191 #[default]
192 Brand,
193 Gray,
194 Success,
195 Warning,
196 Error,
197 Blue,
198 Orange,
199}
200
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
202pub enum CardPattern {
203 Plain,
204 #[default]
205 Raised,
206 Tinted,
207 Elevated,
208}
209
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
211pub enum FeatureIconTone {
212 #[default]
213 Brand,
214 Gray,
215 Blue,
216 Orange,
217}
218
219#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
220pub struct ComponentBorder {
221 pub fill: Fill,
222 pub width: f32,
223}
224
225#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
226pub struct ComponentMotion {
227 pub duration_ms: u64,
228 pub easing: EasingCurve,
229}
230
231#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
232pub struct ResolvedComponentStyle {
233 pub background: Option<Fill>,
234 pub text_color: Option<Color>,
235 pub border: Option<ComponentBorder>,
236 pub radius: Option<f32>,
237 pub height: Option<f32>,
238 pub width: Option<f32>,
239 pub padding_x: Option<f32>,
240 pub padding_y: Option<f32>,
241 pub padding: Option<[f32; 4]>,
242 pub gap: Option<f32>,
243 pub font_size: Option<f32>,
244 pub font_weight: Option<u16>,
245 pub line_height: Option<f32>,
246 pub letter_spacing: Option<f32>,
247 pub icon_size: Option<f32>,
248 pub max_width: Option<f32>,
249 pub shadows: Vec<ShadowLayer>,
250 pub transition: Option<ComponentMotion>,
251}
252
253impl ResolvedComponentStyle {
254 pub fn merge(&self, overlay: &Self) -> Self {
255 Self {
256 background: overlay
257 .background
258 .clone()
259 .or_else(|| self.background.clone()),
260 text_color: overlay.text_color.or(self.text_color),
261 border: overlay.border.clone().or_else(|| self.border.clone()),
262 radius: overlay.radius.or(self.radius),
263 height: overlay.height.or(self.height),
264 width: overlay.width.or(self.width),
265 padding_x: overlay.padding_x.or(self.padding_x),
266 padding_y: overlay.padding_y.or(self.padding_y),
267 padding: overlay.padding.or(self.padding),
268 gap: overlay.gap.or(self.gap),
269 font_size: overlay.font_size.or(self.font_size),
270 font_weight: overlay.font_weight.or(self.font_weight),
271 line_height: overlay.line_height.or(self.line_height),
272 letter_spacing: overlay.letter_spacing.or(self.letter_spacing),
273 icon_size: overlay.icon_size.or(self.icon_size),
274 max_width: overlay.max_width.or(self.max_width),
275 shadows: if overlay.shadows.is_empty() {
276 self.shadows.clone()
277 } else {
278 overlay.shadows.clone()
279 },
280 transition: overlay
281 .transition
282 .clone()
283 .or_else(|| self.transition.clone()),
284 }
285 }
286
287 pub fn padding_box(&self, fallback_x: f32, fallback_y: f32) -> [f32; 4] {
288 self.padding.unwrap_or([
289 self.padding_x.unwrap_or(fallback_x),
290 self.padding_x.unwrap_or(fallback_x),
291 self.padding_y.unwrap_or(fallback_y),
292 self.padding_y.unwrap_or(fallback_y),
293 ])
294 }
295
296 pub fn outer_shadows(&self) -> Vec<BoxShadow> {
297 self.shadows
298 .iter()
299 .filter(|layer| !layer.inset)
300 .map(ShadowLayer::to_box_shadow)
301 .collect()
302 }
303
304 pub fn inset_border(&self) -> Option<ComponentBorder> {
305 self.shadows
306 .iter()
307 .find(|layer| layer.inset && layer.spread_radius > 0.0)
308 .map(|layer| ComponentBorder {
309 fill: Fill::Solid(layer.color),
310 width: layer.spread_radius,
311 })
312 }
313}
314
315#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
316pub struct ComponentStateStyles {
317 pub default: ResolvedComponentStyle,
318 pub hover: Option<ResolvedComponentStyle>,
319 pub active: Option<ResolvedComponentStyle>,
320 pub focus: Option<ResolvedComponentStyle>,
321 pub disabled: Option<ResolvedComponentStyle>,
322 pub error: Option<ResolvedComponentStyle>,
323 pub selected: Option<ResolvedComponentStyle>,
324}
325
326impl ComponentStateStyles {
327 pub fn resolve(&self, state: ComponentState) -> ResolvedComponentStyle {
328 let overlay = match state {
329 ComponentState::Default => None,
330 ComponentState::Hover => self.hover.as_ref(),
331 ComponentState::Active => self.active.as_ref(),
332 ComponentState::Focus => self.focus.as_ref(),
333 ComponentState::Disabled => self.disabled.as_ref(),
334 ComponentState::Error => self.error.as_ref(),
335 ComponentState::Selected => self.selected.as_ref(),
336 };
337 overlay
338 .map(|style| self.default.merge(style))
339 .unwrap_or_else(|| self.default.clone())
340 }
341}
342
343#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
352pub struct ColorTokens {
353 pub primary: Color,
354 pub on_primary: Color,
355 pub primary_hover: Color,
356 pub primary_subtle: Color,
357 pub secondary: Color,
358 pub on_secondary: Color,
359 pub surface: Color,
360 pub on_surface: Color,
361 pub surface_raised: Color,
362 pub surface_sunken: Color,
363 pub background: Color,
364 pub on_background: Color,
365 pub error: Color,
366 pub on_error: Color,
367 pub success: Color,
368 pub warning: Color,
369 pub info: Color,
370 pub border: Color,
371 pub border_strong: Color,
372 pub divider: Color,
373 pub text_primary: Color,
374 pub text_secondary: Color,
375 pub text_muted: Color,
376 pub text_link: Color,
377 pub heading: Color,
378 pub focus_ring: Color,
379}
380
381impl Default for ColorTokens {
382 fn default() -> Self {
383 Self {
384 primary: Color {
385 r: 103,
386 g: 85,
387 b: 143,
388 a: 255,
389 }, on_primary: Color::WHITE,
391 primary_hover: Color {
392 r: 80,
393 g: 63,
394 b: 118,
395 a: 255,
396 },
397 primary_subtle: Color {
398 r: 244,
399 g: 239,
400 b: 255,
401 a: 255,
402 },
403 secondary: Color {
404 r: 98,
405 g: 91,
406 b: 113,
407 a: 255,
408 },
409 on_secondary: Color::WHITE,
410 surface: Color {
411 r: 255,
412 g: 251,
413 b: 254,
414 a: 255,
415 },
416 on_surface: Color {
417 r: 28,
418 g: 27,
419 b: 31,
420 a: 255,
421 },
422 surface_raised: Color {
423 r: 255,
424 g: 255,
425 b: 255,
426 a: 255,
427 },
428 surface_sunken: Color {
429 r: 248,
430 g: 248,
431 b: 248,
432 a: 255,
433 },
434 background: Color {
435 r: 255,
436 g: 251,
437 b: 254,
438 a: 255,
439 },
440 on_background: Color {
441 r: 28,
442 g: 27,
443 b: 31,
444 a: 255,
445 },
446 error: Color {
447 r: 179,
448 g: 38,
449 b: 30,
450 a: 255,
451 },
452 on_error: Color::WHITE,
453 success: Color {
454 r: 16,
455 g: 185,
456 b: 129,
457 a: 255,
458 },
459 warning: Color {
460 r: 245,
461 g: 158,
462 b: 11,
463 a: 255,
464 },
465 info: Color {
466 r: 14,
467 g: 165,
468 b: 233,
469 a: 255,
470 },
471 border: Color {
472 r: 188,
473 g: 188,
474 b: 188,
475 a: 255,
476 },
477 border_strong: Color {
478 r: 148,
479 g: 148,
480 b: 148,
481 a: 255,
482 },
483 divider: Color {
484 r: 188,
485 g: 188,
486 b: 188,
487 a: 255,
488 },
489 text_primary: Color {
490 r: 28,
491 g: 27,
492 b: 31,
493 a: 255,
494 },
495 text_secondary: Color {
496 r: 86,
497 g: 86,
498 b: 86,
499 a: 255,
500 },
501 text_muted: Color {
502 r: 120,
503 g: 120,
504 b: 120,
505 a: 255,
506 },
507 text_link: Color {
508 r: 103,
509 g: 85,
510 b: 143,
511 a: 255,
512 },
513 heading: Color {
514 r: 28,
515 g: 27,
516 b: 31,
517 a: 255,
518 },
519 focus_ring: Color {
520 r: 103,
521 g: 85,
522 b: 143,
523 a: 255,
524 },
525 }
526 }
527}
528
529impl ColorTokens {
530 pub fn dark() -> Self {
531 Self {
532 primary: Color {
533 r: 187,
534 g: 134,
535 b: 252,
536 a: 255,
537 },
538 on_primary: Color {
539 r: 0,
540 g: 0,
541 b: 0,
542 a: 255,
543 },
544 primary_hover: Color {
545 r: 210,
546 g: 178,
547 b: 255,
548 a: 255,
549 },
550 primary_subtle: Color {
551 r: 55,
552 g: 36,
553 b: 86,
554 a: 255,
555 },
556 secondary: Color {
557 r: 3,
558 g: 218,
559 b: 197,
560 a: 255,
561 },
562 on_secondary: Color {
563 r: 0,
564 g: 0,
565 b: 0,
566 a: 255,
567 },
568 surface: Color {
569 r: 30,
570 g: 30,
571 b: 30,
572 a: 255,
573 },
574 on_surface: Color {
575 r: 230,
576 g: 230,
577 b: 230,
578 a: 255,
579 },
580 surface_raised: Color {
581 r: 37,
582 g: 37,
583 b: 37,
584 a: 255,
585 },
586 surface_sunken: Color {
587 r: 12,
588 g: 12,
589 b: 12,
590 a: 255,
591 },
592 background: Color {
593 r: 18,
594 g: 18,
595 b: 18,
596 a: 255,
597 },
598 on_background: Color {
599 r: 230,
600 g: 230,
601 b: 230,
602 a: 255,
603 },
604 error: Color {
605 r: 207,
606 g: 102,
607 b: 121,
608 a: 255,
609 },
610 on_error: Color {
611 r: 0,
612 g: 0,
613 b: 0,
614 a: 255,
615 },
616 success: Color {
617 r: 16,
618 g: 185,
619 b: 129,
620 a: 255,
621 },
622 warning: Color {
623 r: 245,
624 g: 158,
625 b: 11,
626 a: 255,
627 },
628 info: Color {
629 r: 14,
630 g: 165,
631 b: 233,
632 a: 255,
633 },
634 border: Color {
635 r: 60,
636 g: 60,
637 b: 60,
638 a: 255,
639 },
640 border_strong: Color {
641 r: 96,
642 g: 96,
643 b: 96,
644 a: 255,
645 },
646 divider: Color {
647 r: 60,
648 g: 60,
649 b: 60,
650 a: 255,
651 },
652 text_primary: Color {
653 r: 230,
654 g: 230,
655 b: 230,
656 a: 255,
657 },
658 text_secondary: Color {
659 r: 160,
660 g: 160,
661 b: 160,
662 a: 255,
663 },
664 text_muted: Color {
665 r: 120,
666 g: 120,
667 b: 120,
668 a: 255,
669 },
670 text_link: Color {
671 r: 187,
672 g: 134,
673 b: 252,
674 a: 255,
675 },
676 heading: Color {
677 r: 230,
678 g: 230,
679 b: 230,
680 a: 255,
681 },
682 focus_ring: Color {
683 r: 187,
684 g: 134,
685 b: 252,
686 a: 255,
687 },
688 }
689 }
690}
691
692#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
696pub struct SpacingTokens {
697 pub none: f32, pub xs: f32, pub s: f32, pub m: f32, pub l: f32, pub xl: f32, pub xxl: f32, pub xxxl: f32, pub xxxxl: f32, }
707
708impl Default for SpacingTokens {
709 fn default() -> Self {
710 Self {
711 none: 0.0,
712 xs: 4.0,
713 s: 8.0,
714 m: 16.0,
715 l: 24.0,
716 xl: 32.0,
717 xxl: 48.0,
718 xxxl: 64.0,
719 xxxxl: 96.0,
720 }
721 }
722}
723
724#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
729pub struct TypographyTokens {
730 pub font_family_sans: String,
731 pub font_family_serif: String,
732 pub font_family_mono: String,
733 pub font_weight_regular: u16,
734 pub font_weight_medium: u16,
735 pub font_weight_semibold: u16,
736 pub font_weight_bold: u16,
737 pub font_size_xs: f32,
738 pub font_size_sm: f32,
739 pub font_size_base: f32,
740 pub label_large_size: f32,
741 pub body_medium_size: f32,
742 pub body_large_size: f32,
743 pub font_size_lg: f32,
744 pub font_size_xl: f32,
745 pub heading_size: f32,
746 pub heading2_size: f32,
747 pub heading1_size: f32,
748 pub display_sm_size: f32,
749 pub display_md_size: f32,
750 pub line_height_display: f32,
751 pub line_height_heading: f32,
752 pub line_height_snug: f32,
753 pub line_height_normal: f32,
754 pub line_height_relaxed: f32,
755 pub letter_spacing_tight: f32,
756 pub letter_spacing_normal: f32,
757 pub letter_spacing_label: f32,
758 pub letter_spacing_kicker: f32,
759}
760
761impl Default for TypographyTokens {
762 fn default() -> Self {
763 Self {
764 font_family_sans: "\"Inter\", \"Avenir Next\", \"Segoe UI\", Arial, sans-serif".into(),
765 font_family_serif: "\"Iowan Old Style\", \"Palatino Linotype\", \"Book Antiqua\", Georgia, serif".into(),
766 font_family_mono: "\"SFMono-Regular\", Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace".into(),
767 font_weight_regular: 400,
768 font_weight_medium: 500,
769 font_weight_semibold: 600,
770 font_weight_bold: 700,
771 font_size_xs: 12.0,
772 font_size_sm: 13.0,
773 font_size_base: 14.0,
774 label_large_size: 15.0,
775 body_medium_size: 15.0,
776 body_large_size: 17.0,
777 font_size_lg: 20.0,
778 font_size_xl: 24.0,
779 heading_size: 28.0,
780 heading2_size: 36.0,
781 heading1_size: 48.0,
782 display_sm_size: 60.0,
783 display_md_size: 72.0,
784 line_height_display: 0.98,
785 line_height_heading: 1.05,
786 line_height_snug: 1.4,
787 line_height_normal: 1.6,
788 line_height_relaxed: 1.68,
789 letter_spacing_tight: -0.01,
790 letter_spacing_normal: 0.0,
791 letter_spacing_label: 0.1,
792 letter_spacing_kicker: 0.14,
793 }
794 }
795}
796
797#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
801pub struct RadiusTokens {
802 pub none: f32,
803 pub small: f32,
804 pub medium: f32,
805 pub large: f32,
806 pub xl: f32,
807 pub xxl: f32,
808 pub full: f32,
809}
810
811impl Default for RadiusTokens {
812 fn default() -> Self {
813 Self {
814 none: 0.0,
815 small: 4.0,
816 medium: 8.0,
817 large: 12.0,
818 xl: 16.0,
819 xxl: 24.0,
820 full: 9999.0,
821 }
822 }
823}
824
825#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
830pub struct ElevationTokens {
831 pub level0: Option<BoxShadow>,
832 pub level1: Option<BoxShadow>,
833 pub level2: Option<BoxShadow>,
834 pub level3: Option<BoxShadow>,
835 pub level4: Option<BoxShadow>,
836 pub level5: Option<BoxShadow>,
837 pub focus: Option<BoxShadow>,
838}
839
840impl Default for ElevationTokens {
841 fn default() -> Self {
842 let black_alpha = |a| Color {
843 r: 0,
844 g: 0,
845 b: 0,
846 a,
847 };
848 Self {
849 level0: None,
850 level1: Some(BoxShadow {
851 color: black_alpha(40),
852 offset: (0.0, 1.0),
853 blur_radius: 2.0,
854 }),
855 level2: Some(BoxShadow {
856 color: black_alpha(60),
857 offset: (0.0, 2.0),
858 blur_radius: 4.0,
859 }),
860 level3: Some(BoxShadow {
861 color: black_alpha(60),
862 offset: (0.0, 4.0),
863 blur_radius: 8.0,
864 }),
865 level4: None,
866 level5: None,
867 focus: Some(BoxShadow {
868 color: Color {
869 r: 20,
870 g: 184,
871 b: 166,
872 a: 82,
873 },
874 offset: (0.0, 0.0),
875 blur_radius: 0.0,
876 }),
877 }
878 }
879}
880
881#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
882pub struct MotionTokens {
883 pub duration_instant_ms: u64,
884 pub duration_micro_ms: u64,
885 pub duration_fast_ms: u64,
886 pub duration_normal_ms: u64,
887 pub duration_slow_ms: u64,
888 pub duration_deliberate_ms: u64,
889 pub easing_linear: EasingCurve,
890 pub easing_standard: EasingCurve,
891 pub easing_in: EasingCurve,
892 pub easing_out: EasingCurve,
893 pub easing_ease: EasingCurve,
894}
895
896impl Default for MotionTokens {
897 fn default() -> Self {
898 Self {
899 duration_instant_ms: 0,
900 duration_micro_ms: 120,
901 duration_fast_ms: 160,
902 duration_normal_ms: 200,
903 duration_slow_ms: 300,
904 duration_deliberate_ms: 480,
905 easing_linear: EasingCurve::Linear,
906 easing_standard: EasingCurve::CubicBezier(0.16, 0.84, 0.32, 1.0),
907 easing_in: EasingCurve::CubicBezier(0.4, 0.0, 1.0, 1.0),
908 easing_out: EasingCurve::CubicBezier(0.0, 0.0, 0.2, 1.0),
909 easing_ease: EasingCurve::Ease,
910 }
911 }
912}
913
914#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
915pub struct DataVisualizationTokens {
916 pub palette: Vec<Color>,
917}
918
919impl Default for DataVisualizationTokens {
920 fn default() -> Self {
921 Self {
922 palette: vec![
923 Color {
924 r: 20,
925 g: 184,
926 b: 166,
927 a: 255,
928 },
929 Color {
930 r: 77,
931 g: 166,
932 b: 224,
933 a: 255,
934 },
935 Color {
936 r: 245,
937 g: 158,
938 b: 11,
939 a: 255,
940 },
941 Color {
942 r: 244,
943 g: 63,
944 b: 94,
945 a: 255,
946 },
947 Color {
948 r: 132,
949 g: 204,
950 b: 22,
951 a: 255,
952 },
953 Color {
954 r: 14,
955 g: 165,
956 b: 233,
957 a: 255,
958 },
959 Color {
960 r: 168,
961 g: 85,
962 b: 247,
963 a: 255,
964 },
965 Color {
966 r: 249,
967 g: 115,
968 b: 22,
969 a: 255,
970 },
971 ],
972 }
973 }
974}
975
976#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
982pub struct Tokens {
983 pub colors: ColorTokens,
984 pub spacing: SpacingTokens,
985 pub typography: TypographyTokens,
986 pub radii: RadiusTokens,
987 pub elevations: ElevationTokens,
988 pub motion: MotionTokens,
989 pub data_visualization: DataVisualizationTokens,
990}
991
992impl Tokens {
993 pub fn dark() -> Self {
994 Self {
995 colors: ColorTokens::dark(),
996 spacing: SpacingTokens::default(),
997 typography: TypographyTokens::default(),
998 radii: RadiusTokens::default(),
999 elevations: ElevationTokens::default(),
1000 motion: MotionTokens::default(),
1001 data_visualization: DataVisualizationTokens::default(),
1002 }
1003 }
1004}
1005
1006#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1013pub struct ButtonTheme {
1014 pub height: f32,
1015 pub padding_horizontal: f32,
1016 pub padding_vertical: f32,
1017 pub radius: f32,
1018 pub text_size: f32,
1019 pub elevation_rest: Option<BoxShadow>,
1020 pub elevation_hover: Option<BoxShadow>,
1021 pub elevation_pressed: Option<BoxShadow>,
1022 pub focus_stroke: Option<Stroke>,
1023 pub icon_size: f32,
1024 pub font_weight: u16,
1025 pub line_height: f32,
1026 pub transition: Option<ComponentMotion>,
1027 pub sizes: Vec<(ComponentSize, ResolvedComponentStyle)>,
1028 pub hierarchies: Vec<(ButtonHierarchy, ComponentStateStyles)>,
1029}
1030
1031impl ButtonTheme {
1032 pub fn from_tokens(tokens: &Tokens) -> Self {
1033 let transition = Some(ComponentMotion {
1034 duration_ms: tokens.motion.duration_fast_ms,
1035 easing: tokens.motion.easing_standard.clone(),
1036 });
1037 let size_md = ResolvedComponentStyle {
1038 height: Some(40.0),
1039 padding_x: Some(14.0),
1040 padding_y: Some(tokens.spacing.s),
1041 gap: Some(4.0),
1042 font_size: Some(tokens.typography.label_large_size),
1043 font_weight: Some(tokens.typography.font_weight_semibold),
1044 line_height: Some(20.0),
1045 icon_size: Some(20.0),
1046 ..ResolvedComponentStyle::default()
1047 };
1048 let primary = ComponentStateStyles {
1049 default: ResolvedComponentStyle {
1050 background: Some(Fill::Solid(tokens.colors.primary)),
1051 text_color: Some(tokens.colors.on_primary),
1052 border: None,
1053 shadows: tokens
1054 .elevations
1055 .level1
1056 .map(shadow_layer_from_box)
1057 .into_iter()
1058 .collect(),
1059 transition: transition.clone(),
1060 ..ResolvedComponentStyle::default()
1061 },
1062 hover: Some(ResolvedComponentStyle {
1063 background: Some(Fill::Solid(tokens.colors.primary_hover)),
1064 shadows: tokens
1065 .elevations
1066 .level2
1067 .map(shadow_layer_from_box)
1068 .into_iter()
1069 .collect(),
1070 ..ResolvedComponentStyle::default()
1071 }),
1072 active: Some(ResolvedComponentStyle {
1073 shadows: tokens
1074 .elevations
1075 .level0
1076 .map(shadow_layer_from_box)
1077 .into_iter()
1078 .collect(),
1079 ..ResolvedComponentStyle::default()
1080 }),
1081 focus: Some(ResolvedComponentStyle {
1082 shadows: tokens
1083 .elevations
1084 .focus
1085 .map(shadow_layer_from_box)
1086 .into_iter()
1087 .collect(),
1088 ..ResolvedComponentStyle::default()
1089 }),
1090 disabled: Some(ResolvedComponentStyle {
1091 background: Some(Fill::Solid(tokens.colors.border)),
1092 text_color: Some(tokens.colors.text_secondary),
1093 shadows: Vec::new(),
1094 ..ResolvedComponentStyle::default()
1095 }),
1096 ..ComponentStateStyles::default()
1097 };
1098 let secondary_gray = ComponentStateStyles {
1099 default: ResolvedComponentStyle {
1100 background: Some(Fill::Solid(tokens.colors.surface)),
1101 text_color: Some(tokens.colors.text_primary),
1102 border: Some(ComponentBorder {
1103 fill: Fill::Solid(tokens.colors.border),
1104 width: 1.0,
1105 }),
1106 transition: transition.clone(),
1107 ..ResolvedComponentStyle::default()
1108 },
1109 hover: Some(ResolvedComponentStyle {
1110 background: Some(Fill::Solid(tokens.colors.surface_sunken)),
1111 ..ResolvedComponentStyle::default()
1112 }),
1113 disabled: Some(ResolvedComponentStyle {
1114 text_color: Some(tokens.colors.text_secondary),
1115 border: Some(ComponentBorder {
1116 fill: Fill::Solid(tokens.colors.border),
1117 width: 1.0,
1118 }),
1119 ..ResolvedComponentStyle::default()
1120 }),
1121 ..ComponentStateStyles::default()
1122 };
1123 let tertiary_gray = ComponentStateStyles {
1124 default: ResolvedComponentStyle {
1125 background: None,
1126 text_color: Some(tokens.colors.primary),
1127 border: None,
1128 transition,
1129 ..ResolvedComponentStyle::default()
1130 },
1131 hover: Some(ResolvedComponentStyle {
1132 background: Some(Fill::Solid(tokens.colors.surface_sunken)),
1133 ..ResolvedComponentStyle::default()
1134 }),
1135 disabled: Some(ResolvedComponentStyle {
1136 text_color: Some(tokens.colors.text_secondary),
1137 ..ResolvedComponentStyle::default()
1138 }),
1139 ..ComponentStateStyles::default()
1140 };
1141 Self {
1142 height: 42.0,
1143 padding_horizontal: tokens.spacing.m,
1144 padding_vertical: tokens.spacing.s,
1145 radius: tokens.radii.full,
1146 text_size: tokens.typography.label_large_size,
1147 elevation_rest: tokens.elevations.level1,
1148 elevation_hover: tokens.elevations.level2,
1149 elevation_pressed: tokens.elevations.level0,
1150 focus_stroke: Some(Stroke {
1151 fill: fission_ir::op::Fill::Solid(tokens.colors.on_background),
1152 width: 1.0,
1153 dash_array: None,
1154 line_cap: fission_ir::op::LineCap::Butt,
1155 line_join: fission_ir::op::LineJoin::Miter,
1156 }),
1157 icon_size: 20.0,
1158 font_weight: tokens.typography.font_weight_semibold,
1159 line_height: 20.0,
1160 transition: Some(ComponentMotion {
1161 duration_ms: tokens.motion.duration_fast_ms,
1162 easing: tokens.motion.easing_standard.clone(),
1163 }),
1164 sizes: vec![
1165 (
1166 ComponentSize::Sm,
1167 ResolvedComponentStyle {
1168 height: Some(36.0),
1169 padding_x: Some(12.0),
1170 padding_y: Some(tokens.spacing.xs),
1171 gap: Some(4.0),
1172 font_size: Some(tokens.typography.font_size_sm),
1173 line_height: Some(20.0),
1174 icon_size: Some(18.0),
1175 ..ResolvedComponentStyle::default()
1176 },
1177 ),
1178 (ComponentSize::Md, size_md),
1179 (
1180 ComponentSize::Lg,
1181 ResolvedComponentStyle {
1182 height: Some(44.0),
1183 padding_x: Some(16.0),
1184 padding_y: Some(tokens.spacing.s),
1185 gap: Some(6.0),
1186 font_size: Some(tokens.typography.font_size_base),
1187 line_height: Some(24.0),
1188 icon_size: Some(20.0),
1189 ..ResolvedComponentStyle::default()
1190 },
1191 ),
1192 (
1193 ComponentSize::Xl,
1194 ResolvedComponentStyle {
1195 height: Some(48.0),
1196 padding_x: Some(18.0),
1197 padding_y: Some(tokens.spacing.s),
1198 gap: Some(6.0),
1199 font_size: Some(tokens.typography.font_size_base),
1200 line_height: Some(24.0),
1201 icon_size: Some(20.0),
1202 ..ResolvedComponentStyle::default()
1203 },
1204 ),
1205 ],
1206 hierarchies: vec![
1207 (ButtonHierarchy::Primary, primary.clone()),
1208 (ButtonHierarchy::SecondaryColor, secondary_gray.clone()),
1209 (ButtonHierarchy::SecondaryGray, secondary_gray),
1210 (ButtonHierarchy::TertiaryColor, tertiary_gray.clone()),
1211 (ButtonHierarchy::TertiaryGray, tertiary_gray.clone()),
1212 (ButtonHierarchy::LinkColor, tertiary_gray.clone()),
1213 (ButtonHierarchy::LinkGray, tertiary_gray.clone()),
1214 (
1215 ButtonHierarchy::Destructive,
1216 ComponentStateStyles {
1217 default: ResolvedComponentStyle {
1218 background: Some(Fill::Solid(tokens.colors.error)),
1219 text_color: Some(tokens.colors.on_error),
1220 ..primary.default.clone()
1221 },
1222 hover: Some(ResolvedComponentStyle {
1223 background: Some(Fill::Solid(tokens.colors.error.with_alpha(230))),
1224 ..ResolvedComponentStyle::default()
1225 }),
1226 ..primary
1227 },
1228 ),
1229 ],
1230 }
1231 }
1232
1233 pub fn size_style(&self, size: ComponentSize) -> ResolvedComponentStyle {
1234 self.sizes
1235 .iter()
1236 .find(|(candidate, _)| *candidate == size)
1237 .map(|(_, style)| style.clone())
1238 .or_else(|| {
1239 self.sizes
1240 .iter()
1241 .find(|(candidate, _)| *candidate == ComponentSize::Md)
1242 .map(|(_, style)| style.clone())
1243 })
1244 .unwrap_or_else(|| ResolvedComponentStyle {
1245 height: Some(self.height),
1246 padding_x: Some(self.padding_horizontal),
1247 padding_y: Some(self.padding_vertical),
1248 radius: Some(self.radius),
1249 font_size: Some(self.text_size),
1250 font_weight: Some(self.font_weight),
1251 line_height: Some(self.line_height),
1252 icon_size: Some(self.icon_size),
1253 ..ResolvedComponentStyle::default()
1254 })
1255 }
1256
1257 pub fn hierarchy_style(&self, hierarchy: ButtonHierarchy) -> ComponentStateStyles {
1258 self.hierarchies
1259 .iter()
1260 .find(|(candidate, _)| *candidate == hierarchy)
1261 .map(|(_, styles)| styles.clone())
1262 .or_else(|| {
1263 self.hierarchies
1264 .iter()
1265 .find(|(candidate, _)| *candidate == ButtonHierarchy::Primary)
1266 .map(|(_, styles)| styles.clone())
1267 })
1268 .unwrap_or_default()
1269 }
1270
1271 pub fn resolve(
1272 &self,
1273 hierarchy: ButtonHierarchy,
1274 size: ComponentSize,
1275 state: ComponentState,
1276 ) -> ResolvedComponentStyle {
1277 let base = ResolvedComponentStyle {
1278 height: Some(self.height),
1279 padding_x: Some(self.padding_horizontal),
1280 padding_y: Some(self.padding_vertical),
1281 radius: Some(self.radius),
1282 font_size: Some(self.text_size),
1283 font_weight: Some(self.font_weight),
1284 line_height: Some(self.line_height),
1285 icon_size: Some(self.icon_size),
1286 transition: self.transition.clone(),
1287 ..ResolvedComponentStyle::default()
1288 };
1289 base.merge(&self.size_style(size))
1290 .merge(&self.hierarchy_style(hierarchy).resolve(state))
1291 }
1292}
1293
1294#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1299pub struct TextInputTheme {
1300 pub height: f32,
1301 pub padding_h: f32,
1302 pub radius: f32,
1303 pub font_size: f32,
1304 pub border_color: Color,
1305 pub border_width: f32,
1306 pub focus_color: Color,
1307 pub text_color: Color,
1308 pub placeholder_color: Color,
1309 pub line_height: f32,
1310 pub font_weight: u16,
1311 pub sizes: Vec<(ComponentSize, ResolvedComponentStyle)>,
1312 pub states: ComponentStateStyles,
1313 pub placeholder_style: ResolvedComponentStyle,
1314 pub label_style: ResolvedComponentStyle,
1315 pub helper_style: ResolvedComponentStyle,
1316}
1317
1318impl TextInputTheme {
1319 pub fn from_tokens(tokens: &Tokens) -> Self {
1320 Self {
1321 height: 40.0,
1322 padding_h: tokens.spacing.m,
1323 radius: tokens.radii.small,
1324 font_size: tokens.typography.body_large_size,
1325 border_color: tokens.colors.border,
1326 border_width: 1.0,
1327 focus_color: tokens.colors.primary,
1328 text_color: tokens.colors.text_primary,
1329 placeholder_color: tokens.colors.text_secondary,
1330 line_height: 24.0,
1331 font_weight: tokens.typography.font_weight_regular,
1332 sizes: vec![
1333 (
1334 ComponentSize::Sm,
1335 ResolvedComponentStyle {
1336 height: Some(36.0),
1337 padding_x: Some(12.0),
1338 ..ResolvedComponentStyle::default()
1339 },
1340 ),
1341 (
1342 ComponentSize::Md,
1343 ResolvedComponentStyle {
1344 height: Some(40.0),
1345 padding_x: Some(12.0),
1346 ..ResolvedComponentStyle::default()
1347 },
1348 ),
1349 ],
1350 states: ComponentStateStyles {
1351 default: ResolvedComponentStyle {
1352 background: Some(Fill::Solid(tokens.colors.surface)),
1353 text_color: Some(tokens.colors.text_primary),
1354 border: Some(ComponentBorder {
1355 fill: Fill::Solid(tokens.colors.border),
1356 width: 1.0,
1357 }),
1358 shadows: tokens
1359 .elevations
1360 .level1
1361 .map(shadow_layer_from_box)
1362 .into_iter()
1363 .collect(),
1364 ..ResolvedComponentStyle::default()
1365 },
1366 focus: Some(ResolvedComponentStyle {
1367 border: Some(ComponentBorder {
1368 fill: Fill::Solid(tokens.colors.focus_ring),
1369 width: 2.0,
1370 }),
1371 shadows: tokens
1372 .elevations
1373 .focus
1374 .map(shadow_layer_from_box)
1375 .into_iter()
1376 .collect(),
1377 padding_x: Some(11.0),
1378 ..ResolvedComponentStyle::default()
1379 }),
1380 error: Some(ResolvedComponentStyle {
1381 border: Some(ComponentBorder {
1382 fill: Fill::Solid(tokens.colors.error),
1383 width: 1.0,
1384 }),
1385 ..ResolvedComponentStyle::default()
1386 }),
1387 disabled: Some(ResolvedComponentStyle {
1388 background: Some(Fill::Solid(tokens.colors.surface_sunken)),
1389 text_color: Some(tokens.colors.text_secondary),
1390 ..ResolvedComponentStyle::default()
1391 }),
1392 ..ComponentStateStyles::default()
1393 },
1394 placeholder_style: ResolvedComponentStyle {
1395 text_color: Some(tokens.colors.text_muted),
1396 ..ResolvedComponentStyle::default()
1397 },
1398 label_style: ResolvedComponentStyle {
1399 font_size: Some(tokens.typography.font_size_base),
1400 font_weight: Some(tokens.typography.font_weight_medium),
1401 text_color: Some(tokens.colors.text_primary),
1402 ..ResolvedComponentStyle::default()
1403 },
1404 helper_style: ResolvedComponentStyle {
1405 font_size: Some(tokens.typography.font_size_base),
1406 text_color: Some(tokens.colors.text_muted),
1407 ..ResolvedComponentStyle::default()
1408 },
1409 }
1410 }
1411
1412 pub fn size_style(&self, size: ComponentSize) -> ResolvedComponentStyle {
1413 self.sizes
1414 .iter()
1415 .find(|(candidate, _)| *candidate == size)
1416 .map(|(_, style)| style.clone())
1417 .or_else(|| {
1418 self.sizes
1419 .iter()
1420 .find(|(candidate, _)| *candidate == ComponentSize::Md)
1421 .map(|(_, style)| style.clone())
1422 })
1423 .unwrap_or_else(|| ResolvedComponentStyle {
1424 height: Some(self.height),
1425 padding_x: Some(self.padding_h),
1426 ..ResolvedComponentStyle::default()
1427 })
1428 }
1429
1430 pub fn resolve(&self, size: ComponentSize, state: ComponentState) -> ResolvedComponentStyle {
1431 let base = ResolvedComponentStyle {
1432 height: Some(self.height),
1433 padding_x: Some(self.padding_h),
1434 radius: Some(self.radius),
1435 font_size: Some(self.font_size),
1436 line_height: Some(self.line_height),
1437 font_weight: Some(self.font_weight),
1438 text_color: Some(self.text_color),
1439 border: Some(ComponentBorder {
1440 fill: Fill::Solid(self.border_color),
1441 width: self.border_width,
1442 }),
1443 ..ResolvedComponentStyle::default()
1444 };
1445 base.merge(&self.size_style(size))
1446 .merge(&self.states.resolve(state))
1447 }
1448}
1449
1450#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1452pub struct CalendarTheme {
1453 pub bg_color: Color,
1454 pub border_color: Color,
1455 pub radius: f32,
1456 pub selected_bg: Color,
1457 pub selected_text: Color,
1458 pub today_outline: Color,
1459}
1460
1461impl CalendarTheme {
1462 pub fn from_tokens(tokens: &Tokens) -> Self {
1463 Self {
1464 bg_color: tokens.colors.surface,
1465 border_color: tokens.colors.border,
1466 radius: tokens.radii.medium,
1467 selected_bg: tokens.colors.primary,
1468 selected_text: tokens.colors.on_primary,
1469 today_outline: tokens.colors.secondary,
1470 }
1471 }
1472}
1473
1474#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1476pub struct PaginationTheme {
1477 pub spacing: f32,
1478 pub active_bg: Color,
1479 pub active_text: Color,
1480}
1481
1482impl PaginationTheme {
1483 pub fn from_tokens(tokens: &Tokens) -> Self {
1484 Self {
1485 spacing: tokens.spacing.s,
1486 active_bg: tokens.colors.primary,
1487 active_text: tokens.colors.on_primary,
1488 }
1489 }
1490}
1491
1492#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1494pub struct TimelineTheme {
1495 pub dot_size: f32,
1496 pub line_width: f32,
1497 pub dot_color: Color,
1498 pub line_color: Color,
1499}
1500
1501impl TimelineTheme {
1502 pub fn from_tokens(tokens: &Tokens) -> Self {
1503 Self {
1504 dot_size: 12.0,
1505 line_width: 2.0,
1506 dot_color: tokens.colors.primary,
1507 line_color: tokens.colors.border,
1508 }
1509 }
1510}
1511
1512#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1514pub struct SegmentedControlTheme {
1515 pub bg_color: Color,
1516 pub border_color: Color,
1517 pub radius: f32,
1518 pub active_bg: Color,
1519 pub active_text: Color,
1520}
1521
1522impl SegmentedControlTheme {
1523 pub fn from_tokens(tokens: &Tokens) -> Self {
1524 Self {
1525 bg_color: tokens.colors.surface,
1526 border_color: tokens.colors.border,
1527 radius: tokens.radii.full,
1528 active_bg: tokens.colors.primary,
1529 active_text: tokens.colors.on_primary,
1530 }
1531 }
1532}
1533
1534#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1536pub struct AlertTheme {
1537 pub info_bg: Color,
1538 pub warning_bg: Color,
1539 pub error_bg: Color,
1540 pub success_bg: Color,
1541 pub radius: f32,
1542}
1543
1544impl AlertTheme {
1545 pub fn from_tokens(tokens: &Tokens) -> Self {
1546 Self {
1547 info_bg: Color {
1548 r: 230,
1549 g: 242,
1550 b: 255,
1551 a: 255,
1552 },
1553 warning_bg: Color {
1554 r: 255,
1555 g: 244,
1556 b: 229,
1557 a: 255,
1558 },
1559 error_bg: tokens.colors.error.with_alpha(30),
1560 success_bg: Color {
1561 r: 237,
1562 g: 247,
1563 b: 237,
1564 a: 255,
1565 },
1566 radius: tokens.radii.medium,
1567 }
1568 }
1569}
1570
1571#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1573pub struct BadgeTheme {
1574 pub radius: f32,
1575 pub font_size: f32,
1576 pub font_weight: u16,
1577 pub sizes: Vec<(ComponentSize, ResolvedComponentStyle)>,
1578 pub tones: Vec<(BadgeTone, ResolvedComponentStyle)>,
1579}
1580
1581impl BadgeTheme {
1582 pub fn from_tokens(tokens: &Tokens) -> Self {
1583 Self {
1584 radius: tokens.radii.full,
1585 font_size: 10.0,
1586 font_weight: tokens.typography.font_weight_medium,
1587 sizes: vec![
1588 (
1589 ComponentSize::Sm,
1590 ResolvedComponentStyle {
1591 height: Some(20.0),
1592 padding_x: Some(8.0),
1593 font_size: Some(tokens.typography.font_size_xs),
1594 line_height: Some(18.0),
1595 ..ResolvedComponentStyle::default()
1596 },
1597 ),
1598 (
1599 ComponentSize::Md,
1600 ResolvedComponentStyle {
1601 height: Some(24.0),
1602 padding_x: Some(10.0),
1603 font_size: Some(tokens.typography.font_size_base),
1604 line_height: Some(20.0),
1605 ..ResolvedComponentStyle::default()
1606 },
1607 ),
1608 ],
1609 tones: vec![
1610 (
1611 BadgeTone::Brand,
1612 badge_tone(
1613 tokens.colors.primary_subtle,
1614 tokens.colors.primary,
1615 tokens.colors.primary,
1616 ),
1617 ),
1618 (
1619 BadgeTone::Gray,
1620 badge_tone(
1621 tokens.colors.surface_sunken,
1622 tokens.colors.border,
1623 tokens.colors.text_primary,
1624 ),
1625 ),
1626 (
1627 BadgeTone::Success,
1628 badge_tone(
1629 tokens.colors.success.with_alpha(26),
1630 tokens.colors.success.with_alpha(80),
1631 tokens.colors.success,
1632 ),
1633 ),
1634 (
1635 BadgeTone::Warning,
1636 badge_tone(
1637 tokens.colors.warning.with_alpha(26),
1638 tokens.colors.warning.with_alpha(80),
1639 tokens.colors.warning,
1640 ),
1641 ),
1642 (
1643 BadgeTone::Error,
1644 badge_tone(
1645 tokens.colors.error.with_alpha(26),
1646 tokens.colors.error.with_alpha(80),
1647 tokens.colors.error,
1648 ),
1649 ),
1650 (
1651 BadgeTone::Blue,
1652 badge_tone(
1653 tokens.colors.info.with_alpha(26),
1654 tokens.colors.info.with_alpha(80),
1655 tokens.colors.info,
1656 ),
1657 ),
1658 (
1659 BadgeTone::Orange,
1660 badge_tone(
1661 tokens.colors.warning.with_alpha(26),
1662 tokens.colors.warning.with_alpha(80),
1663 tokens.colors.warning,
1664 ),
1665 ),
1666 ],
1667 }
1668 }
1669
1670 pub fn resolve(&self, tone: BadgeTone, size: ComponentSize) -> ResolvedComponentStyle {
1671 let base = ResolvedComponentStyle {
1672 radius: Some(self.radius),
1673 font_size: Some(self.font_size),
1674 font_weight: Some(self.font_weight),
1675 ..ResolvedComponentStyle::default()
1676 };
1677 let size_style = find_size_style(&self.sizes, size);
1678 let tone_style = self
1679 .tones
1680 .iter()
1681 .find(|(candidate, _)| *candidate == tone)
1682 .map(|(_, style)| style.clone())
1683 .or_else(|| {
1684 self.tones
1685 .iter()
1686 .find(|(candidate, _)| *candidate == BadgeTone::Brand)
1687 .map(|(_, style)| style.clone())
1688 })
1689 .unwrap_or_default();
1690 base.merge(&size_style).merge(&tone_style)
1691 }
1692}
1693
1694#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1696pub struct TabsTheme {
1697 pub active_color: Color,
1698 pub inactive_color: Color,
1699 pub indicator_height: f32,
1700 pub background: Color,
1701 pub divider_color: Color,
1702 pub sizes: Vec<(ComponentSize, ResolvedComponentStyle)>,
1703 pub states: ComponentStateStyles,
1704 pub track_style: ResolvedComponentStyle,
1705}
1706
1707impl TabsTheme {
1708 pub fn from_tokens(tokens: &Tokens) -> Self {
1709 Self {
1710 active_color: tokens.colors.primary,
1711 inactive_color: tokens.colors.text_secondary,
1712 indicator_height: 3.0,
1713 background: tokens.colors.background,
1714 divider_color: tokens.colors.border.with_alpha(120),
1715 sizes: vec![
1716 (
1717 ComponentSize::Sm,
1718 ResolvedComponentStyle {
1719 padding_y: Some(10.0),
1720 font_size: Some(tokens.typography.font_size_base),
1721 line_height: Some(20.0),
1722 height: Some(40.0),
1723 ..ResolvedComponentStyle::default()
1724 },
1725 ),
1726 (
1727 ComponentSize::Md,
1728 ResolvedComponentStyle {
1729 padding_y: Some(12.0),
1730 font_size: Some(tokens.typography.font_size_base),
1731 line_height: Some(20.0),
1732 height: Some(44.0),
1733 ..ResolvedComponentStyle::default()
1734 },
1735 ),
1736 ],
1737 states: ComponentStateStyles {
1738 default: ResolvedComponentStyle {
1739 text_color: Some(tokens.colors.text_secondary),
1740 border: Some(ComponentBorder {
1741 fill: Fill::Solid(Color {
1742 r: 0,
1743 g: 0,
1744 b: 0,
1745 a: 0,
1746 }),
1747 width: 2.0,
1748 }),
1749 ..ResolvedComponentStyle::default()
1750 },
1751 hover: Some(ResolvedComponentStyle {
1752 text_color: Some(tokens.colors.text_primary),
1753 ..ResolvedComponentStyle::default()
1754 }),
1755 active: Some(ResolvedComponentStyle {
1756 text_color: Some(tokens.colors.primary),
1757 border: Some(ComponentBorder {
1758 fill: Fill::Solid(tokens.colors.primary),
1759 width: 2.0,
1760 }),
1761 font_weight: Some(tokens.typography.font_weight_semibold),
1762 ..ResolvedComponentStyle::default()
1763 }),
1764 ..ComponentStateStyles::default()
1765 },
1766 track_style: ResolvedComponentStyle {
1767 background: Some(Fill::Solid(tokens.colors.background)),
1768 border: Some(ComponentBorder {
1769 fill: Fill::Solid(tokens.colors.border.with_alpha(120)),
1770 width: 1.0,
1771 }),
1772 ..ResolvedComponentStyle::default()
1773 },
1774 }
1775 }
1776
1777 pub fn resolve_tab(
1778 &self,
1779 size: ComponentSize,
1780 state: ComponentState,
1781 ) -> ResolvedComponentStyle {
1782 find_size_style(&self.sizes, size).merge(&self.states.resolve(state))
1783 }
1784}
1785
1786#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1790pub struct ModalTheme {
1791 pub bg_color: Color,
1792 pub radius: f32,
1793 pub shadow: Option<BoxShadow>,
1794 pub max_width: f32,
1795 pub container_style: ResolvedComponentStyle,
1796 pub scrim_style: ResolvedComponentStyle,
1797 pub scrim_blur: f32,
1798}
1799
1800impl ModalTheme {
1801 pub fn from_tokens(tokens: &Tokens) -> Self {
1802 Self {
1803 bg_color: tokens.colors.surface,
1804 radius: tokens.radii.large,
1805 shadow: tokens.elevations.level3,
1806 max_width: 600.0,
1807 container_style: ResolvedComponentStyle {
1808 background: Some(Fill::Solid(tokens.colors.surface)),
1809 radius: Some(tokens.radii.large),
1810 max_width: Some(600.0),
1811 shadows: tokens
1812 .elevations
1813 .level3
1814 .map(shadow_layer_from_box)
1815 .into_iter()
1816 .collect(),
1817 ..ResolvedComponentStyle::default()
1818 },
1819 scrim_style: ResolvedComponentStyle {
1820 background: Some(Fill::Solid(Color {
1821 r: 15,
1822 g: 23,
1823 b: 42,
1824 a: 153,
1825 })),
1826 ..ResolvedComponentStyle::default()
1827 },
1828 scrim_blur: 4.0,
1829 }
1830 }
1831}
1832
1833#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1835pub struct TreeViewTheme {
1836 pub indent: f32,
1837 pub selected_bg: Color,
1838 pub hover_bg: Color,
1839}
1840
1841impl TreeViewTheme {
1842 pub fn from_tokens(tokens: &Tokens) -> Self {
1843 Self {
1844 indent: 16.0,
1845 selected_bg: tokens.colors.primary.with_alpha(52),
1846 hover_bg: tokens.colors.surface,
1847 }
1848 }
1849}
1850
1851#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1853pub struct ProgressTheme {
1854 pub height: f32,
1855 pub track_color: Color,
1856 pub bar_color: Color,
1857 pub radius: f32,
1858 pub track_style: ResolvedComponentStyle,
1859 pub fill_style: ResolvedComponentStyle,
1860}
1861
1862impl ProgressTheme {
1863 pub fn from_tokens(tokens: &Tokens) -> Self {
1864 Self {
1865 height: 8.0,
1866 track_color: tokens.colors.border,
1867 bar_color: tokens.colors.primary,
1868 radius: tokens.radii.full,
1869 track_style: ResolvedComponentStyle {
1870 height: Some(8.0),
1871 radius: Some(tokens.radii.full),
1872 background: Some(Fill::Solid(tokens.colors.border)),
1873 ..ResolvedComponentStyle::default()
1874 },
1875 fill_style: ResolvedComponentStyle {
1876 height: Some(8.0),
1877 radius: Some(tokens.radii.full),
1878 background: Some(Fill::Solid(tokens.colors.primary)),
1879 ..ResolvedComponentStyle::default()
1880 },
1881 }
1882 }
1883}
1884
1885#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1887pub struct TooltipTheme {
1888 pub bg_color: Color,
1889 pub text_color: Color,
1890 pub radius: f32,
1891 pub font_size: f32,
1892 pub padding_x: f32,
1893 pub padding_y: f32,
1894 pub max_width: f32,
1895 pub style: ResolvedComponentStyle,
1896}
1897
1898impl TooltipTheme {
1899 pub fn from_tokens(tokens: &Tokens) -> Self {
1900 Self {
1901 bg_color: Color {
1902 r: 50,
1903 g: 50,
1904 b: 50,
1905 a: 255,
1906 },
1907 text_color: Color::WHITE,
1908 radius: tokens.radii.small,
1909 font_size: 12.0,
1910 padding_x: 10.0,
1911 padding_y: 8.0,
1912 max_width: 240.0,
1913 style: ResolvedComponentStyle {
1914 background: Some(Fill::Solid(Color {
1915 r: 50,
1916 g: 50,
1917 b: 50,
1918 a: 255,
1919 })),
1920 text_color: Some(Color::WHITE),
1921 radius: Some(tokens.radii.small),
1922 font_size: Some(12.0),
1923 padding_x: Some(10.0),
1924 padding_y: Some(8.0),
1925 max_width: Some(240.0),
1926 shadows: tokens
1927 .elevations
1928 .level2
1929 .map(shadow_layer_from_box)
1930 .into_iter()
1931 .collect(),
1932 ..ResolvedComponentStyle::default()
1933 },
1934 }
1935 }
1936}
1937
1938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1939pub struct CardTheme {
1940 pub padding: f32,
1941 pub radius: f32,
1942 pub default_pattern: CardPattern,
1943 pub patterns: Vec<(CardPattern, ResolvedComponentStyle)>,
1944 pub hover_style: ResolvedComponentStyle,
1945}
1946
1947impl CardTheme {
1948 pub fn from_tokens(tokens: &Tokens) -> Self {
1949 let base_border = ComponentBorder {
1950 fill: Fill::Solid(tokens.colors.border),
1951 width: 1.0,
1952 };
1953 Self {
1954 padding: tokens.spacing.l,
1955 radius: tokens.radii.xl,
1956 default_pattern: CardPattern::Raised,
1957 patterns: vec![
1958 (
1959 CardPattern::Plain,
1960 ResolvedComponentStyle {
1961 background: Some(Fill::Solid(tokens.colors.surface)),
1962 border: Some(base_border.clone()),
1963 radius: Some(tokens.radii.xl),
1964 padding_x: Some(tokens.spacing.l),
1965 padding_y: Some(tokens.spacing.l),
1966 ..ResolvedComponentStyle::default()
1967 },
1968 ),
1969 (
1970 CardPattern::Raised,
1971 ResolvedComponentStyle {
1972 background: Some(Fill::Solid(tokens.colors.surface)),
1973 border: Some(base_border.clone()),
1974 radius: Some(tokens.radii.xl),
1975 padding_x: Some(tokens.spacing.l),
1976 padding_y: Some(tokens.spacing.l),
1977 shadows: tokens
1978 .elevations
1979 .level2
1980 .map(shadow_layer_from_box)
1981 .into_iter()
1982 .collect(),
1983 ..ResolvedComponentStyle::default()
1984 },
1985 ),
1986 (
1987 CardPattern::Tinted,
1988 ResolvedComponentStyle {
1989 background: Some(Fill::Solid(tokens.colors.primary_subtle)),
1990 border: Some(ComponentBorder {
1991 fill: Fill::Solid(tokens.colors.primary.with_alpha(80)),
1992 width: 1.0,
1993 }),
1994 radius: Some(tokens.radii.xl),
1995 padding_x: Some(tokens.spacing.l),
1996 padding_y: Some(tokens.spacing.l),
1997 ..ResolvedComponentStyle::default()
1998 },
1999 ),
2000 (
2001 CardPattern::Elevated,
2002 ResolvedComponentStyle {
2003 background: Some(Fill::Solid(tokens.colors.surface)),
2004 border: Some(base_border),
2005 radius: Some(tokens.radii.xl),
2006 padding_x: Some(tokens.spacing.l),
2007 padding_y: Some(tokens.spacing.l),
2008 shadows: tokens
2009 .elevations
2010 .level1
2011 .map(shadow_layer_from_box)
2012 .into_iter()
2013 .collect(),
2014 ..ResolvedComponentStyle::default()
2015 },
2016 ),
2017 ],
2018 hover_style: ResolvedComponentStyle {
2019 shadows: tokens
2020 .elevations
2021 .level2
2022 .map(shadow_layer_from_box)
2023 .into_iter()
2024 .collect(),
2025 ..ResolvedComponentStyle::default()
2026 },
2027 }
2028 }
2029
2030 pub fn resolve(&self, pattern: CardPattern, hovered: bool) -> ResolvedComponentStyle {
2031 let base = self
2032 .patterns
2033 .iter()
2034 .find(|(candidate, _)| *candidate == pattern)
2035 .map(|(_, style)| style.clone())
2036 .or_else(|| {
2037 self.patterns
2038 .iter()
2039 .find(|(candidate, _)| *candidate == self.default_pattern)
2040 .map(|(_, style)| style.clone())
2041 })
2042 .unwrap_or_default();
2043 if hovered {
2044 base.merge(&self.hover_style)
2045 } else {
2046 base
2047 }
2048 }
2049}
2050
2051#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2052pub struct FeatureIconTheme {
2053 pub sizes: Vec<(ComponentSize, ResolvedComponentStyle)>,
2054 pub tones: Vec<(FeatureIconTone, ResolvedComponentStyle)>,
2055 pub shadow: Option<BoxShadow>,
2056}
2057
2058impl FeatureIconTheme {
2059 pub fn from_tokens(tokens: &Tokens) -> Self {
2060 Self {
2061 sizes: vec![
2062 (
2063 ComponentSize::Md,
2064 ResolvedComponentStyle {
2065 width: Some(40.0),
2066 height: Some(40.0),
2067 radius: Some(tokens.radii.medium),
2068 icon_size: Some(20.0),
2069 ..ResolvedComponentStyle::default()
2070 },
2071 ),
2072 (
2073 ComponentSize::Lg,
2074 ResolvedComponentStyle {
2075 width: Some(48.0),
2076 height: Some(48.0),
2077 radius: Some(10.0),
2078 icon_size: Some(24.0),
2079 ..ResolvedComponentStyle::default()
2080 },
2081 ),
2082 (
2083 ComponentSize::Xl,
2084 ResolvedComponentStyle {
2085 width: Some(56.0),
2086 height: Some(56.0),
2087 radius: Some(12.0),
2088 icon_size: Some(28.0),
2089 ..ResolvedComponentStyle::default()
2090 },
2091 ),
2092 ],
2093 tones: vec![
2094 (
2095 FeatureIconTone::Brand,
2096 badge_tone(
2097 tokens.colors.primary_subtle,
2098 tokens.colors.primary.with_alpha(40),
2099 tokens.colors.primary,
2100 ),
2101 ),
2102 (
2103 FeatureIconTone::Gray,
2104 badge_tone(
2105 tokens.colors.surface_sunken,
2106 tokens.colors.border,
2107 tokens.colors.text_primary,
2108 ),
2109 ),
2110 (
2111 FeatureIconTone::Blue,
2112 badge_tone(
2113 tokens.colors.info.with_alpha(26),
2114 tokens.colors.info.with_alpha(80),
2115 tokens.colors.info,
2116 ),
2117 ),
2118 (
2119 FeatureIconTone::Orange,
2120 badge_tone(
2121 tokens.colors.warning.with_alpha(26),
2122 tokens.colors.warning.with_alpha(80),
2123 tokens.colors.warning,
2124 ),
2125 ),
2126 ],
2127 shadow: tokens.elevations.level1,
2128 }
2129 }
2130}
2131
2132fn badge_tone(background: Color, border: Color, text_color: Color) -> ResolvedComponentStyle {
2133 ResolvedComponentStyle {
2134 background: Some(Fill::Solid(background)),
2135 text_color: Some(text_color),
2136 border: Some(ComponentBorder {
2137 fill: Fill::Solid(border),
2138 width: 1.0,
2139 }),
2140 ..ResolvedComponentStyle::default()
2141 }
2142}
2143
2144fn find_size_style(
2145 styles: &[(ComponentSize, ResolvedComponentStyle)],
2146 size: ComponentSize,
2147) -> ResolvedComponentStyle {
2148 styles
2149 .iter()
2150 .find(|(candidate, _)| *candidate == size)
2151 .map(|(_, style)| style.clone())
2152 .or_else(|| {
2153 styles
2154 .iter()
2155 .find(|(candidate, _)| *candidate == ComponentSize::Md)
2156 .map(|(_, style)| style.clone())
2157 })
2158 .unwrap_or_default()
2159}
2160
2161#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2166pub struct ComponentTheme {
2167 pub button: ButtonTheme,
2168 pub text_input: TextInputTheme,
2169 pub calendar: CalendarTheme,
2170 pub pagination: PaginationTheme,
2171 pub timeline: TimelineTheme,
2172 pub segmented_control: SegmentedControlTheme,
2173 pub alert: AlertTheme,
2174 pub badge: BadgeTheme,
2175 pub tabs: TabsTheme,
2176 pub modal: ModalTheme,
2177 pub tree_view: TreeViewTheme,
2178 pub progress: ProgressTheme,
2179 pub tooltip: TooltipTheme,
2180 pub card: CardTheme,
2181 pub feature_icon: FeatureIconTheme,
2182}
2183
2184impl ComponentTheme {
2185 pub fn from_tokens(tokens: &Tokens) -> Self {
2186 Self {
2187 button: ButtonTheme::from_tokens(tokens),
2188 text_input: TextInputTheme::from_tokens(tokens),
2189 calendar: CalendarTheme::from_tokens(tokens),
2190 pagination: PaginationTheme::from_tokens(tokens),
2191 timeline: TimelineTheme::from_tokens(tokens),
2192 segmented_control: SegmentedControlTheme::from_tokens(tokens),
2193 alert: AlertTheme::from_tokens(tokens),
2194 badge: BadgeTheme::from_tokens(tokens),
2195 tabs: TabsTheme::from_tokens(tokens),
2196 modal: ModalTheme::from_tokens(tokens),
2197 tree_view: TreeViewTheme::from_tokens(tokens),
2198 progress: ProgressTheme::from_tokens(tokens),
2199 tooltip: TooltipTheme::from_tokens(tokens),
2200 card: CardTheme::from_tokens(tokens),
2201 feature_icon: FeatureIconTheme::from_tokens(tokens),
2202 }
2203 }
2204}
2205
2206#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2212pub struct Theme {
2213 pub tokens: Tokens,
2214 pub components: ComponentTheme,
2215 #[serde(default)]
2216 pub design_system: ResolvedDesignSystem,
2217}
2218
2219impl Default for Theme {
2220 fn default() -> Self {
2221 FissionDefaultDesignSystem::theme(DesignMode::Light)
2222 }
2223}
2224
2225impl Theme {
2226 pub fn dark() -> Self {
2227 FissionDefaultDesignSystem::theme(DesignMode::Dark)
2228 }
2229
2230 pub fn from_tokens(tokens: Tokens, mode: DesignMode) -> Self {
2231 let components = ComponentTheme::from_tokens(&tokens);
2232 Self {
2233 tokens,
2234 components,
2235 design_system: ResolvedDesignSystem {
2236 mode,
2237 ..ResolvedDesignSystem::default()
2238 },
2239 }
2240 }
2241}
2242
2243include!(concat!(
2244 env!("OUT_DIR"),
2245 "/generated_default_design_system.rs"
2246));
2247
2248pub mod presets {
2249 pub mod material3 {
2250 include!(concat!(
2251 env!("OUT_DIR"),
2252 "/generated_material3_design_system.rs"
2253 ));
2254 }
2255
2256 pub mod fluent2 {
2257 include!(concat!(
2258 env!("OUT_DIR"),
2259 "/generated_fluent2_design_system.rs"
2260 ));
2261 }
2262
2263 pub mod liquid_glass {
2264 include!(concat!(
2265 env!("OUT_DIR"),
2266 "/generated_liquid_glass_design_system.rs"
2267 ));
2268 }
2269
2270 pub mod cupertino {
2271 include!(concat!(
2272 env!("OUT_DIR"),
2273 "/generated_cupertino_design_system.rs"
2274 ));
2275 }
2276}
2277
2278pub use presets::cupertino::FissionCupertinoDesignSystem;
2279pub use presets::fluent2::FissionFluent2DesignSystem;
2280pub use presets::liquid_glass::FissionLiquidGlassDesignSystem;
2281pub use presets::material3::FissionMaterialDesign3DesignSystem;
2282
2283pub mod fonts {
2287 pub const NOTO_SANS_REGULAR_TTF: &[u8] =
2288 include_bytes!("../fonts/Noto_Sans/static/NotoSans-Regular.ttf");
2289 pub const INTER_24PT_REGULAR_TTF: &[u8] =
2290 include_bytes!("../fonts/Inter/static/Inter_24pt-Regular.ttf");
2291 #[inline]
2292 pub fn default_font_bytes() -> &'static [u8] {
2293 NOTO_SANS_REGULAR_TTF
2294 }
2295}