@csszyx/dynamic 0.4.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.
@@ -0,0 +1,1003 @@
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
+ }