@cwcss/crosswind 0.1.4

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.
Files changed (64) hide show
  1. package/PLUGIN.md +235 -0
  2. package/benchmark/framework-comparison.bench.ts +850 -0
  3. package/bin/cli.ts +365 -0
  4. package/bin/crosswind +0 -0
  5. package/bin/headwind +0 -0
  6. package/build.ts +8 -0
  7. package/crosswind.config.ts +9 -0
  8. package/example/comprehensive.html +70 -0
  9. package/example/index.html +21 -0
  10. package/example/output.css +236 -0
  11. package/examples/plugin/README.md +112 -0
  12. package/examples/plugin/build.ts +32 -0
  13. package/examples/plugin/src/index.html +34 -0
  14. package/examples/plugin/src/index.ts +7 -0
  15. package/headwind +2 -0
  16. package/package.json +92 -0
  17. package/src/build.ts +101 -0
  18. package/src/config.ts +529 -0
  19. package/src/generator.ts +2173 -0
  20. package/src/index.ts +10 -0
  21. package/src/parser.ts +1471 -0
  22. package/src/plugin.ts +118 -0
  23. package/src/preflight-forms.ts +229 -0
  24. package/src/preflight.ts +388 -0
  25. package/src/rules-advanced.ts +477 -0
  26. package/src/rules-effects.ts +457 -0
  27. package/src/rules-forms.ts +103 -0
  28. package/src/rules-grid.ts +241 -0
  29. package/src/rules-interactivity.ts +525 -0
  30. package/src/rules-layout.ts +385 -0
  31. package/src/rules-transforms.ts +412 -0
  32. package/src/rules-typography.ts +486 -0
  33. package/src/rules.ts +805 -0
  34. package/src/scanner.ts +84 -0
  35. package/src/transformer-compile-class.ts +275 -0
  36. package/src/types.ts +197 -0
  37. package/test/advanced-features.test.ts +911 -0
  38. package/test/arbitrary.test.ts +396 -0
  39. package/test/attributify.test.ts +592 -0
  40. package/test/bracket-syntax.test.ts +1133 -0
  41. package/test/build.test.ts +99 -0
  42. package/test/colors.test.ts +934 -0
  43. package/test/flexbox.test.ts +669 -0
  44. package/test/generator.test.ts +597 -0
  45. package/test/grid.test.ts +584 -0
  46. package/test/layout.test.ts +404 -0
  47. package/test/modifiers.test.ts +417 -0
  48. package/test/parser.test.ts +564 -0
  49. package/test/performance-regression.test.ts +376 -0
  50. package/test/performance.test.ts +568 -0
  51. package/test/plugin.test.ts +160 -0
  52. package/test/scanner.test.ts +94 -0
  53. package/test/sizing.test.ts +481 -0
  54. package/test/spacing.test.ts +394 -0
  55. package/test/transformer-compile-class.test.ts +287 -0
  56. package/test/transforms.test.ts +448 -0
  57. package/test/typography.test.ts +632 -0
  58. package/test/variants-form-states.test.ts +225 -0
  59. package/test/variants-group-peer.test.ts +66 -0
  60. package/test/variants-media.test.ts +213 -0
  61. package/test/variants-positional.test.ts +58 -0
  62. package/test/variants-pseudo-elements.test.ts +47 -0
  63. package/test/variants-state.test.ts +62 -0
  64. package/tsconfig.json +18 -0
package/src/rules.ts ADDED
@@ -0,0 +1,805 @@
1
+ import type { CrosswindConfig, ParsedClass, UtilityRuleResult } from './types'
2
+ import { advancedRules } from './rules-advanced'
3
+ import { effectsRules } from './rules-effects'
4
+ import { formsRules } from './rules-forms'
5
+ import { gridRules } from './rules-grid'
6
+ import { interactivityRules } from './rules-interactivity'
7
+ import { layoutRules } from './rules-layout'
8
+ import { transformsRules } from './rules-transforms'
9
+ import { typographyRules } from './rules-typography'
10
+
11
+ export type UtilityRule = (parsed: ParsedClass, config: CrosswindConfig) => Record<string, string> | UtilityRuleResult | undefined
12
+
13
+ /**
14
+ * Built-in utility rules
15
+ * Each rule checks if it matches the parsed class and returns CSS properties
16
+ */
17
+
18
+ // Display utilities
19
+ export const displayRule: UtilityRule = (parsed) => {
20
+ const displays = ['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'inline-grid', 'hidden', 'none']
21
+ if (displays.includes(parsed.utility)) {
22
+ return { display: parsed.utility === 'hidden' ? 'none' : parsed.utility }
23
+ }
24
+ }
25
+
26
+ // Container utilities (for container queries)
27
+ export const containerRule: UtilityRule = (parsed) => {
28
+ // @container -> container-type: inline-size (most common use case)
29
+ if (parsed.utility === '@container') {
30
+ return { 'container-type': 'inline-size' } as Record<string, string>
31
+ }
32
+ // @container-normal -> container-type: normal (for size containment without inline-size)
33
+ if (parsed.utility === '@container-normal') {
34
+ return { 'container-type': 'normal' } as Record<string, string>
35
+ }
36
+ // @container/name -> container-type: inline-size; container-name: name
37
+ if (parsed.utility.startsWith('@container/')) {
38
+ const name = parsed.utility.slice(11) // Remove '@container/'
39
+ return {
40
+ 'container-type': 'inline-size',
41
+ 'container-name': name,
42
+ } as Record<string, string>
43
+ }
44
+ }
45
+
46
+ // Flexbox utilities
47
+ export const flexDirectionRule: UtilityRule = (parsed) => {
48
+ const directions: Record<string, string> = {
49
+ 'flex-row': 'row',
50
+ 'flex-row-reverse': 'row-reverse',
51
+ 'flex-col': 'column',
52
+ 'flex-col-reverse': 'column-reverse',
53
+ }
54
+ return directions[parsed.utility] ? { 'flex-direction': directions[parsed.utility] } : undefined
55
+ }
56
+
57
+ export const flexWrapRule: UtilityRule = (parsed) => {
58
+ const wraps: Record<string, string> = {
59
+ 'flex-wrap': 'wrap',
60
+ 'flex-wrap-reverse': 'wrap-reverse',
61
+ 'flex-nowrap': 'nowrap',
62
+ }
63
+ return wraps[parsed.utility] ? { 'flex-wrap': wraps[parsed.utility] } : undefined
64
+ }
65
+
66
+ export const flexRule: UtilityRule = (parsed) => {
67
+ if (parsed.utility === 'flex' || parsed.utility.startsWith('flex-')) {
68
+ // Handle named flex values
69
+ const flexValues: Record<string, string> = {
70
+ 'flex-1': '1 1 0%',
71
+ 'flex-auto': '1 1 auto',
72
+ 'flex-initial': '0 1 auto',
73
+ 'flex-none': 'none',
74
+ }
75
+ if (flexValues[parsed.utility]) {
76
+ return { flex: flexValues[parsed.utility] }
77
+ }
78
+ // Handle arbitrary flex values
79
+ if (parsed.utility === 'flex' && parsed.arbitrary && parsed.value) {
80
+ return { flex: parsed.value.replace(/_/g, ' ') }
81
+ }
82
+ }
83
+ return undefined
84
+ }
85
+
86
+ export const flexGrowRule: UtilityRule = (parsed) => {
87
+ if (parsed.utility === 'flex-grow' && !parsed.value) {
88
+ return { 'flex-grow': '1' }
89
+ }
90
+ if (parsed.utility === 'flex-grow' && parsed.value) {
91
+ return { 'flex-grow': parsed.value }
92
+ }
93
+ }
94
+
95
+ export const flexShrinkRule: UtilityRule = (parsed) => {
96
+ if (parsed.utility === 'flex-shrink' && !parsed.value) {
97
+ return { 'flex-shrink': '1' }
98
+ }
99
+ if (parsed.utility === 'flex-shrink' && parsed.value) {
100
+ return { 'flex-shrink': parsed.value }
101
+ }
102
+ }
103
+
104
+ export const justifyContentRule: UtilityRule = (parsed) => {
105
+ if (parsed.utility === 'justify' && parsed.value) {
106
+ const values: Record<string, string> = {
107
+ start: 'flex-start',
108
+ end: 'flex-end',
109
+ center: 'center',
110
+ between: 'space-between',
111
+ around: 'space-around',
112
+ evenly: 'space-evenly',
113
+ }
114
+ // Handle named values
115
+ if (values[parsed.value]) {
116
+ return { 'justify-content': values[parsed.value] }
117
+ }
118
+ // Handle arbitrary values
119
+ if (parsed.arbitrary) {
120
+ return { 'justify-content': parsed.value }
121
+ }
122
+ }
123
+ return undefined
124
+ }
125
+
126
+ export const alignItemsRule: UtilityRule = (parsed) => {
127
+ if (parsed.utility === 'items' && parsed.value) {
128
+ const values: Record<string, string> = {
129
+ start: 'flex-start',
130
+ end: 'flex-end',
131
+ center: 'center',
132
+ baseline: 'baseline',
133
+ stretch: 'stretch',
134
+ }
135
+ // Handle named values
136
+ if (values[parsed.value]) {
137
+ return { 'align-items': values[parsed.value] }
138
+ }
139
+ // Handle arbitrary values
140
+ if (parsed.arbitrary) {
141
+ return { 'align-items': parsed.value }
142
+ }
143
+ }
144
+ return undefined
145
+ }
146
+
147
+ export const justifyItemsRule: UtilityRule = (parsed) => {
148
+ // Parsed as utility="justify", value="items-center"
149
+ // Need to reconstruct full utility name
150
+ if (parsed.utility === 'justify' && parsed.value && parsed.value.startsWith('items-')) {
151
+ const values: Record<string, string> = {
152
+ 'items-start': 'start',
153
+ 'items-end': 'end',
154
+ 'items-center': 'center',
155
+ 'items-stretch': 'stretch',
156
+ }
157
+ return values[parsed.value] ? { 'justify-items': values[parsed.value] } : undefined
158
+ }
159
+ return undefined
160
+ }
161
+
162
+ export const alignContentRule: UtilityRule = (parsed) => {
163
+ if (parsed.utility === 'content' && parsed.value) {
164
+ const values: Record<string, string> = {
165
+ normal: 'normal',
166
+ center: 'center',
167
+ start: 'flex-start',
168
+ end: 'flex-end',
169
+ between: 'space-between',
170
+ around: 'space-around',
171
+ evenly: 'space-evenly',
172
+ baseline: 'baseline',
173
+ stretch: 'stretch',
174
+ }
175
+ return values[parsed.value] ? { 'align-content': values[parsed.value] } : undefined
176
+ }
177
+ return undefined
178
+ }
179
+
180
+ // Spacing utilities (margin, padding)
181
+ export const spacingRule: UtilityRule = (parsed, config) => {
182
+ const prefixes: Record<string, string[]> = {
183
+ p: ['padding'],
184
+ px: ['padding-left', 'padding-right'],
185
+ py: ['padding-top', 'padding-bottom'],
186
+ pt: ['padding-top'],
187
+ pr: ['padding-right'],
188
+ pb: ['padding-bottom'],
189
+ pl: ['padding-left'],
190
+ // Logical padding (for RTL support)
191
+ ps: ['padding-inline-start'],
192
+ pe: ['padding-inline-end'],
193
+ m: ['margin'],
194
+ mx: ['margin-left', 'margin-right'],
195
+ my: ['margin-top', 'margin-bottom'],
196
+ mt: ['margin-top'],
197
+ mr: ['margin-right'],
198
+ mb: ['margin-bottom'],
199
+ ml: ['margin-left'],
200
+ // Logical margin (for RTL support)
201
+ ms: ['margin-inline-start'],
202
+ me: ['margin-inline-end'],
203
+ }
204
+
205
+ const properties = prefixes[parsed.utility]
206
+ if (!properties || !parsed.value)
207
+ return undefined
208
+
209
+ // Handle negative values
210
+ let value: string
211
+ if (parsed.value.startsWith('-')) {
212
+ const positiveValue = parsed.value.slice(1)
213
+ const spacing = config.theme.spacing[positiveValue]
214
+ // Special case: -0 should just be 0
215
+ if (positiveValue === '0') {
216
+ value = spacing || '0'
217
+ }
218
+ else {
219
+ value = spacing ? `-${spacing}` : parsed.value
220
+ }
221
+ }
222
+ else {
223
+ value = config.theme.spacing[parsed.value] || parsed.value
224
+ }
225
+
226
+ const result: Record<string, string> = {}
227
+ for (const prop of properties) {
228
+ result[prop] = value
229
+ }
230
+ return result
231
+ }
232
+
233
+ // Width and height utilities
234
+ export const sizingRule: UtilityRule = (parsed, config) => {
235
+ if (parsed.utility === 'w' && parsed.value) {
236
+ const sizeMap: Record<string, string> = {
237
+ full: '100%',
238
+ screen: '100vw',
239
+ auto: 'auto',
240
+ min: 'min-content',
241
+ max: 'max-content',
242
+ fit: 'fit-content',
243
+ }
244
+ // Handle fractions: 1/2 -> 50%
245
+ if (parsed.value.includes('/')) {
246
+ const [num, denom] = parsed.value.split('/').map(Number)
247
+ // Validate: skip invalid fractions (NaN or division by zero)
248
+ if (Number.isNaN(num) || Number.isNaN(denom) || denom === 0) {
249
+ return undefined
250
+ }
251
+ return { width: `${(num / denom) * 100}%` } as Record<string, string>
252
+ }
253
+ // Check spacing config first, then sizeMap, then raw value
254
+ const value = config.theme.spacing[parsed.value] || sizeMap[parsed.value] || parsed.value
255
+ return { width: value } as Record<string, string>
256
+ }
257
+
258
+ if (parsed.utility === 'h' && parsed.value) {
259
+ const sizeMap: Record<string, string> = {
260
+ full: '100%',
261
+ screen: '100vh',
262
+ auto: 'auto',
263
+ min: 'min-content',
264
+ max: 'max-content',
265
+ fit: 'fit-content',
266
+ }
267
+ // Handle fractions: 3/4 -> 75%
268
+ if (parsed.value.includes('/')) {
269
+ const [num, denom] = parsed.value.split('/').map(Number)
270
+ // Validate: skip invalid fractions (NaN or division by zero)
271
+ if (Number.isNaN(num) || Number.isNaN(denom) || denom === 0) {
272
+ return undefined
273
+ }
274
+ return { height: `${(num / denom) * 100}%` } as Record<string, string>
275
+ }
276
+ // Check spacing config first, then sizeMap, then raw value
277
+ const value = config.theme.spacing[parsed.value] || sizeMap[parsed.value] || parsed.value
278
+ return { height: value } as Record<string, string>
279
+ }
280
+
281
+ // Size utility (width + height shorthand)
282
+ if (parsed.utility === 'size' && parsed.value) {
283
+ const sizeMap: Record<string, string> = {
284
+ full: '100%',
285
+ auto: 'auto',
286
+ min: 'min-content',
287
+ max: 'max-content',
288
+ fit: 'fit-content',
289
+ }
290
+ // Handle fractions: 1/2 -> 50%
291
+ if (parsed.value.includes('/')) {
292
+ const [num, denom] = parsed.value.split('/').map(Number)
293
+ if (Number.isNaN(num) || Number.isNaN(denom) || denom === 0) {
294
+ return undefined
295
+ }
296
+ const percent = `${(num / denom) * 100}%`
297
+ return { width: percent, height: percent } as Record<string, string>
298
+ }
299
+ const value = config.theme.spacing[parsed.value] || sizeMap[parsed.value] || parsed.value
300
+ return { width: value, height: value } as Record<string, string>
301
+ }
302
+
303
+ return undefined
304
+ }
305
+
306
+ // Color utilities (background, text, border)
307
+
308
+ // Flat color cache: "blue-500" -> "#3b82f6" (populated on first access per config)
309
+ let flatColorCache: Map<string, string> | null = null
310
+ let flatColorCacheConfig: any = null
311
+
312
+ // Pre-computed color property map (avoid object creation)
313
+ const COLOR_PROPS: Record<string, string> = {
314
+ bg: 'background-color',
315
+ text: 'color',
316
+ border: 'border-color',
317
+ }
318
+
319
+ // Special color keywords (pre-defined)
320
+ const SPECIAL_COLORS: Record<string, string> = {
321
+ current: 'currentColor',
322
+ transparent: 'transparent',
323
+ inherit: 'inherit',
324
+ }
325
+
326
+ // Build flat color cache from theme colors
327
+ function buildFlatColorCache(colors: Record<string, any>): Map<string, string> {
328
+ const cache = new Map<string, string>()
329
+ for (const [colorName, colorValue] of Object.entries(colors)) {
330
+ if (typeof colorValue === 'string') {
331
+ cache.set(colorName, colorValue)
332
+ }
333
+ else if (typeof colorValue === 'object' && colorValue !== null) {
334
+ for (const [shade, shadeValue] of Object.entries(colorValue)) {
335
+ if (typeof shadeValue === 'string') {
336
+ cache.set(`${colorName}-${shade}`, shadeValue)
337
+ }
338
+ }
339
+ }
340
+ }
341
+ return cache
342
+ }
343
+
344
+ export const colorRule: UtilityRule = (parsed, config) => {
345
+ const prop = COLOR_PROPS[parsed.utility]
346
+ if (!prop || !parsed.value)
347
+ return undefined
348
+
349
+ const value = parsed.value
350
+
351
+ // Handle type hint for color: text-[color:var(--muted)] -> color: var(--muted)
352
+ if (parsed.arbitrary && parsed.typeHint === 'color') {
353
+ return { [prop]: value }
354
+ }
355
+
356
+ // Build/update flat color cache if needed
357
+ if (flatColorCache === null || flatColorCacheConfig !== config.theme.colors) {
358
+ flatColorCache = buildFlatColorCache(config.theme.colors)
359
+ flatColorCacheConfig = config.theme.colors
360
+ }
361
+
362
+ // Fast path: Most common case - direct lookup in flat cache (no string parsing)
363
+ // Check for slash (opacity modifier) first
364
+ const slashIdx = value.indexOf('/')
365
+ if (slashIdx === -1) {
366
+ // No opacity - direct lookup
367
+ const colorVal = flatColorCache.get(value)
368
+ if (colorVal) {
369
+ return { [prop]: colorVal }
370
+ }
371
+ }
372
+
373
+ // Slower paths for special cases
374
+
375
+ // Handle opacity modifier (slashIdx already computed above)
376
+ let opacity: number | undefined
377
+ let colorValue = value
378
+
379
+ if (slashIdx !== -1) {
380
+ colorValue = value.slice(0, slashIdx)
381
+ const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
382
+
383
+ // Validate opacity is in 0-100 range
384
+ if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100) {
385
+ return undefined
386
+ }
387
+ opacity = opacityValue / 100
388
+
389
+ // Try flat cache with base color value
390
+ const baseColor = flatColorCache!.get(colorValue)
391
+ if (baseColor) {
392
+ return { [prop]: applyOpacity(baseColor, opacity) }
393
+ }
394
+ }
395
+
396
+ // Special color keywords
397
+ const specialColor = SPECIAL_COLORS[colorValue]
398
+ if (specialColor) {
399
+ return { [prop]: specialColor }
400
+ }
401
+
402
+ // Only use fallback for arbitrary values (e.g., border-[#ff0000], text-[#ff0000]/50)
403
+ const isArbitrary = parsed.arbitrary || (colorValue && colorValue.charCodeAt(0) === 91) // '[' char
404
+ if (isArbitrary && colorValue) {
405
+ const colorVal = opacity !== undefined
406
+ ? applyOpacity(colorValue, opacity)
407
+ : colorValue
408
+ return { [prop]: colorVal }
409
+ }
410
+
411
+ return undefined
412
+ }
413
+
414
+ // Helper to apply opacity to color (moved outside to reduce function creation)
415
+ function applyOpacity(color: string, opacity: number): string {
416
+ // Strip brackets from arbitrary values: [#ff0000] -> #ff0000
417
+ let cleanColor = color
418
+ if (color.charCodeAt(0) === 91 && color.charCodeAt(color.length - 1) === 93) { // '[' and ']'
419
+ cleanColor = color.slice(1, -1)
420
+ }
421
+
422
+ // If color is hex (#rrggbb), convert to rgb with alpha
423
+ if (cleanColor.charCodeAt(0) === 35) { // '#' char code for faster check
424
+ const hex = cleanColor.slice(1)
425
+ const r = Number.parseInt(hex.slice(0, 2), 16)
426
+ const g = Number.parseInt(hex.slice(2, 4), 16)
427
+ const b = Number.parseInt(hex.slice(4, 6), 16)
428
+ return `rgb(${r} ${g} ${b} / ${opacity})`
429
+ }
430
+ // If color already has rgb/rgba format, add/replace alpha
431
+ if (cleanColor.charCodeAt(0) === 114) { // 'r' char code for 'rgb'
432
+ const rgbMatch = cleanColor.match(/rgb\((\d+)\s+(\d+)\s+(\d+)/)
433
+ if (rgbMatch) {
434
+ return `rgb(${rgbMatch[1]} ${rgbMatch[2]} ${rgbMatch[3]} / ${opacity})`
435
+ }
436
+ }
437
+ // If color is oklch format, add alpha channel
438
+ if (cleanColor.charCodeAt(0) === 111) { // 'o' char code for 'oklch'
439
+ const oklchMatch = cleanColor.match(/oklch\(([^)]+)\)/)
440
+ if (oklchMatch) {
441
+ // oklch values are: lightness chroma hue
442
+ // Add alpha: oklch(L C H / alpha)
443
+ return `oklch(${oklchMatch[1]} / ${opacity})`
444
+ }
445
+ }
446
+ // If color is hsl/hsla format, add/replace alpha
447
+ if (cleanColor.charCodeAt(0) === 104) { // 'h' char code for 'hsl'
448
+ const hslMatch = cleanColor.match(/hsl\(([^)]+)\)/)
449
+ if (hslMatch) {
450
+ return `hsl(${hslMatch[1]} / ${opacity})`
451
+ }
452
+ }
453
+ // Fallback: use opacity as-is with the color
454
+ return cleanColor
455
+ }
456
+
457
+ // Placeholder color utilities (placeholder-{color})
458
+ export const placeholderColorRule: UtilityRule = (parsed, config) => {
459
+ if (parsed.utility !== 'placeholder' || !parsed.value)
460
+ return undefined
461
+
462
+ // Build/update flat color cache if needed
463
+ if (flatColorCache === null || flatColorCacheConfig !== config.theme.colors) {
464
+ flatColorCache = buildFlatColorCache(config.theme.colors)
465
+ flatColorCacheConfig = config.theme.colors
466
+ }
467
+
468
+ const value = parsed.value
469
+ const slashIdx = value.indexOf('/')
470
+
471
+ if (slashIdx === -1) {
472
+ // No opacity
473
+ const colorVal = flatColorCache.get(value)
474
+ if (colorVal) {
475
+ return {
476
+ properties: { color: colorVal },
477
+ pseudoElement: '::placeholder',
478
+ }
479
+ }
480
+ }
481
+ else {
482
+ // With opacity modifier
483
+ const colorValue = value.slice(0, slashIdx)
484
+ const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
485
+ if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
486
+ return undefined
487
+ const opacity = opacityValue / 100
488
+ const baseColor = flatColorCache.get(colorValue)
489
+ if (baseColor) {
490
+ return {
491
+ properties: { color: applyOpacity(baseColor, opacity) },
492
+ pseudoElement: '::placeholder',
493
+ }
494
+ }
495
+ }
496
+
497
+ // Special colors
498
+ const specialColor = SPECIAL_COLORS[parsed.value]
499
+ if (specialColor) {
500
+ return {
501
+ properties: { color: specialColor },
502
+ pseudoElement: '::placeholder',
503
+ }
504
+ }
505
+
506
+ return undefined
507
+ }
508
+
509
+ // Typography utilities
510
+ export const fontSizeRule: UtilityRule = (parsed, config) => {
511
+ if (parsed.utility === 'text' && parsed.value) {
512
+ // Handle arbitrary values first
513
+ if (parsed.arbitrary) {
514
+ // If there's a type hint, only handle font-size if it's a length-related type
515
+ // For 'color' type hint, let colorRule handle it
516
+ if (parsed.typeHint) {
517
+ if (parsed.typeHint === 'color') {
518
+ return undefined // Let colorRule handle it
519
+ }
520
+ // 'length' type hint or other size-related types -> font-size
521
+ return { 'font-size': parsed.value } as Record<string, string>
522
+ }
523
+ // No type hint - default to font-size (backwards compatible)
524
+ return { 'font-size': parsed.value } as Record<string, string>
525
+ }
526
+ const fontSize = config.theme.fontSize[parsed.value]
527
+ if (fontSize) {
528
+ return {
529
+ 'font-size': fontSize[0],
530
+ 'line-height': fontSize[1].lineHeight,
531
+ } as Record<string, string>
532
+ }
533
+ }
534
+ }
535
+
536
+ export const fontWeightRule: UtilityRule = (parsed) => {
537
+ if (parsed.utility === 'font' && parsed.value) {
538
+ // Handle arbitrary values first
539
+ if (parsed.arbitrary) {
540
+ return { 'font-weight': parsed.value }
541
+ }
542
+ const weights: Record<string, string> = {
543
+ thin: '100',
544
+ extralight: '200',
545
+ light: '300',
546
+ normal: '400',
547
+ medium: '500',
548
+ semibold: '600',
549
+ bold: '700',
550
+ extrabold: '800',
551
+ black: '900',
552
+ }
553
+ return weights[parsed.value] ? { 'font-weight': weights[parsed.value] } : undefined
554
+ }
555
+ return undefined
556
+ }
557
+
558
+ export const leadingRule: UtilityRule = (parsed) => {
559
+ if (parsed.utility === 'leading' && parsed.value) {
560
+ // Handle arbitrary values first
561
+ if (parsed.arbitrary) {
562
+ return { 'line-height': parsed.value }
563
+ }
564
+ // Named line-height values
565
+ const lineHeights: Record<string, string> = {
566
+ none: '1',
567
+ tight: '1.25',
568
+ snug: '1.375',
569
+ normal: '1.5',
570
+ relaxed: '1.625',
571
+ loose: '2',
572
+ // Numeric values (rem-based)
573
+ '3': '0.75rem',
574
+ '4': '1rem',
575
+ '5': '1.25rem',
576
+ '6': '1.5rem',
577
+ '7': '1.75rem',
578
+ '8': '2rem',
579
+ '9': '2.25rem',
580
+ '10': '2.5rem',
581
+ }
582
+ return lineHeights[parsed.value] ? { 'line-height': lineHeights[parsed.value] } : undefined
583
+ }
584
+ return undefined
585
+ }
586
+
587
+ export const textAlignRule: UtilityRule = (parsed) => {
588
+ if (parsed.utility === 'text' && parsed.value) {
589
+ const aligns: Record<string, string> = {
590
+ left: 'left',
591
+ center: 'center',
592
+ right: 'right',
593
+ justify: 'justify',
594
+ }
595
+ return aligns[parsed.value] ? { 'text-align': aligns[parsed.value] } : undefined
596
+ }
597
+ }
598
+
599
+ // Border utilities
600
+ export const borderWidthRule: UtilityRule = (parsed) => {
601
+ if (parsed.utility === 'border') {
602
+ if (!parsed.value) {
603
+ return { 'border-width': '1px' }
604
+ }
605
+
606
+ // Border width values: 0, 2, 4, 8
607
+ const widthMap: Record<string, string> = {
608
+ 0: '0px',
609
+ 2: '2px',
610
+ 4: '4px',
611
+ 8: '8px',
612
+ }
613
+
614
+ // Handle border-0, border-2, border-4, border-8
615
+ if (widthMap[parsed.value]) {
616
+ return { 'border-width': widthMap[parsed.value] }
617
+ }
618
+
619
+ const sideMap: Record<string, string | string[]> = {
620
+ t: 'border-top-width',
621
+ r: 'border-right-width',
622
+ b: 'border-bottom-width',
623
+ l: 'border-left-width',
624
+ }
625
+
626
+ // Handle border-x and border-y shortcuts
627
+ if (parsed.value === 'x') {
628
+ return {
629
+ 'border-left-width': '1px',
630
+ 'border-right-width': '1px',
631
+ } as Record<string, string>
632
+ }
633
+ if (parsed.value === 'y') {
634
+ return {
635
+ 'border-top-width': '1px',
636
+ 'border-bottom-width': '1px',
637
+ } as Record<string, string>
638
+ }
639
+
640
+ const prop = sideMap[parsed.value]
641
+ if (typeof prop === 'string') {
642
+ return { [prop]: '1px' } as Record<string, string>
643
+ }
644
+ return undefined
645
+ }
646
+ }
647
+
648
+ // Border side width utilities (border-t-0, border-r-2, border-x-4, etc.)
649
+ export const borderSideWidthRule: UtilityRule = (parsed) => {
650
+ const sideUtilities: Record<string, string | string[]> = {
651
+ 'border-t': 'border-top-width',
652
+ 'border-r': 'border-right-width',
653
+ 'border-b': 'border-bottom-width',
654
+ 'border-l': 'border-left-width',
655
+ 'border-x': ['border-left-width', 'border-right-width'],
656
+ 'border-y': ['border-top-width', 'border-bottom-width'],
657
+ // Logical borders (for RTL support)
658
+ 'border-s': 'border-inline-start-width',
659
+ 'border-e': 'border-inline-end-width',
660
+ }
661
+
662
+ const prop = sideUtilities[parsed.utility]
663
+ if (!prop)
664
+ return undefined
665
+
666
+ // Width values: 0, 2, 4, 8 (or default to 1px if no value)
667
+ const widthMap: Record<string, string> = {
668
+ 0: '0px',
669
+ 2: '2px',
670
+ 4: '4px',
671
+ 8: '8px',
672
+ }
673
+
674
+ const width = parsed.value ? widthMap[parsed.value] : '1px'
675
+ if (!width)
676
+ return undefined
677
+
678
+ if (Array.isArray(prop)) {
679
+ return prop.reduce((acc, p) => ({ ...acc, [p]: width }), {} as Record<string, string>)
680
+ }
681
+
682
+ return { [prop]: width }
683
+ }
684
+
685
+ export const borderRadiusRule: UtilityRule = (parsed, config) => {
686
+ if (parsed.utility === 'rounded') {
687
+ const value = parsed.value ? config.theme.borderRadius[parsed.value] : config.theme.borderRadius.DEFAULT
688
+ return value ? { 'border-radius': value } : undefined
689
+ }
690
+
691
+ // Logical border-radius utilities (for RTL/LTR support)
692
+ // rounded-s-* (start) - applies to start corners
693
+ if (parsed.utility === 'rounded-s' && parsed.value) {
694
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
695
+ return {
696
+ 'border-start-start-radius': value,
697
+ 'border-end-start-radius': value,
698
+ } as Record<string, string>
699
+ }
700
+ // rounded-e-* (end) - applies to end corners
701
+ if (parsed.utility === 'rounded-e' && parsed.value) {
702
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
703
+ return {
704
+ 'border-start-end-radius': value,
705
+ 'border-end-end-radius': value,
706
+ } as Record<string, string>
707
+ }
708
+ // rounded-ss-* (start-start corner)
709
+ if (parsed.utility === 'rounded-ss' && parsed.value) {
710
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
711
+ return { 'border-start-start-radius': value } as Record<string, string>
712
+ }
713
+ // rounded-se-* (start-end corner)
714
+ if (parsed.utility === 'rounded-se' && parsed.value) {
715
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
716
+ return { 'border-start-end-radius': value } as Record<string, string>
717
+ }
718
+ // rounded-es-* (end-start corner)
719
+ if (parsed.utility === 'rounded-es' && parsed.value) {
720
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
721
+ return { 'border-end-start-radius': value } as Record<string, string>
722
+ }
723
+ // rounded-ee-* (end-end corner)
724
+ if (parsed.utility === 'rounded-ee' && parsed.value) {
725
+ const value = config.theme.borderRadius[parsed.value] || parsed.value
726
+ return { 'border-end-end-radius': value } as Record<string, string>
727
+ }
728
+ }
729
+
730
+ // Export all rules (order matters - more specific rules first)
731
+ export const builtInRules: UtilityRule[] = [
732
+ // CRITICAL: Most common utilities first for O(1) lookup performance
733
+ // Rule order matters! More specific rules must come before more general ones.
734
+
735
+ // Spacing and sizing rules (w, h, p, m are extremely common)
736
+ spacingRule,
737
+ sizingRule,
738
+
739
+ // ALL rules that use utility names that might conflict MUST be ordered correctly!
740
+ // More specific rules must come before more general ones.
741
+
742
+ // Flexbox/Grid alignment rules (content-* for align-content)
743
+ // MUST come before typography contentRule which generates CSS content property
744
+ alignContentRule, // handles content-center, content-start, etc. -> align-content
745
+
746
+ // Typography rules (text-*)
747
+ fontSizeRule, // handles text-{size} (text-xl, text-sm, etc.)
748
+ textAlignRule, // handles text-{align} (text-center, text-left, etc.)
749
+ ...typographyRules, // handles text-ellipsis, text-wrap, text-transform, contentRule, etc.
750
+ fontWeightRule,
751
+ leadingRule, // handles leading-{size} (leading-tight, leading-none, etc.)
752
+
753
+ // Effects rules that use 'bg' utility (bg-gradient-*, bg-fixed, bg-clip-*, etc.)
754
+ ...effectsRules,
755
+
756
+ // Placeholder color rule (placeholder-{color} -> ::placeholder { color })
757
+ placeholderColorRule,
758
+
759
+ // Color rule (bg, text, border are very common)
760
+ // IMPORTANT: This must come AFTER all specific text-*, bg-*, border-* rules
761
+ // because it will match ANY text-*, bg-*, border-* class
762
+ colorRule,
763
+
764
+ // Advanced rules (container, ring, space, divide, gradients, etc.)
765
+ ...advancedRules,
766
+
767
+ // Layout rules (specific positioning and display)
768
+ ...layoutRules,
769
+
770
+ // Other Flexbox rules
771
+ flexDirectionRule,
772
+ flexWrapRule,
773
+ flexRule,
774
+ flexGrowRule,
775
+ flexShrinkRule,
776
+ justifyContentRule,
777
+ alignItemsRule,
778
+ justifyItemsRule,
779
+
780
+ // Grid rules
781
+ ...gridRules,
782
+
783
+ // Transform and transition rules
784
+ ...transformsRules,
785
+
786
+ // Effects and filters
787
+ ...effectsRules,
788
+
789
+ // Interactivity, SVG, and accessibility
790
+ ...interactivityRules,
791
+
792
+ // Forms utilities
793
+ ...formsRules,
794
+
795
+ // Border rules (specific side rules first)
796
+ borderSideWidthRule,
797
+ borderWidthRule,
798
+ borderRadiusRule,
799
+
800
+ // Container query utilities (@container, @container-normal, @container/name)
801
+ containerRule,
802
+
803
+ // Display rule last (most general - matches many utility names)
804
+ displayRule,
805
+ ]