@urbicon-ui/blocks 6.19.3 → 6.21.1

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 (105) hide show
  1. package/dist/components/Calendar/Calendar.svelte +3 -0
  2. package/dist/components/Calendar/calendar.variants.d.ts +132 -419
  3. package/dist/components/CommandPalette/commandPalette.variants.d.ts +24 -51
  4. package/dist/components/CompositionBar/CompositionBar.svelte +6 -1
  5. package/dist/components/CompositionBar/composition-bar.variants.d.ts +23 -83
  6. package/dist/components/CompositionBar/composition-bar.variants.js +9 -3
  7. package/dist/components/EmptyState/emptyState.variants.d.ts +11 -27
  8. package/dist/components/FileUpload/fileUpload.variants.d.ts +24 -93
  9. package/dist/components/Guide/GuidePanel.svelte +38 -0
  10. package/dist/components/Guide/guide.variants.d.ts +69 -124
  11. package/dist/components/Guide/guide.variants.js +6 -1
  12. package/dist/components/Planner/planner.variants.d.ts +23 -94
  13. package/dist/components/Sankey/sankey.variants.d.ts +15 -48
  14. package/dist/components/SidebarLayout/sidebar-layout.variants.d.ts +10 -38
  15. package/dist/components/SidebarLayout/sidebar-layout.variants.js +12 -2
  16. package/dist/components/ThemeSwitcher/themeSwitcher.variants.d.ts +9 -36
  17. package/dist/i18n/index.d.ts +2 -372
  18. package/dist/icons/IconProvider.svelte +3 -0
  19. package/dist/internal/charts/variants.d.ts +18 -31
  20. package/dist/mint/styles.css +1 -1
  21. package/dist/primitives/Accordion/Accordion.svelte +14 -0
  22. package/dist/primitives/Accordion/AccordionItem.svelte +9 -3
  23. package/dist/primitives/Accordion/accordion.variants.d.ts +12 -43
  24. package/dist/primitives/Accordion/accordion.variants.js +6 -2
  25. package/dist/primitives/Accordion/index.d.ts +12 -0
  26. package/dist/primitives/Alert/alert.variants.d.ts +21 -62
  27. package/dist/primitives/Avatar/Avatar.svelte +74 -48
  28. package/dist/primitives/Avatar/avatar.variants.d.ts +34 -128
  29. package/dist/primitives/Avatar/avatar.variants.js +43 -18
  30. package/dist/primitives/Avatar/index.d.ts +6 -1
  31. package/dist/primitives/Badge/badge.variants.d.ts +19 -92
  32. package/dist/primitives/Badge/badge.variants.js +11 -4
  33. package/dist/primitives/Breadcrumb/breadcrumb.variants.d.ts +13 -40
  34. package/dist/primitives/Button/button.variants.d.ts +19 -98
  35. package/dist/primitives/Button/button.variants.js +13 -9
  36. package/dist/primitives/ButtonGroup/buttongroup.variants.d.ts +9 -29
  37. package/dist/primitives/ButtonGroup/buttongroup.variants.js +3 -1
  38. package/dist/primitives/Card/card.variants.d.ts +14 -81
  39. package/dist/primitives/Checkbox/checkbox.variants.d.ts +18 -77
  40. package/dist/primitives/Collapsible/Collapsible.svelte +13 -0
  41. package/dist/primitives/Collapsible/collapsible.variants.d.ts +11 -40
  42. package/dist/primitives/Collapsible/collapsible.variants.js +7 -2
  43. package/dist/primitives/Collapsible/index.d.ts +16 -0
  44. package/dist/primitives/Combobox/combobox.variants.d.ts +24 -78
  45. package/dist/primitives/ConfirmDialog/index.d.ts +12 -0
  46. package/dist/primitives/Dialog/Dialog.svelte +23 -10
  47. package/dist/primitives/Dialog/dialog.variants.d.ts +15 -54
  48. package/dist/primitives/Drawer/Drawer.svelte +24 -11
  49. package/dist/primitives/Drawer/drawer.variants.d.ts +14 -49
  50. package/dist/primitives/Input/input.variants.d.ts +32 -139
  51. package/dist/primitives/Input/input.variants.js +12 -10
  52. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +55 -197
  53. package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +4 -1
  54. package/dist/primitives/Menu/MenuSubmenu.svelte +1 -0
  55. package/dist/primitives/Menu/menu.variants.d.ts +29 -93
  56. package/dist/primitives/Pagination/pagination.variants.d.ts +16 -54
  57. package/dist/primitives/Pagination/pagination.variants.js +16 -14
  58. package/dist/primitives/Popover/Popover.svelte +1 -1
  59. package/dist/primitives/Popover/popover.variants.d.ts +4 -2
  60. package/dist/primitives/Progress/progress.variants.d.ts +19 -83
  61. package/dist/primitives/Progress/progress.variants.js +2 -1
  62. package/dist/primitives/RadioGroup/radioGroup.variants.d.ts +28 -109
  63. package/dist/primitives/SegmentGroup/SegmentGroup.svelte +14 -6
  64. package/dist/primitives/SegmentGroup/segmentgroup.variants.d.ts +12 -53
  65. package/dist/primitives/SegmentGroup/segmentgroup.variants.js +10 -10
  66. package/dist/primitives/Select/Select.svelte +1 -1
  67. package/dist/primitives/Select/select.variants.d.ts +45 -134
  68. package/dist/primitives/Select/select.variants.js +6 -4
  69. package/dist/primitives/Separator/separator.variants.d.ts +4 -2
  70. package/dist/primitives/Sidebar/Sidebar.svelte +10 -15
  71. package/dist/primitives/Sidebar/sidebar.variants.d.ts +11 -29
  72. package/dist/primitives/Skeleton/skeleton.variants.d.ts +9 -36
  73. package/dist/primitives/Slider/slider.variants.d.ts +37 -136
  74. package/dist/primitives/Slider/slider.variants.js +8 -5
  75. package/dist/primitives/Spinner/spinner.variants.d.ts +38 -135
  76. package/dist/primitives/Stepper/stepper.variants.d.ts +32 -130
  77. package/dist/primitives/Tab/Tab.svelte +17 -6
  78. package/dist/primitives/Tab/tab.variants.d.ts +17 -78
  79. package/dist/primitives/Textarea/textarea.variants.d.ts +27 -112
  80. package/dist/primitives/Textarea/textarea.variants.js +12 -10
  81. package/dist/primitives/Toast/Toaster.svelte +16 -3
  82. package/dist/primitives/Toast/index.d.ts +9 -0
  83. package/dist/primitives/Toast/toast.variants.d.ts +22 -78
  84. package/dist/primitives/Toggle/toggle.variants.d.ts +17 -71
  85. package/dist/primitives/Toolbar/toolbar.variants.d.ts +9 -60
  86. package/dist/primitives/Tooltip/Tooltip.svelte +11 -0
  87. package/dist/primitives/Tooltip/index.d.ts +14 -0
  88. package/dist/primitives/Tooltip/tooltip.variants.d.ts +11 -50
  89. package/dist/primitives/Tooltip/tooltip.variants.js +7 -1
  90. package/dist/style/foundation.css +4 -1
  91. package/dist/style/interaction.css +40 -12
  92. package/dist/style/semantic.css +2 -2
  93. package/dist/style/themes/index.css +1 -1
  94. package/dist/system/attachments/ContextIsolation.svelte +2 -0
  95. package/dist/utils/index.d.ts +2 -1
  96. package/dist/utils/index.js +5 -0
  97. package/dist/utils/overlay.d.ts +33 -4
  98. package/dist/utils/overlay.js +61 -18
  99. package/dist/utils/roving.d.ts +15 -0
  100. package/dist/utils/roving.js +44 -0
  101. package/dist/utils/use-floating-panel.svelte.d.ts +4 -1
  102. package/dist/utils/use-floating-panel.svelte.js +23 -1
  103. package/dist/utils/variants.d.ts +60 -16
  104. package/dist/utils/variants.js +424 -123
  105. package/package.json +9 -6
@@ -2,8 +2,9 @@
2
2
  // ─── cx: class concatenation ─────────────────────────────────────────────────
3
3
  /**
4
4
  * Concatenate class inputs into a single string. Accepts strings, nested
5
- * arrays, and falsy values (filtered out). Trims and joins with single
6
- * spaces.
5
+ * arrays, `{ class: condition }` records (Svelte 5 `ClassValue` / clsx
6
+ * shape — keys with truthy values are included) and falsy values (filtered
7
+ * out). Trims and joins with single spaces.
7
8
  *
8
9
  * Note: `cx()` does **not** deduplicate — `cx('foo', 'foo bar')` returns
9
10
  * `'foo foo bar'`. Duplicate Tailwind classes are harmless at runtime
@@ -20,11 +21,24 @@ export function cx(...inputs) {
20
21
  if (trimmed)
21
22
  parts.push(trimmed);
22
23
  }
24
+ else if (typeof input === 'number') {
25
+ // clsx/ClassValue parity: numbers stringify (0 is already filtered).
26
+ parts.push(String(input));
27
+ }
23
28
  else if (Array.isArray(input)) {
24
29
  const nested = cx(...input);
25
30
  if (nested)
26
31
  parts.push(nested);
27
32
  }
33
+ else if (typeof input === 'object') {
34
+ for (const key of Object.keys(input)) {
35
+ if (!input[key])
36
+ continue;
37
+ const trimmed = key.trim();
38
+ if (trimmed)
39
+ parts.push(trimmed);
40
+ }
41
+ }
28
42
  }
29
43
  return parts.join(' ');
30
44
  }
@@ -53,27 +67,34 @@ const BUCKET_PATTERNS = [
53
67
  // Flex / Grid
54
68
  [/^flex-(row|col|row-reverse|col-reverse)$/, 'flex-direction'],
55
69
  [/^flex-(wrap|nowrap|wrap-reverse)$/, 'flex-wrap'],
56
- [/^flex-(1|auto|initial|none)$/, 'flex'],
57
- [/^grow(-0|-\[.+\])?$/, 'flex-grow'],
58
- [/^shrink(-0|-\[.+\])?$/, 'flex-shrink'],
70
+ // v4 accepts any integer (`flex-2`) and arbitrary values (`flex-[2_2_0%]`).
71
+ [/^flex-(\d+|auto|initial|none|\[[^\]]+\])$/, 'flex'],
72
+ [/^grow(-\d+|-\[.+\])?$/, 'flex-grow'],
73
+ [/^shrink(-\d+|-\[.+\])?$/, 'flex-shrink'],
59
74
  [/^basis-/, 'flex-basis'],
60
75
  [/^order-/, 'order'],
61
- [/^items-(start|end|center|baseline|stretch)$/, 'align-items'],
76
+ [/^items-(start|end|center|baseline|baseline-last|stretch)$/, 'align-items'],
62
77
  [/^justify-(start|end|center|between|around|evenly|normal|stretch)$/, 'justify-content'],
63
- [/^justify-items-(start|end|center|stretch)$/, 'justify-items'],
78
+ [/^justify-items-(start|end|center|stretch|normal)$/, 'justify-items'],
64
79
  [/^justify-self-(auto|start|end|center|stretch)$/, 'justify-self'],
65
80
  [/^content-(start|end|center|between|around|evenly|baseline|stretch|normal)$/, 'align-content'],
66
- [/^self-(auto|start|end|center|stretch|baseline)$/, 'align-self'],
81
+ [/^self-(auto|start|end|center|stretch|baseline|baseline-last)$/, 'align-self'],
67
82
  [/^place-content-/, 'place-content'],
68
83
  [/^place-items-/, 'place-items'],
69
84
  [/^place-self-/, 'place-self'],
70
85
  [/^grid-cols-/, 'grid-template-columns'],
71
86
  [/^grid-rows-/, 'grid-template-rows'],
87
+ [/^grid-flow-/, 'grid-auto-flow'],
88
+ // grid-column vs -start vs -end are three CSS properties — `col-span-2`
89
+ // must not conflict with `col-start-3`.
90
+ [/^col-start-/, 'grid-column-start'],
91
+ [/^col-end-/, 'grid-column-end'],
72
92
  [/^col-/, 'grid-column'],
93
+ [/^row-start-/, 'grid-row-start'],
94
+ [/^row-end-/, 'grid-row-end'],
73
95
  [/^row-/, 'grid-row'],
74
96
  [/^auto-cols-/, 'grid-auto-columns'],
75
97
  [/^auto-rows-/, 'grid-auto-rows'],
76
- [/^auto-flow-/, 'grid-auto-flow'],
77
98
  // Sizing — width / height
78
99
  [/^min-w-/, 'min-w'],
79
100
  [/^min-h-/, 'min-h'],
@@ -103,13 +124,27 @@ const BUCKET_PATTERNS = [
103
124
  [/^ms-/, 'ms'],
104
125
  [/^me-/, 'me'],
105
126
  [/^m-/, 'm'],
106
- // Gap / space-between
127
+ // Gap / space-between / divide
107
128
  [/^gap-x-/, 'gap-x'],
108
129
  [/^gap-y-/, 'gap-y'],
109
130
  [/^gap-/, 'gap'],
131
+ [/^space-x-reverse$/, 'space-x-reverse'],
132
+ [/^space-y-reverse$/, 'space-y-reverse'],
110
133
  [/^space-x-/, 'space-x'],
111
134
  [/^space-y-/, 'space-y'],
112
- // Position offsets
135
+ // divide-* — width / style / color are orthogonal properties.
136
+ [/^divide-(x|y)-reverse$/, (m) => `divide-${m[1]}-reverse`],
137
+ [/^divide-(x|y)(-(\d+|\[[^\]]+\]))?$/, (m) => `divide-${m[1]}-width`],
138
+ [/^divide-(solid|dashed|dotted|double|none)$/, 'divide-style'],
139
+ [/^divide-/, 'divide-color'],
140
+ // Position offsets. v4.1 inset-shadow-* / inset-ring-* must not fall into
141
+ // the `inset-` position bucket.
142
+ [/^inset-shadow-\[/, 'inset-shadow'],
143
+ [/^inset-shadow-(2xs|xs|sm|none)$/, 'inset-shadow'],
144
+ [/^inset-shadow$/, 'inset-shadow'],
145
+ [/^inset-shadow-/, 'inset-shadow-color'],
146
+ [/^inset-ring(-\d+|-\[[^\]]+\])?$/, 'inset-ring-width'],
147
+ [/^inset-ring-/, 'inset-ring-color'],
113
148
  [/^inset-x-/, 'inset-x'],
114
149
  [/^inset-y-/, 'inset-y'],
115
150
  [/^inset-/, 'inset'],
@@ -136,6 +171,11 @@ const BUCKET_PATTERNS = [
136
171
  [/^rounded-s(-|$)/, 'rounded-s'],
137
172
  [/^rounded-e(-|$)/, 'rounded-e'],
138
173
  [/^rounded(-|$)/, 'rounded'],
174
+ // border-collapse / border-spacing are table-layout properties, not colors.
175
+ [/^border-(collapse|separate)$/, 'border-collapse'],
176
+ [/^border-spacing-x-/, 'border-spacing-x'],
177
+ [/^border-spacing-y-/, 'border-spacing-y'],
178
+ [/^border-spacing-/, 'border-spacing'],
139
179
  // Border style (must come before width/color)
140
180
  [/^border-(solid|dashed|dotted|double|hidden|none)$/, 'border-style'],
141
181
  // Border width — `border`, `border-1`, `border-2`, `border-[3px]` (Tailwind v4 accepts any integer)
@@ -145,10 +185,12 @@ const BUCKET_PATTERNS = [
145
185
  // Border color (catch-all, e.g. `border-red-500`, `border-primary`, `border-border-subtle`)
146
186
  [/^border-(x|y|t|r|b|l|s|e)-/, (m) => `border-${m[1]}-color`],
147
187
  [/^border-/, 'border-color'],
148
- // Outline / ring
149
- [/^outline-(none|dashed|dotted|double|hidden)$/, 'outline-style'],
188
+ // Outline / ring. In v4 bare `outline` sets outline-style: solid — it is a
189
+ // style, not a width.
190
+ [/^outline-(solid|dashed|dotted|double|none|hidden)$/, 'outline-style'],
191
+ [/^outline$/, 'outline-style'],
150
192
  [/^outline-offset-/, 'outline-offset'],
151
- [/^outline(-\d+|-\[[^\]]+\])?$/, 'outline-width'],
193
+ [/^outline(-\d+|-\[[^\]]+\])$/, 'outline-width'],
152
194
  [/^outline-/, 'outline-color'],
153
195
  [/^ring-offset-(\d+|\[[^\]]+\])$/, 'ring-offset-width'],
154
196
  [/^ring-offset-/, 'ring-offset-color'],
@@ -162,30 +204,58 @@ const BUCKET_PATTERNS = [
162
204
  [/^bg-clip-/, 'bg-clip'],
163
205
  [/^bg-origin-/, 'bg-origin'],
164
206
  [
165
- /^bg-(top|right|bottom|left|center|left-top|left-bottom|right-top|right-bottom)$/,
207
+ /^bg-(top|right|bottom|left|center|left-top|left-bottom|right-top|right-bottom|top-left|top-right|bottom-left|bottom-right)$/,
166
208
  'bg-position'
167
209
  ],
210
+ // Data-type hints and image-valued arbitraries must not read as bg-color —
211
+ // background-image and background-color are independent CSS properties
212
+ // (a gradient overlay must never strip the color underneath, see the
213
+ // Skeleton wave shimmer and Progress striped fill).
214
+ [/^bg-\[(?:length|size):/, 'bg-size'],
215
+ [/^bg-\[position:/, 'bg-position'],
216
+ [/^bg-\[(?:image:|url\(|linear-gradient\(|radial-gradient\(|conic-gradient\()/, 'bg-image'],
217
+ [/^bg-\[color:/, 'bg-color'],
218
+ [/^bg-position-/, 'bg-position'],
219
+ [/^bg-size-/, 'bg-size'],
220
+ [/^bg-(?:linear|radial|conic)(-|$)/, 'bg-image'],
168
221
  [/^bg-gradient-to-/, 'bg-image'],
222
+ [/^bg-none$/, 'bg-image'],
169
223
  [/^bg-/, 'bg-color'],
170
224
  [/^from-/, 'gradient-from'],
171
225
  [/^via-/, 'gradient-via'],
172
226
  [/^to-/, 'gradient-to'],
173
227
  // Text — size / align / weight / color (specific first). v4 size+leading shorthand: `text-sm/6`, `text-base/relaxed`.
228
+ // v4.1 text-shadow-* must not read as text-color — and like `shadow`,
229
+ // the named size scale is finite; other single-word values are colors
230
+ // (`text-shadow-white` must not strip `text-shadow-lg`).
231
+ [/^text-shadow-\[/, 'text-shadow'],
232
+ [/^text-shadow-(2xs|xs|sm|md|lg|none)$/, 'text-shadow'],
233
+ [/^text-shadow$/, 'text-shadow'],
234
+ [/^text-shadow-/, 'text-shadow-color'],
174
235
  [/^text-(xs|sm|base|lg|xl|\d+xl)(\/[\w.-]+)?$/, 'text-size'],
236
+ // Data-type hints disambiguate the overloaded `text-` arbitraries.
237
+ [/^text-\[length:/, 'text-size'],
238
+ [/^text-\[color:/, 'text-color'],
175
239
  // Arbitrary text-size values starting with a digit (e.g. `text-[11px]`,
176
240
  // `text-[1.5rem]`). Hex/named values like `text-[#fff]` / `text-[var(--x)]`
177
241
  // still fall through to the text-color catch-all below.
178
242
  [/^text-\[[0-9.]/, 'text-size'],
179
243
  [/^text-(left|center|right|justify|start|end)$/, 'text-align'],
180
- [/^text-(ellipsis|clip|wrap|nowrap|balance|pretty)$/, 'text-overflow'],
244
+ // text-overflow (ellipsis/clip) and text-wrap (wrap/nowrap/balance/pretty)
245
+ // are different CSS properties — a wrap override must not strip ellipsis'
246
+ // sibling and vice versa.
247
+ [/^text-(ellipsis|clip)$/, 'text-overflow'],
248
+ [/^text-(wrap|nowrap|balance|pretty)$/, 'text-wrap'],
181
249
  [/^text-(current|inherit|transparent)$/, 'text-color'],
182
250
  [/^text-/, 'text-color'],
183
251
  [/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/, 'font-weight'],
252
+ [/^font-stretch-/, 'font-stretch'],
184
253
  [/^font-(sans|serif|mono)$/, 'font-family'],
185
254
  [/^font-/, 'font-family'],
186
255
  [/^italic$|^not-italic$/, 'font-style'],
187
256
  [/^tracking-/, 'letter-spacing'],
188
257
  [/^leading-/, 'line-height'],
258
+ [/^line-clamp-/, 'line-clamp'],
189
259
  [/^underline$|^overline$|^line-through$|^no-underline$/, 'text-decoration'],
190
260
  // text-decoration sub-properties are orthogonal CSS — three separate buckets.
191
261
  // Order matters: style names first, then thickness (numeric/arbitrary), then color catch-all.
@@ -201,8 +271,10 @@ const BUCKET_PATTERNS = [
201
271
  [/^align-(baseline|top|middle|bottom|text-top|text-bottom|sub|super)$/, 'vertical-align'],
202
272
  // Effects
203
273
  [/^opacity-/, 'opacity'],
204
- [/^shadow-(inner|none)$/, 'shadow'],
205
- [/^shadow(-\w+)?$/, 'shadow'],
274
+ // Named shadow scale is finite; any other `shadow-<word>` is a color
275
+ // (`shadow-primary` must not strip `shadow-md`).
276
+ [/^shadow-(2xs|xs|sm|md|lg|xl|2xl|inner|none)$/, 'shadow'],
277
+ [/^shadow$/, 'shadow'],
206
278
  [/^shadow-\[/, 'shadow'],
207
279
  [/^shadow-/, 'shadow-color'],
208
280
  [/^blur(-\w+)?$/, 'blur'],
@@ -220,13 +292,20 @@ const BUCKET_PATTERNS = [
220
292
  [/^scale-x-/, 'scale-x'],
221
293
  [/^scale-y-/, 'scale-y'],
222
294
  [/^scale-/, 'scale'],
295
+ // v4 3D rotate axes are orthogonal to plain rotate.
296
+ [/^rotate-(x|y|z)-/, (m) => `rotate-${m[1]}`],
223
297
  [/^rotate-/, 'rotate'],
224
298
  [/^translate-x-/, 'translate-x'],
225
299
  [/^translate-y-/, 'translate-y'],
300
+ [/^translate-z-/, 'translate-z'],
301
+ // v4 plain `translate-*` sets both axes.
302
+ [/^translate-/, 'translate'],
226
303
  [/^skew-x-/, 'skew-x'],
227
304
  [/^skew-y-/, 'skew-y'],
228
305
  [/^origin-/, 'transform-origin'],
229
- // Transitions / animations
306
+ // Transitions / animations. v4 transition-behavior must not conflict with
307
+ // transition-property (`transition-discrete` composes with `transition-colors`).
308
+ [/^transition-(discrete|normal)$/, 'transition-behavior'],
230
309
  [/^transition(-\w+)?$/, 'transition'],
231
310
  [/^transition-\[/, 'transition'],
232
311
  [/^duration-/, 'transition-duration'],
@@ -239,6 +318,37 @@ const BUCKET_PATTERNS = [
239
318
  [/^accent-/, 'accent-color'],
240
319
  [/^caret-/, 'caret-color']
241
320
  ];
321
+ // Arbitrary CSS properties whose utility twin should share a bucket, so
322
+ // `[gap:inherit]` and `gap-4` resolve against each other (Codeberg #21).
323
+ // Everything else buckets as `@<property>` — same-property arbitraries
324
+ // conflict, utilities stay untouched (twMerge behaves the same way).
325
+ const ARBITRARY_PROP_ALIAS = {
326
+ gap: 'gap',
327
+ 'column-gap': 'gap-x',
328
+ 'row-gap': 'gap-y',
329
+ transform: 'transform'
330
+ };
331
+ const ARBITRARY_PROP_PATTERN = /^\[((?:--)?[a-zA-Z][a-zA-Z0-9-]*):.+\]$/;
332
+ /**
333
+ * Index of the last `:` outside any `[]`/`()` nesting, or -1. This is the
334
+ * modifier/base boundary: colons inside arbitrary values (`bg-[url(https://…)]`),
335
+ * arbitrary properties (`[gap:inherit]`) and selector modifiers
336
+ * (`supports-[display:grid]:grid`) must not split the class.
337
+ */
338
+ function lastTopLevelColon(cls) {
339
+ let depth = 0;
340
+ let last = -1;
341
+ for (let i = 0; i < cls.length; i++) {
342
+ const ch = cls[i];
343
+ if (ch === '[' || ch === '(')
344
+ depth++;
345
+ else if (ch === ']' || ch === ')')
346
+ depth = depth > 0 ? depth - 1 : 0;
347
+ else if (ch === ':' && depth === 0)
348
+ last = i;
349
+ }
350
+ return last;
351
+ }
242
352
  /**
243
353
  * Returns the conflict-bucket key of a Tailwind class, or `null` if the class
244
354
  * does not match any catalogued utility family. Unknown classes never conflict.
@@ -246,8 +356,10 @@ const BUCKET_PATTERNS = [
246
356
  * Modifier prefixes are part of the key — `hover:bg-red` returns `hover:bg-color`
247
357
  * while `bg-red` returns `bg-color`. The two never strip each other. `!`-important
248
358
  * is normalized away entirely so `!bg-red` and `bg-red` share a bucket.
359
+ * Arbitrary properties bucket per property name (`[gap:inherit]` → `gap`,
360
+ * `[--spinner-speed:1s]` → `@--spinner-speed`).
249
361
  *
250
- * Results are memoized per process — `tailwindBucket` is called O(stages × classes)
362
+ * Results are memoized per process — `tailwindBucket` is called O(sources × classes)
251
363
  * per `tv()` resolve, and the same handful of class strings appear thousands of
252
364
  * times across a render tree. Cap kept high enough for any realistic application
253
365
  * surface; values are short and the map never frees.
@@ -255,11 +367,13 @@ const BUCKET_PATTERNS = [
255
367
  const BUCKET_CACHE = new Map();
256
368
  const BUCKET_CACHE_MAX = 4096;
257
369
  function tailwindBucket(cls) {
370
+ // Stored values are `string | null`, never `undefined` — a single lookup
371
+ // distinguishes hit from miss.
258
372
  const cached = BUCKET_CACHE.get(cls);
259
- if (cached !== undefined || BUCKET_CACHE.has(cls))
373
+ if (cached !== undefined)
260
374
  return cached;
261
375
  // Split off all modifier prefixes (`hover:focus-visible:dark:md:bg-red`).
262
- const lastColon = cls.lastIndexOf(':');
376
+ const lastColon = lastTopLevelColon(cls);
263
377
  let modifiers = lastColon >= 0 ? cls.slice(0, lastColon + 1) : '';
264
378
  let base = lastColon >= 0 ? cls.slice(lastColon + 1) : cls;
265
379
  // Strip `!`-important markers — Tailwind v3 leading (`!bg-red`, `hover:!bg-red`)
@@ -273,23 +387,103 @@ function tailwindBucket(cls) {
273
387
  if (base.endsWith('!'))
274
388
  base = base.slice(0, -1);
275
389
  // Negative spacing like `-mx-2` shares its bucket with `mx-2`.
276
- if (base.startsWith('-'))
390
+ if (base.startsWith('-') && !base.startsWith('-['))
277
391
  base = base.slice(1);
278
392
  let result = null;
279
- for (const [pattern, key] of BUCKET_PATTERNS) {
280
- const match = base.match(pattern);
281
- if (match) {
282
- const bucketKey = typeof key === 'string' ? key : key(match);
283
- result = modifiers + bucketKey;
284
- break;
393
+ const arbitraryProp = base.match(ARBITRARY_PROP_PATTERN);
394
+ if (arbitraryProp) {
395
+ const prop = arbitraryProp[1];
396
+ result = modifiers + (ARBITRARY_PROP_ALIAS[prop] ?? `@${prop}`);
397
+ }
398
+ else {
399
+ for (const [pattern, key] of BUCKET_PATTERNS) {
400
+ const match = base.match(pattern);
401
+ if (match) {
402
+ const bucketKey = typeof key === 'string' ? key : key(match);
403
+ result = modifiers + bucketKey;
404
+ break;
405
+ }
285
406
  }
286
407
  }
287
408
  if (BUCKET_CACHE.size < BUCKET_CACHE_MAX)
288
409
  BUCKET_CACHE.set(cls, result);
289
410
  return result;
290
411
  }
412
+ // Directional shorthand dominance: a LATER class in bucket K also strips
413
+ // earlier classes in DOMINANCE[K]. Longhands never strip their shorthand —
414
+ // a later `pl-2` composes with an earlier `p-4` (Tailwind's own cascade
415
+ // resolves the left side), but a later `p-0` fully replaces earlier `px-4`
416
+ // or `pl-10`, matching what the override author means.
417
+ //
418
+ // Deliberately absent: `text-size` → `line-height`. The library pairs a
419
+ // slot-base `leading-*` with axis-supplied `text-*` across sources by
420
+ // design (labels, timeline meta, table cells) — stripping the leading would
421
+ // silently change typography; leading-* also wins Tailwind's own cascade.
422
+ const DOMINANCE = {
423
+ p: ['px', 'py', 'ps', 'pe', 'pt', 'pr', 'pb', 'pl'],
424
+ px: ['pr', 'pl'],
425
+ py: ['pt', 'pb'],
426
+ m: ['mx', 'my', 'ms', 'me', 'mt', 'mr', 'mb', 'ml'],
427
+ mx: ['mr', 'ml'],
428
+ my: ['mt', 'mb'],
429
+ inset: ['inset-x', 'inset-y', 'start', 'end', 'top', 'right', 'bottom', 'left'],
430
+ 'inset-x': ['right', 'left'],
431
+ 'inset-y': ['top', 'bottom'],
432
+ size: ['w', 'h'],
433
+ gap: ['gap-x', 'gap-y'],
434
+ rounded: [
435
+ 'rounded-tl',
436
+ 'rounded-tr',
437
+ 'rounded-bl',
438
+ 'rounded-br',
439
+ 'rounded-ss',
440
+ 'rounded-se',
441
+ 'rounded-es',
442
+ 'rounded-ee',
443
+ 'rounded-t',
444
+ 'rounded-r',
445
+ 'rounded-b',
446
+ 'rounded-l',
447
+ 'rounded-s',
448
+ 'rounded-e'
449
+ ],
450
+ 'rounded-t': ['rounded-tl', 'rounded-tr'],
451
+ 'rounded-r': ['rounded-tr', 'rounded-br'],
452
+ 'rounded-b': ['rounded-bl', 'rounded-br'],
453
+ 'rounded-l': ['rounded-tl', 'rounded-bl'],
454
+ 'rounded-s': ['rounded-ss', 'rounded-es'],
455
+ 'rounded-e': ['rounded-se', 'rounded-ee'],
456
+ 'border-width': [
457
+ 'border-x-width',
458
+ 'border-y-width',
459
+ 'border-s-width',
460
+ 'border-e-width',
461
+ 'border-t-width',
462
+ 'border-r-width',
463
+ 'border-b-width',
464
+ 'border-l-width'
465
+ ],
466
+ 'border-x-width': ['border-r-width', 'border-l-width'],
467
+ 'border-y-width': ['border-t-width', 'border-b-width'],
468
+ 'border-color': [
469
+ 'border-x-color',
470
+ 'border-y-color',
471
+ 'border-s-color',
472
+ 'border-e-color',
473
+ 'border-t-color',
474
+ 'border-r-color',
475
+ 'border-b-color',
476
+ 'border-l-color'
477
+ ],
478
+ 'border-x-color': ['border-r-color', 'border-l-color'],
479
+ 'border-y-color': ['border-t-color', 'border-b-color'],
480
+ overflow: ['overflow-x', 'overflow-y'],
481
+ scale: ['scale-x', 'scale-y'],
482
+ translate: ['translate-x', 'translate-y', 'translate-z']
483
+ };
291
484
  /**
292
- * Removes classes from `prior` whose conflict-bucket appears in `current`.
485
+ * Removes classes from `prior` whose conflict-bucket appears in `current`,
486
+ * including buckets a later shorthand dominates (`p-0` strips `px-4`).
293
487
  * Stable order preserved within each side.
294
488
  */
295
489
  function stripConflicts(prior, current) {
@@ -298,8 +492,19 @@ function stripConflicts(prior, current) {
298
492
  const used = new Set();
299
493
  for (const cls of current) {
300
494
  const b = tailwindBucket(cls);
301
- if (b)
302
- used.add(b);
495
+ if (!b)
496
+ continue;
497
+ used.add(b);
498
+ // Dominance expansion keeps the modifier prefix: `hover:p` dominates
499
+ // `hover:px`, never the plain-state `px`. Bucket keys contain no `:`,
500
+ // so the last colon is the modifier boundary.
501
+ const keyStart = b.lastIndexOf(':') + 1;
502
+ const dominated = DOMINANCE[b.slice(keyStart)];
503
+ if (dominated) {
504
+ const mods = b.slice(0, keyStart);
505
+ for (const d of dominated)
506
+ used.add(mods + d);
507
+ }
303
508
  }
304
509
  if (used.size === 0)
305
510
  return prior;
@@ -308,6 +513,25 @@ function stripConflicts(prior, current) {
308
513
  return b == null || !used.has(b);
309
514
  });
310
515
  }
516
+ /**
517
+ * Folds an ordered list of class-token sources into one list: each source
518
+ * strips the accumulated earlier sources' conflicting buckets, then appends
519
+ * its own tokens. The last source therefore wins per bucket — deterministic,
520
+ * independent of stylesheet order.
521
+ *
522
+ * Within a single source, order is preserved and same-bucket pairs fall
523
+ * through to the CSS cascade — author-controlled pairings inside one class
524
+ * string (`rounded-md rounded-t-none`) stay intact.
525
+ */
526
+ function foldSources(sources) {
527
+ let acc = [];
528
+ for (const tokens of sources) {
529
+ if (tokens.length === 0)
530
+ continue;
531
+ acc = acc.length === 0 ? tokens : [...stripConflicts(acc, tokens), ...tokens];
532
+ }
533
+ return acc;
534
+ }
311
535
  /**
312
536
  * Tokenize a class-string into individual class tokens. Whitespace-only
313
537
  * fragments and empty strings are filtered.
@@ -364,7 +588,7 @@ function stripUndefined(obj) {
364
588
  }
365
589
  export function matchesCompound(compound, effectiveProps) {
366
590
  for (const key of Object.keys(compound)) {
367
- if (key === 'class' || key === 'className')
591
+ if (key === 'class')
368
592
  continue;
369
593
  const constraint = compound[key];
370
594
  const propValue = falsyToString(effectiveProps[key]);
@@ -388,111 +612,188 @@ function resolveClassValue(value) {
388
612
  return cx(...value);
389
613
  return '';
390
614
  }
615
+ /**
616
+ * Config-time validation — every violation here is a programming error in a
617
+ * `*.variants.ts` (or a consumer's own tv() call), so it throws immediately
618
+ * at module init instead of degrading silently at render time. The compile-
619
+ * time layer (ValidSlotVariants, VariantPropsMap) catches the same class of
620
+ * mistakes for literal configs; this covers JS consumers and values built
621
+ * from imported constants.
622
+ */
623
+ function validateTvConfig(config) {
624
+ const { base, slots, variants = {}, compoundVariants = [], defaultVariants = {} } = config;
625
+ const slotNames = slots != null && Object.keys(slots).length > 0 ? Object.keys(slots) : null;
626
+ if (slotNames && base != null) {
627
+ // Fail loud instead of the historical silent drop (base only ever
628
+ // reached a slot literally named 'base').
629
+ throw new Error('tv(): `base` and `slots` are mutually exclusive — declare the primary slot as `slots.base` instead.');
630
+ }
631
+ const requireBaseSlot = (context) => {
632
+ if (slotNames && !slotNames.includes('base')) {
633
+ throw new Error(`tv(): ${context} is a plain class value, which routes to the 'base' slot — but no slot named 'base' is declared.`);
634
+ }
635
+ };
636
+ // Leaf values must bottom out in class strings (nested arrays allowed,
637
+ // falsy entries tolerated for conditional-array patterns). Objects inside
638
+ // arrays and scalar garbage would silently render as '' — or worse, leak
639
+ // through cx()'s record form as literal slot-name classes — so they throw.
640
+ const checkClassLeaf = (value, context) => {
641
+ if (value == null || value === false)
642
+ return;
643
+ if (typeof value === 'string')
644
+ return;
645
+ if (Array.isArray(value)) {
646
+ for (const [i, v] of value.entries()) {
647
+ checkClassLeaf(v, `${context}[${i}]`);
648
+ }
649
+ return;
650
+ }
651
+ throw new Error(`tv(): ${context} must be a class string (or nested array of strings), got ${typeof value}.`);
652
+ };
653
+ const checkSlotKeys = (value, context) => {
654
+ if (value == null)
655
+ return;
656
+ if (typeof value === 'string' || Array.isArray(value)) {
657
+ if (slotNames)
658
+ requireBaseSlot(context);
659
+ checkClassLeaf(value, context);
660
+ return;
661
+ }
662
+ if (typeof value === 'object') {
663
+ if (!slotNames) {
664
+ throw new Error(`tv(): ${context} is a slot map, but this tv() declares no slots — use a plain class string/array.`);
665
+ }
666
+ for (const key of Object.keys(value)) {
667
+ if (!slotNames.includes(key)) {
668
+ throw new Error(`tv(): ${context} targets unknown slot '${key}' (declared slots: ${slotNames.join(', ')}).`);
669
+ }
670
+ checkClassLeaf(value[key], `${context}.${key}`);
671
+ }
672
+ return;
673
+ }
674
+ throw new Error(`tv(): ${context} must be a class string, array or slot map, got ${typeof value}.`);
675
+ };
676
+ for (const [axis, values] of Object.entries(variants)) {
677
+ for (const [valueName, value] of Object.entries(values)) {
678
+ checkSlotKeys(value, `variants.${axis}.${valueName}`);
679
+ }
680
+ }
681
+ for (const [i, cv] of compoundVariants.entries()) {
682
+ for (const key of Object.keys(cv)) {
683
+ if (key === 'class')
684
+ continue;
685
+ const axis = variants[key];
686
+ if (axis == null) {
687
+ throw new Error(`tv(): compoundVariants[${i}] references unknown variant axis '${key}'.`);
688
+ }
689
+ const constraint = cv[key];
690
+ const values = Array.isArray(constraint) ? constraint : [constraint];
691
+ const booleanAxis = 'true' in axis || 'false' in axis;
692
+ for (const v of values) {
693
+ const normalized = falsyToString(v);
694
+ // 'true'/'false' may be matched even when only one of them is
695
+ // declared — half-declared boolean axes are idiomatic
696
+ // (`loading: { true: … }` + a compound on `loading: false`). The
697
+ // escape only applies to boolean-ish axes; `size: true` on a string
698
+ // axis is a config error.
699
+ if ((normalized === 'true' || normalized === 'false') && booleanAxis)
700
+ continue;
701
+ if (normalized == null || !(normalized in axis)) {
702
+ throw new Error(`tv(): compoundVariants[${i}] matches '${key}: ${String(v)}', but axis '${key}' declares no such value (values: ${Object.keys(axis).join(', ')}).`);
703
+ }
704
+ }
705
+ }
706
+ checkSlotKeys(cv.class, `compoundVariants[${i}].class`);
707
+ }
708
+ for (const [key, value] of Object.entries(defaultVariants)) {
709
+ const axis = variants[key];
710
+ if (axis == null) {
711
+ throw new Error(`tv(): defaultVariants references unknown variant axis '${key}'.`);
712
+ }
713
+ const normalized = falsyToString(value);
714
+ const booleanAxis = 'true' in axis || 'false' in axis;
715
+ if ((normalized === 'true' || normalized === 'false') && booleanAxis)
716
+ continue;
717
+ if (normalized == null || !(normalized in axis)) {
718
+ throw new Error(`tv(): defaultVariants.${key} = '${String(value)}' is not a declared value of axis '${key}' (values: ${Object.keys(axis).join(', ')}).`);
719
+ }
720
+ }
721
+ }
391
722
  // Implementation
392
723
  export function tv(config) {
724
+ validateTvConfig(config);
393
725
  const { base, slots, variants = {}, compoundVariants = [], defaultVariants = {} } = config;
394
726
  const variantEntries = Object.entries(variants);
395
727
  const hasSlots = slots != null && Object.keys(slots).length > 0;
728
+ /**
729
+ * Shared pipeline: first source → each variant axis in declaration order →
730
+ * each matching compoundVariant in array order → call-site class override.
731
+ * `foldSources` lets every later source strip earlier sources' conflicting
732
+ * buckets, so axis and compound order are semantic: declare the axis that
733
+ * must win a shared bucket later. See "tv() engine — explicit trade-offs"
734
+ * in ARCHITECTURE.md. `pickValue` routes config values: identity in no-slot
735
+ * mode; per-slot lookup (slot maps by name, plain strings to the 'base'
736
+ * slot) in slot mode.
737
+ */
738
+ function foldFor(firstSource, effective, pickValue, overrideTokens) {
739
+ const sources = [firstSource];
740
+ for (const [vName, vMap] of variantEntries) {
741
+ const key = falsyToString(effective[vName]);
742
+ if (key == null || !(key in vMap))
743
+ continue;
744
+ sources.push(tokenize(resolveClassValue(pickValue(vMap[key]))));
745
+ }
746
+ for (const cv of compoundVariants) {
747
+ if (!matchesCompound(cv, effective))
748
+ continue;
749
+ sources.push(tokenize(resolveClassValue(pickValue(cv.class))));
750
+ }
751
+ sources.push(overrideTokens);
752
+ return foldSources(sources).join(' ');
753
+ }
396
754
  if (!hasSlots) {
397
- return function resolve(props) {
755
+ const identity = (value) => value;
756
+ const baseTokens = tokenize(resolveClassValue(base));
757
+ const resolve = function resolve(props) {
398
758
  const effective = { ...defaultVariants, ...stripUndefined(props || {}) };
399
- // Pipeline stages later stages strip earlier stages' conflicting
400
- // buckets. See "tv() engine explicit trade-offs" in ARCHITECTURE.md.
401
- const baseTokens = tokenize(resolveClassValue(base));
402
- const variantTokens = [];
403
- for (const [vName, vMap] of variantEntries) {
404
- const key = falsyToString(effective[vName]);
405
- if (key != null && key in vMap) {
406
- variantTokens.push(...tokenize(resolveClassValue(vMap[key])));
407
- }
408
- }
409
- const compoundTokens = [];
410
- for (const cv of compoundVariants) {
411
- if (matchesCompound(cv, effective)) {
412
- compoundTokens.push(...tokenize(resolveClassValue(cv.class ?? cv.className)));
413
- }
414
- }
415
- const overrideTokens = [];
416
- if (props?.class)
417
- overrideTokens.push(...tokenize(resolveClassValue(props.class)));
418
- if (props?.className)
419
- overrideTokens.push(...tokenize(resolveClassValue(props.className)));
420
- // Strip in pipeline order: variants strip base, compounds strip
421
- // (base+variants), overrides strip all prior. Each stage's own classes
422
- // remain untouched.
423
- const afterVariants = [...stripConflicts(baseTokens, variantTokens), ...variantTokens];
424
- const afterCompounds = [...stripConflicts(afterVariants, compoundTokens), ...compoundTokens];
425
- const final = [...stripConflicts(afterCompounds, overrideTokens), ...overrideTokens];
426
- return final.join(' ');
759
+ const overrideTokens = props?.class ? tokenize(cx(props.class)) : [];
760
+ return foldFor(baseTokens, effective, identity, overrideTokens);
427
761
  };
762
+ // Introspection hook for tooling (variants linter, docs-gen) — the
763
+ // config is static module data, exposing it costs nothing.
764
+ Object.defineProperty(resolve, 'config', { value: config, enumerable: false });
765
+ return resolve;
428
766
  }
429
- // Slot mode
430
- const slotEntries = Object.keys(slots);
431
- return function resolve(props) {
767
+ // Slot mode — `slots` is narrowed to non-null by `hasSlots`.
768
+ const slotNames = Object.keys(slots);
769
+ const slotBases = new Map();
770
+ for (const slotName of slotNames) {
771
+ slotBases.set(slotName, tokenize(resolveClassValue(slots[slotName])));
772
+ }
773
+ const pickerFor = (slotName) => (value) => {
774
+ if (value != null && typeof value === 'object' && !Array.isArray(value)) {
775
+ return value[slotName];
776
+ }
777
+ // Plain string/array config values route to the 'base' slot only.
778
+ return slotName === 'base' ? value : undefined;
779
+ };
780
+ const resolve = function resolve(props) {
781
+ if (import.meta.env?.DEV && props != null && props.class != null) {
782
+ console.warn(`tv(): a top-level \`class\` is ignored in slot mode (slots: ${slotNames.join(', ')}) — pass it to the slot function instead, e.g. styles.base({ class }).`);
783
+ }
432
784
  const topProps = { ...defaultVariants, ...stripUndefined(props || {}) };
433
785
  const result = {};
434
- for (const slotName of slotEntries) {
786
+ for (const slotName of slotNames) {
787
+ const pick = pickerFor(slotName);
788
+ const slotBase = slotBases.get(slotName);
435
789
  result[slotName] = function slotFn(slotProps) {
436
790
  const effective = { ...topProps, ...stripUndefined(slotProps || {}) };
437
- // 1. Slot base (merge config.base into 'base' slot if present).
438
- // `config.base` only contributes to a slot literally named
439
- // `'base'`. If a component renames its primary slot (e.g. to
440
- // `wrapper`), `config.base` is silently dropped — slots and
441
- // config.base must agree on the name.
442
- const baseTokens = [];
443
- if (slotName === 'base' && base) {
444
- baseTokens.push(...tokenize(resolveClassValue(base)));
445
- }
446
- baseTokens.push(...tokenize(resolveClassValue(slots[slotName])));
447
- // 2. Per-slot variant resolution
448
- const variantTokens = [];
449
- for (const [vName, vMap] of variantEntries) {
450
- const key = falsyToString(effective[vName]);
451
- if (key == null || !(key in vMap))
452
- continue;
453
- const vValue = vMap[key];
454
- if (vValue != null && typeof vValue === 'object' && !Array.isArray(vValue)) {
455
- const obj = vValue;
456
- if (slotName in obj) {
457
- variantTokens.push(...tokenize(resolveClassValue(obj[slotName])));
458
- }
459
- }
460
- else if (slotName === 'base') {
461
- variantTokens.push(...tokenize(resolveClassValue(vValue)));
462
- }
463
- }
464
- // 3. Compound variant resolution (per-slot)
465
- const compoundTokens = [];
466
- for (const cv of compoundVariants) {
467
- if (!matchesCompound(cv, effective))
468
- continue;
469
- const cvClass = cv.class ?? cv.className;
470
- if (cvClass != null && typeof cvClass === 'object' && !Array.isArray(cvClass)) {
471
- const obj = cvClass;
472
- if (slotName in obj) {
473
- compoundTokens.push(...tokenize(resolveClassValue(obj[slotName])));
474
- }
475
- }
476
- else if (slotName === 'base') {
477
- compoundTokens.push(...tokenize(resolveClassValue(cvClass)));
478
- }
479
- }
480
- // 4. Class override from slotProps
481
- const overrideTokens = [];
482
- if (slotProps?.class)
483
- overrideTokens.push(...tokenize(resolveClassValue(slotProps.class)));
484
- if (slotProps?.className)
485
- overrideTokens.push(...tokenize(resolveClassValue(slotProps.className)));
486
- // Pipeline strip: later stage wins over earlier stage's conflicts.
487
- const afterVariants = [...stripConflicts(baseTokens, variantTokens), ...variantTokens];
488
- const afterCompounds = [
489
- ...stripConflicts(afterVariants, compoundTokens),
490
- ...compoundTokens
491
- ];
492
- const final = [...stripConflicts(afterCompounds, overrideTokens), ...overrideTokens];
493
- return final.join(' ');
791
+ const overrideTokens = slotProps?.class ? tokenize(cx(slotProps.class)) : [];
792
+ return foldFor(slotBase, effective, pick, overrideTokens);
494
793
  };
495
794
  }
496
795
  return result;
497
796
  };
797
+ Object.defineProperty(resolve, 'config', { value: config, enumerable: false });
798
+ return resolve;
498
799
  }