use crate::{
sanitize_str, sanitize_title, AnimationCurve, Palette, Rgba, Vec2, WidgetMaterialDescriptor,
WidgetMaterialQuality, WidgetMaterialShadow, WidgetResolvedMaterial, WidgetResolvedStyle,
WidgetSize, WidgetState, WidgetStyleDescriptor, WidgetStyleOverride, WidgetVariant,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Density {
Compact,
#[default]
Comfortable,
Spacious,
}
impl Density {
pub fn label(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Comfortable => "comfortable",
Self::Spacious => "spacious",
}
}
pub fn scale(self) -> f32 {
match self {
Self::Compact => 0.86,
Self::Comfortable => 1.0,
Self::Spacious => 1.16,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum PlatformStyle {
#[default]
CrossPlatform,
Macos,
Windows,
Linux,
Web,
}
impl PlatformStyle {
pub fn label(self) -> &'static str {
match self {
Self::CrossPlatform => "cross_platform",
Self::Macos => "macos",
Self::Windows => "windows",
Self::Linux => "linux",
Self::Web => "web",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ComponentSize {
XSmall,
Small,
#[default]
Regular,
Large,
XLarge,
}
impl ComponentSize {
pub fn label(self) -> &'static str {
match self {
Self::XSmall => "x_small",
Self::Small => "small",
Self::Regular => "regular",
Self::Large => "large",
Self::XLarge => "x_large",
}
}
pub fn base_height_px(self) -> f32 {
match self {
Self::XSmall => 24.0,
Self::Small => 32.0,
Self::Regular => 40.0,
Self::Large => 52.0,
Self::XLarge => 64.0,
}
}
pub fn widget_size(self) -> WidgetSize {
match self {
Self::XSmall | Self::Small => WidgetSize::Compact,
Self::Regular => WidgetSize::Regular,
Self::Large | Self::XLarge => WidgetSize::Spacious,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ComponentRole {
#[default]
Neutral,
Primary,
Secondary,
Accent,
Success,
Warning,
Danger,
Info,
}
impl ComponentRole {
pub fn label(self) -> &'static str {
match self {
Self::Neutral => "neutral",
Self::Primary => "primary",
Self::Secondary => "secondary",
Self::Accent => "accent",
Self::Success => "success",
Self::Warning => "warning",
Self::Danger => "danger",
Self::Info => "info",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ColorTokens {
pub background: Rgba,
pub surface: Rgba,
pub surface_alt: Rgba,
pub surface_raised: Rgba,
pub border: Rgba,
pub divider: Rgba,
pub text: Rgba,
pub text_muted: Rgba,
pub text_disabled: Rgba,
pub accent: Rgba,
pub accent_alt: Rgba,
pub success: Rgba,
pub warning: Rgba,
pub danger: Rgba,
pub info: Rgba,
pub focus_ring: Rgba,
pub selection: Rgba,
pub overlay_scrim: Rgba,
pub shadow: Rgba,
}
impl ColorTokens {
pub fn from_palette(palette: Palette, accent_alt: Rgba, info: Rgba) -> Self {
Self {
background: palette.background,
surface: palette.panel,
surface_alt: palette.panel_hot.with_alpha(0.86),
surface_raised: palette.panel_hot,
border: palette.text.with_alpha(0.14),
divider: palette.text.with_alpha(0.09),
text: palette.text,
text_muted: palette.muted,
text_disabled: palette.muted.with_alpha(0.52),
accent: palette.accent,
accent_alt,
success: palette.success,
warning: palette.warning,
danger: palette.error,
info,
focus_ring: palette.accent.with_alpha(0.72),
selection: palette.accent.with_alpha(0.28),
overlay_scrim: Rgba::rgba8(0, 0, 0, 166),
shadow: Rgba::rgba8(0, 0, 0, 150),
}
}
pub fn role(self, role: ComponentRole) -> Rgba {
match role {
ComponentRole::Neutral => self.text_muted,
ComponentRole::Primary => self.accent,
ComponentRole::Secondary => self.accent_alt,
ComponentRole::Accent => self.accent,
ComponentRole::Success => self.success,
ComponentRole::Warning => self.warning,
ComponentRole::Danger => self.danger,
ComponentRole::Info => self.info,
}
}
pub fn sanitized(self) -> Self {
Self {
background: self.background.sanitized(),
surface: self.surface.sanitized(),
surface_alt: self.surface_alt.sanitized(),
surface_raised: self.surface_raised.sanitized(),
border: self.border.sanitized(),
divider: self.divider.sanitized(),
text: self.text.sanitized(),
text_muted: self.text_muted.sanitized(),
text_disabled: self.text_disabled.sanitized(),
accent: self.accent.sanitized(),
accent_alt: self.accent_alt.sanitized(),
success: self.success.sanitized(),
warning: self.warning.sanitized(),
danger: self.danger.sanitized(),
info: self.info.sanitized(),
focus_ring: self.focus_ring.sanitized(),
selection: self.selection.sanitized(),
overlay_scrim: self.overlay_scrim.sanitized(),
shadow: self.shadow.sanitized(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TypographyTokens {
pub ui_family: String,
pub mono_family: String,
pub display_family: String,
pub size_xs_px: f32,
pub size_sm_px: f32,
pub size_md_px: f32,
pub size_lg_px: f32,
pub size_xl_px: f32,
pub line_height: f32,
pub weight_regular: u16,
pub weight_medium: u16,
pub weight_bold: u16,
pub letter_spacing_px: f32,
}
impl Default for TypographyTokens {
fn default() -> Self {
Self {
ui_family: "Inter, SF Pro, Segoe UI, sans-serif".to_string(),
mono_family: "Source Code Pro, SF Mono, Consolas, monospace".to_string(),
display_family: "Inter Display, SF Pro Display, sans-serif".to_string(),
size_xs_px: 11.0,
size_sm_px: 13.0,
size_md_px: 15.0,
size_lg_px: 19.0,
size_xl_px: 28.0,
line_height: 1.42,
weight_regular: 400,
weight_medium: 560,
weight_bold: 700,
letter_spacing_px: -0.1,
}
}
}
impl TypographyTokens {
pub fn sanitized(mut self) -> Self {
self.ui_family = sanitize_title(&self.ui_family, 160);
self.mono_family = sanitize_title(&self.mono_family, 160);
self.display_family = sanitize_title(&self.display_family, 160);
self.size_xs_px = finite_range(self.size_xs_px, 6.0, 32.0, 11.0);
self.size_sm_px = finite_range(self.size_sm_px, 8.0, 40.0, 13.0);
self.size_md_px = finite_range(self.size_md_px, 9.0, 48.0, 15.0);
self.size_lg_px = finite_range(self.size_lg_px, 10.0, 72.0, 19.0);
self.size_xl_px = finite_range(self.size_xl_px, 12.0, 120.0, 28.0);
self.line_height = finite_range(self.line_height, 1.0, 2.4, 1.42);
self.weight_regular = self.weight_regular.clamp(100, 950);
self.weight_medium = self.weight_medium.clamp(100, 950);
self.weight_bold = self.weight_bold.clamp(100, 950);
self.letter_spacing_px = finite_range(self.letter_spacing_px, -3.0, 12.0, 0.0);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpacingTokens {
pub unit_px: f32,
pub xxs_px: f32,
pub xs_px: f32,
pub sm_px: f32,
pub md_px: f32,
pub lg_px: f32,
pub xl_px: f32,
pub xxl_px: f32,
pub gutter_px: f32,
pub panel_px: f32,
}
impl SpacingTokens {
pub fn new(unit_px: f32, density: Density) -> Self {
let scale = density.scale();
Self {
unit_px,
xxs_px: unit_px * 0.5 * scale,
xs_px: unit_px * scale,
sm_px: unit_px * 1.5 * scale,
md_px: unit_px * 2.0 * scale,
lg_px: unit_px * 3.0 * scale,
xl_px: unit_px * 4.0 * scale,
xxl_px: unit_px * 6.0 * scale,
gutter_px: unit_px * 2.5 * scale,
panel_px: unit_px * 4.0 * scale,
}
.sanitized()
}
pub fn sanitized(self) -> Self {
Self {
unit_px: finite_range(self.unit_px, 2.0, 32.0, 8.0),
xxs_px: finite_range(self.xxs_px, 1.0, 64.0, 4.0),
xs_px: finite_range(self.xs_px, 2.0, 96.0, 8.0),
sm_px: finite_range(self.sm_px, 2.0, 128.0, 12.0),
md_px: finite_range(self.md_px, 4.0, 160.0, 16.0),
lg_px: finite_range(self.lg_px, 6.0, 220.0, 24.0),
xl_px: finite_range(self.xl_px, 8.0, 300.0, 32.0),
xxl_px: finite_range(self.xxl_px, 12.0, 420.0, 48.0),
gutter_px: finite_range(self.gutter_px, 4.0, 220.0, 20.0),
panel_px: finite_range(self.panel_px, 8.0, 320.0, 32.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RadiusTokens {
pub xs_px: f32,
pub sm_px: f32,
pub md_px: f32,
pub lg_px: f32,
pub xl_px: f32,
pub pill_px: f32,
}
impl RadiusTokens {
pub fn desktop_glass() -> Self {
Self {
xs_px: 6.0,
sm_px: 10.0,
md_px: 16.0,
lg_px: 22.0,
xl_px: 32.0,
pill_px: 999.0,
}
}
pub fn sanitized(self) -> Self {
Self {
xs_px: finite_range(self.xs_px, 0.0, 64.0, 6.0),
sm_px: finite_range(self.sm_px, 0.0, 96.0, 10.0),
md_px: finite_range(self.md_px, 0.0, 128.0, 16.0),
lg_px: finite_range(self.lg_px, 0.0, 180.0, 22.0),
xl_px: finite_range(self.xl_px, 0.0, 240.0, 32.0),
pill_px: finite_range(self.pill_px, 1.0, 4096.0, 999.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BorderTokens {
pub hairline_px: f32,
pub thin_px: f32,
pub regular_px: f32,
pub focus_px: f32,
}
impl Default for BorderTokens {
fn default() -> Self {
Self {
hairline_px: 0.5,
thin_px: 1.0,
regular_px: 1.4,
focus_px: 2.0,
}
}
}
impl BorderTokens {
pub fn sanitized(self) -> Self {
Self {
hairline_px: finite_range(self.hairline_px, 0.0, 4.0, 0.5),
thin_px: finite_range(self.thin_px, 0.0, 8.0, 1.0),
regular_px: finite_range(self.regular_px, 0.0, 12.0, 1.4),
focus_px: finite_range(self.focus_px, 0.0, 16.0, 2.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ShadowTokens {
pub color: Rgba,
pub low_y_px: f32,
pub low_blur_px: f32,
pub medium_y_px: f32,
pub medium_blur_px: f32,
pub high_y_px: f32,
pub high_blur_px: f32,
}
impl Default for ShadowTokens {
fn default() -> Self {
Self {
color: Rgba::rgba8(0, 0, 0, 146),
low_y_px: 6.0,
low_blur_px: 18.0,
medium_y_px: 14.0,
medium_blur_px: 34.0,
high_y_px: 28.0,
high_blur_px: 64.0,
}
}
}
impl ShadowTokens {
pub fn sanitized(self) -> Self {
Self {
color: self.color.sanitized(),
low_y_px: finite_range(self.low_y_px, -64.0, 128.0, 6.0),
low_blur_px: finite_range(self.low_blur_px, 0.0, 180.0, 18.0),
medium_y_px: finite_range(self.medium_y_px, -64.0, 180.0, 14.0),
medium_blur_px: finite_range(self.medium_blur_px, 0.0, 240.0, 34.0),
high_y_px: finite_range(self.high_y_px, -64.0, 240.0, 28.0),
high_blur_px: finite_range(self.high_blur_px, 0.0, 360.0, 64.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BlurTokens {
pub control_px: f32,
pub panel_px: f32,
pub backdrop_px: f32,
}
impl Default for BlurTokens {
fn default() -> Self {
Self {
control_px: 12.0,
panel_px: 22.0,
backdrop_px: 38.0,
}
}
}
impl BlurTokens {
pub fn sanitized(self) -> Self {
Self {
control_px: finite_range(self.control_px, 0.0, 96.0, 12.0),
panel_px: finite_range(self.panel_px, 0.0, 160.0, 22.0),
backdrop_px: finite_range(self.backdrop_px, 0.0, 240.0, 38.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MaterialTokenSet {
pub panel_opacity: f32,
pub control_opacity: f32,
pub saturation: f32,
pub refraction: f32,
pub thickness_px: f32,
pub rim_intensity: f32,
pub specular_intensity: f32,
}
impl Default for MaterialTokenSet {
fn default() -> Self {
Self {
panel_opacity: 0.82,
control_opacity: 0.72,
saturation: 1.28,
refraction: 0.22,
thickness_px: 1.8,
rim_intensity: 0.34,
specular_intensity: 0.30,
}
}
}
impl MaterialTokenSet {
pub fn sanitized(self) -> Self {
Self {
panel_opacity: finite_range(self.panel_opacity, 0.0, 1.0, 0.82),
control_opacity: finite_range(self.control_opacity, 0.0, 1.0, 0.72),
saturation: finite_range(self.saturation, 0.0, 3.0, 1.28),
refraction: finite_range(self.refraction, 0.0, 1.0, 0.22),
thickness_px: finite_range(self.thickness_px, 0.0, 24.0, 1.8),
rim_intensity: finite_range(self.rim_intensity, 0.0, 2.0, 0.34),
specular_intensity: finite_range(self.specular_intensity, 0.0, 2.0, 0.30),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MotionTokens {
pub instant_ms: u32,
pub fast_ms: u32,
pub standard_ms: u32,
pub slow_ms: u32,
pub curve: AnimationCurve,
}
impl Default for MotionTokens {
fn default() -> Self {
Self {
instant_ms: 70,
fast_ms: 140,
standard_ms: 220,
slow_ms: 420,
curve: AnimationCurve::Emphasized,
}
}
}
impl MotionTokens {
pub fn sanitized(self) -> Self {
Self {
instant_ms: self.instant_ms.clamp(0, 250),
fast_ms: self.fast_ms.clamp(40, 600),
standard_ms: self.standard_ms.clamp(80, 1200),
slow_ms: self.slow_ms.clamp(120, 2400),
curve: self.curve,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ZIndexTokens {
pub base: i16,
pub dropdown: i16,
pub overlay: i16,
pub modal: i16,
pub tooltip: i16,
}
impl Default for ZIndexTokens {
fn default() -> Self {
Self {
base: 0,
dropdown: 100,
overlay: 300,
modal: 600,
tooltip: 900,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IconTokens {
pub sm_px: f32,
pub md_px: f32,
pub lg_px: f32,
pub stroke_px: f32,
}
impl Default for IconTokens {
fn default() -> Self {
Self {
sm_px: 14.0,
md_px: 18.0,
lg_px: 24.0,
stroke_px: 1.7,
}
}
}
impl IconTokens {
pub fn sanitized(self) -> Self {
Self {
sm_px: finite_range(self.sm_px, 8.0, 48.0, 14.0),
md_px: finite_range(self.md_px, 8.0, 64.0, 18.0),
lg_px: finite_range(self.lg_px, 10.0, 96.0, 24.0),
stroke_px: finite_range(self.stroke_px, 0.5, 8.0, 1.7),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AudioDesignTokens {
pub meter_green: Rgba,
pub meter_yellow: Rgba,
pub meter_red: Rgba,
pub fader_track_px: f32,
pub fader_thumb_px: f32,
pub knob_diameter_px: f32,
pub waveform_height_px: f32,
}
impl Default for AudioDesignTokens {
fn default() -> Self {
Self {
meter_green: Rgba::rgb8(81, 229, 147),
meter_yellow: Rgba::rgb8(255, 206, 92),
meter_red: Rgba::rgb8(255, 82, 95),
fader_track_px: 172.0,
fader_thumb_px: 28.0,
knob_diameter_px: 44.0,
waveform_height_px: 96.0,
}
}
}
impl AudioDesignTokens {
pub fn sanitized(self) -> Self {
Self {
meter_green: self.meter_green.sanitized(),
meter_yellow: self.meter_yellow.sanitized(),
meter_red: self.meter_red.sanitized(),
fader_track_px: finite_range(self.fader_track_px, 48.0, 640.0, 172.0),
fader_thumb_px: finite_range(self.fader_thumb_px, 10.0, 96.0, 28.0),
knob_diameter_px: finite_range(self.knob_diameter_px, 18.0, 160.0, 44.0),
waveform_height_px: finite_range(self.waveform_height_px, 24.0, 360.0, 96.0),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DesignTokenSet {
pub id: String,
pub name: String,
pub density: Density,
pub platform: PlatformStyle,
pub colors: ColorTokens,
pub typography: TypographyTokens,
pub spacing: SpacingTokens,
pub radius: RadiusTokens,
pub border: BorderTokens,
pub shadow: ShadowTokens,
pub blur: BlurTokens,
pub material: MaterialTokenSet,
pub motion: MotionTokens,
pub z: ZIndexTokens,
pub icon: IconTokens,
pub audio: AudioDesignTokens,
}
impl DesignTokenSet {
pub fn palette(&self) -> Palette {
Palette {
name: self.id.clone(),
background: self.colors.background,
panel: self.colors.surface,
panel_hot: self.colors.surface_raised,
text: self.colors.text,
muted: self.colors.text_muted,
accent: self.colors.accent,
success: self.colors.success,
warning: self.colors.warning,
error: self.colors.danger,
}
}
pub fn component_height_px(&self, size: ComponentSize) -> f32 {
finite_range(
size.base_height_px() * self.density.scale(),
18.0,
112.0,
40.0,
)
}
pub fn component_defaults(
&self,
variant: WidgetVariant,
size: ComponentSize,
role: ComponentRole,
state: WidgetState,
) -> ComponentDefaults {
let tokens = self.clone().sanitized();
let widget_size = size.widget_size();
let accent = tokens.colors.role(role);
let height_px = tokens.component_height_px(size);
let padding_x_px = component_padding_x(&tokens, size);
let padding_y_px = component_padding_y(&tokens, size);
let gap_px = component_gap(&tokens, size);
let radius_px = component_radius(&tokens, size);
let border_width_px = if state.focused {
tokens.border.focus_px
} else {
tokens.border.regular_px
};
let icon_px = component_icon_px(&tokens, size);
let motion_ms = if state.pressed {
tokens.motion.fast_ms
} else if state.disabled {
tokens.motion.instant_ms
} else {
tokens.motion.standard_ms
};
let style = WidgetStyleDescriptor::new(variant, widget_size)
.with_accent(accent)
.with_state(state)
.with_overrides(
WidgetStyleOverride::empty()
.with_radius_px(radius_px)
.with_border_width_px(border_width_px),
)
.resolve();
let material = style.material(component_material_descriptor(&tokens, variant, state));
ComponentDefaults {
size,
role,
variant,
state,
widget_size,
height_px,
padding_x_px,
padding_y_px,
gap_px,
radius_px,
border_width_px,
icon_px,
motion_ms,
focus_ring: tokens.colors.focus_ring,
style,
material,
}
.sanitized()
}
pub fn sanitized(mut self) -> Self {
self.id = sanitize_token_id(&self.id, "token-set");
self.name = sanitize_title(&self.name, 120);
if self.name.is_empty() {
self.name = self.id.clone();
}
self.colors = self.colors.sanitized();
self.typography = self.typography.sanitized();
self.spacing = self.spacing.sanitized();
self.radius = self.radius.sanitized();
self.border = self.border.sanitized();
self.shadow = self.shadow.sanitized();
self.blur = self.blur.sanitized();
self.material = self.material.sanitized();
self.motion = self.motion.sanitized();
self.icon = self.icon.sanitized();
self.audio = self.audio.sanitized();
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ComponentDefaults {
pub size: ComponentSize,
pub role: ComponentRole,
pub variant: WidgetVariant,
pub state: WidgetState,
pub widget_size: WidgetSize,
pub height_px: f32,
pub padding_x_px: f32,
pub padding_y_px: f32,
pub gap_px: f32,
pub radius_px: f32,
pub border_width_px: f32,
pub icon_px: f32,
pub motion_ms: u32,
pub focus_ring: Rgba,
pub style: WidgetResolvedStyle,
pub material: WidgetResolvedMaterial,
}
impl ComponentDefaults {
pub fn sanitized(self) -> Self {
Self {
size: self.size,
role: self.role,
variant: self.variant,
state: self.state,
widget_size: self.widget_size,
height_px: finite_range(self.height_px, 18.0, 160.0, 40.0),
padding_x_px: finite_range(self.padding_x_px, 0.0, 160.0, 14.0),
padding_y_px: finite_range(self.padding_y_px, 0.0, 120.0, 8.0),
gap_px: finite_range(self.gap_px, 0.0, 120.0, 8.0),
radius_px: finite_range(self.radius_px, 0.0, 4096.0, 14.0),
border_width_px: finite_range(self.border_width_px, 0.0, 16.0, 1.0),
icon_px: finite_range(self.icon_px, 8.0, 128.0, 18.0),
motion_ms: self.motion_ms.min(2400),
focus_ring: self.focus_ring.sanitized(),
style: self.style.sanitized(),
material: self.material.sanitized(),
}
}
}
pub fn built_in_token_presets() -> Vec<DesignTokenSet> {
vec![
token_preset(
"midnight_pro",
"Midnight Pro",
Density::Comfortable,
PlatformStyle::CrossPlatform,
Palette::midnight(),
Rgba::rgb8(190, 132, 255),
Rgba::rgb8(93, 210, 255),
),
token_preset(
"graphite_glass",
"Graphite Glass",
Density::Comfortable,
PlatformStyle::Macos,
Palette {
name: "graphite_glass".to_string(),
background: Rgba::rgb8(12, 13, 16),
panel: Rgba::rgba8(31, 33, 38, 220),
panel_hot: Rgba::rgba8(50, 54, 62, 235),
text: Rgba::rgb8(238, 239, 235),
muted: Rgba::rgb8(151, 156, 166),
accent: Rgba::rgb8(173, 199, 224),
success: Rgba::rgb8(137, 210, 158),
warning: Rgba::rgb8(228, 184, 108),
error: Rgba::rgb8(231, 115, 108),
},
Rgba::rgb8(214, 219, 230),
Rgba::rgb8(136, 188, 246),
),
token_preset(
"silver_studio",
"Silver Studio",
Density::Spacious,
PlatformStyle::CrossPlatform,
Palette {
name: "silver_studio".to_string(),
background: Rgba::rgb8(232, 235, 240),
panel: Rgba::rgba8(249, 250, 252, 235),
panel_hot: Rgba::rgba8(255, 255, 255, 245),
text: Rgba::rgb8(28, 33, 42),
muted: Rgba::rgb8(94, 104, 118),
accent: Rgba::rgb8(28, 108, 236),
success: Rgba::rgb8(25, 139, 84),
warning: Rgba::rgb8(174, 111, 14),
error: Rgba::rgb8(196, 48, 62),
},
Rgba::rgb8(111, 79, 232),
Rgba::rgb8(0, 132, 184),
),
token_preset(
"audio_console_dark",
"Audio Console Dark",
Density::Compact,
PlatformStyle::CrossPlatform,
Palette {
name: "audio_console_dark".to_string(),
background: Rgba::rgb8(5, 7, 8),
panel: Rgba::rgba8(15, 19, 20, 238),
panel_hot: Rgba::rgba8(31, 38, 36, 244),
text: Rgba::rgb8(232, 242, 232),
muted: Rgba::rgb8(128, 151, 139),
accent: Rgba::rgb8(82, 224, 150),
success: Rgba::rgb8(80, 224, 126),
warning: Rgba::rgb8(255, 207, 88),
error: Rgba::rgb8(255, 80, 86),
},
Rgba::rgb8(255, 166, 74),
Rgba::rgb8(92, 210, 255),
),
token_preset(
"audio_console_light",
"Audio Console Light",
Density::Compact,
PlatformStyle::CrossPlatform,
Palette {
name: "audio_console_light".to_string(),
background: Rgba::rgb8(221, 222, 214),
panel: Rgba::rgba8(244, 244, 238, 238),
panel_hot: Rgba::rgba8(255, 255, 248, 245),
text: Rgba::rgb8(34, 39, 34),
muted: Rgba::rgb8(98, 110, 101),
accent: Rgba::rgb8(26, 146, 86),
success: Rgba::rgb8(22, 132, 72),
warning: Rgba::rgb8(174, 112, 18),
error: Rgba::rgb8(196, 56, 56),
},
Rgba::rgb8(214, 111, 22),
Rgba::rgb8(0, 126, 178),
),
token_preset(
"developer_dark",
"Developer Dark",
Density::Comfortable,
PlatformStyle::Linux,
Palette {
name: "developer_dark".to_string(),
background: Rgba::rgb8(6, 8, 18),
panel: Rgba::rgba8(13, 18, 34, 232),
panel_hot: Rgba::rgba8(27, 36, 64, 240),
text: Rgba::rgb8(224, 234, 255),
muted: Rgba::rgb8(126, 144, 178),
accent: Rgba::rgb8(105, 169, 255),
success: Rgba::rgb8(84, 219, 158),
warning: Rgba::rgb8(255, 202, 96),
error: Rgba::rgb8(255, 104, 124),
},
Rgba::rgb8(204, 139, 255),
Rgba::rgb8(82, 222, 238),
),
token_preset(
"ai_workspace",
"AI Workspace",
Density::Comfortable,
PlatformStyle::CrossPlatform,
Palette {
name: "ai_workspace".to_string(),
background: Rgba::rgb8(4, 11, 22),
panel: Rgba::rgba8(7, 25, 39, 228),
panel_hot: Rgba::rgba8(18, 54, 72, 240),
text: Rgba::rgb8(220, 255, 248),
muted: Rgba::rgb8(116, 179, 186),
accent: Rgba::rgb8(84, 255, 218),
success: Rgba::rgb8(118, 240, 154),
warning: Rgba::rgb8(255, 208, 112),
error: Rgba::rgb8(255, 116, 145),
},
Rgba::rgb8(175, 122, 255),
Rgba::rgb8(91, 183, 255),
),
token_preset(
"dashboard_pro",
"Dashboard Pro",
Density::Spacious,
PlatformStyle::Web,
Palette {
name: "dashboard_pro".to_string(),
background: Rgba::rgb8(9, 14, 24),
panel: Rgba::rgba8(16, 26, 43, 232),
panel_hot: Rgba::rgba8(30, 47, 72, 240),
text: Rgba::rgb8(232, 240, 255),
muted: Rgba::rgb8(135, 151, 174),
accent: Rgba::rgb8(72, 168, 255),
success: Rgba::rgb8(75, 211, 156),
warning: Rgba::rgb8(255, 184, 78),
error: Rgba::rgb8(255, 96, 116),
},
Rgba::rgb8(255, 135, 74),
Rgba::rgb8(87, 218, 255),
),
]
.into_iter()
.map(DesignTokenSet::sanitized)
.collect()
}
fn token_preset(
id: &str,
name: &str,
density: Density,
platform: PlatformStyle,
palette: Palette,
accent_alt: Rgba,
info: Rgba,
) -> DesignTokenSet {
let colors = ColorTokens::from_palette(palette, accent_alt, info);
let mut material = MaterialTokenSet::default();
let mut blur = BlurTokens::default();
if matches!(density, Density::Compact) {
material.thickness_px = 1.2;
blur.panel_px = 16.0;
}
DesignTokenSet {
id: id.to_string(),
name: name.to_string(),
density,
platform,
colors,
typography: TypographyTokens::default(),
spacing: SpacingTokens::new(8.0, density),
radius: RadiusTokens::desktop_glass(),
border: BorderTokens::default(),
shadow: ShadowTokens::default(),
blur,
material,
motion: MotionTokens::default(),
z: ZIndexTokens::default(),
icon: IconTokens::default(),
audio: AudioDesignTokens::default(),
}
}
fn sanitize_token_id(id: &str, fallback: &str) -> String {
let sanitized = sanitize_str(id, 80).trim().replace(' ', "_");
if sanitized.is_empty() {
fallback.to_string()
} else {
sanitized
}
}
fn finite_range(value: f32, min: f32, max: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.clamp(min, max)
} else {
fallback
}
}
fn component_padding_x(tokens: &DesignTokenSet, size: ComponentSize) -> f32 {
match size {
ComponentSize::XSmall => tokens.spacing.xs_px,
ComponentSize::Small => tokens.spacing.sm_px,
ComponentSize::Regular => tokens.spacing.md_px,
ComponentSize::Large => tokens.spacing.lg_px,
ComponentSize::XLarge => tokens.spacing.xl_px,
}
}
fn component_padding_y(tokens: &DesignTokenSet, size: ComponentSize) -> f32 {
match size {
ComponentSize::XSmall => tokens.spacing.xxs_px,
ComponentSize::Small => tokens.spacing.xs_px,
ComponentSize::Regular => tokens.spacing.sm_px,
ComponentSize::Large => tokens.spacing.md_px,
ComponentSize::XLarge => tokens.spacing.lg_px,
}
}
fn component_gap(tokens: &DesignTokenSet, size: ComponentSize) -> f32 {
match size {
ComponentSize::XSmall => tokens.spacing.xxs_px,
ComponentSize::Small => tokens.spacing.xs_px,
ComponentSize::Regular => tokens.spacing.sm_px,
ComponentSize::Large => tokens.spacing.md_px,
ComponentSize::XLarge => tokens.spacing.lg_px,
}
}
fn component_radius(tokens: &DesignTokenSet, size: ComponentSize) -> f32 {
match size {
ComponentSize::XSmall => tokens.radius.xs_px,
ComponentSize::Small => tokens.radius.sm_px,
ComponentSize::Regular => tokens.radius.md_px,
ComponentSize::Large => tokens.radius.lg_px,
ComponentSize::XLarge => tokens.radius.xl_px,
}
}
fn component_icon_px(tokens: &DesignTokenSet, size: ComponentSize) -> f32 {
match size {
ComponentSize::XSmall | ComponentSize::Small => tokens.icon.sm_px,
ComponentSize::Regular => tokens.icon.md_px,
ComponentSize::Large | ComponentSize::XLarge => tokens.icon.lg_px,
}
}
fn component_material_descriptor(
tokens: &DesignTokenSet,
variant: WidgetVariant,
state: WidgetState,
) -> WidgetMaterialDescriptor {
let mut descriptor = match variant {
WidgetVariant::Filled | WidgetVariant::Tonal => {
WidgetMaterialDescriptor::elevated(if state.pressed { 2.0 } else { 5.0 })
}
WidgetVariant::Outline | WidgetVariant::Ghost => WidgetMaterialDescriptor::flat(),
WidgetVariant::Glass => WidgetMaterialDescriptor::glass(),
};
descriptor.blur_px = match variant {
WidgetVariant::Glass => tokens.blur.panel_px,
WidgetVariant::Filled | WidgetVariant::Tonal => tokens.blur.control_px * 0.25,
WidgetVariant::Outline | WidgetVariant::Ghost => 0.0,
};
descriptor.saturation = tokens.material.saturation;
descriptor.refraction = if variant == WidgetVariant::Glass {
tokens.material.refraction
} else {
0.0
};
descriptor.thickness_px = tokens.material.thickness_px;
descriptor.rim_intensity = tokens.material.rim_intensity;
descriptor.specular_intensity = tokens.material.specular_intensity;
descriptor.shadow = if matches!(variant, WidgetVariant::Outline | WidgetVariant::Ghost) {
WidgetMaterialShadow::none()
} else {
WidgetMaterialShadow {
color: tokens.shadow.color,
offset: Vec2::new(0.0, tokens.shadow.low_y_px),
blur_px: tokens.shadow.low_blur_px,
spread_px: 0.0,
}
};
if state.disabled {
descriptor = descriptor.with_quality(WidgetMaterialQuality::Fallback);
}
descriptor.sanitized()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn built_in_token_presets_have_stable_ids() {
let ids = built_in_token_presets()
.iter()
.map(|preset| preset.id.clone())
.collect::<Vec<_>>();
assert_eq!(
ids,
[
"midnight_pro",
"graphite_glass",
"silver_studio",
"audio_console_dark",
"audio_console_light",
"developer_dark",
"ai_workspace",
"dashboard_pro",
]
);
}
#[test]
fn token_presets_have_readable_text_and_finite_metrics() {
for preset in built_in_token_presets() {
assert!(preset.colors.text.contrast_ratio(preset.colors.background) >= 4.5);
assert!(preset.spacing.md_px.is_finite());
assert!(preset.radius.lg_px.is_finite());
assert!(preset.blur.panel_px.is_finite());
assert!(preset
.component_height_px(ComponentSize::Regular)
.is_finite());
assert!(preset.audio.knob_diameter_px >= 18.0);
}
}
#[test]
fn density_scales_component_heights() {
let compact = token_preset(
"compact",
"Compact",
Density::Compact,
PlatformStyle::CrossPlatform,
Palette::midnight(),
Rgba::WHITE,
Rgba::WHITE,
);
let spacious = token_preset(
"spacious",
"Spacious",
Density::Spacious,
PlatformStyle::CrossPlatform,
Palette::midnight(),
Rgba::WHITE,
Rgba::WHITE,
);
assert!(compact.component_height_px(ComponentSize::Regular) < 40.0);
assert!(spacious.component_height_px(ComponentSize::Regular) > 40.0);
}
#[test]
fn malformed_public_token_values_are_sanitized() {
let mut preset = built_in_token_presets().remove(0);
preset.id = "\u{200f}\x1b[31m".to_string();
preset.spacing.md_px = f32::NAN;
preset.radius.lg_px = f32::INFINITY;
preset.material.panel_opacity = 3.0;
let preset = preset.sanitized();
assert!(!preset.id.is_empty());
assert_eq!(preset.spacing.md_px, 16.0);
assert_eq!(preset.radius.lg_px, 22.0);
assert_eq!(preset.material.panel_opacity, 1.0);
}
#[test]
fn component_defaults_resolve_token_driven_style_and_material() {
let preset = built_in_token_presets()
.into_iter()
.find(|preset| preset.id == "audio_console_dark")
.unwrap();
let defaults = preset.component_defaults(
WidgetVariant::Glass,
ComponentSize::Large,
ComponentRole::Warning,
WidgetState::new().focused(true),
);
assert_eq!(defaults.widget_size, WidgetSize::Spacious);
assert_eq!(defaults.style.accent, preset.colors.warning);
assert_eq!(defaults.border_width_px, preset.border.focus_px);
assert_eq!(defaults.material.blur_px, preset.blur.panel_px * 0.68);
assert!(defaults.height_px > ComponentSize::Regular.base_height_px());
assert!(defaults.motion_ms > 0);
}
#[test]
fn disabled_component_defaults_use_fallback_material_quality() {
let preset = built_in_token_presets().remove(0);
let defaults = preset.component_defaults(
WidgetVariant::Glass,
ComponentSize::Regular,
ComponentRole::Primary,
WidgetState::new().disabled(true),
);
assert_eq!(defaults.material.quality, WidgetMaterialQuality::Fallback);
assert_eq!(defaults.material.blur_px, 0.0);
assert!(defaults.style.opacity < 1.0);
}
#[cfg(feature = "serde")]
#[test]
fn design_token_sets_round_trip_through_serde() {
let preset = built_in_token_presets().remove(0);
let encoded = serde_json::to_string(&preset).unwrap();
let decoded: DesignTokenSet = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.id, preset.id);
assert_eq!(decoded.density, Density::Comfortable);
assert_eq!(decoded.platform, PlatformStyle::CrossPlatform);
}
#[cfg(feature = "serde")]
#[test]
fn component_defaults_round_trip_through_serde() {
let preset = built_in_token_presets().remove(0);
let defaults = preset.component_defaults(
WidgetVariant::Tonal,
ComponentSize::Small,
ComponentRole::Accent,
WidgetState::new().hovered(true),
);
let encoded = serde_json::to_string(&defaults).unwrap();
let decoded: ComponentDefaults = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.size, ComponentSize::Small);
assert_eq!(decoded.role, ComponentRole::Accent);
assert!(decoded.style.state.hovered);
}
}