diff --git a/.changeset/lemon-coats-spend.md b/.changeset/lemon-coats-spend.md new file mode 100644 index 00000000..61878bb8 --- /dev/null +++ b/.changeset/lemon-coats-spend.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/wasm": patch +--- + +Convert light-dark and optimize theme diff --git a/Cargo.lock b/Cargo.lock index 22d9eb16..1f897efc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1294,6 +1294,7 @@ dependencies = [ "insta", "once_cell", "regex", + "rstest", "serde", "serde_json", ] diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 01281779..18924281 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -267,10 +267,7 @@ mod tests { sheet.set_theme(theme); } - assert_eq!( - get_css().unwrap(), - ":root{color-scheme:light;--primary:#FFF;}\n:root[data-theme=dark]{color-scheme:dark;--primary:#000;}\n" - ); + assert_debug_snapshot!(get_css().unwrap()); } #[test] 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 new file mode 100644 index 00000000..d27a91ae --- /dev/null +++ b/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__code_extract.snap @@ -0,0 +1,5 @@ +--- +source: bindings/devup-ui-wasm/src/lib.rs +expression: get_css().unwrap() +--- +":root{color-scheme:light;--primary:light-dark(#FFF,#000)}:root[data-theme=dark]{color-scheme:dark}" diff --git a/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__deserialize_theme.snap b/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__deserialize_theme.snap index 35b833db..809c8b88 100644 --- a/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__deserialize_theme.snap +++ b/bindings/devup-ui-wasm/src/snapshots/devup_ui_wasm__tests__deserialize_theme.snap @@ -2,4 +2,4 @@ source: bindings/devup-ui-wasm/src/lib.rs expression: theme.to_css() --- -":root{color-scheme:light;--primary:#000;}\n:root[data-theme=dark]{color-scheme:dark;--primary:#fff;}\n.typo-default{font-family:Arial;font-size:16px;font-weight:400;line-height:1.5;letter-spacing:0.5em}\n@media (min-width:480px){.typo-default{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5em}}\n@media (min-width:768px){.typo-default{font-family:Arial;font-size:24px;line-height:1.5;letter-spacing:0.5em}}" +":root{color-scheme:light;--primary:light-dark(#000,#FFF)}:root[data-theme=dark]{color-scheme:dark}.typo-default{font-family:Arial;font-size:16px;font-weight:400;line-height:1.5;letter-spacing:0.5em}@media(min-width:480px){.typo-default{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5em}}@media(min-width:768px){.typo-default{font-family:Arial;font-size:24px;line-height:1.5;letter-spacing:0.5em}}" diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index d44729b6..15fb9710 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -20,10 +20,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, - query: _, - } => { + StyleSelector::Media { selector: s, .. } => { if let Some(s) = s { s.replace("&", &format!(".{class_name}")) } else { @@ -452,6 +449,27 @@ mod tests { merge_selector("cls", Some(&["themeDark", "hover"].into()),), ":root[data-theme=dark] .cls:hover" ); + assert_eq!( + merge_selector( + "cls", + Some(&StyleSelector::Media { + query: "print".to_string(), + selector: None, + }) + ), + ".cls" + ); + + assert_eq!( + merge_selector( + "cls", + Some(&StyleSelector::Media { + query: "print".to_string(), + selector: Some("&:hover".to_string()), + }) + ), + ".cls:hover" + ); } #[test] diff --git a/libs/extractor/src/extractor/extract_style_from_jsx.rs b/libs/extractor/src/extractor/extract_style_from_jsx.rs index 83e09ccc..01826118 100644 --- a/libs/extractor/src/extractor/extract_style_from_jsx.rs +++ b/libs/extractor/src/extractor/extract_style_from_jsx.rs @@ -13,26 +13,23 @@ pub fn extract_style_from_jsx<'a>( value: &mut JSXAttributeValue<'a>, ) -> ExtractResult<'a> { match value { - JSXAttributeValue::ExpressionContainer(expression) => { - if expression.expression.is_expression() { - extract_style_from_expression( - ast_builder, - Some(name), - expression.expression.to_expression_mut(), - 0, - &None, - ) - } else { - ExtractResult::default() - } + JSXAttributeValue::ExpressionContainer(expression) + if expression.expression.is_expression() => + { + Some( + expression + .expression + .to_expression() + .clone_in(ast_builder.allocator), + ) } - JSXAttributeValue::StringLiteral(literal) => extract_style_from_expression( - ast_builder, - Some(name), - &mut Expression::StringLiteral(literal.clone_in(ast_builder.allocator)), - 0, - &None, - ), - _ => ExtractResult::default(), + JSXAttributeValue::StringLiteral(literal) => Some(Expression::StringLiteral( + 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() } 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 30039c03..1b0e04ed 100644 --- a/libs/extractor/src/extractor/extract_style_from_member_expression.rs +++ b/libs/extractor/src/extractor/extract_style_from_member_expression.rs @@ -31,11 +31,7 @@ pub(super) fn extract_style_from_member_expression<'a>( let mut ret: Vec = vec![]; match &mut mem.object { - Expression::ArrayExpression(array) => { - if array.elements.is_empty() { - return ExtractResult::default(); - } - + Expression::ArrayExpression(array) if !array.elements.is_empty() => { if let Some(num) = get_number_by_literal_expression(mem_expression) { if num < 0f64 { return ExtractResult::default(); @@ -125,11 +121,7 @@ pub(super) fn extract_style_from_member_expression<'a>( map, }); } - Expression::ObjectExpression(obj) => { - if obj.properties.is_empty() { - return ExtractResult::default(); - } - + Expression::ObjectExpression(obj) if !obj.properties.is_empty() => { let mut map = BTreeMap::new(); if let Some(k) = get_string_by_literal_expression(mem_expression) { let mut etc = None; @@ -156,9 +148,7 @@ pub(super) fn extract_style_from_member_expression<'a>( } match etc { - None => { - return ExtractResult::default(); - } + None => return ExtractResult::default(), Some(etc) => ret.push(ExtractStyleProp::Static(ExtractStyleValue::Dynamic( ExtractDynamicStyle::new( name.unwrap(), diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 8dd88b6c..e66ecdbe 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -14,6 +14,7 @@ extractor = { path = "../extractor" } insta = "1.43.1" serde_json = "1.0.141" criterion = { version = "0.6", features = ["html_reports"] } +rstest = "0.25.0" [[bench]] name = "my_benchmark" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-2.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-2.snap index 27e376b0..b432f5f9 100644 --- a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-2.snap +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-2.snap @@ -2,4 +2,4 @@ source: libs/sheet/src/theme.rs expression: theme.to_css() --- -":root{color-scheme:light;--primary:#000;}\n:root[data-theme=dark]{color-scheme:dark;--primary:#000;}\n" +":root{color-scheme:light;--primary:#000}:root[data-theme=dark]{color-scheme:dark}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-3.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-3.snap index f144fad0..b432f5f9 100644 --- a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-3.snap +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-3.snap @@ -2,4 +2,4 @@ source: libs/sheet/src/theme.rs expression: theme.to_css() --- -":root{color-scheme:light;--primary:#000;}\n:root[data-theme=b]{color-scheme:dark;--primary:#000;}\n" +":root{color-scheme:light;--primary:#000}:root[data-theme=dark]{color-scheme:dark}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-4.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-4.snap index 952dcc47..83c878dd 100644 --- a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-4.snap +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-4.snap @@ -2,4 +2,4 @@ source: libs/sheet/src/theme.rs expression: theme.to_css() --- -":root{color-scheme:light;--primary:#000;}\n:root[data-theme=a]{color-scheme:dark;--primary:#000;}\n:root[data-theme=b]{color-scheme:dark;--primary:#000;}\n:root[data-theme=c]{color-scheme:dark;--primary:#000;}\n" +":root{color-scheme:light;--primary:#000}:root[data-theme=b]{color-scheme:dark}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-5.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-5.snap index a6d06aaa..55f45a1a 100644 --- a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-5.snap +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-5.snap @@ -2,4 +2,4 @@ source: libs/sheet/src/theme.rs expression: theme.to_css() --- -":root{--primary:#000;}\n" +":root{color-scheme:light;--primary:#000}:root[data-theme=a]{color-scheme:dark}:root[data-theme=b]{color-scheme:dark}:root[data-theme=c]{color-scheme:dark}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-6.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-6.snap new file mode 100644 index 00000000..882afbb5 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-6.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/theme.rs +expression: theme.to_css() +--- +":root{--primary:#000}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-7.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-7.snap new file mode 100644 index 00000000..090bd334 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme-7.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/theme.rs +expression: theme.to_css() +--- +":root{color-scheme:light;--primary:#000}:root[data-theme=a]{color-scheme:dark;--primary:#002}:root[data-theme=b]{color-scheme:dark;--primary:#001}:root[data-theme=c]{color-scheme:dark}" diff --git a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme.snap b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme.snap index 27e376b0..8f04f5fd 100644 --- a/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme.snap +++ b/libs/sheet/src/snapshots/sheet__theme__tests__to_css_from_theme.snap @@ -1,5 +1,5 @@ --- source: libs/sheet/src/theme.rs -expression: theme.to_css() +expression: css --- -":root{color-scheme:light;--primary:#000;}\n:root[data-theme=dark]{color-scheme:dark;--primary:#000;}\n" +":root{color-scheme:light;--primary:light-dark(#000,#FFF)}:root[data-theme=dark]{color-scheme:dark}.typo-default{font-family:Arial;font-size:16px;font-weight:400;line-height:1.5;letter-spacing:0.5}@media(min-width:480px){.typo-default{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5}.typo-default1{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5}}" diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 6a1878e1..2917e338 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -1,3 +1,4 @@ +use css::optimize_value::optimize_value; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{BTreeMap, HashMap}; @@ -161,33 +162,90 @@ impl Theme { col }; let single_theme = entries.len() <= 1; + // if other theme is exists, should use light-dark function + let other_theme_key = if entries.len() == 2 { + entries + .iter() + .find(|(k, _)| *k != &default_theme_key) + .map(|(k, _)| k.to_string()) + } else { + None + }; for (theme_name, theme_properties) in entries { - if let Some(theme_key) = if *theme_name == *default_theme_key { + let mut css_contents = vec![]; + let mut css_color_contents = vec![]; + let theme_key = if *theme_name == *default_theme_key { None } else { Some(theme_name) - } { - theme_declaration.push_str( - format!(":root[data-theme={}]{{{}", theme_key, "color-scheme:dark;") - .as_str(), - ); + }; + if let Some(theme_key) = theme_key { + theme_declaration + .push_str(format!(":root[data-theme={}]{{", theme_key).as_str()); + css_contents.push("color-scheme:dark".to_string()); } else { - theme_declaration.push_str( - format!( - ":root{{{}", - if single_theme { - "" - } else { - "color-scheme:light;" - } - ) - .as_str(), - ); + theme_declaration.push_str(format!(":root{{",).as_str()); + if !single_theme { + css_contents.push("color-scheme:light".to_string()); + } } for (prop, value) in theme_properties.0.iter() { - theme_declaration.push_str(format!("--{prop}:{value};").as_str()); + let optimized_value = optimize_value(value); + if theme_key.is_some() { + if other_theme_key.is_none() + && let Some(default_value) = self + .colors + .get(&default_theme_key) + .map(|v| { + v.0.get(prop).and_then(|v| { + if optimize_value(v) == optimized_value { + None + } else { + Some(optimized_value) + } + }) + }) + .flatten() + { + css_color_contents.push(format!("--{prop}:{}", default_value)); + } + } else { + let other_theme_value = other_theme_key + .as_ref() + .map(|other_theme_key| { + self.colors + .get(other_theme_key) + .map(|v| { + v.0.get(prop).and_then(|v| { + let other_theme_value = optimize_value(v.as_str()); + if other_theme_value == optimized_value { + None + } else { + Some(other_theme_value) + } + }) + }) + .flatten() + }) + .flatten(); + // default theme + css_color_contents.push(format!( + "--{prop}:{}", + if let Some(other_theme_value) = other_theme_value { + format!("light-dark({optimized_value},{other_theme_value})") + } else { + optimized_value + } + )); + } } - theme_declaration.push_str("}\n"); + theme_declaration.push_str( + [css_contents, css_color_contents] + .concat() + .join(";") + .as_str(), + ); + theme_declaration.push_str("}"); } } let mut css = theme_declaration; @@ -195,39 +253,40 @@ impl Theme { for ty in self.typography.iter() { for (idx, t) in ty.1.0.iter().enumerate() { if let Some(t) = t { - let css_content = format!( - "{}{}{}{}{}", + let css_content = [ t.font_family .clone() - .map(|v| format!("font-family:{v};")) + .map(|v| format!("font-family:{v}")) .unwrap_or("".to_string()), t.font_size .clone() - .map(|v| format!("font-size:{v};")) + .map(|v| format!("font-size:{v}")) .unwrap_or("".to_string()), t.font_weight .clone() - .map(|v| format!("font-weight:{v};")) + .map(|v| format!("font-weight:{v}")) .unwrap_or("".to_string()), t.line_height .clone() - .map(|v| format!("line-height:{v};")) + .map(|v| format!("line-height:{v}")) .unwrap_or("".to_string()), t.letter_spacing .clone() .map(|v| format!("letter-spacing:{v}")) - .unwrap_or("".to_string()) - ); - if css_content.is_empty() { - continue; + .unwrap_or("".to_string()), + ] + .iter() + .map(|v| v.trim()) + .filter(|v| !v.is_empty()) + .collect::>() + .join(";"); + + if !css_content.is_empty() { + level_map + .entry(idx as u8) + .or_insert_with(Vec::new) + .push(format!(".typo-{}{{{}}}", ty.0, css_content)); } - let typo_css = format!(".typo-{}{{{}}}", ty.0, css_content); - level_map - .get_mut(&(idx as u8)) - .map(|v| v.push(typo_css.clone())) - .unwrap_or_else(|| { - level_map.insert(idx as u8, vec![typo_css]); - }); } } } @@ -239,7 +298,7 @@ impl Theme { .get(level as usize) .map(|v| format!("(min-width:{v}px)")) { - css.push_str(format!("\n@media {}{{{}}}", media, css_vec.join("")).as_str()); + css.push_str(format!("@media{media}{{{}}}", css_vec.join("")).as_str()); } } css @@ -250,6 +309,7 @@ impl Theme { mod tests { use super::*; use insta::assert_debug_snapshot; + use rstest::rstest; #[test] fn to_css_from_theme() { @@ -297,10 +357,7 @@ mod tests { ], ); let css = theme.to_css(); - assert_eq!( - css, - ":root{color-scheme:light;--primary:#000;}\n:root[data-theme=dark]{color-scheme:dark;--primary:#fff;}\n.typo-default{font-family:Arial;font-size:16px;font-weight:400;line-height:1.5;letter-spacing:0.5}\n@media (min-width:480px){.typo-default{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5}.typo-default1{font-family:Arial;font-size:24px;font-weight:400;line-height:1.5;letter-spacing:0.5}}" - ); + assert_debug_snapshot!(css); assert_eq!(Theme::default().to_css(), ""); let mut theme = Theme::default(); @@ -418,16 +475,62 @@ mod tests { }), ); assert_debug_snapshot!(theme.to_css()); + + let mut theme = Theme::default(); + theme.add_color_theme( + "light", + ColorTheme({ + let mut map = HashMap::new(); + map.insert("primary".to_string(), "#000".to_string()); + map + }), + ); + + theme.add_color_theme( + "b", + ColorTheme({ + let mut map = HashMap::new(); + map.insert("primary".to_string(), "#001".to_string()); + map + }), + ); + + theme.add_color_theme( + "a", + ColorTheme({ + let mut map = HashMap::new(); + map.insert("primary".to_string(), "#002".to_string()); + map + }), + ); + + theme.add_color_theme( + "c", + ColorTheme({ + let mut map = HashMap::new(); + map.insert("primary".to_string(), "#000".to_string()); + map + }), + ); + assert_debug_snapshot!(theme.to_css()); } - #[test] - fn update_breakpoints() { + #[rstest] + #[case( + vec![0, 480, 768, 992, 1280], + vec![0, 480, 768, 992, 1280, 1600] + )] + #[case( + vec![0, 480, 768, 992, 1280, 1600], + vec![0, 480, 768, 992, 1280, 1600] + )] + #[case( + vec![0, 480, 768, 992, 1280, 1600, 1920], + vec![0, 480, 768, 992, 1280, 1600, 1920] + )] + fn update_breakpoints(#[case] input: Vec, #[case] expected: Vec) { let mut theme = Theme::default(); - theme.update_breakpoints(vec![0, 480, 768, 992, 1280]); - assert_eq!(theme.breakpoints, vec![0, 480, 768, 992, 1280, 1600]); - theme.update_breakpoints(vec![0, 480, 768, 992, 1280, 1600]); - assert_eq!(theme.breakpoints, vec![0, 480, 768, 992, 1280, 1600]); - theme.update_breakpoints(vec![0, 480, 768, 992, 1280, 1600, 1920]); - assert_eq!(theme.breakpoints, vec![0, 480, 768, 992, 1280, 1600, 1920]); + theme.update_breakpoints(input); + assert_eq!(theme.breakpoints, expected); } }