diff --git a/.changeset/eight-kings-live.md b/.changeset/eight-kings-live.md new file mode 100644 index 00000000..e31ee3b9 --- /dev/null +++ b/.changeset/eight-kings-live.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/wasm": patch +--- + +Fix desctructing issue, Support for number in typo, Implement theme selector diff --git a/.changeset/quiet-rabbits-attend.md b/.changeset/quiet-rabbits-attend.md new file mode 100644 index 00000000..a5a73c9d --- /dev/null +++ b/.changeset/quiet-rabbits-attend.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/react": patch +--- + +Implement ThemeScript, useTheme, getTheme, setTheme diff --git a/.changeset/quiet-waves-confess.md b/.changeset/quiet-waves-confess.md new file mode 100644 index 00000000..41671a14 --- /dev/null +++ b/.changeset/quiet-waves-confess.md @@ -0,0 +1,6 @@ +--- +"@devup-ui/webpack-plugin": patch +"@devup-ui/vite-plugin": patch +--- + +Update diff --git a/Cargo.lock b/Cargo.lock index 6bac79ee..d8961a3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "allocator-api2" @@ -127,6 +127,7 @@ dependencies = [ "extractor", "js-sys", "once_cell", + "serial_test", "sheet", "wasm-bindgen", "wasm-bindgen-test", diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index 2dfbca76..ab94bc85 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -1,5 +1,6 @@ import 'sanitize.css' +import { ThemeHead } from '@devup-ui/react' import type { Metadata } from 'next' import { Footer } from '../components/Footer' @@ -16,8 +17,9 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + + diff --git a/apps/landing/src/app/page.tsx b/apps/landing/src/app/page.tsx index d40efa7f..4f838e44 100644 --- a/apps/landing/src/app/page.tsx +++ b/apps/landing/src/app/page.tsx @@ -4,13 +4,11 @@ import Link from 'next/link' import { CodeBoard } from '../components/CodeBoard' import { Container } from '../components/Container' import { Discord } from '../components/Discord' -import { Header } from '../components/Header' import { URL_PREFIX } from '../constants' export default function HomePage() { return ( <> -
diff --git a/apps/landing/public/light.svg b/apps/landing/src/components/Header/ThemeSwitch.tsx similarity index 60% rename from apps/landing/public/light.svg rename to apps/landing/src/components/Header/ThemeSwitch.tsx index 289dcb98..f13df572 100644 --- a/apps/landing/public/light.svg +++ b/apps/landing/src/components/Header/ThemeSwitch.tsx @@ -1,5 +1,36 @@ - - { + setTheme(getTheme() === 'dark' ? 'default' : 'dark') + }} + > + + - + fill="currentColor" + fillRule="evenodd" + /> + + + ) +} diff --git a/apps/landing/src/components/Header/index.tsx b/apps/landing/src/components/Header/index.tsx index bb7f9c3e..fd45a530 100644 --- a/apps/landing/src/components/Header/index.tsx +++ b/apps/landing/src/components/Header/index.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import { URL_PREFIX } from '../../constants' import { HeaderWrap } from './HeaderWrap' +import { ThemeSwitch } from './ThemeSwitch' export function Header() { return ( @@ -66,7 +67,7 @@ export function Header() { - + diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index 70c77f38..79797c55 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -25,4 +25,4 @@ js-sys = "0.3.76" [dev-dependencies] wasm-bindgen-test = "0.3.50" - +serial_test = "3.2.0" diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 11592055..a15e0600 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -113,23 +113,33 @@ pub fn code_extract( pub fn object_to_typography(obj: Object, level: u8) -> Result { Ok(Typography::new( Reflect::get(&obj, &JsValue::from_str("fontFamily")) - .map(|v| v.as_string()) + .as_ref() + .map(js_value_to_string) .unwrap_or(None), Reflect::get(&obj, &JsValue::from_str("fontSize")) - .map(|v| v.as_string()) + .as_ref() + .map(js_value_to_string) .unwrap_or(None), Reflect::get(&obj, &JsValue::from_str("fontWeight")) - .map(|v| v.as_string()) + .as_ref() + .map(js_value_to_string) .unwrap_or(None), Reflect::get(&obj, &JsValue::from_str("lineHeight")) - .map(|v| v.as_string()) + .as_ref() + .map(js_value_to_string) .unwrap_or(None), Reflect::get(&obj, &JsValue::from_str("letterSpacing")) - .map(|v| v.as_string()) + .as_ref() + .map(js_value_to_string) .unwrap_or(None), level, )) } +pub fn js_value_to_string(js_value: &JsValue) -> Option { + js_value + .as_string() + .or_else(|| js_value.as_f64().map(|v| v.to_string())) +} fn theme_object_to_hashmap(js_value: JsValue) -> Result { let mut theme = Theme::default(); @@ -229,10 +239,12 @@ pub fn get_theme_interface( package_name: &str, color_interface_name: &str, typography_interface_name: &str, -) -> Result { + theme_interface_name: &str, +) -> String { let sheet = GLOBAL_STYLE_SHEET.lock().unwrap(); let mut color_keys = HashSet::new(); let mut typography_keys = HashSet::new(); + let mut theme_keys = HashSet::new(); for color_theme in sheet.theme.colors.themes.values() { color_theme.keys().for_each(|key| { color_keys.insert(key.clone()); @@ -242,11 +254,15 @@ pub fn get_theme_interface( typography_keys.insert(key.clone()); }); + sheet.theme.colors.themes.keys().for_each(|key| { + theme_keys.insert(key.clone()); + }); + if color_keys.is_empty() && typography_keys.is_empty() { - Ok("".to_string()) + String::new() } else { - Ok(format!( - "import \"{}\";declare module \"{}\"{{interface {} {{{}}}interface {} {{{}}}}}", + format!( + "import \"{}\";declare module \"{}\"{{interface {}{{{}}}interface {}{{{}}}interface {}{{{}}}}}", package_name, package_name, color_interface_name, @@ -260,7 +276,83 @@ pub fn get_theme_interface( .into_iter() .map(|key| format!("{}:null;", key)) .collect::>() + .join(""), + theme_interface_name, + theme_keys + .into_iter() + // key to pascal + .map(|key| format!("{}:null;", key)) + .collect::>() .join("") - )) + ) + } +} +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + + #[test] + #[serial] + fn test_code_extract() { + { + let mut sheet = GLOBAL_STYLE_SHEET.lock().unwrap(); + *sheet = StyleSheet::default(); + } + assert_eq!(get_css().unwrap(), ""); + + { + let mut sheet = GLOBAL_STYLE_SHEET.lock().unwrap(); + let mut theme = Theme::default(); + let mut color_theme = ColorTheme::default(); + color_theme.add_color("primary", "#000"); + theme.colors.add_theme("dark", color_theme); + + let mut color_theme = ColorTheme::default(); + color_theme.add_color("primary", "#FFF"); + theme.colors.add_theme("default", color_theme); + sheet.set_theme(theme); + } + + assert_eq!( + get_css().unwrap(), + ":root{--primary:#FFF;}\n:root[data-theme=dark]{--primary:#000;}\n" + ); + } + + #[test] + #[serial] + fn test_get_theme_interface() { + { + let mut sheet = GLOBAL_STYLE_SHEET.lock().unwrap(); + *sheet = StyleSheet::default(); + } + assert_eq!( + get_theme_interface( + "package", + "ColorInterface", + "TypographyInterface", + "ThemeInterface" + ), + "" + ); + + { + let mut sheet = GLOBAL_STYLE_SHEET.lock().unwrap(); + let mut theme = Theme::default(); + let mut color_theme = ColorTheme::default(); + color_theme.add_color("primary", "#000"); + theme.colors.add_theme("dark", color_theme); + sheet.set_theme(theme); + } + assert_eq!( + get_theme_interface( + "package", + "ColorInterface", + "TypographyInterface", + "ThemeInterface" + ), + "import \"package\";declare module \"package\"{interface ColorInterface{$primary:null;}interface TypographyInterface{}interface ThemeInterface{dark:null;}}" + ); } } diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 53c8624c..690a96d5 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -5,7 +5,7 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::sync::Mutex; -#[derive(Debug, PartialEq, Clone, Hash, Eq)] +#[derive(Debug, PartialEq, Clone, Hash, Eq, Ord, PartialOrd)] pub enum StyleSelector { Postfix(String), Prefix(String), @@ -16,6 +16,13 @@ impl From<&str> for StyleSelector { fn from(value: &str) -> Self { if let Some(s) = value.strip_prefix("group") { Dual("*[role=group]".to_string(), to_kebab_case(s)) + } else if let Some(s) = value.strip_prefix("theme") { + // first character should lower case + Prefix(format!( + ":root[data-theme={}{}]", + s.chars().next().unwrap().to_ascii_lowercase(), + &s[1..] + )) } else { Postfix(value.to_string()) } @@ -43,7 +50,7 @@ pub fn merge_selector(class_name: &str, selector: Option<&StyleSelector>) -> Str SelectorSeparator::Single => format!(".{}:{}", class_name, postfix), SelectorSeparator::Double => format!(".{}::{}", class_name, postfix), }, - Prefix(prefix) => format!("{} {}", prefix, class_name), + Prefix(prefix) => format!("{} .{}", prefix, class_name), Dual(prefix, postfix) => match get_selector_separator(postfix) { SelectorSeparator::Single => format!("{}:{} .{}", prefix, postfix, class_name), SelectorSeparator::Double => format!("{}::{} .{}", prefix, postfix, class_name), @@ -59,15 +66,6 @@ pub enum SelectorSeparator { Double, } -impl SelectorSeparator { - pub fn separator(&self) -> &str { - match self { - SelectorSeparator::Single => ":", - SelectorSeparator::Double => "::", - } - } -} - static DOUBLE_SEPARATOR: Lazy> = Lazy::new(|| { let mut set = HashSet::new(); @@ -534,5 +532,43 @@ mod tests { assert_eq!(Prefix(".cls".to_string()).to_string(), "-.cls-"); assert_eq!(Postfix(".cls".to_string()).to_string(), "-.cls"); + + assert_eq!( + StyleSelector::from("themeLight"), + Prefix(":root[data-theme=light]".to_string()) + ); + } + + #[test] + fn test_merge_selector() { + assert_eq!(merge_selector("cls", Some(&"hover".into())), ".cls:hover"); + assert_eq!( + merge_selector("cls", Some(&"placeholder".into())), + ".cls::placeholder" + ); + assert_eq!( + merge_selector("cls", Some(&"themeDark".into())), + ":root[data-theme=dark] .cls" + ); + assert_eq!( + merge_selector( + "cls", + Some(&Dual( + ":root[data-theme=dark]".to_string(), + "hover".to_string() + )), + ), + ":root[data-theme=dark]:hover .cls" + ); + assert_eq!( + merge_selector( + "cls", + Some(&Dual( + ":root[data-theme=dark]".to_string(), + "placeholder".to_string() + )), + ), + ":root[data-theme=dark]::placeholder .cls" + ); } } diff --git a/libs/extractor/src/gen_class_name.rs b/libs/extractor/src/gen_class_name.rs index 6d82e308..da898719 100644 --- a/libs/extractor/src/gen_class_name.rs +++ b/libs/extractor/src/gen_class_name.rs @@ -37,7 +37,7 @@ fn gen_class_name<'a>( None, ), )), - ExtractStyleProp::Responsive(res) => merge_expression_for_class_name( + ExtractStyleProp::StaticArray(res) => merge_expression_for_class_name( ast_builder, res.iter() .filter_map(|st| gen_class_name(ast_builder, st)) @@ -49,31 +49,31 @@ fn gen_class_name<'a>( alternate, .. } => { - let consequent = if let Some(con) = consequent { - gen_class_name(ast_builder, con).unwrap_or(Expression::StringLiteral( - ast_builder.alloc_string_literal(SPAN, "", None), - )) + let consequent = consequent + .as_ref() + .and_then(|con| gen_class_name(ast_builder, con)) + .unwrap_or_else(|| { + Expression::StringLiteral(ast_builder.alloc_string_literal(SPAN, "", None)) + }); + + let alternate = alternate + .as_ref() + .and_then(|alt| gen_class_name(ast_builder, alt)) + .unwrap_or_else(|| { + Expression::StringLiteral(ast_builder.alloc_string_literal(SPAN, "", None)) + }); + if is_same_expression(&consequent, &alternate) { + Some(consequent) } else { - Expression::StringLiteral(ast_builder.alloc_string_literal(SPAN, "", None)) - }; - let alternate = if let Some(alt) = alternate { - gen_class_name(ast_builder, alt).unwrap_or(Expression::StringLiteral( - ast_builder.alloc_string_literal(SPAN, "", None), + Some(Expression::ConditionalExpression( + ast_builder.alloc_conditional_expression( + SPAN, + condition.clone_in(ast_builder.allocator), + consequent, + alternate, + ), )) - } else { - Expression::StringLiteral(ast_builder.alloc_string_literal(SPAN, "", None)) - }; - if is_same_expression(&consequent, &alternate) { - return Some(consequent); } - Some(Expression::ConditionalExpression( - ast_builder.alloc_conditional_expression( - SPAN, - condition.clone_in(ast_builder.allocator), - consequent, - alternate, - ), - )) } } } diff --git a/libs/extractor/src/gen_style.rs b/libs/extractor/src/gen_style.rs index c62ac2b4..d50de264 100644 --- a/libs/extractor/src/gen_style.rs +++ b/libs/extractor/src/gen_style.rs @@ -6,7 +6,6 @@ use oxc_ast::ast::{ }; use oxc_ast::AstBuilder; use oxc_span::SPAN; - pub fn gen_styles<'a>( ast_builder: &AstBuilder<'a>, style_props: &[ExtractStyleProp<'a>], @@ -60,7 +59,7 @@ fn gen_style<'a>( )); } }, - ExtractStyleProp::Responsive(res) => { + ExtractStyleProp::StaticArray(res) => { properties.append( &mut res .iter() diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 8d35044b..5a242118 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -22,7 +22,7 @@ use std::error::Error; #[derive(Debug)] pub enum ExtractStyleProp<'a> { Static(ExtractStyleValue), - Responsive(Vec>), + StaticArray(Vec>), /// static + static ex) margin={test?"4px":"8px"} --> className={test?"margin-4px-0":"margin-8px-0"} /// static + dynamic ex) margin={test?a:"8px"} --> className={test?"margin-0":"margin-8px-0"} style={{ "--margin-0": a }} /// dynamic + dynamic ex) margin={test?a:b} --> className="margin-0" style={{ "--margin-0": test?a:b }} @@ -51,7 +51,7 @@ impl ExtractStyleProp<'_> { } styles } - ExtractStyleProp::Responsive(ref array) => { + ExtractStyleProp::StaticArray(ref array) => { array.iter().flat_map(|s| s.extract()).collect() } } @@ -445,6 +445,17 @@ mod tests { "test.tsx", r#"import { Box } from "@devup-ui/core"; ; +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap()); + assert_debug_snapshot!(extract( + "test.tsx", + r#"import { Flex } from "@devup-ui/core"; +; "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -1222,6 +1233,19 @@ export { } ) .unwrap()); + + reset_class_map(); + assert_debug_snapshot!(extract( + "test.js", + r#"import {Flex} from '@devup-ui/core' + + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap()); } #[test] @@ -1537,5 +1561,38 @@ import {Button} from '@devup/ui' } ) .unwrap()); + + reset_class_map(); + assert_debug_snapshot!(extract( + "test.js", + r#"import {css} from '@devup-ui/core' +
+ "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap()); + } + + #[test] + #[serial] + fn theme_props() { + reset_class_map(); + assert_debug_snapshot!(extract( + "test.js", + r#"import {Box} from '@devup-ui/core' + + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_file: None + } + ) + .unwrap()); } } diff --git a/libs/extractor/src/snapshots/extractor__tests__css_props_destructuring_assignment-2.snap b/libs/extractor/src/snapshots/extractor__tests__css_props_destructuring_assignment-2.snap new file mode 100644 index 00000000..296cd53f --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__css_props_destructuring_assignment-2.snap @@ -0,0 +1,54 @@ +--- +source: libs/extractor/src/lib.rs +expression: "extract(\"test.js\",\nr#\"import {css} from '@devup-ui/core'\n
\n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap()" +--- +ExtractOutput { + styles: [ + Static( + ExtractStaticStyle { + property: "margin", + value: "4px", + level: 0, + selector: None, + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "padding", + value: "4px", + level: 0, + selector: None, + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "border", + value: "solid 1px red", + level: 0, + selector: None, + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "blue", + level: 0, + selector: None, + basic: false, + }, + ), + ], + code: "import \"@devup-ui/core/devup-ui.css\";\nimport { css } from \"@devup-ui/core\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_responsive_style_props-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_responsive_style_props-2.snap new file mode 100644 index 00000000..afbdb219 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__extract_responsive_style_props-2.snap @@ -0,0 +1,36 @@ +--- +source: libs/extractor/src/lib.rs +expression: "extract(\"test.tsx\",\nr#\"import { Flex } from \"@devup-ui/core\";\n;\n\"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap()" +--- +ExtractOutput { + styles: [ + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + basic: true, + }, + ), + Static( + ExtractStaticStyle { + property: "display", + value: "none", + level: 0, + selector: None, + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 2, + selector: None, + basic: false, + }, + ), + ], + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__ternary_operator_in_selector-3.snap b/libs/extractor/src/snapshots/extractor__tests__ternary_operator_in_selector-3.snap new file mode 100644 index 00000000..dbda4785 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__ternary_operator_in_selector-3.snap @@ -0,0 +1,70 @@ +--- +source: libs/extractor/src/lib.rs +expression: "extract(\"test.js\",\nr#\"import {Flex} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap()" +--- +ExtractOutput { + styles: [ + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: None, + basic: true, + }, + ), + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: Some( + Postfix( + "hover", + ), + ), + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "blue", + level: 0, + selector: Some( + Postfix( + "hover", + ), + ), + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "fontWeight", + value: "bold", + level: 0, + selector: Some( + Postfix( + "hover", + ), + ), + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "red", + level: 0, + selector: Some( + Postfix( + "hover", + ), + ), + basic: false, + }, + ), + ], + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__theme_props.snap b/libs/extractor/src/snapshots/extractor__tests__theme_props.snap new file mode 100644 index 00000000..c7fe2221 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__theme_props.snap @@ -0,0 +1,35 @@ +--- +source: libs/extractor/src/lib.rs +expression: "extract(\"test.js\",\nr#\"import {Box} from '@devup-ui/core'\n \n \"#,\nExtractOption\n{ package: \"@devup-ui/core\".to_string(), css_file: None }).unwrap()" +--- +ExtractOutput { + styles: [ + Static( + ExtractStaticStyle { + property: "display", + value: "none", + level: 0, + selector: Some( + Prefix( + ":root[data-theme=dark]", + ), + ), + basic: false, + }, + ), + Static( + ExtractStaticStyle { + property: "display", + value: "flex", + level: 0, + selector: Some( + Prefix( + ":root[data-theme=light]", + ), + ), + basic: false, + }, + ), + ], + code: "import \"@devup-ui/core/devup-ui.css\";\n
;\n", +} diff --git a/libs/extractor/src/style_extractor.rs b/libs/extractor/src/style_extractor.rs index 9cbfeb9f..6a97e1af 100644 --- a/libs/extractor/src/style_extractor.rs +++ b/libs/extractor/src/style_extractor.rs @@ -5,7 +5,6 @@ use oxc_ast::ast::{Expression, JSXAttributeValue, ObjectPropertyKind, PropertyKe use crate::extract_style::ExtractStyleValue::{Dynamic, Static, Typography}; use crate::extract_style::{ExtractDynamicStyle, ExtractStaticStyle}; -use crate::style_extractor::ExtractResult::ChangeTag; use oxc_ast::AstBuilder; use oxc_span::SPAN; use oxc_syntax::operator::{BinaryOperator, LogicalOperator}; @@ -142,40 +141,39 @@ pub fn extract_style_from_expression<'a>( } if props_styles.is_empty() { ExtractResult::Maintain + } else if let Some(tag) = tag { + ExtractResult::ExtractStyleWithChangeTag(props_styles, tag) } else { - let ret = if let Some(tag) = tag { - ExtractResult::ExtractStyleWithChangeTag(props_styles, tag) - } else { - ExtractResult::ExtractStyle(props_styles) - }; - ret + ExtractResult::ExtractStyle(props_styles) } } Expression::ConditionalExpression(ref mut conditional) => { - let mut consequent = None; - let mut alternate = None; - if let ExtractResult::ExtractStyle(mut styles) = extract_style_from_expression( - ast_builder, - None, - &mut conditional.consequent, - level, - None, - ) { - consequent = Some(Box::new(styles.remove(0))); - } - if let ExtractResult::ExtractStyle(mut styles) = extract_style_from_expression( - ast_builder, - None, - &mut conditional.alternate, - level, - selector, - ) { - alternate = Some(Box::new(styles.remove(0))); - } ExtractResult::ExtractStyle(vec![ExtractStyleProp::Conditional { condition: conditional.test.clone_in(ast_builder.allocator), - consequent, - alternate, + consequent: if let ExtractResult::ExtractStyle(styles) = + extract_style_from_expression( + ast_builder, + None, + &mut conditional.consequent, + level, + None, + ) { + Some(Box::new(ExtractStyleProp::StaticArray(styles))) + } else { + None + }, + alternate: if let ExtractResult::ExtractStyle(styles) = + extract_style_from_expression( + ast_builder, + None, + &mut conditional.alternate, + level, + selector, + ) { + Some(Box::new(ExtractStyleProp::StaticArray(styles))) + } else { + None + }, }]) } Expression::ParenthesizedExpression(parenthesized) => extract_style_from_expression( @@ -195,7 +193,7 @@ pub fn extract_style_from_expression<'a>( } if name == "as" { - return ChangeTag(expression.clone_in(ast_builder.allocator)); + return ExtractResult::ChangeTag(expression.clone_in(ast_builder.allocator)); // return match expression { // Expression::StringLiteral(ident) => ExtractResult::ChangeTag( @@ -247,9 +245,7 @@ pub fn extract_style_from_expression<'a>( Some(selector), ); } - if name == "typography" { - typo = true; - } + typo = name == "typography"; } match expression { Expression::ComputedMemberExpression(mem) => { @@ -297,19 +293,18 @@ pub fn extract_style_from_expression<'a>( ExtractResult::Maintain } } - _ => { - if let Some(name) = name { - return ExtractResult::ExtractStyle(vec![ - ExtractStyleProp::Static(Dynamic(ExtractDynamicStyle::new( + _ => name + .map(|name| { + ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static( + Dynamic(ExtractDynamicStyle::new( name, level, expression_to_code(expression).as_str(), selector.map(|s| s.into()), - ))), - ]); - } - ExtractResult::Maintain - } + )), + )]) + }) + .unwrap_or_else(|| ExtractResult::Maintain), } } Expression::ObjectExpression(obj) => { @@ -371,38 +366,38 @@ pub fn extract_style_from_expression<'a>( } Expression::Identifier(_) => { if let Some(name) = name { - return ExtractResult::ExtractStyle(vec![ - ExtractStyleProp::Static(Dynamic(ExtractDynamicStyle::new( + ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static( + Dynamic(ExtractDynamicStyle::new( name, level, expression_to_code(expression).as_str(), selector.map(|s| s.into()), - ))), - ]); + )), + )]) + } else { + ExtractResult::Maintain } - ExtractResult::Maintain } _ => ExtractResult::Maintain, } } - Expression::Identifier(_) => { - if let Some(name) = name { - return ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static( - Dynamic(ExtractDynamicStyle::new( + Expression::Identifier(_) => name + .map(|name| { + ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static(Dynamic( + ExtractDynamicStyle::new( name, level, expression_to_code(expression).as_str(), selector.map(|s| s.into()), - )), - )]); - } - ExtractResult::Maintain - } + ), + ))]) + }) + .unwrap_or(ExtractResult::Maintain), _ => ExtractResult::Maintain, } } - Expression::NumericLiteral(v) => { - if let Some(name) = name { + Expression::NumericLiteral(v) => name + .map(|name| { ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static(Static( ExtractStaticStyle::new( name, @@ -411,10 +406,8 @@ pub fn extract_style_from_expression<'a>( selector.map(|s| s.into()), ), ))]) - } else { - ExtractResult::Maintain - } - } + }) + .unwrap_or(ExtractResult::Maintain), Expression::TemplateLiteral(tmp) => { if let Some(name) = name { if tmp.quasis.len() == 1 { @@ -442,8 +435,8 @@ pub fn extract_style_from_expression<'a>( ExtractResult::Maintain } } - Expression::StringLiteral(v) => { - if let Some(name) = name { + Expression::StringLiteral(v) => name + .map(|name| { ExtractResult::ExtractStyle(vec![ExtractStyleProp::Static(if typo { Typography(v.value.as_str().to_string()) } else { @@ -454,10 +447,8 @@ pub fn extract_style_from_expression<'a>( selector.map(|s| s.into()), )) })]) - } else { - ExtractResult::Maintain - } - } + }) + .unwrap_or(ExtractResult::Maintain), Expression::Identifier(identifier) => { if IGNORED_IDENTIFIERS.contains(&identifier.name.as_str()) { ExtractResult::Maintain @@ -483,7 +474,9 @@ pub fn extract_style_from_expression<'a>( level, selector, ) { - ExtractResult::ExtractStyle(mut styles) => Some(Box::new(styles.remove(0))), + ExtractResult::ExtractStyle(styles) => { + Some(Box::new(ExtractStyleProp::StaticArray(styles))) + } _ => None, } }); @@ -495,7 +488,6 @@ pub fn extract_style_from_expression<'a>( alternate: res, }]) } - LogicalOperator::And => { ExtractResult::ExtractStyle(vec![ExtractStyleProp::Conditional { condition: logical.left.clone_in(ast_builder.allocator), @@ -542,15 +534,13 @@ pub fn extract_style_from_expression<'a>( let mut props = vec![]; for (idx, element) in array.elements.iter_mut().enumerate() { - let a = extract_style_from_expression( + if let ExtractResult::ExtractStyle(mut styles) = extract_style_from_expression( ast_builder, name, element.to_expression_mut(), idx as u8, selector, - ); - - if let ExtractResult::ExtractStyle(mut styles) = a { + ) { props.append(&mut styles); } } @@ -572,7 +562,7 @@ pub fn extract_style_from_expression<'a>( level, selector, ) { - Some(Box::new(ExtractStyleProp::Responsive(styles))) + Some(Box::new(ExtractStyleProp::StaticArray(styles))) } else { None }, @@ -584,7 +574,7 @@ pub fn extract_style_from_expression<'a>( level, selector, ) { - Some(Box::new(ExtractStyleProp::Responsive(styles))) + Some(Box::new(ExtractStyleProp::StaticArray(styles))) } else { None }, @@ -594,10 +584,9 @@ pub fn extract_style_from_expression<'a>( let mut props = vec![]; for p in obj.properties.iter_mut() { if let ObjectPropertyKind::ObjectProperty(ref mut o) = p { - let name = o.key.name().unwrap(); if let ExtractResult::ExtractStyle(ref mut ret) = extract_style_from_expression( ast_builder, - Some(&name), + Some(&o.key.name().unwrap()), &mut o.value, level, selector, diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 41af7d94..d277c6d0 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -8,23 +8,19 @@ use std::collections::HashSet; /// Convert a value to a pixel value pub fn convert_value(value: &str) -> String { - let value = value.to_string(); - if let Ok(num) = value.parse::() { - let num = num * 4.0; - return format!("{}px", num); - } value + .parse::() + .map_or_else(|_| value.to_string(), |num| format!("{}px", num * 4.0)) } pub fn expression_to_code(expression: &Expression) -> String { - let source = ""; let allocator = Allocator::default(); - let ast_builder = oxc_ast::AstBuilder::new(&allocator); - let mut parsed = Parser::new(&allocator, source, SourceType::d_ts()).parse(); + let mut parsed = Parser::new(&allocator, "", SourceType::d_ts()).parse(); parsed.program.body.insert( 0, Statement::ExpressionStatement( - ast_builder.alloc_expression_statement(SPAN, expression.clone_in(&allocator)), + oxc_ast::AstBuilder::new(&allocator) + .alloc_expression_statement(SPAN, expression.clone_in(&allocator)), ), ); let code = Codegen::new().build(&parsed.program).code; diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index 04762240..172f18ec 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -112,13 +112,28 @@ impl StyleSheet { let mut sorted_props = props.iter().collect::>(); sorted_props.sort_by(|a, b| { if a.basic == b.basic { - if a.selector.is_some() && b.selector.is_none() { - return Greater; + match (a.selector.is_some(), b.selector.is_some()) { + (true, false) => Greater, + (false, true) => Less, + (true, true) => { + if a.selector == b.selector { + if a.property == b.property { + a.value.cmp(&b.value) + } else { + a.property.cmp(&b.property) + } + } else { + a.selector.cmp(&b.selector) + } + } + (false, false) => { + if a.property == b.property { + a.value.cmp(&b.value) + } else { + a.property.cmp(&b.property) + } + } } - if a.selector.is_none() && b.selector.is_some() { - return Less; - } - a.property.cmp(&b.property) } else { b.basic.cmp(&a.basic) } @@ -179,13 +194,11 @@ mod tests { let mut sheet = StyleSheet::default(); sheet.add_property("test", "background-color", 1, "red", None, false); sheet.add_property("test", "background", 1, "some", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); sheet.add_property("test", "border", 0, "1px solid", None, false); sheet.add_property("test", "border-color", 0, "red", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); } #[test] @@ -200,7 +213,6 @@ mod tests { false, ); sheet.add_property("test", "background-color", 1, "some", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); @@ -213,13 +225,11 @@ mod tests { Some(&StyleSelector::Postfix("hover".to_string())), false, ); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); sheet.add_property("test", "background-color", 1, "red", None, false); sheet.add_property("test", "background", 1, "some", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); } #[test] @@ -227,19 +237,16 @@ mod tests { let mut sheet = StyleSheet::default(); sheet.add_property("test", "background-color", 1, "red", None, true); sheet.add_property("test", "background", 1, "some", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); sheet.add_property("test", "border", 0, "1px solid", None, false); sheet.add_property("test", "border-color", 0, "red", None, true); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); sheet.add_property("test", "display", 0, "flex", None, true); sheet.add_property("test", "display", 0, "block", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); } @@ -255,7 +262,12 @@ mod tests { false, ); sheet.add_property("test", "background-color", 1, "some", None, true); - sheet.set_theme(Theme::default()); + assert_debug_snapshot!(sheet.create_css()); + + let mut sheet = StyleSheet::default(); + sheet.add_property("test", "display", 0, "flex", None, true); + sheet.add_property("test", "display", 0, "none", None, false); + sheet.add_property("test", "display", 2, "flex", None, false); assert_debug_snapshot!(sheet.create_css()); } @@ -263,12 +275,15 @@ mod tests { fn test_create_css() { let mut sheet = StyleSheet::default(); sheet.add_property("test", "mx", 1, "40px", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); let mut sheet = StyleSheet::default(); sheet.add_css("test", "display:flex;"); - sheet.set_theme(Theme::default()); + assert_debug_snapshot!(sheet.create_css()); + + let mut sheet = StyleSheet::default(); + sheet.add_property("test", "mx", 2, "40px", None, false); + sheet.add_property("test", "my", 2, "40px", None, false); assert_debug_snapshot!(sheet.create_css()); } @@ -276,7 +291,6 @@ mod tests { fn wrong_breakpoint() { let mut sheet = StyleSheet::default(); sheet.add_property("test", "mx", 10, "40px", None, false); - sheet.set_theme(Theme::default()); assert_debug_snapshot!(sheet.create_css()); } @@ -285,7 +299,22 @@ mod tests { let mut sheet = StyleSheet::default(); sheet.add_property("test", "mx", 1, "40px", Some(&"groupHover".into()), false); sheet.add_property("test", "mx", 2, "50px", Some(&"groupHover".into()), false); - sheet.set_theme(Theme::default()); + assert_debug_snapshot!(sheet.create_css()); + } + + #[test] + fn test_theme_selector() { + let mut sheet = StyleSheet::default(); + sheet.add_property("test", "mx", 0, "40px", Some(&"themeDark".into()), false); + sheet.add_property("test", "my", 0, "40px", Some(&"themeDark".into()), false); + sheet.add_property("test", "mx", 0, "50px", Some(&"themeLight".into()), false); + assert_debug_snapshot!(sheet.create_css()); + + let mut sheet = StyleSheet::default(); + sheet.add_property("test", "mx", 0, "50px", Some(&"themeLight".into()), false); + sheet.add_property("test", "mx", 0, "41px", None, false); + sheet.add_property("test", "mx", 0, "51px", Some(&"themeLight".into()), false); + sheet.add_property("test", "mx", 0, "42px", None, false); assert_debug_snapshot!(sheet.create_css()); } } diff --git a/libs/sheet/src/snapshots/sheet__tests__create_css-3.snap b/libs/sheet/src/snapshots/sheet__tests__create_css-3.snap new file mode 100644 index 00000000..f2393096 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__create_css-3.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: sheet.create_css() +--- +"\n@media (min-width:768px){.test{margin-left:40px;margin-right:40px;}.test{margin-top:40px;margin-bottom:40px;}}" diff --git a/libs/sheet/src/snapshots/sheet__tests__create_css_with_selector_and_basic_sort_test-2.snap b/libs/sheet/src/snapshots/sheet__tests__create_css_with_selector_and_basic_sort_test-2.snap new file mode 100644 index 00000000..33689d2e --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__create_css_with_selector_and_basic_sort_test-2.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: sheet.create_css() +--- +".test{display:flex}.test{display:none}\n@media (min-width:768px){.test{display:flex}}" diff --git a/libs/sheet/src/snapshots/sheet__tests__theme_selector-2.snap b/libs/sheet/src/snapshots/sheet__tests__theme_selector-2.snap new file mode 100644 index 00000000..17a4f92f --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__theme_selector-2.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: sheet.create_css() +--- +".test{margin-left:41px;margin-right:41px;}.test{margin-left:42px;margin-right:42px;}:root[data-theme=light] .test{margin-left:50px;margin-right:50px;}:root[data-theme=light] .test{margin-left:51px;margin-right:51px;}" diff --git a/libs/sheet/src/snapshots/sheet__tests__theme_selector.snap b/libs/sheet/src/snapshots/sheet__tests__theme_selector.snap new file mode 100644 index 00000000..bafbf207 --- /dev/null +++ b/libs/sheet/src/snapshots/sheet__tests__theme_selector.snap @@ -0,0 +1,5 @@ +--- +source: libs/sheet/src/lib.rs +expression: sheet.create_css() +--- +":root[data-theme=dark] .test{margin-left:40px;margin-right:40px;}:root[data-theme=dark] .test{margin-top:40px;margin-bottom:40px;}:root[data-theme=light] .test{margin-left:50px;margin-right:50px;}" diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index d8420144..481d2d14 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -246,4 +246,15 @@ mod tests { ); assert_eq!(theme.to_css(), ""); } + + #[test] + fn update_break_points() { + let mut theme = Theme::default(); + theme.update_break_points(vec![0, 480, 768, 992, 1280]); + assert_eq!(theme.break_points, vec![0, 480, 768, 992, 1280]); + theme.update_break_points(vec![0, 480, 768, 992, 1280, 1600]); + assert_eq!(theme.break_points, vec![0, 480, 768, 992, 1280, 1600]); + theme.update_break_points(vec![0, 480, 768, 992, 1280, 1600, 1920]); + assert_eq!(theme.break_points, vec![0, 480, 768, 992, 1280, 1600, 1920]); + } } diff --git a/package.json b/package.json index 7b16d205..276e9a2a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "vitest": "^3.0.3", "@vitest/coverage-v8": "^3.0.3", "@changesets/cli": "^2.27.11", - "@types/node": "^22.10.7" + "@types/node": "^22.10.7", + "happy-dom": "^16.7.2", + "@testing-library/react": "^16.2.0" }, "author": "devfive", "packageManager": "pnpm@9.15.4", diff --git a/packages/react/package.json b/packages/react/package.json index 1d4b17a3..10e6ef3a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,6 +27,7 @@ "csstype": "^3.1" }, "devDependencies": { + "rollup-plugin-preserve-directives": "^0.4.0", "vite": "^6.0.11", "vite-plugin-dts": "^4.5.0", "vitest": "^3.0.3", diff --git a/packages/react/src/__tests__/index.test.ts b/packages/react/src/__tests__/index.test.ts index 9e29b154..9f683625 100644 --- a/packages/react/src/__tests__/index.test.ts +++ b/packages/react/src/__tests__/index.test.ts @@ -13,6 +13,13 @@ describe('export', () => { Grid: expect.any(Function), css: expect.any(Function), + + ThemeScript: expect.any(Function), + + getTheme: expect.any(Function), + setTheme: expect.any(Function), + + useTheme: expect.any(Function), }) }) }) diff --git a/packages/react/src/components/ThemeScript.tsx b/packages/react/src/components/ThemeScript.tsx new file mode 100644 index 00000000..fbc08641 --- /dev/null +++ b/packages/react/src/components/ThemeScript.tsx @@ -0,0 +1,19 @@ +import type { DevupTheme } from '../types/theme' + +interface ThemeScriptProps { + auto?: boolean + theme?: keyof DevupTheme +} + +export function ThemeScript({ auto = true, theme }: ThemeScriptProps) { + return ( + +
+`; + +exports[`ThemeScript > should apply ThemeScript with not auto 1`] = ` +
+ +
+`; + +exports[`ThemeScript > should apply ThemeScript with theme 1`] = ` +
+ +
+`; diff --git a/packages/react/src/hooks/__tests__/use-safe-effect.browser.test.ts b/packages/react/src/hooks/__tests__/use-safe-effect.browser.test.ts new file mode 100644 index 00000000..7fba8f2f --- /dev/null +++ b/packages/react/src/hooks/__tests__/use-safe-effect.browser.test.ts @@ -0,0 +1,9 @@ +import { useLayoutEffect } from 'react' + +import { useSafeEffect } from '../use-safe-effect' + +describe('useSafeEffect', () => { + it('should return useLayoutEffect', async () => { + expect(useSafeEffect).toBe(useLayoutEffect) + }) +}) diff --git a/packages/react/src/hooks/__tests__/use-safe-effect.test.ts b/packages/react/src/hooks/__tests__/use-safe-effect.test.ts new file mode 100644 index 00000000..f566810d --- /dev/null +++ b/packages/react/src/hooks/__tests__/use-safe-effect.test.ts @@ -0,0 +1,19 @@ +import { useEffect, useLayoutEffect } from 'react' + +beforeEach(() => { + vi.resetModules() +}) +describe('useSafeEffect', () => { + it('return useEffect in the server', async () => { + const { useSafeEffect } = await import('../use-safe-effect') + // @ts-ignore + expect(useSafeEffect).toBe(useEffect) + }) + it('return useEffect in the client', async () => { + // @ts-ignore + globalThis.window = {} + + const { useSafeEffect } = await import('../use-safe-effect') + expect(useSafeEffect).toBe(useLayoutEffect) + }) +}) diff --git a/packages/react/src/hooks/__tests__/use-theme.browser.test.ts b/packages/react/src/hooks/__tests__/use-theme.browser.test.ts new file mode 100644 index 00000000..714d1df8 --- /dev/null +++ b/packages/react/src/hooks/__tests__/use-theme.browser.test.ts @@ -0,0 +1,26 @@ +import { renderHook, waitFor } from '@testing-library/react' + +beforeEach(() => { + vi.resetModules() +}) + +describe('useTheme', () => { + it('should return theme', async () => { + const { useTheme } = await import('../use-theme') + const { result } = renderHook(() => useTheme()) + expect(result.current).toBeNull() + + document.documentElement.setAttribute('data-theme', 'dark') + await waitFor(() => { + expect(result.current).toBe('dark') + }) + const { result: newResult } = renderHook(() => useTheme()) + expect(newResult.current).toBe('dark') + }) + it('should return theme when already set', async () => { + const { useTheme } = await import('../use-theme') + document.documentElement.setAttribute('data-theme', 'dark') + const { result } = renderHook(() => useTheme()) + expect(result.current).toBe('dark') + }) +}) diff --git a/packages/react/src/hooks/use-safe-effect.ts b/packages/react/src/hooks/use-safe-effect.ts new file mode 100644 index 00000000..a2a7f125 --- /dev/null +++ b/packages/react/src/hooks/use-safe-effect.ts @@ -0,0 +1,5 @@ +'use client' +import { useEffect, useLayoutEffect } from 'react' + +export const useSafeEffect: typeof useEffect = + typeof window === 'undefined' ? useEffect : useLayoutEffect diff --git a/packages/react/src/hooks/use-theme.ts b/packages/react/src/hooks/use-theme.ts new file mode 100644 index 00000000..0ce90307 --- /dev/null +++ b/packages/react/src/hooks/use-theme.ts @@ -0,0 +1,48 @@ +'use client' +import { useId, useState } from 'react' + +import type { DevupTheme } from '../types/theme' +import { useSafeEffect } from './use-safe-effect' + +let observer: null | MutationObserver = null +const setThemeMap: Record> = {} +let globalTheme: keyof DevupTheme | null = null + +export function useTheme(): keyof DevupTheme | null { + const id = useId() + const [theme, setTheme] = useState(globalTheme) + useSafeEffect(() => { + if (globalTheme !== null) return + const currentTheme = document.documentElement.getAttribute('data-theme') + if (currentTheme !== null && currentTheme !== theme) + setTheme(currentTheme as keyof DevupTheme) + }, []) + useSafeEffect(() => { + const targetNode = document.documentElement + setThemeMap[id] = setTheme + if (!observer) { + observer = new MutationObserver(() => { + const theme = document.documentElement.getAttribute('data-theme') + globalTheme = theme as keyof DevupTheme + for (const key in setThemeMap) + setThemeMap[key](theme as keyof DevupTheme) + }) + observer.observe(targetNode, { + attributes: true, + attributeFilter: ['data-theme'], + childList: false, + subtree: false, + characterData: false, + attributeOldValue: false, + characterDataOldValue: false, + }) + } + + return () => { + delete setThemeMap[id] + if (observer && Object.keys(setThemeMap).length === 0) + observer.disconnect() + } + }, [id]) + return theme +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index e0c99c4a..d9b0bacd 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,8 +6,12 @@ export { Grid } from './components/Grid' export { Image } from './components/Image' export { Input } from './components/Input' export { Text } from './components/Text' +export { ThemeScript } from './components/ThemeScript' export { VStack } from './components/VStack' +export { useTheme } from './hooks/use-theme' export type { DevupProps } from './types/props' -export type { DevupThemeColors } from './types/theme' +export type { DevupTheme, DevupThemeColors } from './types/theme' export type { DevupThemeTypography } from './types/typography' export { css } from './utils/css' +export { getTheme } from './utils/get-theme' +export { setTheme } from './utils/set-theme' diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts index 60dde67f..41a90b51 100644 --- a/packages/react/src/types/props/index.ts +++ b/packages/react/src/types/props/index.ts @@ -21,7 +21,7 @@ import type { DevupUiOverflowProps } from './overflow' import type { DevupUiOverflowBehaviorProps } from './overflow-behavior' import type { DevupUiPositionProps } from './position' import type { DevupUiScrollbarProps } from './scrollbar' -import type { DevupSelectorProps } from './selector' +import type { DevupSelectorProps, DevupThemeSelectorProps } from './selector' import type { DevupUiShapeProps } from './shape' import type { DevupUiTableProps } from './table' import type { DevupUiTextProps } from './text' @@ -61,7 +61,10 @@ export interface DevupCommonProps DevupUiUiProps, DevupUiViewTransitionProps {} -export interface DevupProps extends DevupCommonProps, DevupSelectorProps { +export interface DevupProps + extends DevupCommonProps, + DevupSelectorProps, + DevupThemeSelectorProps { as?: React.ElementType } diff --git a/packages/react/src/types/props/selector/index.ts b/packages/react/src/types/props/selector/index.ts index 61df135c..db02c2a6 100644 --- a/packages/react/src/types/props/selector/index.ts +++ b/packages/react/src/types/props/selector/index.ts @@ -1,5 +1,15 @@ +import type { DevupTheme } from '../../theme' import type { DevupCommonProps } from '../index' +type toPascalCase = S extends `${infer T}${infer U}` + ? `${Uppercase}${U}` + : S + +export type DevupThemeSelectorProps = { + [K in keyof DevupTheme as `_theme${toPascalCase}`]?: DevupCommonProps & + DevupSelectorProps +} + export interface DevupSelectorProps { _active?: DevupCommonProps _checked?: DevupCommonProps diff --git a/packages/react/src/types/theme.ts b/packages/react/src/types/theme.ts index d72a6113..c4163c27 100644 --- a/packages/react/src/types/theme.ts +++ b/packages/react/src/types/theme.ts @@ -1,2 +1,5 @@ /* eslint-disable @typescript-eslint/no-empty-object-type */ export interface DevupThemeColors {} + +/* eslint-disable @typescript-eslint/no-empty-object-type */ +export interface DevupTheme {} diff --git a/packages/react/src/utils/__tests__/index.test.ts b/packages/react/src/utils/__tests__/css.test.ts similarity index 100% rename from packages/react/src/utils/__tests__/index.test.ts rename to packages/react/src/utils/__tests__/css.test.ts diff --git a/packages/react/src/utils/__tests__/get-theme.browser.test.ts b/packages/react/src/utils/__tests__/get-theme.browser.test.ts new file mode 100644 index 00000000..ae36f540 --- /dev/null +++ b/packages/react/src/utils/__tests__/get-theme.browser.test.ts @@ -0,0 +1,8 @@ +import { getTheme } from '../get-theme' + +describe('getTheme', () => { + it('should return theme', async () => { + document.documentElement.setAttribute('data-theme', 'dark') + expect(getTheme()).toBe('dark') + }) +}) diff --git a/packages/react/src/utils/__tests__/set-theme.browser.test.ts b/packages/react/src/utils/__tests__/set-theme.browser.test.ts new file mode 100644 index 00000000..e5bdfcc1 --- /dev/null +++ b/packages/react/src/utils/__tests__/set-theme.browser.test.ts @@ -0,0 +1,10 @@ +import type { DevupTheme } from '../../types/theme' +import { setTheme } from '../set-theme' + +describe('setTheme', () => { + it('should set theme', async () => { + expect(document.documentElement.getAttribute('data-theme')).toBe(null) + setTheme('dark' as keyof DevupTheme) + expect(document.documentElement.getAttribute('data-theme')).toBe('dark') + }) +}) diff --git a/packages/react/src/utils/get-theme.ts b/packages/react/src/utils/get-theme.ts new file mode 100644 index 00000000..47fc7643 --- /dev/null +++ b/packages/react/src/utils/get-theme.ts @@ -0,0 +1,7 @@ +'use client' + +import { DevupTheme } from '../types/theme' + +export function getTheme(): keyof DevupTheme | null { + return document.documentElement.getAttribute('data-theme') as keyof DevupTheme +} diff --git a/packages/react/src/utils/set-theme.ts b/packages/react/src/utils/set-theme.ts new file mode 100644 index 00000000..92f642d3 --- /dev/null +++ b/packages/react/src/utils/set-theme.ts @@ -0,0 +1,8 @@ +'use client' + +import { DevupTheme } from '../types/theme' + +export function setTheme(theme: keyof DevupTheme): void { + document.documentElement.setAttribute('data-theme', theme) + localStorage.setItem('__DF_THEME_SELECTED__', theme) +} diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 970d8913..6893a9b2 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -1,3 +1,4 @@ +import preserveDirectives from 'rollup-plugin-preserve-directives' import dts from 'vite-plugin-dts' import { defineConfig } from 'vitest/config' @@ -37,6 +38,7 @@ export default defineConfig({ return } }, + plugins: [preserveDirectives()], external: (source) => { return !(source.includes('src') || source.startsWith('.')) }, diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts index 14ee6785..11088346 100644 --- a/packages/vite-plugin/src/__tests__/plugin.test.ts +++ b/packages/vite-plugin/src/__tests__/plugin.test.ts @@ -45,6 +45,7 @@ describe('devupUIPlugin', () => { libPackage, 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) expect(readFileSync).toHaveBeenCalledWith(devupPath, 'utf-8') expect(existsSync).toHaveBeenCalledWith(interfacePath) @@ -135,6 +136,7 @@ describe('devupUIPlugin', () => { libPackage, 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) expect(readFileSync).toHaveBeenCalledWith(devupPath, 'utf-8') expect(existsSync).toHaveBeenCalledWith(interfacePath) @@ -172,6 +174,7 @@ describe('devupUIPlugin', () => { libPackage, 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) expect(readFileSync).toHaveBeenCalledWith(devupPath, 'utf-8') expect(existsSync).toHaveBeenCalledWith(interfacePath) diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts index 3470fc76..c132238b 100644 --- a/packages/vite-plugin/src/plugin.ts +++ b/packages/vite-plugin/src/plugin.ts @@ -27,6 +27,7 @@ function writeDataFiles(options: Omit) { options.package, 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) if (interfaceCode) { if (!existsSync(options.interfacePath)) mkdirSync(options.interfacePath) diff --git a/packages/webpack-plugin/src/__tests__/plugin.test.ts b/packages/webpack-plugin/src/__tests__/plugin.test.ts index b8c40041..252bbd9e 100644 --- a/packages/webpack-plugin/src/__tests__/plugin.test.ts +++ b/packages/webpack-plugin/src/__tests__/plugin.test.ts @@ -66,6 +66,7 @@ describe('devupUIPlugin', () => { '@devup-ui/react', 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) expect(mkdirSync).toHaveBeenCalledWith('.df') expect(writeFileSync).toHaveBeenCalledWith( diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts index 1d4d4685..6d93d9a7 100644 --- a/packages/webpack-plugin/src/plugin.ts +++ b/packages/webpack-plugin/src/plugin.ts @@ -47,6 +47,7 @@ export class DevupUIWebpackPlugin { this.options.package, 'DevupThemeColors', 'DevupThemeTypography', + 'DevupTheme', ) if (interfaceCode) { if (!existsSync(this.options.interfacePath)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87970174..184f1c5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,21 +14,27 @@ importers: '@changesets/cli': specifier: ^2.27.11 version: 2.27.11 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/node': specifier: ^22.10.7 version: 22.10.7 '@vitest/coverage-v8': specifier: ^3.0.3 - version: 3.0.3(vitest@3.0.3(@types/node@22.10.7)(terser@5.37.0)) + version: 3.0.3(vitest@3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0)) eslint: specifier: ^9.18.0 version: 9.18.0 eslint-plugin-devup: specifier: ^2.0.1 version: 2.0.1(@types/eslint@9.6.1)(@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3) + happy-dom: + specifier: ^16.7.2 + version: 16.7.2 vitest: specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(terser@5.37.0) + version: 3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0) apps/landing: dependencies: @@ -299,7 +305,7 @@ importers: version: 4.5.0(@types/node@22.10.7)(rollup@4.31.0)(typescript@5.7.3)(vite@6.0.11(@types/node@22.10.7)(terser@5.37.0)) vitest: specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(terser@5.37.0) + version: 3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0) packages/react: dependencies: @@ -313,6 +319,9 @@ importers: '@types/react': specifier: ^19 version: 19.0.7 + rollup-plugin-preserve-directives: + specifier: ^0.4.0 + version: 0.4.0(rollup@4.31.0) typescript: specifier: ^5.7.3 version: 5.7.3 @@ -324,7 +333,7 @@ importers: version: 4.5.0(@types/node@22.10.7)(rollup@4.31.0)(typescript@5.7.3)(vite@6.0.11(@types/node@22.10.7)(terser@5.37.0)) vitest: specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(terser@5.37.0) + version: 3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0) packages/vite-plugin: dependencies: @@ -362,7 +371,7 @@ importers: version: 4.5.0(@types/node@22.10.7)(rollup@4.31.0)(typescript@5.7.3)(vite@6.0.11(@types/node@22.10.7)(terser@5.37.0)) vitest: specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(terser@5.37.0) + version: 3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0) packages: @@ -1826,6 +1835,25 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@ts-morph/common@0.20.0': resolution: {integrity: sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==} @@ -1835,6 +1863,9 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2362,6 +2393,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -2372,6 +2407,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -2678,6 +2716,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3069,6 +3110,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@16.7.2: + resolution: {integrity: sha512-zOzw0xyYlDaF/ylwbAsduYZZVRTd5u7IwlFkGbEathIeJMLp3vrN3cHm3RS7PZpD9gr/IO16bHEswcgNyWTsqw==} + engines: {node: '>=18.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3451,6 +3496,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3863,6 +3912,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prismjs@1.27.0: resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} engines: {node: '>=6'} @@ -3909,6 +3962,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -4013,6 +4069,11 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup-plugin-preserve-directives@0.4.0: + resolution: {integrity: sha512-gx4nBxYm5BysmEQS+e2tAMrtFxrGvk+Pe5ppafRibQi0zlW7VYAbEGk6IKDw9sJGPdFWgVTE0o4BU4cdG0Fylg==} + peerDependencies: + rollup: 2.x || 3.x || 4.x + rollup@4.31.0: resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4522,6 +4583,10 @@ packages: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -4536,6 +4601,10 @@ packages: webpack-cli: optional: true + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6281,6 +6350,27 @@ snapshots: - supports-color - typescript + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.7 + '@types/react-dom': 19.0.3(@types/react@19.0.7) + '@ts-morph/common@0.20.0': dependencies: fast-glob: 3.3.3 @@ -6294,6 +6384,8 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.5 @@ -6480,7 +6572,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.3(vitest@3.0.3(@types/node@22.10.7)(terser@5.37.0))': + '@vitest/coverage-v8@3.0.3(vitest@3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6494,7 +6586,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.3(@types/node@22.10.7)(terser@5.37.0) + vitest: 3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0) transitivePeerDependencies: - supports-color @@ -7187,6 +7279,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} argparse@1.0.10: @@ -7195,6 +7289,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.3 @@ -7523,6 +7621,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.1 @@ -8093,6 +8193,11 @@ snapshots: graphemer@1.4.0: {} + happy-dom@16.7.2: + dependencies: + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -8497,6 +8602,8 @@ snapshots: dependencies: yallist: 4.0.0 + lz-string@1.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -9093,6 +9200,12 @@ snapshots: prettier@3.4.2: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prismjs@1.27.0: {} prismjs@1.29.0: {} @@ -9134,6 +9247,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-refresh@0.14.2: {} react-syntax-highlighter@15.6.1(react@19.0.0): @@ -9292,6 +9407,12 @@ snapshots: reusify@1.0.4: {} + rollup-plugin-preserve-directives@0.4.0(rollup@4.31.0): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.31.0) + magic-string: 0.30.17 + rollup: 4.31.0 + rollup@4.31.0: dependencies: '@types/estree': 1.0.6 @@ -9851,7 +9972,7 @@ snapshots: fsevents: 2.3.3 terser: 5.37.0 - vitest@3.0.3(@types/node@22.10.7)(terser@5.37.0): + vitest@3.0.3(@types/node@22.10.7)(happy-dom@16.7.2)(terser@5.37.0): dependencies: '@vitest/expect': 3.0.3 '@vitest/mocker': 3.0.3(vite@6.0.11(@types/node@22.10.7)(terser@5.37.0)) @@ -9875,6 +9996,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.7 + happy-dom: 16.7.2 transitivePeerDependencies: - jiti - less @@ -9896,6 +10018,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + webidl-conversions@7.0.0: {} + webpack-sources@3.2.3: {} webpack@5.97.1: @@ -9928,6 +10052,8 @@ snapshots: - esbuild - uglify-js + whatwg-mimetype@3.0.0: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/vitest.config.ts b/vitest.config.ts index a32cfd32..6d0bede5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,5 +7,23 @@ export default defineConfig({ include: ['packages/*/src/**'], exclude: ['packages/*/src/types', 'packages/*/src/**/__tests__'], }, + workspace: [ + { + test: { + name: 'node', + include: ['packages/*/src/**/__tests__/**/*.test.{ts,tsx}'], + exclude: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], + globals: true, + }, + }, + { + test: { + include: ['packages/*/src/**/__tests__/**/*.browser.test.{ts,tsx}'], + name: 'happy-dom', + environment: 'happy-dom', + globals: true, + }, + }, + ], }, }) diff --git a/vitest.workspace.ts b/vitest.workspace.ts deleted file mode 100644 index 8222369f..00000000 --- a/vitest.workspace.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineWorkspace } from 'vitest/config' - -export default defineWorkspace([ - { - test: { - name: 'node', - include: ['packages/*/src/**/__tests__/**/*.test.*'], - globals: true, - }, - }, -])