diff --git a/.changepacks/changepack_log_TdRMiDSjcjQCHtCLj3xBB.json b/.changepacks/changepack_log_TdRMiDSjcjQCHtCLj3xBB.json
new file mode 100644
index 00000000..52a39566
--- /dev/null
+++ b/.changepacks/changepack_log_TdRMiDSjcjQCHtCLj3xBB.json
@@ -0,0 +1 @@
+{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Support tailwind","date":"2026-02-03T17:44:43.722386900Z"}
\ No newline at end of file
diff --git a/benchmark.js b/benchmark.js
index cbaebe8c..69512574 100644
--- a/benchmark.js
+++ b/benchmark.js
@@ -85,6 +85,16 @@ function clearBuildFile() {
recursive: true,
force: true,
})
+ if (existsSync('./benchmark/next-tailwind-turbo-devup-ui/.next'))
+ rmSync('./benchmark/next-tailwind-turbo-devup-ui/.next', {
+ recursive: true,
+ force: true,
+ })
+ if (existsSync('./benchmark/next-tailwind-turbo-devup-ui/df'))
+ rmSync('./benchmark/next-tailwind-turbo-devup-ui/df', {
+ recursive: true,
+ force: true,
+ })
}
function checkDirSize(path) {
@@ -135,5 +145,6 @@ result.push(benchmark('devup-ui-single'))
result.push(benchmark('tailwind-turbo'))
result.push(benchmark('devup-ui-single-turbo'))
result.push(benchmark('vanilla-extract-devup-ui'))
+result.push(benchmark('tailwind-turbo-devup-ui'))
console.info(result.join('\n'))
diff --git a/benchmark/next-tailwind-turbo-devup-ui/.gitignore b/benchmark/next-tailwind-turbo-devup-ui/.gitignore
new file mode 100644
index 00000000..5ef6a520
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/benchmark/next-tailwind-turbo-devup-ui/README.md b/benchmark/next-tailwind-turbo-devup-ui/README.md
new file mode 100644
index 00000000..665152ea
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/README.md
@@ -0,0 +1 @@
+## Nextjs App
diff --git a/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs b/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs
new file mode 100644
index 00000000..00a680c8
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs
@@ -0,0 +1,3 @@
+import { DevupUI } from '@devup-ui/next-plugin'
+
+export default DevupUI({})
diff --git a/benchmark/next-tailwind-turbo-devup-ui/package.json b/benchmark/next-tailwind-turbo-devup-ui/package.json
new file mode 100644
index 00000000..e1101087
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "next-tailwind-turbo-devup-ui-benchmark",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build --experimental-debug-memory-usage",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "next": "^16.1",
+ "react": "^19.2",
+ "react-dom": "^19.2",
+ "react-icons": "^5.5",
+ "@devup-ui/react": "workspace:^"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.1",
+ "postcss": "^8.5",
+ "@types/node": "^25",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "typescript": "^5",
+ "tailwindcss": "^4.1",
+ "@devup-ui/next-plugin": "workspace:^"
+ }
+}
diff --git a/benchmark/next-tailwind-turbo-devup-ui/public/file.svg b/benchmark/next-tailwind-turbo-devup-ui/public/file.svg
new file mode 100644
index 00000000..004145cd
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/next-tailwind-turbo-devup-ui/public/globe.svg b/benchmark/next-tailwind-turbo-devup-ui/public/globe.svg
new file mode 100644
index 00000000..567f17b0
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/next-tailwind-turbo-devup-ui/public/next.svg b/benchmark/next-tailwind-turbo-devup-ui/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/next-tailwind-turbo-devup-ui/public/vercel.svg b/benchmark/next-tailwind-turbo-devup-ui/public/vercel.svg
new file mode 100644
index 00000000..77053960
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/next-tailwind-turbo-devup-ui/public/window.svg b/benchmark/next-tailwind-turbo-devup-ui/public/window.svg
new file mode 100644
index 00000000..b2b2a44f
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/next-tailwind-turbo-devup-ui/src/app/favicon.ico b/benchmark/next-tailwind-turbo-devup-ui/src/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/benchmark/next-tailwind-turbo-devup-ui/src/app/favicon.ico differ
diff --git a/benchmark/next-tailwind-turbo-devup-ui/src/app/layout.tsx b/benchmark/next-tailwind-turbo-devup-ui/src/app/layout.tsx
new file mode 100644
index 00000000..6b8b4518
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/src/app/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/benchmark/next-tailwind-turbo-devup-ui/src/app/page.tsx b/benchmark/next-tailwind-turbo-devup-ui/src/app/page.tsx
new file mode 100644
index 00000000..b57a10a2
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/src/app/page.tsx
@@ -0,0 +1,43 @@
+'use client'
+
+import { Box, Button } from '@devup-ui/react'
+import { useState } from 'react'
+
+export default function HomePage() {
+ const [color, setColor] = useState('text-yellow-500')
+ const [enabled, setEnabled] = useState(false)
+
+ return (
+
+
+ Track & field champions:
+
+
+ hello
+ hello
+
+
+ text
+
+
+ hello
+
+
hello
+
+
+ )
+}
diff --git a/benchmark/next-tailwind-turbo-devup-ui/tsconfig.json b/benchmark/next-tailwind-turbo-devup-ui/tsconfig.json
new file mode 100644
index 00000000..a9788073
--- /dev/null
+++ b/benchmark/next-tailwind-turbo-devup-ui/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ "df/*.d.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/bun.lock b/bun.lock
index 9b37d873..f403f19c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -309,6 +309,27 @@
"typescript": "^5",
},
},
+ "benchmark/next-tailwind-turbo-devup-ui": {
+ "name": "next-tailwind-turbo-devup-ui-benchmark",
+ "version": "0.1.0",
+ "dependencies": {
+ "@devup-ui/react": "workspace:^",
+ "next": "^16.1",
+ "react": "^19.2",
+ "react-dom": "^19.2",
+ "react-icons": "^5.5",
+ },
+ "devDependencies": {
+ "@devup-ui/next-plugin": "workspace:^",
+ "@tailwindcss/postcss": "^4.1",
+ "@types/node": "^25",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "postcss": "^8.5",
+ "tailwindcss": "^4.1",
+ "typescript": "^5",
+ },
+ },
"benchmark/next-vanilla-extract": {
"name": "next-vanilla-extract-benchmark",
"version": "0.1.0",
@@ -2566,6 +2587,8 @@
"next-tailwind-turbo-benchmark": ["next-tailwind-turbo-benchmark@workspace:benchmark/next-tailwind-turbo"],
+ "next-tailwind-turbo-devup-ui-benchmark": ["next-tailwind-turbo-devup-ui-benchmark@workspace:benchmark/next-tailwind-turbo-devup-ui"],
+
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"next-vanilla-extract-benchmark": ["next-vanilla-extract-benchmark@workspace:benchmark/next-vanilla-extract"],
diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs
index 8caf6f3f..a27c6177 100644
--- a/libs/extractor/src/lib.rs
+++ b/libs/extractor/src/lib.rs
@@ -7,6 +7,7 @@ mod gen_class_name;
mod gen_style;
mod import_alias_visit;
mod prop_modify_utils;
+mod tailwind;
mod util_type;
mod utils;
mod vanilla_extract;
@@ -13084,4 +13085,416 @@ const Button = styled.button({ bg: 'red' })
.unwrap()
));
}
+
+ #[test]
+ #[serial]
+ fn test_tailwind_classname_extraction() {
+ // Test Tailwind className extraction - classes should be replaced with devup-ui generated names
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_classname_with_variants() {
+ // Test Tailwind with hover and responsive variants
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_classname_with_devup_props() {
+ // Test Tailwind className combined with devup-ui style props
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_arbitrary_values() {
+ // Test Tailwind arbitrary values like w-[100px]
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_negative_values() {
+ // Test Tailwind negative values like -m-4
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_full_integration() {
+ // Comprehensive integration test: realistic multi-component scenario
+ // Tests multiple Tailwind features together in a real-world usage pattern
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "components/Card.tsx",
+ r#"import {Box, Flex, Text} from '@devup-ui/core'
+
+// Card component with comprehensive Tailwind usage
+export const Card = () => (
+
+
+
+ A
+
+
+ Title
+ Subtitle
+
+
+
+
+ Content goes here with multiple Tailwind utilities combined.
+
+
+
+)
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_with_all_variant_types() {
+ // Test all variant types: responsive, state, dark mode
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_mixed_with_devup_responsive() {
+ // Test Tailwind className with devup-ui responsive array props
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_with_as_prop() {
+ // Test Tailwind className with "as" prop for polymorphic components
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+text
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_with_conditional_template_literal() {
+ // Test Tailwind className with conditional expression in template literal
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_logical_expression() {
+ // Test LogicalExpression in template literal (covers lines 239-242, 290-292 in prop_modify_utils.rs)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_parenthesized_expression() {
+ // Test ParenthesizedExpression in template literal (covers lines 244-246, 295-296 in prop_modify_utils.rs)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_nested_template_literal() {
+ // Test nested TemplateLiteral (covers lines 248, 299-302 in prop_modify_utils.rs)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_variable_expression() {
+ // Test non-string expressions (variables/identifiers) - covers line 250 in prop_modify_utils.rs
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_function_call_expression() {
+ // Test function call expressions in template literal (covers line 250 default branch)
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
+
+ #[test]
+ #[serial]
+ fn test_tailwind_template_literal_with_style_order() {
+ // Test style_order parameter in build_tailwind_class_mapping (covers line 178)
+ // styleOrder prop must be set explicitly to trigger the style_order code path
+ reset_class_map();
+ reset_file_map();
+ assert_debug_snapshot!(ToBTreeSet::from(
+ extract(
+ "test.tsx",
+ r#"import {Box} from '@devup-ui/core'
+
+ hello
+
+"#,
+ 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()
+ ));
+ }
}
diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs
index 932f9743..83b58442 100644
--- a/libs/extractor/src/prop_modify_utils.rs
+++ b/libs/extractor/src/prop_modify_utils.rs
@@ -1,7 +1,10 @@
-use crate::ExtractStyleProp;
+use crate::extract_style::ExtractStyleProperty;
+use crate::extract_style::style_property::StyleProperty;
use crate::gen_class_name::gen_class_names;
use crate::gen_style::gen_styles;
+use crate::tailwind::{has_tailwind_classes, parse_single_class, parse_tailwind_to_styles};
use crate::utils::{get_string_by_property_key, merge_object_expressions};
+use crate::{ExtractStyleProp, ExtractStyleValue};
use oxc_allocator::CloneIn;
use oxc_ast::AstBuilder;
use oxc_ast::ast::JSXAttributeName::Identifier;
@@ -10,8 +13,10 @@ use oxc_ast::ast::{
PropertyKey, PropertyKind, TemplateElementValue,
};
use oxc_span::SPAN;
+use std::collections::HashMap;
/// modify object props
+/// Returns extracted Tailwind styles from static className strings
pub fn modify_prop_object<'a>(
ast_builder: &AstBuilder<'a>,
props: &mut oxc_allocator::Vec>,
@@ -20,7 +25,7 @@ pub fn modify_prop_object<'a>(
style_vars: Option>,
props_prop: Option>,
filename: Option<&str>,
-) {
+) -> Vec {
let mut class_name_prop = None;
let mut style_prop = None;
let mut spread_props = vec![];
@@ -47,14 +52,15 @@ pub fn modify_prop_object<'a>(
}
}
- if let Some(ex) = get_class_name_expression(
+ let (class_name_expr, tailwind_styles) = get_class_name_expression(
ast_builder,
&class_name_prop,
styles,
style_order,
&spread_props,
filename,
- ) {
+ );
+ if let Some(ex) = class_name_expr {
props.push(ast_builder.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
@@ -89,8 +95,10 @@ pub fn modify_prop_object<'a>(
.object_property_kind_spread_property(SPAN, ex.clone_in(ast_builder.allocator)),
);
}
+ tailwind_styles
}
/// modify JSX props
+/// Returns extracted Tailwind styles from static className strings
pub fn modify_props<'a>(
ast_builder: &AstBuilder<'a>,
props: &mut oxc_allocator::Vec>,
@@ -99,7 +107,7 @@ pub fn modify_props<'a>(
style_vars: Option>,
props_prop: Option>,
filename: Option<&str>,
-) {
+) -> Vec {
let mut class_name_prop = None;
let mut style_prop = None;
let mut spread_props = vec![];
@@ -138,14 +146,15 @@ pub fn modify_props<'a>(
}
}
}
- if let Some(ex) = get_class_name_expression(
+ let (class_name_expr, tailwind_styles) = get_class_name_expression(
ast_builder,
&class_name_prop,
styles,
style_order,
&spread_props,
filename,
- ) {
+ );
+ if let Some(ex) = class_name_expr {
props.push(ast_builder.jsx_attribute_item_attribute(
SPAN,
ast_builder.jsx_attribute_name_identifier(SPAN, "className"),
@@ -178,8 +187,10 @@ pub fn modify_props<'a>(
),
);
}
+ tailwind_styles
}
+/// Returns (className expression, extracted Tailwind styles)
pub fn get_class_name_expression<'a>(
ast_builder: &AstBuilder<'a>,
class_name_prop: &Option>,
@@ -187,14 +198,29 @@ pub fn get_class_name_expression<'a>(
style_order: Option,
spread_props: &[Expression<'a>],
filename: Option<&str>,
-) -> Option> {
- // should modify class name prop
- merge_string_expressions(
+) -> (Option>, Vec) {
+ // Extract Tailwind styles from static className strings and generate class names
+ let (tailwind_styles, tailwind_class_expr) =
+ extract_tailwind_from_class_name(ast_builder, class_name_prop, style_order, filename);
+
+ // Determine the className expression to use:
+ // - If we extracted Tailwind styles, use generated class names (replace original)
+ // - Otherwise, preserve the original className
+ let class_name_to_use = if tailwind_class_expr.is_some() {
+ // Tailwind className → replaced with generated class names
+ tailwind_class_expr
+ } else {
+ // Non-Tailwind className → keep original
+ class_name_prop
+ .as_ref()
+ .map(|class_name| convert_class_name(ast_builder, class_name))
+ };
+
+ // Merge class names: [tailwind/original class names] + [devup-ui component styles]
+ let expression = merge_string_expressions(
ast_builder,
[
- class_name_prop
- .as_ref()
- .map(|class_name| convert_class_name(ast_builder, class_name)),
+ class_name_to_use,
gen_class_names(ast_builder, styles, style_order, filename),
]
.into_iter()
@@ -221,9 +247,254 @@ pub fn get_class_name_expression<'a>(
})
.collect::>()
.as_slice(),
+ );
+
+ (expression, tailwind_styles)
+}
+
+/// Apply style_order to all ExtractStyleValue items
+fn apply_style_order_to_styles(styles: &mut [ExtractStyleValue], style_order: Option) {
+ if let Some(order) = style_order {
+ for style in styles.iter_mut() {
+ style.set_style_order(order);
+ }
+ }
+}
+
+/// Extract Tailwind CSS styles from a static className string and generate devup-ui class names
+/// Returns (extracted styles for CSS generation, generated class names expression)
+fn extract_tailwind_from_class_name<'a>(
+ ast_builder: &AstBuilder<'a>,
+ class_name_prop: &Option>,
+ style_order: Option,
+ filename: Option<&str>,
+) -> (Vec, Option>) {
+ // Extract from static string literals
+ if let Some(Expression::StringLiteral(literal)) = class_name_prop {
+ let class_str = literal.value.as_str();
+ if has_tailwind_classes(class_str) {
+ let mut tailwind_styles = parse_tailwind_to_styles(class_str, filename);
+ if !tailwind_styles.is_empty() {
+ // Apply style_order to all extracted Tailwind styles
+ apply_style_order_to_styles(&mut tailwind_styles, style_order);
+
+ // Convert ExtractStyleValue to ExtractStyleProp::Static for gen_class_names
+ let mut tailwind_style_props: Vec = tailwind_styles
+ .iter()
+ .cloned()
+ .map(ExtractStyleProp::Static)
+ .collect();
+
+ // Generate devup-ui class names for the Tailwind styles
+ let class_names_expr = gen_class_names(
+ ast_builder,
+ &mut tailwind_style_props,
+ style_order,
+ filename,
+ );
+
+ return (tailwind_styles, class_names_expr);
+ }
+ }
+ }
+
+ // Extract from template literals (e.g., `${cond ? 'text-red' : 'text-blue'} p-4`)
+ if let Some(Expression::TemplateLiteral(template)) = class_name_prop {
+ let all_classes = extract_all_classes_from_template_literal(template);
+ if has_tailwind_classes(&all_classes) {
+ // Build mapping from Tailwind class → generated class name
+ let class_mapping = build_tailwind_class_mapping(&all_classes, style_order, filename);
+
+ if !class_mapping.is_empty() {
+ // Collect all styles for CSS generation
+ let mut tailwind_styles = parse_tailwind_to_styles(&all_classes, filename);
+ // Apply style_order to all extracted Tailwind styles
+ apply_style_order_to_styles(&mut tailwind_styles, style_order);
+
+ // Build new template literal with replaced class names
+ let new_template =
+ rebuild_template_literal_with_mapping(ast_builder, template, &class_mapping);
+
+ return (tailwind_styles, Some(new_template));
+ }
+ }
+ }
+
+ (Vec::new(), None)
+}
+
+/// Build a mapping from Tailwind class name to generated devup-ui class name
+fn build_tailwind_class_mapping(
+ class_str: &str,
+ style_order: Option,
+ filename: Option<&str>,
+) -> HashMap {
+ let mut mapping = HashMap::new();
+
+ for class in class_str.split_whitespace() {
+ if let Some(parsed) = parse_single_class(class) {
+ let mut static_style = parsed.to_static_style();
+ if let Some(order) = style_order {
+ static_style.style_order = Some(order);
+ }
+ // Extract to get the generated class name
+ if let StyleProperty::ClassName(generated) = static_style.extract(filename) {
+ mapping.insert(class.to_string(), generated);
+ }
+ }
+ }
+
+ mapping
+}
+
+/// Rebuild a template literal, replacing Tailwind classes with generated class names
+fn rebuild_template_literal_with_mapping<'a>(
+ ast_builder: &AstBuilder<'a>,
+ template: &oxc_ast::ast::TemplateLiteral<'a>,
+ class_mapping: &HashMap,
+) -> Expression<'a> {
+ // Rebuild quasis with replaced class names
+ let new_quasis = template.quasis.iter().map(|quasi| {
+ let raw = quasi.value.raw.as_str();
+ let replaced = replace_classes_in_string(raw, class_mapping);
+ let cooked = quasi.value.cooked.as_ref().map(|c| {
+ let replaced_cooked = replace_classes_in_string(c.as_str(), class_mapping);
+ ast_builder.atom(&replaced_cooked)
+ });
+ ast_builder.template_element(
+ quasi.span,
+ TemplateElementValue {
+ raw: ast_builder.atom(&replaced),
+ cooked,
+ },
+ quasi.tail,
+ false, // escape_raw
+ )
+ });
+
+ // Rebuild expressions with replaced class names
+ let new_expressions = template
+ .expressions
+ .iter()
+ .map(|expr| rebuild_expression_with_mapping(ast_builder, expr, class_mapping));
+
+ ast_builder.expression_template_literal(
+ template.span,
+ oxc_allocator::Vec::from_iter_in(new_quasis, ast_builder.allocator),
+ oxc_allocator::Vec::from_iter_in(new_expressions, ast_builder.allocator),
)
}
+/// Replace Tailwind class names in a string with generated class names
+fn replace_classes_in_string(s: &str, class_mapping: &HashMap) -> String {
+ let mut result = s.to_string();
+ // Sort by length descending to avoid partial replacements (e.g., "text-3xl" before "text-3")
+ let mut sorted_classes: Vec<_> = class_mapping.iter().collect();
+ sorted_classes.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
+
+ for (tailwind_class, generated_class) in sorted_classes {
+ result = result.replace(tailwind_class, generated_class);
+ }
+ result
+}
+
+/// Rebuild an expression, replacing Tailwind classes in string literals
+fn rebuild_expression_with_mapping<'a>(
+ ast_builder: &AstBuilder<'a>,
+ expr: &Expression<'a>,
+ class_mapping: &HashMap,
+) -> Expression<'a> {
+ match expr {
+ Expression::StringLiteral(lit) => {
+ let replaced = replace_classes_in_string(lit.value.as_str(), class_mapping);
+ ast_builder.expression_string_literal(SPAN, ast_builder.atom(&replaced), None)
+ }
+ Expression::ConditionalExpression(cond) => {
+ let consequent =
+ rebuild_expression_with_mapping(ast_builder, &cond.consequent, class_mapping);
+ let alternate =
+ rebuild_expression_with_mapping(ast_builder, &cond.alternate, class_mapping);
+ ast_builder.expression_conditional(
+ cond.span,
+ cond.test.clone_in(ast_builder.allocator),
+ consequent,
+ alternate,
+ )
+ }
+ Expression::LogicalExpression(logic) => {
+ let left = rebuild_expression_with_mapping(ast_builder, &logic.left, class_mapping);
+ let right = rebuild_expression_with_mapping(ast_builder, &logic.right, class_mapping);
+ ast_builder.expression_logical(logic.span, left, logic.operator, right)
+ }
+ Expression::ParenthesizedExpression(paren) => {
+ let inner =
+ rebuild_expression_with_mapping(ast_builder, &paren.expression, class_mapping);
+ ast_builder.expression_parenthesized(paren.span, inner)
+ }
+ Expression::TemplateLiteral(inner_template) => {
+ rebuild_template_literal_with_mapping(ast_builder, inner_template, class_mapping)
+ }
+ // For other expressions (variables, etc.), keep as-is
+ _ => expr.clone_in(ast_builder.allocator),
+ }
+}
+
+/// Extract all class name strings from a template literal, including from conditional expressions
+fn extract_all_classes_from_template_literal(template: &oxc_ast::ast::TemplateLiteral) -> String {
+ let mut classes = Vec::new();
+
+ // Extract from quasis (static parts of template literal)
+ for quasi in &template.quasis {
+ let raw = quasi.value.raw.as_str();
+ if !raw.trim().is_empty() {
+ classes.push(raw.trim().to_string());
+ }
+ }
+
+ // Extract from expressions (dynamic parts)
+ for expr in &template.expressions {
+ extract_classes_from_expression(expr, &mut classes);
+ }
+
+ classes.join(" ")
+}
+
+/// Recursively extract class name strings from an expression
+fn extract_classes_from_expression(expr: &Expression, classes: &mut Vec) {
+ match expr {
+ // Direct string literal: 'text-red-500'
+ Expression::StringLiteral(lit) => {
+ let value = lit.value.as_str().trim();
+ if !value.is_empty() {
+ classes.push(value.to_string());
+ }
+ }
+ // Ternary/conditional: cond ? 'text-red' : 'text-blue'
+ Expression::ConditionalExpression(cond) => {
+ extract_classes_from_expression(&cond.consequent, classes);
+ extract_classes_from_expression(&cond.alternate, classes);
+ }
+ // Logical OR: value || 'fallback'
+ Expression::LogicalExpression(logic) => {
+ extract_classes_from_expression(&logic.left, classes);
+ extract_classes_from_expression(&logic.right, classes);
+ }
+ // Parenthesized expression: (expr)
+ Expression::ParenthesizedExpression(paren) => {
+ extract_classes_from_expression(&paren.expression, classes);
+ }
+ // Template literal inside expression
+ Expression::TemplateLiteral(inner_template) => {
+ let inner_classes = extract_all_classes_from_template_literal(inner_template);
+ if !inner_classes.is_empty() {
+ classes.push(inner_classes);
+ }
+ }
+ // Other expressions (variables, function calls, etc.) - skip, can't extract statically
+ _ => {}
+ }
+}
+
pub fn get_style_expression<'a>(
ast_builder: &AstBuilder<'a>,
style_prop: &Option>,
diff --git a/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_basic.snap b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_basic.snap
new file mode 100644
index 00000000..8baa4082
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_basic.snap
@@ -0,0 +1,36 @@
+---
+source: libs/extractor/src/tailwind.rs
+expression: sort_styles(styles)
+---
+{
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+}
diff --git a/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_complex.snap b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_complex.snap
new file mode 100644
index 00000000..1d47576b
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_complex.snap
@@ -0,0 +1,110 @@
+---
+source: libs/extractor/src/tailwind.rs
+expression: sort_styles(styles)
+---
+{
+ Static(
+ ExtractStaticStyle {
+ property: "align-items",
+ value: "center",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#FFF",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-radius",
+ value: ".5rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 10px 15px -3px rgb(0 0 0 / .1),0 4px 6px -4px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "justify-content",
+ value: "space-between",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "transition-duration",
+ value: "200ms",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "transition-property",
+ value: "box-shadow",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+}
diff --git a/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_responsive.snap b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_responsive.snap
new file mode 100644
index 00000000..429e176b
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_responsive.snap
@@ -0,0 +1,36 @@
+---
+source: libs/extractor/src/tailwind.rs
+expression: sort_styles(styles)
+---
+{
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#3B82F6",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 3,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "2rem",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+}
diff --git a/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_variants.snap b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_variants.snap
new file mode 100644
index 00000000..4c4efe70
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tailwind__tests__parse_tailwind_to_styles_variants.snap
@@ -0,0 +1,48 @@
+---
+source: libs/extractor/src/tailwind.rs
+expression: sort_styles(styles)
+---
+{
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#3B82F6",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#FFF",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "outline",
+ value: "2px solid transparent",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:focus",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_arbitrary_values.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_arbitrary_values.snap
new file mode 100644
index 00000000..600bd6d9
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_arbitrary_values.snap
@@ -0,0 +1,39 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#F00",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "height",
+ value: "50vh",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "100px",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_extraction.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_extraction.snap
new file mode 100644
index 00000000..405c547e
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_extraction.snap
@@ -0,0 +1,29 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_devup_props.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_devup_props.snap
new file mode 100644
index 00000000..419ad0a7
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_devup_props.snap
@@ -0,0 +1,39 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "margin",
+ value: "8px",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_variants.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_variants.snap
new file mode 100644
index 00000000..e55ce0f5
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_classname_with_variants.snap
@@ -0,0 +1,43 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#3B82F6",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "2rem",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_full_integration.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_full_integration.snap
new file mode 100644
index 00000000..0c7d9440
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_full_integration.snap
@@ -0,0 +1,415 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"components/Card.tsx\",\nr#\"import {Box, Flex, Text} from '@devup-ui/core'\n\n// Card component with comprehensive Tailwind usage\nexport const Card = () => (\n \n \n \n A\n \n \n Title\n Subtitle\n \n \n \n \n Content goes here with multiple Tailwind utilities combined.\n \n \n \n)\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "align-items",
+ value: "center",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "align-items",
+ value: "center",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#1F2937",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#FFF",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-color",
+ value: "#374151",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-color",
+ value: "#E5E7EB",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-radius",
+ value: ".5rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-radius",
+ value: "9999px",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 20px 25px -5px rgb(0 0 0 / .1),0 8px 10px -6px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#111827",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#374151",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#6B7280",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#9CA3AF",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#D1D5DB",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#FFF",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#FFF",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 0,
+ selector: None,
+ style_order: Some(
+ 0,
+ ),
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "flex",
+ value: "1 1 0",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "flex-direction",
+ value: "column",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "flex-direction",
+ value: "row",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-size",
+ value: ".875rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-size",
+ value: "1.125rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-size",
+ value: "1.25rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-weight",
+ value: "600",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-weight",
+ value: "700",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "gap",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "height",
+ value: "3rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "justify-content",
+ value: "center",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "line-height",
+ value: "1.625",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "margin-top",
+ value: ".25rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "margin-top",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1.5rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding-top",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "transition-property",
+ value: "box-shadow",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "width",
+ value: "3rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n// Card component with comprehensive Tailwind usage\nexport const Card = () => \n
\n
\n A\n
\n
\n Title\n Subtitle\n
\n
\n
\n \n Content goes here with multiple Tailwind utilities combined.\n \n
\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_mixed_with_devup_responsive.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_mixed_with_devup_responsive.snap
new file mode 100644
index 00000000..ed64853b
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_mixed_with_devup_responsive.snap
@@ -0,0 +1,123 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "background",
+ value: "blue",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background",
+ value: "green",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background",
+ value: "red",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "border-radius",
+ value: ".5rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 20px 25px -5px rgb(0 0 0 / .1),0 8px 10px -6px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 4px 6px -1px rgb(0 0 0 / .1),0 2px 4px -2px rgb(0 0 0 / .1)",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "block",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "display",
+ value: "flex",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "16px",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "24px",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "8px",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_negative_values.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_negative_values.snap
new file mode 100644
index 00000000..6fbafc75
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_negative_values.snap
@@ -0,0 +1,29 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "margin",
+ value: "-1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "transform",
+ value: "translateX(-50%)",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_function_call_expression.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_function_call_expression.snap
new file mode 100644
index 00000000..20f35865
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_function_call_expression.snap
@@ -0,0 +1,29 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_logical_expression.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_logical_expression.snap
new file mode 100644
index 00000000..3d4bcb1d
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_logical_expression.snap
@@ -0,0 +1,39 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_nested_template_literal.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_nested_template_literal.snap
new file mode 100644
index 00000000..fbf19c74
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_nested_template_literal.snap
@@ -0,0 +1,19 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_parenthesized_expression.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_parenthesized_expression.snap
new file mode 100644
index 00000000..65171e2b
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_parenthesized_expression.snap
@@ -0,0 +1,39 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_style_order.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_style_order.snap
new file mode 100644
index 00000000..96adc8f4
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_style_order.snap
@@ -0,0 +1,45 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#22C55E",
+ level: 0,
+ selector: None,
+ style_order: Some(
+ 5,
+ ),
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: Some(
+ 5,
+ ),
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: Some(
+ 5,
+ ),
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_variable_expression.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_variable_expression.snap
new file mode 100644
index 00000000..96f1eff0
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_template_literal_with_variable_expression.snap
@@ -0,0 +1,29 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#EF4444",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_with_all_variant_types.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_all_variant_types.snap
new file mode 100644
index 00000000..b116c3f9
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_all_variant_types.snap
@@ -0,0 +1,153 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#111827",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark] &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#1F2937",
+ level: 0,
+ selector: Some(
+ Selector(
+ ":root[data-theme=dark]:hover &",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "background-color",
+ value: "#3B82F6",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:hover",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "box-shadow",
+ value: "0 0 0 2px var(--tw-ring-color)",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:focus",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "opacity",
+ value: ".5",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:disabled",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: ".5rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1.5rem",
+ level: 2,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "1rem",
+ level: 1,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "2.5rem",
+ level: 4,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "2rem",
+ level: 3,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding",
+ value: "3rem",
+ level: 5,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "transform",
+ value: "scale(.95)",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:active",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_with_as_prop.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_as_prop.snap
new file mode 100644
index 00000000..20c32115
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_as_prop.snap
@@ -0,0 +1,19 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\ntext\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#111827",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\ntext
;\n",
+}
diff --git a/libs/extractor/src/snapshots/extractor__tests__tailwind_with_conditional_template_literal.snap b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_conditional_template_literal.snap
new file mode 100644
index 00000000..ff2b5b95
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__tailwind_with_conditional_template_literal.snap
@@ -0,0 +1,49 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\n\n hello\n\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
+---
+ToBTreeSet {
+ styles: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#22C55E",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "#3B82F6",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "font-size",
+ value: "1.875rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ Static(
+ ExtractStaticStyle {
+ property: "padding-right",
+ value: "1.25rem",
+ level: 0,
+ selector: None,
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n\n hello\n
;\n",
+}
diff --git a/libs/extractor/src/tailwind.rs b/libs/extractor/src/tailwind.rs
new file mode 100644
index 00000000..72b72a50
--- /dev/null
+++ b/libs/extractor/src/tailwind.rs
@@ -0,0 +1,5970 @@
+//! Tailwind CSS class parser for devup-ui extraction
+//!
+//! This module parses Tailwind CSS class strings and converts them to
+//! ExtractStyleValue objects for integration with the devup-ui extraction system.
+
+// The nested if-let pattern is intentional for readability in parsing code.
+// Using if-let chains would make the code harder to read and modify.
+#![allow(clippy::collapsible_if)]
+
+use css::style_selector::StyleSelector;
+use phf::phf_map;
+
+use crate::extract_style::{
+ extract_static_style::ExtractStaticStyle, extract_style_value::ExtractStyleValue,
+};
+
+/// Responsive breakpoint levels matching devup-ui convention
+/// 0 = base (no prefix)
+/// 1 = sm (640px)
+/// 2 = md (768px)
+/// 3 = lg (1024px)
+/// 4 = xl (1280px)
+/// 5 = 2xl (1536px)
+///
+/// Map of responsive prefix to level
+static RESPONSIVE_PREFIX_MAP: phf::Map<&'static str, u8> = phf_map! {
+ "sm" => 1,
+ "md" => 2,
+ "lg" => 3,
+ "xl" => 4,
+ "2xl" => 5,
+};
+
+/// Variant prefixes that map to CSS pseudo-classes/selectors
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum TailwindVariant {
+ Hover,
+ Focus,
+ FocusVisible,
+ FocusWithin,
+ Active,
+ Visited,
+ Disabled,
+ Enabled,
+ Checked,
+ Indeterminate,
+ Default,
+ Required,
+ Valid,
+ Invalid,
+ InRange,
+ OutOfRange,
+ PlaceholderShown,
+ Autofill,
+ ReadOnly,
+ FirstChild,
+ LastChild,
+ OnlyChild,
+ OddChild,
+ EvenChild,
+ FirstOfType,
+ LastOfType,
+ OnlyOfType,
+ Empty,
+ Target,
+ Open,
+ Dark,
+ Placeholder,
+ Before,
+ After,
+ Selection,
+ Marker,
+ FirstLetter,
+ FirstLine,
+ Backdrop,
+ File,
+ GroupHover,
+ GroupFocus,
+ GroupActive,
+ GroupDisabled,
+ PeerHover,
+ PeerFocus,
+ PeerActive,
+ PeerDisabled,
+ PeerChecked,
+ PeerInvalid,
+ Print,
+ Screen,
+ Portrait,
+ Landscape,
+ MotionReduce,
+ MotionSafe,
+ ContrastMore,
+ ContrastLess,
+ ForcedColors,
+ Rtl,
+ Ltr,
+}
+
+impl TailwindVariant {
+ /// Convert variant to StyleSelector
+ pub fn to_selector(self) -> StyleSelector {
+ match self {
+ TailwindVariant::Hover => StyleSelector::Selector("&:hover".to_string()),
+ TailwindVariant::Focus => StyleSelector::Selector("&:focus".to_string()),
+ TailwindVariant::FocusVisible => StyleSelector::Selector("&:focus-visible".to_string()),
+ TailwindVariant::FocusWithin => StyleSelector::Selector("&:focus-within".to_string()),
+ TailwindVariant::Active => StyleSelector::Selector("&:active".to_string()),
+ TailwindVariant::Visited => StyleSelector::Selector("&:visited".to_string()),
+ TailwindVariant::Disabled => StyleSelector::Selector("&:disabled".to_string()),
+ TailwindVariant::Enabled => StyleSelector::Selector("&:enabled".to_string()),
+ TailwindVariant::Checked => StyleSelector::Selector("&:checked".to_string()),
+ TailwindVariant::Indeterminate => {
+ StyleSelector::Selector("&:indeterminate".to_string())
+ }
+ TailwindVariant::Default => StyleSelector::Selector("&:default".to_string()),
+ TailwindVariant::Required => StyleSelector::Selector("&:required".to_string()),
+ TailwindVariant::Valid => StyleSelector::Selector("&:valid".to_string()),
+ TailwindVariant::Invalid => StyleSelector::Selector("&:invalid".to_string()),
+ TailwindVariant::InRange => StyleSelector::Selector("&:in-range".to_string()),
+ TailwindVariant::OutOfRange => StyleSelector::Selector("&:out-of-range".to_string()),
+ TailwindVariant::PlaceholderShown => {
+ StyleSelector::Selector("&:placeholder-shown".to_string())
+ }
+ TailwindVariant::Autofill => StyleSelector::Selector("&:autofill".to_string()),
+ TailwindVariant::ReadOnly => StyleSelector::Selector("&:read-only".to_string()),
+ TailwindVariant::FirstChild => StyleSelector::Selector("&:first-child".to_string()),
+ TailwindVariant::LastChild => StyleSelector::Selector("&:last-child".to_string()),
+ TailwindVariant::OnlyChild => StyleSelector::Selector("&:only-child".to_string()),
+ TailwindVariant::OddChild => StyleSelector::Selector("&:nth-child(odd)".to_string()),
+ TailwindVariant::EvenChild => StyleSelector::Selector("&:nth-child(even)".to_string()),
+ TailwindVariant::FirstOfType => StyleSelector::Selector("&:first-of-type".to_string()),
+ TailwindVariant::LastOfType => StyleSelector::Selector("&:last-of-type".to_string()),
+ TailwindVariant::OnlyOfType => StyleSelector::Selector("&:only-of-type".to_string()),
+ TailwindVariant::Empty => StyleSelector::Selector("&:empty".to_string()),
+ TailwindVariant::Target => StyleSelector::Selector("&:target".to_string()),
+ TailwindVariant::Open => StyleSelector::Selector("&[open]".to_string()),
+ TailwindVariant::Dark => {
+ StyleSelector::Selector(":root[data-theme=dark] &".to_string())
+ }
+ TailwindVariant::Placeholder => StyleSelector::Selector("&::placeholder".to_string()),
+ TailwindVariant::Before => StyleSelector::Selector("&::before".to_string()),
+ TailwindVariant::After => StyleSelector::Selector("&::after".to_string()),
+ TailwindVariant::Selection => StyleSelector::Selector("&::selection".to_string()),
+ TailwindVariant::Marker => StyleSelector::Selector("&::marker".to_string()),
+ TailwindVariant::FirstLetter => StyleSelector::Selector("&::first-letter".to_string()),
+ TailwindVariant::FirstLine => StyleSelector::Selector("&::first-line".to_string()),
+ TailwindVariant::Backdrop => StyleSelector::Selector("&::backdrop".to_string()),
+ TailwindVariant::File => StyleSelector::Selector("&::file-selector-button".to_string()),
+ TailwindVariant::GroupHover => {
+ StyleSelector::Selector("*[role=group]:hover &".to_string())
+ }
+ TailwindVariant::GroupFocus => {
+ StyleSelector::Selector("*[role=group]:focus &".to_string())
+ }
+ TailwindVariant::GroupActive => {
+ StyleSelector::Selector("*[role=group]:active &".to_string())
+ }
+ TailwindVariant::GroupDisabled => {
+ StyleSelector::Selector("*[role=group]:disabled &".to_string())
+ }
+ TailwindVariant::PeerHover => StyleSelector::Selector(".peer:hover ~ &".to_string()),
+ TailwindVariant::PeerFocus => StyleSelector::Selector(".peer:focus ~ &".to_string()),
+ TailwindVariant::PeerActive => StyleSelector::Selector(".peer:active ~ &".to_string()),
+ TailwindVariant::PeerDisabled => {
+ StyleSelector::Selector(".peer:disabled ~ &".to_string())
+ }
+ TailwindVariant::PeerChecked => {
+ StyleSelector::Selector(".peer:checked ~ &".to_string())
+ }
+ TailwindVariant::PeerInvalid => {
+ StyleSelector::Selector(".peer:invalid ~ &".to_string())
+ }
+ TailwindVariant::Print => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "print".to_string(),
+ selector: None,
+ },
+ TailwindVariant::Screen => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "screen".to_string(),
+ selector: None,
+ },
+ TailwindVariant::Portrait => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(orientation: portrait)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::Landscape => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(orientation: landscape)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::MotionReduce => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(prefers-reduced-motion: reduce)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::MotionSafe => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(prefers-reduced-motion: no-preference)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::ContrastMore => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(prefers-contrast: more)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::ContrastLess => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(prefers-contrast: less)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::ForcedColors => StyleSelector::At {
+ kind: css::style_selector::AtRuleKind::Media,
+ query: "(forced-colors: active)".to_string(),
+ selector: None,
+ },
+ TailwindVariant::Rtl => StyleSelector::Selector("[dir=rtl] &".to_string()),
+ TailwindVariant::Ltr => StyleSelector::Selector("[dir=ltr] &".to_string()),
+ }
+ }
+
+ /// Parse variant from string prefix
+ pub fn from_prefix(prefix: &str) -> Option {
+ match prefix {
+ "hover" => Some(TailwindVariant::Hover),
+ "focus" => Some(TailwindVariant::Focus),
+ "focus-visible" => Some(TailwindVariant::FocusVisible),
+ "focus-within" => Some(TailwindVariant::FocusWithin),
+ "active" => Some(TailwindVariant::Active),
+ "visited" => Some(TailwindVariant::Visited),
+ "disabled" => Some(TailwindVariant::Disabled),
+ "enabled" => Some(TailwindVariant::Enabled),
+ "checked" => Some(TailwindVariant::Checked),
+ "indeterminate" => Some(TailwindVariant::Indeterminate),
+ "default" => Some(TailwindVariant::Default),
+ "required" => Some(TailwindVariant::Required),
+ "valid" => Some(TailwindVariant::Valid),
+ "invalid" => Some(TailwindVariant::Invalid),
+ "in-range" => Some(TailwindVariant::InRange),
+ "out-of-range" => Some(TailwindVariant::OutOfRange),
+ "placeholder-shown" => Some(TailwindVariant::PlaceholderShown),
+ "autofill" => Some(TailwindVariant::Autofill),
+ "read-only" => Some(TailwindVariant::ReadOnly),
+ "first" => Some(TailwindVariant::FirstChild),
+ "last" => Some(TailwindVariant::LastChild),
+ "only" => Some(TailwindVariant::OnlyChild),
+ "odd" => Some(TailwindVariant::OddChild),
+ "even" => Some(TailwindVariant::EvenChild),
+ "first-of-type" => Some(TailwindVariant::FirstOfType),
+ "last-of-type" => Some(TailwindVariant::LastOfType),
+ "only-of-type" => Some(TailwindVariant::OnlyOfType),
+ "empty" => Some(TailwindVariant::Empty),
+ "target" => Some(TailwindVariant::Target),
+ "open" => Some(TailwindVariant::Open),
+ "dark" => Some(TailwindVariant::Dark),
+ "placeholder" => Some(TailwindVariant::Placeholder),
+ "before" => Some(TailwindVariant::Before),
+ "after" => Some(TailwindVariant::After),
+ "selection" => Some(TailwindVariant::Selection),
+ "marker" => Some(TailwindVariant::Marker),
+ "first-letter" => Some(TailwindVariant::FirstLetter),
+ "first-line" => Some(TailwindVariant::FirstLine),
+ "backdrop" => Some(TailwindVariant::Backdrop),
+ "file" => Some(TailwindVariant::File),
+ "group-hover" => Some(TailwindVariant::GroupHover),
+ "group-focus" => Some(TailwindVariant::GroupFocus),
+ "group-active" => Some(TailwindVariant::GroupActive),
+ "group-disabled" => Some(TailwindVariant::GroupDisabled),
+ "peer-hover" => Some(TailwindVariant::PeerHover),
+ "peer-focus" => Some(TailwindVariant::PeerFocus),
+ "peer-active" => Some(TailwindVariant::PeerActive),
+ "peer-disabled" => Some(TailwindVariant::PeerDisabled),
+ "peer-checked" => Some(TailwindVariant::PeerChecked),
+ "peer-invalid" => Some(TailwindVariant::PeerInvalid),
+ "print" => Some(TailwindVariant::Print),
+ "screen" => Some(TailwindVariant::Screen),
+ "portrait" => Some(TailwindVariant::Portrait),
+ "landscape" => Some(TailwindVariant::Landscape),
+ "motion-reduce" => Some(TailwindVariant::MotionReduce),
+ "motion-safe" => Some(TailwindVariant::MotionSafe),
+ "contrast-more" => Some(TailwindVariant::ContrastMore),
+ "contrast-less" => Some(TailwindVariant::ContrastLess),
+ "forced-colors" => Some(TailwindVariant::ForcedColors),
+ "rtl" => Some(TailwindVariant::Rtl),
+ "ltr" => Some(TailwindVariant::Ltr),
+ _ => None,
+ }
+ }
+}
+
+/// Parsed Tailwind class with all components
+#[derive(Debug, Clone, PartialEq)]
+pub struct TailwindClass {
+ /// Responsive level (0=base, 1=sm, 2=md, 3=lg, 4=xl, 5=2xl)
+ pub responsive: u8,
+ /// Variants/modifiers applied
+ pub variants: Vec,
+ /// CSS property name
+ pub property: String,
+ /// CSS value
+ pub value: String,
+ /// Whether this is a negative value
+ pub negative: bool,
+}
+
+impl TailwindClass {
+ /// Convert to ExtractStaticStyle
+ pub fn to_static_style(&self) -> ExtractStaticStyle {
+ // For transform property, negative is already incorporated into the value
+ // (e.g., translateX(-1rem)), so don't add prefix again
+ let value = if self.negative && self.property != "transform" {
+ format!("-{}", self.value)
+ } else {
+ self.value.clone()
+ };
+
+ let selector = if self.variants.is_empty() {
+ None
+ } else {
+ // Combine multiple variants into a single selector
+ Some(self.combine_selectors())
+ };
+
+ ExtractStaticStyle::new(&self.property, &value, self.responsive, selector)
+ }
+
+ /// Combine multiple variant selectors
+ fn combine_selectors(&self) -> StyleSelector {
+ if self.variants.len() == 1 {
+ return self.variants[0].to_selector();
+ }
+
+ // For multiple variants, combine them
+ // e.g., dark:hover: becomes :root[data-theme=dark] &:hover
+ let mut selector_str = String::new();
+ let mut has_at_rule = None;
+
+ for variant in &self.variants {
+ let sel = variant.to_selector();
+ match sel {
+ StyleSelector::Selector(s) => {
+ if selector_str.is_empty() {
+ selector_str = s;
+ } else {
+ // Combine selectors
+ selector_str =
+ format!("{}{}", selector_str.replace(" &", ""), s.replace("&", ""));
+ if !selector_str.contains(" &") && !selector_str.ends_with(" &") {
+ selector_str.push_str(" &");
+ }
+ }
+ }
+ StyleSelector::At { kind, query, .. } => {
+ has_at_rule = Some((kind, query));
+ }
+ // SAFETY: TailwindVariant::to_selector() never produces Global.
+ // This arm exists only for exhaustive matching. If reached, it indicates
+ // a bug where a new TailwindVariant was added that produces Global.
+ StyleSelector::Global(_, _) => {
+ unreachable!("TailwindVariant should not produce Global selector")
+ }
+ }
+ }
+
+ if let Some((kind, query)) = has_at_rule {
+ StyleSelector::At {
+ kind,
+ query,
+ selector: if selector_str.is_empty() {
+ None
+ } else {
+ Some(selector_str)
+ },
+ }
+ } else {
+ StyleSelector::Selector(selector_str)
+ }
+ }
+}
+
+/// Tailwind color values
+static TAILWIND_COLORS: phf::Map<&'static str, &'static str> = phf_map! {
+ // Inherit/Current/Transparent
+ "inherit" => "inherit",
+ "current" => "currentColor",
+ "transparent" => "transparent",
+ // Black and White
+ "black" => "#000",
+ "white" => "#fff",
+ // Slate
+ "slate-50" => "#f8fafc",
+ "slate-100" => "#f1f5f9",
+ "slate-200" => "#e2e8f0",
+ "slate-300" => "#cbd5e1",
+ "slate-400" => "#94a3b8",
+ "slate-500" => "#64748b",
+ "slate-600" => "#475569",
+ "slate-700" => "#334155",
+ "slate-800" => "#1e293b",
+ "slate-900" => "#0f172a",
+ "slate-950" => "#020617",
+ // Gray
+ "gray-50" => "#f9fafb",
+ "gray-100" => "#f3f4f6",
+ "gray-200" => "#e5e7eb",
+ "gray-300" => "#d1d5db",
+ "gray-400" => "#9ca3af",
+ "gray-500" => "#6b7280",
+ "gray-600" => "#4b5563",
+ "gray-700" => "#374151",
+ "gray-800" => "#1f2937",
+ "gray-900" => "#111827",
+ "gray-950" => "#030712",
+ // Zinc
+ "zinc-50" => "#fafafa",
+ "zinc-100" => "#f4f4f5",
+ "zinc-200" => "#e4e4e7",
+ "zinc-300" => "#d4d4d8",
+ "zinc-400" => "#a1a1aa",
+ "zinc-500" => "#71717a",
+ "zinc-600" => "#52525b",
+ "zinc-700" => "#3f3f46",
+ "zinc-800" => "#27272a",
+ "zinc-900" => "#18181b",
+ "zinc-950" => "#09090b",
+ // Neutral
+ "neutral-50" => "#fafafa",
+ "neutral-100" => "#f5f5f5",
+ "neutral-200" => "#e5e5e5",
+ "neutral-300" => "#d4d4d4",
+ "neutral-400" => "#a3a3a3",
+ "neutral-500" => "#737373",
+ "neutral-600" => "#525252",
+ "neutral-700" => "#404040",
+ "neutral-800" => "#262626",
+ "neutral-900" => "#171717",
+ "neutral-950" => "#0a0a0a",
+ // Stone
+ "stone-50" => "#fafaf9",
+ "stone-100" => "#f5f5f4",
+ "stone-200" => "#e7e5e4",
+ "stone-300" => "#d6d3d1",
+ "stone-400" => "#a8a29e",
+ "stone-500" => "#78716c",
+ "stone-600" => "#57534e",
+ "stone-700" => "#44403c",
+ "stone-800" => "#292524",
+ "stone-900" => "#1c1917",
+ "stone-950" => "#0c0a09",
+ // Red
+ "red-50" => "#fef2f2",
+ "red-100" => "#fee2e2",
+ "red-200" => "#fecaca",
+ "red-300" => "#fca5a5",
+ "red-400" => "#f87171",
+ "red-500" => "#ef4444",
+ "red-600" => "#dc2626",
+ "red-700" => "#b91c1c",
+ "red-800" => "#991b1b",
+ "red-900" => "#7f1d1d",
+ "red-950" => "#450a0a",
+ // Orange
+ "orange-50" => "#fff7ed",
+ "orange-100" => "#ffedd5",
+ "orange-200" => "#fed7aa",
+ "orange-300" => "#fdba74",
+ "orange-400" => "#fb923c",
+ "orange-500" => "#f97316",
+ "orange-600" => "#ea580c",
+ "orange-700" => "#c2410c",
+ "orange-800" => "#9a3412",
+ "orange-900" => "#7c2d12",
+ "orange-950" => "#431407",
+ // Amber
+ "amber-50" => "#fffbeb",
+ "amber-100" => "#fef3c7",
+ "amber-200" => "#fde68a",
+ "amber-300" => "#fcd34d",
+ "amber-400" => "#fbbf24",
+ "amber-500" => "#f59e0b",
+ "amber-600" => "#d97706",
+ "amber-700" => "#b45309",
+ "amber-800" => "#92400e",
+ "amber-900" => "#78350f",
+ "amber-950" => "#451a03",
+ // Yellow
+ "yellow-50" => "#fefce8",
+ "yellow-100" => "#fef9c3",
+ "yellow-200" => "#fef08a",
+ "yellow-300" => "#fde047",
+ "yellow-400" => "#facc15",
+ "yellow-500" => "#eab308",
+ "yellow-600" => "#ca8a04",
+ "yellow-700" => "#a16207",
+ "yellow-800" => "#854d0e",
+ "yellow-900" => "#713f12",
+ "yellow-950" => "#422006",
+ // Lime
+ "lime-50" => "#f7fee7",
+ "lime-100" => "#ecfccb",
+ "lime-200" => "#d9f99d",
+ "lime-300" => "#bef264",
+ "lime-400" => "#a3e635",
+ "lime-500" => "#84cc16",
+ "lime-600" => "#65a30d",
+ "lime-700" => "#4d7c0f",
+ "lime-800" => "#3f6212",
+ "lime-900" => "#365314",
+ "lime-950" => "#1a2e05",
+ // Green
+ "green-50" => "#f0fdf4",
+ "green-100" => "#dcfce7",
+ "green-200" => "#bbf7d0",
+ "green-300" => "#86efac",
+ "green-400" => "#4ade80",
+ "green-500" => "#22c55e",
+ "green-600" => "#16a34a",
+ "green-700" => "#15803d",
+ "green-800" => "#166534",
+ "green-900" => "#14532d",
+ "green-950" => "#052e16",
+ // Emerald
+ "emerald-50" => "#ecfdf5",
+ "emerald-100" => "#d1fae5",
+ "emerald-200" => "#a7f3d0",
+ "emerald-300" => "#6ee7b7",
+ "emerald-400" => "#34d399",
+ "emerald-500" => "#10b981",
+ "emerald-600" => "#059669",
+ "emerald-700" => "#047857",
+ "emerald-800" => "#065f46",
+ "emerald-900" => "#064e3b",
+ "emerald-950" => "#022c22",
+ // Teal
+ "teal-50" => "#f0fdfa",
+ "teal-100" => "#ccfbf1",
+ "teal-200" => "#99f6e4",
+ "teal-300" => "#5eead4",
+ "teal-400" => "#2dd4bf",
+ "teal-500" => "#14b8a6",
+ "teal-600" => "#0d9488",
+ "teal-700" => "#0f766e",
+ "teal-800" => "#115e59",
+ "teal-900" => "#134e4a",
+ "teal-950" => "#042f2e",
+ // Cyan
+ "cyan-50" => "#ecfeff",
+ "cyan-100" => "#cffafe",
+ "cyan-200" => "#a5f3fc",
+ "cyan-300" => "#67e8f9",
+ "cyan-400" => "#22d3ee",
+ "cyan-500" => "#06b6d4",
+ "cyan-600" => "#0891b2",
+ "cyan-700" => "#0e7490",
+ "cyan-800" => "#155e75",
+ "cyan-900" => "#164e63",
+ "cyan-950" => "#083344",
+ // Sky
+ "sky-50" => "#f0f9ff",
+ "sky-100" => "#e0f2fe",
+ "sky-200" => "#bae6fd",
+ "sky-300" => "#7dd3fc",
+ "sky-400" => "#38bdf8",
+ "sky-500" => "#0ea5e9",
+ "sky-600" => "#0284c7",
+ "sky-700" => "#0369a1",
+ "sky-800" => "#075985",
+ "sky-900" => "#0c4a6e",
+ "sky-950" => "#082f49",
+ // Blue
+ "blue-50" => "#eff6ff",
+ "blue-100" => "#dbeafe",
+ "blue-200" => "#bfdbfe",
+ "blue-300" => "#93c5fd",
+ "blue-400" => "#60a5fa",
+ "blue-500" => "#3b82f6",
+ "blue-600" => "#2563eb",
+ "blue-700" => "#1d4ed8",
+ "blue-800" => "#1e40af",
+ "blue-900" => "#1e3a8a",
+ "blue-950" => "#172554",
+ // Indigo
+ "indigo-50" => "#eef2ff",
+ "indigo-100" => "#e0e7ff",
+ "indigo-200" => "#c7d2fe",
+ "indigo-300" => "#a5b4fc",
+ "indigo-400" => "#818cf8",
+ "indigo-500" => "#6366f1",
+ "indigo-600" => "#4f46e5",
+ "indigo-700" => "#4338ca",
+ "indigo-800" => "#3730a3",
+ "indigo-900" => "#312e81",
+ "indigo-950" => "#1e1b4b",
+ // Violet
+ "violet-50" => "#f5f3ff",
+ "violet-100" => "#ede9fe",
+ "violet-200" => "#ddd6fe",
+ "violet-300" => "#c4b5fd",
+ "violet-400" => "#a78bfa",
+ "violet-500" => "#8b5cf6",
+ "violet-600" => "#7c3aed",
+ "violet-700" => "#6d28d9",
+ "violet-800" => "#5b21b6",
+ "violet-900" => "#4c1d95",
+ "violet-950" => "#2e1065",
+ // Purple
+ "purple-50" => "#faf5ff",
+ "purple-100" => "#f3e8ff",
+ "purple-200" => "#e9d5ff",
+ "purple-300" => "#d8b4fe",
+ "purple-400" => "#c084fc",
+ "purple-500" => "#a855f7",
+ "purple-600" => "#9333ea",
+ "purple-700" => "#7e22ce",
+ "purple-800" => "#6b21a8",
+ "purple-900" => "#581c87",
+ "purple-950" => "#3b0764",
+ // Fuchsia
+ "fuchsia-50" => "#fdf4ff",
+ "fuchsia-100" => "#fae8ff",
+ "fuchsia-200" => "#f5d0fe",
+ "fuchsia-300" => "#f0abfc",
+ "fuchsia-400" => "#e879f9",
+ "fuchsia-500" => "#d946ef",
+ "fuchsia-600" => "#c026d3",
+ "fuchsia-700" => "#a21caf",
+ "fuchsia-800" => "#86198f",
+ "fuchsia-900" => "#701a75",
+ "fuchsia-950" => "#4a044e",
+ // Pink
+ "pink-50" => "#fdf2f8",
+ "pink-100" => "#fce7f3",
+ "pink-200" => "#fbcfe8",
+ "pink-300" => "#f9a8d4",
+ "pink-400" => "#f472b6",
+ "pink-500" => "#ec4899",
+ "pink-600" => "#db2777",
+ "pink-700" => "#be185d",
+ "pink-800" => "#9d174d",
+ "pink-900" => "#831843",
+ "pink-950" => "#500724",
+ // Rose
+ "rose-50" => "#fff1f2",
+ "rose-100" => "#ffe4e6",
+ "rose-200" => "#fecdd3",
+ "rose-300" => "#fda4af",
+ "rose-400" => "#fb7185",
+ "rose-500" => "#f43f5e",
+ "rose-600" => "#e11d48",
+ "rose-700" => "#be123c",
+ "rose-800" => "#9f1239",
+ "rose-900" => "#881337",
+ "rose-950" => "#4c0519",
+};
+
+/// Spacing scale (Tailwind default: 1 unit = 0.25rem = 4px)
+static SPACING_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "0" => "0px",
+ "px" => "1px",
+ "0.5" => "0.125rem",
+ "1" => "0.25rem",
+ "1.5" => "0.375rem",
+ "2" => "0.5rem",
+ "2.5" => "0.625rem",
+ "3" => "0.75rem",
+ "3.5" => "0.875rem",
+ "4" => "1rem",
+ "5" => "1.25rem",
+ "6" => "1.5rem",
+ "7" => "1.75rem",
+ "8" => "2rem",
+ "9" => "2.25rem",
+ "10" => "2.5rem",
+ "11" => "2.75rem",
+ "12" => "3rem",
+ "14" => "3.5rem",
+ "16" => "4rem",
+ "20" => "5rem",
+ "24" => "6rem",
+ "28" => "7rem",
+ "32" => "8rem",
+ "36" => "9rem",
+ "40" => "10rem",
+ "44" => "11rem",
+ "48" => "12rem",
+ "52" => "13rem",
+ "56" => "14rem",
+ "60" => "15rem",
+ "64" => "16rem",
+ "72" => "18rem",
+ "80" => "20rem",
+ "96" => "24rem",
+ "auto" => "auto",
+ "full" => "100%",
+ "1/2" => "50%",
+ "1/3" => "33.333333%",
+ "2/3" => "66.666667%",
+ "1/4" => "25%",
+ "2/4" => "50%",
+ "3/4" => "75%",
+ "1/5" => "20%",
+ "2/5" => "40%",
+ "3/5" => "60%",
+ "4/5" => "80%",
+ "1/6" => "16.666667%",
+ "2/6" => "33.333333%",
+ "3/6" => "50%",
+ "4/6" => "66.666667%",
+ "5/6" => "83.333333%",
+ "1/12" => "8.333333%",
+ "2/12" => "16.666667%",
+ "3/12" => "25%",
+ "4/12" => "33.333333%",
+ "5/12" => "41.666667%",
+ "6/12" => "50%",
+ "7/12" => "58.333333%",
+ "8/12" => "66.666667%",
+ "9/12" => "75%",
+ "10/12" => "83.333333%",
+ "11/12" => "91.666667%",
+ "screen" => "100vw",
+ "svw" => "100svw",
+ "lvw" => "100lvw",
+ "dvw" => "100dvw",
+ "min" => "min-content",
+ "max" => "max-content",
+ "fit" => "fit-content",
+};
+
+/// Font size scale
+static FONT_SIZE_SCALE: phf::Map<&'static str, (&'static str, &'static str)> = phf_map! {
+ "xs" => ("0.75rem", "1rem"),
+ "sm" => ("0.875rem", "1.25rem"),
+ "base" => ("1rem", "1.5rem"),
+ "lg" => ("1.125rem", "1.75rem"),
+ "xl" => ("1.25rem", "1.75rem"),
+ "2xl" => ("1.5rem", "2rem"),
+ "3xl" => ("1.875rem", "2.25rem"),
+ "4xl" => ("2.25rem", "2.5rem"),
+ "5xl" => ("3rem", "1"),
+ "6xl" => ("3.75rem", "1"),
+ "7xl" => ("4.5rem", "1"),
+ "8xl" => ("6rem", "1"),
+ "9xl" => ("8rem", "1"),
+};
+
+/// Font weight scale
+static FONT_WEIGHT_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "thin" => "100",
+ "extralight" => "200",
+ "light" => "300",
+ "normal" => "400",
+ "medium" => "500",
+ "semibold" => "600",
+ "bold" => "700",
+ "extrabold" => "800",
+ "black" => "900",
+};
+
+/// Border radius scale
+static BORDER_RADIUS_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "none" => "0px",
+ "sm" => "0.125rem",
+ "" => "0.25rem",
+ "md" => "0.375rem",
+ "lg" => "0.5rem",
+ "xl" => "0.75rem",
+ "2xl" => "1rem",
+ "3xl" => "1.5rem",
+ "full" => "9999px",
+};
+
+/// Opacity scale
+static OPACITY_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "0" => "0",
+ "5" => "0.05",
+ "10" => "0.1",
+ "15" => "0.15",
+ "20" => "0.2",
+ "25" => "0.25",
+ "30" => "0.3",
+ "35" => "0.35",
+ "40" => "0.4",
+ "45" => "0.45",
+ "50" => "0.5",
+ "55" => "0.55",
+ "60" => "0.6",
+ "65" => "0.65",
+ "70" => "0.7",
+ "75" => "0.75",
+ "80" => "0.8",
+ "85" => "0.85",
+ "90" => "0.9",
+ "95" => "0.95",
+ "100" => "1",
+};
+
+/// Z-index scale
+static Z_INDEX_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "0" => "0",
+ "10" => "10",
+ "20" => "20",
+ "30" => "30",
+ "40" => "40",
+ "50" => "50",
+ "auto" => "auto",
+};
+
+/// Box shadow scale
+static BOX_SHADOW_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "sm" => "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+ "" => "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+ "md" => "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+ "lg" => "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
+ "xl" => "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
+ "2xl" => "0 25px 50px -12px rgb(0 0 0 / 0.25)",
+ "inner" => "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",
+ "none" => "0 0 #0000",
+};
+
+/// Border width scale
+static BORDER_WIDTH_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "0" => "0px",
+ "" => "1px",
+ "2" => "2px",
+ "4" => "4px",
+ "8" => "8px",
+};
+
+/// Transition duration scale
+static DURATION_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "0" => "0s",
+ "75" => "75ms",
+ "100" => "100ms",
+ "150" => "150ms",
+ "200" => "200ms",
+ "300" => "300ms",
+ "500" => "500ms",
+ "700" => "700ms",
+ "1000" => "1000ms",
+};
+
+/// Ease timing functions
+static EASE_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
+ "linear" => "linear",
+ "in" => "cubic-bezier(0.4, 0, 1, 1)",
+ "out" => "cubic-bezier(0, 0, 0.2, 1)",
+ "in-out" => "cubic-bezier(0.4, 0, 0.2, 1)",
+};
+
+/// Check if a string contains Tailwind classes
+pub fn has_tailwind_classes(class_str: &str) -> bool {
+ // Simple heuristic: if it looks like a Tailwind class pattern
+ let parts: Vec<&str> = class_str.split_whitespace().collect();
+ for part in parts {
+ if is_likely_tailwind_class(part) {
+ return true;
+ }
+ }
+ false
+}
+
+/// Check if a single class looks like a Tailwind utility
+fn is_likely_tailwind_class(class: &str) -> bool {
+ // Strip any responsive/variant prefixes
+ let class = class
+ .split(':')
+ .next_back()
+ .unwrap_or(class)
+ .trim_start_matches('-');
+
+ // Common Tailwind prefixes
+ let prefixes = [
+ "bg-",
+ "text-",
+ "font-",
+ "p-",
+ "px-",
+ "py-",
+ "pt-",
+ "pr-",
+ "pb-",
+ "pl-",
+ "m-",
+ "mx-",
+ "my-",
+ "mt-",
+ "mr-",
+ "mb-",
+ "ml-",
+ "w-",
+ "h-",
+ "min-w-",
+ "max-w-",
+ "min-h-",
+ "max-h-",
+ "flex",
+ "grid",
+ "block",
+ "inline",
+ "hidden",
+ "absolute",
+ "relative",
+ "fixed",
+ "sticky",
+ "top-",
+ "right-",
+ "bottom-",
+ "left-",
+ "inset-",
+ "z-",
+ "opacity-",
+ "rounded",
+ "border",
+ "shadow",
+ "gap-",
+ "space-",
+ "items-",
+ "justify-",
+ "content-",
+ "self-",
+ "order-",
+ "col-",
+ "row-",
+ "overflow-",
+ "object-",
+ "aspect-",
+ "transition",
+ "duration-",
+ "ease-",
+ "delay-",
+ "animate-",
+ "cursor-",
+ "select-",
+ "resize",
+ "appearance-",
+ "outline",
+ "ring",
+ "fill-",
+ "stroke-",
+ "sr-",
+ "not-sr-",
+ "container",
+ "columns-",
+ "break-",
+ "decoration-",
+ "underline",
+ "overline",
+ "line-through",
+ "no-underline",
+ "uppercase",
+ "lowercase",
+ "capitalize",
+ "normal-case",
+ "truncate",
+ "leading-",
+ "tracking-",
+ "list-",
+ "align-",
+ "whitespace-",
+ "hyphens-",
+ "blur",
+ "brightness-",
+ "contrast-",
+ "grayscale",
+ "invert",
+ "saturate-",
+ "sepia",
+ "drop-shadow",
+ "backdrop-",
+ "scale-",
+ "rotate-",
+ "translate-",
+ "skew-",
+ "origin-",
+ "accent-",
+ "caret-",
+ "scroll-",
+ "snap-",
+ "touch-",
+ "will-change-",
+ "table",
+ "clear-",
+ "float-",
+ "isolate",
+ "isolation-",
+ "mix-blend-",
+ "bg-blend-",
+ "divide-",
+ "place-",
+ "grow",
+ "shrink",
+ "basis-",
+ ];
+
+ // Exact matches for utility classes without values
+ let exact_matches = [
+ "flex",
+ "inline-flex",
+ "grid",
+ "inline-grid",
+ "block",
+ "inline-block",
+ "inline",
+ "contents",
+ "flow-root",
+ "hidden",
+ "invisible",
+ "visible",
+ "collapse",
+ "absolute",
+ "relative",
+ "fixed",
+ "sticky",
+ "static",
+ "isolate",
+ "isolation-auto",
+ "container",
+ "truncate",
+ "uppercase",
+ "lowercase",
+ "capitalize",
+ "normal-case",
+ "italic",
+ "not-italic",
+ "underline",
+ "overline",
+ "line-through",
+ "no-underline",
+ "antialiased",
+ "subpixel-antialiased",
+ "ordinal",
+ "slashed-zero",
+ "lining-nums",
+ "oldstyle-nums",
+ "proportional-nums",
+ "tabular-nums",
+ "diagonal-fractions",
+ "stacked-fractions",
+ "sr-only",
+ "not-sr-only",
+ "resize",
+ "resize-none",
+ "resize-y",
+ "resize-x",
+ "transition",
+ "transition-none",
+ "transition-all",
+ "transition-colors",
+ "transition-opacity",
+ "transition-shadow",
+ "transition-transform",
+ "animate-none",
+ "animate-spin",
+ "animate-ping",
+ "animate-pulse",
+ "animate-bounce",
+ "grayscale",
+ "grayscale-0",
+ "invert",
+ "invert-0",
+ "sepia",
+ "sepia-0",
+ "backdrop-blur",
+ "backdrop-blur-none",
+ "backdrop-grayscale",
+ "backdrop-grayscale-0",
+ "backdrop-invert",
+ "backdrop-invert-0",
+ "backdrop-sepia",
+ "backdrop-sepia-0",
+ "table",
+ "table-caption",
+ "table-cell",
+ "table-column",
+ "table-column-group",
+ "table-footer-group",
+ "table-header-group",
+ "table-row-group",
+ "table-row",
+ "border-collapse",
+ "border-separate",
+ "grow",
+ "grow-0",
+ "shrink",
+ "shrink-0",
+ "rounded",
+ "rounded-none",
+ "rounded-sm",
+ "rounded-md",
+ "rounded-lg",
+ "rounded-xl",
+ "rounded-2xl",
+ "rounded-3xl",
+ "rounded-full",
+ "border",
+ "border-0",
+ "border-2",
+ "border-4",
+ "border-8",
+ "shadow",
+ "shadow-sm",
+ "shadow-md",
+ "shadow-lg",
+ "shadow-xl",
+ "shadow-2xl",
+ "shadow-inner",
+ "shadow-none",
+ "outline",
+ "outline-none",
+ "outline-dashed",
+ "outline-dotted",
+ "outline-double",
+ "ring",
+ "ring-0",
+ "ring-1",
+ "ring-2",
+ "ring-4",
+ "ring-8",
+ "ring-inset",
+ "blur",
+ "blur-none",
+ "blur-sm",
+ "blur-md",
+ "blur-lg",
+ "blur-xl",
+ "blur-2xl",
+ "blur-3xl",
+ ];
+
+ if exact_matches.contains(&class) {
+ return true;
+ }
+
+ for prefix in prefixes {
+ if let Some(value_part) = class.strip_prefix(prefix) {
+ // For prefixes that end with '-', validate the value part
+ if prefix.ends_with('-') {
+ if is_valid_tailwind_value(value_part) {
+ return true;
+ }
+ } else {
+ // Prefix without dash (like "flex", "grid") - exact prefix match is enough
+ return true;
+ }
+ }
+ }
+
+ // Check for arbitrary value syntax
+ if class.contains('[') && class.contains(']') {
+ return true;
+ }
+
+ false
+}
+
+/// Check if a value part looks like a valid Tailwind value
+fn is_valid_tailwind_value(value: &str) -> bool {
+ if value.is_empty() {
+ return false;
+ }
+
+ // Arbitrary value syntax [...]
+ if value.starts_with('[') && value.ends_with(']') {
+ return true;
+ }
+
+ // Common keywords
+ let keywords = [
+ "auto",
+ "full",
+ "screen",
+ "min",
+ "max",
+ "fit",
+ "px",
+ "none",
+ "inherit",
+ "current",
+ "transparent",
+ "black",
+ "white",
+ ];
+ if keywords.contains(&value) {
+ return true;
+ }
+
+ // Numeric values (including decimals like 0.5, 1.5)
+ let first_char = value.chars().next().unwrap();
+ if first_char.is_ascii_digit() {
+ return true;
+ }
+
+ // Color names with shade (e.g., red-500, blue-100)
+ let color_names = [
+ "slate", "gray", "zinc", "neutral", "stone", "red", "orange", "amber", "yellow", "lime",
+ "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia",
+ "pink", "rose",
+ ];
+ for color in color_names {
+ if let Some(rest) = value.strip_prefix(color) {
+ // Must be followed by nothing or a dash and number
+ if rest.is_empty() || rest.starts_with('-') {
+ return true;
+ }
+ }
+ }
+
+ // Size suffixes (xs, sm, md, lg, xl, 2xl, etc.)
+ let size_keywords = [
+ "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl",
+ ];
+ if size_keywords.contains(&value) {
+ return true;
+ }
+
+ // Fraction values (1/2, 1/3, 2/3, etc.)
+ if value.contains('/') {
+ let parts: Vec<&str> = value.split('/').collect();
+ if parts.len() == 2
+ && parts[0].chars().all(|c| c.is_ascii_digit())
+ && parts[1].chars().all(|c| c.is_ascii_digit())
+ {
+ return true;
+ }
+ }
+
+ false
+}
+
+/// Parse a className string into a list of ExtractStyleValue
+pub fn parse_tailwind_to_styles(class_str: &str, filename: Option<&str>) -> Vec {
+ let mut styles = Vec::new();
+
+ for class in class_str.split_whitespace() {
+ if let Some(parsed) = parse_single_class(class) {
+ let static_style = parsed.to_static_style();
+ styles.push(ExtractStyleValue::Static(static_style));
+ }
+ }
+
+ // Set filename for all styles if provided
+ if filename.is_some() {
+ // The filename is already used in ExtractStaticStyle through the extract() method
+ }
+
+ styles
+}
+
+/// Parse a single Tailwind class string
+pub fn parse_single_class(class: &str) -> Option {
+ let mut remaining = class;
+ let mut responsive_level: u8 = 0;
+ let mut variants: Vec = Vec::new();
+
+ // Handle negative prefix at the start
+ let negative = remaining.starts_with('-');
+ if negative {
+ remaining = &remaining[1..];
+ }
+
+ // Parse prefixes (responsive and variants)
+ while let Some(colon_pos) = remaining.find(':') {
+ let prefix = &remaining[..colon_pos];
+
+ // Check if it's a responsive prefix
+ if let Some(&level) = RESPONSIVE_PREFIX_MAP.get(prefix) {
+ responsive_level = level;
+ } else if let Some(variant) = TailwindVariant::from_prefix(prefix) {
+ variants.push(variant);
+ } else {
+ // Unknown prefix, might be an arbitrary variant
+ // For now, skip unknown prefixes
+ }
+
+ remaining = &remaining[colon_pos + 1..];
+ }
+
+ // Now parse the utility class
+ parse_utility(remaining, negative).map(|(property, value)| TailwindClass {
+ responsive: responsive_level,
+ variants,
+ property,
+ value,
+ negative,
+ })
+}
+
+/// Parse a utility class (without prefixes) into property and value
+fn parse_utility(class: &str, is_negative: bool) -> Option<(String, String)> {
+ // Handle arbitrary values first
+ if let Some(result) = parse_arbitrary_value(class) {
+ return Some(result);
+ }
+
+ // Layout utilities
+ if let Some(result) = parse_layout_utility(class) {
+ return Some(result);
+ }
+
+ // Flexbox & Grid
+ if let Some(result) = parse_flex_grid_utility(class) {
+ return Some(result);
+ }
+
+ // Spacing (padding, margin)
+ if let Some(result) = parse_spacing_utility(class, is_negative) {
+ return Some(result);
+ }
+
+ // Sizing (width, height)
+ if let Some(result) = parse_sizing_utility(class) {
+ return Some(result);
+ }
+
+ // Typography
+ if let Some(result) = parse_typography_utility(class) {
+ return Some(result);
+ }
+
+ // Backgrounds
+ if let Some(result) = parse_background_utility(class) {
+ return Some(result);
+ }
+
+ // Borders
+ if let Some(result) = parse_border_utility(class) {
+ return Some(result);
+ }
+
+ // Effects (shadow, opacity)
+ if let Some(result) = parse_effects_utility(class) {
+ return Some(result);
+ }
+
+ // Filters
+ if let Some(result) = parse_filter_utility(class) {
+ return Some(result);
+ }
+
+ // Transitions & Animation
+ if let Some(result) = parse_transition_utility(class) {
+ return Some(result);
+ }
+
+ // Transforms
+ if let Some(result) = parse_transform_utility(class, is_negative) {
+ return Some(result);
+ }
+
+ // Interactivity
+ if let Some(result) = parse_interactivity_utility(class) {
+ return Some(result);
+ }
+
+ // SVG
+ if let Some(result) = parse_svg_utility(class) {
+ return Some(result);
+ }
+
+ // Accessibility
+ if let Some(result) = parse_accessibility_utility(class) {
+ return Some(result);
+ }
+
+ None
+}
+
+/// Parse arbitrary value syntax: class-[value]
+fn parse_arbitrary_value(class: &str) -> Option<(String, String)> {
+ if !class.contains('[') {
+ return None;
+ }
+
+ let bracket_start = class.find('[')?;
+ let bracket_end = class.rfind(']')?;
+
+ if bracket_end <= bracket_start {
+ return None;
+ }
+
+ let prefix = &class[..bracket_start];
+ let value = &class[bracket_start + 1..bracket_end];
+
+ // Replace underscores with spaces in arbitrary values
+ let value = value.replace('_', " ");
+
+ match prefix {
+ "w-" => Some(("width".to_string(), value)),
+ "h-" => Some(("height".to_string(), value)),
+ "min-w-" => Some(("min-width".to_string(), value)),
+ "max-w-" => Some(("max-width".to_string(), value)),
+ "min-h-" => Some(("min-height".to_string(), value)),
+ "max-h-" => Some(("max-height".to_string(), value)),
+ "p-" => Some(("padding".to_string(), value)),
+ "px-" => Some(("padding-inline".to_string(), value)),
+ "py-" => Some(("padding-block".to_string(), value)),
+ "pt-" => Some(("padding-top".to_string(), value)),
+ "pr-" => Some(("padding-right".to_string(), value)),
+ "pb-" => Some(("padding-bottom".to_string(), value)),
+ "pl-" => Some(("padding-left".to_string(), value)),
+ "m-" => Some(("margin".to_string(), value)),
+ "mx-" => Some(("margin-inline".to_string(), value)),
+ "my-" => Some(("margin-block".to_string(), value)),
+ "mt-" => Some(("margin-top".to_string(), value)),
+ "mr-" => Some(("margin-right".to_string(), value)),
+ "mb-" => Some(("margin-bottom".to_string(), value)),
+ "ml-" => Some(("margin-left".to_string(), value)),
+ "top-" => Some(("top".to_string(), value)),
+ "right-" => Some(("right".to_string(), value)),
+ "bottom-" => Some(("bottom".to_string(), value)),
+ "left-" => Some(("left".to_string(), value)),
+ "inset-" => Some(("inset".to_string(), value)),
+ "inset-x-" => Some(("inset-inline".to_string(), value)),
+ "inset-y-" => Some(("inset-block".to_string(), value)),
+ "gap-" => Some(("gap".to_string(), value)),
+ "gap-x-" => Some(("column-gap".to_string(), value)),
+ "gap-y-" => Some(("row-gap".to_string(), value)),
+ "text-" => Some(("color".to_string(), value)),
+ "bg-" => Some(("background-color".to_string(), value)),
+ "border-" => Some(("border-color".to_string(), value)),
+ "rounded-" => Some(("border-radius".to_string(), value)),
+ "opacity-" => Some(("opacity".to_string(), value)),
+ "z-" => Some(("z-index".to_string(), value)),
+ "font-" => Some(("font-family".to_string(), value)),
+ "tracking-" => Some(("letter-spacing".to_string(), value)),
+ "leading-" => Some(("line-height".to_string(), value)),
+ "duration-" => Some(("transition-duration".to_string(), value)),
+ "delay-" => Some(("transition-delay".to_string(), value)),
+ "scale-" => Some(("transform".to_string(), format!("scale({})", value))),
+ "rotate-" => Some(("transform".to_string(), format!("rotate({})", value))),
+ "translate-x-" => Some(("transform".to_string(), format!("translateX({})", value))),
+ "translate-y-" => Some(("transform".to_string(), format!("translateY({})", value))),
+ "skew-x-" => Some(("transform".to_string(), format!("skewX({})", value))),
+ "skew-y-" => Some(("transform".to_string(), format!("skewY({})", value))),
+ "aspect-" => Some(("aspect-ratio".to_string(), value)),
+ "columns-" => Some(("columns".to_string(), value)),
+ "grid-cols-" => Some((
+ "grid-template-columns".to_string(),
+ format!("repeat({}, minmax(0, 1fr))", value),
+ )),
+ "grid-rows-" => Some((
+ "grid-template-rows".to_string(),
+ format!("repeat({}, minmax(0, 1fr))", value),
+ )),
+ "col-span-" => Some((
+ "grid-column".to_string(),
+ format!("span {} / span {}", value, value),
+ )),
+ "row-span-" => Some((
+ "grid-row".to_string(),
+ format!("span {} / span {}", value, value),
+ )),
+ "basis-" => Some(("flex-basis".to_string(), value)),
+ "blur-" => Some(("filter".to_string(), format!("blur({})", value))),
+ "brightness-" => Some(("filter".to_string(), format!("brightness({})", value))),
+ "contrast-" => Some(("filter".to_string(), format!("contrast({})", value))),
+ "saturate-" => Some(("filter".to_string(), format!("saturate({})", value))),
+ "backdrop-blur-" => Some(("backdrop-filter".to_string(), format!("blur({})", value))),
+ _ => None,
+ }
+}
+
+/// Parse layout utilities (display, position, visibility, etc.)
+fn parse_layout_utility(class: &str) -> Option<(String, String)> {
+ match class {
+ // Display
+ "block" => Some(("display".to_string(), "block".to_string())),
+ "inline-block" => Some(("display".to_string(), "inline-block".to_string())),
+ "inline" => Some(("display".to_string(), "inline".to_string())),
+ "flex" => Some(("display".to_string(), "flex".to_string())),
+ "inline-flex" => Some(("display".to_string(), "inline-flex".to_string())),
+ "table" => Some(("display".to_string(), "table".to_string())),
+ "inline-table" => Some(("display".to_string(), "inline-table".to_string())),
+ "table-caption" => Some(("display".to_string(), "table-caption".to_string())),
+ "table-cell" => Some(("display".to_string(), "table-cell".to_string())),
+ "table-column" => Some(("display".to_string(), "table-column".to_string())),
+ "table-column-group" => Some(("display".to_string(), "table-column-group".to_string())),
+ "table-footer-group" => Some(("display".to_string(), "table-footer-group".to_string())),
+ "table-header-group" => Some(("display".to_string(), "table-header-group".to_string())),
+ "table-row-group" => Some(("display".to_string(), "table-row-group".to_string())),
+ "table-row" => Some(("display".to_string(), "table-row".to_string())),
+ "flow-root" => Some(("display".to_string(), "flow-root".to_string())),
+ "grid" => Some(("display".to_string(), "grid".to_string())),
+ "inline-grid" => Some(("display".to_string(), "inline-grid".to_string())),
+ "contents" => Some(("display".to_string(), "contents".to_string())),
+ "list-item" => Some(("display".to_string(), "list-item".to_string())),
+ "hidden" => Some(("display".to_string(), "none".to_string())),
+
+ // Position
+ "static" => Some(("position".to_string(), "static".to_string())),
+ "fixed" => Some(("position".to_string(), "fixed".to_string())),
+ "absolute" => Some(("position".to_string(), "absolute".to_string())),
+ "relative" => Some(("position".to_string(), "relative".to_string())),
+ "sticky" => Some(("position".to_string(), "sticky".to_string())),
+
+ // Visibility
+ "visible" => Some(("visibility".to_string(), "visible".to_string())),
+ "invisible" => Some(("visibility".to_string(), "hidden".to_string())),
+ "collapse" => Some(("visibility".to_string(), "collapse".to_string())),
+
+ // Box sizing
+ "box-border" => Some(("box-sizing".to_string(), "border-box".to_string())),
+ "box-content" => Some(("box-sizing".to_string(), "content-box".to_string())),
+
+ // Float
+ "float-start" => Some(("float".to_string(), "inline-start".to_string())),
+ "float-end" => Some(("float".to_string(), "inline-end".to_string())),
+ "float-right" => Some(("float".to_string(), "right".to_string())),
+ "float-left" => Some(("float".to_string(), "left".to_string())),
+ "float-none" => Some(("float".to_string(), "none".to_string())),
+
+ // Clear
+ "clear-start" => Some(("clear".to_string(), "inline-start".to_string())),
+ "clear-end" => Some(("clear".to_string(), "inline-end".to_string())),
+ "clear-left" => Some(("clear".to_string(), "left".to_string())),
+ "clear-right" => Some(("clear".to_string(), "right".to_string())),
+ "clear-both" => Some(("clear".to_string(), "both".to_string())),
+ "clear-none" => Some(("clear".to_string(), "none".to_string())),
+
+ // Isolation
+ "isolate" => Some(("isolation".to_string(), "isolate".to_string())),
+ "isolation-auto" => Some(("isolation".to_string(), "auto".to_string())),
+
+ // Object fit
+ "object-contain" => Some(("object-fit".to_string(), "contain".to_string())),
+ "object-cover" => Some(("object-fit".to_string(), "cover".to_string())),
+ "object-fill" => Some(("object-fit".to_string(), "fill".to_string())),
+ "object-none" => Some(("object-fit".to_string(), "none".to_string())),
+ "object-scale-down" => Some(("object-fit".to_string(), "scale-down".to_string())),
+
+ // Object position
+ "object-bottom" => Some(("object-position".to_string(), "bottom".to_string())),
+ "object-center" => Some(("object-position".to_string(), "center".to_string())),
+ "object-left" => Some(("object-position".to_string(), "left".to_string())),
+ "object-left-bottom" => Some(("object-position".to_string(), "left bottom".to_string())),
+ "object-left-top" => Some(("object-position".to_string(), "left top".to_string())),
+ "object-right" => Some(("object-position".to_string(), "right".to_string())),
+ "object-right-bottom" => Some(("object-position".to_string(), "right bottom".to_string())),
+ "object-right-top" => Some(("object-position".to_string(), "right top".to_string())),
+ "object-top" => Some(("object-position".to_string(), "top".to_string())),
+
+ // Overflow
+ "overflow-auto" => Some(("overflow".to_string(), "auto".to_string())),
+ "overflow-hidden" => Some(("overflow".to_string(), "hidden".to_string())),
+ "overflow-clip" => Some(("overflow".to_string(), "clip".to_string())),
+ "overflow-visible" => Some(("overflow".to_string(), "visible".to_string())),
+ "overflow-scroll" => Some(("overflow".to_string(), "scroll".to_string())),
+ "overflow-x-auto" => Some(("overflow-x".to_string(), "auto".to_string())),
+ "overflow-y-auto" => Some(("overflow-y".to_string(), "auto".to_string())),
+ "overflow-x-hidden" => Some(("overflow-x".to_string(), "hidden".to_string())),
+ "overflow-y-hidden" => Some(("overflow-y".to_string(), "hidden".to_string())),
+ "overflow-x-clip" => Some(("overflow-x".to_string(), "clip".to_string())),
+ "overflow-y-clip" => Some(("overflow-y".to_string(), "clip".to_string())),
+ "overflow-x-visible" => Some(("overflow-x".to_string(), "visible".to_string())),
+ "overflow-y-visible" => Some(("overflow-y".to_string(), "visible".to_string())),
+ "overflow-x-scroll" => Some(("overflow-x".to_string(), "scroll".to_string())),
+ "overflow-y-scroll" => Some(("overflow-y".to_string(), "scroll".to_string())),
+
+ // Overscroll
+ "overscroll-auto" => Some(("overscroll-behavior".to_string(), "auto".to_string())),
+ "overscroll-contain" => Some(("overscroll-behavior".to_string(), "contain".to_string())),
+ "overscroll-none" => Some(("overscroll-behavior".to_string(), "none".to_string())),
+ "overscroll-x-auto" => Some(("overscroll-behavior-x".to_string(), "auto".to_string())),
+ "overscroll-x-contain" => {
+ Some(("overscroll-behavior-x".to_string(), "contain".to_string()))
+ }
+ "overscroll-x-none" => Some(("overscroll-behavior-x".to_string(), "none".to_string())),
+ "overscroll-y-auto" => Some(("overscroll-behavior-y".to_string(), "auto".to_string())),
+ "overscroll-y-contain" => {
+ Some(("overscroll-behavior-y".to_string(), "contain".to_string()))
+ }
+ "overscroll-y-none" => Some(("overscroll-behavior-y".to_string(), "none".to_string())),
+
+ _ => {
+ // Aspect ratio
+ if let Some(rest) = class.strip_prefix("aspect-") {
+ let value = match rest {
+ "auto" => "auto".to_string(),
+ "square" => "1 / 1".to_string(),
+ "video" => "16 / 9".to_string(),
+ v => v.replace('-', " / "),
+ };
+ return Some(("aspect-ratio".to_string(), value));
+ }
+
+ // Columns
+ if let Some(rest) = class.strip_prefix("columns-") {
+ let value = match rest {
+ "auto" => "auto".to_string(),
+ "3xs" => "16rem".to_string(),
+ "2xs" => "18rem".to_string(),
+ "xs" => "20rem".to_string(),
+ "sm" => "24rem".to_string(),
+ "md" => "28rem".to_string(),
+ "lg" => "32rem".to_string(),
+ "xl" => "36rem".to_string(),
+ "2xl" => "42rem".to_string(),
+ "3xl" => "48rem".to_string(),
+ "4xl" => "56rem".to_string(),
+ "5xl" => "64rem".to_string(),
+ "6xl" => "72rem".to_string(),
+ "7xl" => "80rem".to_string(),
+ v => v.to_string(),
+ };
+ return Some(("columns".to_string(), value));
+ }
+
+ // Break utilities
+ if let Some(rest) = class.strip_prefix("break-") {
+ return match rest {
+ "after-auto" => Some(("break-after".to_string(), "auto".to_string())),
+ "after-avoid" => Some(("break-after".to_string(), "avoid".to_string())),
+ "after-all" => Some(("break-after".to_string(), "all".to_string())),
+ "after-avoid-page" => {
+ Some(("break-after".to_string(), "avoid-page".to_string()))
+ }
+ "after-page" => Some(("break-after".to_string(), "page".to_string())),
+ "after-left" => Some(("break-after".to_string(), "left".to_string())),
+ "after-right" => Some(("break-after".to_string(), "right".to_string())),
+ "after-column" => Some(("break-after".to_string(), "column".to_string())),
+ "before-auto" => Some(("break-before".to_string(), "auto".to_string())),
+ "before-avoid" => Some(("break-before".to_string(), "avoid".to_string())),
+ "before-all" => Some(("break-before".to_string(), "all".to_string())),
+ "before-avoid-page" => {
+ Some(("break-before".to_string(), "avoid-page".to_string()))
+ }
+ "before-page" => Some(("break-before".to_string(), "page".to_string())),
+ "before-left" => Some(("break-before".to_string(), "left".to_string())),
+ "before-right" => Some(("break-before".to_string(), "right".to_string())),
+ "before-column" => Some(("break-before".to_string(), "column".to_string())),
+ "inside-auto" => Some(("break-inside".to_string(), "auto".to_string())),
+ "inside-avoid" => Some(("break-inside".to_string(), "avoid".to_string())),
+ "inside-avoid-page" => {
+ Some(("break-inside".to_string(), "avoid-page".to_string()))
+ }
+ "inside-avoid-column" => {
+ Some(("break-inside".to_string(), "avoid-column".to_string()))
+ }
+ _ => None,
+ };
+ }
+
+ // Box decoration break
+ if class == "box-decoration-clone" {
+ return Some(("box-decoration-break".to_string(), "clone".to_string()));
+ }
+ if class == "box-decoration-slice" {
+ return Some(("box-decoration-break".to_string(), "slice".to_string()));
+ }
+
+ // Z-index
+ if let Some(rest) = class.strip_prefix("z-") {
+ if let Some(&value) = Z_INDEX_SCALE.get(rest) {
+ return Some(("z-index".to_string(), value.to_string()));
+ }
+ }
+
+ // Top/Right/Bottom/Left/Inset
+ if let Some(rest) = class.strip_prefix("top-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("top".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("right-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("right".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("bottom-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("bottom".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("left-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("left".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("inset-x-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("inset-inline".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("inset-y-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("inset-block".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("inset-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("inset".to_string(), value.to_string()));
+ }
+ }
+
+ None
+ }
+ }
+}
+
+/// Parse flexbox and grid utilities
+fn parse_flex_grid_utility(class: &str) -> Option<(String, String)> {
+ match class {
+ // Flex basis
+ "basis-auto" => Some(("flex-basis".to_string(), "auto".to_string())),
+ "basis-full" => Some(("flex-basis".to_string(), "100%".to_string())),
+
+ // Flex direction
+ "flex-row" => Some(("flex-direction".to_string(), "row".to_string())),
+ "flex-row-reverse" => Some(("flex-direction".to_string(), "row-reverse".to_string())),
+ "flex-col" => Some(("flex-direction".to_string(), "column".to_string())),
+ "flex-col-reverse" => Some(("flex-direction".to_string(), "column-reverse".to_string())),
+
+ // Flex wrap
+ "flex-wrap" => Some(("flex-wrap".to_string(), "wrap".to_string())),
+ "flex-wrap-reverse" => Some(("flex-wrap".to_string(), "wrap-reverse".to_string())),
+ "flex-nowrap" => Some(("flex-wrap".to_string(), "nowrap".to_string())),
+
+ // Flex
+ "flex-1" => Some(("flex".to_string(), "1 1 0%".to_string())),
+ "flex-auto" => Some(("flex".to_string(), "1 1 auto".to_string())),
+ "flex-initial" => Some(("flex".to_string(), "0 1 auto".to_string())),
+ "flex-none" => Some(("flex".to_string(), "none".to_string())),
+
+ // Grow/Shrink
+ "grow" => Some(("flex-grow".to_string(), "1".to_string())),
+ "grow-0" => Some(("flex-grow".to_string(), "0".to_string())),
+ "shrink" => Some(("flex-shrink".to_string(), "1".to_string())),
+ "shrink-0" => Some(("flex-shrink".to_string(), "0".to_string())),
+
+ // Order
+ "order-first" => Some(("order".to_string(), "-9999".to_string())),
+ "order-last" => Some(("order".to_string(), "9999".to_string())),
+ "order-none" => Some(("order".to_string(), "0".to_string())),
+
+ // Grid template columns
+ "grid-cols-none" => Some(("grid-template-columns".to_string(), "none".to_string())),
+ "grid-cols-subgrid" => Some(("grid-template-columns".to_string(), "subgrid".to_string())),
+
+ // Grid template rows
+ "grid-rows-none" => Some(("grid-template-rows".to_string(), "none".to_string())),
+ "grid-rows-subgrid" => Some(("grid-template-rows".to_string(), "subgrid".to_string())),
+
+ // Grid column
+ "col-auto" => Some(("grid-column".to_string(), "auto".to_string())),
+ "col-span-full" => Some(("grid-column".to_string(), "1 / -1".to_string())),
+ "col-start-auto" => Some(("grid-column-start".to_string(), "auto".to_string())),
+ "col-end-auto" => Some(("grid-column-end".to_string(), "auto".to_string())),
+
+ // Grid row
+ "row-auto" => Some(("grid-row".to_string(), "auto".to_string())),
+ "row-span-full" => Some(("grid-row".to_string(), "1 / -1".to_string())),
+ "row-start-auto" => Some(("grid-row-start".to_string(), "auto".to_string())),
+ "row-end-auto" => Some(("grid-row-end".to_string(), "auto".to_string())),
+
+ // Grid auto flow
+ "grid-flow-row" => Some(("grid-auto-flow".to_string(), "row".to_string())),
+ "grid-flow-col" => Some(("grid-auto-flow".to_string(), "column".to_string())),
+ "grid-flow-dense" => Some(("grid-auto-flow".to_string(), "dense".to_string())),
+ "grid-flow-row-dense" => Some(("grid-auto-flow".to_string(), "row dense".to_string())),
+ "grid-flow-col-dense" => Some(("grid-auto-flow".to_string(), "column dense".to_string())),
+
+ // Grid auto columns
+ "auto-cols-auto" => Some(("grid-auto-columns".to_string(), "auto".to_string())),
+ "auto-cols-min" => Some(("grid-auto-columns".to_string(), "min-content".to_string())),
+ "auto-cols-max" => Some(("grid-auto-columns".to_string(), "max-content".to_string())),
+ "auto-cols-fr" => Some((
+ "grid-auto-columns".to_string(),
+ "minmax(0, 1fr)".to_string(),
+ )),
+
+ // Grid auto rows
+ "auto-rows-auto" => Some(("grid-auto-rows".to_string(), "auto".to_string())),
+ "auto-rows-min" => Some(("grid-auto-rows".to_string(), "min-content".to_string())),
+ "auto-rows-max" => Some(("grid-auto-rows".to_string(), "max-content".to_string())),
+ "auto-rows-fr" => Some(("grid-auto-rows".to_string(), "minmax(0, 1fr)".to_string())),
+
+ // Justify content
+ "justify-normal" => Some(("justify-content".to_string(), "normal".to_string())),
+ "justify-start" => Some(("justify-content".to_string(), "flex-start".to_string())),
+ "justify-end" => Some(("justify-content".to_string(), "flex-end".to_string())),
+ "justify-center" => Some(("justify-content".to_string(), "center".to_string())),
+ "justify-between" => Some(("justify-content".to_string(), "space-between".to_string())),
+ "justify-around" => Some(("justify-content".to_string(), "space-around".to_string())),
+ "justify-evenly" => Some(("justify-content".to_string(), "space-evenly".to_string())),
+ "justify-stretch" => Some(("justify-content".to_string(), "stretch".to_string())),
+
+ // Justify items
+ "justify-items-start" => Some(("justify-items".to_string(), "start".to_string())),
+ "justify-items-end" => Some(("justify-items".to_string(), "end".to_string())),
+ "justify-items-center" => Some(("justify-items".to_string(), "center".to_string())),
+ "justify-items-stretch" => Some(("justify-items".to_string(), "stretch".to_string())),
+
+ // Justify self
+ "justify-self-auto" => Some(("justify-self".to_string(), "auto".to_string())),
+ "justify-self-start" => Some(("justify-self".to_string(), "start".to_string())),
+ "justify-self-end" => Some(("justify-self".to_string(), "end".to_string())),
+ "justify-self-center" => Some(("justify-self".to_string(), "center".to_string())),
+ "justify-self-stretch" => Some(("justify-self".to_string(), "stretch".to_string())),
+
+ // Align content
+ "content-normal" => Some(("align-content".to_string(), "normal".to_string())),
+ "content-center" => Some(("align-content".to_string(), "center".to_string())),
+ "content-start" => Some(("align-content".to_string(), "flex-start".to_string())),
+ "content-end" => Some(("align-content".to_string(), "flex-end".to_string())),
+ "content-between" => Some(("align-content".to_string(), "space-between".to_string())),
+ "content-around" => Some(("align-content".to_string(), "space-around".to_string())),
+ "content-evenly" => Some(("align-content".to_string(), "space-evenly".to_string())),
+ "content-baseline" => Some(("align-content".to_string(), "baseline".to_string())),
+ "content-stretch" => Some(("align-content".to_string(), "stretch".to_string())),
+
+ // Align items
+ "items-start" => Some(("align-items".to_string(), "flex-start".to_string())),
+ "items-end" => Some(("align-items".to_string(), "flex-end".to_string())),
+ "items-center" => Some(("align-items".to_string(), "center".to_string())),
+ "items-baseline" => Some(("align-items".to_string(), "baseline".to_string())),
+ "items-stretch" => Some(("align-items".to_string(), "stretch".to_string())),
+
+ // Align self
+ "self-auto" => Some(("align-self".to_string(), "auto".to_string())),
+ "self-start" => Some(("align-self".to_string(), "flex-start".to_string())),
+ "self-end" => Some(("align-self".to_string(), "flex-end".to_string())),
+ "self-center" => Some(("align-self".to_string(), "center".to_string())),
+ "self-stretch" => Some(("align-self".to_string(), "stretch".to_string())),
+ "self-baseline" => Some(("align-self".to_string(), "baseline".to_string())),
+
+ // Place content
+ "place-content-center" => Some(("place-content".to_string(), "center".to_string())),
+ "place-content-start" => Some(("place-content".to_string(), "start".to_string())),
+ "place-content-end" => Some(("place-content".to_string(), "end".to_string())),
+ "place-content-between" => Some(("place-content".to_string(), "space-between".to_string())),
+ "place-content-around" => Some(("place-content".to_string(), "space-around".to_string())),
+ "place-content-evenly" => Some(("place-content".to_string(), "space-evenly".to_string())),
+ "place-content-baseline" => Some(("place-content".to_string(), "baseline".to_string())),
+ "place-content-stretch" => Some(("place-content".to_string(), "stretch".to_string())),
+
+ // Place items
+ "place-items-start" => Some(("place-items".to_string(), "start".to_string())),
+ "place-items-end" => Some(("place-items".to_string(), "end".to_string())),
+ "place-items-center" => Some(("place-items".to_string(), "center".to_string())),
+ "place-items-baseline" => Some(("place-items".to_string(), "baseline".to_string())),
+ "place-items-stretch" => Some(("place-items".to_string(), "stretch".to_string())),
+
+ // Place self
+ "place-self-auto" => Some(("place-self".to_string(), "auto".to_string())),
+ "place-self-start" => Some(("place-self".to_string(), "start".to_string())),
+ "place-self-end" => Some(("place-self".to_string(), "end".to_string())),
+ "place-self-center" => Some(("place-self".to_string(), "center".to_string())),
+ "place-self-stretch" => Some(("place-self".to_string(), "stretch".to_string())),
+
+ _ => {
+ // Flex basis with spacing scale
+ if let Some(rest) = class.strip_prefix("basis-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("flex-basis".to_string(), value.to_string()));
+ }
+ }
+
+ // Order with number
+ if let Some(rest) = class.strip_prefix("order-") {
+ return Some(("order".to_string(), rest.to_string()));
+ }
+
+ // Grid cols
+ if let Some(rest) = class.strip_prefix("grid-cols-") {
+ if let Ok(n) = rest.parse::() {
+ return Some((
+ "grid-template-columns".to_string(),
+ format!("repeat({}, minmax(0, 1fr))", n),
+ ));
+ }
+ }
+
+ // Grid rows
+ if let Some(rest) = class.strip_prefix("grid-rows-") {
+ if let Ok(n) = rest.parse::() {
+ return Some((
+ "grid-template-rows".to_string(),
+ format!("repeat({}, minmax(0, 1fr))", n),
+ ));
+ }
+ }
+
+ // Col span
+ if let Some(rest) = class.strip_prefix("col-span-") {
+ if let Ok(n) = rest.parse::() {
+ return Some((
+ "grid-column".to_string(),
+ format!("span {} / span {}", n, n),
+ ));
+ }
+ }
+
+ // Col start
+ if let Some(rest) = class.strip_prefix("col-start-") {
+ return Some(("grid-column-start".to_string(), rest.to_string()));
+ }
+
+ // Col end
+ if let Some(rest) = class.strip_prefix("col-end-") {
+ return Some(("grid-column-end".to_string(), rest.to_string()));
+ }
+
+ // Row span
+ if let Some(rest) = class.strip_prefix("row-span-") {
+ if let Ok(n) = rest.parse::() {
+ return Some(("grid-row".to_string(), format!("span {} / span {}", n, n)));
+ }
+ }
+
+ // Row start
+ if let Some(rest) = class.strip_prefix("row-start-") {
+ return Some(("grid-row-start".to_string(), rest.to_string()));
+ }
+
+ // Row end
+ if let Some(rest) = class.strip_prefix("row-end-") {
+ return Some(("grid-row-end".to_string(), rest.to_string()));
+ }
+
+ // Gap
+ if let Some(rest) = class.strip_prefix("gap-x-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("column-gap".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("gap-y-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("row-gap".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("gap-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("gap".to_string(), value.to_string()));
+ }
+ }
+
+ None
+ }
+ }
+}
+
+/// Parse spacing utilities (padding, margin, space)
+fn parse_spacing_utility(class: &str, _is_negative: bool) -> Option<(String, String)> {
+ // Note: is_negative is handled at a higher level in TailwindClass::to_static_style()
+ // We don't apply the negative sign here to avoid double-negation
+
+ // Padding
+ if let Some(rest) = class.strip_prefix("px-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-inline".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("py-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-block".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("pt-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-top".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("pr-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-right".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("pb-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-bottom".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("pl-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-left".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("ps-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-inline-start".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("pe-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding-inline-end".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("p-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("padding".to_string(), value.to_string()));
+ }
+ }
+
+ // Margin
+ if let Some(rest) = class.strip_prefix("mx-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-inline".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("my-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-block".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("mt-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-top".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("mr-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-right".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("mb-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-bottom".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("ml-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-left".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("ms-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-inline-start".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("me-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin-inline-end".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("m-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("margin".to_string(), value.to_string()));
+ }
+ }
+
+ // Space between
+ if let Some(rest) = class.strip_prefix("space-x-") {
+ if rest == "reverse" {
+ return Some(("--tw-space-x-reverse".to_string(), "1".to_string()));
+ }
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("column-gap".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("space-y-") {
+ if rest == "reverse" {
+ return Some(("--tw-space-y-reverse".to_string(), "1".to_string()));
+ }
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("row-gap".to_string(), value.to_string()));
+ }
+ }
+
+ None
+}
+
+/// Parse sizing utilities (width, height, min/max)
+fn parse_sizing_utility(class: &str) -> Option<(String, String)> {
+ // Width
+ if let Some(rest) = class.strip_prefix("w-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("width".to_string(), value.to_string()));
+ }
+ }
+
+ // Min width
+ if let Some(rest) = class.strip_prefix("min-w-") {
+ let value = match rest {
+ "0" => "0px".to_string(),
+ "full" => "100%".to_string(),
+ "min" => "min-content".to_string(),
+ "max" => "max-content".to_string(),
+ "fit" => "fit-content".to_string(),
+ _ => {
+ if let Some(&v) = SPACING_SCALE.get(rest) {
+ v.to_string()
+ } else {
+ return None;
+ }
+ }
+ };
+ return Some(("min-width".to_string(), value));
+ }
+
+ // Max width
+ if let Some(rest) = class.strip_prefix("max-w-") {
+ let value = match rest {
+ "none" => "none".to_string(),
+ "0" => "0rem".to_string(),
+ "xs" => "20rem".to_string(),
+ "sm" => "24rem".to_string(),
+ "md" => "28rem".to_string(),
+ "lg" => "32rem".to_string(),
+ "xl" => "36rem".to_string(),
+ "2xl" => "42rem".to_string(),
+ "3xl" => "48rem".to_string(),
+ "4xl" => "56rem".to_string(),
+ "5xl" => "64rem".to_string(),
+ "6xl" => "72rem".to_string(),
+ "7xl" => "80rem".to_string(),
+ "full" => "100%".to_string(),
+ "min" => "min-content".to_string(),
+ "max" => "max-content".to_string(),
+ "fit" => "fit-content".to_string(),
+ "prose" => "65ch".to_string(),
+ "screen-sm" => "640px".to_string(),
+ "screen-md" => "768px".to_string(),
+ "screen-lg" => "1024px".to_string(),
+ "screen-xl" => "1280px".to_string(),
+ "screen-2xl" => "1536px".to_string(),
+ _ => {
+ if let Some(&v) = SPACING_SCALE.get(rest) {
+ v.to_string()
+ } else {
+ return None;
+ }
+ }
+ };
+ return Some(("max-width".to_string(), value));
+ }
+
+ // Height
+ if let Some(rest) = class.strip_prefix("h-") {
+ let value = match rest {
+ "screen" => "100vh".to_string(),
+ "svh" => "100svh".to_string(),
+ "lvh" => "100lvh".to_string(),
+ "dvh" => "100dvh".to_string(),
+ _ => {
+ if let Some(&v) = SPACING_SCALE.get(rest) {
+ v.to_string()
+ } else {
+ return None;
+ }
+ }
+ };
+ return Some(("height".to_string(), value));
+ }
+
+ // Min height
+ if let Some(rest) = class.strip_prefix("min-h-") {
+ let value = match rest {
+ "0" => "0px".to_string(),
+ "full" => "100%".to_string(),
+ "screen" => "100vh".to_string(),
+ "svh" => "100svh".to_string(),
+ "lvh" => "100lvh".to_string(),
+ "dvh" => "100dvh".to_string(),
+ "min" => "min-content".to_string(),
+ "max" => "max-content".to_string(),
+ "fit" => "fit-content".to_string(),
+ _ => {
+ if let Some(&v) = SPACING_SCALE.get(rest) {
+ v.to_string()
+ } else {
+ return None;
+ }
+ }
+ };
+ return Some(("min-height".to_string(), value));
+ }
+
+ // Max height
+ if let Some(rest) = class.strip_prefix("max-h-") {
+ let value = match rest {
+ "none" => "none".to_string(),
+ "full" => "100%".to_string(),
+ "screen" => "100vh".to_string(),
+ "svh" => "100svh".to_string(),
+ "lvh" => "100lvh".to_string(),
+ "dvh" => "100dvh".to_string(),
+ "min" => "min-content".to_string(),
+ "max" => "max-content".to_string(),
+ "fit" => "fit-content".to_string(),
+ _ => {
+ if let Some(&v) = SPACING_SCALE.get(rest) {
+ v.to_string()
+ } else {
+ return None;
+ }
+ }
+ };
+ return Some(("max-height".to_string(), value));
+ }
+
+ // Size (width and height)
+ if let Some(rest) = class.strip_prefix("size-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ // This should set both width and height
+ // For simplicity, we'll use the width shorthand and handle height separately
+ return Some(("width".to_string(), value.to_string()));
+ }
+ }
+
+ None
+}
+
+/// Parse typography utilities
+fn parse_typography_utility(class: &str) -> Option<(String, String)> {
+ // Font family
+ match class {
+ "font-sans" => return Some(("font-family".to_string(), "ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'".to_string())),
+ "font-serif" => return Some(("font-family".to_string(), "ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif".to_string())),
+ "font-mono" => return Some(("font-family".to_string(), "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace".to_string())),
+ _ => {}
+ }
+
+ // Font size
+ if let Some(rest) = class.strip_prefix("text-") {
+ // First check if it's a color
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("color".to_string(), color.to_string()));
+ }
+ // Then check if it's a font size
+ if let Some(&(size, _line_height)) = FONT_SIZE_SCALE.get(rest) {
+ // Return font-size (line-height would need separate handling)
+ return Some(("font-size".to_string(), size.to_string()));
+ }
+ // Text alignment
+ match rest {
+ "left" => return Some(("text-align".to_string(), "left".to_string())),
+ "center" => return Some(("text-align".to_string(), "center".to_string())),
+ "right" => return Some(("text-align".to_string(), "right".to_string())),
+ "justify" => return Some(("text-align".to_string(), "justify".to_string())),
+ "start" => return Some(("text-align".to_string(), "start".to_string())),
+ "end" => return Some(("text-align".to_string(), "end".to_string())),
+ _ => {}
+ }
+ }
+
+ // Font weight
+ if let Some(rest) = class.strip_prefix("font-") {
+ if let Some(&weight) = FONT_WEIGHT_SCALE.get(rest) {
+ return Some(("font-weight".to_string(), weight.to_string()));
+ }
+ }
+
+ // Font style
+ match class {
+ "italic" => return Some(("font-style".to_string(), "italic".to_string())),
+ "not-italic" => return Some(("font-style".to_string(), "normal".to_string())),
+ _ => {}
+ }
+
+ // Text decoration
+ match class {
+ "underline" => return Some(("text-decoration-line".to_string(), "underline".to_string())),
+ "overline" => return Some(("text-decoration-line".to_string(), "overline".to_string())),
+ "line-through" => {
+ return Some((
+ "text-decoration-line".to_string(),
+ "line-through".to_string(),
+ ));
+ }
+ "no-underline" => return Some(("text-decoration-line".to_string(), "none".to_string())),
+ _ => {}
+ }
+
+ // Text transform
+ match class {
+ "uppercase" => return Some(("text-transform".to_string(), "uppercase".to_string())),
+ "lowercase" => return Some(("text-transform".to_string(), "lowercase".to_string())),
+ "capitalize" => return Some(("text-transform".to_string(), "capitalize".to_string())),
+ "normal-case" => return Some(("text-transform".to_string(), "none".to_string())),
+ _ => {}
+ }
+
+ // Text overflow
+ match class {
+ "truncate" => {
+ return Some(("text-overflow".to_string(), "ellipsis".to_string()));
+ }
+ "text-ellipsis" => return Some(("text-overflow".to_string(), "ellipsis".to_string())),
+ "text-clip" => return Some(("text-overflow".to_string(), "clip".to_string())),
+ _ => {}
+ }
+
+ // Text wrap
+ match class {
+ "text-wrap" => return Some(("text-wrap".to_string(), "wrap".to_string())),
+ "text-nowrap" => return Some(("text-wrap".to_string(), "nowrap".to_string())),
+ "text-balance" => return Some(("text-wrap".to_string(), "balance".to_string())),
+ "text-pretty" => return Some(("text-wrap".to_string(), "pretty".to_string())),
+ _ => {}
+ }
+
+ // Whitespace
+ if let Some(rest) = class.strip_prefix("whitespace-") {
+ return Some(("white-space".to_string(), rest.to_string()));
+ }
+
+ // Word break
+ match class {
+ "break-normal" => return Some(("word-break".to_string(), "normal".to_string())),
+ "break-words" => return Some(("overflow-wrap".to_string(), "break-word".to_string())),
+ "break-all" => return Some(("word-break".to_string(), "break-all".to_string())),
+ "break-keep" => return Some(("word-break".to_string(), "keep-all".to_string())),
+ _ => {}
+ }
+
+ // Hyphens
+ if let Some(rest) = class.strip_prefix("hyphens-") {
+ return Some(("hyphens".to_string(), rest.to_string()));
+ }
+
+ // Letter spacing
+ if let Some(rest) = class.strip_prefix("tracking-") {
+ let value = match rest {
+ "tighter" => "-0.05em".to_string(),
+ "tight" => "-0.025em".to_string(),
+ "normal" => "0em".to_string(),
+ "wide" => "0.025em".to_string(),
+ "wider" => "0.05em".to_string(),
+ "widest" => "0.1em".to_string(),
+ _ => rest.to_string(),
+ };
+ return Some(("letter-spacing".to_string(), value));
+ }
+
+ // Line height
+ if let Some(rest) = class.strip_prefix("leading-") {
+ let value = match rest {
+ "none" => "1".to_string(),
+ "tight" => "1.25".to_string(),
+ "snug" => "1.375".to_string(),
+ "normal" => "1.5".to_string(),
+ "relaxed" => "1.625".to_string(),
+ "loose" => "2".to_string(),
+ "3" => ".75rem".to_string(),
+ "4" => "1rem".to_string(),
+ "5" => "1.25rem".to_string(),
+ "6" => "1.5rem".to_string(),
+ "7" => "1.75rem".to_string(),
+ "8" => "2rem".to_string(),
+ "9" => "2.25rem".to_string(),
+ "10" => "2.5rem".to_string(),
+ _ => rest.to_string(),
+ };
+ return Some(("line-height".to_string(), value));
+ }
+
+ // List style type
+ if let Some(rest) = class.strip_prefix("list-") {
+ match rest {
+ "inside" => return Some(("list-style-position".to_string(), "inside".to_string())),
+ "outside" => return Some(("list-style-position".to_string(), "outside".to_string())),
+ "none" => return Some(("list-style-type".to_string(), "none".to_string())),
+ "disc" => return Some(("list-style-type".to_string(), "disc".to_string())),
+ "decimal" => return Some(("list-style-type".to_string(), "decimal".to_string())),
+ _ => {}
+ }
+ }
+
+ // Vertical align
+ if let Some(rest) = class.strip_prefix("align-") {
+ return Some(("vertical-align".to_string(), rest.to_string()));
+ }
+
+ // Content
+ if let Some(rest) = class.strip_prefix("content-") {
+ if rest == "none" {
+ return Some(("content".to_string(), "none".to_string()));
+ }
+ }
+
+ None
+}
+
+/// Parse background utilities
+fn parse_background_utility(class: &str) -> Option<(String, String)> {
+ // Background color
+ if let Some(rest) = class.strip_prefix("bg-") {
+ // Check if it's a color
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("background-color".to_string(), color.to_string()));
+ }
+ // Background attachment
+ match rest {
+ "fixed" => return Some(("background-attachment".to_string(), "fixed".to_string())),
+ "local" => return Some(("background-attachment".to_string(), "local".to_string())),
+ "scroll" => return Some(("background-attachment".to_string(), "scroll".to_string())),
+ // Background clip
+ "clip-border" => {
+ return Some(("background-clip".to_string(), "border-box".to_string()));
+ }
+ "clip-padding" => {
+ return Some(("background-clip".to_string(), "padding-box".to_string()));
+ }
+ "clip-content" => {
+ return Some(("background-clip".to_string(), "content-box".to_string()));
+ }
+ "clip-text" => return Some(("background-clip".to_string(), "text".to_string())),
+ // Background origin
+ "origin-border" => {
+ return Some(("background-origin".to_string(), "border-box".to_string()));
+ }
+ "origin-padding" => {
+ return Some(("background-origin".to_string(), "padding-box".to_string()));
+ }
+ "origin-content" => {
+ return Some(("background-origin".to_string(), "content-box".to_string()));
+ }
+ // Background position
+ "bottom" => return Some(("background-position".to_string(), "bottom".to_string())),
+ "center" => return Some(("background-position".to_string(), "center".to_string())),
+ "left" => return Some(("background-position".to_string(), "left".to_string())),
+ "left-bottom" => {
+ return Some(("background-position".to_string(), "left bottom".to_string()));
+ }
+ "left-top" => return Some(("background-position".to_string(), "left top".to_string())),
+ "right" => return Some(("background-position".to_string(), "right".to_string())),
+ "right-bottom" => {
+ return Some((
+ "background-position".to_string(),
+ "right bottom".to_string(),
+ ));
+ }
+ "right-top" => {
+ return Some(("background-position".to_string(), "right top".to_string()));
+ }
+ "top" => return Some(("background-position".to_string(), "top".to_string())),
+ // Background repeat
+ "repeat" => return Some(("background-repeat".to_string(), "repeat".to_string())),
+ "no-repeat" => return Some(("background-repeat".to_string(), "no-repeat".to_string())),
+ "repeat-x" => return Some(("background-repeat".to_string(), "repeat-x".to_string())),
+ "repeat-y" => return Some(("background-repeat".to_string(), "repeat-y".to_string())),
+ "repeat-round" => return Some(("background-repeat".to_string(), "round".to_string())),
+ "repeat-space" => return Some(("background-repeat".to_string(), "space".to_string())),
+ // Background size
+ "auto" => return Some(("background-size".to_string(), "auto".to_string())),
+ "cover" => return Some(("background-size".to_string(), "cover".to_string())),
+ "contain" => return Some(("background-size".to_string(), "contain".to_string())),
+ // Gradients
+ "none" => return Some(("background-image".to_string(), "none".to_string())),
+ _ => {}
+ }
+
+ // Gradient directions
+ if let Some(dir) = rest.strip_prefix("gradient-to-") {
+ let direction = match dir {
+ "t" => "to top".to_string(),
+ "tr" => "to top right".to_string(),
+ "r" => "to right".to_string(),
+ "br" => "to bottom right".to_string(),
+ "b" => "to bottom".to_string(),
+ "bl" => "to bottom left".to_string(),
+ "l" => "to left".to_string(),
+ "tl" => "to top left".to_string(),
+ _ => return None,
+ };
+ return Some((
+ "background-image".to_string(),
+ format!("linear-gradient({}, var(--tw-gradient-stops))", direction),
+ ));
+ }
+ }
+
+ // Gradient color stops
+ if let Some(rest) = class.strip_prefix("from-") {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-gradient-from".to_string(), color.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("via-") {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-gradient-via".to_string(), color.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("to-") {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-gradient-to".to_string(), color.to_string()));
+ }
+ }
+
+ None
+}
+
+/// Parse border utilities
+fn parse_border_utility(class: &str) -> Option<(String, String)> {
+ // Border radius
+ if let Some(rest) = class.strip_prefix("rounded-") {
+ // Specific corners
+ if let Some(corner) = rest.strip_prefix("t-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-top-left-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("r-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-top-right-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("b-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-bottom-right-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("l-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-bottom-left-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("tl-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-top-left-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("tr-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-top-right-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("br-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-bottom-right-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(corner) = rest.strip_prefix("bl-") {
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(corner) {
+ return Some(("border-bottom-left-radius".to_string(), value.to_string()));
+ }
+ }
+ if let Some(&value) = BORDER_RADIUS_SCALE.get(rest) {
+ return Some(("border-radius".to_string(), value.to_string()));
+ }
+ }
+
+ // Standalone "rounded" (without suffix) - note: "rounded-*" variants are handled
+ // via BORDER_RADIUS_SCALE lookup above in the strip_prefix("rounded-") branch
+ if class == "rounded" {
+ return Some(("border-radius".to_string(), "0.25rem".to_string()));
+ }
+
+ // Border width
+ if let Some(rest) = class.strip_prefix("border-") {
+ // Border color
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("border-color".to_string(), color.to_string()));
+ }
+
+ // Border width per side
+ if let Some(width) = rest.strip_prefix("t-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-top-width".to_string(), value.to_string()));
+ }
+ }
+ if let Some(width) = rest.strip_prefix("r-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-right-width".to_string(), value.to_string()));
+ }
+ }
+ if let Some(width) = rest.strip_prefix("b-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-bottom-width".to_string(), value.to_string()));
+ }
+ }
+ if let Some(width) = rest.strip_prefix("l-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-left-width".to_string(), value.to_string()));
+ }
+ }
+ if let Some(width) = rest.strip_prefix("x-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-inline-width".to_string(), value.to_string()));
+ }
+ }
+ if let Some(width) = rest.strip_prefix("y-") {
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(width) {
+ return Some(("border-block-width".to_string(), value.to_string()));
+ }
+ }
+
+ // Border width
+ if let Some(&value) = BORDER_WIDTH_SCALE.get(rest) {
+ return Some(("border-width".to_string(), value.to_string()));
+ }
+
+ // Border style
+ match rest {
+ "solid" => return Some(("border-style".to_string(), "solid".to_string())),
+ "dashed" => return Some(("border-style".to_string(), "dashed".to_string())),
+ "dotted" => return Some(("border-style".to_string(), "dotted".to_string())),
+ "double" => return Some(("border-style".to_string(), "double".to_string())),
+ "hidden" => return Some(("border-style".to_string(), "hidden".to_string())),
+ "none" => return Some(("border-style".to_string(), "none".to_string())),
+ _ => {}
+ }
+
+ // Border collapse (for tables)
+ if rest == "collapse" {
+ return Some(("border-collapse".to_string(), "collapse".to_string()));
+ }
+ if rest == "separate" {
+ return Some(("border-collapse".to_string(), "separate".to_string()));
+ }
+ }
+
+ // Standalone "border" (without suffix) - note: "border-0/2/4/8" variants are handled
+ // via BORDER_WIDTH_SCALE lookup above in the strip_prefix("border-") branch
+ if class == "border" {
+ return Some(("border-width".to_string(), "1px".to_string()));
+ }
+
+ // Outline
+ if let Some(rest) = class.strip_prefix("outline-") {
+ match rest {
+ "none" => {
+ return Some(("outline".to_string(), "2px solid transparent".to_string()));
+ }
+ "0" => return Some(("outline-width".to_string(), "0px".to_string())),
+ "1" => return Some(("outline-width".to_string(), "1px".to_string())),
+ "2" => return Some(("outline-width".to_string(), "2px".to_string())),
+ "4" => return Some(("outline-width".to_string(), "4px".to_string())),
+ "8" => return Some(("outline-width".to_string(), "8px".to_string())),
+ "dashed" => return Some(("outline-style".to_string(), "dashed".to_string())),
+ "dotted" => return Some(("outline-style".to_string(), "dotted".to_string())),
+ "double" => return Some(("outline-style".to_string(), "double".to_string())),
+ _ => {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("outline-color".to_string(), color.to_string()));
+ }
+ }
+ }
+ }
+ if class == "outline" {
+ return Some(("outline-style".to_string(), "solid".to_string()));
+ }
+
+ // Ring
+ if let Some(rest) = class.strip_prefix("ring-") {
+ match rest {
+ "0" => {
+ return Some((
+ "--tw-ring-offset-shadow".to_string(),
+ "0 0 #0000".to_string(),
+ ));
+ }
+ "1" => {
+ return Some((
+ "box-shadow".to_string(),
+ "0 0 0 1px var(--tw-ring-color)".to_string(),
+ ));
+ }
+ "2" => {
+ return Some((
+ "box-shadow".to_string(),
+ "0 0 0 2px var(--tw-ring-color)".to_string(),
+ ));
+ }
+ "4" => {
+ return Some((
+ "box-shadow".to_string(),
+ "0 0 0 4px var(--tw-ring-color)".to_string(),
+ ));
+ }
+ "8" => {
+ return Some((
+ "box-shadow".to_string(),
+ "0 0 0 8px var(--tw-ring-color)".to_string(),
+ ));
+ }
+ "inset" => return Some(("--tw-ring-inset".to_string(), "inset".to_string())),
+ _ => {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-ring-color".to_string(), color.to_string()));
+ }
+ }
+ }
+ }
+ if class == "ring" {
+ return Some((
+ "box-shadow".to_string(),
+ "0 0 0 3px var(--tw-ring-color)".to_string(),
+ ));
+ }
+
+ // Divide
+ if let Some(rest) = class.strip_prefix("divide-") {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-divide-color".to_string(), color.to_string()));
+ }
+ match rest {
+ "x" => return Some(("--tw-divide-x-reverse".to_string(), "0".to_string())),
+ "x-0" => return Some(("border-inline-width".to_string(), "0px".to_string())),
+ "x-2" => return Some(("border-inline-width".to_string(), "2px".to_string())),
+ "x-4" => return Some(("border-inline-width".to_string(), "4px".to_string())),
+ "x-8" => return Some(("border-inline-width".to_string(), "8px".to_string())),
+ "x-reverse" => return Some(("--tw-divide-x-reverse".to_string(), "1".to_string())),
+ "y" => return Some(("--tw-divide-y-reverse".to_string(), "0".to_string())),
+ "y-0" => return Some(("border-block-width".to_string(), "0px".to_string())),
+ "y-2" => return Some(("border-block-width".to_string(), "2px".to_string())),
+ "y-4" => return Some(("border-block-width".to_string(), "4px".to_string())),
+ "y-8" => return Some(("border-block-width".to_string(), "8px".to_string())),
+ "y-reverse" => return Some(("--tw-divide-y-reverse".to_string(), "1".to_string())),
+ "solid" => return Some(("border-style".to_string(), "solid".to_string())),
+ "dashed" => return Some(("border-style".to_string(), "dashed".to_string())),
+ "dotted" => return Some(("border-style".to_string(), "dotted".to_string())),
+ "double" => return Some(("border-style".to_string(), "double".to_string())),
+ "none" => return Some(("border-style".to_string(), "none".to_string())),
+ _ => {}
+ }
+ }
+
+ None
+}
+
+/// Parse effects utilities (shadow, opacity, mix-blend, etc.)
+fn parse_effects_utility(class: &str) -> Option<(String, String)> {
+ // Box shadow
+ if let Some(rest) = class.strip_prefix("shadow-") {
+ if let Some(&value) = BOX_SHADOW_SCALE.get(rest) {
+ return Some(("box-shadow".to_string(), value.to_string()));
+ }
+ // Shadow color
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("--tw-shadow-color".to_string(), color.to_string()));
+ }
+ }
+ if class == "shadow" {
+ return Some((
+ "box-shadow".to_string(),
+ "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)".to_string(),
+ ));
+ }
+
+ // Opacity
+ if let Some(rest) = class.strip_prefix("opacity-") {
+ if let Some(&value) = OPACITY_SCALE.get(rest) {
+ return Some(("opacity".to_string(), value.to_string()));
+ }
+ }
+
+ // Mix blend mode
+ if let Some(rest) = class.strip_prefix("mix-blend-") {
+ return Some(("mix-blend-mode".to_string(), rest.to_string()));
+ }
+
+ // Background blend mode
+ if let Some(rest) = class.strip_prefix("bg-blend-") {
+ return Some(("background-blend-mode".to_string(), rest.to_string()));
+ }
+
+ None
+}
+
+/// Parse filter utilities (blur, brightness, contrast, etc.)
+fn parse_filter_utility(class: &str) -> Option<(String, String)> {
+ // Blur
+ if let Some(rest) = class.strip_prefix("blur-") {
+ let value = match rest {
+ "none" => "0".to_string(),
+ "sm" => "4px".to_string(),
+ "md" => "12px".to_string(),
+ "lg" => "16px".to_string(),
+ "xl" => "24px".to_string(),
+ "2xl" => "40px".to_string(),
+ "3xl" => "64px".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), format!("blur({})", value)));
+ }
+ if class == "blur" {
+ return Some(("filter".to_string(), "blur(8px)".to_string()));
+ }
+
+ // Brightness
+ if let Some(rest) = class.strip_prefix("brightness-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "75" => ".75".to_string(),
+ "90" => ".9".to_string(),
+ "95" => ".95".to_string(),
+ "100" => "1".to_string(),
+ "105" => "1.05".to_string(),
+ "110" => "1.1".to_string(),
+ "125" => "1.25".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), format!("brightness({})", value)));
+ }
+
+ // Contrast
+ if let Some(rest) = class.strip_prefix("contrast-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "75" => ".75".to_string(),
+ "100" => "1".to_string(),
+ "125" => "1.25".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), format!("contrast({})", value)));
+ }
+
+ // Drop shadow
+ if let Some(rest) = class.strip_prefix("drop-shadow-") {
+ let value = match rest {
+ "sm" => "drop-shadow(0 1px 1px rgb(0 0 0 / 0.05))".to_string(),
+ "md" => "drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))".to_string(),
+ "lg" => "drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))".to_string(),
+ "xl" => "drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08))".to_string(),
+ "2xl" => "drop-shadow(0 25px 25px rgb(0 0 0 / 0.15))".to_string(),
+ "none" => "drop-shadow(0 0 #0000)".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), value));
+ }
+ if class == "drop-shadow" {
+ return Some((
+ "filter".to_string(),
+ "drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06))"
+ .to_string(),
+ ));
+ }
+
+ // Grayscale
+ if class == "grayscale" {
+ return Some(("filter".to_string(), "grayscale(100%)".to_string()));
+ }
+ if class == "grayscale-0" {
+ return Some(("filter".to_string(), "grayscale(0)".to_string()));
+ }
+
+ // Hue rotate
+ if let Some(rest) = class.strip_prefix("hue-rotate-") {
+ let value = match rest {
+ "0" => "0deg".to_string(),
+ "15" => "15deg".to_string(),
+ "30" => "30deg".to_string(),
+ "60" => "60deg".to_string(),
+ "90" => "90deg".to_string(),
+ "180" => "180deg".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), format!("hue-rotate({})", value)));
+ }
+
+ // Invert
+ if class == "invert" {
+ return Some(("filter".to_string(), "invert(100%)".to_string()));
+ }
+ if class == "invert-0" {
+ return Some(("filter".to_string(), "invert(0)".to_string()));
+ }
+
+ // Saturate
+ if let Some(rest) = class.strip_prefix("saturate-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "100" => "1".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some(("filter".to_string(), format!("saturate({})", value)));
+ }
+
+ // Sepia
+ if class == "sepia" {
+ return Some(("filter".to_string(), "sepia(100%)".to_string()));
+ }
+ if class == "sepia-0" {
+ return Some(("filter".to_string(), "sepia(0)".to_string()));
+ }
+
+ // Backdrop filters
+ if let Some(rest) = class.strip_prefix("backdrop-blur-") {
+ let value = match rest {
+ "none" => "0".to_string(),
+ "sm" => "4px".to_string(),
+ "md" => "12px".to_string(),
+ "lg" => "16px".to_string(),
+ "xl" => "24px".to_string(),
+ "2xl" => "40px".to_string(),
+ "3xl" => "64px".to_string(),
+ _ => return None,
+ };
+ return Some(("backdrop-filter".to_string(), format!("blur({})", value)));
+ }
+ if class == "backdrop-blur" {
+ return Some(("backdrop-filter".to_string(), "blur(8px)".to_string()));
+ }
+
+ if let Some(rest) = class.strip_prefix("backdrop-brightness-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "75" => ".75".to_string(),
+ "90" => ".9".to_string(),
+ "95" => ".95".to_string(),
+ "100" => "1".to_string(),
+ "105" => "1.05".to_string(),
+ "110" => "1.1".to_string(),
+ "125" => "1.25".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some((
+ "backdrop-filter".to_string(),
+ format!("brightness({})", value),
+ ));
+ }
+
+ if let Some(rest) = class.strip_prefix("backdrop-contrast-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "75" => ".75".to_string(),
+ "100" => "1".to_string(),
+ "125" => "1.25".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some((
+ "backdrop-filter".to_string(),
+ format!("contrast({})", value),
+ ));
+ }
+
+ if class == "backdrop-grayscale" {
+ return Some(("backdrop-filter".to_string(), "grayscale(100%)".to_string()));
+ }
+ if class == "backdrop-grayscale-0" {
+ return Some(("backdrop-filter".to_string(), "grayscale(0)".to_string()));
+ }
+
+ if class == "backdrop-invert" {
+ return Some(("backdrop-filter".to_string(), "invert(100%)".to_string()));
+ }
+ if class == "backdrop-invert-0" {
+ return Some(("backdrop-filter".to_string(), "invert(0)".to_string()));
+ }
+
+ if let Some(rest) = class.strip_prefix("backdrop-opacity-") {
+ if let Some(&value) = OPACITY_SCALE.get(rest) {
+ return Some(("backdrop-filter".to_string(), format!("opacity({})", value)));
+ }
+ }
+
+ if let Some(rest) = class.strip_prefix("backdrop-saturate-") {
+ let value = match rest {
+ "0" => "0".to_string(),
+ "50" => ".5".to_string(),
+ "100" => "1".to_string(),
+ "150" => "1.5".to_string(),
+ "200" => "2".to_string(),
+ _ => return None,
+ };
+ return Some((
+ "backdrop-filter".to_string(),
+ format!("saturate({})", value),
+ ));
+ }
+
+ if class == "backdrop-sepia" {
+ return Some(("backdrop-filter".to_string(), "sepia(100%)".to_string()));
+ }
+ if class == "backdrop-sepia-0" {
+ return Some(("backdrop-filter".to_string(), "sepia(0)".to_string()));
+ }
+
+ None
+}
+
+/// Parse transition and animation utilities
+fn parse_transition_utility(class: &str) -> Option<(String, String)> {
+ // Transition
+ match class {
+ "transition-none" => return Some(("transition-property".to_string(), "none".to_string())),
+ "transition-all" => return Some(("transition-property".to_string(), "all".to_string())),
+ "transition" => return Some(("transition-property".to_string(), "color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter".to_string())),
+ "transition-colors" => return Some(("transition-property".to_string(), "color, background-color, border-color, text-decoration-color, fill, stroke".to_string())),
+ "transition-opacity" => return Some(("transition-property".to_string(), "opacity".to_string())),
+ "transition-shadow" => return Some(("transition-property".to_string(), "box-shadow".to_string())),
+ "transition-transform" => return Some(("transition-property".to_string(), "transform".to_string())),
+ _ => {}
+ }
+
+ // Duration
+ if let Some(rest) = class.strip_prefix("duration-") {
+ if let Some(&value) = DURATION_SCALE.get(rest) {
+ return Some(("transition-duration".to_string(), value.to_string()));
+ }
+ }
+
+ // Ease (timing function)
+ if let Some(rest) = class.strip_prefix("ease-") {
+ if let Some(&value) = EASE_SCALE.get(rest) {
+ return Some(("transition-timing-function".to_string(), value.to_string()));
+ }
+ }
+
+ // Delay
+ if let Some(rest) = class.strip_prefix("delay-") {
+ if let Some(&value) = DURATION_SCALE.get(rest) {
+ return Some(("transition-delay".to_string(), value.to_string()));
+ }
+ }
+
+ // Animation
+ match class {
+ "animate-none" => return Some(("animation".to_string(), "none".to_string())),
+ "animate-spin" => {
+ return Some((
+ "animation".to_string(),
+ "spin 1s linear infinite".to_string(),
+ ));
+ }
+ "animate-ping" => {
+ return Some((
+ "animation".to_string(),
+ "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite".to_string(),
+ ));
+ }
+ "animate-pulse" => {
+ return Some((
+ "animation".to_string(),
+ "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite".to_string(),
+ ));
+ }
+ "animate-bounce" => {
+ return Some(("animation".to_string(), "bounce 1s infinite".to_string()));
+ }
+ _ => {}
+ }
+
+ None
+}
+
+/// Parse transform utilities (scale, rotate, translate, skew)
+fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, String)> {
+ // Scale
+ if let Some(rest) = class.strip_prefix("scale-x-") {
+ let value = parse_scale_value(rest)?;
+ return Some(("transform".to_string(), format!("scaleX({})", value)));
+ }
+ if let Some(rest) = class.strip_prefix("scale-y-") {
+ let value = parse_scale_value(rest)?;
+ return Some(("transform".to_string(), format!("scaleY({})", value)));
+ }
+ if let Some(rest) = class.strip_prefix("scale-") {
+ let value = parse_scale_value(rest)?;
+ return Some(("transform".to_string(), format!("scale({})", value)));
+ }
+
+ // Rotate
+ if let Some(rest) = class.strip_prefix("rotate-") {
+ let value = match rest {
+ "0" => "0deg".to_string(),
+ "1" => "1deg".to_string(),
+ "2" => "2deg".to_string(),
+ "3" => "3deg".to_string(),
+ "6" => "6deg".to_string(),
+ "12" => "12deg".to_string(),
+ "45" => "45deg".to_string(),
+ "90" => "90deg".to_string(),
+ "180" => "180deg".to_string(),
+ _ => return None,
+ };
+ let neg_prefix = if is_negative { "-" } else { "" };
+ return Some((
+ "transform".to_string(),
+ format!("rotate({}{})", neg_prefix, value),
+ ));
+ }
+
+ // Translate
+ if let Some(rest) = class.strip_prefix("translate-x-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ let neg_prefix = if is_negative { "-" } else { "" };
+ return Some((
+ "transform".to_string(),
+ format!("translateX({}{})", neg_prefix, value),
+ ));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("translate-y-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ let neg_prefix = if is_negative { "-" } else { "" };
+ return Some((
+ "transform".to_string(),
+ format!("translateY({}{})", neg_prefix, value),
+ ));
+ }
+ }
+
+ // Skew
+ if let Some(rest) = class.strip_prefix("skew-x-") {
+ let value = match rest {
+ "0" => "0deg".to_string(),
+ "1" => "1deg".to_string(),
+ "2" => "2deg".to_string(),
+ "3" => "3deg".to_string(),
+ "6" => "6deg".to_string(),
+ "12" => "12deg".to_string(),
+ _ => return None,
+ };
+ let neg_prefix = if is_negative { "-" } else { "" };
+ return Some((
+ "transform".to_string(),
+ format!("skewX({}{})", neg_prefix, value),
+ ));
+ }
+ if let Some(rest) = class.strip_prefix("skew-y-") {
+ let value = match rest {
+ "0" => "0deg".to_string(),
+ "1" => "1deg".to_string(),
+ "2" => "2deg".to_string(),
+ "3" => "3deg".to_string(),
+ "6" => "6deg".to_string(),
+ "12" => "12deg".to_string(),
+ _ => return None,
+ };
+ let neg_prefix = if is_negative { "-" } else { "" };
+ return Some((
+ "transform".to_string(),
+ format!("skewY({}{})", neg_prefix, value),
+ ));
+ }
+
+ // Transform origin
+ if let Some(rest) = class.strip_prefix("origin-") {
+ let value = match rest {
+ "center" => "center".to_string(),
+ "top" => "top".to_string(),
+ "top-right" => "top right".to_string(),
+ "right" => "right".to_string(),
+ "bottom-right" => "bottom right".to_string(),
+ "bottom" => "bottom".to_string(),
+ "bottom-left" => "bottom left".to_string(),
+ "left" => "left".to_string(),
+ "top-left" => "top left".to_string(),
+ _ => return None,
+ };
+ return Some(("transform-origin".to_string(), value));
+ }
+
+ None
+}
+
+/// Parse scale value (50 -> 0.5, 100 -> 1, 150 -> 1.5)
+fn parse_scale_value(s: &str) -> Option {
+ let n: u32 = s.parse().ok()?;
+ Some(n as f64 / 100.0)
+}
+
+/// Parse interactivity utilities (cursor, pointer-events, resize, etc.)
+fn parse_interactivity_utility(class: &str) -> Option<(String, String)> {
+ // Accent color
+ if let Some(rest) = class.strip_prefix("accent-") {
+ if rest == "auto" {
+ return Some(("accent-color".to_string(), "auto".to_string()));
+ }
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("accent-color".to_string(), color.to_string()));
+ }
+ }
+
+ // Appearance
+ match class {
+ "appearance-none" => return Some(("appearance".to_string(), "none".to_string())),
+ "appearance-auto" => return Some(("appearance".to_string(), "auto".to_string())),
+ _ => {}
+ }
+
+ // Cursor
+ if let Some(rest) = class.strip_prefix("cursor-") {
+ return Some(("cursor".to_string(), rest.to_string()));
+ }
+
+ // Caret color
+ if let Some(rest) = class.strip_prefix("caret-") {
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("caret-color".to_string(), color.to_string()));
+ }
+ }
+
+ // Pointer events
+ if let Some(rest) = class.strip_prefix("pointer-events-") {
+ return Some(("pointer-events".to_string(), rest.to_string()));
+ }
+
+ // Resize
+ match class {
+ "resize-none" => return Some(("resize".to_string(), "none".to_string())),
+ "resize-y" => return Some(("resize".to_string(), "vertical".to_string())),
+ "resize-x" => return Some(("resize".to_string(), "horizontal".to_string())),
+ "resize" => return Some(("resize".to_string(), "both".to_string())),
+ _ => {}
+ }
+
+ // Scroll behavior
+ if let Some(rest) = class.strip_prefix("scroll-") {
+ match rest {
+ "auto" => return Some(("scroll-behavior".to_string(), "auto".to_string())),
+ "smooth" => return Some(("scroll-behavior".to_string(), "smooth".to_string())),
+ _ => {}
+ }
+ }
+
+ // Scroll margin/padding
+ if let Some(rest) = class.strip_prefix("scroll-m-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("scroll-margin".to_string(), value.to_string()));
+ }
+ }
+ if let Some(rest) = class.strip_prefix("scroll-p-") {
+ if let Some(&value) = SPACING_SCALE.get(rest) {
+ return Some(("scroll-padding".to_string(), value.to_string()));
+ }
+ }
+
+ // Scroll snap
+ if let Some(rest) = class.strip_prefix("snap-") {
+ match rest {
+ "start" => return Some(("scroll-snap-align".to_string(), "start".to_string())),
+ "end" => return Some(("scroll-snap-align".to_string(), "end".to_string())),
+ "center" => return Some(("scroll-snap-align".to_string(), "center".to_string())),
+ "align-none" => return Some(("scroll-snap-align".to_string(), "none".to_string())),
+ "none" => return Some(("scroll-snap-type".to_string(), "none".to_string())),
+ "x" => {
+ return Some((
+ "scroll-snap-type".to_string(),
+ "x var(--tw-scroll-snap-strictness)".to_string(),
+ ));
+ }
+ "y" => {
+ return Some((
+ "scroll-snap-type".to_string(),
+ "y var(--tw-scroll-snap-strictness)".to_string(),
+ ));
+ }
+ "both" => {
+ return Some((
+ "scroll-snap-type".to_string(),
+ "both var(--tw-scroll-snap-strictness)".to_string(),
+ ));
+ }
+ "mandatory" => {
+ return Some((
+ "--tw-scroll-snap-strictness".to_string(),
+ "mandatory".to_string(),
+ ));
+ }
+ "proximity" => {
+ return Some((
+ "--tw-scroll-snap-strictness".to_string(),
+ "proximity".to_string(),
+ ));
+ }
+ "normal" => return Some(("scroll-snap-stop".to_string(), "normal".to_string())),
+ "always" => return Some(("scroll-snap-stop".to_string(), "always".to_string())),
+ _ => {}
+ }
+ }
+
+ // Touch action
+ if let Some(rest) = class.strip_prefix("touch-") {
+ return Some(("touch-action".to_string(), rest.to_string()));
+ }
+
+ // User select
+ if let Some(rest) = class.strip_prefix("select-") {
+ return Some(("user-select".to_string(), rest.to_string()));
+ }
+
+ // Will change
+ if let Some(rest) = class.strip_prefix("will-change-") {
+ let value = match rest {
+ "auto" => "auto".to_string(),
+ "scroll" => "scroll-position".to_string(),
+ "contents" => "contents".to_string(),
+ "transform" => "transform".to_string(),
+ _ => return None,
+ };
+ return Some(("will-change".to_string(), value));
+ }
+
+ None
+}
+
+/// Parse SVG utilities (fill, stroke)
+fn parse_svg_utility(class: &str) -> Option<(String, String)> {
+ // Fill
+ if let Some(rest) = class.strip_prefix("fill-") {
+ if rest == "none" {
+ return Some(("fill".to_string(), "none".to_string()));
+ }
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("fill".to_string(), color.to_string()));
+ }
+ }
+
+ // Stroke
+ if let Some(rest) = class.strip_prefix("stroke-") {
+ if rest == "none" {
+ return Some(("stroke".to_string(), "none".to_string()));
+ }
+ // Stroke width
+ match rest {
+ "0" => return Some(("stroke-width".to_string(), "0".to_string())),
+ "1" => return Some(("stroke-width".to_string(), "1".to_string())),
+ "2" => return Some(("stroke-width".to_string(), "2".to_string())),
+ _ => {}
+ }
+ // Stroke color
+ if let Some(&color) = TAILWIND_COLORS.get(rest) {
+ return Some(("stroke".to_string(), color.to_string()));
+ }
+ }
+
+ None
+}
+
+/// Parse accessibility utilities (screen reader only)
+fn parse_accessibility_utility(class: &str) -> Option<(String, String)> {
+ match class {
+ "sr-only" => {
+ // This utility requires multiple CSS properties
+ // We'll return the most important one
+ Some(("position".to_string(), "absolute".to_string()))
+ }
+ "not-sr-only" => Some(("position".to_string(), "static".to_string())),
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use css::class_map::reset_class_map;
+ use css::file_map::reset_file_map;
+ use insta::assert_debug_snapshot;
+ use rstest::rstest;
+ use serial_test::serial;
+ use std::collections::BTreeSet;
+
+ // Helper to sort styles for consistent snapshots
+ fn sort_styles(styles: Vec) -> BTreeSet {
+ styles.into_iter().collect()
+ }
+
+ #[test]
+ fn test_has_tailwind_classes_basic() {
+ assert!(has_tailwind_classes("bg-red-500 text-white"));
+ assert!(has_tailwind_classes("p-4 m-2"));
+ assert!(has_tailwind_classes("flex items-center"));
+ assert!(!has_tailwind_classes("my-custom-class"));
+ assert!(!has_tailwind_classes(""));
+ }
+
+ #[test]
+ fn test_has_tailwind_classes_with_responsive() {
+ assert!(has_tailwind_classes("sm:bg-blue-500"));
+ assert!(has_tailwind_classes("md:flex lg:hidden"));
+ assert!(has_tailwind_classes("hover:bg-red-500"));
+ }
+
+ #[test]
+ fn test_has_tailwind_classes_with_arbitrary() {
+ assert!(has_tailwind_classes("w-[100px]"));
+ assert!(has_tailwind_classes("text-[#ff0000]"));
+ assert!(has_tailwind_classes("p-[calc(100%-20px)]"));
+ }
+
+ #[rstest]
+ #[case("bg-red-500", "background-color", "#ef4444")]
+ #[case("bg-blue-500", "background-color", "#3b82f6")]
+ #[case("bg-black", "background-color", "#000")]
+ #[case("bg-white", "background-color", "#fff")]
+ #[case("bg-transparent", "background-color", "transparent")]
+ #[case("text-red-500", "color", "#ef4444")]
+ #[case("text-white", "color", "#fff")]
+ fn test_parse_color_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("p-4", "padding", "1rem")]
+ #[case("p-0", "padding", "0px")]
+ #[case("p-px", "padding", "1px")]
+ #[case("p-0.5", "padding", "0.125rem")]
+ #[case("px-4", "padding-inline", "1rem")]
+ #[case("py-2", "padding-block", "0.5rem")]
+ #[case("pt-4", "padding-top", "1rem")]
+ #[case("pr-4", "padding-right", "1rem")]
+ #[case("pb-4", "padding-bottom", "1rem")]
+ #[case("pl-4", "padding-left", "1rem")]
+ #[case("m-4", "margin", "1rem")]
+ #[case("mx-auto", "margin-inline", "auto")]
+ #[case("my-4", "margin-block", "1rem")]
+ #[case("mt-4", "margin-top", "1rem")]
+ fn test_parse_spacing_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("w-full", "width", "100%")]
+ #[case("w-screen", "width", "100vw")]
+ #[case("w-auto", "width", "auto")]
+ #[case("w-1/2", "width", "50%")]
+ #[case("w-4", "width", "1rem")]
+ #[case("h-full", "height", "100%")]
+ #[case("h-screen", "height", "100vh")]
+ #[case("min-w-0", "min-width", "0px")]
+ #[case("min-w-full", "min-width", "100%")]
+ #[case("max-w-sm", "max-width", "24rem")]
+ #[case("max-w-xl", "max-width", "36rem")]
+ fn test_parse_sizing_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("flex", "display", "flex")]
+ #[case("inline-flex", "display", "inline-flex")]
+ #[case("grid", "display", "grid")]
+ #[case("block", "display", "block")]
+ #[case("hidden", "display", "none")]
+ #[case("absolute", "position", "absolute")]
+ #[case("relative", "position", "relative")]
+ #[case("fixed", "position", "fixed")]
+ #[case("sticky", "position", "sticky")]
+ fn test_parse_layout_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("flex-row", "flex-direction", "row")]
+ #[case("flex-col", "flex-direction", "column")]
+ #[case("flex-wrap", "flex-wrap", "wrap")]
+ #[case("flex-1", "flex", "1 1 0%")]
+ #[case("justify-center", "justify-content", "center")]
+ #[case("justify-between", "justify-content", "space-between")]
+ #[case("items-center", "align-items", "center")]
+ #[case("items-start", "align-items", "flex-start")]
+ #[case("gap-4", "gap", "1rem")]
+ fn test_parse_flex_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("font-bold", "font-weight", "700")]
+ #[case("font-normal", "font-weight", "400")]
+ #[case("text-sm", "font-size", "0.875rem")]
+ #[case("text-xl", "font-size", "1.25rem")]
+ #[case("text-center", "text-align", "center")]
+ #[case("italic", "font-style", "italic")]
+ #[case("underline", "text-decoration-line", "underline")]
+ #[case("uppercase", "text-transform", "uppercase")]
+ fn test_parse_typography_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("rounded", "border-radius", "0.25rem")]
+ #[case("rounded-none", "border-radius", "0px")]
+ #[case("rounded-full", "border-radius", "9999px")]
+ #[case("rounded-lg", "border-radius", "0.5rem")]
+ #[case("border", "border-width", "1px")]
+ #[case("border-2", "border-width", "2px")]
+ #[case("border-red-500", "border-color", "#ef4444")]
+ fn test_parse_border_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("opacity-50", "opacity", "0.5")]
+ #[case("opacity-100", "opacity", "1")]
+ #[case("opacity-0", "opacity", "0")]
+ fn test_parse_opacity_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("z-10", "z-index", "10")]
+ #[case("z-50", "z-index", "50")]
+ #[case("z-auto", "z-index", "auto")]
+ fn test_parse_z_index_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[test]
+ fn test_parse_responsive_prefix() {
+ let parsed = parse_single_class("sm:bg-red-500").expect("Should parse");
+ assert_eq!(parsed.responsive, 1);
+ assert_eq!(parsed.property, "background-color");
+ assert_eq!(parsed.value, "#ef4444");
+
+ let parsed = parse_single_class("md:p-4").expect("Should parse");
+ assert_eq!(parsed.responsive, 2);
+ assert_eq!(parsed.property, "padding");
+ assert_eq!(parsed.value, "1rem");
+
+ let parsed = parse_single_class("lg:flex").expect("Should parse");
+ assert_eq!(parsed.responsive, 3);
+ assert_eq!(parsed.property, "display");
+ assert_eq!(parsed.value, "flex");
+
+ let parsed = parse_single_class("xl:hidden").expect("Should parse");
+ assert_eq!(parsed.responsive, 4);
+ assert_eq!(parsed.property, "display");
+ assert_eq!(parsed.value, "none");
+
+ let parsed = parse_single_class("2xl:w-full").expect("Should parse");
+ assert_eq!(parsed.responsive, 5);
+ assert_eq!(parsed.property, "width");
+ assert_eq!(parsed.value, "100%");
+ }
+
+ #[test]
+ fn test_parse_variant_hover() {
+ let parsed = parse_single_class("hover:bg-blue-500").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::Hover);
+ assert_eq!(parsed.property, "background-color");
+ assert_eq!(parsed.value, "#3b82f6");
+ }
+
+ #[test]
+ fn test_parse_variant_focus() {
+ let parsed = parse_single_class("focus:outline-none").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::Focus);
+ }
+
+ #[test]
+ fn test_parse_variant_dark() {
+ let parsed = parse_single_class("dark:bg-gray-800").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::Dark);
+ assert_eq!(parsed.property, "background-color");
+ assert_eq!(parsed.value, "#1f2937");
+ }
+
+ #[test]
+ fn test_parse_combined_responsive_variant() {
+ let parsed = parse_single_class("sm:hover:bg-red-500").expect("Should parse");
+ assert_eq!(parsed.responsive, 1);
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::Hover);
+ assert_eq!(parsed.property, "background-color");
+ }
+
+ #[test]
+ fn test_parse_arbitrary_width() {
+ let parsed = parse_single_class("w-[100px]").expect("Should parse");
+ assert_eq!(parsed.property, "width");
+ assert_eq!(parsed.value, "100px");
+ }
+
+ #[test]
+ fn test_parse_arbitrary_color() {
+ let parsed = parse_single_class("bg-[#ff0000]").expect("Should parse");
+ assert_eq!(parsed.property, "background-color");
+ assert_eq!(parsed.value, "#ff0000");
+ }
+
+ #[test]
+ fn test_parse_arbitrary_calc() {
+ let parsed = parse_single_class("w-[calc(100%-20px)]").expect("Should parse");
+ assert_eq!(parsed.property, "width");
+ assert_eq!(parsed.value, "calc(100%-20px)");
+ }
+
+ #[test]
+ fn test_parse_negative_margin() {
+ let parsed = parse_single_class("-m-4").expect("Should parse");
+ assert_eq!(parsed.property, "margin");
+ assert_eq!(parsed.value, "1rem");
+ assert!(parsed.negative);
+
+ let static_style = parsed.to_static_style();
+ assert_eq!(static_style.value(), "-1rem");
+ }
+
+ #[test]
+ fn test_parse_negative_translate() {
+ let parsed = parse_single_class("-translate-x-4").expect("Should parse");
+ assert_eq!(parsed.property, "transform");
+ assert_eq!(parsed.value, "translateX(-1rem)");
+ }
+
+ #[test]
+ fn test_variant_to_selector() {
+ assert_eq!(
+ TailwindVariant::Hover.to_selector(),
+ StyleSelector::Selector("&:hover".to_string())
+ );
+ assert_eq!(
+ TailwindVariant::Focus.to_selector(),
+ StyleSelector::Selector("&:focus".to_string())
+ );
+ assert_eq!(
+ TailwindVariant::Dark.to_selector(),
+ StyleSelector::Selector(":root[data-theme=dark] &".to_string())
+ );
+ assert_eq!(
+ TailwindVariant::GroupHover.to_selector(),
+ StyleSelector::Selector("*[role=group]:hover &".to_string())
+ );
+ }
+
+ #[test]
+ fn test_to_static_style() {
+ let parsed = parse_single_class("bg-red-500").expect("Should parse");
+ let static_style = parsed.to_static_style();
+
+ assert_eq!(static_style.property(), "background-color");
+ // ExtractStaticStyle::new() uses optimize_value() which uppercases hex colors
+ assert_eq!(static_style.value(), "#EF4444");
+ assert_eq!(static_style.level(), 0);
+ assert!(static_style.selector().is_none());
+ }
+
+ #[test]
+ fn test_to_static_style_with_responsive() {
+ let parsed = parse_single_class("md:p-4").expect("Should parse");
+ let static_style = parsed.to_static_style();
+
+ assert_eq!(static_style.property(), "padding");
+ assert_eq!(static_style.value(), "1rem");
+ assert_eq!(static_style.level(), 2);
+ }
+
+ #[test]
+ fn test_to_static_style_with_variant() {
+ let parsed = parse_single_class("hover:bg-blue-500").expect("Should parse");
+ let static_style = parsed.to_static_style();
+
+ assert_eq!(static_style.property(), "background-color");
+ assert!(static_style.selector().is_some());
+ let selector = static_style.selector().unwrap();
+ assert_eq!(selector.to_string(), "&:hover");
+ }
+
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_basic() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles = parse_tailwind_to_styles("bg-red-500 p-4 flex", None);
+ assert_eq!(styles.len(), 3);
+
+ assert_debug_snapshot!(sort_styles(styles));
+ }
+
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_responsive() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles = parse_tailwind_to_styles("sm:bg-blue-500 md:p-8 lg:flex", None);
+ assert_eq!(styles.len(), 3);
+
+ assert_debug_snapshot!(sort_styles(styles));
+ }
+
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_variants() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles =
+ parse_tailwind_to_styles("hover:bg-blue-500 focus:outline-none dark:text-white", None);
+
+ assert_debug_snapshot!(sort_styles(styles));
+ }
+
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_complex() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles = parse_tailwind_to_styles(
+ "flex items-center justify-between p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200",
+ None,
+ );
+
+ assert_debug_snapshot!(sort_styles(styles));
+ }
+
+ #[test]
+ fn test_parse_grid_utilities() {
+ let parsed = parse_single_class("grid-cols-3").expect("Should parse");
+ assert_eq!(parsed.property, "grid-template-columns");
+ assert_eq!(parsed.value, "repeat(3, minmax(0, 1fr))");
+
+ let parsed = parse_single_class("col-span-2").expect("Should parse");
+ assert_eq!(parsed.property, "grid-column");
+ assert_eq!(parsed.value, "span 2 / span 2");
+
+ let parsed = parse_single_class("row-span-3").expect("Should parse");
+ assert_eq!(parsed.property, "grid-row");
+ assert_eq!(parsed.value, "span 3 / span 3");
+ }
+
+ #[test]
+ fn test_parse_transition_utilities() {
+ let parsed = parse_single_class("transition").expect("Should parse");
+ assert_eq!(parsed.property, "transition-property");
+
+ let parsed = parse_single_class("duration-300").expect("Should parse");
+ assert_eq!(parsed.property, "transition-duration");
+ assert_eq!(parsed.value, "300ms");
+
+ let parsed = parse_single_class("ease-in-out").expect("Should parse");
+ assert_eq!(parsed.property, "transition-timing-function");
+ }
+
+ #[test]
+ fn test_parse_transform_utilities() {
+ let parsed = parse_single_class("scale-50").expect("Should parse");
+ assert_eq!(parsed.property, "transform");
+ assert_eq!(parsed.value, "scale(0.5)");
+
+ let parsed = parse_single_class("rotate-45").expect("Should parse");
+ assert_eq!(parsed.property, "transform");
+ assert_eq!(parsed.value, "rotate(45deg)");
+
+ let parsed = parse_single_class("translate-x-4").expect("Should parse");
+ assert_eq!(parsed.property, "transform");
+ assert_eq!(parsed.value, "translateX(1rem)");
+ }
+
+ #[test]
+ fn test_parse_filter_utilities() {
+ let parsed = parse_single_class("blur").expect("Should parse");
+ assert_eq!(parsed.property, "filter");
+ assert_eq!(parsed.value, "blur(8px)");
+
+ let parsed = parse_single_class("blur-lg").expect("Should parse");
+ assert_eq!(parsed.property, "filter");
+ assert_eq!(parsed.value, "blur(16px)");
+
+ let parsed = parse_single_class("grayscale").expect("Should parse");
+ assert_eq!(parsed.property, "filter");
+ assert_eq!(parsed.value, "grayscale(100%)");
+ }
+
+ #[test]
+ fn test_parse_interactivity_utilities() {
+ let parsed = parse_single_class("cursor-pointer").expect("Should parse");
+ assert_eq!(parsed.property, "cursor");
+ assert_eq!(parsed.value, "pointer");
+
+ let parsed = parse_single_class("select-none").expect("Should parse");
+ assert_eq!(parsed.property, "user-select");
+ assert_eq!(parsed.value, "none");
+
+ let parsed = parse_single_class("pointer-events-none").expect("Should parse");
+ assert_eq!(parsed.property, "pointer-events");
+ assert_eq!(parsed.value, "none");
+ }
+
+ #[test]
+ fn test_parse_svg_utilities() {
+ let parsed = parse_single_class("fill-red-500").expect("Should parse");
+ assert_eq!(parsed.property, "fill");
+ assert_eq!(parsed.value, "#ef4444");
+
+ let parsed = parse_single_class("stroke-black").expect("Should parse");
+ assert_eq!(parsed.property, "stroke");
+ assert_eq!(parsed.value, "#000");
+
+ let parsed = parse_single_class("stroke-2").expect("Should parse");
+ assert_eq!(parsed.property, "stroke-width");
+ assert_eq!(parsed.value, "2");
+ }
+
+ #[test]
+ fn test_peer_variants() {
+ let parsed = parse_single_class("peer-hover:bg-blue-500").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::PeerHover);
+
+ let selector = parsed.variants[0].to_selector();
+ assert_eq!(selector.to_string(), ".peer:hover ~ &");
+ }
+
+ #[test]
+ fn test_group_variants() {
+ let parsed = parse_single_class("group-hover:bg-blue-500").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::GroupHover);
+
+ let selector = parsed.variants[0].to_selector();
+ assert_eq!(selector.to_string(), "*[role=group]:hover &");
+ }
+
+ #[test]
+ fn test_media_variants() {
+ let parsed = parse_single_class("print:hidden").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 1);
+ assert_eq!(parsed.variants[0], TailwindVariant::Print);
+
+ let selector = parsed.variants[0].to_selector();
+ if let StyleSelector::At { kind, query, .. } = selector {
+ assert_eq!(kind, css::style_selector::AtRuleKind::Media);
+ assert_eq!(query, "print");
+ } else {
+ panic!("Expected At selector");
+ }
+ }
+
+ #[test]
+ fn test_accessibility_utilities() {
+ let parsed = parse_single_class("sr-only").expect("Should parse");
+ assert_eq!(parsed.property, "position");
+ assert_eq!(parsed.value, "absolute");
+ }
+
+ #[test]
+ fn test_empty_string() {
+ let styles = parse_tailwind_to_styles("", None);
+ assert!(styles.is_empty());
+ }
+
+ #[test]
+ fn test_unknown_class() {
+ let result = parse_single_class("unknown-class");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_is_likely_tailwind_class_exact_matches() {
+ assert!(is_likely_tailwind_class("flex"));
+ assert!(is_likely_tailwind_class("grid"));
+ assert!(is_likely_tailwind_class("hidden"));
+ assert!(is_likely_tailwind_class("absolute"));
+ assert!(is_likely_tailwind_class("truncate"));
+ assert!(is_likely_tailwind_class("sr-only"));
+ }
+
+ // ==================== WAVE 1: TailwindVariant Tests ====================
+
+ // Wave 1.1: Pseudo-class variant selectors (lines 104-131)
+ #[rstest]
+ #[case(TailwindVariant::FocusVisible, "&:focus-visible")]
+ #[case(TailwindVariant::FocusWithin, "&:focus-within")]
+ #[case(TailwindVariant::Visited, "&:visited")]
+ #[case(TailwindVariant::Enabled, "&:enabled")]
+ #[case(TailwindVariant::Checked, "&:checked")]
+ #[case(TailwindVariant::Indeterminate, "&:indeterminate")]
+ #[case(TailwindVariant::Default, "&:default")]
+ #[case(TailwindVariant::Required, "&:required")]
+ #[case(TailwindVariant::Valid, "&:valid")]
+ #[case(TailwindVariant::Invalid, "&:invalid")]
+ #[case(TailwindVariant::InRange, "&:in-range")]
+ #[case(TailwindVariant::OutOfRange, "&:out-of-range")]
+ #[case(TailwindVariant::PlaceholderShown, "&:placeholder-shown")]
+ #[case(TailwindVariant::Autofill, "&:autofill")]
+ #[case(TailwindVariant::ReadOnly, "&:read-only")]
+ #[case(TailwindVariant::FirstChild, "&:first-child")]
+ #[case(TailwindVariant::LastChild, "&:last-child")]
+ #[case(TailwindVariant::OnlyChild, "&:only-child")]
+ #[case(TailwindVariant::OddChild, "&:nth-child(odd)")]
+ #[case(TailwindVariant::EvenChild, "&:nth-child(even)")]
+ #[case(TailwindVariant::FirstOfType, "&:first-of-type")]
+ #[case(TailwindVariant::LastOfType, "&:last-of-type")]
+ #[case(TailwindVariant::OnlyOfType, "&:only-of-type")]
+ #[case(TailwindVariant::Empty, "&:empty")]
+ #[case(TailwindVariant::Target, "&:target")]
+ #[case(TailwindVariant::Open, "&[open]")]
+ fn test_variant_to_selector_pseudo_classes(
+ #[case] variant: TailwindVariant,
+ #[case] expected: &str,
+ ) {
+ assert_eq!(
+ variant.to_selector(),
+ StyleSelector::Selector(expected.to_string())
+ );
+ }
+
+ // Wave 1.2: Pseudo-element variant selectors (lines 133-141)
+ #[rstest]
+ #[case(TailwindVariant::Placeholder, "&::placeholder")]
+ #[case(TailwindVariant::Before, "&::before")]
+ #[case(TailwindVariant::After, "&::after")]
+ #[case(TailwindVariant::Selection, "&::selection")]
+ #[case(TailwindVariant::Marker, "&::marker")]
+ #[case(TailwindVariant::FirstLetter, "&::first-letter")]
+ #[case(TailwindVariant::FirstLine, "&::first-line")]
+ #[case(TailwindVariant::Backdrop, "&::backdrop")]
+ #[case(TailwindVariant::File, "&::file-selector-button")]
+ fn test_variant_to_selector_pseudo_elements(
+ #[case] variant: TailwindVariant,
+ #[case] expected: &str,
+ ) {
+ assert_eq!(
+ variant.to_selector(),
+ StyleSelector::Selector(expected.to_string())
+ );
+ }
+
+ // Wave 1.3: Group/Peer variant selectors (lines 143-151)
+ #[rstest]
+ #[case(TailwindVariant::GroupFocus, "*[role=group]:focus &")]
+ #[case(TailwindVariant::GroupActive, "*[role=group]:active &")]
+ #[case(TailwindVariant::GroupDisabled, "*[role=group]:disabled &")]
+ #[case(TailwindVariant::PeerFocus, ".peer:focus ~ &")]
+ #[case(TailwindVariant::PeerActive, ".peer:active ~ &")]
+ #[case(TailwindVariant::PeerDisabled, ".peer:disabled ~ &")]
+ #[case(TailwindVariant::PeerChecked, ".peer:checked ~ &")]
+ #[case(TailwindVariant::PeerInvalid, ".peer:invalid ~ &")]
+ fn test_variant_to_selector_group_peer(
+ #[case] variant: TailwindVariant,
+ #[case] expected: &str,
+ ) {
+ assert_eq!(
+ variant.to_selector(),
+ StyleSelector::Selector(expected.to_string())
+ );
+ }
+
+ // Wave 1.4: Media variant selectors (lines 153-162)
+ #[rstest]
+ #[case(TailwindVariant::Screen, "screen")]
+ #[case(TailwindVariant::Portrait, "(orientation: portrait)")]
+ #[case(TailwindVariant::Landscape, "(orientation: landscape)")]
+ #[case(TailwindVariant::MotionReduce, "(prefers-reduced-motion: reduce)")]
+ #[case(TailwindVariant::MotionSafe, "(prefers-reduced-motion: no-preference)")]
+ #[case(TailwindVariant::ContrastMore, "(prefers-contrast: more)")]
+ #[case(TailwindVariant::ContrastLess, "(prefers-contrast: less)")]
+ #[case(TailwindVariant::ForcedColors, "(forced-colors: active)")]
+ fn test_variant_to_selector_media_queries(
+ #[case] variant: TailwindVariant,
+ #[case] expected_query: &str,
+ ) {
+ if let StyleSelector::At { kind, query, .. } = variant.to_selector() {
+ assert_eq!(kind, css::style_selector::AtRuleKind::Media);
+ assert_eq!(query, expected_query);
+ } else {
+ panic!("Expected At selector for {:?}", variant);
+ }
+ }
+
+ // Wave 1.4 continued: Direction variants (lines 161-162)
+ #[rstest]
+ #[case(TailwindVariant::Rtl, "[dir=rtl] &")]
+ #[case(TailwindVariant::Ltr, "[dir=ltr] &")]
+ fn test_variant_to_selector_direction(
+ #[case] variant: TailwindVariant,
+ #[case] expected: &str,
+ ) {
+ assert_eq!(
+ variant.to_selector(),
+ StyleSelector::Selector(expected.to_string())
+ );
+ }
+
+ // Wave 1.4: from_prefix tests for untested variants (lines 220-230)
+ #[rstest]
+ #[case("screen", TailwindVariant::Screen)]
+ #[case("portrait", TailwindVariant::Portrait)]
+ #[case("landscape", TailwindVariant::Landscape)]
+ #[case("motion-reduce", TailwindVariant::MotionReduce)]
+ #[case("motion-safe", TailwindVariant::MotionSafe)]
+ #[case("contrast-more", TailwindVariant::ContrastMore)]
+ #[case("contrast-less", TailwindVariant::ContrastLess)]
+ #[case("forced-colors", TailwindVariant::ForcedColors)]
+ #[case("rtl", TailwindVariant::Rtl)]
+ #[case("ltr", TailwindVariant::Ltr)]
+ fn test_from_prefix_media_variants(#[case] prefix: &str, #[case] expected: TailwindVariant) {
+ assert_eq!(TailwindVariant::from_prefix(prefix), Some(expected));
+ }
+
+ // ==================== WAVE 2: Edge Cases & Arbitrary Values ====================
+
+ // Wave 2.1: combine_selectors with At-rule (lines 292-295)
+ #[test]
+ fn test_combine_selectors_at_rule_with_hover() {
+ let parsed = parse_single_class("print:hover:bg-blue-500").expect("Should parse");
+ assert_eq!(parsed.variants.len(), 2);
+ assert_eq!(parsed.variants[0], TailwindVariant::Print);
+ assert_eq!(parsed.variants[1], TailwindVariant::Hover);
+
+ let static_style = parsed.to_static_style();
+ let selector = static_style.selector().expect("Should have selector");
+ // Should combine into At rule with nested hover selector
+ if let StyleSelector::At {
+ kind,
+ query,
+ selector: nested,
+ } = selector
+ {
+ assert_eq!(*kind, css::style_selector::AtRuleKind::Media);
+ assert_eq!(query, "print");
+ assert!(nested.is_some());
+ } else {
+ panic!("Expected At selector");
+ }
+ }
+
+ // Wave 2.2: is_valid_tailwind_value edge cases (lines 816, 825, 859, 864-866)
+ #[test]
+ fn test_has_tailwind_classes_arbitrary_syntax() {
+ // Line 816: arbitrary value syntax detection
+ assert!(has_tailwind_classes("w-[100px]"));
+ assert!(has_tailwind_classes("bg-[#ff0000]"));
+ assert!(has_tailwind_classes("p-[calc(100%-20px)]"));
+ }
+
+ #[test]
+ fn test_is_valid_tailwind_value_empty() {
+ // Line 825: empty value should return false
+ assert!(!is_valid_tailwind_value(""));
+ }
+
+ #[test]
+ fn test_is_valid_tailwind_value_size_keywords() {
+ // Line 859: size keywords
+ assert!(is_valid_tailwind_value("xs"));
+ assert!(is_valid_tailwind_value("sm"));
+ assert!(is_valid_tailwind_value("md"));
+ assert!(is_valid_tailwind_value("lg"));
+ assert!(is_valid_tailwind_value("xl"));
+ assert!(is_valid_tailwind_value("2xl"));
+ assert!(is_valid_tailwind_value("3xl"));
+ }
+
+ #[test]
+ fn test_is_valid_tailwind_value_fractions() {
+ // Lines 864-866: fraction values
+ assert!(is_valid_tailwind_value("1/2"));
+ assert!(is_valid_tailwind_value("1/3"));
+ assert!(is_valid_tailwind_value("2/3"));
+ assert!(is_valid_tailwind_value("1/4"));
+ assert!(is_valid_tailwind_value("3/4"));
+ }
+
+ // Wave 2.3: parse_arbitrary_value extended tests (lines 1057-1084)
+ #[rstest]
+ #[case("border-[#ff0000]", "border-color", "#ff0000")]
+ #[case("opacity-[0.5]", "opacity", "0.5")]
+ #[case("z-[999]", "z-index", "999")]
+ #[case("font-[Arial]", "font-family", "Arial")]
+ #[case("tracking-[0.2em]", "letter-spacing", "0.2em")]
+ #[case("leading-[2]", "line-height", "2")]
+ #[case("duration-[500ms]", "transition-duration", "500ms")]
+ #[case("delay-[200ms]", "transition-delay", "200ms")]
+ #[case("aspect-[16/9]", "aspect-ratio", "16/9")]
+ #[case("columns-[3]", "columns", "3")]
+ #[case("basis-[200px]", "flex-basis", "200px")]
+ fn test_parse_arbitrary_values_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("scale-[1.5]", "transform", "scale(1.5)")]
+ #[case("rotate-[30deg]", "transform", "rotate(30deg)")]
+ #[case("translate-x-[50px]", "transform", "translateX(50px)")]
+ #[case("translate-y-[50px]", "transform", "translateY(50px)")]
+ #[case("skew-x-[10deg]", "transform", "skewX(10deg)")]
+ #[case("skew-y-[10deg]", "transform", "skewY(10deg)")]
+ fn test_parse_arbitrary_transform_values(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case(
+ "grid-cols-[200px_1fr]",
+ "grid-template-columns",
+ "repeat(200px 1fr, minmax(0, 1fr))"
+ )]
+ #[case(
+ "grid-rows-[auto_1fr]",
+ "grid-template-rows",
+ "repeat(auto 1fr, minmax(0, 1fr))"
+ )]
+ #[case("col-span-[2]", "grid-column", "span 2 / span 2")]
+ #[case("row-span-[3]", "grid-row", "span 3 / span 3")]
+ fn test_parse_arbitrary_grid_values(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("blur-[10px]", "filter", "blur(10px)")]
+ #[case("brightness-[1.2]", "filter", "brightness(1.2)")]
+ #[case("contrast-[1.5]", "filter", "contrast(1.5)")]
+ #[case("saturate-[2]", "filter", "saturate(2)")]
+ #[case("backdrop-blur-[5px]", "backdrop-filter", "blur(5px)")]
+ fn test_parse_arbitrary_filter_values(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // ==================== WAVE 3: Layout & Flex/Grid Utilities ====================
+
+ // Wave 3.1: aspect-ratio utilities (lines 1198-1204)
+ #[rstest]
+ #[case("aspect-auto", "aspect-ratio", "auto")]
+ #[case("aspect-square", "aspect-ratio", "1 / 1")]
+ #[case("aspect-video", "aspect-ratio", "16 / 9")]
+ fn test_parse_aspect_ratio_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 3.2: columns utilities (lines 1209-1226)
+ #[rstest]
+ #[case("columns-auto", "columns", "auto")]
+ #[case("columns-3xs", "columns", "16rem")]
+ #[case("columns-2xs", "columns", "18rem")]
+ #[case("columns-xs", "columns", "20rem")]
+ #[case("columns-sm", "columns", "24rem")]
+ #[case("columns-md", "columns", "28rem")]
+ #[case("columns-lg", "columns", "32rem")]
+ #[case("columns-xl", "columns", "36rem")]
+ #[case("columns-2xl", "columns", "42rem")]
+ #[case("columns-3xl", "columns", "48rem")]
+ #[case("columns-4xl", "columns", "56rem")]
+ #[case("columns-5xl", "columns", "64rem")]
+ #[case("columns-6xl", "columns", "72rem")]
+ #[case("columns-7xl", "columns", "80rem")]
+ fn test_parse_columns_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 3.3: break utilities (lines 1231-1252)
+ #[rstest]
+ #[case("break-after-auto", "break-after", "auto")]
+ #[case("break-after-avoid", "break-after", "avoid")]
+ #[case("break-after-all", "break-after", "all")]
+ #[case("break-after-avoid-page", "break-after", "avoid-page")]
+ #[case("break-after-page", "break-after", "page")]
+ #[case("break-after-left", "break-after", "left")]
+ #[case("break-after-right", "break-after", "right")]
+ #[case("break-after-column", "break-after", "column")]
+ #[case("break-before-auto", "break-before", "auto")]
+ #[case("break-before-avoid", "break-before", "avoid")]
+ #[case("break-before-all", "break-before", "all")]
+ #[case("break-before-avoid-page", "break-before", "avoid-page")]
+ #[case("break-before-page", "break-before", "page")]
+ #[case("break-before-left", "break-before", "left")]
+ #[case("break-before-right", "break-before", "right")]
+ #[case("break-before-column", "break-before", "column")]
+ #[case("break-inside-auto", "break-inside", "auto")]
+ #[case("break-inside-avoid", "break-inside", "avoid")]
+ #[case("break-inside-avoid-page", "break-inside", "avoid-page")]
+ #[case("break-inside-avoid-column", "break-inside", "avoid-column")]
+ fn test_parse_break_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 3.4: Position utilities (lines 1273-1304)
+ #[rstest]
+ #[case("top-0", "top", "0px")]
+ #[case("top-4", "top", "1rem")]
+ #[case("right-0", "right", "0px")]
+ #[case("right-4", "right", "1rem")]
+ #[case("bottom-0", "bottom", "0px")]
+ #[case("bottom-4", "bottom", "1rem")]
+ #[case("left-0", "left", "0px")]
+ #[case("left-4", "left", "1rem")]
+ #[case("inset-0", "inset", "0px")]
+ #[case("inset-4", "inset", "1rem")]
+ #[case("inset-x-0", "inset-inline", "0px")]
+ #[case("inset-x-4", "inset-inline", "1rem")]
+ #[case("inset-y-0", "inset-block", "0px")]
+ #[case("inset-y-4", "inset-block", "1rem")]
+ fn test_parse_position_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 3.5: Flex/Grid extended utilities (lines 1463-1529)
+ #[rstest]
+ #[case("basis-4", "flex-basis", "1rem")]
+ #[case("basis-8", "flex-basis", "2rem")]
+ #[case("order-1", "order", "1")]
+ #[case("order-12", "order", "12")]
+ #[case("grid-cols-1", "grid-template-columns", "repeat(1, minmax(0, 1fr))")]
+ #[case("grid-cols-12", "grid-template-columns", "repeat(12, minmax(0, 1fr))")]
+ #[case("grid-rows-1", "grid-template-rows", "repeat(1, minmax(0, 1fr))")]
+ #[case("grid-rows-6", "grid-template-rows", "repeat(6, minmax(0, 1fr))")]
+ #[case("col-start-1", "grid-column-start", "1")]
+ #[case("col-start-auto", "grid-column-start", "auto")]
+ #[case("col-end-1", "grid-column-end", "1")]
+ #[case("col-end-auto", "grid-column-end", "auto")]
+ #[case("row-start-1", "grid-row-start", "1")]
+ #[case("row-start-auto", "grid-row-start", "auto")]
+ #[case("row-end-1", "grid-row-end", "1")]
+ #[case("row-end-auto", "grid-row-end", "auto")]
+ #[case("gap-x-4", "column-gap", "1rem")]
+ #[case("gap-y-4", "row-gap", "1rem")]
+ fn test_parse_flex_grid_extended_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // ==================== WAVE 4: Spacing, Sizing & Typography ====================
+
+ // Wave 4.1: Logical spacing utilities (lines 1580-1656)
+ #[rstest]
+ #[case("ps-4", "padding-inline-start", "1rem")]
+ #[case("pe-4", "padding-inline-end", "1rem")]
+ #[case("ms-4", "margin-inline-start", "1rem")]
+ #[case("me-4", "margin-inline-end", "1rem")]
+ #[case("mr-4", "margin-right", "1rem")]
+ #[case("mb-4", "margin-bottom", "1rem")]
+ #[case("ml-4", "margin-left", "1rem")]
+ #[case("space-x-4", "column-gap", "1rem")]
+ #[case("space-y-4", "row-gap", "1rem")]
+ #[case("space-x-reverse", "--tw-space-x-reverse", "1")]
+ #[case("space-y-reverse", "--tw-space-y-reverse", "1")]
+ fn test_parse_logical_spacing_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 4.2: Sizing variants (lines 1677-1789)
+ #[rstest]
+ #[case("min-w-min", "min-width", "min-content")]
+ #[case("min-w-max", "min-width", "max-content")]
+ #[case("min-w-fit", "min-width", "fit-content")]
+ #[case("max-w-2xl", "max-width", "42rem")]
+ #[case("max-w-3xl", "max-width", "48rem")]
+ #[case("max-w-4xl", "max-width", "56rem")]
+ #[case("max-w-5xl", "max-width", "64rem")]
+ #[case("max-w-6xl", "max-width", "72rem")]
+ #[case("max-w-7xl", "max-width", "80rem")]
+ #[case("max-w-min", "max-width", "min-content")]
+ #[case("max-w-max", "max-width", "max-content")]
+ #[case("max-w-fit", "max-width", "fit-content")]
+ #[case("max-w-prose", "max-width", "65ch")]
+ #[case("max-w-screen-sm", "max-width", "640px")]
+ #[case("max-w-screen-md", "max-width", "768px")]
+ #[case("max-w-screen-lg", "max-width", "1024px")]
+ #[case("max-w-screen-xl", "max-width", "1280px")]
+ #[case("max-w-screen-2xl", "max-width", "1536px")]
+ fn test_parse_width_utilities_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("h-svh", "height", "100svh")]
+ #[case("h-lvh", "height", "100lvh")]
+ #[case("h-dvh", "height", "100dvh")]
+ #[case("min-h-svh", "min-height", "100svh")]
+ #[case("min-h-lvh", "min-height", "100lvh")]
+ #[case("min-h-dvh", "min-height", "100dvh")]
+ #[case("min-h-min", "min-height", "min-content")]
+ #[case("min-h-max", "min-height", "max-content")]
+ #[case("min-h-fit", "min-height", "fit-content")]
+ #[case("max-h-svh", "max-height", "100svh")]
+ #[case("max-h-lvh", "max-height", "100lvh")]
+ #[case("max-h-dvh", "max-height", "100dvh")]
+ #[case("max-h-min", "max-height", "min-content")]
+ #[case("max-h-max", "max-height", "max-content")]
+ #[case("max-h-fit", "max-height", "fit-content")]
+ fn test_parse_height_utilities_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 4.3: Typography extended (lines 1829-1940)
+ #[rstest]
+ #[case("text-start", "text-align", "start")]
+ #[case("text-end", "text-align", "end")]
+ #[case("hyphens-none", "hyphens", "none")]
+ #[case("hyphens-manual", "hyphens", "manual")]
+ #[case("hyphens-auto", "hyphens", "auto")]
+ #[case("tracking-tighter", "letter-spacing", "-0.05em")]
+ #[case("tracking-tight", "letter-spacing", "-0.025em")]
+ #[case("tracking-normal", "letter-spacing", "0em")]
+ #[case("tracking-wide", "letter-spacing", "0.025em")]
+ #[case("tracking-wider", "letter-spacing", "0.05em")]
+ #[case("tracking-widest", "letter-spacing", "0.1em")]
+ fn test_parse_typography_extended_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("leading-none", "line-height", "1")]
+ #[case("leading-tight", "line-height", "1.25")]
+ #[case("leading-snug", "line-height", "1.375")]
+ #[case("leading-normal", "line-height", "1.5")]
+ #[case("leading-relaxed", "line-height", "1.625")]
+ #[case("leading-loose", "line-height", "2")]
+ #[case("leading-3", "line-height", ".75rem")]
+ #[case("leading-4", "line-height", "1rem")]
+ #[case("leading-5", "line-height", "1.25rem")]
+ #[case("leading-6", "line-height", "1.5rem")]
+ #[case("leading-7", "line-height", "1.75rem")]
+ #[case("leading-8", "line-height", "2rem")]
+ #[case("leading-9", "line-height", "2.25rem")]
+ #[case("leading-10", "line-height", "2.5rem")]
+ fn test_parse_leading_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 4.4: List styles & alignment (lines 1947-1965)
+ #[rstest]
+ #[case("list-inside", "list-style-position", "inside")]
+ #[case("list-outside", "list-style-position", "outside")]
+ #[case("list-none", "list-style-type", "none")]
+ #[case("list-disc", "list-style-type", "disc")]
+ #[case("list-decimal", "list-style-type", "decimal")]
+ #[case("align-baseline", "vertical-align", "baseline")]
+ #[case("align-top", "vertical-align", "top")]
+ #[case("align-middle", "vertical-align", "middle")]
+ #[case("align-bottom", "vertical-align", "bottom")]
+ #[case("content-none", "content", "none")]
+ fn test_parse_list_align_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // ==================== WAVE 5: Backgrounds, Borders, Effects, etc. ====================
+
+ // Wave 5.1: Background utilities (lines 1981-2051)
+ #[rstest]
+ #[case("bg-fixed", "background-attachment", "fixed")]
+ #[case("bg-local", "background-attachment", "local")]
+ #[case("bg-scroll", "background-attachment", "scroll")]
+ #[case("bg-clip-border", "background-clip", "border-box")]
+ #[case("bg-clip-padding", "background-clip", "padding-box")]
+ #[case("bg-clip-content", "background-clip", "content-box")]
+ #[case("bg-clip-text", "background-clip", "text")]
+ #[case("bg-origin-border", "background-origin", "border-box")]
+ #[case("bg-origin-padding", "background-origin", "padding-box")]
+ #[case("bg-origin-content", "background-origin", "content-box")]
+ fn test_parse_background_attachment_clip_origin(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("bg-bottom", "background-position", "bottom")]
+ #[case("bg-center", "background-position", "center")]
+ #[case("bg-left", "background-position", "left")]
+ #[case("bg-left-bottom", "background-position", "left bottom")]
+ #[case("bg-left-top", "background-position", "left top")]
+ #[case("bg-right", "background-position", "right")]
+ #[case("bg-right-bottom", "background-position", "right bottom")]
+ #[case("bg-right-top", "background-position", "right top")]
+ #[case("bg-top", "background-position", "top")]
+ fn test_parse_background_position(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("bg-repeat", "background-repeat", "repeat")]
+ #[case("bg-no-repeat", "background-repeat", "no-repeat")]
+ #[case("bg-repeat-x", "background-repeat", "repeat-x")]
+ #[case("bg-repeat-y", "background-repeat", "repeat-y")]
+ #[case("bg-repeat-round", "background-repeat", "round")]
+ #[case("bg-repeat-space", "background-repeat", "space")]
+ #[case("bg-auto", "background-size", "auto")]
+ #[case("bg-cover", "background-size", "cover")]
+ #[case("bg-contain", "background-size", "contain")]
+ #[case("bg-none", "background-image", "none")]
+ fn test_parse_background_repeat_size(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case(
+ "bg-gradient-to-t",
+ "background-image",
+ "linear-gradient(to top, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-tr",
+ "background-image",
+ "linear-gradient(to top right, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-r",
+ "background-image",
+ "linear-gradient(to right, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-br",
+ "background-image",
+ "linear-gradient(to bottom right, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-b",
+ "background-image",
+ "linear-gradient(to bottom, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-bl",
+ "background-image",
+ "linear-gradient(to bottom left, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-l",
+ "background-image",
+ "linear-gradient(to left, var(--tw-gradient-stops))"
+ )]
+ #[case(
+ "bg-gradient-to-tl",
+ "background-image",
+ "linear-gradient(to top left, var(--tw-gradient-stops))"
+ )]
+ fn test_parse_background_gradient(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.2: Gradient stops (lines 2057-2068)
+ #[rstest]
+ #[case("from-red-500", "--tw-gradient-from", "#ef4444")]
+ #[case("from-blue-500", "--tw-gradient-from", "#3b82f6")]
+ #[case("via-red-500", "--tw-gradient-via", "#ef4444")]
+ #[case("via-blue-500", "--tw-gradient-via", "#3b82f6")]
+ #[case("to-red-500", "--tw-gradient-to", "#ef4444")]
+ #[case("to-blue-500", "--tw-gradient-to", "#3b82f6")]
+ fn test_parse_gradient_stops(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.3: Border corners/sides (lines 2081-2189)
+ #[rstest]
+ #[case("rounded-t-lg", "border-top-left-radius", "0.5rem")]
+ #[case("rounded-r-lg", "border-top-right-radius", "0.5rem")]
+ #[case("rounded-b-lg", "border-bottom-right-radius", "0.5rem")]
+ #[case("rounded-l-lg", "border-bottom-left-radius", "0.5rem")]
+ #[case("rounded-tl-lg", "border-top-left-radius", "0.5rem")]
+ #[case("rounded-tr-lg", "border-top-right-radius", "0.5rem")]
+ #[case("rounded-br-lg", "border-bottom-right-radius", "0.5rem")]
+ #[case("rounded-bl-lg", "border-bottom-left-radius", "0.5rem")]
+ fn test_parse_border_radius_corners(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("border-t-2", "border-top-width", "2px")]
+ #[case("border-r-2", "border-right-width", "2px")]
+ #[case("border-b-2", "border-bottom-width", "2px")]
+ #[case("border-l-2", "border-left-width", "2px")]
+ #[case("border-x-2", "border-inline-width", "2px")]
+ #[case("border-y-2", "border-block-width", "2px")]
+ fn test_parse_border_width_sides(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.4: Border styles, outline, ring, divide (lines 2211-2313)
+ #[rstest]
+ #[case("border-solid", "border-style", "solid")]
+ #[case("border-dashed", "border-style", "dashed")]
+ #[case("border-dotted", "border-style", "dotted")]
+ #[case("border-double", "border-style", "double")]
+ #[case("border-hidden", "border-style", "hidden")]
+ #[case("border-none", "border-style", "none")]
+ fn test_parse_border_styles(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("outline-none", "outline", "2px solid transparent")]
+ #[case("outline", "outline-style", "solid")]
+ #[case("outline-dashed", "outline-style", "dashed")]
+ #[case("outline-dotted", "outline-style", "dotted")]
+ #[case("outline-double", "outline-style", "double")]
+ #[case("outline-0", "outline-width", "0px")]
+ #[case("outline-1", "outline-width", "1px")]
+ #[case("outline-2", "outline-width", "2px")]
+ #[case("outline-4", "outline-width", "4px")]
+ #[case("outline-8", "outline-width", "8px")]
+ fn test_parse_outline_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("ring", "box-shadow", "0 0 0 3px var(--tw-ring-color)")]
+ #[case("ring-0", "--tw-ring-offset-shadow", "0 0 #0000")]
+ #[case("ring-1", "box-shadow", "0 0 0 1px var(--tw-ring-color)")]
+ #[case("ring-2", "box-shadow", "0 0 0 2px var(--tw-ring-color)")]
+ #[case("ring-inset", "--tw-ring-inset", "inset")]
+ fn test_parse_ring_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("divide-x", "--tw-divide-x-reverse", "0")]
+ #[case("divide-y", "--tw-divide-y-reverse", "0")]
+ #[case("divide-x-2", "border-inline-width", "2px")]
+ #[case("divide-y-2", "border-block-width", "2px")]
+ #[case("divide-x-reverse", "--tw-divide-x-reverse", "1")]
+ #[case("divide-y-reverse", "--tw-divide-y-reverse", "1")]
+ fn test_parse_divide_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.5: Effects (lines 2328-2350)
+ #[rstest]
+ #[case("shadow-red-500", "--tw-shadow-color", "#ef4444")]
+ #[case("shadow-blue-500", "--tw-shadow-color", "#3b82f6")]
+ fn test_parse_shadow_color_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("bg-blend-normal", "background-blend-mode", "normal")]
+ #[case("bg-blend-multiply", "background-blend-mode", "multiply")]
+ #[case("bg-blend-screen", "background-blend-mode", "screen")]
+ #[case("bg-blend-overlay", "background-blend-mode", "overlay")]
+ fn test_parse_blend_mode_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.6: Filters & backdrop filters (lines 2365-2555)
+ #[rstest]
+ #[case("blur-none", "filter", "blur(0)")]
+ #[case("blur-sm", "filter", "blur(4px)")]
+ #[case("blur-md", "filter", "blur(12px)")]
+ #[case("blur-xl", "filter", "blur(24px)")]
+ #[case("blur-2xl", "filter", "blur(40px)")]
+ #[case("blur-3xl", "filter", "blur(64px)")]
+ fn test_parse_blur_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("brightness-0", "filter", "brightness(0)")]
+ #[case("brightness-50", "filter", "brightness(.5)")]
+ #[case("brightness-100", "filter", "brightness(1)")]
+ #[case("brightness-150", "filter", "brightness(1.5)")]
+ #[case("brightness-200", "filter", "brightness(2)")]
+ fn test_parse_brightness_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("contrast-0", "filter", "contrast(0)")]
+ #[case("contrast-50", "filter", "contrast(.5)")]
+ #[case("contrast-100", "filter", "contrast(1)")]
+ #[case("contrast-150", "filter", "contrast(1.5)")]
+ #[case("contrast-200", "filter", "contrast(2)")]
+ fn test_parse_contrast_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case(
+ "drop-shadow",
+ "filter",
+ "drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06))"
+ )]
+ #[case("drop-shadow-sm", "filter", "drop-shadow(0 1px 1px rgb(0 0 0 / 0.05))")]
+ #[case(
+ "drop-shadow-md",
+ "filter",
+ "drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))"
+ )]
+ #[case(
+ "drop-shadow-lg",
+ "filter",
+ "drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))"
+ )]
+ #[case(
+ "drop-shadow-xl",
+ "filter",
+ "drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08))"
+ )]
+ #[case(
+ "drop-shadow-2xl",
+ "filter",
+ "drop-shadow(0 25px 25px rgb(0 0 0 / 0.15))"
+ )]
+ #[case("drop-shadow-none", "filter", "drop-shadow(0 0 #0000)")]
+ fn test_parse_drop_shadow_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("hue-rotate-0", "filter", "hue-rotate(0deg)")]
+ #[case("hue-rotate-15", "filter", "hue-rotate(15deg)")]
+ #[case("hue-rotate-30", "filter", "hue-rotate(30deg)")]
+ #[case("hue-rotate-60", "filter", "hue-rotate(60deg)")]
+ #[case("hue-rotate-90", "filter", "hue-rotate(90deg)")]
+ #[case("hue-rotate-180", "filter", "hue-rotate(180deg)")]
+ fn test_parse_hue_rotate_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("invert-0", "filter", "invert(0)")]
+ #[case("invert", "filter", "invert(100%)")]
+ #[case("saturate-0", "filter", "saturate(0)")]
+ #[case("saturate-50", "filter", "saturate(.5)")]
+ #[case("saturate-100", "filter", "saturate(1)")]
+ #[case("saturate-150", "filter", "saturate(1.5)")]
+ #[case("saturate-200", "filter", "saturate(2)")]
+ #[case("sepia-0", "filter", "sepia(0)")]
+ #[case("sepia", "filter", "sepia(100%)")]
+ fn test_parse_filter_effects_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("backdrop-blur", "backdrop-filter", "blur(8px)")]
+ #[case("backdrop-blur-sm", "backdrop-filter", "blur(4px)")]
+ #[case("backdrop-blur-md", "backdrop-filter", "blur(12px)")]
+ #[case("backdrop-blur-lg", "backdrop-filter", "blur(16px)")]
+ #[case("backdrop-blur-xl", "backdrop-filter", "blur(24px)")]
+ #[case("backdrop-blur-2xl", "backdrop-filter", "blur(40px)")]
+ #[case("backdrop-blur-3xl", "backdrop-filter", "blur(64px)")]
+ #[case("backdrop-blur-none", "backdrop-filter", "blur(0)")]
+ fn test_parse_backdrop_blur_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("backdrop-brightness-0", "backdrop-filter", "brightness(0)")]
+ #[case("backdrop-brightness-100", "backdrop-filter", "brightness(1)")]
+ #[case("backdrop-contrast-0", "backdrop-filter", "contrast(0)")]
+ #[case("backdrop-contrast-100", "backdrop-filter", "contrast(1)")]
+ #[case("backdrop-grayscale-0", "backdrop-filter", "grayscale(0)")]
+ #[case("backdrop-grayscale", "backdrop-filter", "grayscale(100%)")]
+ #[case("backdrop-invert-0", "backdrop-filter", "invert(0)")]
+ #[case("backdrop-invert", "backdrop-filter", "invert(100%)")]
+ #[case("backdrop-opacity-0", "backdrop-filter", "opacity(0)")]
+ #[case("backdrop-opacity-100", "backdrop-filter", "opacity(1)")]
+ #[case("backdrop-saturate-0", "backdrop-filter", "saturate(0)")]
+ #[case("backdrop-saturate-100", "backdrop-filter", "saturate(1)")]
+ #[case("backdrop-sepia-0", "backdrop-filter", "sepia(0)")]
+ #[case("backdrop-sepia", "backdrop-filter", "sepia(100%)")]
+ fn test_parse_backdrop_filter_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.7: Transitions & animations (lines 2600-2618)
+ #[rstest]
+ #[case("delay-0", "transition-delay", "0s")]
+ #[case("delay-75", "transition-delay", "75ms")]
+ #[case("delay-100", "transition-delay", "100ms")]
+ #[case("delay-150", "transition-delay", "150ms")]
+ #[case("delay-200", "transition-delay", "200ms")]
+ #[case("delay-300", "transition-delay", "300ms")]
+ #[case("delay-500", "transition-delay", "500ms")]
+ #[case("delay-700", "transition-delay", "700ms")]
+ #[case("delay-1000", "transition-delay", "1000ms")]
+ fn test_parse_delay_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("animate-none", "animation", "none")]
+ #[case("animate-spin", "animation", "spin 1s linear infinite")]
+ #[case(
+ "animate-ping",
+ "animation",
+ "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite"
+ )]
+ #[case(
+ "animate-pulse",
+ "animation",
+ "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"
+ )]
+ #[case("animate-bounce", "animation", "bounce 1s infinite")]
+ fn test_parse_animation_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.8: Transforms (lines 2630-2716)
+ #[rstest]
+ #[case("scale-x-0", "transform", "scaleX(0)")]
+ #[case("scale-x-50", "transform", "scaleX(0.5)")]
+ #[case("scale-x-100", "transform", "scaleX(1)")]
+ #[case("scale-x-150", "transform", "scaleX(1.5)")]
+ #[case("scale-y-0", "transform", "scaleY(0)")]
+ #[case("scale-y-50", "transform", "scaleY(0.5)")]
+ #[case("scale-y-100", "transform", "scaleY(1)")]
+ #[case("scale-y-150", "transform", "scaleY(1.5)")]
+ fn test_parse_scale_axis_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("rotate-0", "transform", "rotate(0deg)")]
+ #[case("rotate-1", "transform", "rotate(1deg)")]
+ #[case("rotate-2", "transform", "rotate(2deg)")]
+ #[case("rotate-3", "transform", "rotate(3deg)")]
+ #[case("rotate-6", "transform", "rotate(6deg)")]
+ #[case("rotate-12", "transform", "rotate(12deg)")]
+ #[case("rotate-90", "transform", "rotate(90deg)")]
+ #[case("rotate-180", "transform", "rotate(180deg)")]
+ fn test_parse_rotate_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("translate-y-4", "transform", "translateY(1rem)")]
+ #[case("translate-y-px", "transform", "translateY(1px)")]
+ #[case("translate-y-full", "transform", "translateY(100%)")]
+ #[case("translate-y-1/2", "transform", "translateY(50%)")]
+ fn test_parse_translate_y_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("skew-x-0", "transform", "skewX(0deg)")]
+ #[case("skew-x-1", "transform", "skewX(1deg)")]
+ #[case("skew-x-2", "transform", "skewX(2deg)")]
+ #[case("skew-x-3", "transform", "skewX(3deg)")]
+ #[case("skew-x-6", "transform", "skewX(6deg)")]
+ #[case("skew-x-12", "transform", "skewX(12deg)")]
+ #[case("skew-y-0", "transform", "skewY(0deg)")]
+ #[case("skew-y-1", "transform", "skewY(1deg)")]
+ #[case("skew-y-2", "transform", "skewY(2deg)")]
+ #[case("skew-y-3", "transform", "skewY(3deg)")]
+ #[case("skew-y-6", "transform", "skewY(6deg)")]
+ #[case("skew-y-12", "transform", "skewY(12deg)")]
+ fn test_parse_skew_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("origin-center", "transform-origin", "center")]
+ #[case("origin-top", "transform-origin", "top")]
+ #[case("origin-top-right", "transform-origin", "top right")]
+ #[case("origin-right", "transform-origin", "right")]
+ #[case("origin-bottom-right", "transform-origin", "bottom right")]
+ #[case("origin-bottom", "transform-origin", "bottom")]
+ #[case("origin-bottom-left", "transform-origin", "bottom left")]
+ #[case("origin-left", "transform-origin", "left")]
+ #[case("origin-top-left", "transform-origin", "top left")]
+ fn test_parse_transform_origin_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.9: Interactivity (lines 2732-2842)
+ #[rstest]
+ #[case("accent-auto", "accent-color", "auto")]
+ #[case("accent-red-500", "accent-color", "#ef4444")]
+ #[case("appearance-none", "appearance", "none")]
+ #[case("appearance-auto", "appearance", "auto")]
+ #[case("caret-red-500", "caret-color", "#ef4444")]
+ fn test_parse_interactivity_accent_caret(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("scroll-auto", "scroll-behavior", "auto")]
+ #[case("scroll-smooth", "scroll-behavior", "smooth")]
+ #[case("scroll-m-4", "scroll-margin", "1rem")]
+ #[case("scroll-p-4", "scroll-padding", "1rem")]
+ fn test_parse_scroll_behavior_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("snap-start", "scroll-snap-align", "start")]
+ #[case("snap-end", "scroll-snap-align", "end")]
+ #[case("snap-center", "scroll-snap-align", "center")]
+ #[case("snap-align-none", "scroll-snap-align", "none")]
+ #[case("snap-none", "scroll-snap-type", "none")]
+ #[case("snap-x", "scroll-snap-type", "x var(--tw-scroll-snap-strictness)")]
+ #[case("snap-y", "scroll-snap-type", "y var(--tw-scroll-snap-strictness)")]
+ #[case(
+ "snap-both",
+ "scroll-snap-type",
+ "both var(--tw-scroll-snap-strictness)"
+ )]
+ #[case("snap-mandatory", "--tw-scroll-snap-strictness", "mandatory")]
+ #[case("snap-proximity", "--tw-scroll-snap-strictness", "proximity")]
+ #[case("snap-normal", "scroll-snap-stop", "normal")]
+ #[case("snap-always", "scroll-snap-stop", "always")]
+ fn test_parse_snap_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("touch-auto", "touch-action", "auto")]
+ #[case("touch-none", "touch-action", "none")]
+ #[case("touch-pan-x", "touch-action", "pan-x")]
+ #[case("touch-pan-y", "touch-action", "pan-y")]
+ #[case("touch-manipulation", "touch-action", "manipulation")]
+ fn test_parse_touch_action_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("will-change-auto", "will-change", "auto")]
+ #[case("will-change-scroll", "will-change", "scroll-position")]
+ #[case("will-change-contents", "will-change", "contents")]
+ #[case("will-change-transform", "will-change", "transform")]
+ fn test_parse_will_change_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 5.10: SVG utilities (lines 2853-2863)
+ #[rstest]
+ #[case("fill-none", "fill", "none")]
+ #[case("stroke-none", "stroke", "none")]
+ #[case("stroke-0", "stroke-width", "0")]
+ #[case("stroke-1", "stroke-width", "1")]
+ fn test_parse_svg_extended_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // ============================================================================
+ // WAVE 6: Coverage Gap Tests - Remaining Uncovered Lines
+ // ============================================================================
+
+ // Wave 6.1: TailwindVariant::from_prefix() returning None (line 230)
+ #[test]
+ fn test_tailwind_variant_unknown_prefix_returns_none() {
+ assert_eq!(TailwindVariant::from_prefix("unknown-prefix"), None);
+ assert_eq!(TailwindVariant::from_prefix("nonexistent"), None);
+ assert_eq!(TailwindVariant::from_prefix("foo-bar"), None);
+ assert_eq!(TailwindVariant::from_prefix("xyz"), None);
+ assert_eq!(TailwindVariant::from_prefix(""), None);
+ }
+
+ // Wave 6.2: parse_arbitrary_value edge cases (lines 1015, 1084)
+ #[test]
+ fn test_parse_arbitrary_value_malformed_brackets() {
+ // Test bracket_end <= bracket_start case (line 1015)
+ let result = parse_arbitrary_value("][");
+ assert!(result.is_none());
+
+ let result = parse_arbitrary_value("w-][");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_parse_arbitrary_value_unknown_prefix() {
+ // Test unknown prefix fallback (line 1084)
+ let result = parse_arbitrary_value("unknown-[value]");
+ assert!(result.is_none());
+
+ let result = parse_arbitrary_value("foo-[bar]");
+ assert!(result.is_none());
+
+ let result = parse_arbitrary_value("xyz-[123px]");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.3: Custom aspect-ratio (line 1202)
+ #[rstest]
+ #[case("aspect-4-3", "aspect-ratio", "4 / 3")]
+ #[case("aspect-16-10", "aspect-ratio", "16 / 10")]
+ #[case("aspect-21-9", "aspect-ratio", "21 / 9")]
+ #[case("aspect-3-2", "aspect-ratio", "3 / 2")]
+ fn test_parse_custom_aspect_ratio(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse custom aspect ratio");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.4: Numeric columns fallback (line 1224)
+ #[rstest]
+ #[case("columns-1", "columns", "1")]
+ #[case("columns-2", "columns", "2")]
+ #[case("columns-3", "columns", "3")]
+ #[case("columns-4", "columns", "4")]
+ #[case("columns-5", "columns", "5")]
+ #[case("columns-6", "columns", "6")]
+ #[case("columns-12", "columns", "12")]
+ fn test_parse_numeric_columns(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse numeric columns");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.5: Box decoration break (lines 1258, 1261)
+ #[rstest]
+ #[case("box-decoration-clone", "box-decoration-break", "clone")]
+ #[case("box-decoration-slice", "box-decoration-break", "slice")]
+ fn test_parse_box_decoration_break(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse box-decoration-break");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.6: min-w/max-w/h fallback paths (lines 1681-1785)
+ #[test]
+ fn test_min_w_unknown_value_returns_none() {
+ // min-w- with unknown value should return None (line 1684)
+ let result = parse_single_class("min-w-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_max_w_unknown_value_returns_none() {
+ // max-w- with unknown value should return None (line 1721)
+ let result = parse_single_class("max-w-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_h_unknown_value_returns_none() {
+ // h- with unknown value should return None (line 1739)
+ let result = parse_single_class("h-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_min_h_unknown_value_returns_none() {
+ // min-h- with unknown value should return None (line 1762)
+ let result = parse_single_class("min-h-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_max_h_unknown_value_returns_none() {
+ // max-h- with unknown value should return None (line 1785)
+ let result = parse_single_class("max-h-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_size_unknown_value_returns_none() {
+ // size- with unknown value should return None (line 1797)
+ let result = parse_single_class("size-unknown");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.7: Typography edge cases (lines 1833-1892)
+ #[rstest]
+ #[case("line-through", "text-decoration-line", "line-through")]
+ fn test_parse_line_through(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse line-through");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("truncate", "text-overflow", "ellipsis")]
+ fn test_parse_truncate(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse truncate");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("whitespace-normal", "white-space", "normal")]
+ #[case("whitespace-nowrap", "white-space", "nowrap")]
+ #[case("whitespace-pre", "white-space", "pre")]
+ #[case("whitespace-pre-line", "white-space", "pre-line")]
+ #[case("whitespace-pre-wrap", "white-space", "pre-wrap")]
+ #[case("whitespace-break-spaces", "white-space", "break-spaces")]
+ fn test_parse_whitespace_utilities(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse whitespace");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.8: Tracking/leading fallbacks (lines 1918, 1940)
+ #[rstest]
+ #[case("tracking-custom", "letter-spacing", "custom")]
+ #[case("tracking-0.5em", "letter-spacing", "0.5em")]
+ fn test_parse_tracking_fallback(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse tracking fallback");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("leading-custom", "line-height", "custom")]
+ #[case("leading-1.5", "line-height", "1.5")]
+ fn test_parse_leading_fallback(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse leading fallback");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.9: List style fallback (line 1953)
+ #[test]
+ fn test_list_style_unknown_returns_none() {
+ // list- with unknown value should not match (line 1953)
+ let result = parse_single_class("list-unknown");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.10: Gradient direction fallback (line 2049)
+ #[test]
+ fn test_bg_gradient_unknown_direction_returns_none() {
+ // bg-gradient-to- with unknown direction should return None
+ let result = parse_single_class("bg-gradient-to-xyz");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.11: Individual rounded variants (via BORDER_RADIUS_SCALE lookup)
+ #[rstest]
+ #[case("rounded-none", "border-radius", "0px")]
+ #[case("rounded-sm", "border-radius", "0.125rem")]
+ #[case("rounded-md", "border-radius", "0.375rem")]
+ #[case("rounded-lg", "border-radius", "0.5rem")]
+ #[case("rounded-xl", "border-radius", "0.75rem")]
+ #[case("rounded-2xl", "border-radius", "1rem")]
+ #[case("rounded-3xl", "border-radius", "1.5rem")]
+ #[case("rounded-full", "border-radius", "9999px")]
+ fn test_parse_individual_rounded_variants(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse rounded variant");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.12: Border collapse/separate (border-0/2/4/8 handled via BORDER_WIDTH_SCALE)
+ #[rstest]
+ #[case("border-collapse", "border-collapse", "collapse")]
+ #[case("border-separate", "border-collapse", "separate")]
+ fn test_parse_border_collapse_separate(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse border collapse/separate");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("border-0", "border-width", "0px")]
+ #[case("border-2", "border-width", "2px")]
+ #[case("border-4", "border-width", "4px")]
+ #[case("border-8", "border-width", "8px")]
+ fn test_parse_border_width_standalone(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse border width");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.13: Outline color (lines 2250-2251)
+ #[rstest]
+ #[case("outline-black", "outline-color", "#000")]
+ #[case("outline-white", "outline-color", "#fff")]
+ #[case("outline-red-500", "outline-color", "#ef4444")]
+ #[case("outline-blue-500", "outline-color", "#3b82f6")]
+ fn test_parse_outline_color(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse outline color");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.14: Ring utilities (lines 2273-2281)
+ #[rstest]
+ #[case("ring-4", "box-shadow", "0 0 0 4px var(--tw-ring-color)")]
+ #[case("ring-8", "box-shadow", "0 0 0 8px var(--tw-ring-color)")]
+ fn test_parse_ring_width_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse ring width");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("ring-black", "--tw-ring-color", "#000")]
+ #[case("ring-white", "--tw-ring-color", "#fff")]
+ #[case("ring-red-500", "--tw-ring-color", "#ef4444")]
+ #[case("ring-blue-500", "--tw-ring-color", "#3b82f6")]
+ fn test_parse_ring_color(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse ring color");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.15: Divide utilities (lines 2293-2313)
+ #[rstest]
+ #[case("divide-black", "--tw-divide-color", "#000")]
+ #[case("divide-white", "--tw-divide-color", "#fff")]
+ #[case("divide-red-500", "--tw-divide-color", "#ef4444")]
+ fn test_parse_divide_color(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse divide color");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("divide-solid", "border-style", "solid")]
+ #[case("divide-dashed", "border-style", "dashed")]
+ #[case("divide-dotted", "border-style", "dotted")]
+ #[case("divide-double", "border-style", "double")]
+ #[case("divide-none", "border-style", "none")]
+ fn test_parse_divide_style(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse divide style");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 6.16: Filter fallbacks (lines 2390, 2405, 2419)
+ #[test]
+ fn test_brightness_unknown_value_returns_none() {
+ let result = parse_single_class("brightness-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("brightness-999");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_contrast_unknown_value_returns_none() {
+ let result = parse_single_class("contrast-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("contrast-999");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_drop_shadow_unknown_value_returns_none() {
+ let result = parse_single_class("drop-shadow-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("drop-shadow-huge");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.17: Backdrop filter extended values (lines 2504-2555)
+ #[rstest]
+ #[case("backdrop-brightness-105", "backdrop-filter", "brightness(1.05)")]
+ #[case("backdrop-brightness-110", "backdrop-filter", "brightness(1.1)")]
+ #[case("backdrop-brightness-125", "backdrop-filter", "brightness(1.25)")]
+ #[case("backdrop-brightness-150", "backdrop-filter", "brightness(1.5)")]
+ #[case("backdrop-brightness-200", "backdrop-filter", "brightness(2)")]
+ fn test_parse_backdrop_brightness_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse backdrop-brightness");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("backdrop-contrast-125", "backdrop-filter", "contrast(1.25)")]
+ #[case("backdrop-contrast-150", "backdrop-filter", "contrast(1.5)")]
+ #[case("backdrop-contrast-200", "backdrop-filter", "contrast(2)")]
+ fn test_parse_backdrop_contrast_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse backdrop-contrast");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("backdrop-saturate-150", "backdrop-filter", "saturate(1.5)")]
+ #[case("backdrop-saturate-200", "backdrop-filter", "saturate(2)")]
+ fn test_parse_backdrop_saturate_extended(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse backdrop-saturate");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[test]
+ fn test_backdrop_brightness_unknown_returns_none() {
+ let result = parse_single_class("backdrop-brightness-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_backdrop_contrast_unknown_returns_none() {
+ let result = parse_single_class("backdrop-contrast-unknown");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_backdrop_saturate_unknown_returns_none() {
+ let result = parse_single_class("backdrop-saturate-unknown");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.18: Transform fallbacks (lines 2654-2714)
+ #[test]
+ fn test_rotate_unknown_value_returns_none() {
+ let result = parse_single_class("rotate-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("rotate-999");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_skew_x_unknown_value_returns_none() {
+ let result = parse_single_class("skew-x-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("skew-x-99");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_skew_y_unknown_value_returns_none() {
+ let result = parse_single_class("skew-y-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("skew-y-99");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_origin_unknown_value_returns_none() {
+ let result = parse_single_class("origin-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("origin-middle");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.19: Snap/will-change fallbacks (lines 2819, 2840)
+ #[test]
+ fn test_snap_unknown_value_returns_none() {
+ let result = parse_single_class("snap-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("snap-xyz");
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_will_change_unknown_value_returns_none() {
+ let result = parse_single_class("will-change-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("will-change-xyz");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.20: Break utilities fallback (line 1252)
+ #[test]
+ fn test_break_unknown_value_returns_none() {
+ let result = parse_single_class("break-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("break-xyz");
+ assert!(result.is_none());
+ }
+
+ // Wave 6.21: Backdrop blur unknown value (line 2488)
+ #[test]
+ fn test_backdrop_blur_unknown_returns_none() {
+ let result = parse_single_class("backdrop-blur-unknown");
+ assert!(result.is_none());
+ let result = parse_single_class("backdrop-blur-huge");
+ assert!(result.is_none());
+ }
+
+ // ============================================================================
+ // WAVE 7: Additional Coverage Gap Tests
+ // ============================================================================
+
+ // Wave 7.1: is_likely_tailwind_class arbitrary value syntax (line 816)
+ #[rstest]
+ #[case("w-[100px]")]
+ #[case("h-[50vh]")]
+ #[case("bg-[#ff0000]")]
+ #[case("text-[1.5rem]")]
+ #[case("grid-cols-[1fr_2fr]")]
+ #[case("p-[calc(100%-20px)]")]
+ fn test_is_likely_tailwind_class_arbitrary_syntax(#[case] class: &str) {
+ assert!(is_likely_tailwind_class(class));
+ }
+
+ // Wave 7.2: is_valid_tailwind_value fraction values (lines 864-866)
+ #[rstest]
+ #[case("1/2", true)]
+ #[case("2/3", true)]
+ #[case("3/4", true)]
+ #[case("5/6", true)]
+ #[case("11/12", true)]
+ #[case("a/b", false)]
+ #[case("foo/bar", false)]
+ fn test_is_valid_tailwind_value_fractions_extended(
+ #[case] value: &str,
+ #[case] expected: bool,
+ ) {
+ assert_eq!(is_valid_tailwind_value(value), expected);
+ }
+
+ // Wave 7.3: min-w/max-w/min-h/max-h/size with SPACING_SCALE values (lines 1682, 1719, 1760, 1783, 1797)
+ #[rstest]
+ #[case("min-w-4", "min-width", "1rem")]
+ #[case("min-w-8", "min-width", "2rem")]
+ #[case("min-w-12", "min-width", "3rem")]
+ #[case("min-w-px", "min-width", "1px")]
+ #[case("min-w-0.5", "min-width", "0.125rem")]
+ fn test_parse_min_w_spacing_scale(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse min-w spacing");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("max-w-4", "max-width", "1rem")]
+ #[case("max-w-8", "max-width", "2rem")]
+ #[case("max-w-12", "max-width", "3rem")]
+ #[case("max-w-px", "max-width", "1px")]
+ fn test_parse_max_w_spacing_scale(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse max-w spacing");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("min-h-4", "min-height", "1rem")]
+ #[case("min-h-8", "min-height", "2rem")]
+ #[case("min-h-12", "min-height", "3rem")]
+ #[case("min-h-px", "min-height", "1px")]
+ fn test_parse_min_h_spacing_scale(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse min-h spacing");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("max-h-4", "max-height", "1rem")]
+ #[case("max-h-8", "max-height", "2rem")]
+ #[case("max-h-12", "max-height", "3rem")]
+ #[case("max-h-px", "max-height", "1px")]
+ fn test_parse_max_h_spacing_scale(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse max-h spacing");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ #[rstest]
+ #[case("size-4", "width", "1rem")]
+ #[case("size-8", "width", "2rem")]
+ #[case("size-12", "width", "3rem")]
+ #[case("size-px", "width", "1px")]
+ #[case("size-0.5", "width", "0.125rem")]
+ fn test_parse_size_spacing_scale(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse size spacing");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.4: text- prefix fallback when not font-size or text-align (line 1833)
+ #[rstest]
+ #[case("text-unknown")]
+ #[case("text-foo")]
+ #[case("text-bar")]
+ fn test_text_prefix_unknown_returns_none(#[case] class: &str) {
+ let result = parse_single_class(class);
+ assert!(result.is_none());
+ }
+
+ // Wave 7.5: Individual rounded variants via BORDER_RADIUS_SCALE lookup
+ #[rstest]
+ #[case("rounded", "border-radius", "0.25rem")]
+ #[case("rounded-none", "border-radius", "0px")]
+ #[case("rounded-sm", "border-radius", "0.125rem")]
+ #[case("rounded-md", "border-radius", "0.375rem")]
+ #[case("rounded-lg", "border-radius", "0.5rem")]
+ #[case("rounded-xl", "border-radius", "0.75rem")]
+ #[case("rounded-2xl", "border-radius", "1rem")]
+ #[case("rounded-3xl", "border-radius", "1.5rem")]
+ #[case("rounded-full", "border-radius", "9999px")]
+ fn test_rounded_variants_full_path(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse rounded");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.6: border-0/2/4/8 via BORDER_WIDTH_SCALE lookup
+ #[rstest]
+ #[case("border", "border-width", "1px")]
+ #[case("border-0", "border-width", "0px")]
+ #[case("border-2", "border-width", "2px")]
+ #[case("border-4", "border-width", "4px")]
+ #[case("border-8", "border-width", "8px")]
+ fn test_border_width_standalone_full_path(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse border");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.7: divide- unknown value fallback (line 2313)
+ #[rstest]
+ #[case("divide-unknown")]
+ #[case("divide-xyz")]
+ fn test_divide_unknown_returns_none(#[case] class: &str) {
+ let result = parse_single_class(class);
+ assert!(result.is_none());
+ }
+
+ // Wave 7.8: shadow without suffix (line 2333)
+ #[rstest]
+ #[case(
+ "shadow",
+ "box-shadow",
+ "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
+ )]
+ fn test_shadow_without_suffix(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse shadow");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.9: mix-blend- prefix (line 2345)
+ #[rstest]
+ #[case("mix-blend-normal", "mix-blend-mode", "normal")]
+ #[case("mix-blend-multiply", "mix-blend-mode", "multiply")]
+ #[case("mix-blend-screen", "mix-blend-mode", "screen")]
+ #[case("mix-blend-overlay", "mix-blend-mode", "overlay")]
+ #[case("mix-blend-darken", "mix-blend-mode", "darken")]
+ #[case("mix-blend-lighten", "mix-blend-mode", "lighten")]
+ #[case("mix-blend-color-dodge", "mix-blend-mode", "color-dodge")]
+ #[case("mix-blend-color-burn", "mix-blend-mode", "color-burn")]
+ #[case("mix-blend-hard-light", "mix-blend-mode", "hard-light")]
+ #[case("mix-blend-soft-light", "mix-blend-mode", "soft-light")]
+ #[case("mix-blend-difference", "mix-blend-mode", "difference")]
+ #[case("mix-blend-exclusion", "mix-blend-mode", "exclusion")]
+ #[case("mix-blend-hue", "mix-blend-mode", "hue")]
+ #[case("mix-blend-saturation", "mix-blend-mode", "saturation")]
+ #[case("mix-blend-color", "mix-blend-mode", "color")]
+ #[case("mix-blend-luminosity", "mix-blend-mode", "luminosity")]
+ fn test_parse_mix_blend_mode(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse mix-blend");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.10: blur- unknown value (line 2368)
+ #[rstest]
+ #[case("blur-unknown")]
+ #[case("blur-huge")]
+ #[case("blur-4xl")]
+ fn test_blur_unknown_value_returns_none(#[case] class: &str) {
+ let result = parse_single_class(class);
+ assert!(result.is_none());
+ }
+
+ // Wave 7.11: grayscale-0 (line 2432)
+ #[rstest]
+ #[case("grayscale-0", "filter", "grayscale(0)")]
+ fn test_grayscale_zero(
+ #[case] class: &str,
+ #[case] expected_prop: &str,
+ #[case] expected_value: &str,
+ ) {
+ let parsed = parse_single_class(class).expect("Should parse grayscale");
+ assert_eq!(parsed.property, expected_prop);
+ assert_eq!(parsed.value, expected_value);
+ }
+
+ // Wave 7.12: hue-rotate- unknown value (line 2444)
+ #[rstest]
+ #[case("hue-rotate-unknown")]
+ #[case("hue-rotate-999")]
+ #[case("hue-rotate-45")]
+ fn test_hue_rotate_unknown_returns_none(#[case] class: &str) {
+ let result = parse_single_class(class);
+ assert!(result.is_none());
+ }
+
+ // Wave 7.13: saturate- unknown value (line 2465)
+ #[rstest]
+ #[case("saturate-unknown")]
+ #[case("saturate-999")]
+ #[case("saturate-75")]
+ fn test_saturate_unknown_returns_none(#[case] class: &str) {
+ let result = parse_single_class(class);
+ assert!(result.is_none());
+ }
+
+ // ============================================================================
+ // WAVE 8: Coverage Gap Tests for Lines 816, 866, 295
+ // ============================================================================
+
+ // Wave 8.1: has_tailwind_classes with arbitrary CSS syntax (line 816)
+ // Classes with [...] that don't match any prefix trigger the arbitrary check
+ // Note: Classes with ':' get split (variant prefix removal), so avoid them
+ #[rstest]
+ #[case("custom-[value]")]
+ #[case("xyz-[test]")]
+ #[case("my-class-[10px]")]
+ #[case("foo-[bar]")]
+ fn test_has_tailwind_classes_arbitrary_css_syntax(#[case] class: &str) {
+ assert!(has_tailwind_classes(class));
+ }
+
+ // Wave 8.2: is_likely_tailwind_class with pure arbitrary syntax (line 816)
+ // These classes have brackets but don't match any standard prefix
+ // The check at line 816 triggers when a class has [...] but doesn't match prefixes
+ #[rstest]
+ #[case("custom-[value]")]
+ #[case("xyz-[100px]")]
+ #[case("my-[test]")]
+ #[case("unknown-[arbitrary]")]
+ fn test_is_likely_tailwind_class_pure_arbitrary_syntax(#[case] class: &str) {
+ assert!(is_likely_tailwind_class(class));
+ }
+
+ // Wave 8.3: parse_tailwind_to_styles integration for rounded variants (via BORDER_RADIUS_SCALE)
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_rounded_integration() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles = parse_tailwind_to_styles(
+ "rounded-none rounded-sm rounded-md rounded-lg rounded-xl rounded-2xl rounded-3xl rounded-full",
+ None,
+ );
+ assert_eq!(styles.len(), 8);
+ }
+
+ // Wave 8.4: parse_tailwind_to_styles integration for border widths (via BORDER_WIDTH_SCALE)
+ #[test]
+ #[serial]
+ fn test_parse_tailwind_to_styles_border_width_integration() {
+ reset_class_map();
+ reset_file_map();
+
+ let styles = parse_tailwind_to_styles("border border-0 border-2 border-4 border-8", None);
+ assert_eq!(styles.len(), 5);
+ }
+
+ // Wave 8.5: is_valid_tailwind_value fraction edge case (line 866)
+ // Edge case where value starts with '/' - empty first part is vacuously all-digits
+ #[rstest]
+ #[case("/2", true)]
+ #[case("/12", true)]
+ #[case("/123", true)]
+ fn test_is_valid_tailwind_value_slash_prefix_fraction(
+ #[case] value: &str,
+ #[case] expected: bool,
+ ) {
+ assert_eq!(is_valid_tailwind_value(value), expected);
+ }
+}
diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs
index 51f98253..4af7eaf1 100644
--- a/libs/extractor/src/visit.rs
+++ b/libs/extractor/src/visit.rs
@@ -407,7 +407,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
});
if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() {
- modify_prop_object(
+ let tailwind_styles = modify_prop_object(
&self.ast,
&mut obj.properties,
&mut props_styles,
@@ -416,6 +416,8 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
props,
self.split_filename.as_deref(),
);
+ // Add extracted Tailwind styles to the visitor's style set
+ self.styles.extend(tailwind_styles);
}
it.arguments[0] = Argument::from(tag);
@@ -616,7 +618,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
.rev()
.for_each(|ex| props_styles.push(ExtractStyleProp::Static(ex)));
- modify_props(
+ let tailwind_styles = modify_props(
&self.ast,
attrs,
&mut props_styles,
@@ -625,6 +627,8 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
props,
self.split_filename.as_deref(),
);
+ // Add extracted Tailwind styles to the visitor's style set
+ self.styles.extend(tailwind_styles);
props_styles
.iter()