@csszyx/dynamic 0.7.0 → 0.8.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/dist/index.d.mts +92 -0
- package/dist/index.mjs +2 -0
- package/dist/react.d.mts +86 -0
- package/dist/react.mjs +37 -0
- package/dist/shared/dynamic.BLXLKBNg.mjs +1149 -0
- package/package.json +22 -8
- package/src/css-generator.ts +0 -1003
- package/src/index.ts +0 -122
- package/src/injector.ts +0 -226
- package/src/manifest.ts +0 -119
- package/src/react.ts +0 -166
- package/src/ssr.ts +0 -5
- package/tests/css-generator.test.ts +0 -339
- package/tests/injector.test.ts +0 -222
- package/tests/manifest.test.ts +0 -177
- package/tests/react.test.ts +0 -221
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -8
package/src/css-generator.ts
DELETED
|
@@ -1,1003 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSS Rule Generator for @csszyx/dynamic.
|
|
3
|
-
*
|
|
4
|
-
* Takes a Tailwind class name (e.g. "p-4", "hover:bg-blue-500", "sm:flex")
|
|
5
|
-
* and generates the CSS rule string ready to inject into a CSSStyleSheet.
|
|
6
|
-
*
|
|
7
|
-
* Uses Tailwind v4 CSS custom property conventions:
|
|
8
|
-
* - Spacing: calc(var(--spacing) * N)
|
|
9
|
-
* - Colors: var(--color-{name}-{shade})
|
|
10
|
-
* - Text sizes: var(--text-{size})
|
|
11
|
-
* - Border radii: var(--radius-{size})
|
|
12
|
-
*
|
|
13
|
-
* Returns empty string for unknown classes — graceful no-op, not a crash.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ── Variant metadata ──────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
/** Breakpoint tiers (min-width values, Tailwind v4 defaults). */
|
|
19
|
-
export const BREAKPOINTS: Record<string, string> = {
|
|
20
|
-
sm: '40rem',
|
|
21
|
-
md: '48rem',
|
|
22
|
-
lg: '64rem',
|
|
23
|
-
xl: '80rem',
|
|
24
|
-
'2xl': '96rem',
|
|
25
|
-
'max-sm': '40rem',
|
|
26
|
-
'max-md': '48rem',
|
|
27
|
-
'max-lg': '64rem',
|
|
28
|
-
'max-xl': '80rem',
|
|
29
|
-
'max-2xl': '96rem',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Pseudo-class suffixes for common state variants. */
|
|
33
|
-
const PSEUDO_CLASS_MAP: Record<string, string> = {
|
|
34
|
-
hover: ':hover',
|
|
35
|
-
focus: ':focus',
|
|
36
|
-
'focus-visible': ':focus-visible',
|
|
37
|
-
'focus-within': ':focus-within',
|
|
38
|
-
active: ':active',
|
|
39
|
-
visited: ':visited',
|
|
40
|
-
disabled: ':disabled',
|
|
41
|
-
checked: ':checked',
|
|
42
|
-
required: ':required',
|
|
43
|
-
optional: ':optional',
|
|
44
|
-
valid: ':valid',
|
|
45
|
-
invalid: ':invalid',
|
|
46
|
-
placeholder: '::placeholder',
|
|
47
|
-
before: '::before',
|
|
48
|
-
after: '::after',
|
|
49
|
-
first: ':first-child',
|
|
50
|
-
last: ':last-child',
|
|
51
|
-
odd: ':nth-child(odd)',
|
|
52
|
-
even: ':nth-child(even)',
|
|
53
|
-
empty: ':empty',
|
|
54
|
-
open: '[open]',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// ── Spacing utilities ─────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Maps Tailwind spacing utility prefix → CSS logical/physical properties (Tailwind v4).
|
|
61
|
-
* Tailwind v4 uses logical properties (padding-inline, margin-block, etc.).
|
|
62
|
-
*/
|
|
63
|
-
const SPACING_PROPS: Record<string, string[]> = {
|
|
64
|
-
// Padding
|
|
65
|
-
p: ['padding'],
|
|
66
|
-
pt: ['padding-top'],
|
|
67
|
-
pr: ['padding-right'],
|
|
68
|
-
pb: ['padding-bottom'],
|
|
69
|
-
pl: ['padding-left'],
|
|
70
|
-
px: ['padding-inline'],
|
|
71
|
-
py: ['padding-block'],
|
|
72
|
-
ps: ['padding-inline-start'],
|
|
73
|
-
pe: ['padding-inline-end'],
|
|
74
|
-
pbs: ['padding-block-start'],
|
|
75
|
-
pbe: ['padding-block-end'],
|
|
76
|
-
// Margin
|
|
77
|
-
m: ['margin'],
|
|
78
|
-
mt: ['margin-top'],
|
|
79
|
-
mr: ['margin-right'],
|
|
80
|
-
mb: ['margin-bottom'],
|
|
81
|
-
ml: ['margin-left'],
|
|
82
|
-
mx: ['margin-inline'],
|
|
83
|
-
my: ['margin-block'],
|
|
84
|
-
ms: ['margin-inline-start'],
|
|
85
|
-
me: ['margin-inline-end'],
|
|
86
|
-
mbs: ['margin-block-start'],
|
|
87
|
-
mbe: ['margin-block-end'],
|
|
88
|
-
// Gap
|
|
89
|
-
gap: ['gap'],
|
|
90
|
-
'gap-x': ['column-gap'],
|
|
91
|
-
'gap-y': ['row-gap'],
|
|
92
|
-
// Sizing
|
|
93
|
-
w: ['width'],
|
|
94
|
-
h: ['height'],
|
|
95
|
-
'min-w': ['min-width'],
|
|
96
|
-
'max-w': ['max-width'],
|
|
97
|
-
'min-h': ['min-height'],
|
|
98
|
-
'max-h': ['max-height'],
|
|
99
|
-
size: ['width', 'height'],
|
|
100
|
-
// Position
|
|
101
|
-
top: ['top'],
|
|
102
|
-
right: ['right'],
|
|
103
|
-
bottom: ['bottom'],
|
|
104
|
-
left: ['left'],
|
|
105
|
-
inset: ['inset'],
|
|
106
|
-
'inset-x': ['inset-inline'],
|
|
107
|
-
'inset-y': ['inset-block'],
|
|
108
|
-
'inset-s': ['inset-inline-start'],
|
|
109
|
-
'inset-e': ['inset-inline-end'],
|
|
110
|
-
// Flex
|
|
111
|
-
basis: ['flex-basis'],
|
|
112
|
-
// Typography
|
|
113
|
-
indent: ['text-indent'],
|
|
114
|
-
'outline-offset': ['outline-offset'],
|
|
115
|
-
'underline-offset': ['text-underline-offset'],
|
|
116
|
-
// Scroll
|
|
117
|
-
'scroll-m': ['scroll-margin'],
|
|
118
|
-
'scroll-mt': ['scroll-margin-top'],
|
|
119
|
-
'scroll-mr': ['scroll-margin-right'],
|
|
120
|
-
'scroll-mb': ['scroll-margin-bottom'],
|
|
121
|
-
'scroll-ml': ['scroll-margin-left'],
|
|
122
|
-
'scroll-mx': ['scroll-margin-inline'],
|
|
123
|
-
'scroll-my': ['scroll-margin-block'],
|
|
124
|
-
'scroll-ms': ['scroll-margin-inline-start'],
|
|
125
|
-
'scroll-me': ['scroll-margin-inline-end'],
|
|
126
|
-
'scroll-p': ['scroll-padding'],
|
|
127
|
-
'scroll-pt': ['scroll-padding-top'],
|
|
128
|
-
'scroll-pr': ['scroll-padding-right'],
|
|
129
|
-
'scroll-pb': ['scroll-padding-bottom'],
|
|
130
|
-
'scroll-pl': ['scroll-padding-left'],
|
|
131
|
-
'scroll-px': ['scroll-padding-inline'],
|
|
132
|
-
'scroll-py': ['scroll-padding-block'],
|
|
133
|
-
'scroll-ps': ['scroll-padding-inline-start'],
|
|
134
|
-
'scroll-pe': ['scroll-padding-inline-end'],
|
|
135
|
-
// Border spacing (CSS custom properties in Tailwind v4)
|
|
136
|
-
'border-spacing-x': ['--tw-border-spacing-x'],
|
|
137
|
-
'border-spacing-y': ['--tw-border-spacing-y'],
|
|
138
|
-
// Ring offset (CSS custom property)
|
|
139
|
-
'ring-offset': ['--tw-ring-offset-width'],
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/** CSS keyword values for size properties (non-numeric). */
|
|
143
|
-
const SIZE_KEYWORDS: Record<string, string> = {
|
|
144
|
-
auto: 'auto',
|
|
145
|
-
full: '100%',
|
|
146
|
-
screen: '100vw', // width context; height context uses 100vh — see below
|
|
147
|
-
svw: '100svw',
|
|
148
|
-
dvw: '100dvw',
|
|
149
|
-
lvw: '100lvw',
|
|
150
|
-
svh: '100svh',
|
|
151
|
-
dvh: '100dvh',
|
|
152
|
-
lvh: '100lvh',
|
|
153
|
-
fit: 'fit-content',
|
|
154
|
-
min: 'min-content',
|
|
155
|
-
max: 'max-content',
|
|
156
|
-
none: 'none',
|
|
157
|
-
px: '1px',
|
|
158
|
-
'0': '0',
|
|
159
|
-
xs: 'var(--container-xs)',
|
|
160
|
-
sm: 'var(--container-sm)',
|
|
161
|
-
md: 'var(--container-md)',
|
|
162
|
-
lg: 'var(--container-lg)',
|
|
163
|
-
xl: 'var(--container-xl)',
|
|
164
|
-
'2xl': 'var(--container-2xl)',
|
|
165
|
-
'3xl': 'var(--container-3xl)',
|
|
166
|
-
'4xl': 'var(--container-4xl)',
|
|
167
|
-
'5xl': 'var(--container-5xl)',
|
|
168
|
-
'6xl': 'var(--container-6xl)',
|
|
169
|
-
'7xl': 'var(--container-7xl)',
|
|
170
|
-
prose: 'var(--container-prose)',
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Resolves a Tailwind spacing/size value to a CSS value string.
|
|
175
|
-
* Handles: 0, px, auto, full, numeric, fraction, arbitrary.
|
|
176
|
-
*
|
|
177
|
-
* @param v - Tailwind spacing value token (e.g. "4", "px", "auto", "[13px]")
|
|
178
|
-
* @param prop - CSS property name, used to disambiguate "screen" (100vh vs 100vw)
|
|
179
|
-
* @returns CSS value string (e.g. "calc(var(--spacing) * 4)", "1px", "auto")
|
|
180
|
-
*/
|
|
181
|
-
function resolveSpacingValue(v: string, prop?: string): string {
|
|
182
|
-
if (v === '0') {return '0';}
|
|
183
|
-
if (v === 'px') {return '1px';}
|
|
184
|
-
|
|
185
|
-
// Height screen → 100vh, width screen → 100vw
|
|
186
|
-
if (v === 'screen') {
|
|
187
|
-
if (prop && (prop === 'height' || prop === 'min-height' || prop === 'max-height')) {
|
|
188
|
-
return '100vh';
|
|
189
|
-
}
|
|
190
|
-
return '100vw';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (v in SIZE_KEYWORDS) {return SIZE_KEYWORDS[v];}
|
|
194
|
-
|
|
195
|
-
// Arbitrary value: [13px], [calc(100%-2rem)], etc.
|
|
196
|
-
if (v.startsWith('[') && v.endsWith(']')) {
|
|
197
|
-
return v.slice(1, -1).replace(/_/g, ' ');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// CSS variable shorthand: (--my-var) → var(--my-var)
|
|
201
|
-
if (v.startsWith('(') && v.endsWith(')') && v.includes('--')) {
|
|
202
|
-
return `var(${v.slice(1, -1)})`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Fraction: 1/2 → 50%
|
|
206
|
-
if (v.includes('/')) {
|
|
207
|
-
const slash = v.indexOf('/');
|
|
208
|
-
const num = parseFloat(v.slice(0, slash));
|
|
209
|
-
const den = parseFloat(v.slice(slash + 1));
|
|
210
|
-
if (!isNaN(num) && !isNaN(den) && den !== 0) {
|
|
211
|
-
const pct = (num / den) * 100;
|
|
212
|
-
return `${parseFloat(pct.toFixed(6))}%`;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Negative numeric: -4 → calc(var(--spacing) * -4)
|
|
217
|
-
if (v.startsWith('-') && !isNaN(parseFloat(v.slice(1)))) {
|
|
218
|
-
return `calc(var(--spacing) * ${v})`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Numeric: 4 → calc(var(--spacing) * 4)
|
|
222
|
-
if (!isNaN(parseFloat(v))) {
|
|
223
|
-
return `calc(var(--spacing) * ${v})`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return v;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── Color utilities ───────────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
/** Maps Tailwind color utility prefix → CSS property. */
|
|
232
|
-
const COLOR_PROPS: Record<string, string> = {
|
|
233
|
-
bg: 'background-color',
|
|
234
|
-
text: 'color',
|
|
235
|
-
border: 'border-color',
|
|
236
|
-
'border-t': 'border-top-color',
|
|
237
|
-
'border-r': 'border-right-color',
|
|
238
|
-
'border-b': 'border-bottom-color',
|
|
239
|
-
'border-l': 'border-left-color',
|
|
240
|
-
'border-x': 'border-inline-color',
|
|
241
|
-
'border-y': 'border-block-color',
|
|
242
|
-
'border-s': 'border-inline-start-color',
|
|
243
|
-
'border-e': 'border-inline-end-color',
|
|
244
|
-
divide: 'border-color',
|
|
245
|
-
outline: 'outline-color',
|
|
246
|
-
fill: 'fill',
|
|
247
|
-
stroke: 'stroke',
|
|
248
|
-
from: '--tw-gradient-from',
|
|
249
|
-
via: '--tw-gradient-via',
|
|
250
|
-
to: '--tw-gradient-to',
|
|
251
|
-
decoration: 'text-decoration-color',
|
|
252
|
-
accent: 'accent-color',
|
|
253
|
-
caret: 'caret-color',
|
|
254
|
-
shadow: '--tw-shadow-color',
|
|
255
|
-
'inset-shadow': '--tw-inset-shadow-color',
|
|
256
|
-
ring: '--tw-ring-color',
|
|
257
|
-
'ring-offset': '--tw-ring-offset-color',
|
|
258
|
-
'drop-shadow': '--tw-drop-shadow-color',
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
/** Colors that don't use CSS custom properties in Tailwind v4. */
|
|
262
|
-
const DIRECT_COLOR_KEYWORDS = new Set([
|
|
263
|
-
'white', 'black', 'transparent', 'inherit', 'current', 'currentColor',
|
|
264
|
-
]);
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Resolves a Tailwind color value to a CSS value string.
|
|
268
|
-
* Handles: named scales (blue-500), keywords, arbitrary, opacity modifiers.
|
|
269
|
-
*
|
|
270
|
-
* @param v - Tailwind color value token (e.g. "blue-500", "white", "[#ff6b35]", "blue-500/50")
|
|
271
|
-
* @returns CSS value string (e.g. "var(--color-blue-500)", "white", "color-mix(...)")
|
|
272
|
-
*/
|
|
273
|
-
function resolveColorValue(v: string): string {
|
|
274
|
-
if (DIRECT_COLOR_KEYWORDS.has(v)) {
|
|
275
|
-
if (v === 'current') {return 'currentColor';}
|
|
276
|
-
return v;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Arbitrary: [#ff6b35], [color:var(--my)]
|
|
280
|
-
if (v.startsWith('[') && v.endsWith(']')) {
|
|
281
|
-
const inner = v.slice(1, -1).replace(/_/g, ' ');
|
|
282
|
-
if (inner.startsWith('color:')) {return inner.slice(6);}
|
|
283
|
-
return inner;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// CSS variable shorthand: (--my-color) → var(--my-color)
|
|
287
|
-
if (v.startsWith('(') && v.endsWith(')') && v.includes('--')) {
|
|
288
|
-
return `var(${v.slice(1, -1)})`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Opacity modifier: blue-500/50 → color-mix(...)
|
|
292
|
-
const slashIdx = v.lastIndexOf('/');
|
|
293
|
-
if (slashIdx > 0) {
|
|
294
|
-
const colorPart = v.slice(0, slashIdx);
|
|
295
|
-
const opacity = v.slice(slashIdx + 1);
|
|
296
|
-
const colorVar = DIRECT_COLOR_KEYWORDS.has(colorPart)
|
|
297
|
-
? colorPart
|
|
298
|
-
: `var(--color-${colorPart})`;
|
|
299
|
-
const opacityPct = v.includes('.') ? `${parseFloat(opacity) * 100}%` : `${opacity}%`;
|
|
300
|
-
return `color-mix(in srgb, ${colorVar} ${opacityPct}, transparent)`;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return `var(--color-${v})`;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── Text size utilities ───────────────────────────────────────────────────────
|
|
307
|
-
|
|
308
|
-
/** Named Tailwind v4 text sizes → CSS var. */
|
|
309
|
-
const TEXT_SIZES = new Set([
|
|
310
|
-
'xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', '8xl', '9xl',
|
|
311
|
-
]);
|
|
312
|
-
|
|
313
|
-
// ── Keyword class lookup ──────────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Static keyword classes with known CSS output.
|
|
317
|
-
* Only covers classes commonly needed at runtime (form renderers, layout).
|
|
318
|
-
* Build-time classes are always in the manifest → never reach the generator.
|
|
319
|
-
*/
|
|
320
|
-
const KEYWORD_RULES: Record<string, string> = {
|
|
321
|
-
// Display
|
|
322
|
-
flex: 'display: flex',
|
|
323
|
-
'inline-flex': 'display: inline-flex',
|
|
324
|
-
block: 'display: block',
|
|
325
|
-
'inline-block': 'display: inline-block',
|
|
326
|
-
inline: 'display: inline',
|
|
327
|
-
grid: 'display: grid',
|
|
328
|
-
'inline-grid': 'display: inline-grid',
|
|
329
|
-
hidden: 'display: none',
|
|
330
|
-
contents: 'display: contents',
|
|
331
|
-
'flow-root': 'display: flow-root',
|
|
332
|
-
table: 'display: table',
|
|
333
|
-
'table-row': 'display: table-row',
|
|
334
|
-
'table-cell': 'display: table-cell',
|
|
335
|
-
'list-item': 'display: list-item',
|
|
336
|
-
// Position
|
|
337
|
-
static: 'position: static',
|
|
338
|
-
fixed: 'position: fixed',
|
|
339
|
-
absolute: 'position: absolute',
|
|
340
|
-
relative: 'position: relative',
|
|
341
|
-
sticky: 'position: sticky',
|
|
342
|
-
// Visibility
|
|
343
|
-
visible: 'visibility: visible',
|
|
344
|
-
invisible: 'visibility: hidden',
|
|
345
|
-
collapse: 'visibility: collapse',
|
|
346
|
-
// Overflow
|
|
347
|
-
'overflow-auto': 'overflow: auto',
|
|
348
|
-
'overflow-hidden': 'overflow: hidden',
|
|
349
|
-
'overflow-visible': 'overflow: visible',
|
|
350
|
-
'overflow-scroll': 'overflow: scroll',
|
|
351
|
-
'overflow-clip': 'overflow: clip',
|
|
352
|
-
'overflow-x-auto': 'overflow-x: auto',
|
|
353
|
-
'overflow-x-hidden': 'overflow-x: hidden',
|
|
354
|
-
'overflow-y-auto': 'overflow-y: auto',
|
|
355
|
-
'overflow-y-hidden': 'overflow-y: hidden',
|
|
356
|
-
'overflow-y-scroll': 'overflow-y: scroll',
|
|
357
|
-
'overflow-x-scroll': 'overflow-x: scroll',
|
|
358
|
-
// Flex direction
|
|
359
|
-
'flex-row': 'flex-direction: row',
|
|
360
|
-
'flex-col': 'flex-direction: column',
|
|
361
|
-
'flex-row-reverse': 'flex-direction: row-reverse',
|
|
362
|
-
'flex-col-reverse': 'flex-direction: column-reverse',
|
|
363
|
-
// Flex wrap
|
|
364
|
-
'flex-wrap': 'flex-wrap: wrap',
|
|
365
|
-
'flex-nowrap': 'flex-wrap: nowrap',
|
|
366
|
-
'flex-wrap-reverse': 'flex-wrap: wrap-reverse',
|
|
367
|
-
// Flex sizing
|
|
368
|
-
'flex-1': 'flex: 1 1 0%',
|
|
369
|
-
'flex-auto': 'flex: 1 1 auto',
|
|
370
|
-
'flex-none': 'flex: none',
|
|
371
|
-
// Align
|
|
372
|
-
'items-start': 'align-items: flex-start',
|
|
373
|
-
'items-center': 'align-items: center',
|
|
374
|
-
'items-end': 'align-items: flex-end',
|
|
375
|
-
'items-stretch': 'align-items: stretch',
|
|
376
|
-
'items-baseline': 'align-items: baseline',
|
|
377
|
-
'self-start': 'align-self: flex-start',
|
|
378
|
-
'self-center': 'align-self: center',
|
|
379
|
-
'self-end': 'align-self: flex-end',
|
|
380
|
-
'self-stretch': 'align-self: stretch',
|
|
381
|
-
'self-auto': 'align-self: auto',
|
|
382
|
-
// Justify
|
|
383
|
-
'justify-start': 'justify-content: flex-start',
|
|
384
|
-
'justify-center': 'justify-content: center',
|
|
385
|
-
'justify-end': 'justify-content: flex-end',
|
|
386
|
-
'justify-between': 'justify-content: space-between',
|
|
387
|
-
'justify-around': 'justify-content: space-around',
|
|
388
|
-
'justify-evenly': 'justify-content: space-evenly',
|
|
389
|
-
'justify-stretch': 'justify-content: stretch',
|
|
390
|
-
'justify-items-start': 'justify-items: start',
|
|
391
|
-
'justify-items-center': 'justify-items: center',
|
|
392
|
-
'justify-items-end': 'justify-items: end',
|
|
393
|
-
'justify-items-stretch': 'justify-items: stretch',
|
|
394
|
-
// Place
|
|
395
|
-
'place-content-center': 'place-content: center',
|
|
396
|
-
'place-content-start': 'place-content: start',
|
|
397
|
-
'place-content-end': 'place-content: end',
|
|
398
|
-
'place-content-between': 'place-content: space-between',
|
|
399
|
-
'place-content-around': 'place-content: space-around',
|
|
400
|
-
'place-content-evenly': 'place-content: space-evenly',
|
|
401
|
-
'place-items-center': 'place-items: center',
|
|
402
|
-
'place-items-start': 'place-items: start',
|
|
403
|
-
'place-items-end': 'place-items: end',
|
|
404
|
-
'place-items-stretch': 'place-items: stretch',
|
|
405
|
-
// Width / Height special
|
|
406
|
-
'w-auto': 'width: auto',
|
|
407
|
-
'w-full': 'width: 100%',
|
|
408
|
-
'w-screen': 'width: 100vw',
|
|
409
|
-
'w-svw': 'width: 100svw',
|
|
410
|
-
'w-dvw': 'width: 100dvw',
|
|
411
|
-
'w-min': 'width: min-content',
|
|
412
|
-
'w-max': 'width: max-content',
|
|
413
|
-
'w-fit': 'width: fit-content',
|
|
414
|
-
'h-auto': 'height: auto',
|
|
415
|
-
'h-full': 'height: 100%',
|
|
416
|
-
'h-screen': 'height: 100vh',
|
|
417
|
-
'h-svh': 'height: 100svh',
|
|
418
|
-
'h-dvh': 'height: 100dvh',
|
|
419
|
-
'h-min': 'height: min-content',
|
|
420
|
-
'h-max': 'height: max-content',
|
|
421
|
-
'h-fit': 'height: fit-content',
|
|
422
|
-
'min-h-0': 'min-height: 0',
|
|
423
|
-
'min-h-full': 'min-height: 100%',
|
|
424
|
-
'min-h-screen': 'min-height: 100vh',
|
|
425
|
-
'max-h-full': 'max-height: 100%',
|
|
426
|
-
'max-h-screen': 'max-height: 100vh',
|
|
427
|
-
'max-h-none': 'max-height: none',
|
|
428
|
-
'min-w-0': 'min-width: 0',
|
|
429
|
-
'min-w-full': 'min-width: 100%',
|
|
430
|
-
'max-w-full': 'max-width: 100%',
|
|
431
|
-
'max-w-none': 'max-width: none',
|
|
432
|
-
'max-w-screen': 'max-width: 100vw',
|
|
433
|
-
// Font weight keywords
|
|
434
|
-
'font-thin': 'font-weight: 100',
|
|
435
|
-
'font-extralight': 'font-weight: 200',
|
|
436
|
-
'font-light': 'font-weight: 300',
|
|
437
|
-
'font-normal': 'font-weight: 400',
|
|
438
|
-
'font-medium': 'font-weight: 500',
|
|
439
|
-
'font-semibold': 'font-weight: 600',
|
|
440
|
-
'font-bold': 'font-weight: 700',
|
|
441
|
-
'font-extrabold': 'font-weight: 800',
|
|
442
|
-
'font-black': 'font-weight: 900',
|
|
443
|
-
// Font style
|
|
444
|
-
'italic': 'font-style: italic',
|
|
445
|
-
'not-italic': 'font-style: normal',
|
|
446
|
-
// Text align
|
|
447
|
-
'text-left': 'text-align: left',
|
|
448
|
-
'text-center': 'text-align: center',
|
|
449
|
-
'text-right': 'text-align: right',
|
|
450
|
-
'text-justify': 'text-align: justify',
|
|
451
|
-
'text-start': 'text-align: start',
|
|
452
|
-
'text-end': 'text-align: end',
|
|
453
|
-
// Text transform
|
|
454
|
-
'uppercase': 'text-transform: uppercase',
|
|
455
|
-
'lowercase': 'text-transform: lowercase',
|
|
456
|
-
'capitalize': 'text-transform: capitalize',
|
|
457
|
-
'normal-case': 'text-transform: none',
|
|
458
|
-
// Text decoration
|
|
459
|
-
'underline': 'text-decoration-line: underline',
|
|
460
|
-
'overline': 'text-decoration-line: overline',
|
|
461
|
-
'line-through': 'text-decoration-line: line-through',
|
|
462
|
-
'no-underline': 'text-decoration-line: none',
|
|
463
|
-
// Text wrap
|
|
464
|
-
'text-wrap': 'text-wrap: wrap',
|
|
465
|
-
'text-nowrap': 'text-wrap: nowrap',
|
|
466
|
-
'text-balance': 'text-wrap: balance',
|
|
467
|
-
'text-pretty': 'text-wrap: pretty',
|
|
468
|
-
// Whitespace
|
|
469
|
-
'whitespace-normal': 'white-space: normal',
|
|
470
|
-
'whitespace-nowrap': 'white-space: nowrap',
|
|
471
|
-
'whitespace-pre': 'white-space: pre',
|
|
472
|
-
'whitespace-pre-line': 'white-space: pre-line',
|
|
473
|
-
'whitespace-pre-wrap': 'white-space: pre-wrap',
|
|
474
|
-
'whitespace-break-spaces': 'white-space: break-spaces',
|
|
475
|
-
// Border style
|
|
476
|
-
'border-solid': 'border-style: solid',
|
|
477
|
-
'border-dashed': 'border-style: dashed',
|
|
478
|
-
'border-dotted': 'border-style: dotted',
|
|
479
|
-
'border-double': 'border-style: double',
|
|
480
|
-
'border-none': 'border-style: none',
|
|
481
|
-
// Rounded special values
|
|
482
|
-
'rounded-none': 'border-radius: 0',
|
|
483
|
-
'rounded-full': 'border-radius: calc(infinity * 1px)',
|
|
484
|
-
// Cursor
|
|
485
|
-
'cursor-auto': 'cursor: auto',
|
|
486
|
-
'cursor-default': 'cursor: default',
|
|
487
|
-
'cursor-pointer': 'cursor: pointer',
|
|
488
|
-
'cursor-wait': 'cursor: wait',
|
|
489
|
-
'cursor-text': 'cursor: text',
|
|
490
|
-
'cursor-move': 'cursor: move',
|
|
491
|
-
'cursor-not-allowed': 'cursor: not-allowed',
|
|
492
|
-
'cursor-crosshair': 'cursor: crosshair',
|
|
493
|
-
'cursor-grab': 'cursor: grab',
|
|
494
|
-
'cursor-grabbing': 'cursor: grabbing',
|
|
495
|
-
// Pointer events
|
|
496
|
-
'pointer-events-none': 'pointer-events: none',
|
|
497
|
-
'pointer-events-auto': 'pointer-events: auto',
|
|
498
|
-
// User select
|
|
499
|
-
'select-none': 'user-select: none',
|
|
500
|
-
'select-text': 'user-select: text',
|
|
501
|
-
'select-all': 'user-select: all',
|
|
502
|
-
'select-auto': 'user-select: auto',
|
|
503
|
-
// Object fit
|
|
504
|
-
'object-contain': 'object-fit: contain',
|
|
505
|
-
'object-cover': 'object-fit: cover',
|
|
506
|
-
'object-fill': 'object-fit: fill',
|
|
507
|
-
'object-none': 'object-fit: none',
|
|
508
|
-
'object-scale-down': 'object-fit: scale-down',
|
|
509
|
-
// Truncate
|
|
510
|
-
'truncate': 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap',
|
|
511
|
-
'text-ellipsis': 'text-overflow: ellipsis',
|
|
512
|
-
'text-clip': 'text-overflow: clip',
|
|
513
|
-
// Grow / shrink
|
|
514
|
-
'grow': 'flex-grow: 1',
|
|
515
|
-
'grow-0': 'flex-grow: 0',
|
|
516
|
-
'shrink': 'flex-shrink: 1',
|
|
517
|
-
'shrink-0': 'flex-shrink: 0',
|
|
518
|
-
// Appearance
|
|
519
|
-
'appearance-none': 'appearance: none',
|
|
520
|
-
'appearance-auto': 'appearance: auto',
|
|
521
|
-
// Isolate
|
|
522
|
-
'isolate': 'isolation: isolate',
|
|
523
|
-
'isolation-auto': 'isolation: auto',
|
|
524
|
-
// List style
|
|
525
|
-
'list-none': 'list-style-type: none',
|
|
526
|
-
'list-disc': 'list-style-type: disc',
|
|
527
|
-
'list-decimal': 'list-style-type: decimal',
|
|
528
|
-
// Box sizing
|
|
529
|
-
'box-border': 'box-sizing: border-box',
|
|
530
|
-
'box-content': 'box-sizing: content-box',
|
|
531
|
-
// Float
|
|
532
|
-
'float-left': 'float: left',
|
|
533
|
-
'float-right': 'float: right',
|
|
534
|
-
'float-none': 'float: none',
|
|
535
|
-
'float-start': 'float: inline-start',
|
|
536
|
-
'float-end': 'float: inline-end',
|
|
537
|
-
'clear-left': 'clear: left',
|
|
538
|
-
'clear-right': 'clear: right',
|
|
539
|
-
'clear-both': 'clear: both',
|
|
540
|
-
'clear-none': 'clear: none',
|
|
541
|
-
// SR only
|
|
542
|
-
'sr-only': 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border-width: 0',
|
|
543
|
-
'not-sr-only': 'position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; white-space: normal',
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
// ── Opacity utilities ─────────────────────────────────────────────────────────
|
|
547
|
-
|
|
548
|
-
/** Named opacity values (Tailwind uses 0-100 scale). */
|
|
549
|
-
const OPACITY_NAMED: Record<string, string> = {
|
|
550
|
-
0: '0', 5: '0.05', 10: '0.1', 15: '0.15', 20: '0.2', 25: '0.25',
|
|
551
|
-
30: '0.3', 35: '0.35', 40: '0.4', 45: '0.45', 50: '0.5',
|
|
552
|
-
55: '0.55', 60: '0.6', 65: '0.65', 70: '0.7', 75: '0.75',
|
|
553
|
-
80: '0.8', 85: '0.85', 90: '0.9', 95: '0.95', 100: '1',
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// ── Border radius utilities ───────────────────────────────────────────────────
|
|
557
|
-
|
|
558
|
-
const RADIUS_SIZES = new Set([
|
|
559
|
-
'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl',
|
|
560
|
-
]);
|
|
561
|
-
|
|
562
|
-
// ── CSS escaping ──────────────────────────────────────────────────────────────
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Escapes a Tailwind class name for use as a CSS selector.
|
|
566
|
-
* @param cls - Tailwind class name to escape
|
|
567
|
-
* @returns escaped CSS selector string safe to use in a rule
|
|
568
|
-
*/
|
|
569
|
-
function escapeCSSSelector(cls: string): string {
|
|
570
|
-
// Escape: : / [ ] . # @ ( ) % + ~ = | ^ $ * ,
|
|
571
|
-
return cls.replace(/[^a-zA-Z0-9\-_]/g, c => `\\${c}`);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// ── Variant parsing ───────────────────────────────────────────────────────────
|
|
575
|
-
|
|
576
|
-
const MIN_BREAKPOINTS = new Set(['sm', 'md', 'lg', 'xl', '2xl']);
|
|
577
|
-
const MAX_BREAKPOINTS = new Set(['max-sm', 'max-md', 'max-lg', 'max-xl', 'max-2xl']);
|
|
578
|
-
const CONTAINER_MIN = new Set(['@sm', '@md', '@lg', '@xl', '@2xl']);
|
|
579
|
-
const CONTAINER_MAX = new Set(['@max-sm', '@max-md', '@max-lg', '@max-xl', '@max-2xl']);
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
*
|
|
583
|
-
*/
|
|
584
|
-
export type Tier =
|
|
585
|
-
| 'base'
|
|
586
|
-
| 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
587
|
-
| 'max-2xl' | 'max-xl' | 'max-lg' | 'max-md' | 'max-sm'
|
|
588
|
-
| '@sm' | '@md' | '@lg' | '@xl' | '@2xl'
|
|
589
|
-
| '@max-2xl' | '@max-xl' | '@max-lg' | '@max-md' | '@max-sm';
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
*
|
|
593
|
-
*/
|
|
594
|
-
export interface ParsedVariants {
|
|
595
|
-
tier: Tier;
|
|
596
|
-
/** Pseudo-class suffix to append to selector (e.g. ":hover"). */
|
|
597
|
-
pseudoSuffix: string;
|
|
598
|
-
/** For dark: variant — selector prefix (e.g. ".dark "). */
|
|
599
|
-
selectorPrefix: string;
|
|
600
|
-
/** The base utility class name (e.g. "p-4", "bg-blue-500"). */
|
|
601
|
-
utility: string;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Parses variant prefixes from a Tailwind class name.
|
|
606
|
-
* Handles stacked variants: sm:hover:bg-blue-600 → tier=sm, pseudo=:hover, utility=bg-blue-600
|
|
607
|
-
*
|
|
608
|
-
* Tailwind stacking convention: breakpoint FIRST, then state variant.
|
|
609
|
-
* e.g. sm:hover: → @media(min-width: 40rem) { :hover { ... } }
|
|
610
|
-
*
|
|
611
|
-
* @param cls - full Tailwind class name including variant prefixes (e.g. "sm:hover:bg-blue-600")
|
|
612
|
-
* @returns parsed variant metadata including tier, pseudoSuffix, selectorPrefix, and utility
|
|
613
|
-
*/
|
|
614
|
-
export function parseVariants(cls: string): ParsedVariants {
|
|
615
|
-
const parts = cls.split(':');
|
|
616
|
-
let tier: Tier = 'base';
|
|
617
|
-
let pseudoSuffix = '';
|
|
618
|
-
let selectorPrefix = '';
|
|
619
|
-
let utilityIdx = 0;
|
|
620
|
-
|
|
621
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
622
|
-
const variant = parts[i];
|
|
623
|
-
|
|
624
|
-
if (MIN_BREAKPOINTS.has(variant)) {
|
|
625
|
-
tier = variant as Tier;
|
|
626
|
-
utilityIdx = i + 1;
|
|
627
|
-
} else if (MAX_BREAKPOINTS.has(variant)) {
|
|
628
|
-
tier = variant as Tier;
|
|
629
|
-
utilityIdx = i + 1;
|
|
630
|
-
} else if (CONTAINER_MIN.has(variant)) {
|
|
631
|
-
tier = variant as Tier;
|
|
632
|
-
utilityIdx = i + 1;
|
|
633
|
-
} else if (CONTAINER_MAX.has(variant)) {
|
|
634
|
-
tier = variant as Tier;
|
|
635
|
-
utilityIdx = i + 1;
|
|
636
|
-
} else if (variant === 'dark') {
|
|
637
|
-
selectorPrefix = '.dark ';
|
|
638
|
-
utilityIdx = i + 1;
|
|
639
|
-
} else if (variant === 'light') {
|
|
640
|
-
selectorPrefix = '.light ';
|
|
641
|
-
utilityIdx = i + 1;
|
|
642
|
-
} else if (variant in PSEUDO_CLASS_MAP) {
|
|
643
|
-
pseudoSuffix += PSEUDO_CLASS_MAP[variant];
|
|
644
|
-
utilityIdx = i + 1;
|
|
645
|
-
} else {
|
|
646
|
-
// Unknown variant (e.g. group-hover, aria-*, data-*) — keep as tier=base
|
|
647
|
-
utilityIdx = i + 1;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const utility = parts.slice(utilityIdx).join(':');
|
|
652
|
-
return { tier, pseudoSuffix, selectorPrefix, utility };
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// ── Main generator ────────────────────────────────────────────────────────────
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Generates a CSS rule body (without selector) for a utility class.
|
|
659
|
-
* Returns empty string for unknown/unsupported classes.
|
|
660
|
-
*
|
|
661
|
-
* @param utility - the base Tailwind utility (e.g. "p-4", "bg-blue-500", "flex")
|
|
662
|
-
* @returns CSS declarations string (e.g. "padding: calc(var(--spacing) * 4)")
|
|
663
|
-
*/
|
|
664
|
-
export function generateDeclarations(utility: string): string {
|
|
665
|
-
// ── 1. Keyword lookup (fastest path) ───────────────────────────────────
|
|
666
|
-
if (utility in KEYWORD_RULES) {return KEYWORD_RULES[utility];}
|
|
667
|
-
|
|
668
|
-
// ── 2. Opacity ──────────────────────────────────────────────────────────
|
|
669
|
-
if (utility.startsWith('opacity-')) {
|
|
670
|
-
const val = utility.slice(8);
|
|
671
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
672
|
-
return `opacity: ${val.slice(1, -1)}`;
|
|
673
|
-
}
|
|
674
|
-
const n = parseInt(val, 10);
|
|
675
|
-
if (!isNaN(n)) {
|
|
676
|
-
const v = OPACITY_NAMED[n] ?? String(n / 100);
|
|
677
|
-
return `opacity: ${v}`;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ── 3. Z-index ──────────────────────────────────────────────────────────
|
|
682
|
-
if (utility.startsWith('z-')) {
|
|
683
|
-
const val = utility.slice(2);
|
|
684
|
-
if (val === 'auto') {return 'z-index: auto';}
|
|
685
|
-
if (val.startsWith('[') && val.endsWith(']')) {return `z-index: ${val.slice(1, -1)}`;}
|
|
686
|
-
if (!isNaN(parseInt(val, 10))) {return `z-index: ${val}`;}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// ── 4. Border width ──────────────────────────────────────────────────────
|
|
690
|
-
if (utility === 'border') {return 'border-width: 1px';}
|
|
691
|
-
if (/^border-[trblxyse]$/.test(utility)) {
|
|
692
|
-
const side = utility.slice(7);
|
|
693
|
-
const cssSide: Record<string, string> = {
|
|
694
|
-
t: 'border-top-width', r: 'border-right-width',
|
|
695
|
-
b: 'border-bottom-width', l: 'border-left-width',
|
|
696
|
-
x: 'border-inline-width', y: 'border-block-width',
|
|
697
|
-
s: 'border-inline-start-width', e: 'border-inline-end-width',
|
|
698
|
-
};
|
|
699
|
-
if (side in cssSide) {return `${cssSide[side]}: 1px`;}
|
|
700
|
-
}
|
|
701
|
-
if (/^border-\d+$/.test(utility)) {
|
|
702
|
-
const n = utility.slice(7);
|
|
703
|
-
return `border-width: ${n}px`;
|
|
704
|
-
}
|
|
705
|
-
// border-t-2, border-r-4, etc.
|
|
706
|
-
const borderSideWidth = utility.match(/^border-([trblxse])-(\d+)$/);
|
|
707
|
-
if (borderSideWidth) {
|
|
708
|
-
const [, side, n] = borderSideWidth;
|
|
709
|
-
const cssSide: Record<string, string> = {
|
|
710
|
-
t: 'border-top-width', r: 'border-right-width',
|
|
711
|
-
b: 'border-bottom-width', l: 'border-left-width',
|
|
712
|
-
x: 'border-inline-width', y: 'border-block-width',
|
|
713
|
-
s: 'border-inline-start-width', e: 'border-inline-end-width',
|
|
714
|
-
};
|
|
715
|
-
if (side in cssSide) {return `${cssSide[side]}: ${n}px`;}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// ── 5. Border radius ────────────────────────────────────────────────────
|
|
719
|
-
if (utility === 'rounded') {return 'border-radius: var(--radius)';}
|
|
720
|
-
if (utility.startsWith('rounded-')) {
|
|
721
|
-
const val = utility.slice(8);
|
|
722
|
-
if (val === 'none') {return 'border-radius: 0';}
|
|
723
|
-
if (val === 'full') {return 'border-radius: calc(infinity * 1px)';}
|
|
724
|
-
if (RADIUS_SIZES.has(val)) {return `border-radius: var(--radius-${val})`;}
|
|
725
|
-
// Directional: rounded-t, rounded-b, etc.
|
|
726
|
-
const roundedDir: Record<string, string> = {
|
|
727
|
-
t: 'border-top-left-radius: var(--radius); border-top-right-radius: var(--radius)',
|
|
728
|
-
r: 'border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius)',
|
|
729
|
-
b: 'border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius)',
|
|
730
|
-
l: 'border-top-left-radius: var(--radius); border-bottom-left-radius: var(--radius)',
|
|
731
|
-
tl: 'border-top-left-radius: var(--radius)',
|
|
732
|
-
tr: 'border-top-right-radius: var(--radius)',
|
|
733
|
-
bl: 'border-bottom-left-radius: var(--radius)',
|
|
734
|
-
br: 'border-bottom-right-radius: var(--radius)',
|
|
735
|
-
};
|
|
736
|
-
if (val in roundedDir) {return roundedDir[val];}
|
|
737
|
-
// rounded-t-lg, rounded-tr-sm, etc.
|
|
738
|
-
const m = val.match(/^([trblse]+)-(.+)$/);
|
|
739
|
-
if (m) {
|
|
740
|
-
const [, dir, size] = m;
|
|
741
|
-
const sizeVal = RADIUS_SIZES.has(size)
|
|
742
|
-
? `var(--radius-${size})`
|
|
743
|
-
: size === 'full' ? 'calc(infinity * 1px)' : size === 'none' ? '0' : size;
|
|
744
|
-
if (dir in roundedDir) {
|
|
745
|
-
return roundedDir[dir].replace(/var\(--radius\)/g, sizeVal);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
749
|
-
return `border-radius: ${val.slice(1, -1)}`;
|
|
750
|
-
}
|
|
751
|
-
if (RADIUS_SIZES.has(val)) {return `border-radius: var(--radius-${val})`;}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// ── 6. Text size ────────────────────────────────────────────────────────
|
|
755
|
-
if (utility.startsWith('text-') && !utility.startsWith('text-opacity')) {
|
|
756
|
-
const val = utility.slice(5);
|
|
757
|
-
// Text color: handled by color utilities below
|
|
758
|
-
// Text size: xs, sm, base, lg, xl, 2xl, ...
|
|
759
|
-
if (TEXT_SIZES.has(val)) {
|
|
760
|
-
return `font-size: var(--text-${val}); line-height: var(--tw-leading, var(--text-${val}--line-height))`;
|
|
761
|
-
}
|
|
762
|
-
// Arbitrary text size
|
|
763
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
764
|
-
return `font-size: ${val.slice(1, -1).replace(/_/g, ' ')}`;
|
|
765
|
-
}
|
|
766
|
-
// Color (text-blue-500, text-white, etc.) — falls through to color section
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// ── 7. Leading (line-height) ────────────────────────────────────────────
|
|
770
|
-
if (utility.startsWith('leading-')) {
|
|
771
|
-
const val = utility.slice(8);
|
|
772
|
-
const named: Record<string, string> = {
|
|
773
|
-
none: '1', tight: '1.25', snug: '1.375', normal: '1.5',
|
|
774
|
-
relaxed: '1.625', loose: '2',
|
|
775
|
-
};
|
|
776
|
-
if (val in named) {return `line-height: ${named[val]}`;}
|
|
777
|
-
if (val.startsWith('[') && val.endsWith(']')) {return `line-height: ${val.slice(1, -1)}`;}
|
|
778
|
-
if (!isNaN(parseFloat(val))) {return `line-height: calc(var(--spacing) * ${val})`;}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// ── 8. Tracking (letter-spacing) ───────────────────────────────────────
|
|
782
|
-
if (utility.startsWith('tracking-')) {
|
|
783
|
-
const val = utility.slice(9);
|
|
784
|
-
const named: Record<string, string> = {
|
|
785
|
-
tighter: 'var(--tracking-tighter)',
|
|
786
|
-
tight: 'var(--tracking-tight)',
|
|
787
|
-
normal: 'var(--tracking-normal)',
|
|
788
|
-
wide: 'var(--tracking-wide)',
|
|
789
|
-
wider: 'var(--tracking-wider)',
|
|
790
|
-
widest: 'var(--tracking-widest)',
|
|
791
|
-
};
|
|
792
|
-
if (val in named) {return `letter-spacing: ${named[val]}`;}
|
|
793
|
-
if (val.startsWith('[') && val.endsWith(']')) {return `letter-spacing: ${val.slice(1, -1)}`;}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// ── 9. Font family ──────────────────────────────────────────────────────
|
|
797
|
-
if (utility.startsWith('font-') && !KEYWORD_RULES[utility]) {
|
|
798
|
-
const val = utility.slice(5);
|
|
799
|
-
const familyNames: Record<string, string> = {
|
|
800
|
-
sans: 'var(--font-sans, ui-sans-serif, system-ui, sans-serif)',
|
|
801
|
-
serif: 'var(--font-serif, ui-serif, Georgia, serif)',
|
|
802
|
-
mono: 'var(--font-mono, ui-monospace, SFMono-Regular, monospace)',
|
|
803
|
-
};
|
|
804
|
-
if (val in familyNames) {return `font-family: ${familyNames[val]}`;}
|
|
805
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
806
|
-
return `font-family: ${val.slice(1, -1).replace(/_/g, ' ')}`;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// ── 10. Shadow ──────────────────────────────────────────────────────────
|
|
811
|
-
if (utility === 'shadow') {return 'box-shadow: var(--shadow)';}
|
|
812
|
-
if (utility.startsWith('shadow-')) {
|
|
813
|
-
const val = utility.slice(7);
|
|
814
|
-
if (val.startsWith('[') && val.endsWith(']')) {
|
|
815
|
-
return `box-shadow: ${val.slice(1, -1).replace(/_/g, ' ')}`;
|
|
816
|
-
}
|
|
817
|
-
const shadows = new Set(['xs', 'sm', 'md', 'lg', 'xl', '2xl', 'none', 'inner']);
|
|
818
|
-
if (shadows.has(val)) {
|
|
819
|
-
return val === 'none' ? 'box-shadow: none' : `box-shadow: var(--shadow-${val})`;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// ── 11. Outline ─────────────────────────────────────────────────────────
|
|
824
|
-
if (utility === 'outline-none') {return 'outline: 2px solid transparent; outline-offset: 2px';}
|
|
825
|
-
if (utility.startsWith('outline-')) {
|
|
826
|
-
const val = utility.slice(8);
|
|
827
|
-
if (/^\d+$/.test(val)) {return `outline-width: ${val}px`;}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// ── 12. Ring ────────────────────────────────────────────────────────────
|
|
831
|
-
if (utility === 'ring') {return '--tw-ring-shadow: 0 0 0 3px var(--tw-ring-color, #3b82f680)';}
|
|
832
|
-
if (utility.startsWith('ring-')) {
|
|
833
|
-
const val = utility.slice(5);
|
|
834
|
-
if (/^\d+$/.test(val)) {return `--tw-ring-shadow: 0 0 0 ${val}px var(--tw-ring-color, #3b82f680)`;}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// ── 13. Grow / shrink numbers ───────────────────────────────────────────
|
|
838
|
-
if (utility.startsWith('grow-')) {return `flex-grow: ${utility.slice(5)}`;}
|
|
839
|
-
if (utility.startsWith('shrink-')) {return `flex-shrink: ${utility.slice(7)}`;}
|
|
840
|
-
|
|
841
|
-
// ── 14. Order ───────────────────────────────────────────────────────────
|
|
842
|
-
if (utility.startsWith('order-')) {
|
|
843
|
-
const val = utility.slice(6);
|
|
844
|
-
if (val === 'first') {return 'order: -9999';}
|
|
845
|
-
if (val === 'last') {return 'order: 9999';}
|
|
846
|
-
if (val === 'none') {return 'order: 0';}
|
|
847
|
-
return `order: ${val}`;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// ── 15. Columns ─────────────────────────────────────────────────────────
|
|
851
|
-
if (utility.startsWith('columns-')) {
|
|
852
|
-
const val = utility.slice(8);
|
|
853
|
-
if (!isNaN(parseInt(val))) {return `columns: ${val}`;}
|
|
854
|
-
return `columns: var(--container-${val})`;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// ── 16. Grid cols/rows ──────────────────────────────────────────────────
|
|
858
|
-
if (utility.startsWith('grid-cols-')) {
|
|
859
|
-
const val = utility.slice(10);
|
|
860
|
-
if (val === 'none') {return 'grid-template-columns: none';}
|
|
861
|
-
if (val === 'subgrid') {return 'grid-template-columns: subgrid';}
|
|
862
|
-
if (val.startsWith('[')) {return `grid-template-columns: ${val.slice(1, -1).replace(/_/g, ' ')}`;}
|
|
863
|
-
return `grid-template-columns: repeat(${val}, minmax(0, 1fr))`;
|
|
864
|
-
}
|
|
865
|
-
if (utility.startsWith('grid-rows-')) {
|
|
866
|
-
const val = utility.slice(10);
|
|
867
|
-
if (val === 'none') {return 'grid-template-rows: none';}
|
|
868
|
-
if (val === 'subgrid') {return 'grid-template-rows: subgrid';}
|
|
869
|
-
if (val.startsWith('[')) {return `grid-template-rows: ${val.slice(1, -1).replace(/_/g, ' ')}`;}
|
|
870
|
-
return `grid-template-rows: repeat(${val}, minmax(0, 1fr))`;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// ── 17. Col/row span ────────────────────────────────────────────────────
|
|
874
|
-
if (utility.startsWith('col-span-')) {
|
|
875
|
-
const val = utility.slice(9);
|
|
876
|
-
return val === 'full' ? 'grid-column: 1 / -1' : `grid-column: span ${val} / span ${val}`;
|
|
877
|
-
}
|
|
878
|
-
if (utility.startsWith('row-span-')) {
|
|
879
|
-
const val = utility.slice(9);
|
|
880
|
-
return val === 'full' ? 'grid-row: 1 / -1' : `grid-row: span ${val} / span ${val}`;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// ── 18. Scale / rotate / translate (CSS transforms in v4) ───────────────
|
|
884
|
-
if (utility.startsWith('scale-x-')) {
|
|
885
|
-
const val = utility.slice(8);
|
|
886
|
-
return `--tw-scale-x: ${parseFloat(val) / 100}; scale: var(--tw-scale-x) var(--tw-scale-y, 1)`;
|
|
887
|
-
}
|
|
888
|
-
if (utility.startsWith('scale-y-')) {
|
|
889
|
-
const val = utility.slice(8);
|
|
890
|
-
return `--tw-scale-y: ${parseFloat(val) / 100}; scale: var(--tw-scale-x, 1) var(--tw-scale-y)`;
|
|
891
|
-
}
|
|
892
|
-
if (utility.startsWith('scale-')) {
|
|
893
|
-
const val = utility.slice(6);
|
|
894
|
-
const n = parseFloat(val) / 100;
|
|
895
|
-
return `scale: ${n}`;
|
|
896
|
-
}
|
|
897
|
-
if (utility.startsWith('rotate-')) {
|
|
898
|
-
const val = utility.slice(7);
|
|
899
|
-
if (val.startsWith('[')) {return `rotate: ${val.slice(1, -1)}`;}
|
|
900
|
-
return `rotate: ${val}deg`;
|
|
901
|
-
}
|
|
902
|
-
if (utility.startsWith('translate-x-')) {
|
|
903
|
-
const val = utility.slice(12);
|
|
904
|
-
const v = resolveSpacingValue(val, 'width');
|
|
905
|
-
return `translate: ${v} var(--tw-translate-y, 0)`;
|
|
906
|
-
}
|
|
907
|
-
if (utility.startsWith('translate-y-')) {
|
|
908
|
-
const val = utility.slice(12);
|
|
909
|
-
const v = resolveSpacingValue(val, 'height');
|
|
910
|
-
return `translate: var(--tw-translate-x, 0) ${v}`;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// ── 19. Transitions ─────────────────────────────────────────────────────
|
|
914
|
-
if (utility === 'transition') {
|
|
915
|
-
return 'transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, 150ms)';
|
|
916
|
-
}
|
|
917
|
-
if (utility === 'transition-all') {
|
|
918
|
-
return 'transition-property: all; transition-timing-function: var(--tw-ease, ease); transition-duration: var(--tw-duration, 150ms)';
|
|
919
|
-
}
|
|
920
|
-
if (utility === 'transition-none') {return 'transition-property: none';}
|
|
921
|
-
if (utility.startsWith('duration-')) {
|
|
922
|
-
const val = utility.slice(9);
|
|
923
|
-
if (val.startsWith('[')) {return `transition-duration: ${val.slice(1, -1)}`;}
|
|
924
|
-
return `transition-duration: ${val}ms`;
|
|
925
|
-
}
|
|
926
|
-
if (utility.startsWith('ease-')) {
|
|
927
|
-
const eases: Record<string, string> = {
|
|
928
|
-
linear: 'linear', in: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
929
|
-
out: 'cubic-bezier(0, 0, 0.2, 1)', 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
930
|
-
};
|
|
931
|
-
const val = utility.slice(5);
|
|
932
|
-
if (val in eases) {return `transition-timing-function: ${eases[val]}`;}
|
|
933
|
-
return `transition-timing-function: var(--ease-${val})`;
|
|
934
|
-
}
|
|
935
|
-
if (utility.startsWith('delay-')) {
|
|
936
|
-
const val = utility.slice(6);
|
|
937
|
-
if (val.startsWith('[')) {return `transition-delay: ${val.slice(1, -1)}`;}
|
|
938
|
-
return `transition-delay: ${val}ms`;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// ── 20. Color utilities (color properties) ──────────────────────────────
|
|
942
|
-
// Try all color prefixes, longest match first to avoid partial matches
|
|
943
|
-
const colorPrefixes = Object.keys(COLOR_PROPS).sort((a, b) => b.length - a.length);
|
|
944
|
-
for (const prefix of colorPrefixes) {
|
|
945
|
-
if (utility === prefix || utility.startsWith(prefix + '-')) {
|
|
946
|
-
const rest = utility.slice(prefix.length + 1);
|
|
947
|
-
if (!rest && utility !== prefix) {continue;} // prefix without value
|
|
948
|
-
if (!rest && utility === prefix) {continue;} // bare prefix, no color value
|
|
949
|
-
const cssProp = COLOR_PROPS[prefix];
|
|
950
|
-
const colorVal = resolveColorValue(rest);
|
|
951
|
-
return `${cssProp}: ${colorVal}`;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// ── 21. Spacing utilities ────────────────────────────────────────────────
|
|
956
|
-
// Try all spacing prefixes, longest match first
|
|
957
|
-
const spacingPrefixes = Object.keys(SPACING_PROPS).sort((a, b) => b.length - a.length);
|
|
958
|
-
for (const prefix of spacingPrefixes) {
|
|
959
|
-
const dashPrefix = prefix + '-';
|
|
960
|
-
if (utility.startsWith(dashPrefix)) {
|
|
961
|
-
const val = utility.slice(dashPrefix.length);
|
|
962
|
-
// Handle negative: -m-4 → the class name would be "-m-4"
|
|
963
|
-
const negative = val.startsWith('-');
|
|
964
|
-
const rawVal = negative ? val.slice(1) : val;
|
|
965
|
-
const props = SPACING_PROPS[prefix];
|
|
966
|
-
|
|
967
|
-
const resolved = resolveSpacingValue(negative ? `-${rawVal}` : rawVal, props[0]);
|
|
968
|
-
if (!resolved) {continue;}
|
|
969
|
-
|
|
970
|
-
return props.map(p => `${p}: ${resolved}`).join('; ');
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// ── 22. Arbitrary property: [property:value] ────────────────────────────
|
|
975
|
-
if (utility.startsWith('[') && utility.endsWith(']') && utility.includes(':')) {
|
|
976
|
-
const inner = utility.slice(1, -1).replace(/_/g, ' ');
|
|
977
|
-
const colonIdx = inner.indexOf(':');
|
|
978
|
-
const prop = inner.slice(0, colonIdx);
|
|
979
|
-
const val = inner.slice(colonIdx + 1);
|
|
980
|
-
return `${prop}: ${val}`;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Unknown class — return empty (graceful no-op)
|
|
984
|
-
return '';
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* Generates a complete CSS rule string for a Tailwind class name.
|
|
989
|
-
*
|
|
990
|
-
* @param className - full Tailwind class (e.g. "hover:bg-blue-500", "sm:p-4", "p-4")
|
|
991
|
-
* @returns CSS rule string (e.g. ".hover\\:bg-blue-500:hover { background-color: var(--color-blue-500) }")
|
|
992
|
-
* or empty string for unknown classes
|
|
993
|
-
*/
|
|
994
|
-
export function generateCSSRule(className: string): string {
|
|
995
|
-
const { utility, pseudoSuffix, selectorPrefix } = parseVariants(className);
|
|
996
|
-
const declarations = generateDeclarations(utility);
|
|
997
|
-
|
|
998
|
-
if (!declarations) {return '';}
|
|
999
|
-
|
|
1000
|
-
const escapedClass = escapeCSSSelector(className);
|
|
1001
|
-
const selector = `${selectorPrefix}.${escapedClass}${pseudoSuffix}`;
|
|
1002
|
-
return `${selector} { ${declarations} }`;
|
|
1003
|
-
}
|