@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.
- package/LICENSE.md +21 -0
- package/README.md +390 -0
- package/dist/build.d.ts +24 -0
- package/dist/config.d.ts +5 -0
- package/dist/generator.d.ts +31 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12798 -0
- package/dist/parser.d.ts +42 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/preflight-forms.d.ts +5 -0
- package/dist/preflight.d.ts +2 -0
- package/dist/rules-advanced.d.ts +27 -0
- package/dist/rules-effects.d.ts +25 -0
- package/dist/rules-forms.d.ts +7 -0
- package/dist/rules-grid.d.ts +13 -0
- package/dist/rules-interactivity.d.ts +41 -0
- package/dist/rules-layout.d.ts +26 -0
- package/dist/rules-transforms.d.ts +33 -0
- package/dist/rules-typography.d.ts +41 -0
- package/dist/rules.d.ts +39 -0
- package/dist/scanner.d.ts +18 -0
- package/dist/transformer-compile-class.d.ts +37 -0
- package/{src/types.ts → dist/types.d.ts} +17 -86
- package/package.json +1 -1
- package/PLUGIN.md +0 -235
- package/benchmark/framework-comparison.bench.ts +0 -850
- package/bin/cli.ts +0 -365
- package/bin/crosswind +0 -0
- package/bin/headwind +0 -0
- package/build.ts +0 -8
- package/crosswind.config.ts +0 -9
- package/example/comprehensive.html +0 -70
- package/example/index.html +0 -21
- package/example/output.css +0 -236
- package/examples/plugin/README.md +0 -112
- package/examples/plugin/build.ts +0 -32
- package/examples/plugin/src/index.html +0 -34
- package/examples/plugin/src/index.ts +0 -7
- package/headwind +0 -2
- package/src/build.ts +0 -101
- package/src/config.ts +0 -529
- package/src/generator.ts +0 -2173
- package/src/index.ts +0 -10
- package/src/parser.ts +0 -1471
- package/src/plugin.ts +0 -118
- package/src/preflight-forms.ts +0 -229
- package/src/preflight.ts +0 -388
- package/src/rules-advanced.ts +0 -477
- package/src/rules-effects.ts +0 -461
- package/src/rules-forms.ts +0 -103
- package/src/rules-grid.ts +0 -241
- package/src/rules-interactivity.ts +0 -525
- package/src/rules-layout.ts +0 -385
- package/src/rules-transforms.ts +0 -412
- package/src/rules-typography.ts +0 -486
- package/src/rules.ts +0 -809
- package/src/scanner.ts +0 -84
- package/src/transformer-compile-class.ts +0 -275
- package/test/advanced-features.test.ts +0 -911
- package/test/arbitrary.test.ts +0 -396
- package/test/attributify.test.ts +0 -592
- package/test/bracket-syntax.test.ts +0 -1133
- package/test/build.test.ts +0 -99
- package/test/colors.test.ts +0 -934
- package/test/flexbox.test.ts +0 -669
- package/test/generator.test.ts +0 -597
- package/test/grid.test.ts +0 -584
- package/test/layout.test.ts +0 -404
- package/test/modifiers.test.ts +0 -417
- package/test/parser.test.ts +0 -564
- package/test/performance-regression.test.ts +0 -376
- package/test/performance.test.ts +0 -568
- package/test/plugin.test.ts +0 -160
- package/test/scanner.test.ts +0 -94
- package/test/sizing.test.ts +0 -481
- package/test/spacing.test.ts +0 -394
- package/test/transformer-compile-class.test.ts +0 -287
- package/test/transforms.test.ts +0 -448
- package/test/typography.test.ts +0 -632
- package/test/variants-form-states.test.ts +0 -225
- package/test/variants-group-peer.test.ts +0 -66
- package/test/variants-media.test.ts +0 -213
- package/test/variants-positional.test.ts +0 -58
- package/test/variants-pseudo-elements.test.ts +0 -47
- package/test/variants-state.test.ts +0 -62
- 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
|
-
]
|