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    /// Font faces packaged with this design system.
41    ///
42    /// Hosts register these faces with their text measurer and renderer before
43    /// the first frame so declared weight, style, and variation axes are used
44    /// consistently instead of synthesized fallbacks.
45    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    /// Logo and image assets declared by the design system.
155    pub logos: Vec<DesignAsset>,
156    /// Font assets declared by the design system.
157    pub fonts: Vec<DesignFontAsset>,
158}
159
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161pub struct DesignAsset {
162    /// Stable asset identifier from the DSP package.
163    pub id: String,
164    /// Path to the asset relative to the DSP file.
165    pub path: String,
166    /// File format such as `svg`, `png`, or `webp`.
167    pub format: String,
168}
169
170/// Metadata for a font face declared by a Design System Package.
171#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172pub struct DesignFontAsset {
173    /// CSS/font family name exposed to app code.
174    pub family: String,
175    /// OpenType font weight, normally in the `100..=900` range.
176    pub weight: u16,
177    /// Font slope style.
178    pub style: PackagedFontStyle,
179    /// Path to the font file relative to the DSP file.
180    pub path: String,
181    /// Font format such as `truetype`, `opentype`, `woff`, or `woff2`.
182    pub format: String,
183    /// Optional variation-axis defaults.
184    pub axes: Vec<FontVariationAxis>,
185}
186
187/// A variation-axis default applied when a packaged font is registered.
188#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
189pub struct FontVariationAxis {
190    /// Four-byte OpenType variation tag, for example `wght`.
191    pub tag: [u8; 4],
192    /// Axis value used when the font face is registered.
193    pub value: f32,
194}
195
196/// Font slope metadata used by packaged design-system fonts.
197#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
198pub enum PackagedFontStyle {
199    /// Upright roman glyphs.
200    #[default]
201    Normal,
202    /// Italic glyphs.
203    Italic,
204    /// Oblique glyphs.
205    Oblique,
206}
207
208/// A font face embedded in an application binary by design-system codegen.
209#[derive(Clone, Copy, Debug)]
210pub struct PackagedFont {
211    /// CSS/font family name exposed to app code.
212    pub family: &'static str,
213    /// OpenType font weight.
214    pub weight: u16,
215    /// Font slope style.
216    pub style: PackagedFontStyle,
217    /// Font format such as `truetype`, `opentype`, `woff`, or `woff2`.
218    pub format: &'static str,
219    /// Embedded font bytes.
220    pub data: &'static [u8],
221    /// Optional variation-axis defaults.
222    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/// Semantic color palette for the application.
414///
415/// Provides primary, secondary, surface, background, error, border, and text
416/// colors. Each color has an `on_*` counterpart for content displayed on that
417/// surface (e.g., `on_primary` is the text/icon color used on `primary` backgrounds).
418///
419/// The [`Default`] implementation provides a light theme. Use [`ColorTokens::dark()`]
420/// for dark mode colors.
421#[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            }, // Purple 40
460            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/// Standard spacing scale used for padding, margins, and gaps.
763///
764/// Values: `none` (0), `xs` (4), `s` (8), `m` (16), `l` (24), `xl` (32).
765#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
766pub struct SpacingTokens {
767    pub none: f32,  // 0
768    pub xs: f32,    // 4
769    pub s: f32,     // 8
770    pub m: f32,     // 16
771    pub l: f32,     // 24
772    pub xl: f32,    // 32
773    pub xxl: f32,   // 48
774    pub xxxl: f32,  // 64
775    pub xxxxl: f32, // 96
776}
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/// Font size scale for text elements.
795///
796/// Sizes: `label_large_size` (15), `body_medium_size` (15), `body_large_size` (17),
797/// `heading_size` (28).
798#[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/// Corner radius scale for rounded containers.
868///
869/// Values: `small` (4), `medium` (8), `large` (12), `full` (9999 -- fully rounded pill).
870#[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/// Box shadow levels for surface elevation.
896///
897/// Six levels (0-5). Levels 0, 4, and 5 default to `None`. Levels 1-3 provide
898/// progressively stronger shadows with increasing blur radius and y-offset.
899#[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/// The complete set of primitive design tokens.
1055///
1056/// Combines [`ColorTokens`], [`SpacingTokens`], [`TypographyTokens`],
1057/// [`RadiusTokens`], and [`ElevationTokens`]. The [`Default`] implementation
1058/// provides light-mode values. Use [`Tokens::dark()`] for dark mode.
1059#[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// --- Component Themes ---
1085
1086/// Visual parameters for the `Button` widget.
1087///
1088/// Includes dimensions, padding, corner radius, text size, elevation for
1089/// rest/hover/pressed states, and an optional focus stroke.
1090#[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/// Visual parameters for the `TextInput` widget.
1373///
1374/// Controls height, horizontal padding, corner radius, font size, and colors
1375/// for border, focus ring, text, and placeholder.
1376#[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/// Visual parameters for the `Calendar` widget.
1529#[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/// Visual parameters for the `Pagination` widget.
1553#[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/// Visual parameters for the `Timeline` widget.
1571#[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/// Visual parameters for the `SegmentedControl` widget.
1591#[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/// Visual parameters for the `Alert` widget, with per-severity background colors.
1613#[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/// Visual parameters for the `Badge` widget.
1650#[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/// Visual parameters for the `Tabs` widget.
1773#[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/// Visual parameters for the `Modal` widget.
1865///
1866/// Controls the dialog background color, corner radius, shadow, and maximum width.
1867#[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/// Visual parameters for the `TreeView` widget.
1912#[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/// Visual parameters for the `ProgressBar` widget.
1930#[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/// Visual parameters for the `Tooltip` widget.
1964#[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/// Aggregates all per-component visual themes.
2240///
2241/// Each field holds the theme for a specific widget type. Construct via
2242/// [`ComponentTheme::from_tokens()`] to derive all values from the primitive tokens.
2243#[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/// The top-level theme combining primitive [`Tokens`] and derived [`ComponentTheme`].
2285///
2286/// Use [`Theme::default()`] for light mode and [`Theme::dark()`] for dark mode.
2287/// For custom themes, construct [`Tokens`] manually and derive components via
2288/// [`ComponentTheme::from_tokens()`].
2289#[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
2361/// Bundled font files embedded at compile time.
2362///
2363/// Provides Noto Sans Regular (the default) and Inter 24pt Regular.
2364pub 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}