Skip to content
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_tailwind_v4_theme.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Tailwind classes in a className follow Tailwind CSS v4: colors are the v4 default palette (oklch) and the colors and shadows of devup.json theme are usable as Tailwind theme values (bg-primary, text-text, shadow-card, as var(--token)); the opacity modifier (bg-red-500/50, /[71.37%], /(--alpha)) mixes with color-mix as v4 does; a trailing or leading ! makes every declaration important; blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia and their backdrop-* forms compose through Tailwind's custom properties instead of overwriting filter and backdrop-filter; gradients use bg-linear-*, bg-conic-* and bg-radial-* with from-*, via-* and to-* composing through the gradient variables (bg-gradient-to-* is not defined by v4 and stays as written); animate-* brings the @keyframes it names and transition, duration, delay and ease follow v4; space-* and divide-* apply to the children like v4 (:where(& > :not(:last-child))); @property rules are emitted only for the custom properties a class uses. Anything the build cannot reproduce exactly (drop-shadow-*) stays as written in the className. No new build errors",
"date": "2026-10-01T00:00:00.000Z"
}
42 changes: 41 additions & 1 deletion libs/css/src/theme_tokens.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{LazyLock, RwLock};

#[derive(Default, Debug)]
struct ThemeTokenRegistry {
length: BTreeMap<String, Vec<u8>>,
shadow: BTreeMap<String, Vec<u8>>,
typography: Vec<String>,
colors: BTreeSet<String>,
}

static TOKEN_REGISTRY: LazyLock<RwLock<ThemeTokenRegistry>> =
Expand All @@ -27,6 +28,28 @@ pub fn set_typography_keys(keys: Vec<String>) {
}
}

/// Registers the names of the theme's colors, which Tailwind classes such as
/// `bg-primary` can use
pub fn set_color_tokens(names: BTreeSet<String>) {
if let Ok(mut registry) = TOKEN_REGISTRY.write() {
registry.colors = names;
}
}

/// Whether the theme defines the color `name`, a CSS variable of that name
pub fn is_color_token(name: &str) -> bool {
TOKEN_REGISTRY
.read()
.is_ok_and(|registry| registry.colors.contains(name))
}

/// Whether the theme defines the shadow `name`, a CSS variable of that name
pub fn is_shadow_token(name: &str) -> bool {
TOKEN_REGISTRY
.read()
.is_ok_and(|registry| registry.shadow.contains_key(name))
}

/// Typography names defined by the registered theme, so a dynamic
/// `typography` value under a selector can resolve to one class per name.
pub fn get_typography_keys() -> Vec<String> {
Expand Down Expand Up @@ -122,6 +145,23 @@ mod tests {
assert_eq!(get_typography_keys(), Vec::<String>::new());
}

#[test]
#[serial]
fn test_color_and_shadow_tokens() {
set_color_tokens(BTreeSet::from(["primary".to_string()]));
let mut shadow = BTreeMap::new();
shadow.insert("card".to_string(), vec![0]);
set_theme_token_levels(BTreeMap::new(), shadow);

assert!(is_color_token("primary"));
assert!(!is_color_token("card"));
assert!(is_shadow_token("card"));
assert!(!is_shadow_token("primary"));
set_color_tokens(BTreeSet::new());
set_theme_token_levels(BTreeMap::new(), BTreeMap::new());
assert!(!is_color_token("primary"));
}

#[test]
fn test_is_responsive_theme_token_without_prefix() {
let value = std::hint::black_box("noprefix");
Expand Down
4 changes: 4 additions & 0 deletions libs/extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ mod source_map;
mod style_values;
mod stylex;
mod tailwind;
mod tailwind_children;
mod tailwind_color;
mod tailwind_effects;
mod tailwind_motion;
mod util_type;
mod utils;
mod vanilla_extract;
Expand Down
21 changes: 11 additions & 10 deletions libs/extractor/src/prop_modify_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::extract_style::extract_css::ExtractCss;
use crate::extract_style::style_property::StyleProperty;
use crate::gen_class_name::gen_class_names;
use crate::gen_style::gen_styles;
use crate::tailwind::{PROPERTY_RULES, PROPERTY_RULES_FILE, parse_class};
use crate::tailwind::{PROPERTY_RULES_FILE, parse_class};
use crate::utils::{get_str_by_property_key, merge_object_expressions};
use crate::{ExtractStyleProp, ExtractStyleValue};
use oxc_allocator::{CloneIn, FromIn, GetAllocator};
Expand All @@ -15,6 +15,7 @@ use oxc_ast::ast::{
};
use oxc_ast::builder::AstBuilder;
use oxc_span::SPAN;
use std::collections::BTreeSet;

/// Combine two optional className expressions into a conditional expression.
/// `condition ? con_expr : alt_expr`, falling back to `""` for the missing branch.
Expand Down Expand Up @@ -314,7 +315,7 @@ pub fn get_class_name_expression<'a>(
style_order,
filename,
styles: Vec::new(),
property_rules: false,
rules: BTreeSet::new(),
};
let compiled = class_name_prop
.as_ref()
Expand Down Expand Up @@ -364,19 +365,19 @@ struct TailwindClassName<'f> {
style_order: Option<u8>,
filename: Option<&'f str>,
styles: Vec<ExtractStyleValue>,
/// Whether a compiled class uses the custom properties Tailwind registers
property_rules: bool,
/// The global rules the compiled classes need, @property and @keyframes
rules: BTreeSet<&'static str>,
}

impl TailwindClassName<'_> {
fn into_styles(self) -> Vec<ExtractStyleValue> {
let mut styles = self.styles;
if self.property_rules {
styles.push(ExtractStyleValue::Css(ExtractCss {
css: PROPERTY_RULES.to_string(),
styles.extend(self.rules.into_iter().map(|rule| {
ExtractStyleValue::Css(ExtractCss {
css: rule.to_string(),
file: PROPERTY_RULES_FILE.to_string(),
}));
}
})
}));
styles
}

Expand All @@ -393,7 +394,7 @@ impl TailwindClassName<'_> {
let whole = (!open_start || start > 0) && (!open_end || end < text.len());
if let Some(tailwind) = whole.then(|| parse_class(class)).flatten() {
compiled.push_str(&text[written..start]);
self.property_rules |= tailwind.uses_properties();
self.rules.extend(tailwind.rules());
let mut separator = "";
for mut style in tailwind.styles() {
if let Some(order) = self.style_order {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ expression: sort_styles(styles)
Static(
ExtractStaticStyle {
property: "background-color",
value: "#EF4444",
value: "oklch(63.7% .237 25.331)",
level: 0,
selector: None,
style_order: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,46 @@ source: libs/extractor/src/tailwind.rs
expression: sort_styles(styles)
---
{
Static(
ExtractStaticStyle {
property: "--tw-duration",
value: "200ms",
level: 0,
selector: None,
style_order: None,
layer: None,
},
),
Static(
ExtractStaticStyle {
property: "--tw-shadow",
value: "0 10px 15px -3px var(--tw-shadow-color,rgb(0 0 0 / .1)),0 4px 6px -4px var(--tw-shadow-color,rgb(0 0 0 / .1))",
level: 0,
selector: Some(
At {
kind: Media,
query: "(hover:hover)",
selector: Some(
"&:hover",
),
outer: [],
file: None,
},
),
style_order: None,
layer: None,
},
),
Static(
ExtractStaticStyle {
property: "--tw-shadow",
value: "0 4px 6px -1px var(--tw-shadow-color,rgb(0 0 0 / .1)),0 2px 4px -2px var(--tw-shadow-color,rgb(0 0 0 / .1))",
level: 0,
selector: None,
style_order: None,
layer: None,
},
),
Static(
ExtractStaticStyle {
property: "align-items",
Expand Down Expand Up @@ -36,7 +76,17 @@ expression: sort_styles(styles)
Static(
ExtractStaticStyle {
property: "box-shadow",
value: "0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1)",
value: "var(--tw-inset-shadow,0 0 #0000),var(--tw-inset-ring-shadow,0 0 #0000),var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)",
level: 0,
selector: None,
style_order: None,
layer: None,
},
),
Static(
ExtractStaticStyle {
property: "box-shadow",
value: "var(--tw-inset-shadow,0 0 #0000),var(--tw-inset-ring-shadow,0 0 #0000),var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)",
level: 0,
selector: Some(
At {
Expand All @@ -55,8 +105,8 @@ expression: sort_styles(styles)
),
Static(
ExtractStaticStyle {
property: "box-shadow",
value: "0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1)",
property: "display",
value: "flex",
level: 0,
selector: None,
style_order: None,
Expand All @@ -65,8 +115,8 @@ expression: sort_styles(styles)
),
Static(
ExtractStaticStyle {
property: "display",
value: "flex",
property: "justify-content",
value: "space-between",
level: 0,
selector: None,
style_order: None,
Expand All @@ -75,8 +125,8 @@ expression: sort_styles(styles)
),
Static(
ExtractStaticStyle {
property: "justify-content",
value: "space-between",
property: "padding",
value: "1rem",
level: 0,
selector: None,
style_order: None,
Expand All @@ -85,8 +135,8 @@ expression: sort_styles(styles)
),
Static(
ExtractStaticStyle {
property: "padding",
value: "1rem",
property: "transition-duration",
value: "200ms",
level: 0,
selector: None,
style_order: None,
Expand All @@ -96,7 +146,7 @@ expression: sort_styles(styles)
Static(
ExtractStaticStyle {
property: "transition-duration",
value: "200ms",
value: "var(--tw-duration,150ms)",
level: 0,
selector: None,
style_order: None,
Expand All @@ -113,4 +163,14 @@ expression: sort_styles(styles)
layer: None,
},
),
Static(
ExtractStaticStyle {
property: "transition-timing-function",
value: "var(--tw-ease,cubic-bezier(.4,0,.2,1))",
level: 0,
selector: None,
style_order: None,
layer: None,
},
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ expression: sort_styles(styles)
Static(
ExtractStaticStyle {
property: "background-color",
value: "#3B82F6",
value: "oklch(62.3% .214 259.815)",
level: 1,
selector: None,
style_order: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ expression: sort_styles(styles)
Static(
ExtractStaticStyle {
property: "background-color",
value: "#3B82F6",
value: "oklch(62.3% .214 259.815)",
level: 0,
selector: Some(
At {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ToBTreeSet {
Static(
ExtractStaticStyle {
property: "background-color",
value: "#EF4444",
value: "oklch(63.7% .237 25.331)",
level: 0,
selector: None,
style_order: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ToBTreeSet {
Static(
ExtractStaticStyle {
property: "background-color",
value: "#EF4444",
value: "oklch(63.7% .237 25.331)",
level: 0,
selector: None,
style_order: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ToBTreeSet {
Static(
ExtractStaticStyle {
property: "background-color",
value: "#3B82F6",
value: "oklch(62.3% .214 259.815)",
level: 0,
selector: Some(
At {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ToBTreeSet {
Static(
ExtractStaticStyle {
property: "color",
value: "#EF4444",
value: "oklch(63.7% .237 25.331)",
level: 0,
selector: None,
style_order: None,
Expand Down
Loading
Loading