/// Elevation shadow scale. /// /// Shadows communicate visual hierarchy and z-depth. Use the named scale — /// it maps to appropriate platform-native elevation on each backend. /// Named shadow/elevation tokens. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Shadow { /// No shadow — flat, on-surface elements. None, /// Subtle drop shadow — slightly elevated cards. Sm, /// Standard card shadow — interactive elements. Md, /// Prominent shadow — dialogs, dropdowns, popovers. Lg, /// Maximum elevation — toasts, context menus, tooltips. Xl, } /// A fully-resolved shadow specification. #[derive(Debug, Clone)] pub struct ShadowSpec { /// X offset in dp. pub offset_x: f32, /// Y offset in dp (positive = down). pub offset_y: f32, /// Blur radius in dp. pub blur: f32, /// Spread radius in dp. pub spread: f32, /// Shadow color (RGBA). pub color: (u8, u8, u8, f32), } impl ShadowSpec { /// CSS box-shadow string for this spec. pub fn to_css(&self) -> String { format!( "{}px {}px {}px {}px rgba({},{},{},{})", self.offset_x, self.offset_y, self.blur, self.spread, self.color.0, self.color.1, self.color.2, self.color.3, ) } } /// Maps Shadow tokens to ShadowSpecs. #[derive(Debug, Clone)] pub struct ShadowScale { pub sm: ShadowSpec, pub md: ShadowSpec, pub lg: ShadowSpec, pub xl: ShadowSpec, } impl ShadowScale { /// Default light-mode shadow scale. pub fn light() -> Self { Self { sm: ShadowSpec { offset_x: 0.0, offset_y: 1.0, blur: 3.0, spread: 0.0, color: (0, 0, 0, 0.12), }, md: ShadowSpec { offset_x: 0.0, offset_y: 4.0, blur: 12.0, spread: -2.0, color: (0, 0, 0, 0.15), }, lg: ShadowSpec { offset_x: 0.0, offset_y: 8.0, blur: 24.0, spread: -4.0, color: (0, 0, 0, 0.18), }, xl: ShadowSpec { offset_x: 0.0, offset_y: 16.0, blur: 48.0, spread: -8.0, color: (0, 0, 0, 0.22), }, } } /// Default dark-mode shadow scale (more subtle, less visible on dark bg). pub fn dark() -> Self { Self { sm: ShadowSpec { offset_x: 0.0, offset_y: 1.0, blur: 3.0, spread: 0.0, color: (0, 0, 0, 0.3), }, md: ShadowSpec { offset_x: 0.0, offset_y: 4.0, blur: 12.0, spread: -2.0, color: (0, 0, 0, 0.4), }, lg: ShadowSpec { offset_x: 0.0, offset_y: 8.0, blur: 24.0, spread: -4.0, color: (0, 0, 0, 0.5), }, xl: ShadowSpec { offset_x: 0.0, offset_y: 16.0, blur: 48.0, spread: -8.0, color: (0, 0, 0, 0.6), }, } } /// Resolve a Shadow token to a ShadowSpec. /// Returns None for Shadow::None (no spec needed). pub fn resolve(&self, shadow: &Shadow) -> Option<&ShadowSpec> { match shadow { Shadow::None => None, Shadow::Sm => Some(&self.sm), Shadow::Md => Some(&self.md), Shadow::Lg => Some(&self.lg), Shadow::Xl => Some(&self.xl), } } } #[cfg(test)] mod tests { use super::*; #[test] fn shadow_none_resolves_to_none() { let scale = ShadowScale::light(); assert!(scale.resolve(&Shadow::None).is_none()); } #[test] fn shadow_sm_resolves() { let scale = ShadowScale::light(); let spec = scale.resolve(&Shadow::Sm).unwrap(); assert!(spec.blur > 0.0); } #[test] fn shadow_xl_has_larger_blur_than_sm() { let scale = ShadowScale::light(); let sm = scale.resolve(&Shadow::Sm).unwrap(); let xl = scale.resolve(&Shadow::Xl).unwrap(); assert!(xl.blur > sm.blur); } #[test] fn shadow_spec_to_css() { let spec = ShadowSpec { offset_x: 0.0, offset_y: 4.0, blur: 12.0, spread: -2.0, color: (0, 0, 0, 0.15), }; let css = spec.to_css(); assert!(css.contains("rgba(0,0,0,0.15)")); } #[test] fn dark_shadows_are_more_opaque() { let light = ShadowScale::light(); let dark = ShadowScale::dark(); let l_alpha = light.resolve(&Shadow::Md).unwrap().color.3; let d_alpha = dark.resolve(&Shadow::Md).unwrap().color.3; assert!(d_alpha > l_alpha); } }