From ef36c441366446db88c90d1d56460999ac5c3c2d Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 1 Apr 2026 13:04:09 +0900 Subject: [PATCH 1/5] Fix shadow issue --- Cargo.lock | 1 + libs/css/src/lib.rs | 1 + libs/css/src/theme_tokens.rs | 55 ++++++ .../src/extract_style/extract_static_style.rs | 37 +++- libs/extractor/src/extract_style/mod.rs | 2 +- .../extract_global_style_from_expression.rs | 4 +- .../extract_keyframes_from_expression.rs | 12 +- .../extract_style_from_expression.rs | 106 +++++++++-- .../src/extractor/extract_style_from_jsx.rs | 24 ++- .../extract_style_from_member_expression.rs | 25 ++- .../extractor/extract_style_from_styled.rs | 6 +- .../extractor/extract_style_from_stylex.rs | 8 + libs/extractor/src/lib.rs | 180 ++++++++++++++++++ ...nsive_length_token_literal_vs_array-2.snap | 30 +++ ...nsive_length_token_literal_vs_array-3.snap | 20 ++ ...nsive_length_token_literal_vs_array-4.snap | 30 +++ ...ponsive_length_token_literal_vs_array.snap | 30 +++ ...nsive_shadow_token_literal_vs_array-2.snap | 30 +++ ...nsive_shadow_token_literal_vs_array-3.snap | 20 ++ ...nsive_shadow_token_literal_vs_array-4.snap | 30 +++ ...ponsive_shadow_token_literal_vs_array.snap | 30 +++ libs/extractor/src/visit.rs | 5 +- libs/sheet/Cargo.toml | 1 + libs/sheet/src/lib.rs | 113 +++++++++-- libs/sheet/src/theme.rs | 102 ++++++++-- .../__snapshots__/index.browser.test.tsx.snap | 6 +- .../src/__tests__/css-loader.test.ts | 2 +- .../next-plugin/src/__tests__/loader.test.ts | 36 ++-- .../next-plugin/src/__tests__/plugin.test.ts | 27 +-- packages/next-plugin/src/css-loader.ts | 11 +- packages/next-plugin/src/loader.ts | 41 +++- packages/next-plugin/src/plugin.ts | 66 ++++--- packages/plugin-utils/src/types.ts | 1 + 33 files changed, 959 insertions(+), 133 deletions(-) create mode 100644 libs/css/src/theme_tokens.rs create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap diff --git a/Cargo.lock b/Cargo.lock index 3a17808e..feb649e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2186,6 +2186,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "serial_test", ] [[package]] diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 6801cc46..4f542229 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -9,6 +9,7 @@ pub mod optimize_value; pub mod rm_css_comment; mod selector_separator; pub mod style_selector; +pub mod theme_tokens; pub mod utils; use std::collections::BTreeMap; diff --git a/libs/css/src/theme_tokens.rs b/libs/css/src/theme_tokens.rs new file mode 100644 index 00000000..f7a18619 --- /dev/null +++ b/libs/css/src/theme_tokens.rs @@ -0,0 +1,55 @@ +use std::collections::BTreeMap; +use std::sync::{LazyLock, RwLock}; + +#[derive(Default, Debug)] +struct ThemeTokenRegistry { + length: BTreeMap>, + shadow: BTreeMap>, +} + +static TOKEN_REGISTRY: LazyLock> = + LazyLock::new(|| RwLock::new(ThemeTokenRegistry::default())); + +pub fn set_theme_token_levels( + length: BTreeMap>, + shadow: BTreeMap>, +) { + if let Ok(mut registry) = TOKEN_REGISTRY.write() { + registry.length = length; + registry.shadow = shadow; + } +} + +/// Look up a `$token` in the length and shadow registries. +/// Returns the responsive breakpoint levels if the token is defined +/// with more than one level, regardless of which CSS property it's used on. +pub fn get_responsive_theme_token(value: &str) -> Option> { + let token = value.strip_prefix('$')?; + let registry = TOKEN_REGISTRY.read().ok()?; + + registry + .length + .get(token) + .or_else(|| registry.shadow.get(token)) + .filter(|levels| levels.len() > 1) + .cloned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_responsive_theme_token() { + let mut length = BTreeMap::new(); + length.insert("containerX".to_string(), vec![0, 2]); + let mut shadow = BTreeMap::new(); + shadow.insert("card".to_string(), vec![0, 3]); + set_theme_token_levels(length, shadow); + + assert_eq!(get_responsive_theme_token("$containerX"), Some(vec![0, 2])); + assert_eq!(get_responsive_theme_token("$card"), Some(vec![0, 3])); + assert_eq!(get_responsive_theme_token("$unknown"), None); + assert_eq!(get_responsive_theme_token("noprefix"), None); + } +} diff --git a/libs/extractor/src/extract_style/extract_static_style.rs b/libs/extractor/src/extract_style/extract_static_style.rs index abe853cb..940a7e51 100644 --- a/libs/extractor/src/extract_style/extract_static_style.rs +++ b/libs/extractor/src/extract_style/extract_static_style.rs @@ -1,3 +1,5 @@ +use std::fmt::{Debug, Formatter}; + use css::{ optimize_multi_css_value::{check_multi_css_optimize, optimize_mutli_css_value}, optimize_value::optimize_value, @@ -12,7 +14,14 @@ use crate::{ utils::{convert_value, gcd}, }; -#[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] +#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash, Ord, PartialOrd, Default)] +pub enum ThemeTokenResolution { + #[default] + CssVariable, + FirstValue, +} + +#[derive(PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] pub struct ExtractStaticStyle { /// property pub property: String, @@ -26,6 +35,21 @@ pub struct ExtractStaticStyle { pub style_order: Option, /// CSS layer name (from vanilla-extract layer()) pub layer: Option, + /// How theme tokens should be resolved when converting to CSS. + pub theme_token_resolution: ThemeTokenResolution, +} + +impl Debug for ExtractStaticStyle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExtractStaticStyle") + .field("property", &self.property) + .field("value", &self.value) + .field("level", &self.level) + .field("selector", &self.selector) + .field("style_order", &self.style_order) + .field("layer", &self.layer) + .finish() + } } impl ExtractStaticStyle { @@ -55,6 +79,7 @@ impl ExtractStaticStyle { selector: selector.map(optimize_selector), style_order: None, layer: None, + theme_token_resolution: ThemeTokenResolution::CssVariable, } } @@ -88,9 +113,15 @@ impl ExtractStaticStyle { selector, style_order: Some(0), layer: None, + theme_token_resolution: ThemeTokenResolution::CssVariable, } } + pub fn with_theme_token_resolution(mut self, resolution: ThemeTokenResolution) -> Self { + self.theme_token_resolution = resolution; + self + } + /// Get the layer name pub fn layer(&self) -> Option<&str> { self.layer.as_deref() @@ -115,6 +146,10 @@ impl ExtractStaticStyle { pub fn style_order(&self) -> Option { self.style_order } + + pub fn theme_token_resolution(&self) -> ThemeTokenResolution { + self.theme_token_resolution + } } impl ExtractStyleProperty for ExtractStaticStyle { diff --git a/libs/extractor/src/extract_style/mod.rs b/libs/extractor/src/extract_style/mod.rs index 2d02c05b..05d4646d 100644 --- a/libs/extractor/src/extract_style/mod.rs +++ b/libs/extractor/src/extract_style/mod.rs @@ -4,7 +4,7 @@ pub(super) mod extract_dynamic_style; pub(super) mod extract_font_face; pub(super) mod extract_import; pub(super) mod extract_keyframes; -pub(super) mod extract_static_style; +pub mod extract_static_style; pub mod extract_style_value; pub mod style_property; diff --git a/libs/extractor/src/extractor/extract_global_style_from_expression.rs b/libs/extractor/src/extractor/extract_global_style_from_expression.rs index 689cca78..3186e876 100644 --- a/libs/extractor/src/extractor/extract_global_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_global_style_from_expression.rs @@ -8,7 +8,8 @@ use crate::{ extract_style_value::ExtractStyleValue, }, extractor::{ - GlobalExtractResult, extract_style_from_expression::extract_style_from_expression, + GlobalExtractResult, + extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, }, utils::{get_string_by_literal_expression, get_string_by_property_key}, }; @@ -166,6 +167,7 @@ pub fn extract_global_style_from_expression<'a>( }, file.to_string(), )), + LiteralHandling::ExpandResponsiveThemeToken, ); // Filter out @layer property from styles and set layer on remaining styles diff --git a/libs/extractor/src/extractor/extract_keyframes_from_expression.rs b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs index 4d8f72d4..a13900ce 100644 --- a/libs/extractor/src/extractor/extract_keyframes_from_expression.rs +++ b/libs/extractor/src/extractor/extract_keyframes_from_expression.rs @@ -3,7 +3,7 @@ use crate::{ extract_style::{extract_keyframes::ExtractKeyframes, extract_style_value::ExtractStyleValue}, extractor::{ ExtractResult, KeyframesExtractResult, - extract_style_from_expression::extract_style_from_expression, + extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, }, utils::get_string_by_property_key, }; @@ -23,8 +23,14 @@ pub fn extract_keyframes_from_expression<'a>( if let ObjectPropertyKind::ObjectProperty(o) = p && let Some(name) = get_string_by_property_key(&o.key) { - let ExtractResult { styles, .. } = - extract_style_from_expression(ast_builder, None, &mut o.value, 0, &None); + let ExtractResult { styles, .. } = extract_style_from_expression( + ast_builder, + None, + &mut o.value, + 0, + &None, + LiteralHandling::ExpandResponsiveThemeToken, + ); let mut styles = styles .into_iter() diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs index 32ab7f37..55e4c5b6 100644 --- a/libs/extractor/src/extractor/extract_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_expression.rs @@ -2,7 +2,8 @@ use crate::{ ExtractStyleProp, css_utils::{css_to_style, css_to_style_literal}, extract_style::{ - extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle, + extract_dynamic_style::ExtractDynamicStyle, + extract_static_style::{ExtractStaticStyle, ThemeTokenResolution}, extract_style_value::ExtractStyleValue, }, extractor::{ @@ -15,7 +16,8 @@ use crate::{ }; use css::{ add_selector_params, disassemble_property, get_enum_property_map, get_enum_property_value, - is_special_property::is_special_property, style_selector::StyleSelector, utils::to_kebab_case, + is_special_property::is_special_property, style_selector::StyleSelector, + theme_tokens::get_responsive_theme_token, utils::to_kebab_case, }; use oxc_allocator::CloneIn; use oxc_ast::{ @@ -29,12 +31,47 @@ use oxc_span::SPAN; const IGNORED_IDENTIFIERS: [&str; 3] = ["undefined", "NaN", "Infinity"]; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LiteralHandling { + ExpandResponsiveThemeToken, + KeepSingleClass, +} + +fn create_static_styles<'a>( + name: &str, + value: &str, + levels: &[u8], + selector: &Option, + resolution: ThemeTokenResolution, +) -> Vec> { + let mut styles = Vec::new(); + + for &level in levels { + if let Some(map) = get_enum_property_value(name, value) { + styles.extend(map.into_iter().map(|(k, v)| { + ExtractStyleProp::Static(ExtractStyleValue::Static( + ExtractStaticStyle::new(&k, &v, level, selector.clone()) + .with_theme_token_resolution(resolution), + )) + })); + } else { + styles.push(ExtractStyleProp::Static(ExtractStyleValue::Static( + ExtractStaticStyle::new(name, value, level, selector.clone()) + .with_theme_token_resolution(resolution), + ))); + } + } + + styles +} + pub fn extract_style_from_expression<'a>( ast_builder: &AstBuilder<'a>, name: Option<&str>, expression: &mut Expression<'a>, level: u8, selector: &Option, + literal_handling: LiteralHandling, ) -> ExtractResult<'a> { let mut typo = false; @@ -72,6 +109,7 @@ pub fn extract_style_from_expression<'a>( &mut prop.value, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); props_styles.extend(styles); tag = _tag.or(tag); @@ -91,6 +129,7 @@ pub fn extract_style_from_expression<'a>( &mut prop.argument, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); props_styles.extend(styles); tag = _tag.or(tag); @@ -119,6 +158,7 @@ pub fn extract_style_from_expression<'a>( &mut conditional.consequent, level, &None, + literal_handling, ) .styles, ))), @@ -129,6 +169,7 @@ pub fn extract_style_from_expression<'a>( &mut conditional.alternate, level, selector, + literal_handling, ) .styles, ))), @@ -143,6 +184,7 @@ pub fn extract_style_from_expression<'a>( &mut parenthesized.expression, level, &None, + literal_handling, ), Expression::TemplateLiteral(tmp) => ExtractResult { styles: css_to_style_literal(tmp, level, selector) @@ -227,6 +269,7 @@ pub fn extract_style_from_expression<'a>( &mut o.value, level, &Some(StyleSelector::Selector(sel)), + literal_handling, ) .styles, ); @@ -265,6 +308,7 @@ pub fn extract_style_from_expression<'a>( &mut o.value, level, &Some(at_selector), + literal_handling, ) .styles, ); @@ -287,6 +331,7 @@ pub fn extract_style_from_expression<'a>( } else { new_selector.into() }), + literal_handling, ); } typo = name == "typography"; @@ -299,19 +344,43 @@ pub fn extract_style_from_expression<'a>( value.to_string(), ))] } else { - // Create a new ExtractStaticStyle - if let Some(map) = get_enum_property_value(name, &value) { - map.into_iter() - .map(|(k, v)| { - ExtractStyleProp::Static(ExtractStyleValue::Static( - ExtractStaticStyle::new(&k, &v, level, selector.clone()), - )) - }) - .collect() + if matches!( + literal_handling, + LiteralHandling::ExpandResponsiveThemeToken + ) { + if let Some(levels) = get_responsive_theme_token(&value) { + create_static_styles( + name, + &value, + &levels, + selector, + ThemeTokenResolution::CssVariable, + ) + } else { + create_static_styles( + name, + &value, + &[level], + selector, + ThemeTokenResolution::CssVariable, + ) + } + } else if get_responsive_theme_token(&value).is_some() { + create_static_styles( + name, + &value, + &[level], + selector, + ThemeTokenResolution::FirstValue, + ) } else { - vec![ExtractStyleProp::Static(ExtractStyleValue::Static( - ExtractStaticStyle::new(name, &value, level, selector.clone()), - ))] + create_static_styles( + name, + &value, + &[level], + selector, + ThemeTokenResolution::CssVariable, + ) } }, ..ExtractResult::default() @@ -359,6 +428,7 @@ pub fn extract_style_from_expression<'a>( &mut exp.expression, level, selector, + literal_handling, ), Expression::ComputedMemberExpression(mem) => { extract_style_from_member_expression(ast_builder, name, mem, level, selector) @@ -470,6 +540,7 @@ pub fn extract_style_from_expression<'a>( &mut logical.right, level, selector, + literal_handling, ) .styles, ))); @@ -515,6 +586,7 @@ pub fn extract_style_from_expression<'a>( &mut logical.left, level, selector, + literal_handling, ) .styles, ))), @@ -530,6 +602,7 @@ pub fn extract_style_from_expression<'a>( &mut parenthesized.expression, level, selector, + literal_handling, ), Expression::ArrayExpression(array) => { let mut props = vec![]; @@ -543,6 +616,7 @@ pub fn extract_style_from_expression<'a>( element, idx as u8, selector, + LiteralHandling::KeepSingleClass, ) .styles, ); @@ -564,6 +638,7 @@ pub fn extract_style_from_expression<'a>( &mut conditional.consequent, level, selector, + literal_handling, ) } else { ExtractResult { @@ -576,6 +651,7 @@ pub fn extract_style_from_expression<'a>( &mut conditional.consequent, level, selector, + literal_handling, ) .styles, ))), @@ -586,6 +662,7 @@ pub fn extract_style_from_expression<'a>( &mut conditional.alternate, level, selector, + literal_handling, ) .styles, ))), @@ -639,6 +716,7 @@ pub fn extract_style_from_expression<'a>( &mut o.value, level, &selector, + literal_handling, ) .styles, ); diff --git a/libs/extractor/src/extractor/extract_style_from_jsx.rs b/libs/extractor/src/extractor/extract_style_from_jsx.rs index b77b6d81..9bae9edd 100644 --- a/libs/extractor/src/extractor/extract_style_from_jsx.rs +++ b/libs/extractor/src/extractor/extract_style_from_jsx.rs @@ -1,5 +1,6 @@ use crate::extractor::{ - ExtractResult, extract_style_from_expression::extract_style_from_expression, + ExtractResult, + extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, }; use oxc_allocator::CloneIn; use oxc_ast::{ @@ -12,7 +13,7 @@ pub fn extract_style_from_jsx<'a>( name: &str, value: &mut JSXAttributeValue<'a>, ) -> ExtractResult<'a> { - match value { + let expression = match value { JSXAttributeValue::ExpressionContainer(expression) => expression .expression .as_expression() @@ -21,9 +22,18 @@ pub fn extract_style_from_jsx<'a>( literal.clone_in(ast_builder.allocator), )), _ => None, - } - .map(|mut expression| { - extract_style_from_expression(ast_builder, Some(name), &mut expression, 0, &None) - }) - .unwrap_or_default() + }; + + expression + .map(|mut expression| { + extract_style_from_expression( + ast_builder, + Some(name), + &mut expression, + 0, + &None, + LiteralHandling::ExpandResponsiveThemeToken, + ) + }) + .unwrap_or_default() } diff --git a/libs/extractor/src/extractor/extract_style_from_member_expression.rs b/libs/extractor/src/extractor/extract_style_from_member_expression.rs index 066d4970..d2ca8966 100644 --- a/libs/extractor/src/extractor/extract_style_from_member_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs @@ -2,7 +2,9 @@ use crate::{ ExtractStyleProp, extractor::{ ExtractResult, - extract_style_from_expression::{dynamic_style, extract_style_from_expression}, + extract_style_from_expression::{ + LiteralHandling, dynamic_style, extract_style_from_expression, + }, }, utils::{ get_number_by_literal_expression, get_string_by_literal_expression, @@ -53,7 +55,14 @@ pub(super) fn extract_style_from_member_expression<'a>( } else if idx as f64 == num && let Some(p) = p.as_expression_mut() { - return extract_style_from_expression(ast_builder, name, p, level, selector); + return extract_style_from_expression( + ast_builder, + name, + p, + level, + selector, + LiteralHandling::ExpandResponsiveThemeToken, + ); } } return ExtractResult { @@ -106,7 +115,15 @@ pub(super) fn extract_style_from_member_expression<'a>( map.insert( idx.to_string(), Box::new(ExtractStyleProp::StaticArray( - extract_style_from_expression(ast_builder, name, p, level, selector).styles, + extract_style_from_expression( + ast_builder, + name, + p, + level, + selector, + LiteralHandling::ExpandResponsiveThemeToken, + ) + .styles, )), ); } @@ -134,6 +151,7 @@ pub(super) fn extract_style_from_member_expression<'a>( &mut o.value, level, selector, + LiteralHandling::ExpandResponsiveThemeToken, ) .styles, ..ExtractResult::default() @@ -176,6 +194,7 @@ pub(super) fn extract_style_from_member_expression<'a>( &mut o.value, level, selector, + LiteralHandling::ExpandResponsiveThemeToken, ) .styles, )), diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index b36c3d26..2f814143 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -5,7 +5,10 @@ use crate::{ component::ExportVariableKind, css_utils::css_to_style_literal, extract_style::extract_style_value::ExtractStyleValue, - extractor::{ExtractResult, extract_style_from_expression::extract_style_from_expression}, + extractor::{ + ExtractResult, + extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, + }, gen_class_name::gen_class_names, gen_style::gen_styles, utils::{merge_object_expressions, wrap_array_filter}, @@ -124,6 +127,7 @@ pub fn extract_style_from_styled<'a>( }, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); if let Some(default_class_name) = default_class_name { styles.extend(default_class_name.into_iter().map(ExtractStyleProp::Static)); diff --git a/libs/extractor/src/extractor/extract_style_from_stylex.rs b/libs/extractor/src/extractor/extract_style_from_stylex.rs index f922ab84..032f19f4 100644 --- a/libs/extractor/src/extractor/extract_style_from_stylex.rs +++ b/libs/extractor/src/extractor/extract_style_from_stylex.rs @@ -144,6 +144,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: decomposed.selector, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); } @@ -194,6 +195,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); continue; @@ -215,6 +217,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: decomposed.selector, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); } @@ -236,6 +239,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); } @@ -260,6 +264,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); continue; @@ -284,6 +289,7 @@ pub fn extract_stylex_namespace_styles<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); } @@ -386,6 +392,7 @@ fn extract_stylex_dynamic_namespace<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); continue; @@ -403,6 +410,7 @@ fn extract_stylex_dynamic_namespace<'a>( selector: None, style_order: None, layer: None, + theme_token_resolution: Default::default(), }, ))); } diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 374bb74d..3bd0158e 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -16544,4 +16544,184 @@ const composed = stylex.create({ combined: { ...stylex.include(base.root) } });" .unwrap() )); } + + #[test] + #[serial] + fn test_responsive_length_token_literal_vs_array() { + use css::theme_tokens::set_theme_token_levels; + + let mut length = BTreeMap::new(); + length.insert("containerX".to_string(), vec![0, 2]); + set_theme_token_levels(length, BTreeMap::new()); + + // String literal: w="$containerX" → expands to multiple breakpoint classes + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Expression: w={"$containerX"} → also expands (same as string literal) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Array with single element: w={["$containerX"]} → single class, base value only + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Mixed array: w={["1px", null, "$containerX"]} → token inside array stays single per slot + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn test_responsive_shadow_token_literal_vs_array() { + use css::theme_tokens::set_theme_token_levels; + + let mut shadow = BTreeMap::new(); + shadow.insert("card".to_string(), vec![0, 3]); + set_theme_token_levels(BTreeMap::new(), shadow); + + // String literal: boxShadow="$card" → expands to multiple breakpoint classes + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Expression: boxShadow={"$card"} → also expands (same as string literal) + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Array with single element: boxShadow={["$card"]} → single class, base value only + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + + // Mixed array: boxShadow={["none", null, null, "$card"]} → token inside array stays single per slot + reset_class_map(); + reset_file_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/react' + + "#, + ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } } diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap new file mode 100644 index 00000000..59d0c728 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-2.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16580 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 2, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap new file mode 100644 index 00000000..9fa5d699 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-3.snap @@ -0,0 +1,20 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16600 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap new file mode 100644 index 00000000..a1d1c25f --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array-4.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16620 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 2, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "width", + value: "1px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap new file mode 100644 index 00000000..a2699333 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_length_token_literal_vs_array.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16560 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "width", + value: "$containerX", + level: 2, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap new file mode 100644 index 00000000..0a10d8d2 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-2.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16650 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 3, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap new file mode 100644 index 00000000..c68ff47d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-3.snap @@ -0,0 +1,20 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16670 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap new file mode 100644 index 00000000..27842963 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array-4.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16710 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 3, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "none", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap new file mode 100644 index 00000000..cf43cb3b --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__responsive_shadow_token_literal_vs_array.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +assertion_line: 16630 +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/react'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), + Static( + ExtractStaticStyle { + property: "box-shadow", + value: "$card", + level: 3, + selector: None, + style_order: None, + layer: None, + }, + ), + }, + code: "import \"@devup-ui/react/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index b057b88a..96e715ab 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -10,7 +10,7 @@ use crate::extractor::extract_style_from_stylex::extract_stylex_namespace_styles use crate::extractor::{ ExtractResult, GlobalExtractResult, extract_global_style_from_expression::extract_global_style_from_expression, - extract_style_from_expression::extract_style_from_expression, + extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, extract_style_from_jsx::extract_style_from_jsx, extract_style_from_styled::extract_style_from_styled, }; @@ -603,6 +603,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { }, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); if styles.is_empty() { @@ -800,6 +801,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { it.arguments[1].to_expression_mut(), 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); props_styles.extend(styles); @@ -1135,6 +1137,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { &mut spread.argument, 0, &None, + LiteralHandling::ExpandResponsiveThemeToken, ); if !styles.is_empty() { props_styles.extend(styles.into_iter().rev()); diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 959b9dd2..c0d6b903 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -15,6 +15,7 @@ rustc-hash = "2" insta = "1.46.3" criterion = { version = "0.8", features = ["html_reports"] } rstest = "0.26.1" +serial_test = "3.4.0" [[bench]] name = "my_benchmark" diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index f0e1a194..43913527 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -2,10 +2,12 @@ pub mod theme; use crate::theme::Theme; use css::{ - merge_selector, + merge_selector, sheet_to_classname, style_selector::{AtRuleKind, StyleSelector}, + theme_tokens::set_theme_token_levels, }; use extractor::extract_style::ExtractStyleProperty; +use extractor::extract_style::extract_static_style::ThemeTokenResolution; use extractor::extract_style::extract_style_value::ExtractStyleValue; use extractor::extract_style::style_property::StyleProperty; use regex_lite::Regex; @@ -324,6 +326,10 @@ impl StyleSheet { } pub fn set_theme(&mut self, theme: Theme) { + set_theme_token_levels( + theme.get_length_token_levels(), + theme.get_shadow_token_levels(), + ); self.theme = theme; } @@ -338,19 +344,57 @@ impl StyleSheet { for style in styles.iter() { match style { ExtractStyleValue::Static(st) => { - if let Some(StyleProperty::ClassName(cls)) = - style.extract(if !single_css { Some(filename) } else { None }) - && self.add_property_with_layer( - &cls, - st.property(), - st.level(), - st.value(), - st.selector(), - st.style_order(), - if !single_css { Some(filename) } else { None }, - st.layer(), - ) - { + let resolved_value = + if st.theme_token_resolution() == ThemeTokenResolution::FirstValue { + if let Some(token) = st.value().strip_prefix('$') { + match st.property() { + "box-shadow" => self + .theme + .get_default_shadow_value(token) + .map(str::to_string) + .unwrap_or_else(|| st.value().to_string()), + _ => self + .theme + .get_default_length_value(token) + .map(str::to_string) + .unwrap_or_else(|| st.value().to_string()), + } + } else { + st.value().to_string() + } + } else { + st.value().to_string() + }; + + let class_name = + if st.theme_token_resolution() == ThemeTokenResolution::FirstValue { + let selector = st.selector().map(ToString::to_string); + sheet_to_classname( + st.property(), + st.level(), + Some(&resolved_value), + selector.as_deref(), + st.style_order(), + if !single_css { Some(filename) } else { None }, + ) + } else if let Some(StyleProperty::ClassName(cls)) = + style.extract(if !single_css { Some(filename) } else { None }) + { + cls + } else { + continue; + }; + + if self.add_property_with_layer( + &class_name, + st.property(), + st.level(), + &resolved_value, + st.selector(), + st.style_order(), + if !single_css { Some(filename) } else { None }, + st.layer(), + ) { collected = true; if st.style_order() == Some(0) { updated_base_style = true; @@ -850,10 +894,16 @@ mod tests { use crate::theme::{ColorTheme, Typography}; use super::*; + use css::{class_map::reset_class_map, file_map::reset_file_map}; + use extractor::extract_style::extract_static_style::{ + ExtractStaticStyle, ThemeTokenResolution, + }; use extractor::{ExtractOption, extract}; use insta::assert_debug_snapshot; use rstest::rstest; + use rustc_hash::FxHashSet; + use serial_test::serial; #[rstest] #[case("1px", "1px")] @@ -2069,7 +2119,10 @@ mod tests { } #[test] + #[serial] fn test_update_styles() { + reset_class_map(); + reset_file_map(); let mut sheet = StyleSheet::default(); sheet.update_styles(&FxHashSet::default(), "index.tsx", true); assert_debug_snapshot!( @@ -2275,4 +2328,36 @@ mod tests { assert!(css.contains("opacity:1;transform:scale(1)")); assert_debug_snapshot!(css.split("*/").nth(1).unwrap()); } + + #[test] + #[serial] + fn test_first_value_theme_token_resolution_uses_base_value_only() { + reset_class_map(); + reset_file_map(); + let mut sheet = StyleSheet::default(); + let theme: Theme = serde_json::from_str( + r#"{ + "length": { + "default": { + "containerX": ["1px", null, "2px"] + } + } + }"#, + ) + .unwrap(); + sheet.set_theme(theme); + + let mut styles = FxHashSet::default(); + styles.insert(ExtractStyleValue::Static( + ExtractStaticStyle::new("width", "$containerX", 0, None) + .with_theme_token_resolution(ThemeTokenResolution::FirstValue), + )); + + let (collected, _) = sheet.update_styles(&styles, "test.tsx", true); + assert!(collected); + + let css = sheet.create_css(None, false); + assert!(css.contains("width:1px")); + assert!(!css.contains("width:2px")); + } } diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 700b49b9..5ede14ba 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -382,6 +382,15 @@ pub type LengthTheme = BTreeMap; /// e.g., { "sm": "0 1px 2px rgba(0,0,0,0.1)", "md": ["0 2px 4px rgba(0,0,0,0.1)", null, "0 4px 8px rgba(0,0,0,0.2)"] } pub type ShadowTheme = BTreeMap; +fn default_variant_key(themes: &BTreeMap) -> Option<&str> { + themes + .keys() + .find(|k| *k == "default") + .or_else(|| themes.keys().find(|k| *k == "light")) + .or_else(|| themes.keys().next()) + .map(String::as_str) +} + /// Convert a JSON number to a length value: `n * 4` + "px". fn number_to_length(n: &serde_json::Number) -> String { // as_f64() covers both integer and float JSON numbers @@ -451,7 +460,7 @@ pub struct Theme { pub typography: BTreeMap, #[serde(default, deserialize_with = "deserialize_length_themes")] pub length: BTreeMap, - #[serde(default)] + #[serde(default, alias = "shadow")] pub shadows: BTreeMap, } @@ -506,16 +515,63 @@ impl Theme { } pub fn get_default_theme(&self) -> Option { - self.colors - .keys() - .find(|k| *k == "default") - .or_else(|| { - self.colors - .keys() - .find(|k| *k == "light") - .or_else(|| self.colors.keys().next()) - }) - .cloned() + default_variant_key(&self.colors).map(str::to_string) + } + + pub fn get_length_token_levels(&self) -> BTreeMap> { + self.length.values().flat_map(|theme| theme.iter()).fold( + BTreeMap::>::new(), + |mut acc, (name, values)| { + let entry = acc.entry(name.clone()).or_default(); + for (idx, value) in values.0.iter().enumerate() { + if value.is_some() + && let Ok(level) = u8::try_from(idx) + && !entry.contains(&level) + { + entry.push(level); + } + } + acc + }, + ) + } + + pub fn get_shadow_token_levels(&self) -> BTreeMap> { + self.shadows.values().flat_map(|theme| theme.iter()).fold( + BTreeMap::>::new(), + |mut acc, (name, values)| { + let entry = acc.entry(name.clone()).or_default(); + for (idx, value) in values.0.iter().enumerate() { + if value.is_some() + && let Ok(level) = u8::try_from(idx) + && !entry.contains(&level) + { + entry.push(level); + } + } + acc + }, + ) + } + + pub fn get_default_length_value(&self, token: &str) -> Option<&str> { + let default_key = default_variant_key(&self.length)?; + self.length + .get(default_key)? + .get(token)? + .0 + .first()? + .as_deref() + } + + pub fn get_default_shadow_value(&self, token: &str) -> Option<&str> { + let default_key = default_variant_key(&self.shadows)?; + self.shadows + .get(default_key)? + .get(token)? + .0 + .first()? + .as_deref() } pub fn to_css(&self) -> String { @@ -2185,4 +2241,28 @@ mod tests { let css = theme.to_css(); assert_debug_snapshot!(css); } + + #[test] + fn test_shadow_alias_deserializes_to_shadows() { + let theme: Theme = serde_json::from_str( + r#"{ + "shadow": { + "light": { + "card": ["0 1px 2px #0003", null, "0 4px 8px #0003"] + } + } + }"#, + ) + .unwrap(); + + let shadow = theme.shadows.get("light").unwrap().get("card").unwrap(); + assert_eq!( + shadow.0, + vec![ + Some("0 1px 2px #0003".to_string()), + None, + Some("0 4px 8px #0003".to_string()) + ] + ); + } } diff --git a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap index 88fd7f30..065451e1 100644 --- a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -201,7 +201,7 @@ exports[`Button should render loading spinner when loading is true 1`] = `