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_tailwind_classes.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: each class compiles on its own and every class Tailwind does not define (custom, plugin and analytics classes) stays as written, where any recognized class used to rebuild the whole className without them and could rewrite part of another class; a class touching a template interpolation (icon-${name}) is left as written, and template classNames no longer get spaces inserted around interpolations. A class whose variant is not understood stays as written instead of applying unconditionally; variants follow Tailwind v4: stacked variants compose (hover:focus: matches the element itself), hover is guarded by @media (hover: hover), group-*/peer-* use the group/peer marker classes (and group-hover/name), and data-*, aria-*, has-*, nth-*, *, **, supports-[…] and [&…]/[@media …] variants compile. Utilities emit all their declarations (size-*, rounded-t-*, truncate, sr-only, not-sr-only, text-sm with its line height, text-sm/6, leading-*), translate/rotate/scale/skew compose through Tailwind's individual properties and registered variables (@property) with negative values kept negative, and arbitrary values follow Tailwind's grammar: spaces for underscores except in url() and var() names, \\_ for an underscore, spaces around math operators, type hints, [property:value] and (--variable) shorthands. Words that only share a utility's prefix (select-wrapper, order-summary, cursor-foo) are no longer compiled",
"date": "2026-10-01T00:00:00.000Z"
}
18 changes: 9 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 86 additions & 0 deletions libs/extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14553,6 +14553,92 @@ const Button = styled.button({ bg: 'red' })
));
}

// Each Tailwind class becomes the classes of its styles; every other class,
// and every class that runs into an interpolation, stays as written
#[rstest]
#[case(
r#"<Box className="p-4 custom prose my-p-4-class" />"#,
r#"<div className="a custom prose my-p-4-class" />"#
)]
#[case(r#"<Box className="card hidden" />"#, r#"<div className="card a" />"#)]
#[case(
r#"<Box className="data-active:p-4 not-hover:m-4 [&>*]:p-4" />"#,
r#"<div className="a not-hover:m-4 b" />"#
)]
#[case(
"<Box className={`p-${size} mt-4 ${tone}-text`} />",
"<div className={`p-${size} a ${tone}-text`} />"
)]
#[case(
r"<Box className={`p-4 \`q\` \${y} a\\b\rc ${x}`} />",
r"<div className={`a \`q\` \${y} a\\b\rc ${x}`} />"
)]
#[case(
"<Box className={`custom ${on ? 'p-4' : x}`} />",
r#"<div className={`custom ${on ? "a" : x}`} />"#
)]
#[case(
r#"<Box className={on ? "p-4" : "custom"} />"#,
r#"<div className={(on ? "a" : "custom") || ""} />"#
)]
#[case(
r#"<Box className={on ? "custom" : "p-4"} />"#,
r#"<div className={(on ? "custom" : "a") || ""} />"#
)]
#[case(
r#"<Box className={on && "p-4"} />"#,
r#"<div className={on && "a" || ""} />"#
)]
#[case(
r#"<Box className={"p-4" || x} />"#,
r#"<div className={"a" || x || ""} />"#
)]
#[case(r#"<Box className={("p-4")} />"#, r#"<div className={"a" || ""} />"#)]
#[case(
"<Box className={`icon-${name} ${a}${b}`} />",
"-<div className={`icon-${name} ${a}${b}`} />"
)]
#[case(
r#"<Box className={("custom")} />"#,
r#"-<div className={"custom" || ""} />"#
)]
#[case(
r#"<Box className={on && "custom"} />"#,
r#"-<div className={on && "custom" || ""} />"#
)]
#[case(
r#"<Box className={on ? "a1" : "b1"} />"#,
r#"-<div className={(on ? "a1" : "b1") || ""} />"#
)]
#[case(
"<Box className={`custom ${x}`} />",
"-<div className={`custom ${x}`} />"
)]
#[case("<Box className={cls} />", "-<div className={cls || \"\"} />")]
#[serial]
fn test_tailwind_keeps_other_classes(#[case] jsx: &str, #[case] expected: &str) {
reset_class_map();
reset_file_map();
let output = extract(
"test.tsx",
&format!("import {{Box}} from '@devup-ui/core'\n{jsx}\n"),
ExtractOption {
package: "@devup-ui/core".to_string(),
css_dir: "@devup-ui/core".to_string(),
single_css: true,
import_main_css: false,
import_aliases: HashMap::new(),
},
)
.unwrap();
// A leading `-` marks code that needs no stylesheet
let expected = expected.strip_prefix('-').map_or_else(
|| format!("import \"@devup-ui/core/devup-ui.css\";\n{expected};\n"),
|code| format!("{code};\n"),
);
assert_eq!(output.code, expected);
}

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