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 @@
-
+
+ )
+}
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 (
+
+ )
+}
diff --git a/packages/react/src/components/__tests__/ThemeScript.browser.test.tsx b/packages/react/src/components/__tests__/ThemeScript.browser.test.tsx
new file mode 100644
index 00000000..85318704
--- /dev/null
+++ b/packages/react/src/components/__tests__/ThemeScript.browser.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@testing-library/react'
+import { expect } from 'vitest'
+
+import { DevupTheme } from '../../types/theme'
+import { ThemeScript } from '../ThemeScript'
+
+describe('ThemeScript', () => {
+ it('should apply ThemeScript', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+ it('should apply ThemeScript with theme', () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toMatchSnapshot()
+ })
+ it('should apply ThemeScript with not auto', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+})
diff --git a/packages/react/src/components/__tests__/__snapshots__/ThemeScript.browser.test.tsx.snap b/packages/react/src/components/__tests__/__snapshots__/ThemeScript.browser.test.tsx.snap
new file mode 100644
index 00000000..5d839b64
--- /dev/null
+++ b/packages/react/src/components/__tests__/__snapshots__/ThemeScript.browser.test.tsx.snap
@@ -0,0 +1,27 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`ThemeScript > should apply ThemeScript 1`] = `
+
+
+
+`;
+
+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,
- },
- },
-])