Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_global_css_fixes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "globalCss: theme tokens ($text) resolve to var(--text) in global rules instead of being written as invalid $text; imports given as { url, query } objects with unquoted keys are emitted instead of dropped; an '@layer name' key puts the rules it holds in that cascade layer (layers nest as base.reset) instead of flattening them into invalid declarations, and a layer name that is not one identifier is a build error",
"date": "2026-09-30T00:00:00.000Z"
}
9 changes: 9 additions & 0 deletions apps/landing/src/app/(detail)/docs/features/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,15 @@ globalCss({
})
```

Theme tokens work as in any style (`body: { color: '$text' }`). `imports` takes URLs or `{ url, query }` objects, emitted as `@import` rules; an `'@layer name'` key puts the rules it holds in that cascade layer, and layers nest (`'@layer base': { '@layer reset': { ... } }` is `base.reset`):

```tsx
globalCss({
imports: ['reset.css', { url: 'print.css', query: 'print' }],
'@layer utilities': { '.hidden': { display: 'none' } },
})
```

## keyframes() API

Define CSS animations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ fn collect_global_styles<'a>(
);
}
}
} else if let Some(layer) = name
.strip_prefix("@layer")
.filter(|rest| rest.starts_with(char::is_whitespace))
.map(str::trim)
{
if layer.is_empty()
|| !layer.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')
})
{
styles.push(unreadable_key(&o.key, false));
continue;
}
// `'@layer name': { selector: rules }` puts the rules it holds in the layer
let mut layered = vec![];
collect_global_styles(
ast_builder,
&mut o.value,
file,
at_rules,
&mut layered,
);
place_in_layer(&mut layered, layer);
styles.extend(layered);
} else if let Some(at_rule) = global_at_rule_key(&name) {
let mut nested = at_rules.to_vec();
nested.push(at_rule);
Expand All @@ -106,9 +130,7 @@ fn collect_global_styles<'a>(
let mut query = None;
for p in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(o) = p
&& let Some(ident) = o.key.as_expression()
&& let Some(ident) =
get_string_by_literal_expression(ident)
&& let Some(ident) = get_str_by_property_key(&o.key)
{
if ident == "url" {
url =
Expand Down
36 changes: 36 additions & 0 deletions libs/extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10854,6 +10854,42 @@ const className = myCss({ bg: "red" })
));
}

#[test]
#[serial]
fn test_global_css_layer_blocks_and_import_objects() {
reset_class_map();
reset_file_map();
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
r"import {globalCss} from '@devup-ui/react'
globalCss({
imports: [{ url: 'a.css', query: 'screen' }, { 'url': 'b.css' }],
'@layer utilities': { '.x': { color: 'blue' }, '.y': { color: 'red', _print: { color: 'black' } } },
'@layer base': { '@layer reset': { html: { m: 0 } } },
})",
ExtractOption::default(),
)
.unwrap()
));
for key in ["'@layer a, b'", "'@layer a b'"] {
reset_class_map();
reset_file_map();
let message = extract(
"test.tsx",
&format!("import {{globalCss}} from '@devup-ui/react'\nglobalCss({{ {key}: {{ '.x': {{ color: 'red' }} }} }})"),
ExtractOption::default(),
)
.err()
.map(|error| error.to_string())
.unwrap_or_default();
assert!(
message.contains("`globalCss()` cannot use"),
"{key}: {message}"
);
}
}

#[test]
#[serial]
fn test_global_css_with_imports() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
source: libs/extractor/src/lib.rs
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {globalCss} from '@devup-ui/react'\nglobalCss({\n imports: [{ url: 'a.css', query: 'screen' }, { 'url': 'b.css' }],\n '@layer utilities': { '.x': { color: 'blue' }, '.y': { color: 'red', _print: { color: 'black' } } },\n '@layer base': { '@layer reset': { html: { m: 0 } } },\n})\",\nExtractOption::default(),).unwrap())"
---
ToBTreeSet {
styles: {
Static(
ExtractStaticStyle {
property: "color",
value: "black",
level: 0,
selector: Some(
At {
kind: Media,
query: "print",
selector: Some(
".y",
),
outer: [],
file: Some(
"test.tsx",
),
},
),
style_order: Some(
0,
),
layer: Some(
"utilities",
),
},
),
Static(
ExtractStaticStyle {
property: "color",
value: "blue",
level: 0,
selector: Some(
Global(
".x",
"test.tsx",
),
),
style_order: Some(
0,
),
layer: Some(
"utilities",
),
},
),
Static(
ExtractStaticStyle {
property: "color",
value: "red",
level: 0,
selector: Some(
Global(
".y",
"test.tsx",
),
),
style_order: Some(
0,
),
layer: Some(
"utilities",
),
},
),
Static(
ExtractStaticStyle {
property: "margin",
value: "0",
level: 0,
selector: Some(
Global(
"html",
"test.tsx",
),
),
style_order: Some(
0,
),
layer: Some(
"base.reset",
),
},
),
Import(
ExtractImport {
url: "\"a.css\" screen",
file: "test.tsx",
},
),
Import(
ExtractImport {
url: "\"b.css\"",
file: "test.tsx",
},
),
},
code: "import \"@devup-ui/react/devup-ui-0.css\";\n;\n",
}
28 changes: 25 additions & 3 deletions libs/sheet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,9 +1066,7 @@ impl StyleSheet {
css.push('{');
open_selector = Some(selector);
}
css.push_str(&prop.property);
css.push(':');
css.push_str(&prop.value);
prop.write_declaration(css);
}
if open_selector.is_some() {
css.push('}');
Expand Down Expand Up @@ -3589,6 +3587,30 @@ mod tests {
}
}

#[test]
#[serial]
fn test_global_css_reads_theme_tokens() {
let mut sheet = StyleSheet::default();
let output = extract(
"global.tsx",
"import {globalCss} from '@devup-ui/core';globalCss({ body: { color: '$text', border: '1px solid $line.100' } })",
ExtractOption {
package: "@devup-ui/core".to_string(),
css_dir: "@devup-ui/core".to_string(),
single_css: true,
import_main_css: false,
import_aliases: std::collections::HashMap::new(),
},
)
.unwrap();
sheet.update_styles(&output.styles, "global.tsx", true);
let css = sheet.create_css(None, false);
assert!(
css.contains("body{border:1px solid var(--line-100);color:var(--text)}"),
"{css}"
);
}

#[test]
#[serial]
fn test_rm_global_css_drops_global_at_rules() {
Expand Down
Loading