From ceda3dd7e64940eaa8f0a4756c3beaf8f0b4c22b Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 9 Jan 2026 01:33:48 +0900 Subject: [PATCH 1/4] Support supports rule --- libs/css/src/lib.rs | 22 +- libs/css/src/style_selector.rs | 105 ++++++-- libs/extractor/src/css_utils.rs | 251 +++++++++++++----- ...extract_static_css_with_media_query-2.snap | 8 +- ...extract_static_css_with_media_query-3.snap | 5 +- ...__extract_static_css_with_media_query.snap | 5 +- libs/sheet/src/lib.rs | 83 ++++-- .../sheet__tests__print_selector.snap | 2 +- .../sheet__tests__selector_with_supports.snap | 5 + 9 files changed, 348 insertions(+), 138 deletions(-) create mode 100644 libs/sheet/src/snapshots/sheet__tests__selector_with_supports.snap diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index ec7d891b..740a3e52 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -40,7 +40,7 @@ pub fn merge_selector(class_name: &str, selector: Option<&StyleSelector>) -> Str if let Some(selector) = selector { match selector { StyleSelector::Selector(value) => value.replace("&", &format!(".{class_name}")), - StyleSelector::Media { selector: s, .. } => { + StyleSelector::At { selector: s, .. } => { if let Some(s) = s { s.replace("&", &format!(".{class_name}")) } else { @@ -85,7 +85,12 @@ pub fn add_selector_params(selector: StyleSelector, params: &str) -> StyleSelect StyleSelector::Global(value, file) => { StyleSelector::Global(format!("{}({})", value, params), file) } - StyleSelector::Media { query, selector } => StyleSelector::Media { + StyleSelector::At { + kind, + query, + selector, + } => StyleSelector::At { + kind, query: query.to_string(), selector: selector.map(|s| format!("{}({})", s, params)), }, @@ -317,6 +322,7 @@ mod tests { use crate::{ class_map::{get_class_map, reset_class_map, set_class_map}, debug::set_debug, + style_selector::AtRuleKind, }; use super::*; @@ -721,7 +727,8 @@ mod tests { assert_eq!( merge_selector( "cls", - Some(&StyleSelector::Media { + Some(&StyleSelector::At { + kind: AtRuleKind::Media, query: "print".to_string(), selector: None }) @@ -732,7 +739,8 @@ mod tests { assert_eq!( merge_selector( "cls", - Some(&StyleSelector::Media { + Some(&StyleSelector::At { + kind: AtRuleKind::Media, query: "print".to_string(), selector: Some("&:hover".to_string()) }) @@ -796,13 +804,15 @@ mod tests { ); assert_eq!( add_selector_params( - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "print".to_string(), selector: Some("&:is".to_string()) }, "test" ), - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "print".to_string(), selector: Some("&:is(test)".to_string()) } diff --git a/libs/css/src/style_selector.rs b/libs/css/src/style_selector.rs index e070a887..6ab526fc 100644 --- a/libs/css/src/style_selector.rs +++ b/libs/css/src/style_selector.rs @@ -10,9 +10,28 @@ use crate::{ utils::to_camel_case, }; +#[derive( + Debug, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Eq, Serialize, Deserialize, Default, +)] +pub enum AtRuleKind { + #[default] + Media, + Supports, +} + +impl Display for AtRuleKind { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + AtRuleKind::Media => write!(f, "media"), + AtRuleKind::Supports => write!(f, "supports"), + } + } +} + #[derive(Debug, PartialEq, Clone, Hash, Eq, Serialize, Deserialize)] pub enum StyleSelector { - Media { + At { + kind: AtRuleKind, query: String, selector: Option, }, @@ -30,7 +49,12 @@ fn optimize_selector_string(selector: &str) -> String { } pub fn optimize_selector(selector: StyleSelector) -> StyleSelector { match selector { - StyleSelector::Media { query, selector } => StyleSelector::Media { + StyleSelector::At { + kind, + query, + selector, + } => StyleSelector::At { + kind, query: query.to_string(), selector: selector .as_ref() @@ -54,15 +78,21 @@ impl Ord for StyleSelector { fn cmp(&self, other: &Self) -> Ordering { match (self, other) { ( - StyleSelector::Media { + StyleSelector::At { + kind: ka, query: a, selector: aa, }, - StyleSelector::Media { + StyleSelector::At { + kind: kb, query: b, selector: bb, }, ) => { + let k = (*ka as u8).cmp(&(*kb as u8)); + if k != Ordering::Equal { + return k; + } let c = a.cmp(b); if c == Ordering::Equal { aa.cmp(bb) } else { c } } @@ -74,20 +104,8 @@ impl Ord for StyleSelector { order_cmp } } - ( - StyleSelector::Media { - selector: _, - query: _, - }, - StyleSelector::Selector(_), - ) => Ordering::Greater, - ( - StyleSelector::Selector(_), - StyleSelector::Media { - selector: _, - query: _, - }, - ) => Ordering::Less, + (StyleSelector::At { .. }, StyleSelector::Selector(_)) => Ordering::Greater, + (StyleSelector::Selector(_), StyleSelector::At { .. }) => Ordering::Less, (StyleSelector::Global(a, _), StyleSelector::Global(b, _)) => { if a == b { return Ordering::Equal; @@ -144,7 +162,8 @@ impl From<&str> for StyleSelector { // first character should lower case StyleSelector::Selector(format!(":root[data-theme={}] &", to_camel_case(s))) } else if value == "print" { - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "print".to_string(), selector: None, } @@ -201,11 +220,16 @@ impl Display for StyleSelector { "{}", match self { StyleSelector::Selector(value) => value.to_string(), - StyleSelector::Media { query, selector } => { + StyleSelector::At { + kind, + query, + selector, + } => { + let space = if query.starts_with('(') { "" } else { " " }; if let Some(selector) = selector { - format!("@{query} {selector}") + format!("@{kind}{space}{query} {selector}") } else { - format!("@{query}") + format!("@{kind}{space}{query}") } } StyleSelector::Global(value, _) => value.to_string(), @@ -255,11 +279,19 @@ mod tests { #[rstest] #[case(StyleSelector::Selector("&:hover".to_string()), "&:hover")] - #[case(StyleSelector::Media { + #[case(StyleSelector::At { + kind: AtRuleKind::Media, query: "screen and (max-width: 600px)".to_string(), selector: None, }, - "@screen and (max-width: 600px)" + "@media screen and (max-width: 600px)" + )] + #[case(StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display: grid)".to_string(), + selector: None, + }, + "@supports(display: grid)" )] #[case(StyleSelector::Global(":root[data-theme=dark]".to_string(), "file.rs".to_string()), ":root[data-theme=dark]")] fn test_style_selector_display(#[case] selector: StyleSelector, #[case] expected: &str) { @@ -269,7 +301,8 @@ mod tests { #[rstest] #[case( - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "screen".to_string(), selector: None, }, @@ -282,16 +315,31 @@ mod tests { std::cmp::Ordering::Less )] #[case( - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "a".to_string(), selector: None, }, - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "b".to_string(), selector: None, }, std::cmp::Ordering::Less )] + #[case( + StyleSelector::At { + kind: AtRuleKind::Media, + query: "(min-width: 768px)".to_string(), + selector: None, + }, + StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display: grid)".to_string(), + selector: None, + }, + std::cmp::Ordering::Less + )] #[case( StyleSelector::Global(":root[data-theme=dark]".to_string(), "file1.rs".to_string()), StyleSelector::Global(":root[data-theme=light]".to_string(), "file2.rs".to_string()), @@ -304,7 +352,8 @@ mod tests { )] #[case( StyleSelector::Selector("&:hover".to_string()), - StyleSelector::Media { + StyleSelector::At { + kind: AtRuleKind::Media, query: "screen".to_string(), selector: None, }, diff --git a/libs/extractor/src/css_utils.rs b/libs/extractor/src/css_utils.rs index b1f87f15..a2abec7d 100644 --- a/libs/extractor/src/css_utils.rs +++ b/libs/extractor/src/css_utils.rs @@ -4,7 +4,7 @@ use crate::utils::{get_string_by_literal_expression, wrap_direct_call}; use css::{ optimize_multi_css_value::{check_multi_css_optimize, optimize_mutli_css_value}, rm_css_comment::rm_css_comment, - style_selector::StyleSelector, + style_selector::{AtRuleKind, StyleSelector}, }; use oxc_allocator::Allocator; use oxc_span::SPAN; @@ -246,23 +246,26 @@ pub fn css_to_style( let mut styles = vec![]; let mut input = css; - if input.contains("@media") { - let media_inputs = input - .split("@media") - .flat_map(|s| { - let s = s.trim(); - if s.is_empty() { - None - } else { - Some(format!("@media{s}")) + // Split by at-rules (@media, @supports) to handle multiple at-rules in a single input + for at_rule in ["@media", "@supports"] { + if input.contains(at_rule) { + let at_inputs = input + .split(at_rule) + .flat_map(|s| { + let s = s.trim(); + if s.is_empty() { + None + } else { + Some(format!("{at_rule}{s}")) + } + }) + .collect::>(); + if at_inputs.len() > 1 { + for at_input in at_inputs { + styles.extend(css_to_style(&at_input, level, selector)); } - }) - .collect::>(); - if media_inputs.len() > 1 { - for media_input in media_inputs { - styles.extend(css_to_style(&media_input, level, selector)); + return styles; } - return styles; } } @@ -322,9 +325,10 @@ pub fn css_to_style( end = rest.find('}').unwrap_or(rest.len()); } let block = &rest[..end]; - let sel = &if let Some(StyleSelector::Media { query, .. }) = selector { + let sel = &if let Some(StyleSelector::At { kind, query, .. }) = selector { let local_sel = selector_part.trim().to_string(); - Some(StyleSelector::Media { + Some(StyleSelector::At { + kind: *kind, query: query.clone(), selector: if local_sel == "&" { None @@ -335,11 +339,19 @@ pub fn css_to_style( } else { let sel = selector_part.trim().to_string(); if sel.starts_with("@media") { - Some(StyleSelector::Media { + Some(StyleSelector::At { + kind: AtRuleKind::Media, query: sel.replace(" ", "").replace("and(", "and (")["@media".len()..] .to_string(), selector: None, }) + } else if sel.starts_with("@supports") { + Some(StyleSelector::At { + kind: AtRuleKind::Supports, + query: sel.replace(" ", "").replace("and(", "and (")["@supports".len()..] + .to_string(), + selector: None, + }) } else if sel.is_empty() { selector.clone() } else { @@ -543,11 +555,13 @@ mod tests { color: #fff; }`", vec![ - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), @@ -564,19 +578,23 @@ mod tests { color: #fff; }`", vec![ - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)and (max-width:1024px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)and (max-width:1024px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), @@ -594,19 +612,23 @@ mod tests { } }`", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover,&:active,&:nth-child(2)".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover,&:active,&:nth-child(2)".to_string()), })), @@ -624,19 +646,23 @@ mod tests { } }`", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), @@ -664,35 +690,43 @@ mod tests { } }`", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: Some("&:hover".to_string()), })), - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), @@ -710,19 +744,23 @@ mod tests { color: #000; }`", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), @@ -737,6 +775,51 @@ mod tests { }`", vec![] )] + // @supports test cases + #[case( + "`@supports (display: grid) { + display: grid; + grid-template-columns: 1fr 1fr; + }`", + vec![ + ("display", "grid", Some(StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display:grid)".to_string(), + selector: None, + })), + ("grid-template-columns", "1fr 1fr", Some(StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display:grid)".to_string(), + selector: None, + })), + ] + )] + #[case( + "`@supports (display: flex) { + &:hover { + display: flex; + } + }`", + vec![ + ("display", "flex", Some(StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display:flex)".to_string(), + selector: Some("&:hover".to_string()), + })), + ] + )] + #[case( + "`@supports not (display: grid) { + display: block; + }`", + vec![ + ("display", "block", Some(StyleSelector::At { + kind: AtRuleKind::Supports, + query: "not(display:grid)".to_string(), + selector: None, + })), + ] + )] #[case( "`ul { font-family: 'Roboto Hello', sans-serif; }`", vec![ @@ -939,11 +1022,13 @@ mod tests { color: #fff; }", vec![ - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), @@ -960,19 +1045,23 @@ mod tests { color: #fff; }", vec![ - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)and (max-width:1024px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)and (max-width:1024px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), @@ -990,19 +1079,23 @@ mod tests { } }", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover,&:active,&:nth-child(2)".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover,&:active,&:nth-child(2)".to_string()), })), @@ -1020,19 +1113,23 @@ mod tests { } }", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), @@ -1060,35 +1157,43 @@ mod tests { } }", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: Some("&:hover".to_string()), })), - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: Some("&:hover".to_string()), })), @@ -1106,19 +1211,23 @@ mod tests { color: #000; }", vec![ - ("border", "1px solid #FFF", Some(StyleSelector::Media { + ("border", "1px solid #FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("color", "#FFF", Some(StyleSelector::Media { + ("color", "#FFF", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width:768px)".to_string(), selector: None, })), - ("border", "1px solid #000", Some(StyleSelector::Media { + ("border", "1px solid #000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), - ("color", "#000", Some(StyleSelector::Media { + ("color", "#000", Some(StyleSelector::At { + kind: AtRuleKind::Media, query: "(max-width:768px)and (min-width:480px)".to_string(), selector: None, })), diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-2.snap index e89e32b6..86da1b52 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { @@ -10,7 +10,8 @@ ToBTreeSet { value: "blue", level: 0, selector: Some( - Media { + At { + kind: Media, query: "(min-width:768px)", selector: Some( "&:active", @@ -26,7 +27,8 @@ ToBTreeSet { value: "red", level: 0, selector: Some( - Media { + At { + kind: Media, query: "(min-width:768px)", selector: Some( "&:hover", diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-3.snap b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-3.snap index 8e3bc30f..ead2764b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query-3.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { @@ -10,7 +10,8 @@ ToBTreeSet { value: "red", level: 0, selector: Some( - Media { + At { + kind: Media, query: "(min-width:768px)", selector: None, }, diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query.snap b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query.snap index 2aca949e..9a47ff54 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_static_css_with_media_query.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { css } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { @@ -10,7 +10,8 @@ ToBTreeSet { value: "red", level: 0, selector: Some( - Media { + At { + kind: Media, query: "(min-width:768px)", selector: None, }, diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index d9c7bb7a..bbc7e181 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -1,7 +1,10 @@ pub mod theme; use crate::theme::Theme; -use css::{merge_selector, style_selector::StyleSelector}; +use css::{ + merge_selector, + style_selector::{AtRuleKind, StyleSelector}, +}; use extractor::extract_style::ExtractStyleProperty; use extractor::extract_style::extract_style_value::ExtractStyleValue; use extractor::extract_style::style_property::StyleProperty; @@ -434,23 +437,18 @@ impl StyleSheet { .iter() .partition(|prop| matches!(prop.selector, Some(StyleSelector::Global(_, _)))); global_props.sort(); - let (mut medias, mut sorted_props): (Vec<_>, Vec<_>) = - rest.into_iter().partition(|prop| { - matches!( - prop.selector, - Some(StyleSelector::Media { - query: _, - selector: _ - }) - ) - }); + let (mut at_rules, mut sorted_props): (Vec<_>, Vec<_>) = rest + .into_iter() + .partition(|prop| matches!(prop.selector, Some(StyleSelector::At { .. }))); sorted_props.sort(); - medias.sort(); - let medias = { - let mut map = BTreeMap::new(); - for prop in medias { - if let Some(StyleSelector::Media { query, .. }) = &prop.selector { - map.entry(query).or_insert_with(Vec::new).push(prop); + at_rules.sort(); + let at_rules = { + let mut map: BTreeMap<(AtRuleKind, &String), Vec<_>> = BTreeMap::new(); + for prop in at_rules { + if let Some(StyleSelector::At { kind, query, .. }) = &prop.selector { + map.entry((*kind, query)) + .or_insert_with(Vec::new) + .push(prop); } } map @@ -513,21 +511,34 @@ impl StyleSheet { .as_str(), ); } - for (media, props) in medias { + for ((kind, query), props) in at_rules { let inner_css = props .into_iter() .map(ExtractStyle::extract) .collect::(); current_css.push_str( if let Some(break_point) = break_point { - format!("@media(min-width:{break_point}px)and {media}{{{inner_css}}}") + match kind { + AtRuleKind::Media => { + // Combine @media queries with 'and' + format!( + "@media(min-width:{break_point}px)and {query}{{{inner_css}}}" + ) + } + AtRuleKind::Supports => { + // Nest @supports inside @media for breakpoint + format!( + "@media(min-width:{break_point}px){{@supports{query}{{{inner_css}}}}}" + ) + } + } } else { format!( - "@media{}{{{}}}", - if media.starts_with("(") { - media.clone() + "@{kind}{}{{{}}}", + if query.starts_with("(") { + query.clone() } else { - format!(" {media}") + format!(" {query}") }, inner_css.as_str() ) @@ -1311,7 +1322,8 @@ mod tests { "margin-top", 0, "40px", - Some(&StyleSelector::Media { + Some(&StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width: 1024px)".to_string(), selector: Some("&:hover".to_string()), }), @@ -1323,7 +1335,8 @@ mod tests { "margin-bottom", 0, "40px", - Some(&StyleSelector::Media { + Some(&StyleSelector::At { + kind: AtRuleKind::Media, query: "(min-width: 1024px)".to_string(), selector: Some("&:hover".to_string()), }), @@ -1334,6 +1347,26 @@ mod tests { assert_debug_snapshot!(sheet.create_css(None, false).split("*/").nth(1).unwrap()); } + #[test] + fn test_selector_with_supports() { + let mut sheet = StyleSheet::default(); + sheet.add_property( + "test", + "display", + 0, + "grid", + Some(&StyleSelector::At { + kind: AtRuleKind::Supports, + query: "(display: grid)".to_string(), + selector: None, + }), + None, + None, + ); + + assert_debug_snapshot!(sheet.create_css(None, false).split("*/").nth(1).unwrap()); + } + #[test] fn test_deserialize() { { diff --git a/libs/sheet/src/snapshots/sheet__tests__print_selector.snap b/libs/sheet/src/snapshots/sheet__tests__print_selector.snap index 72167edc..053d618c 100644 --- a/libs/sheet/src/snapshots/sheet__tests__print_selector.snap +++ b/libs/sheet/src/snapshots/sheet__tests__print_selector.snap @@ -1,5 +1,5 @@ --- source: libs/sheet/src/lib.rs -expression: sheet.create_css() +expression: "sheet.create_css(None, false).split(\"*/\").nth(1).unwrap()" --- "@media print{.test{margin-bottom:40px}.test{margin-left:40px}.test{margin-right:40px}.test{margin-top:40px}}@media(min-width:480px)and print{.test{margin-bottom:40px}.test{margin-left:40px}.test{margin-right:40px}.test{margin-top:40px}}" diff --git a/libs/sheet/src/snapshots/sheet__tests__selector_with_supports.snap b/libs/sheet/src/snapshots/sheet__tests__selector_with_supports.snap new file mode 100644 index 00000000..fafcfe00 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__selector_with_supports.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: "sheet.create_css(None, false).split(\"*/\").nth(1).unwrap()" +--- +"@supports(display: grid){.test{display:grid}}" From b16b3b1b4f77512f4c8694585079e0235724eb71 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 9 Jan 2026 01:45:26 +0900 Subject: [PATCH 2/4] Support container rule --- libs/css/src/style_selector.rs | 16 +++++++++ libs/extractor/src/css_utils.rs | 36 +++++++++++++++++-- libs/sheet/src/lib.rs | 26 ++++++++++++++ ...sheet__tests__selector_with_container.snap | 5 +++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 libs/sheet/src/snapshots/sheet__tests__selector_with_container.snap diff --git a/libs/css/src/style_selector.rs b/libs/css/src/style_selector.rs index 6ab526fc..4760bf7e 100644 --- a/libs/css/src/style_selector.rs +++ b/libs/css/src/style_selector.rs @@ -17,6 +17,7 @@ pub enum AtRuleKind { #[default] Media, Supports, + Container, } impl Display for AtRuleKind { @@ -24,6 +25,7 @@ impl Display for AtRuleKind { match self { AtRuleKind::Media => write!(f, "media"), AtRuleKind::Supports => write!(f, "supports"), + AtRuleKind::Container => write!(f, "container"), } } } @@ -293,6 +295,20 @@ mod tests { }, "@supports(display: grid)" )] + #[case(StyleSelector::At { + kind: AtRuleKind::Container, + query: "(min-width: 768px)".to_string(), + selector: None, + }, + "@container(min-width: 768px)" + )] + #[case(StyleSelector::At { + kind: AtRuleKind::Container, + query: "sidebar (min-width: 400px)".to_string(), + selector: None, + }, + "@container sidebar (min-width: 400px)" + )] #[case(StyleSelector::Global(":root[data-theme=dark]".to_string(), "file.rs".to_string()), ":root[data-theme=dark]")] fn test_style_selector_display(#[case] selector: StyleSelector, #[case] expected: &str) { let output = format!("{selector}"); diff --git a/libs/extractor/src/css_utils.rs b/libs/extractor/src/css_utils.rs index a2abec7d..a73008b0 100644 --- a/libs/extractor/src/css_utils.rs +++ b/libs/extractor/src/css_utils.rs @@ -246,8 +246,8 @@ pub fn css_to_style( let mut styles = vec![]; let mut input = css; - // Split by at-rules (@media, @supports) to handle multiple at-rules in a single input - for at_rule in ["@media", "@supports"] { + // Split by at-rules (@media, @supports, @container) to handle multiple at-rules in a single input + for at_rule in ["@media", "@supports", "@container"] { if input.contains(at_rule) { let at_inputs = input .split(at_rule) @@ -352,6 +352,13 @@ pub fn css_to_style( .to_string(), selector: None, }) + } else if sel.starts_with("@container") { + Some(StyleSelector::At { + kind: AtRuleKind::Container, + query: sel.replace(" ", "").replace("and(", "and (")["@container".len()..] + .to_string(), + selector: None, + }) } else if sel.is_empty() { selector.clone() } else { @@ -820,6 +827,31 @@ mod tests { })), ] )] + // @container test cases + #[case( + "`@container (min-width: 768px) { + padding: 10px; + }`", + vec![ + ("padding", "10px", Some(StyleSelector::At { + kind: AtRuleKind::Container, + query: "(min-width:768px)".to_string(), + selector: None, + })), + ] + )] + #[case( + "`@container sidebar (min-width: 400px) { + display: flex; + }`", + vec![ + ("display", "flex", Some(StyleSelector::At { + kind: AtRuleKind::Container, + query: "sidebar(min-width:400px)".to_string(), + selector: None, + })), + ] + )] #[case( "`ul { font-family: 'Roboto Hello', sans-serif; }`", vec![ diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index bbc7e181..6c8455d8 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -531,6 +531,12 @@ impl StyleSheet { "@media(min-width:{break_point}px){{@supports{query}{{{inner_css}}}}}" ) } + AtRuleKind::Container => { + // Nest @container inside @media for breakpoint + format!( + "@media(min-width:{break_point}px){{@container{query}{{{inner_css}}}}}" + ) + } } } else { format!( @@ -1367,6 +1373,26 @@ mod tests { assert_debug_snapshot!(sheet.create_css(None, false).split("*/").nth(1).unwrap()); } + #[test] + fn test_selector_with_container() { + let mut sheet = StyleSheet::default(); + sheet.add_property( + "test", + "padding", + 0, + "10px", + Some(&StyleSelector::At { + kind: AtRuleKind::Container, + query: "(min-width: 768px)".to_string(), + selector: None, + }), + None, + None, + ); + + assert_debug_snapshot!(sheet.create_css(None, false).split("*/").nth(1).unwrap()); + } + #[test] fn test_deserialize() { { diff --git a/libs/sheet/src/snapshots/sheet__tests__selector_with_container.snap b/libs/sheet/src/snapshots/sheet__tests__selector_with_container.snap new file mode 100644 index 00000000..ba7ca3d7 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__selector_with_container.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: "sheet.create_css(None, false).split(\"*/\").nth(1).unwrap()" +--- +"@container(min-width: 768px){.test{padding:10px}}" From e899ae50544bcf5e89744fa160c10ed71c074584 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 12 Jan 2026 23:23:07 +0900 Subject: [PATCH 3/4] Update snapshot and rules --- .../devup_ui_wasm__tests__code_extract.snap | 2 +- libs/css/src/style_selector.rs | 4 +- .../extract_style_from_expression.rs | 42 +++ libs/extractor/src/lib.rs | 339 ++++++++++++++++++ .../extractor__tests__apply_typography-2.snap | 2 +- .../extractor__tests__apply_typography-3.snap | 2 +- .../extractor__tests__apply_typography.snap | 2 +- ...ractor__tests__apply_var_typography-2.snap | 2 +- ...ractor__tests__apply_var_typography-3.snap | 2 +- ...ractor__tests__apply_var_typography-4.snap | 2 +- ...xtractor__tests__apply_var_typography.snap | 2 +- ...xtractor__tests__at_rules_at_prefix-2.snap | 24 ++ ...xtractor__tests__at_rules_at_prefix-3.snap | 24 ++ .../extractor__tests__at_rules_at_prefix.snap | 24 ++ ...__tests__at_rules_underscore_prefix-2.snap | 24 ++ ...__tests__at_rules_underscore_prefix-3.snap | 24 ++ ...__tests__at_rules_underscore_prefix-4.snap | 24 ++ ...or__tests__at_rules_underscore_prefix.snap | 24 ++ ...tor__tests__avoid_same_name_component.snap | 2 +- .../extractor__tests__backtick_prop-2.snap | 2 +- .../extractor__tests__backtick_prop.snap | 2 +- .../extractor__tests__component_in_func.snap | 2 +- .../extractor__tests__convert_tag-2.snap | 2 +- .../extractor__tests__convert_tag-3.snap | 2 +- .../extractor__tests__convert_tag-4.snap | 2 +- .../extractor__tests__convert_tag-5.snap | 2 +- .../extractor__tests__convert_tag.snap | 2 +- ..._css_props_destructuring_assignment-2.snap | 2 +- ...s__css_props_destructuring_assignment.snap | 2 +- .../extractor__tests__custom_selector-2.snap | 2 +- .../extractor__tests__custom_selector-3.snap | 2 +- .../extractor__tests__custom_selector.snap | 2 +- ...tractor__tests__duplicate_style_props.snap | 2 +- ...ts__extract_class_name_from_component.snap | 2 +- ...tract_compound_responsive_style_props.snap | 2 +- ...tests__extract_conditional_selector-2.snap | 2 +- ...tests__extract_conditional_selector-3.snap | 2 +- ...tests__extract_conditional_selector-4.snap | 2 +- ...__tests__extract_conditional_selector.snap | 2 +- ...s__extract_conditional_style_props-10.snap | 2 +- ...ts__extract_conditional_style_props-2.snap | 2 +- ...ts__extract_conditional_style_props-3.snap | 2 +- ...ts__extract_conditional_style_props-4.snap | 2 +- ...ts__extract_conditional_style_props-5.snap | 2 +- ...ts__extract_conditional_style_props-6.snap | 2 +- ...ts__extract_conditional_style_props-7.snap | 2 +- ...ts__extract_conditional_style_props-8.snap | 2 +- ...ts__extract_conditional_style_props-9.snap | 2 +- ...ests__extract_conditional_style_props.snap | 2 +- ...itional_style_props_with_class_name-2.snap | 2 +- ...nditional_style_props_with_class_name.snap | 2 +- ...tests__extract_dynamic_logical_case-2.snap | 2 +- ...tests__extract_dynamic_logical_case-3.snap | 2 +- ...tests__extract_dynamic_logical_case-4.snap | 2 +- ...__tests__extract_dynamic_logical_case.snap | 2 +- ...xtract_dynamic_responsive_style_props.snap | 2 +- ..._tests__extract_dynamic_style_props-2.snap | 2 +- ..._tests__extract_dynamic_style_props-3.snap | 2 +- ..._tests__extract_dynamic_style_props-4.snap | 2 +- ...r__tests__extract_dynamic_style_props.snap | 2 +- ...tract_dynamic_style_props_with_type-2.snap | 2 +- ...tract_dynamic_style_props_with_type-3.snap | 2 +- ...extract_dynamic_style_props_with_type.snap | 2 +- ...tractor__tests__extract_global_css-10.snap | 2 +- ...xtractor__tests__extract_global_css-2.snap | 2 +- ...xtractor__tests__extract_global_css-3.snap | 2 +- ...xtractor__tests__extract_global_css-4.snap | 2 +- ...xtractor__tests__extract_global_css-5.snap | 2 +- ...xtractor__tests__extract_global_css-6.snap | 2 +- ...xtractor__tests__extract_global_css-7.snap | 2 +- ...xtractor__tests__extract_global_css-8.snap | 2 +- ...xtractor__tests__extract_global_css-9.snap | 2 +- .../extractor__tests__extract_global_css.snap | 2 +- ...ests__extract_global_css_with_empty-2.snap | 2 +- ...ests__extract_global_css_with_empty-3.snap | 2 +- ...ests__extract_global_css_with_empty-4.snap | 2 +- ...ests__extract_global_css_with_empty-5.snap | 2 +- ...ests__extract_global_css_with_empty-6.snap | 2 +- ..._tests__extract_global_css_with_empty.snap | 2 +- ..._extract_global_css_with_font_faces-2.snap | 2 +- ..._extract_global_css_with_font_faces-3.snap | 2 +- ..._extract_global_css_with_font_faces-4.snap | 2 +- ..._extract_global_css_with_font_faces-5.snap | 2 +- ..._extract_global_css_with_font_faces-6.snap | 2 +- ..._extract_global_css_with_font_faces-7.snap | 2 +- ...s__extract_global_css_with_font_faces.snap | 2 +- ...ts__extract_global_css_with_imports-2.snap | 2 +- ...ts__extract_global_css_with_imports-3.snap | 2 +- ...ests__extract_global_css_with_imports.snap | 2 +- ...s__extract_global_css_with_selector-2.snap | 2 +- ...s__extract_global_css_with_selector-3.snap | 2 +- ...s__extract_global_css_with_selector-4.snap | 2 +- ...sts__extract_global_css_with_selector.snap | 2 +- ...ct_global_css_with_template_literal-2.snap | 2 +- ...ct_global_css_with_template_literal-3.snap | 2 +- ...ct_global_css_with_template_literal-4.snap | 2 +- ...ct_global_css_with_template_literal-5.snap | 2 +- ...ct_global_css_with_template_literal-6.snap | 2 +- ...ct_global_css_with_template_literal-7.snap | 2 +- ...ct_global_css_with_template_literal-8.snap | 2 +- ...ract_global_css_with_template_literal.snap | 2 +- ...tract_global_css_with_wrong_imports-2.snap | 2 +- ...extract_global_css_with_wrong_imports.snap | 2 +- .../extractor__tests__extract_just_tsx-2.snap | 2 +- .../extractor__tests__extract_just_tsx.snap | 2 +- ...extractor__tests__extract_keyframs-10.snap | 2 +- .../extractor__tests__extract_keyframs-2.snap | 2 +- .../extractor__tests__extract_keyframs-3.snap | 2 +- .../extractor__tests__extract_keyframs-4.snap | 2 +- .../extractor__tests__extract_keyframs-5.snap | 2 +- .../extractor__tests__extract_keyframs-6.snap | 2 +- .../extractor__tests__extract_keyframs-7.snap | 2 +- .../extractor__tests__extract_keyframs-8.snap | 2 +- .../extractor__tests__extract_keyframs-9.snap | 2 +- .../extractor__tests__extract_keyframs.snap | 2 +- ...or__tests__extract_keyframs_literal-2.snap | 2 +- ...ctor__tests__extract_keyframs_literal.snap | 2 +- ...ractor__tests__extract_logical_case-2.snap | 2 +- ...ractor__tests__extract_logical_case-3.snap | 2 +- ...ractor__tests__extract_logical_case-4.snap | 2 +- ...xtractor__tests__extract_logical_case.snap | 2 +- ...tor__tests__extract_nested_selector-4.snap | 2 +- ...tor__tests__extract_nested_selector-5.snap | 2 +- ...tor__tests__extract_nested_selector-6.snap | 2 +- ...tor__tests__extract_nested_selector-7.snap | 2 +- ...tor__tests__extract_nested_selector-9.snap | 2 +- ...actor__tests__extract_nested_selector.snap | 2 +- ..._responsive_conditional_style_props-2.snap | 2 +- ..._responsive_conditional_style_props-3.snap | 2 +- ..._responsive_conditional_style_props-4.snap | 2 +- ..._responsive_conditional_style_props-5.snap | 2 +- ..._responsive_conditional_style_props-6.snap | 2 +- ..._responsive_conditional_style_props-7.snap | 2 +- ..._responsive_conditional_style_props-8.snap | 2 +- ...ct_responsive_conditional_style_props.snap | 2 +- ...itional_style_props_with_class_name-2.snap | 2 +- ...nditional_style_props_with_class_name.snap | 2 +- ...sts__extract_responsive_style_props-2.snap | 2 +- ...tests__extract_responsive_style_props.snap | 2 +- ...namic_value_conditional_style_props-2.snap | 2 +- ...dynamic_value_conditional_style_props.snap | 2 +- ..._same_value_conditional_style_props-2.snap | 2 +- ..._same_value_conditional_style_props-3.snap | 2 +- ..._same_value_conditional_style_props-4.snap | 2 +- ..._same_value_conditional_style_props-5.snap | 2 +- ..._same_value_conditional_style_props-6.snap | 2 +- ...ct_same_value_conditional_style_props.snap | 2 +- .../extractor__tests__extract_selector-2.snap | 2 +- .../extractor__tests__extract_selector-3.snap | 2 +- .../extractor__tests__extract_selector-4.snap | 2 +- .../extractor__tests__extract_selector-5.snap | 2 +- .../extractor__tests__extract_selector-8.snap | 2 +- .../extractor__tests__extract_selector.snap | 2 +- ...ests__extract_selector_with_literal-2.snap | 2 +- ..._tests__extract_selector_with_literal.snap | 2 +- ...s__extract_selector_with_responsive-2.snap | 2 +- ...s__extract_selector_with_responsive-3.snap | 2 +- ...sts__extract_selector_with_responsive.snap | 2 +- ...xtract_static_css_class_name_props-10.snap | 2 +- ...xtract_static_css_class_name_props-11.snap | 2 +- ...xtract_static_css_class_name_props-12.snap | 2 +- ...xtract_static_css_class_name_props-13.snap | 2 +- ...xtract_static_css_class_name_props-14.snap | 2 +- ...xtract_static_css_class_name_props-15.snap | 2 +- ...xtract_static_css_class_name_props-16.snap | 2 +- ...extract_static_css_class_name_props-2.snap | 2 +- ...extract_static_css_class_name_props-3.snap | 2 +- ...extract_static_css_class_name_props-4.snap | 2 +- ...extract_static_css_class_name_props-5.snap | 2 +- ...extract_static_css_class_name_props-6.snap | 2 +- ...extract_static_css_class_name_props-7.snap | 2 +- ...extract_static_css_class_name_props-8.snap | 2 +- ...extract_static_css_class_name_props-9.snap | 2 +- ...__extract_static_css_class_name_props.snap | 2 +- ...ests__extract_static_css_with_theme-2.snap | 2 +- ...ests__extract_static_css_with_theme-3.snap | 2 +- ..._tests__extract_static_css_with_theme.snap | 2 +- ...tractor__tests__extract_style_props-2.snap | 2 +- ...tractor__tests__extract_style_props-3.snap | 2 +- ...tractor__tests__extract_style_props-4.snap | 2 +- ...tractor__tests__extract_style_props-5.snap | 2 +- ...tractor__tests__extract_style_props-6.snap | 2 +- ...extractor__tests__extract_style_props.snap | 2 +- ...extract_style_props_with_class_name-2.snap | 2 +- ...extract_style_props_with_class_name-3.snap | 2 +- ...extract_style_props_with_class_name-4.snap | 2 +- ...extract_style_props_with_class_name-5.snap | 2 +- ...extract_style_props_with_class_name-6.snap | 2 +- ...extract_style_props_with_class_name-7.snap | 2 +- ...extract_style_props_with_class_name-8.snap | 2 +- ...extract_style_props_with_class_name-9.snap | 2 +- ...__extract_style_props_with_class_name.snap | 2 +- ...tract_style_props_with_default_import.snap | 2 +- ...act_style_props_with_namespace_import.snap | 2 +- ...sts__extract_style_props_with_var_css.snap | 2 +- ...act_variable_style_props_with_style-2.snap | 2 +- ...tract_variable_style_props_with_style.snap | 2 +- ...or__tests__extract_wrong_global_css-2.snap | 2 +- ...or__tests__extract_wrong_global_css-3.snap | 2 +- ...xtract_wrong_responsive_style_props-2.snap | 2 +- ..._extract_wrong_responsive_style_props.snap | 2 +- ...tractor__tests__global_css_at_rules-2.snap | 28 ++ ...tractor__tests__global_css_at_rules-3.snap | 28 ++ ...tractor__tests__global_css_at_rules-4.snap | 82 +++++ ...extractor__tests__global_css_at_rules.snap | 28 ++ ...xtractor__tests__group_selector_props.snap | 2 +- ...ractor__tests__ignore_special_props-2.snap | 2 +- ...xtractor__tests__ignore_special_props.snap | 2 +- ...ctor__tests__import_wrong_component-2.snap | 2 +- ...ractor__tests__import_wrong_component.snap | 2 +- .../extractor__tests__maintain_value.snap | 2 +- ...actor__tests__media_query_selectors-2.snap | 24 ++ ...actor__tests__media_query_selectors-3.snap | 24 ++ ...actor__tests__media_query_selectors-4.snap | 24 ++ ...actor__tests__media_query_selectors-5.snap | 48 +++ ...tractor__tests__media_query_selectors.snap | 39 ++ .../extractor__tests__negative_props-2.snap | 2 +- .../extractor__tests__negative_props-5.snap | 2 +- .../extractor__tests__negative_props-6.snap | 2 +- .../extractor__tests__negative_props-7.snap | 2 +- .../extractor__tests__negative_props.snap | 2 +- .../extractor__tests__nested_theme_props.snap | 2 +- ...actor__tests__optimize_aspect_ratio-2.snap | 2 +- ...actor__tests__optimize_aspect_ratio-3.snap | 2 +- ...tractor__tests__optimize_aspect_ratio.snap | 2 +- .../extractor__tests__optimize_func-2.snap | 2 +- .../extractor__tests__optimize_func-3.snap | 2 +- .../extractor__tests__optimize_func-4.snap | 2 +- .../extractor__tests__optimize_func-5.snap | 2 +- .../extractor__tests__optimize_func-6.snap | 2 +- .../extractor__tests__optimize_func-7.snap | 2 +- .../extractor__tests__optimize_func.snap | 2 +- ...rops_direct_array_responsive_select-2.snap | 2 +- ..._props_direct_array_responsive_select.snap | 2 +- ...r__tests__props_direct_array_select-2.snap | 2 +- ...r__tests__props_direct_array_select-3.snap | 2 +- ...r__tests__props_direct_array_select-4.snap | 2 +- ...r__tests__props_direct_array_select-5.snap | 2 +- ...r__tests__props_direct_array_select-6.snap | 2 +- ...r__tests__props_direct_array_select-7.snap | 2 +- ...tor__tests__props_direct_array_select.snap | 2 +- ...props_direct_hybrid_responsive_select.snap | 2 +- ...ops_direct_object_responsive_select-2.snap | 2 +- ...props_direct_object_responsive_select.snap | 2 +- ...__tests__props_direct_object_select-2.snap | 2 +- ...__tests__props_direct_object_select-4.snap | 2 +- ...or__tests__props_direct_object_select.snap | 2 +- ...ct_variable_array_responsive_select-2.snap | 2 +- ...rect_variable_array_responsive_select.snap | 2 +- ...ect_variable_object_responsive_select.snap | 2 +- ...props_direct_variable_object_select-2.snap | 2 +- ...__props_direct_variable_object_select.snap | 2 +- .../extractor__tests__props_direct_wrong.snap | 2 +- ...ractor__tests__props_multi_expression.snap | 2 +- ...ts__props_wrong_direct_array_select-2.snap | 2 +- ...ts__props_wrong_direct_array_select-3.snap | 2 +- ...ts__props_wrong_direct_array_select-4.snap | 2 +- ...ts__props_wrong_direct_array_select-5.snap | 2 +- ...ests__props_wrong_direct_array_select.snap | 2 +- ...s__props_wrong_direct_object_select-2.snap | 2 +- ...s__props_wrong_direct_object_select-3.snap | 2 +- ...s__props_wrong_direct_object_select-4.snap | 2 +- ...sts__props_wrong_direct_object_select.snap | 2 +- .../extractor__tests__remove_semicolon-2.snap | 2 +- .../extractor__tests__remove_semicolon-3.snap | 2 +- .../extractor__tests__remove_semicolon-4.snap | 2 +- .../extractor__tests__remove_semicolon.snap | 2 +- .../extractor__tests__rest_props.snap | 2 +- .../extractor__tests__style_order-2.snap | 2 +- .../extractor__tests__style_order-3.snap | 2 +- .../extractor__tests__style_order-4.snap | 2 +- .../extractor__tests__style_order-5.snap | 2 +- .../extractor__tests__style_order-6.snap | 2 +- .../extractor__tests__style_order-7.snap | 2 +- .../extractor__tests__style_order-8.snap | 2 +- .../extractor__tests__style_order.snap | 2 +- .../extractor__tests__style_order2-2.snap | 2 +- .../extractor__tests__style_order2-3.snap | 2 +- .../extractor__tests__style_order2.snap | 2 +- .../extractor__tests__style_variables-10.snap | 2 +- .../extractor__tests__style_variables-2.snap | 2 +- .../extractor__tests__style_variables-3.snap | 2 +- .../extractor__tests__style_variables-4.snap | 2 +- .../extractor__tests__style_variables-5.snap | 2 +- .../extractor__tests__style_variables-6.snap | 2 +- .../extractor__tests__style_variables-7.snap | 2 +- .../extractor__tests__style_variables-8.snap | 2 +- .../extractor__tests__style_variables.snap | 2 +- ...extractor__tests__style_variables_mjs.snap | 2 +- ...actor__tests__support_transpile_cjs-2.snap | 2 +- ...actor__tests__support_transpile_cjs-3.snap | 2 +- ...actor__tests__support_transpile_cjs-4.snap | 2 +- ...tractor__tests__support_transpile_cjs.snap | 2 +- ...actor__tests__support_transpile_mjs-2.snap | 2 +- ...actor__tests__support_transpile_mjs-3.snap | 2 +- ...actor__tests__support_transpile_mjs-4.snap | 2 +- ...actor__tests__support_transpile_mjs-5.snap | 2 +- ...tractor__tests__support_transpile_mjs.snap | 2 +- ...ctor__tests__template_literal_props-2.snap | 2 +- ...ctor__tests__template_literal_props-3.snap | 2 +- ...ctor__tests__template_literal_props-4.snap | 2 +- ...ctor__tests__template_literal_props-5.snap | 2 +- ...ctor__tests__template_literal_props-6.snap | 2 +- ...ractor__tests__template_literal_props.snap | 2 +- ...tests__ternary_operator_in_selector-2.snap | 2 +- ...tests__ternary_operator_in_selector-3.snap | 2 +- ...__tests__ternary_operator_in_selector.snap | 2 +- .../extractor__tests__theme_props.snap | 2 +- .../extractor__tests__theme_selector-2.snap | 2 +- .../extractor__tests__theme_selector-3.snap | 2 +- .../extractor__tests__theme_selector.snap | 2 +- .../extractor__tests__with_prefix.snap | 2 +- ...tractor__tests__wrong_style_variables.snap | 2 +- libs/extractor/src/visit.rs | 17 +- libs/sheet/src/lib.rs | 48 +++ .../sheet__tests__all_media_selector.snap | 5 + .../sheet__tests__create_css-10.snap | 2 +- .../sheet__tests__create_css-11.snap | 2 +- .../sheet__tests__create_css-12.snap | 2 +- .../snapshots/sheet__tests__create_css-2.snap | 2 +- .../snapshots/sheet__tests__create_css-3.snap | 2 +- .../snapshots/sheet__tests__create_css-4.snap | 2 +- .../snapshots/sheet__tests__create_css-5.snap | 2 +- .../snapshots/sheet__tests__create_css-6.snap | 2 +- .../snapshots/sheet__tests__create_css-8.snap | 2 +- .../snapshots/sheet__tests__create_css-9.snap | 2 +- .../snapshots/sheet__tests__create_css.snap | 2 +- .../sheet__tests__create_css_sort_test-2.snap | 2 +- .../sheet__tests__create_css_sort_test.snap | 2 +- ...ts__create_css_with_basic_sort_test-2.snap | 2 +- ...ts__create_css_with_basic_sort_test-3.snap | 2 +- ...ests__create_css_with_basic_sort_test.snap | 2 +- ...ts__create_css_with_global_selector-2.snap | 2 +- ...ts__create_css_with_global_selector-3.snap | 2 +- ...ts__create_css_with_global_selector-4.snap | 2 +- ...ts__create_css_with_global_selector-5.snap | 2 +- ...ts__create_css_with_global_selector-6.snap | 2 +- ...ts__create_css_with_global_selector-7.snap | 2 +- ...ts__create_css_with_global_selector-8.snap | 2 +- ...ts__create_css_with_global_selector-9.snap | 2 +- ...ests__create_css_with_global_selector.snap | 2 +- ...eet__tests__create_css_with_imports-2.snap | 2 +- ...eet__tests__create_css_with_imports-3.snap | 2 +- ...sheet__tests__create_css_with_imports.snap | 2 +- ...s_with_selector_and_basic_sort_test-2.snap | 2 +- ...css_with_selector_and_basic_sort_test.snap | 2 +- ..._create_css_with_selector_sort_test-2.snap | 2 +- ..._create_css_with_selector_sort_test-3.snap | 2 +- ...s__create_css_with_selector_sort_test.snap | 2 +- .../snapshots/sheet__tests__font_face.snap | 2 +- .../snapshots/sheet__tests__keyframes-2.snap | 2 +- .../snapshots/sheet__tests__keyframes.snap | 2 +- .../sheet__tests__print_selector-2.snap | 2 +- .../sheet__tests__reset_global_css-2.snap | 2 +- .../sheet__tests__reset_global_css-3.snap | 2 +- .../sheet__tests__reset_global_css.snap | 2 +- .../sheet__tests__screen_selector.snap | 5 + .../sheet__tests__selector_with_prefix.snap | 2 +- .../sheet__tests__selector_with_query.snap | 2 +- .../sheet__tests__speech_selector.snap | 5 + ...heet__tests__style_order_create_css-2.snap | 2 +- .../sheet__tests__style_order_create_css.snap | 2 +- .../sheet__tests__theme_selector-2.snap | 2 +- .../sheet__tests__theme_selector-3.snap | 2 +- .../sheet__tests__wrong_breakpoint.snap | 2 +- .../react/src/types/props/selector/index.ts | 17 + 366 files changed, 1313 insertions(+), 344 deletions(-) create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__global_css_at_rules-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__global_css_at_rules-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__global_css_at_rules-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__global_css_at_rules.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__media_query_selectors-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__media_query_selectors-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__media_query_selectors-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__media_query_selectors-5.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__media_query_selectors.snap create mode 100644 libs/sheet/src/snapshots/sheet__tests__all_media_selector.snap create mode 100644 libs/sheet/src/snapshots/sheet__tests__screen_selector.snap create mode 100644 libs/sheet/src/snapshots/sheet__tests__speech_selector.snap diff --git a/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__code_extract.snap b/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__code_extract.snap index f3b79c30..f147d7ee 100644 --- a/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__code_extract.snap +++ b/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__code_extract.snap @@ -1,5 +1,5 @@ --- source: bindings/devup-ui-wasm/src/lib.rs -expression: get_css(None).unwrap() +expression: "get_css(None, false).unwrap().split(\"*/\").nth(1).unwrap()" --- "@layer t;@layer t{:root{color-scheme:light;--primary:light-dark(#FFF,#000)}:root[data-theme=dark]{color-scheme:dark}}" diff --git a/libs/css/src/style_selector.rs b/libs/css/src/style_selector.rs index 4760bf7e..0dddd22e 100644 --- a/libs/css/src/style_selector.rs +++ b/libs/css/src/style_selector.rs @@ -163,10 +163,10 @@ impl From<&str> for StyleSelector { } else if let Some(s) = value.strip_prefix("theme-") { // first character should lower case StyleSelector::Selector(format!(":root[data-theme={}] &", to_camel_case(s))) - } else if value == "print" { + } else if matches!(value.as_str(), "print" | "screen" | "speech" | "all") { StyleSelector::At { kind: AtRuleKind::Media, - query: "print".to_string(), + query: value.to_string(), selector: None, } } else { diff --git a/libs/extractor/src/extractor/extract_style_from_expression.rs b/libs/extractor/src/extractor/extract_style_from_expression.rs index 3c878909..888b2b93 100644 --- a/libs/extractor/src/extractor/extract_style_from_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_expression.rs @@ -239,6 +239,48 @@ pub fn extract_style_from_expression<'a>( }; } + // Handle at-rules: @media, @supports, @container (or _media, _supports, _container) + let at_rule_name = name + .strip_prefix("@") + .or_else(|| name.strip_prefix("_")) + .filter(|n| matches!(*n, "media" | "supports" | "container")); + + if let Some(at_rule) = at_rule_name + && let Expression::ObjectExpression(obj) = expression + { + let mut props = vec![]; + for p in obj.properties.iter_mut() { + if let ObjectPropertyKind::ObjectProperty(o) = p + && let Some(query) = get_string_by_property_key(&o.key) + { + let at_selector = StyleSelector::At { + kind: match at_rule { + "media" => css::style_selector::AtRuleKind::Media, + "supports" => css::style_selector::AtRuleKind::Supports, + "container" => css::style_selector::AtRuleKind::Container, + _ => unreachable!(), + }, + query: query.to_string(), + selector: selector.as_ref().map(|s| s.to_string()), + }; + props.extend( + extract_style_from_expression( + ast_builder, + None, + &mut o.value, + level, + &Some(at_selector), + ) + .styles, + ); + } + } + return ExtractResult { + styles: props, + ..ExtractResult::default() + }; + } + if let Some(new_selector) = name.strip_prefix("_") { return extract_style_from_expression( ast_builder, diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 78f2b7c1..d24faa7f 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -9020,6 +9020,345 @@ const margin = 5; )); } + #[test] + #[serial] + fn test_media_query_selectors() { + // Test _print media query selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _screen media query selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _speech media query selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _all media query selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test multiple media query selectors combined + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn test_at_rules_underscore_prefix() { + // Test _container at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _media at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _supports at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test _container with named container + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn test_at_rules_at_prefix() { + // Test @container at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test @media at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test @supports at-rule + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + } + + #[test] + #[serial] + fn test_global_css_at_rules() { + // Test globalCss with @media nested inside selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {globalCss} from '@devup-ui/core' +globalCss({ + body: { + "@media": { + "(min-width: 768px)": { bg: "white" } + } + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test globalCss with @supports nested inside selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {globalCss} from '@devup-ui/core' +globalCss({ + ".grid-container": { + "@supports": { + "(display: grid)": { display: "grid" } + } + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test globalCss with @container nested inside selector + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {globalCss} from '@devup-ui/core' +globalCss({ + ".card": { + "@container": { + "(min-width: 400px)": { p: 4 } + } + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test globalCss with multiple at-rules nested inside selectors + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {globalCss} from '@devup-ui/core' +globalCss({ + body: { + bg: "gray", + "@media": { + "(min-width: 768px)": { bg: "white" }, + "(prefers-color-scheme: dark)": { bg: "black" } + } + }, + ".container": { + "@supports": { + "(display: grid)": { display: "grid" } + } + } +}) +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + } + #[rstest] #[case("test.tsx", "const x = 1;", "@devup-ui/react", false)] // no package string #[case( diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_typography-2.snap b/libs/extractor/src/snapshots/extractor__tests__apply_typography-2.snap index 96205e24..13aa2085 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_typography-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_typography-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_typography-3.snap b/libs/extractor/src/snapshots/extractor__tests__apply_typography-3.snap index d91897e2..db27b0e6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_typography-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_typography-3.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_typography.snap b/libs/extractor/src/snapshots/extractor__tests__apply_typography.snap index 70a43587..54037105 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_typography.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_typography.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-2.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-2.snap index fd54ef0d..312595a0 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: {}, diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-3.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-3.snap index 2edef42f..b2502eba 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-3.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: {}, diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-4.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-4.snap index 7b55cf25..ae0e5ff9 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography-4.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box as DevupButton} from '@devup-ui/core'\n \n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box as DevupButton} from '@devup-ui/core'\n \n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: { diff --git a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography.snap b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography.snap index 6126d98a..1f65a842 100644 --- a/libs/extractor/src/snapshots/extractor__tests__apply_var_typography.snap +++ b/libs/extractor/src/snapshots/extractor__tests__apply_var_typography.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Text} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" --- ToBTreeSet { styles: {}, diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-2.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-2.snap new file mode 100644 index 00000000..5db8c825 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-2.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: Some( + At { + kind: Media, + query: "(min-width: 768px)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-3.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-3.snap new file mode 100644 index 00000000..bf57c692 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix-3.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: Some( + At { + kind: Supports, + query: "(display: flex)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix.snap new file mode 100644 index 00000000..e370b3c7 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_at_prefix.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: Some( + At { + kind: Container, + query: "(min-width: 400px)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-2.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-2.snap new file mode 100644 index 00000000..d441a6a8 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-2.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: Some( + At { + kind: Media, + query: "(min-width: 768px)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-3.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-3.snap new file mode 100644 index 00000000..c635b562 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-3.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "display", + value: "grid", + level: 0, + selector: Some( + At { + kind: Supports, + query: "(display: grid)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-4.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-4.snap new file mode 100644 index 00000000..f5008b91 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix-4.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "padding", + value: "16px", + level: 0, + selector: Some( + At { + kind: Container, + query: "sidebar (min-width: 400px)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix.snap b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix.snap new file mode 100644 index 00000000..9d3bb36d --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__at_rules_underscore_prefix.snap @@ -0,0 +1,24 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: Some( + At { + kind: Container, + query: "(min-width: 400px)", + selector: None, + }, + ), + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__avoid_same_name_component.snap b/libs/extractor/src/snapshots/extractor__tests__avoid_same_name_component.snap index 0d152461..1cdbadae 100644 --- a/libs/extractor/src/snapshots/extractor__tests__avoid_same_name_component.snap +++ b/libs/extractor/src/snapshots/extractor__tests__avoid_same_name_component.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\nimport {Button} from '@devup/ui'\n ;\n ;