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