@cwcss/crosswind 0.1.5 → 0.1.6

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 (86) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +390 -0
  3. package/dist/build.d.ts +24 -0
  4. package/dist/config.d.ts +5 -0
  5. package/dist/generator.d.ts +31 -0
  6. package/dist/index.d.ts +10 -0
  7. package/dist/index.js +12798 -0
  8. package/dist/parser.d.ts +42 -0
  9. package/dist/plugin.d.ts +22 -0
  10. package/dist/preflight-forms.d.ts +5 -0
  11. package/dist/preflight.d.ts +2 -0
  12. package/dist/rules-advanced.d.ts +27 -0
  13. package/dist/rules-effects.d.ts +25 -0
  14. package/dist/rules-forms.d.ts +7 -0
  15. package/dist/rules-grid.d.ts +13 -0
  16. package/dist/rules-interactivity.d.ts +41 -0
  17. package/dist/rules-layout.d.ts +26 -0
  18. package/dist/rules-transforms.d.ts +33 -0
  19. package/dist/rules-typography.d.ts +41 -0
  20. package/dist/rules.d.ts +39 -0
  21. package/dist/scanner.d.ts +18 -0
  22. package/dist/transformer-compile-class.d.ts +37 -0
  23. package/{src/types.ts → dist/types.d.ts} +17 -86
  24. package/package.json +1 -1
  25. package/PLUGIN.md +0 -235
  26. package/benchmark/framework-comparison.bench.ts +0 -850
  27. package/bin/cli.ts +0 -365
  28. package/bin/crosswind +0 -0
  29. package/bin/headwind +0 -0
  30. package/build.ts +0 -8
  31. package/crosswind.config.ts +0 -9
  32. package/example/comprehensive.html +0 -70
  33. package/example/index.html +0 -21
  34. package/example/output.css +0 -236
  35. package/examples/plugin/README.md +0 -112
  36. package/examples/plugin/build.ts +0 -32
  37. package/examples/plugin/src/index.html +0 -34
  38. package/examples/plugin/src/index.ts +0 -7
  39. package/headwind +0 -2
  40. package/src/build.ts +0 -101
  41. package/src/config.ts +0 -529
  42. package/src/generator.ts +0 -2173
  43. package/src/index.ts +0 -10
  44. package/src/parser.ts +0 -1471
  45. package/src/plugin.ts +0 -118
  46. package/src/preflight-forms.ts +0 -229
  47. package/src/preflight.ts +0 -388
  48. package/src/rules-advanced.ts +0 -477
  49. package/src/rules-effects.ts +0 -461
  50. package/src/rules-forms.ts +0 -103
  51. package/src/rules-grid.ts +0 -241
  52. package/src/rules-interactivity.ts +0 -525
  53. package/src/rules-layout.ts +0 -385
  54. package/src/rules-transforms.ts +0 -412
  55. package/src/rules-typography.ts +0 -486
  56. package/src/rules.ts +0 -809
  57. package/src/scanner.ts +0 -84
  58. package/src/transformer-compile-class.ts +0 -275
  59. package/test/advanced-features.test.ts +0 -911
  60. package/test/arbitrary.test.ts +0 -396
  61. package/test/attributify.test.ts +0 -592
  62. package/test/bracket-syntax.test.ts +0 -1133
  63. package/test/build.test.ts +0 -99
  64. package/test/colors.test.ts +0 -934
  65. package/test/flexbox.test.ts +0 -669
  66. package/test/generator.test.ts +0 -597
  67. package/test/grid.test.ts +0 -584
  68. package/test/layout.test.ts +0 -404
  69. package/test/modifiers.test.ts +0 -417
  70. package/test/parser.test.ts +0 -564
  71. package/test/performance-regression.test.ts +0 -376
  72. package/test/performance.test.ts +0 -568
  73. package/test/plugin.test.ts +0 -160
  74. package/test/scanner.test.ts +0 -94
  75. package/test/sizing.test.ts +0 -481
  76. package/test/spacing.test.ts +0 -394
  77. package/test/transformer-compile-class.test.ts +0 -287
  78. package/test/transforms.test.ts +0 -448
  79. package/test/typography.test.ts +0 -632
  80. package/test/variants-form-states.test.ts +0 -225
  81. package/test/variants-group-peer.test.ts +0 -66
  82. package/test/variants-media.test.ts +0 -213
  83. package/test/variants-positional.test.ts +0 -58
  84. package/test/variants-pseudo-elements.test.ts +0 -47
  85. package/test/variants-state.test.ts +0 -62
  86. package/tsconfig.json +0 -18
package/src/rules.ts DELETED
@@ -1,809 +0,0 @@
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 (#rgb or #rrggbb), convert to rgb with alpha
423
- if (cleanColor.charCodeAt(0) === 35) { // '#' char code for faster check
424
- let hex = cleanColor.slice(1)
425
- // Expand 3-char hex (#rgb) to 6-char (#rrggbb)
426
- if (hex.length === 3) {
427
- hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
428
- }
429
- const r = Number.parseInt(hex.slice(0, 2), 16)
430
- const g = Number.parseInt(hex.slice(2, 4), 16)
431
- const b = Number.parseInt(hex.slice(4, 6), 16)
432
- return `rgb(${r} ${g} ${b} / ${opacity})`
433
- }
434
- // If color already has rgb/rgba format, add/replace alpha
435
- if (cleanColor.charCodeAt(0) === 114) { // 'r' char code for 'rgb'
436
- const rgbMatch = cleanColor.match(/rgb\((\d+)\s+(\d+)\s+(\d+)/)
437
- if (rgbMatch) {
438
- return `rgb(${rgbMatch[1]} ${rgbMatch[2]} ${rgbMatch[3]} / ${opacity})`
439
- }
440
- }
441
- // If color is oklch format, add alpha channel
442
- if (cleanColor.charCodeAt(0) === 111) { // 'o' char code for 'oklch'
443
- const oklchMatch = cleanColor.match(/oklch\(([^)]+)\)/)
444
- if (oklchMatch) {
445
- // oklch values are: lightness chroma hue
446
- // Add alpha: oklch(L C H / alpha)
447
- return `oklch(${oklchMatch[1]} / ${opacity})`
448
- }
449
- }
450
- // If color is hsl/hsla format, add/replace alpha
451
- if (cleanColor.charCodeAt(0) === 104) { // 'h' char code for 'hsl'
452
- const hslMatch = cleanColor.match(/hsl\(([^)]+)\)/)
453
- if (hslMatch) {
454
- return `hsl(${hslMatch[1]} / ${opacity})`
455
- }
456
- }
457
- // Fallback: use opacity as-is with the color
458
- return cleanColor
459
- }
460
-
461
- // Placeholder color utilities (placeholder-{color})
462
- export const placeholderColorRule: UtilityRule = (parsed, config) => {
463
- if (parsed.utility !== 'placeholder' || !parsed.value)
464
- return undefined
465
-
466
- // Build/update flat color cache if needed
467
- if (flatColorCache === null || flatColorCacheConfig !== config.theme.colors) {
468
- flatColorCache = buildFlatColorCache(config.theme.colors)
469
- flatColorCacheConfig = config.theme.colors
470
- }
471
-
472
- const value = parsed.value
473
- const slashIdx = value.indexOf('/')
474
-
475
- if (slashIdx === -1) {
476
- // No opacity
477
- const colorVal = flatColorCache.get(value)
478
- if (colorVal) {
479
- return {
480
- properties: { color: colorVal },
481
- pseudoElement: '::placeholder',
482
- }
483
- }
484
- }
485
- else {
486
- // With opacity modifier
487
- const colorValue = value.slice(0, slashIdx)
488
- const opacityValue = Number.parseInt(value.slice(slashIdx + 1), 10)
489
- if (Number.isNaN(opacityValue) || opacityValue < 0 || opacityValue > 100)
490
- return undefined
491
- const opacity = opacityValue / 100
492
- const baseColor = flatColorCache.get(colorValue)
493
- if (baseColor) {
494
- return {
495
- properties: { color: applyOpacity(baseColor, opacity) },
496
- pseudoElement: '::placeholder',
497
- }
498
- }
499
- }
500
-
501
- // Special colors
502
- const specialColor = SPECIAL_COLORS[parsed.value]
503
- if (specialColor) {
504
- return {
505
- properties: { color: specialColor },
506
- pseudoElement: '::placeholder',
507
- }
508
- }
509
-
510
- return undefined
511
- }
512
-
513
- // Typography utilities
514
- export const fontSizeRule: UtilityRule = (parsed, config) => {
515
- if (parsed.utility === 'text' && parsed.value) {
516
- // Handle arbitrary values first
517
- if (parsed.arbitrary) {
518
- // If there's a type hint, only handle font-size if it's a length-related type
519
- // For 'color' type hint, let colorRule handle it
520
- if (parsed.typeHint) {
521
- if (parsed.typeHint === 'color') {
522
- return undefined // Let colorRule handle it
523
- }
524
- // 'length' type hint or other size-related types -> font-size
525
- return { 'font-size': parsed.value } as Record<string, string>
526
- }
527
- // No type hint - default to font-size (backwards compatible)
528
- return { 'font-size': parsed.value } as Record<string, string>
529
- }
530
- const fontSize = config.theme.fontSize[parsed.value]
531
- if (fontSize) {
532
- return {
533
- 'font-size': fontSize[0],
534
- 'line-height': fontSize[1].lineHeight,
535
- } as Record<string, string>
536
- }
537
- }
538
- }
539
-
540
- export const fontWeightRule: UtilityRule = (parsed) => {
541
- if (parsed.utility === 'font' && parsed.value) {
542
- // Handle arbitrary values first
543
- if (parsed.arbitrary) {
544
- return { 'font-weight': parsed.value }
545
- }
546
- const weights: Record<string, string> = {
547
- thin: '100',
548
- extralight: '200',
549
- light: '300',
550
- normal: '400',
551
- medium: '500',
552
- semibold: '600',
553
- bold: '700',
554
- extrabold: '800',
555
- black: '900',
556
- }
557
- return weights[parsed.value] ? { 'font-weight': weights[parsed.value] } : undefined
558
- }
559
- return undefined
560
- }
561
-
562
- export const leadingRule: UtilityRule = (parsed) => {
563
- if (parsed.utility === 'leading' && parsed.value) {
564
- // Handle arbitrary values first
565
- if (parsed.arbitrary) {
566
- return { 'line-height': parsed.value }
567
- }
568
- // Named line-height values
569
- const lineHeights: Record<string, string> = {
570
- none: '1',
571
- tight: '1.25',
572
- snug: '1.375',
573
- normal: '1.5',
574
- relaxed: '1.625',
575
- loose: '2',
576
- // Numeric values (rem-based)
577
- '3': '0.75rem',
578
- '4': '1rem',
579
- '5': '1.25rem',
580
- '6': '1.5rem',
581
- '7': '1.75rem',
582
- '8': '2rem',
583
- '9': '2.25rem',
584
- '10': '2.5rem',
585
- }
586
- return lineHeights[parsed.value] ? { 'line-height': lineHeights[parsed.value] } : undefined
587
- }
588
- return undefined
589
- }
590
-
591
- export const textAlignRule: UtilityRule = (parsed) => {
592
- if (parsed.utility === 'text' && parsed.value) {
593
- const aligns: Record<string, string> = {
594
- left: 'left',
595
- center: 'center',
596
- right: 'right',
597
- justify: 'justify',
598
- }
599
- return aligns[parsed.value] ? { 'text-align': aligns[parsed.value] } : undefined
600
- }
601
- }
602
-
603
- // Border utilities
604
- export const borderWidthRule: UtilityRule = (parsed) => {
605
- if (parsed.utility === 'border') {
606
- if (!parsed.value) {
607
- return { 'border-width': '1px' }
608
- }
609
-
610
- // Border width values: 0, 2, 4, 8
611
- const widthMap: Record<string, string> = {
612
- 0: '0px',
613
- 2: '2px',
614
- 4: '4px',
615
- 8: '8px',
616
- }
617
-
618
- // Handle border-0, border-2, border-4, border-8
619
- if (widthMap[parsed.value]) {
620
- return { 'border-width': widthMap[parsed.value] }
621
- }
622
-
623
- const sideMap: Record<string, string | string[]> = {
624
- t: 'border-top-width',
625
- r: 'border-right-width',
626
- b: 'border-bottom-width',
627
- l: 'border-left-width',
628
- }
629
-
630
- // Handle border-x and border-y shortcuts
631
- if (parsed.value === 'x') {
632
- return {
633
- 'border-left-width': '1px',
634
- 'border-right-width': '1px',
635
- } as Record<string, string>
636
- }
637
- if (parsed.value === 'y') {
638
- return {
639
- 'border-top-width': '1px',
640
- 'border-bottom-width': '1px',
641
- } as Record<string, string>
642
- }
643
-
644
- const prop = sideMap[parsed.value]
645
- if (typeof prop === 'string') {
646
- return { [prop]: '1px' } as Record<string, string>
647
- }
648
- return undefined
649
- }
650
- }
651
-
652
- // Border side width utilities (border-t-0, border-r-2, border-x-4, etc.)
653
- export const borderSideWidthRule: UtilityRule = (parsed) => {
654
- const sideUtilities: Record<string, string | string[]> = {
655
- 'border-t': 'border-top-width',
656
- 'border-r': 'border-right-width',
657
- 'border-b': 'border-bottom-width',
658
- 'border-l': 'border-left-width',
659
- 'border-x': ['border-left-width', 'border-right-width'],
660
- 'border-y': ['border-top-width', 'border-bottom-width'],
661
- // Logical borders (for RTL support)
662
- 'border-s': 'border-inline-start-width',
663
- 'border-e': 'border-inline-end-width',
664
- }
665
-
666
- const prop = sideUtilities[parsed.utility]
667
- if (!prop)
668
- return undefined
669
-
670
- // Width values: 0, 2, 4, 8 (or default to 1px if no value)
671
- const widthMap: Record<string, string> = {
672
- 0: '0px',
673
- 2: '2px',
674
- 4: '4px',
675
- 8: '8px',
676
- }
677
-
678
- const width = parsed.value ? widthMap[parsed.value] : '1px'
679
- if (!width)
680
- return undefined
681
-
682
- if (Array.isArray(prop)) {
683
- return prop.reduce((acc, p) => ({ ...acc, [p]: width }), {} as Record<string, string>)
684
- }
685
-
686
- return { [prop]: width }
687
- }
688
-
689
- export const borderRadiusRule: UtilityRule = (parsed, config) => {
690
- if (parsed.utility === 'rounded') {
691
- const value = parsed.value ? config.theme.borderRadius[parsed.value] : config.theme.borderRadius.DEFAULT
692
- return value ? { 'border-radius': value } : undefined
693
- }
694
-
695
- // Logical border-radius utilities (for RTL/LTR support)
696
- // rounded-s-* (start) - applies to start corners
697
- if (parsed.utility === 'rounded-s' && parsed.value) {
698
- const value = config.theme.borderRadius[parsed.value] || parsed.value
699
- return {
700
- 'border-start-start-radius': value,
701
- 'border-end-start-radius': value,
702
- } as Record<string, string>
703
- }
704
- // rounded-e-* (end) - applies to end corners
705
- if (parsed.utility === 'rounded-e' && parsed.value) {
706
- const value = config.theme.borderRadius[parsed.value] || parsed.value
707
- return {
708
- 'border-start-end-radius': value,
709
- 'border-end-end-radius': value,
710
- } as Record<string, string>
711
- }
712
- // rounded-ss-* (start-start corner)
713
- if (parsed.utility === 'rounded-ss' && parsed.value) {
714
- const value = config.theme.borderRadius[parsed.value] || parsed.value
715
- return { 'border-start-start-radius': value } as Record<string, string>
716
- }
717
- // rounded-se-* (start-end corner)
718
- if (parsed.utility === 'rounded-se' && parsed.value) {
719
- const value = config.theme.borderRadius[parsed.value] || parsed.value
720
- return { 'border-start-end-radius': value } as Record<string, string>
721
- }
722
- // rounded-es-* (end-start corner)
723
- if (parsed.utility === 'rounded-es' && parsed.value) {
724
- const value = config.theme.borderRadius[parsed.value] || parsed.value
725
- return { 'border-end-start-radius': value } as Record<string, string>
726
- }
727
- // rounded-ee-* (end-end corner)
728
- if (parsed.utility === 'rounded-ee' && parsed.value) {
729
- const value = config.theme.borderRadius[parsed.value] || parsed.value
730
- return { 'border-end-end-radius': value } as Record<string, string>
731
- }
732
- }
733
-
734
- // Export all rules (order matters - more specific rules first)
735
- export const builtInRules: UtilityRule[] = [
736
- // CRITICAL: Most common utilities first for O(1) lookup performance
737
- // Rule order matters! More specific rules must come before more general ones.
738
-
739
- // Spacing and sizing rules (w, h, p, m are extremely common)
740
- spacingRule,
741
- sizingRule,
742
-
743
- // ALL rules that use utility names that might conflict MUST be ordered correctly!
744
- // More specific rules must come before more general ones.
745
-
746
- // Flexbox/Grid alignment rules (content-* for align-content)
747
- // MUST come before typography contentRule which generates CSS content property
748
- alignContentRule, // handles content-center, content-start, etc. -> align-content
749
-
750
- // Typography rules (text-*)
751
- fontSizeRule, // handles text-{size} (text-xl, text-sm, etc.)
752
- textAlignRule, // handles text-{align} (text-center, text-left, etc.)
753
- ...typographyRules, // handles text-ellipsis, text-wrap, text-transform, contentRule, etc.
754
- fontWeightRule,
755
- leadingRule, // handles leading-{size} (leading-tight, leading-none, etc.)
756
-
757
- // Effects rules that use 'bg' utility (bg-gradient-*, bg-fixed, bg-clip-*, etc.)
758
- ...effectsRules,
759
-
760
- // Placeholder color rule (placeholder-{color} -> ::placeholder { color })
761
- placeholderColorRule,
762
-
763
- // Color rule (bg, text, border are very common)
764
- // IMPORTANT: This must come AFTER all specific text-*, bg-*, border-* rules
765
- // because it will match ANY text-*, bg-*, border-* class
766
- colorRule,
767
-
768
- // Advanced rules (container, ring, space, divide, gradients, etc.)
769
- ...advancedRules,
770
-
771
- // Layout rules (specific positioning and display)
772
- ...layoutRules,
773
-
774
- // Other Flexbox rules
775
- flexDirectionRule,
776
- flexWrapRule,
777
- flexRule,
778
- flexGrowRule,
779
- flexShrinkRule,
780
- justifyContentRule,
781
- alignItemsRule,
782
- justifyItemsRule,
783
-
784
- // Grid rules
785
- ...gridRules,
786
-
787
- // Transform and transition rules
788
- ...transformsRules,
789
-
790
- // Effects and filters
791
- ...effectsRules,
792
-
793
- // Interactivity, SVG, and accessibility
794
- ...interactivityRules,
795
-
796
- // Forms utilities
797
- ...formsRules,
798
-
799
- // Border rules (specific side rules first)
800
- borderSideWidthRule,
801
- borderWidthRule,
802
- borderRadiusRule,
803
-
804
- // Container query utilities (@container, @container-normal, @container/name)
805
- containerRule,
806
-
807
- // Display rule last (most general - matches many utility names)
808
- displayRule,
809
- ]