Skip to main content

fission_theme/
lib.rs

1//! Design token system and component themes for the Fission UI framework.
2//!
3//! This crate defines the complete visual language: colors, spacing, typography,
4//! corner radii, elevations (box shadows), and per-component theme overrides.
5//! It follows the Material Design 3 token architecture.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use fission_theme::Theme;
11//!
12//! let light = Theme::default();
13//! let dark = Theme::dark();
14//! ```
15
16pub 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/// Semantic color palette for the application.
344///
345/// Provides primary, secondary, surface, background, error, border, and text
346/// colors. Each color has an `on_*` counterpart for content displayed on that
347/// surface (e.g., `on_primary` is the text/icon color used on `primary` backgrounds).
348///
349/// The [`Default`] implementation provides a light theme. Use [`ColorTokens::dark()`]
350/// for dark mode colors.
351#[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            }, // Purple 40
390            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/// Standard spacing scale used for padding, margins, and gaps.
693///
694/// Values: `none` (0), `xs` (4), `s` (8), `m` (16), `l` (24), `xl` (32).
695#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
696pub struct SpacingTokens {
697    pub none: f32,  // 0
698    pub xs: f32,    // 4
699    pub s: f32,     // 8
700    pub m: f32,     // 16
701    pub l: f32,     // 24
702    pub xl: f32,    // 32
703    pub xxl: f32,   // 48
704    pub xxxl: f32,  // 64
705    pub xxxxl: f32, // 96
706}
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/// Font size scale for text elements.
725///
726/// Sizes: `label_large_size` (15), `body_medium_size` (15), `body_large_size` (17),
727/// `heading_size` (28).
728#[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/// Corner radius scale for rounded containers.
798///
799/// Values: `small` (4), `medium` (8), `large` (12), `full` (9999 -- fully rounded pill).
800#[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/// Box shadow levels for surface elevation.
826///
827/// Six levels (0-5). Levels 0, 4, and 5 default to `None`. Levels 1-3 provide
828/// progressively stronger shadows with increasing blur radius and y-offset.
829#[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/// The complete set of primitive design tokens.
977///
978/// Combines [`ColorTokens`], [`SpacingTokens`], [`TypographyTokens`],
979/// [`RadiusTokens`], and [`ElevationTokens`]. The [`Default`] implementation
980/// provides light-mode values. Use [`Tokens::dark()`] for dark mode.
981#[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// --- Component Themes ---
1007
1008/// Visual parameters for the `Button` widget.
1009///
1010/// Includes dimensions, padding, corner radius, text size, elevation for
1011/// rest/hover/pressed states, and an optional focus stroke.
1012#[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/// Visual parameters for the `TextInput` widget.
1295///
1296/// Controls height, horizontal padding, corner radius, font size, and colors
1297/// for border, focus ring, text, and placeholder.
1298#[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/// Visual parameters for the `Calendar` widget.
1451#[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/// Visual parameters for the `Pagination` widget.
1475#[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/// Visual parameters for the `Timeline` widget.
1493#[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/// Visual parameters for the `SegmentedControl` widget.
1513#[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/// Visual parameters for the `Alert` widget, with per-severity background colors.
1535#[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/// Visual parameters for the `Badge` widget.
1572#[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/// Visual parameters for the `Tabs` widget.
1695#[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/// Visual parameters for the `Modal` widget.
1787///
1788/// Controls the dialog background color, corner radius, shadow, and maximum width.
1789#[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/// Visual parameters for the `TreeView` widget.
1834#[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/// Visual parameters for the `ProgressBar` widget.
1852#[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/// Visual parameters for the `Tooltip` widget.
1886#[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/// Aggregates all per-component visual themes.
2162///
2163/// Each field holds the theme for a specific widget type. Construct via
2164/// [`ComponentTheme::from_tokens()`] to derive all values from the primitive tokens.
2165#[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/// The top-level theme combining primitive [`Tokens`] and derived [`ComponentTheme`].
2207///
2208/// Use [`Theme::default()`] for light mode and [`Theme::dark()`] for dark mode.
2209/// For custom themes, construct [`Tokens`] manually and derive components via
2210/// [`ComponentTheme::from_tokens()`].
2211#[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
2283/// Bundled font files embedded at compile time.
2284///
2285/// Provides Noto Sans Regular (the default) and Inter 24pt Regular.
2286pub 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}