@webjsdev/ui 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,839 @@
1
+ /**
2
+ * The class grammar behind `webjsui lint`: how one Tailwind token is parsed,
3
+ * which class GROUP it belongs to, and which appearance CATEGORY that group
4
+ * maps to.
5
+ *
6
+ * `GROUP_CATEGORY` is a verbatim transcription of `packages/lint/src/grammar/
7
+ * categories.ts` from `shadcn-ui/lint` (https://github.com/shadcn-ui/lint),
8
+ * same group ids in the same order, so it can be diffed against upstream. The
9
+ * taxonomy is adopted as-is on purpose: `@webjsdev/ui` exists for shadcn
10
+ * parity, and a WebJs-local redefinition would make `allow: ["layout"]` mean
11
+ * two different things in two tools an agent runs side by side. Two placements
12
+ * are counterintuitive and both are shadcn's: padding is `spacing` and margin
13
+ * is layout (`null`).
14
+ *
15
+ * `groupOf(utility)` is WebJs's own resolver from a utility to one of those
16
+ * group ids. It is a prefix table rather than `tailwind-merge`'s config, which
17
+ * the linter deliberately does not depend on (a runtime dependency for a
18
+ * dev-time analysis). A utility it cannot place resolves to `null`, which is
19
+ * layout, the permissive direction.
20
+ *
21
+ * @module lint/grammar
22
+ */
23
+
24
+ /** @type {Record<string, 'color'|'typography'|'spacing'|'shape'|'effects'|'motion'|null>} */
25
+ export const GROUP_CATEGORY = {
26
+ aspect: null,
27
+ container: null,
28
+ 'container-type': null,
29
+ 'container-named': null,
30
+ contain: null,
31
+ 'contain-size': null,
32
+ 'contain-layout': null,
33
+ 'contain-paint': null,
34
+ 'contain-style': null,
35
+ columns: null,
36
+ 'break-after': null,
37
+ 'break-before': null,
38
+ 'break-inside': null,
39
+ 'box-decoration': null,
40
+ box: null,
41
+ display: null,
42
+ sr: null,
43
+ float: null,
44
+ clear: null,
45
+ isolation: null,
46
+ 'object-fit': null,
47
+ 'object-position': null,
48
+ overflow: null,
49
+ 'overflow-x': null,
50
+ 'overflow-y': null,
51
+ overscroll: null,
52
+ 'overscroll-x': null,
53
+ 'overscroll-y': null,
54
+ position: null,
55
+ inset: null,
56
+ 'inset-x': null,
57
+ 'inset-y': null,
58
+ start: null,
59
+ end: null,
60
+ 'inset-bs': null,
61
+ 'inset-be': null,
62
+ top: null,
63
+ right: null,
64
+ bottom: null,
65
+ left: null,
66
+ visibility: null,
67
+ z: null,
68
+ basis: null,
69
+ 'flex-direction': null,
70
+ 'flex-wrap': null,
71
+ flex: null,
72
+ grow: null,
73
+ shrink: null,
74
+ order: null,
75
+ 'grid-cols': null,
76
+ 'col-start-end': null,
77
+ 'col-start': null,
78
+ 'col-end': null,
79
+ 'grid-rows': null,
80
+ 'row-start-end': null,
81
+ 'row-start': null,
82
+ 'row-end': null,
83
+ 'grid-flow': null,
84
+ 'auto-cols': null,
85
+ 'auto-rows': null,
86
+ gap: 'spacing',
87
+ 'gap-x': 'spacing',
88
+ 'gap-y': 'spacing',
89
+ 'justify-content': null,
90
+ 'justify-items': null,
91
+ 'justify-self': null,
92
+ 'align-content': null,
93
+ 'align-items': null,
94
+ 'align-self': null,
95
+ 'place-content': null,
96
+ 'place-items': null,
97
+ 'place-self': null,
98
+ p: 'spacing',
99
+ px: 'spacing',
100
+ py: 'spacing',
101
+ ps: 'spacing',
102
+ pe: 'spacing',
103
+ pbs: 'spacing',
104
+ pbe: 'spacing',
105
+ pt: 'spacing',
106
+ pr: 'spacing',
107
+ pb: 'spacing',
108
+ pl: 'spacing',
109
+ m: null,
110
+ mx: null,
111
+ my: null,
112
+ ms: null,
113
+ me: null,
114
+ mbs: null,
115
+ mbe: null,
116
+ mt: null,
117
+ mr: null,
118
+ mb: null,
119
+ ml: null,
120
+ 'space-x': 'spacing',
121
+ 'space-x-reverse': 'spacing',
122
+ 'space-y': 'spacing',
123
+ 'space-y-reverse': 'spacing',
124
+ size: null,
125
+ 'inline-size': null,
126
+ 'min-inline-size': null,
127
+ 'max-inline-size': null,
128
+ 'block-size': null,
129
+ 'min-block-size': null,
130
+ 'max-block-size': null,
131
+ w: null,
132
+ 'min-w': null,
133
+ 'max-w': null,
134
+ h: null,
135
+ 'min-h': null,
136
+ 'max-h': null,
137
+ 'font-size': 'typography',
138
+ 'font-smoothing': 'typography',
139
+ 'font-style': 'typography',
140
+ 'font-weight': 'typography',
141
+ 'font-stretch': 'typography',
142
+ 'font-family': 'typography',
143
+ 'font-features': 'typography',
144
+ 'fvn-normal': 'typography',
145
+ 'fvn-ordinal': 'typography',
146
+ 'fvn-slashed-zero': 'typography',
147
+ 'fvn-figure': 'typography',
148
+ 'fvn-spacing': 'typography',
149
+ 'fvn-fraction': 'typography',
150
+ tracking: 'typography',
151
+ 'line-clamp': 'typography',
152
+ leading: 'typography',
153
+ 'list-image': 'typography',
154
+ 'list-style-position': 'typography',
155
+ 'list-style-type': 'typography',
156
+ 'text-alignment': null,
157
+ 'placeholder-color': 'color',
158
+ 'text-color': 'color',
159
+ 'text-decoration': 'typography',
160
+ 'text-decoration-style': 'typography',
161
+ 'text-decoration-thickness': 'typography',
162
+ 'text-decoration-color': 'color',
163
+ 'underline-offset': 'typography',
164
+ 'text-transform': 'typography',
165
+ 'text-overflow': 'typography',
166
+ 'text-wrap': 'typography',
167
+ indent: 'typography',
168
+ 'tab-size': null,
169
+ 'vertical-align': null,
170
+ whitespace: null,
171
+ break: null,
172
+ wrap: null,
173
+ hyphens: 'typography',
174
+ content: null,
175
+ 'bg-attachment': 'effects',
176
+ 'bg-clip': 'effects',
177
+ 'bg-origin': 'effects',
178
+ 'bg-position': 'effects',
179
+ 'bg-repeat': 'effects',
180
+ 'bg-size': 'effects',
181
+ 'bg-image': 'effects',
182
+ 'bg-color': 'color',
183
+ 'gradient-from-pos': 'effects',
184
+ 'gradient-via-pos': 'effects',
185
+ 'gradient-to-pos': 'effects',
186
+ 'gradient-from': 'color',
187
+ 'gradient-via': 'color',
188
+ 'gradient-to': 'color',
189
+ rounded: 'shape',
190
+ 'rounded-s': 'shape',
191
+ 'rounded-e': 'shape',
192
+ 'rounded-t': 'shape',
193
+ 'rounded-r': 'shape',
194
+ 'rounded-b': 'shape',
195
+ 'rounded-l': 'shape',
196
+ 'rounded-ss': 'shape',
197
+ 'rounded-se': 'shape',
198
+ 'rounded-ee': 'shape',
199
+ 'rounded-es': 'shape',
200
+ 'rounded-tl': 'shape',
201
+ 'rounded-tr': 'shape',
202
+ 'rounded-br': 'shape',
203
+ 'rounded-bl': 'shape',
204
+ 'border-w': 'shape',
205
+ 'border-w-x': 'shape',
206
+ 'border-w-y': 'shape',
207
+ 'border-w-s': 'shape',
208
+ 'border-w-e': 'shape',
209
+ 'border-w-bs': 'shape',
210
+ 'border-w-be': 'shape',
211
+ 'border-w-t': 'shape',
212
+ 'border-w-r': 'shape',
213
+ 'border-w-b': 'shape',
214
+ 'border-w-l': 'shape',
215
+ 'divide-x': 'shape',
216
+ 'divide-x-reverse': 'shape',
217
+ 'divide-y': 'shape',
218
+ 'divide-y-reverse': 'shape',
219
+ 'border-style': 'shape',
220
+ 'divide-style': 'shape',
221
+ 'border-color': 'color',
222
+ 'border-color-x': 'color',
223
+ 'border-color-y': 'color',
224
+ 'border-color-s': 'color',
225
+ 'border-color-e': 'color',
226
+ 'border-color-bs': 'color',
227
+ 'border-color-be': 'color',
228
+ 'border-color-t': 'color',
229
+ 'border-color-r': 'color',
230
+ 'border-color-b': 'color',
231
+ 'border-color-l': 'color',
232
+ 'divide-color': 'color',
233
+ 'outline-style': 'shape',
234
+ 'outline-offset': 'shape',
235
+ 'outline-w': 'shape',
236
+ 'outline-color': 'color',
237
+ shadow: 'effects',
238
+ 'shadow-color': 'color',
239
+ 'inset-shadow': 'effects',
240
+ 'inset-shadow-color': 'color',
241
+ 'ring-w': 'shape',
242
+ 'ring-w-inset': 'shape',
243
+ 'ring-color': 'color',
244
+ 'ring-offset-w': 'shape',
245
+ 'ring-offset-color': 'color',
246
+ 'inset-ring-w': 'shape',
247
+ 'inset-ring-color': 'color',
248
+ 'text-shadow': 'effects',
249
+ 'text-shadow-color': 'color',
250
+ opacity: 'effects',
251
+ 'mix-blend': 'effects',
252
+ 'bg-blend': 'effects',
253
+ 'mask-clip': 'effects',
254
+ 'mask-composite': 'effects',
255
+ 'mask-image-linear-pos': 'effects',
256
+ 'mask-image-linear-from-pos': 'effects',
257
+ 'mask-image-linear-to-pos': 'effects',
258
+ 'mask-image-linear-from-color': 'color',
259
+ 'mask-image-linear-to-color': 'color',
260
+ 'mask-image-t-from-pos': 'effects',
261
+ 'mask-image-t-to-pos': 'effects',
262
+ 'mask-image-t-from-color': 'color',
263
+ 'mask-image-t-to-color': 'color',
264
+ 'mask-image-r-from-pos': 'effects',
265
+ 'mask-image-r-to-pos': 'effects',
266
+ 'mask-image-r-from-color': 'color',
267
+ 'mask-image-r-to-color': 'color',
268
+ 'mask-image-b-from-pos': 'effects',
269
+ 'mask-image-b-to-pos': 'effects',
270
+ 'mask-image-b-from-color': 'color',
271
+ 'mask-image-b-to-color': 'color',
272
+ 'mask-image-l-from-pos': 'effects',
273
+ 'mask-image-l-to-pos': 'effects',
274
+ 'mask-image-l-from-color': 'color',
275
+ 'mask-image-l-to-color': 'color',
276
+ 'mask-image-x-from-pos': 'effects',
277
+ 'mask-image-x-to-pos': 'effects',
278
+ 'mask-image-x-from-color': 'color',
279
+ 'mask-image-x-to-color': 'color',
280
+ 'mask-image-y-from-pos': 'effects',
281
+ 'mask-image-y-to-pos': 'effects',
282
+ 'mask-image-y-from-color': 'color',
283
+ 'mask-image-y-to-color': 'color',
284
+ 'mask-image-radial': 'effects',
285
+ 'mask-image-radial-from-pos': 'effects',
286
+ 'mask-image-radial-to-pos': 'effects',
287
+ 'mask-image-radial-from-color': 'color',
288
+ 'mask-image-radial-to-color': 'color',
289
+ 'mask-image-radial-shape': 'effects',
290
+ 'mask-image-radial-size': 'effects',
291
+ 'mask-image-radial-pos': 'effects',
292
+ 'mask-image-conic-pos': 'effects',
293
+ 'mask-image-conic-from-pos': 'effects',
294
+ 'mask-image-conic-to-pos': 'effects',
295
+ 'mask-image-conic-from-color': 'color',
296
+ 'mask-image-conic-to-color': 'color',
297
+ 'mask-mode': 'effects',
298
+ 'mask-origin': 'effects',
299
+ 'mask-position': 'effects',
300
+ 'mask-repeat': 'effects',
301
+ 'mask-size': 'effects',
302
+ 'mask-type': 'effects',
303
+ 'mask-image': 'effects',
304
+ filter: 'effects',
305
+ blur: 'effects',
306
+ brightness: 'effects',
307
+ contrast: 'effects',
308
+ 'drop-shadow': 'effects',
309
+ 'drop-shadow-color': 'color',
310
+ grayscale: 'effects',
311
+ 'hue-rotate': 'effects',
312
+ invert: 'effects',
313
+ saturate: 'effects',
314
+ sepia: 'effects',
315
+ 'backdrop-filter': 'effects',
316
+ 'backdrop-blur': 'effects',
317
+ 'backdrop-brightness': 'effects',
318
+ 'backdrop-contrast': 'effects',
319
+ 'backdrop-grayscale': 'effects',
320
+ 'backdrop-hue-rotate': 'effects',
321
+ 'backdrop-invert': 'effects',
322
+ 'backdrop-opacity': 'effects',
323
+ 'backdrop-saturate': 'effects',
324
+ 'backdrop-sepia': 'effects',
325
+ 'border-collapse': null,
326
+ 'border-spacing': 'spacing',
327
+ 'border-spacing-x': 'spacing',
328
+ 'border-spacing-y': 'spacing',
329
+ 'table-layout': null,
330
+ caption: null,
331
+ transition: 'motion',
332
+ 'transition-behavior': 'motion',
333
+ duration: 'motion',
334
+ ease: 'motion',
335
+ delay: 'motion',
336
+ animate: 'motion',
337
+ backface: null,
338
+ perspective: null,
339
+ 'perspective-origin': null,
340
+ rotate: null,
341
+ 'rotate-x': null,
342
+ 'rotate-y': null,
343
+ 'rotate-z': null,
344
+ scale: null,
345
+ 'scale-x': null,
346
+ 'scale-y': null,
347
+ 'scale-z': null,
348
+ 'scale-3d': null,
349
+ skew: null,
350
+ 'skew-x': null,
351
+ 'skew-y': null,
352
+ transform: null,
353
+ 'transform-origin': null,
354
+ 'transform-style': null,
355
+ translate: null,
356
+ 'translate-x': null,
357
+ 'translate-y': null,
358
+ 'translate-z': null,
359
+ 'translate-none': null,
360
+ zoom: null,
361
+ accent: 'color',
362
+ appearance: null,
363
+ 'caret-color': 'color',
364
+ 'color-scheme': null,
365
+ cursor: null,
366
+ 'field-sizing': null,
367
+ 'pointer-events': null,
368
+ resize: null,
369
+ 'scroll-behavior': null,
370
+ 'scrollbar-thumb-color': 'color',
371
+ 'scrollbar-track-color': 'color',
372
+ 'scrollbar-gutter': null,
373
+ 'scrollbar-w': null,
374
+ 'scroll-m': null,
375
+ 'scroll-mx': null,
376
+ 'scroll-my': null,
377
+ 'scroll-ms': null,
378
+ 'scroll-me': null,
379
+ 'scroll-mbs': null,
380
+ 'scroll-mbe': null,
381
+ 'scroll-mt': null,
382
+ 'scroll-mr': null,
383
+ 'scroll-mb': null,
384
+ 'scroll-ml': null,
385
+ 'scroll-p': null,
386
+ 'scroll-px': null,
387
+ 'scroll-py': null,
388
+ 'scroll-ps': null,
389
+ 'scroll-pe': null,
390
+ 'scroll-pbs': null,
391
+ 'scroll-pbe': null,
392
+ 'scroll-pt': null,
393
+ 'scroll-pr': null,
394
+ 'scroll-pb': null,
395
+ 'scroll-pl': null,
396
+ 'snap-align': null,
397
+ 'snap-stop': null,
398
+ 'snap-type': null,
399
+ 'snap-strictness': null,
400
+ touch: null,
401
+ 'touch-x': null,
402
+ 'touch-y': null,
403
+ 'touch-pz': null,
404
+ select: null,
405
+ 'will-change': null,
406
+ fill: 'color',
407
+ 'stroke-w': 'shape',
408
+ stroke: 'color',
409
+ 'forced-color-adjust': null,
410
+ };
411
+
412
+ /**
413
+ * Arbitrary properties (`[color:red]`) are categorized by CSS property name
414
+ * instead, first match winning (upstream: `ARBITRARY_PROPERTY_RULES`).
415
+ * @type {Array<[RegExp, 'color'|'typography'|'spacing'|'shape'|'effects'|'motion']>}
416
+ */
417
+ const ARBITRARY_PROPERTY_RULES = [
418
+ [/(?:^|-)color$|^(?:background|fill|stroke|--tw-(?:gradient-(?:from|via|to)|shadow-color|ring-color|inset-ring-color|inset-shadow-color)|--tw-.*-color)$/, 'color'],
419
+ [/^(?:padding|gap$|row-gap$|column-gap$)/, 'spacing'],
420
+ [/^(?:font|letter-spacing$|line-height$|text-decoration|text-transform$|text-indent$|text-underline|word-spacing$|list-style)/, 'typography'],
421
+ [/^(?:border(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?(?:-(?:width|style|radius))?$|border-.*-radius$|outline|--tw-ring-width$|--tw-ring-inset$)/, 'shape'],
422
+ [/^(?:box-shadow|text-shadow|opacity|filter|backdrop-filter|mix-blend-mode|background-blend-mode|--tw-(?:shadow|inset-shadow|drop-shadow|blur|brightness|contrast|grayscale|hue-rotate|invert|saturate|sepia|backdrop-.*)$)/, 'effects'],
423
+ [/^(?:transition|animation|--tw-(?:duration|ease|delay)$)/, 'motion'],
424
+ ];
425
+
426
+ const ARBITRARY_PREFIX = 'arbitrary..';
427
+
428
+ export const CATEGORIES = ['color', 'typography', 'spacing', 'shape', 'effects', 'motion'];
429
+
430
+ /**
431
+ * The appearance category of a group id (`null` is layout).
432
+ * @param {string|null} groupId
433
+ */
434
+ export function categoryOf(groupId) {
435
+ if (groupId === null) return null;
436
+ if (groupId.startsWith(ARBITRARY_PREFIX)) {
437
+ const property = groupId.slice(ARBITRARY_PREFIX.length);
438
+ for (const [pattern, category] of ARBITRARY_PROPERTY_RULES) if (pattern.test(property)) return category;
439
+ return null;
440
+ }
441
+ return GROUP_CATEGORY[groupId] ?? null;
442
+ }
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // Utility -> group resolution
446
+ // ---------------------------------------------------------------------------
447
+
448
+ const T_SHIRT = /^(?:\d*xs|sm|md|lg|\d*xl)$/;
449
+ const NUMERIC = /^-?\d+(?:\.\d+)?$/;
450
+ const FRACTION = /^\d+\/\d+$/;
451
+ const LENGTH_HINT = /^(?:length|size|percentage|number):/;
452
+ const COLOR_HINT = /^color:/;
453
+ const COLOR_VALUE = /^(?:#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|var\(--color|--color-)/;
454
+
455
+ /** `[...]` or `(...)` shorthand inner text, or null when the value is neither. */
456
+ function arbitraryInner(value) {
457
+ if (value.startsWith('[') && value.endsWith(']')) return value.slice(1, -1);
458
+ if (value.startsWith('(') && value.endsWith(')')) return value.slice(1, -1);
459
+ return null;
460
+ }
461
+
462
+ /** Whether an arbitrary value reads as a colour (a hint, a colour function or a colour variable). */
463
+ function isArbitraryColor(value) {
464
+ const inner = arbitraryInner(value);
465
+ if (inner === null) return false;
466
+ return COLOR_HINT.test(inner) || COLOR_VALUE.test(inner);
467
+ }
468
+
469
+ /** Whether an arbitrary value reads as a length / number (the non-colour direction). */
470
+ function isArbitraryLength(value) {
471
+ const inner = arbitraryInner(value);
472
+ if (inner === null) return false;
473
+ if (LENGTH_HINT.test(inner)) return true;
474
+ if (COLOR_HINT.test(inner) || COLOR_VALUE.test(inner)) return false;
475
+ return /^(?:-?\d|calc\(|min\(|max\(|clamp\(|var\()/.test(inner);
476
+ }
477
+
478
+ function isNumberish(value) {
479
+ return NUMERIC.test(value) || FRACTION.test(value) || value === 'px' || value === 'full' || value === 'auto';
480
+ }
481
+
482
+ /**
483
+ * Split `utility` at its first `-` into `[head, rest]` where `head` is the
484
+ * longest prefix in `heads`. Returns null when no head matches.
485
+ * @param {string} utility
486
+ * @param {string[]} heads
487
+ */
488
+ function splitHead(utility, heads) {
489
+ for (const h of heads) {
490
+ if (utility === h) return [h, ''];
491
+ if (utility.startsWith(h + '-')) return [h, utility.slice(h.length + 1)];
492
+ }
493
+ return null;
494
+ }
495
+
496
+ /** Groups that take `<prefix>` or `<prefix>-<value>` and need no value disambiguation. Longest first. */
497
+ const SIMPLE_GROUPS = [
498
+ 'container-type', 'contain', 'columns', 'break-after', 'break-before', 'break-inside', 'box-decoration',
499
+ 'overflow-x', 'overflow-y', 'overflow', 'overscroll-x', 'overscroll-y', 'overscroll',
500
+ 'inset-x', 'inset-y', 'inset-bs', 'inset-be', 'inset', 'start', 'end', 'top', 'right', 'bottom', 'left', 'z',
501
+ 'basis', 'grow', 'shrink', 'order', 'grid-cols', 'grid-rows', 'grid-flow', 'auto-cols', 'auto-rows',
502
+ 'gap-x', 'gap-y', 'gap',
503
+ 'justify-items', 'justify-self', 'place-content', 'place-items', 'place-self',
504
+ 'px', 'py', 'ps', 'pe', 'pbs', 'pbe', 'pt', 'pr', 'pb', 'pl', 'p',
505
+ 'mx', 'my', 'ms', 'me', 'mbs', 'mbe', 'mt', 'mr', 'mb', 'ml', 'm',
506
+ 'space-x-reverse', 'space-y-reverse', 'space-x', 'space-y',
507
+ 'size', 'min-inline-size', 'max-inline-size', 'inline-size', 'min-block-size', 'max-block-size', 'block-size',
508
+ 'min-w', 'max-w', 'w', 'min-h', 'max-h', 'h',
509
+ 'font-stretch', 'tracking', 'line-clamp', 'leading', 'underline-offset', 'indent', 'tab-size', 'whitespace', 'hyphens',
510
+ 'gradient-from-pos', 'gradient-via-pos', 'gradient-to-pos',
511
+ 'outline-offset', 'opacity', 'mix-blend', 'bg-blend',
512
+ 'blur', 'brightness', 'contrast', 'grayscale', 'hue-rotate', 'invert', 'saturate', 'sepia',
513
+ 'backdrop-blur', 'backdrop-brightness', 'backdrop-contrast', 'backdrop-grayscale', 'backdrop-hue-rotate',
514
+ 'backdrop-invert', 'backdrop-opacity', 'backdrop-saturate', 'backdrop-sepia', 'backdrop-filter',
515
+ 'border-spacing-x', 'border-spacing-y', 'border-spacing', 'caption',
516
+ 'transition-behavior', 'transition', 'duration', 'ease', 'delay', 'animate',
517
+ 'backface', 'perspective-origin', 'perspective',
518
+ 'rotate-x', 'rotate-y', 'rotate-z', 'rotate', 'scale-3d', 'scale-x', 'scale-y', 'scale-z', 'scale',
519
+ 'skew-x', 'skew-y', 'skew', 'translate-none', 'translate-x', 'translate-y', 'translate-z', 'translate', 'zoom',
520
+ 'accent', 'appearance', 'cursor', 'field-sizing', 'pointer-events', 'resize', 'scroll-behavior',
521
+ 'scrollbar-gutter', 'scrollbar-w',
522
+ 'scroll-mx', 'scroll-my', 'scroll-ms', 'scroll-me', 'scroll-mbs', 'scroll-mbe', 'scroll-mt', 'scroll-mr', 'scroll-mb', 'scroll-ml', 'scroll-m',
523
+ 'scroll-px', 'scroll-py', 'scroll-ps', 'scroll-pe', 'scroll-pbs', 'scroll-pbe', 'scroll-pt', 'scroll-pr', 'scroll-pb', 'scroll-pl', 'scroll-p',
524
+ 'snap-align', 'snap-stop', 'snap-type', 'snap-strictness', 'touch-x', 'touch-y', 'touch-pz', 'touch',
525
+ 'select', 'will-change', 'forced-color-adjust', 'aspect', 'container', 'float', 'clear', 'filter',
526
+ 'mask-clip', 'mask-composite', 'mask-mode', 'mask-origin', 'mask-position', 'mask-repeat', 'mask-size', 'mask-type',
527
+ 'placeholder', 'caret', 'fill', 'stroke',
528
+ ];
529
+
530
+ const DISPLAY = new Set(['block', 'inline-block', 'inline', 'flex', 'inline-flex', 'table', 'inline-table',
531
+ 'table-caption', 'table-cell', 'table-column', 'table-column-group', 'table-footer-group', 'table-header-group',
532
+ 'table-row-group', 'table-row', 'flow-root', 'grid', 'inline-grid', 'contents', 'list-item', 'hidden']);
533
+ const POSITION = new Set(['static', 'fixed', 'absolute', 'relative', 'sticky']);
534
+ const KEYWORDS = {
535
+ 'sr-only': 'sr', 'not-sr-only': 'sr', isolate: 'isolation', 'isolation-auto': 'isolation',
536
+ visible: 'visibility', invisible: 'visibility', collapse: 'visibility',
537
+ 'box-border': 'box', 'box-content': 'box',
538
+ italic: 'font-style', 'not-italic': 'font-style', antialiased: 'font-smoothing', 'subpixel-antialiased': 'font-smoothing',
539
+ 'normal-nums': 'fvn-normal', ordinal: 'fvn-ordinal', 'slashed-zero': 'fvn-slashed-zero',
540
+ 'lining-nums': 'fvn-figure', 'oldstyle-nums': 'fvn-figure', 'proportional-nums': 'fvn-spacing', 'tabular-nums': 'fvn-spacing',
541
+ 'diagonal-fractions': 'fvn-fraction', 'stacked-fractions': 'fvn-fraction',
542
+ underline: 'text-decoration', overline: 'text-decoration', 'line-through': 'text-decoration', 'no-underline': 'text-decoration',
543
+ uppercase: 'text-transform', lowercase: 'text-transform', capitalize: 'text-transform', 'normal-case': 'text-transform',
544
+ truncate: 'text-overflow', 'break-normal': 'break', 'break-words': 'break', 'break-all': 'break', 'break-keep': 'break',
545
+ 'border-collapse': 'border-collapse', 'border-separate': 'border-collapse',
546
+ 'table-auto': 'table-layout', 'table-fixed': 'table-layout',
547
+ 'transform-none': 'transform', 'transform-gpu': 'transform', 'transform-cpu': 'transform',
548
+ 'ring-inset': 'ring-w-inset', 'flex-wrap': 'flex-wrap', 'flex-nowrap': 'flex-wrap', 'flex-wrap-reverse': 'flex-wrap',
549
+ 'flex-row': 'flex-direction', 'flex-row-reverse': 'flex-direction', 'flex-col': 'flex-direction', 'flex-col-reverse': 'flex-direction',
550
+ };
551
+ const BORDER_STYLES = new Set(['solid', 'dashed', 'dotted', 'double', 'hidden', 'none']);
552
+ const RADIUS_SIDES = ['ss', 'se', 'ee', 'es', 'tl', 'tr', 'br', 'bl', 's', 'e', 't', 'r', 'b', 'l'];
553
+ const BORDER_SIDES = ['bs', 'be', 'x', 'y', 's', 'e', 't', 'r', 'b', 'l'];
554
+ const SHADOW_SIZES = /^(?:none|2xs|xs|sm|md|lg|xl|2xl|inner)$/;
555
+
556
+ /**
557
+ * Resolve a utility (variants, negative, important and opacity already
558
+ * stripped) to a class-group id, or `null` for one the table cannot place.
559
+ * @param {string} utility
560
+ * @returns {string|null}
561
+ */
562
+ export function groupOf(utility) {
563
+ if (!utility) return null;
564
+ // Arbitrary property: `[padding:13px]`.
565
+ if (utility.startsWith('[') && utility.endsWith(']')) {
566
+ const colon = utility.indexOf(':');
567
+ if (colon > 1) return ARBITRARY_PREFIX + utility.slice(1, colon);
568
+ return null;
569
+ }
570
+ if (DISPLAY.has(utility)) return 'display';
571
+ if (POSITION.has(utility)) return 'position';
572
+ if (Object.hasOwn(KEYWORDS, utility)) return KEYWORDS[utility];
573
+
574
+ let s;
575
+ // text-shadow-* (before text-*, whose head would otherwise swallow it)
576
+ if ((s = splitHead(utility, ['text-shadow']))) {
577
+ const v = s[1];
578
+ if (v === '' || SHADOW_SIZES.test(v) || isArbitraryLength(v)) return 'text-shadow';
579
+ return 'text-shadow-color';
580
+ }
581
+ // text-*
582
+ if ((s = splitHead(utility, ['text']))) {
583
+ const v = s[1];
584
+ if (/^(?:left|center|right|justify|start|end)$/.test(v)) return 'text-alignment';
585
+ if (/^(?:wrap|nowrap|balance|pretty)$/.test(v)) return 'text-wrap';
586
+ if (/^(?:ellipsis|clip)$/.test(v)) return 'text-overflow';
587
+ if (v === 'base' || T_SHIRT.test(v) || isArbitraryLength(v)) return 'font-size';
588
+ // A size with a line-height modifier, in every spelling of the modifier:
589
+ // `text-sm/6`, `text-sm/[17px]`, `text-sm/(--lh)`.
590
+ if (/^(?:base|xs|sm|lg|\dxl|xl)\/(?:[\w.]+|\[.+\]|\(.+\))$/.test(v)) return 'font-size';
591
+ return 'text-color';
592
+ }
593
+ // font-*
594
+ if ((s = splitHead(utility, ['font']))) {
595
+ const v = s[1];
596
+ if (/^(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/.test(v) || NUMERIC.test(v)) return 'font-weight';
597
+ if (arbitraryInner(v)?.startsWith('weight:') || (arbitraryInner(v) && NUMERIC.test(arbitraryInner(v)))) return 'font-weight';
598
+ return 'font-family';
599
+ }
600
+ // bg-*
601
+ if ((s = splitHead(utility, ['bg']))) {
602
+ const v = s[1];
603
+ if (/^blend-/.test(v)) return 'bg-blend';
604
+ if (/^(?:fixed|local|scroll)$/.test(v)) return 'bg-attachment';
605
+ if (/^clip-/.test(v)) return 'bg-clip';
606
+ if (/^origin-/.test(v)) return 'bg-origin';
607
+ if (/^(?:top|bottom|left|right|center)(?:-(?:top|bottom|left|right))?$/.test(v) || /^position-/.test(v)) return 'bg-position';
608
+ if (/^(?:repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/.test(v)) return 'bg-repeat';
609
+ if (/^(?:auto|cover|contain)$/.test(v) || /^size-/.test(v)) return 'bg-size';
610
+ if (v === 'none' || /^(?:linear|radial|conic|gradient)-/.test(v)) return 'bg-image';
611
+ const inner = arbitraryInner(v);
612
+ if (inner !== null && /^(?:url\(|image:|linear-gradient|radial-gradient|conic-gradient)/.test(inner)) return 'bg-image';
613
+ // A typed arbitrary value names its own property, so it is not a colour.
614
+ if (inner !== null && /^(?:length|size|percentage):/.test(inner)) return 'bg-size';
615
+ if (inner !== null && /^position:/.test(inner)) return 'bg-position';
616
+ return 'bg-color';
617
+ }
618
+ // gradient stops
619
+ for (const stop of ['from', 'via', 'to']) {
620
+ if ((s = splitHead(utility, [stop]))) {
621
+ const v = s[1];
622
+ if (/^\d+%$/.test(v) || NUMERIC.test(v) || isArbitraryLength(v)) return `gradient-${stop}-pos`;
623
+ return `gradient-${stop}`;
624
+ }
625
+ }
626
+ // rounded
627
+ if ((s = splitHead(utility, ['rounded']))) {
628
+ const v = s[1];
629
+ if (v === '') return 'rounded';
630
+ const side = RADIUS_SIDES.find((x) => v === x || v.startsWith(x + '-'));
631
+ return side ? `rounded-${side}` : 'rounded';
632
+ }
633
+ // border
634
+ if ((s = splitHead(utility, ['border']))) {
635
+ const v = s[1];
636
+ if (v === '') return 'border-w';
637
+ if (BORDER_STYLES.has(v)) return 'border-style';
638
+ if (v === 'collapse' || v === 'separate') return 'border-collapse';
639
+ if (v.startsWith('spacing')) {
640
+ // The group is the AXIS (`border-spacing-x`), never the value after it.
641
+ const axis = /^spacing-([xy])(?:-|$)/.exec(v);
642
+ return axis ? `border-spacing-${axis[1]}` : 'border-spacing';
643
+ }
644
+ const side = BORDER_SIDES.find((x) => v === x || v.startsWith(x + '-'));
645
+ const rest = side ? v.slice(side.length + 1) : v;
646
+ const suffix = side ? `-${side}` : '';
647
+ if (rest === '' || NUMERIC.test(rest) || isArbitraryLength(rest)) return `border-w${suffix}`;
648
+ if (BORDER_STYLES.has(rest)) return 'border-style';
649
+ return `border-color${suffix}`;
650
+ }
651
+ // divide
652
+ if ((s = splitHead(utility, ['divide']))) {
653
+ const v = s[1];
654
+ if (/^(?:x|y)(?:-reverse)?$/.test(v) || /^(?:x|y)-/.test(v)) {
655
+ const axis = v[0];
656
+ if (v.endsWith('-reverse')) return `divide-${axis}-reverse`;
657
+ return `divide-${axis}`;
658
+ }
659
+ if (BORDER_STYLES.has(v)) return 'divide-style';
660
+ return 'divide-color';
661
+ }
662
+ // outline
663
+ if ((s = splitHead(utility, ['outline']))) {
664
+ const v = s[1];
665
+ if (v === '' || NUMERIC.test(v) || isArbitraryLength(v)) return 'outline-w';
666
+ if (BORDER_STYLES.has(v)) return 'outline-style';
667
+ if (v.startsWith('offset')) return 'outline-offset';
668
+ return 'outline-color';
669
+ }
670
+ // ring / inset-ring
671
+ for (const [head, group] of [['inset-ring', 'inset-ring'], ['ring', 'ring']]) {
672
+ if ((s = splitHead(utility, [head]))) {
673
+ const v = s[1];
674
+ if (head === 'ring' && v.startsWith('offset')) {
675
+ const rest = v.slice(6).replace(/^-/, '');
676
+ return rest === '' || NUMERIC.test(rest) || isArbitraryLength(rest) ? 'ring-offset-w' : 'ring-offset-color';
677
+ }
678
+ if (v === '' || NUMERIC.test(v) || isArbitraryLength(v)) return `${group}-w`;
679
+ return `${group}-color`;
680
+ }
681
+ }
682
+ // shadow / inset-shadow / drop-shadow
683
+ for (const head of ['inset-shadow', 'drop-shadow', 'shadow']) {
684
+ if ((s = splitHead(utility, [head]))) {
685
+ const v = s[1];
686
+ if (v === '' || SHADOW_SIZES.test(v) || (arbitraryInner(v) !== null && !isArbitraryColor(v))) return head;
687
+ return `${head}-color`;
688
+ }
689
+ }
690
+ // decoration
691
+ if ((s = splitHead(utility, ['decoration']))) {
692
+ const v = s[1];
693
+ if (/^(?:solid|double|dotted|dashed|wavy)$/.test(v)) return 'text-decoration-style';
694
+ if (/^(?:auto|from-font)$/.test(v) || NUMERIC.test(v) || isArbitraryLength(v)) return 'text-decoration-thickness';
695
+ return 'text-decoration-color';
696
+ }
697
+ // stroke width vs colour
698
+ if ((s = splitHead(utility, ['stroke']))) {
699
+ const v = s[1];
700
+ if (NUMERIC.test(v) || isArbitraryLength(v)) return 'stroke-w';
701
+ return 'stroke';
702
+ }
703
+ if (utility.startsWith('placeholder-')) return 'placeholder-color';
704
+ if (utility.startsWith('caret-')) return 'caret-color';
705
+ if (utility.startsWith('scheme-')) return 'color-scheme';
706
+ if (utility.startsWith('list-')) {
707
+ const v = utility.slice(5);
708
+ if (v === 'inside' || v === 'outside') return 'list-style-position';
709
+ if (v.startsWith('image-')) return 'list-image';
710
+ return 'list-style-type';
711
+ }
712
+ if (utility.startsWith('object-')) {
713
+ return /^object-(?:contain|cover|fill|none|scale-down)$/.test(utility) ? 'object-fit' : 'object-position';
714
+ }
715
+ if (utility.startsWith('justify-')) return 'justify-content';
716
+ if (utility.startsWith('items-')) return 'align-items';
717
+ if (utility.startsWith('self-')) return 'align-self';
718
+ if (utility.startsWith('content-')) {
719
+ return /^content-(?:normal|center|start|end|between|around|evenly|baseline|stretch)$/.test(utility) ? 'align-content' : 'content';
720
+ }
721
+ if (utility.startsWith('align-')) return 'vertical-align';
722
+ if (utility.startsWith('wrap-')) return 'wrap';
723
+ if (utility.startsWith('origin-')) return 'transform-origin';
724
+ if (utility.startsWith('transform-')) return 'transform-style';
725
+ if (utility === 'transform') return 'transform';
726
+ if (utility.startsWith('col-')) {
727
+ if (utility.startsWith('col-start-')) return 'col-start';
728
+ if (utility.startsWith('col-end-')) return 'col-end';
729
+ return 'col-start-end';
730
+ }
731
+ if (utility.startsWith('row-')) {
732
+ if (utility.startsWith('row-start-')) return 'row-start';
733
+ if (utility.startsWith('row-end-')) return 'row-end';
734
+ return 'row-start-end';
735
+ }
736
+ if ((s = splitHead(utility, ['flex']))) {
737
+ // `flex` alone is display (handled above); `flex-1` / `flex-auto` / `flex-[..]` is the flex shorthand.
738
+ return 'flex';
739
+ }
740
+ if (utility.startsWith('mask-')) return 'mask-image';
741
+ // `scrollbar-gutter-*` and `scrollbar-w-*` are layout groups resolved by
742
+ // SIMPLE_GROUPS below; only the remaining `scrollbar-*` names are colours.
743
+ if (utility.startsWith('scrollbar-') && !/^scrollbar-(?:gutter|w)(?:-|$)/.test(utility)) {
744
+ return utility.includes('thumb') ? 'scrollbar-thumb-color' : 'scrollbar-track-color';
745
+ }
746
+ if ((s = splitHead(utility, SIMPLE_GROUPS))) {
747
+ if (s[0] === 'placeholder') return 'placeholder-color';
748
+ if (s[0] === 'caret') return 'caret-color';
749
+ return s[0];
750
+ }
751
+ return null;
752
+ }
753
+
754
+ // ---------------------------------------------------------------------------
755
+ // Token parsing
756
+ // ---------------------------------------------------------------------------
757
+
758
+ /**
759
+ * @typedef {{
760
+ * raw: string, variants: string[], utility: string, base: string,
761
+ * group: string|null, category: string|null,
762
+ * arbitraryValue: boolean, negative: boolean, important: boolean,
763
+ * opacity: string|null,
764
+ * }} ParsedClass
765
+ */
766
+
767
+ /** Split `s` on `sep` at bracket and paren depth zero. */
768
+ function splitTopLevel(s, sep) {
769
+ const out = [];
770
+ let depth = 0;
771
+ let cur = '';
772
+ for (let i = 0; i < s.length; i++) {
773
+ const c = s[i];
774
+ if (c === '[' || c === '(') depth++;
775
+ else if (c === ']' || c === ')') depth = Math.max(0, depth - 1);
776
+ if (c === sep && depth === 0) { out.push(cur); cur = ''; continue; }
777
+ cur += c;
778
+ }
779
+ out.push(cur);
780
+ return out;
781
+ }
782
+
783
+ /**
784
+ * Parse one class token. Variants are every top-level `:` segment but the last;
785
+ * the last is the utility. A token is an arbitrary VALUE when, and only when,
786
+ * its utility segment contains a `[`, so `[&_svg]:size-4` (an arbitrary
787
+ * variant) is not one and `[padding:13px]` (an arbitrary property) is.
788
+ *
789
+ * @param {string} token
790
+ * @returns {ParsedClass}
791
+ */
792
+ export function parseToken(token) {
793
+ const segments = splitTopLevel(token, ':');
794
+ let utility = segments.pop() ?? '';
795
+ const variants = segments;
796
+ let negative = false;
797
+ let important = false;
798
+ if (utility.startsWith('!')) { important = true; utility = utility.slice(1); }
799
+ if (utility.endsWith('!')) { important = true; utility = utility.slice(0, -1); }
800
+ if (utility.startsWith('-') && utility.length > 1) { negative = true; utility = utility.slice(1); }
801
+
802
+ let base = utility;
803
+ let opacity = null;
804
+ const parts = splitTopLevel(utility, '/');
805
+ if (parts.length > 1) {
806
+ const candidate = parts.slice(0, -1).join('/');
807
+ const g = groupOf(candidate);
808
+ if (categoryOf(g) === 'color') { base = candidate; opacity = parts[parts.length - 1]; }
809
+ }
810
+ const group = groupOf(base);
811
+ return {
812
+ raw: token,
813
+ variants,
814
+ utility,
815
+ base,
816
+ group,
817
+ category: categoryOf(group),
818
+ arbitraryValue: utility.includes('['),
819
+ negative,
820
+ important,
821
+ opacity,
822
+ };
823
+ }
824
+
825
+ /**
826
+ * Whether `allow` grants this class: it names the class's category (`layout`
827
+ * for the null category) or its exact group id (`rounded`, which covers the
828
+ * plain radius group and not the corner groups).
829
+ *
830
+ * @param {ParsedClass} parsed
831
+ * @param {string[]|undefined} allow
832
+ */
833
+ export function isAllowed(parsed, allow) {
834
+ if (!allow || allow.length === 0) return false;
835
+ const category = parsed.category ?? 'layout';
836
+ if (allow.includes(category)) return true;
837
+ if (parsed.group !== null && allow.includes(parsed.group)) return true;
838
+ return false;
839
+ }