twirlwind 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,100 +10,58 @@ Converts style objects, CSS declaration strings, and `CSSStyleDeclaration` value
10
10
  npm install twirlwind
11
11
  ```
12
12
 
13
- ## Quick start
13
+ ## Usage
14
14
 
15
15
  ```ts
16
- import { styleToClassName } from 'twirlwind'
16
+ import { twirl } from 'twirlwind'
17
17
 
18
- styleToClassName({
19
- display: 'flex',
20
- justifyContent: 'center',
21
- padding: '16px 8px',
22
- backgroundColor: 'oklch(62.3% 0.214 259.815)'
23
- })
24
- // → "flex py-4 px-2 justify-center bg-blue-500"
18
+ twirl({ display: 'flex', padding: '16px 8px', color: '#ef4444' })
19
+ // → "flex py-4 px-2 text-red-500"
25
20
  ```
26
21
 
27
- ## API
28
-
29
- ### `styleToTailwind(input, options?)`
22
+ ### Inputs
30
23
 
31
- Full conversion result with metadata.
24
+ `twirl()` accepts any of these and returns a class string:
32
25
 
33
26
  ```ts
34
- import { styleToTailwind } from 'twirlwind'
35
-
36
- const result = styleToTailwind({ display: 'flex', width: '37px' })
37
-
38
- result.className // "flex w-[37px]"
39
- result.classes // ["flex", "w-[37px]"]
40
- result.exact // ConvertedDeclaration[] — exact utility matches
41
- result.arbitrary // ConvertedDeclaration[] — arbitrary value/property fallbacks
42
- result.unmatched // CssDeclaration[] — declarations that couldn't be converted
43
- result.warnings // ConversionWarning[]
44
- ```
45
-
46
- ### `styleToClassName(input, options?)`
27
+ // Style object
28
+ twirl({ backgroundColor: 'white', fontSize: '16px' })
47
29
 
48
- Returns just the class string.
30
+ // CSS string
31
+ twirl('display: flex; padding: 16px')
49
32
 
50
- ```ts
51
- styleToClassName({ padding: '16px', color: 'white' })
52
- // → "p-4 text-white"
53
- ```
54
-
55
- ### `styleToClasses(input, options?)`
56
-
57
- Returns an array of class strings.
33
+ // CSSStyleDeclaration (browser)
34
+ twirl(element.style)
58
35
 
59
- ```ts
60
- styleToClasses({ margin: '8px 16px' })
61
- // → ["my-2", "mx-4"]
36
+ // Computed styles
37
+ twirl(getComputedStyle(element))
62
38
  ```
63
39
 
64
- ### `cssTextToTailwind(cssText, options?)`
40
+ ### Detailed result
65
41
 
66
- Parses a CSS declaration string.
42
+ Use `twirl.convert()` when you need metadata:
67
43
 
68
44
  ```ts
69
- cssTextToTailwind('display: flex; gap: 16px; padding: 8px')
70
- // → { className: "flex p-2 gap-4", ... }
71
- ```
72
-
73
- ## Input formats
74
-
75
- ```ts
76
- // Style object (camelCase or kebab-case)
77
- styleToClassName({ backgroundColor: 'white', 'font-size': '16px' })
78
-
79
- // CSS declaration string
80
- styleToClassName('display: flex; padding: 16px')
45
+ const result = twirl.convert({ display: 'flex', width: '37px' })
81
46
 
82
- // CSSStyleDeclaration (browser)
83
- styleToClassName(element.style)
84
-
85
- // Iterable of [property, value] pairs
86
- styleToClassName(
87
- new Map([
88
- ['display', 'flex'],
89
- ['padding', '16px']
90
- ])
91
- )
47
+ result.className // "flex w-[37px]"
48
+ result.classes // ["flex", "w-[37px]"]
49
+ result.exact // exact utility matches
50
+ result.arbitrary // arbitrary value/property fallbacks
51
+ result.unmatched // declarations that couldn't be converted
92
52
  ```
93
53
 
94
54
  ## Features
95
55
 
96
56
  ### Color matching
97
57
 
98
- Matches colors across formats to Tailwind's palette — OKLCH (v4), hex (v3), `rgb()`, keywords, and opacity modifiers.
58
+ Matches across formats — OKLCH, hex, `rgb()`, keywords, opacity modifiers.
99
59
 
100
60
  ```ts
101
- styleToClassName({ color: '#ef4444' }) // "text-red-500"
102
- styleToClassName({ color: 'rgb(59 130 246)' }) // "text-blue-500"
103
- styleToClassName({ color: 'oklch(62.3% 0.214 259.815)' }) // "text-blue-500"
104
- styleToClassName({ color: 'oklch(62.3% 0.214 259.815 / 50%)' }) // "text-blue-500/50"
105
- styleToClassName({ color: 'inherit' }) // "text-inherit"
106
- styleToClassName({ color: 'currentColor' }) // "text-current"
61
+ twirl({ color: '#ef4444' }) // "text-red-500"
62
+ twirl({ color: 'rgb(59 130 246)' }) // "text-blue-500"
63
+ twirl({ color: 'oklch(62.3% 0.214 259.815 / 50%)' }) // "text-blue-500/50"
64
+ twirl({ color: 'currentColor' }) // "text-current"
107
65
  ```
108
66
 
109
67
  ### Shorthand expansion
@@ -111,49 +69,39 @@ styleToClassName({ color: 'currentColor' }) // "text-current"
111
69
  CSS shorthands decompose into Tailwind longhands.
112
70
 
113
71
  ```ts
114
- styleToClassName({ border: '2px solid #ef4444' })
115
- // "border-2 border-red-500 border-solid"
116
-
117
- styleToClassName({ font: 'bold 16px/1.5 sans-serif' })
118
- // → "font-bold text-base leading-normal font-sans"
119
-
120
- styleToClassName({ background: 'white center no-repeat' })
121
- // → "bg-white bg-center bg-no-repeat"
122
-
123
- styleToClassName({ transition: 'all 200ms ease-in' })
124
- // → "transition-all duration-200 ease-in"
72
+ twirl({ border: '2px solid #ef4444' }) // "border-2 border-red-500 border-solid"
73
+ twirl({ font: 'bold 16px/1.5 sans-serif' }) // "font-bold text-base leading-normal font-sans"
74
+ twirl({ background: 'white center no-repeat' }) // "bg-white bg-center bg-no-repeat"
125
75
  ```
126
76
 
127
- ### Multi-value transforms and filters
77
+ ### Multi-value parsing
128
78
 
129
- Compound `transform` and `filter` declarations decompose into individual utility classes.
79
+ Compound `transform` and `filter` declarations decompose into individual classes.
130
80
 
131
81
  ```ts
132
- styleToClassName({ transform: 'translateX(8px) rotate(45deg)' })
133
- // "rotate-45 translate-x-2"
134
-
135
- styleToClassName({ filter: 'blur(8px) brightness(0.75)' })
136
- // → "blur brightness-75"
82
+ twirl({ transform: 'translateX(8px) rotate(45deg)' }) // "rotate-45 translate-x-2"
83
+ twirl({ filter: 'blur(8px) brightness(0.75)' }) // "blur brightness-75"
84
+ twirl({ scrollSnapType: 'x mandatory' }) // "snap-x snap-mandatory"
137
85
  ```
138
86
 
139
87
  ### Compression
140
88
 
141
- Expanded longhands compress to shorthand utilities when values match.
89
+ Expanded longhands compress to shorthand utilities.
142
90
 
143
91
  ```ts
144
- styleToClassName({ margin: '8px' }) // "m-2" (not "mt-2 mr-2 mb-2 ml-2")
145
- styleToClassName({ inset: '0' }) // "inset-0"
146
- styleToClassName({ padding: '8px 16px' }) // "py-2 px-4"
147
- styleToClassName({ borderRadius: '8px' }) // "rounded-lg"
148
- styleToClassName({ gap: '12px 12px' }) // "gap-3"
92
+ twirl({ margin: '8px' }) // "m-2"
93
+ twirl({ inset: '0' }) // "inset-0"
94
+ twirl({ padding: '8px 16px' }) // "py-2 px-4"
95
+ twirl({ borderRadius: '8px' }) // "rounded-lg"
96
+ twirl({ gap: '12px 12px' }) // "gap-3"
149
97
  ```
150
98
 
151
99
  ### Variants
152
100
 
153
- Nested objects map to Tailwind variants — pseudo-classes, media queries, and container queries.
101
+ Nested objects map to Tailwind variants.
154
102
 
155
103
  ```ts
156
- styleToClassName({
104
+ twirl({
157
105
  color: 'white',
158
106
  ':hover': { color: '#3b82f6' },
159
107
  '@media (min-width: 768px)': { display: 'grid' },
@@ -163,48 +111,25 @@ styleToClassName({
163
111
  // → "text-white hover:text-blue-500 md:grid dark:bg-black @lg:flex"
164
112
  ```
165
113
 
166
- ### Scroll snap decomposition
167
-
168
- ```ts
169
- styleToClassName({ scrollSnapType: 'x mandatory' })
170
- // → "snap-x snap-mandatory"
171
- ```
172
-
173
114
  ### Arbitrary fallback
174
115
 
175
- Every CSS property produces valid output. Unknown properties use `[property:value]` syntax.
116
+ Every CSS property produces valid output.
176
117
 
177
118
  ```ts
178
- styleToClassName({ scrollTimelineName: '--main' })
179
- // "[scroll-timeline-name:--main]"
180
-
181
- styleToClassName({ width: '37px' })
182
- // → "w-[37px]"
119
+ twirl({ scrollTimelineName: '--main' }) // "[scroll-timeline-name:--main]"
120
+ twirl({ width: '37px' }) // "w-[37px]"
183
121
  ```
184
122
 
185
123
  ## Options
186
124
 
187
125
  ```ts
188
- styleToTailwind(input, {
189
- // Allow arbitrary value utilities like w-[37px]
126
+ twirl(input, {
190
127
  allowArbitraryValues: true, // default: true
191
-
192
- // Allow arbitrary property fallbacks like [scroll-timeline-name:--x]
193
128
  allowArbitraryProperties: true, // default: true
194
-
195
- // Compress matching longhands to shorthands
196
129
  compression: 'safe', // "none" | "safe" | "aggressive"
197
-
198
- // Sort output classes
199
130
  sort: 'grouped', // "input" | "tailwind" | "grouped"
200
-
201
- // Color matching strategy
202
131
  colorMatch: 'exact', // "exact" | "nearest" | "none"
203
-
204
- // Spacing token multiplier mode
205
132
  numericMultipliers: 'integer', // "all" | "integer" | "never"
206
-
207
- // Custom theme colors/spacing
208
133
  theme: {
209
134
  colors: { brand: '#ff6600' },
210
135
  spacing: { '18': '4.5rem' }
@@ -215,9 +140,9 @@ styleToTailwind(input, {
215
140
  ## How it works
216
141
 
217
142
  1. **Normalize** — camelCase → kebab-case, numeric → px, vendor prefixes, `!important`
218
- 2. **Expand shorthands** — `margin`, `border`, `font`, `background`, `transition`, `overflow`, `gap`, etc.
143
+ 2. **Expand** — `margin`, `border`, `font`, `background`, `transition`, `overflow`, `gap`, etc.
219
144
  3. **Convert** — exact utility → value alias → spacing token → color match → arbitrary value → arbitrary property
220
- 4. **Compress** — merge matching longhands back to axis/all shorthand utilities
145
+ 4. **Compress** — merge longhands back to shorthand utilities
221
146
  5. **Sort** — deterministic output ordering
222
147
 
223
148
  ## License
package/dist/index.d.mts CHANGED
@@ -4,55 +4,42 @@ interface StyleObject {
4
4
  [property: string]: StylePrimitive | StyleObject;
5
5
  }
6
6
  type StyleInput = string | StyleObject | CSSStyleDeclaration | Iterable<[string, StylePrimitive]>;
7
- type ConversionMode = 'pretty' | 'lossless' | 'strict';
8
- type SortMode = 'input' | 'tailwind' | 'grouped';
9
- type CompressionMode = 'none' | 'safe' | 'aggressive';
10
- type ColorMatchMode = 'exact' | 'nearest' | 'none';
11
- type NumericMultiplierMode = 'all' | 'integer' | 'never';
12
- type TwirlwindTheme = {
7
+ type Theme = {
13
8
  spacing?: Record<string, string>;
14
9
  colors?: Record<string, string>;
15
10
  };
16
- type StyleToTailwindOptions = {
17
- mode?: ConversionMode;
18
- tailwindVersion?: '4';
19
- theme?: TwirlwindTheme;
11
+ type Options = {
12
+ theme?: Theme;
20
13
  allowArbitraryValues?: boolean;
21
14
  allowArbitraryProperties?: boolean;
22
- preferThemeTokens?: boolean;
23
- compression?: CompressionMode;
24
- sort?: SortMode;
15
+ compression?: 'none' | 'safe' | 'aggressive';
16
+ sort?: 'input' | 'tailwind' | 'grouped';
25
17
  important?: boolean;
26
- colorMatch?: ColorMatchMode;
27
- numericMultipliers?: NumericMultiplierMode;
18
+ colorMatch?: 'exact' | 'nearest' | 'none';
19
+ numericMultipliers?: 'all' | 'integer' | 'never';
28
20
  };
29
- type CssDeclaration = {
21
+ type Declaration = {
30
22
  property: string;
31
23
  value: string;
32
24
  important: boolean;
33
25
  variants: string[];
34
26
  };
35
- type ConvertedDeclaration = CssDeclaration & {
27
+ type ConvertedDeclaration = Declaration & {
36
28
  className: string;
37
29
  kind: 'exact' | 'arbitrary';
38
30
  };
39
- type ConversionWarning = {
40
- declaration: CssDeclaration;
41
- message: string;
42
- };
43
- type StyleToTailwindResult = {
31
+ type Result = {
44
32
  className: string;
45
33
  classes: string[];
46
34
  exact: ConvertedDeclaration[];
47
35
  arbitrary: ConvertedDeclaration[];
48
- unmatched: CssDeclaration[];
49
- warnings: ConversionWarning[];
36
+ unmatched: Declaration[];
50
37
  };
51
38
  //#endregion
52
39
  //#region src/index.d.ts
53
- declare function styleToTailwind(input: StyleInput, options?: StyleToTailwindOptions): StyleToTailwindResult;
54
- declare function styleToClassName(input: StyleInput, options?: StyleToTailwindOptions): string;
55
- declare function styleToClasses(input: StyleInput, options?: StyleToTailwindOptions): string[];
56
- declare function cssTextToTailwind(cssText: string, options?: StyleToTailwindOptions): StyleToTailwindResult;
40
+ declare function twirl(input: StyleInput, options?: Options): string;
41
+ declare namespace twirl {
42
+ var convert: (input: StyleInput, options?: Options) => Result;
43
+ }
57
44
  //#endregion
58
- export { type ConvertedDeclaration, type CssDeclaration, type StyleInput, type StyleObject, type StylePrimitive, type StyleToTailwindOptions, type StyleToTailwindResult, type TwirlwindTheme, cssTextToTailwind, styleToClassName, styleToClasses, styleToTailwind };
45
+ export { type ConvertedDeclaration, type Declaration, type Options, type Result, type StyleInput, type StyleObject, type StylePrimitive, type Theme, twirl };
package/dist/index.mjs CHANGED
@@ -1316,6 +1316,25 @@ const exactUtilities = {
1316
1316
  contain: "overscroll-y-contain",
1317
1317
  none: "overscroll-y-none"
1318
1318
  },
1319
+ "scrollbar-width": {
1320
+ auto: "scrollbar-auto",
1321
+ thin: "scrollbar-thin",
1322
+ none: "scrollbar-none"
1323
+ },
1324
+ "scrollbar-gutter": {
1325
+ stable: "scrollbar-gutter-stable",
1326
+ "stable both-edges": "scrollbar-gutter-both-edges"
1327
+ },
1328
+ "mask-type": {
1329
+ alpha: "mask-type-alpha",
1330
+ luminance: "mask-type-luminance"
1331
+ },
1332
+ "mask-composite": {
1333
+ add: "mask-composite-add",
1334
+ subtract: "mask-composite-subtract",
1335
+ intersect: "mask-composite-intersect",
1336
+ exclude: "mask-composite-exclude"
1337
+ },
1319
1338
  "scroll-behavior": {
1320
1339
  auto: "scroll-auto",
1321
1340
  smooth: "scroll-smooth"
@@ -1419,6 +1438,24 @@ const exactUtilities = {
1419
1438
  "0 0": "origin-top-left",
1420
1439
  "0% 0%": "origin-top-left"
1421
1440
  },
1441
+ "perspective-origin": {
1442
+ center: "perspective-origin-center",
1443
+ top: "perspective-origin-top",
1444
+ "top right": "perspective-origin-top-right",
1445
+ right: "perspective-origin-right",
1446
+ "bottom right": "perspective-origin-bottom-right",
1447
+ bottom: "perspective-origin-bottom",
1448
+ "bottom left": "perspective-origin-bottom-left",
1449
+ left: "perspective-origin-left",
1450
+ "top left": "perspective-origin-top-left",
1451
+ "100% 0": "perspective-origin-top-right",
1452
+ "100% 0%": "perspective-origin-top-right",
1453
+ "100% 100%": "perspective-origin-bottom-right",
1454
+ "0 100%": "perspective-origin-bottom-left",
1455
+ "0% 100%": "perspective-origin-bottom-left",
1456
+ "0 0": "perspective-origin-top-left",
1457
+ "0% 0%": "perspective-origin-top-left"
1458
+ },
1422
1459
  "scroll-snap-align": {
1423
1460
  start: "snap-start",
1424
1461
  end: "snap-end",
@@ -1668,6 +1705,9 @@ const arbitraryPrefixes = {
1668
1705
  "flex-shrink": "shrink",
1669
1706
  "word-spacing": "word-spacing",
1670
1707
  hyphens: "hyphens",
1708
+ "scrollbar-color": "scrollbar-color",
1709
+ "font-feature-settings": "font-feature",
1710
+ "perspective-origin": "perspective-origin",
1671
1711
  "column-rule-color": "column-rule",
1672
1712
  "column-rule-width": "column-rule",
1673
1713
  "column-rule-style": "column-rule",
@@ -2214,6 +2254,15 @@ function convertDeclaration(declaration, options) {
2214
2254
  if (Array.isArray(filterResult)) return filterResult.map((cls) => converted(declaration, cls, "exact"));
2215
2255
  return converted(declaration, filterResult, "exact");
2216
2256
  }
2257
+ const shadowClass = convertShadow(declaration);
2258
+ if (shadowClass) return converted(declaration, shadowClass, "exact");
2259
+ const gradientResult = convertGradient(declaration, options);
2260
+ if (gradientResult) {
2261
+ if (Array.isArray(gradientResult)) return gradientResult.map((cls) => converted(declaration, cls, "exact"));
2262
+ return converted(declaration, gradientResult, "exact");
2263
+ }
2264
+ const varRef = convertVarReference(declaration);
2265
+ if (varRef) return converted(declaration, varRef, "exact");
2217
2266
  const prefix = arbitraryPrefixes[declaration.property];
2218
2267
  if (prefix && options.allowArbitraryValues) return converted(declaration, arbitraryValue(prefix, declaration.value), "arbitrary");
2219
2268
  if (options.allowArbitraryProperties) return converted(declaration, arbitraryProperty(declaration.property, declaration.value), "arbitrary");
@@ -2522,6 +2571,9 @@ function convertTransform(declaration, options) {
2522
2571
  if (declaration.property === "scale-x") return scaleClass(declaration.value, "scale-x");
2523
2572
  if (declaration.property === "scale-y") return scaleClass(declaration.value, "scale-y");
2524
2573
  if (declaration.property === "scale-z") return scaleClass(declaration.value, "scale-z");
2574
+ if (declaration.property === "skew") return angleClass("skew", declaration.value);
2575
+ if (declaration.property === "skew-x") return angleClass("skew-x", declaration.value);
2576
+ if (declaration.property === "skew-y") return angleClass("skew-y", declaration.value);
2525
2577
  if (declaration.property === "translate") return translateClass(declaration.value, options);
2526
2578
  if (declaration.property === "translate-x") return translateAxisClass("translate-x", declaration.value, options);
2527
2579
  if (declaration.property === "translate-y") return translateAxisClass("translate-y", declaration.value, options);
@@ -2623,6 +2675,135 @@ function scaleClass(value, prefix = "scale") {
2623
2675
  if (!Number.isInteger(percent)) return `${prefix}-[${value.trim()}]`;
2624
2676
  return percent < 0 ? `-${prefix}-${Math.abs(percent)}` : `${prefix}-${percent}`;
2625
2677
  }
2678
+ const shadowValues = {
2679
+ "0 1px 2px 0 rgb(0 0 0 / 0.05)": "shadow-xs",
2680
+ "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)": "shadow-sm",
2681
+ "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)": "shadow-md",
2682
+ "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)": "shadow-lg",
2683
+ "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)": "shadow-xl",
2684
+ "0 25px 50px -12px rgb(0 0 0 / 0.25)": "shadow-2xl",
2685
+ "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)": "shadow-inner"
2686
+ };
2687
+ function convertShadow(declaration) {
2688
+ if (declaration.property !== "box-shadow") return void 0;
2689
+ return shadowValues[declaration.value.trim().replace(/\s+/g, " ")];
2690
+ }
2691
+ const varPrefixes = {
2692
+ color: "text",
2693
+ "background-color": "bg",
2694
+ "border-color": "border",
2695
+ "border-top-color": "border-t",
2696
+ "border-right-color": "border-r",
2697
+ "border-bottom-color": "border-b",
2698
+ "border-left-color": "border-l",
2699
+ "outline-color": "outline",
2700
+ fill: "fill",
2701
+ stroke: "stroke",
2702
+ "caret-color": "caret",
2703
+ "accent-color": "accent",
2704
+ "text-decoration-color": "decoration",
2705
+ width: "w",
2706
+ height: "h",
2707
+ "min-width": "min-w",
2708
+ "min-height": "min-h",
2709
+ "max-width": "max-w",
2710
+ "max-height": "max-h",
2711
+ padding: "p",
2712
+ "padding-top": "pt",
2713
+ "padding-right": "pr",
2714
+ "padding-bottom": "pb",
2715
+ "padding-left": "pl",
2716
+ margin: "m",
2717
+ "margin-top": "mt",
2718
+ "margin-right": "mr",
2719
+ "margin-bottom": "mb",
2720
+ "margin-left": "ml",
2721
+ gap: "gap",
2722
+ "row-gap": "gap-y",
2723
+ "column-gap": "gap-x",
2724
+ "font-size": "text",
2725
+ "font-family": "font",
2726
+ "border-radius": "rounded",
2727
+ "border-width": "border",
2728
+ "box-shadow": "shadow",
2729
+ "line-height": "leading",
2730
+ "letter-spacing": "tracking"
2731
+ };
2732
+ function convertVarReference(declaration) {
2733
+ const match = declaration.value.match(/^var\(\s*(--[\w-]+)\s*\)$/);
2734
+ if (!match?.[1]) return void 0;
2735
+ const prefix = varPrefixes[declaration.property];
2736
+ if (prefix) return `${prefix}-(${match[1]})`;
2737
+ }
2738
+ const gradientDirections = {
2739
+ "to right": "bg-linear-to-r",
2740
+ "to left": "bg-linear-to-l",
2741
+ "to top": "bg-linear-to-t",
2742
+ "to bottom": "bg-linear-to-b",
2743
+ "to top right": "bg-linear-to-tr",
2744
+ "to top left": "bg-linear-to-tl",
2745
+ "to bottom right": "bg-linear-to-br",
2746
+ "to bottom left": "bg-linear-to-bl"
2747
+ };
2748
+ function convertGradient(declaration, options) {
2749
+ if (declaration.property !== "background-image" && declaration.property !== "background") return void 0;
2750
+ const linearMatch = declaration.value.trim().match(/^linear-gradient\((.+)\)$/);
2751
+ if (!linearMatch?.[1]) return void 0;
2752
+ const inner = linearMatch[1];
2753
+ const firstComma = findTopLevelComma(inner);
2754
+ if (firstComma === -1) return void 0;
2755
+ const directionPart = inner.slice(0, firstComma).trim();
2756
+ const colorsPart = inner.slice(firstComma + 1).trim();
2757
+ const dirClass = gradientDirections[directionPart] ?? (directionPart.match(/^\d+deg$/) ? `bg-linear-${directionPart.replace("deg", "")}` : void 0);
2758
+ if (!dirClass) return void 0;
2759
+ const colorStops = splitGradientStops(colorsPart);
2760
+ if (colorStops.length < 2 || colorStops.length > 3) return void 0;
2761
+ const classes = [dirClass];
2762
+ const fromColor = matchGradientColor(colorStops[0] ?? "", "from", options);
2763
+ if (!fromColor) return void 0;
2764
+ classes.push(fromColor);
2765
+ if (colorStops.length === 3) {
2766
+ const viaColor = matchGradientColor(colorStops[1] ?? "", "via", options);
2767
+ if (!viaColor) return void 0;
2768
+ classes.push(viaColor);
2769
+ }
2770
+ const toColor = matchGradientColor(colorStops[colorStops.length - 1] ?? "", "to", options);
2771
+ if (!toColor) return void 0;
2772
+ classes.push(toColor);
2773
+ return classes;
2774
+ }
2775
+ function matchGradientColor(stop, prefix, options) {
2776
+ const color = stop.trim().split(/\s+/)[0];
2777
+ if (!color) return void 0;
2778
+ const normalized = normalizeColor(color);
2779
+ const keyword = colorKeywords[normalized];
2780
+ if (keyword) return `${prefix}-${keyword}`;
2781
+ const hexToken = lookupHexToken(normalized);
2782
+ if (hexToken) return `${prefix}-${hexToken}`;
2783
+ const token = Object.entries(options.theme.colors).find(([, c]) => normalizeColor(c) === normalized)?.[0];
2784
+ if (token) return `${prefix}-${token}`;
2785
+ return `${prefix}-[${color}]`;
2786
+ }
2787
+ function findTopLevelComma(value) {
2788
+ let depth = 0;
2789
+ for (let i = 0; i < value.length; i++) if (value[i] === "(") depth++;
2790
+ else if (value[i] === ")") depth--;
2791
+ else if (value[i] === "," && depth === 0) return i;
2792
+ return -1;
2793
+ }
2794
+ function splitGradientStops(value) {
2795
+ const stops = [];
2796
+ let depth = 0;
2797
+ let start = 0;
2798
+ for (let i = 0; i < value.length; i++) if (value[i] === "(") depth++;
2799
+ else if (value[i] === ")") depth--;
2800
+ else if (value[i] === "," && depth === 0) {
2801
+ stops.push(value.slice(start, i).trim());
2802
+ start = i + 1;
2803
+ }
2804
+ stops.push(value.slice(start).trim());
2805
+ return stops.filter(Boolean);
2806
+ }
2626
2807
  function converted(declaration, className, kind) {
2627
2808
  const important = declaration.important ? "!" : "";
2628
2809
  const variants = declaration.variants.length > 0 ? `${declaration.variants.join(":")}:` : "";
@@ -3482,8 +3663,6 @@ const defaultTheme = {
3482
3663
  };
3483
3664
  function resolveOptions(options = {}) {
3484
3665
  return {
3485
- mode: options.mode ?? "pretty",
3486
- tailwindVersion: "4",
3487
3666
  theme: {
3488
3667
  spacing: {
3489
3668
  ...defaultTheme.spacing,
@@ -3496,7 +3675,6 @@ function resolveOptions(options = {}) {
3496
3675
  },
3497
3676
  allowArbitraryValues: options.allowArbitraryValues ?? true,
3498
3677
  allowArbitraryProperties: options.allowArbitraryProperties ?? true,
3499
- preferThemeTokens: options.preferThemeTokens ?? true,
3500
3678
  compression: options.compression ?? "safe",
3501
3679
  sort: options.sort ?? "grouped",
3502
3680
  important: options.important ?? false,
@@ -3506,40 +3684,27 @@ function resolveOptions(options = {}) {
3506
3684
  }
3507
3685
  //#endregion
3508
3686
  //#region src/index.ts
3509
- function styleToTailwind(input, options) {
3510
- const resolvedOptions = resolveOptions(options);
3511
- const conversionPairs = expandShorthands(normalizeInput(input)).map((declaration) => ({
3687
+ function convert(input, options) {
3688
+ const resolved = resolveOptions(options);
3689
+ const pairs = expandShorthands(normalizeInput(input)).map((declaration) => ({
3512
3690
  declaration,
3513
- converted: convertDeclaration(declaration, resolvedOptions)
3691
+ converted: convertDeclaration(declaration, resolved)
3514
3692
  }));
3515
- const convertedDeclarations = sortConverted(compressConverted(conversionPairs.flatMap(({ converted }) => {
3693
+ const sorted = sortConverted(compressConverted(pairs.flatMap(({ converted }) => {
3516
3694
  if (!converted) return [];
3517
3695
  return Array.isArray(converted) ? converted : [converted];
3518
- }), resolvedOptions), resolvedOptions);
3519
- const exact = convertedDeclarations.filter((converted) => converted.kind === "exact");
3520
- const arbitrary = convertedDeclarations.filter((converted) => converted.kind === "arbitrary");
3521
- const unmatched = conversionPairs.flatMap(({ declaration, converted }) => converted ? [] : [declaration]);
3522
- const classes = convertedDeclarations.map(({ className }) => className);
3696
+ }), resolved), resolved);
3523
3697
  return {
3524
- className: classes.join(" "),
3525
- classes,
3526
- exact,
3527
- arbitrary,
3528
- unmatched,
3529
- warnings: unmatched.map((declaration) => ({
3530
- declaration,
3531
- message: "No Tailwind utility or fallback could be emitted."
3532
- }))
3698
+ className: sorted.map(({ className }) => className).join(" "),
3699
+ classes: sorted.map(({ className }) => className),
3700
+ exact: sorted.filter((c) => c.kind === "exact"),
3701
+ arbitrary: sorted.filter((c) => c.kind === "arbitrary"),
3702
+ unmatched: pairs.flatMap(({ declaration, converted }) => converted ? [] : [declaration])
3533
3703
  };
3534
3704
  }
3535
- function styleToClassName(input, options) {
3536
- return styleToTailwind(input, options).className;
3537
- }
3538
- function styleToClasses(input, options) {
3539
- return styleToTailwind(input, options).classes;
3540
- }
3541
- function cssTextToTailwind(cssText, options) {
3542
- return styleToTailwind(cssText, options);
3705
+ function twirl(input, options) {
3706
+ return convert(input, options).className;
3543
3707
  }
3708
+ twirl.convert = convert;
3544
3709
  //#endregion
3545
- export { cssTextToTailwind, styleToClassName, styleToClasses, styleToTailwind };
3710
+ export { twirl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twirlwind",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Tailwind v4-first CSS style object to utility class serializer with lossless arbitrary-property fallback.",
5
5
  "keywords": [
6
6
  "converter",