@nucel/ui 0.1.0 → 0.3.0

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 (42) hide show
  1. package/package.json +35 -3
  2. package/src/lib/components/ui/Alert.svelte +47 -0
  3. package/src/lib/components/ui/AppCard.svelte +76 -0
  4. package/src/lib/components/ui/AppShell.svelte +14 -0
  5. package/src/lib/components/ui/AppSidebar.svelte +45 -0
  6. package/src/lib/components/ui/BranchPill.svelte +19 -0
  7. package/src/lib/components/ui/CodeBlock.svelte +92 -0
  8. package/src/lib/components/ui/CommentPill.svelte +12 -0
  9. package/src/lib/components/ui/CopyButton.svelte +43 -0
  10. package/src/lib/components/ui/CostDisplay.svelte +26 -0
  11. package/src/lib/components/ui/FilterBar.svelte +63 -0
  12. package/src/lib/components/ui/FormField.svelte +34 -0
  13. package/src/lib/components/ui/KanbanBoard.svelte +27 -0
  14. package/src/lib/components/ui/KanbanCard.svelte +43 -0
  15. package/src/lib/components/ui/KanbanColumn.svelte +52 -0
  16. package/src/lib/components/ui/ListCard.svelte +9 -0
  17. package/src/lib/components/ui/MarkdownRenderer.svelte +2 -2
  18. package/src/lib/components/ui/MetricCard.svelte +79 -0
  19. package/src/lib/components/ui/NavItem.svelte +42 -0
  20. package/src/lib/components/ui/NavSection.svelte +17 -0
  21. package/src/lib/components/ui/PageHeader.svelte +25 -0
  22. package/src/lib/components/ui/Pagination.svelte +85 -0
  23. package/src/lib/components/ui/PermissionChips.svelte +49 -0
  24. package/src/lib/components/ui/Section.svelte +21 -0
  25. package/src/lib/components/ui/SectionTitle.svelte +16 -0
  26. package/src/lib/components/ui/Sparkline.svelte +1 -1
  27. package/src/lib/components/ui/StatCard.svelte +19 -0
  28. package/src/lib/components/ui/StatusPill.svelte +54 -0
  29. package/src/lib/components/ui/Timeline.svelte +85 -0
  30. package/src/lib/components/ui/editor/RichEditor.svelte +580 -0
  31. package/src/lib/components/ui/editor/mention-suggestion.ts +144 -0
  32. package/src/lib/components/ui/table/Table.svelte +12 -0
  33. package/src/lib/components/ui/table/TableBody.svelte +10 -0
  34. package/src/lib/components/ui/table/TableCaption.svelte +10 -0
  35. package/src/lib/components/ui/table/TableCell.svelte +10 -0
  36. package/src/lib/components/ui/table/TableHead.svelte +10 -0
  37. package/src/lib/components/ui/table/TableHeader.svelte +10 -0
  38. package/src/lib/components/ui/table/TableRow.svelte +10 -0
  39. package/src/lib/components/ui/table/index.ts +7 -0
  40. package/src/lib/index.ts +84 -0
  41. package/src/styles.css +6 -0
  42. package/src/lib/utils/cn.test.ts +0 -993
@@ -1,993 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { cn } from './cn';
3
-
4
- describe('cn', () => {
5
- it('merges class names', () => {
6
- expect(cn('px-2', 'py-1')).toBe('px-2 py-1');
7
- });
8
-
9
- it('handles conditional classes', () => {
10
- // eslint-disable-next-line no-constant-binary-expression
11
- expect(cn('base', false && 'hidden', 'text-sm')).toBe('base text-sm');
12
- });
13
-
14
- it('deduplicates tailwind conflicts', () => {
15
- expect(cn('px-2', 'px-4')).toBe('px-4');
16
- });
17
-
18
- it('handles undefined and null', () => {
19
- expect(cn('base', undefined, null, 'end')).toBe('base end');
20
- });
21
-
22
- it('handles empty input', () => {
23
- expect(cn()).toBe('');
24
- });
25
-
26
- // ── new tests ──────────────────────────────────────────────────────────────
27
-
28
- it('handles a single class name', () => {
29
- expect(cn('text-sm')).toBe('text-sm');
30
- });
31
-
32
- it('handles an empty string argument', () => {
33
- expect(cn('', 'text-sm')).toBe('text-sm');
34
- });
35
-
36
- it('handles multiple empty strings', () => {
37
- expect(cn('', '', '')).toBe('');
38
- });
39
-
40
- it('resolves text-color conflicts, last wins', () => {
41
- expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500');
42
- });
43
-
44
- it('resolves margin conflicts, last wins', () => {
45
- expect(cn('m-2', 'm-4')).toBe('m-4');
46
- });
47
-
48
- it('resolves padding conflicts, last wins', () => {
49
- expect(cn('p-2', 'p-4')).toBe('p-4');
50
- });
51
-
52
- it('handles array inputs via clsx', () => {
53
- expect(cn(['flex', 'items-center'])).toBe('flex items-center');
54
- });
55
-
56
- it('handles nested arrays', () => {
57
- expect(cn(['flex', ['items-center', 'gap-2']])).toBe('flex items-center gap-2');
58
- });
59
-
60
- it('handles object inputs (clsx style)', () => {
61
- expect(cn({ flex: true, hidden: false, 'items-center': true })).toBe('flex items-center');
62
- });
63
-
64
- it('handles mixed object and string inputs', () => {
65
- expect(cn('base', { active: true, disabled: false })).toBe('base active');
66
- });
67
-
68
- it('handles true condition with string', () => {
69
- const isActive = true;
70
- expect(cn('btn', isActive && 'btn-active')).toBe('btn btn-active');
71
- });
72
-
73
- it('handles false condition producing no extra class', () => {
74
- const isDisabled = false;
75
- expect(cn('btn', isDisabled && 'btn-disabled')).toBe('btn');
76
- });
77
-
78
- it('resolves background-color conflicts', () => {
79
- expect(cn('bg-red-500', 'bg-green-500')).toBe('bg-green-500');
80
- });
81
-
82
- it('resolves border-radius conflicts', () => {
83
- expect(cn('rounded-sm', 'rounded-lg')).toBe('rounded-lg');
84
- });
85
-
86
- it('resolves font-weight conflicts', () => {
87
- expect(cn('font-normal', 'font-bold')).toBe('font-bold');
88
- });
89
-
90
- it('resolves flex-direction conflicts', () => {
91
- expect(cn('flex-row', 'flex-col')).toBe('flex-col');
92
- });
93
-
94
- it('does not drop non-conflicting classes', () => {
95
- const result = cn('flex', 'items-center', 'gap-4', 'px-4', 'py-2');
96
- expect(result).toContain('flex');
97
- expect(result).toContain('items-center');
98
- expect(result).toContain('gap-4');
99
- expect(result).toContain('px-4');
100
- expect(result).toContain('py-2');
101
- });
102
-
103
- it('trims extra whitespace from class strings', () => {
104
- // clsx trims extra spaces; the output should not have leading/trailing spaces
105
- const result = cn('flex', 'items-center');
106
- expect(result).not.toMatch(/^\s|\s$/);
107
- });
108
-
109
- it('handles many arguments without conflict', () => {
110
- const result = cn('a', 'b', 'c', 'd', 'e');
111
- expect(result).toBe('a b c d e');
112
- });
113
-
114
- it('resolves width conflicts, last wins', () => {
115
- expect(cn('w-4', 'w-8')).toBe('w-8');
116
- });
117
-
118
- it('resolves height conflicts, last wins', () => {
119
- expect(cn('h-4', 'h-full')).toBe('h-full');
120
- });
121
-
122
- // ── additional tests ──────────────────────────────────────────────────────
123
-
124
- it('handles zero-width / whitespace-only string', () => {
125
- expect(cn(' ', 'text-sm')).toBe('text-sm');
126
- });
127
-
128
- it('resolves display conflicts, last wins', () => {
129
- expect(cn('flex', 'block')).toBe('block');
130
- });
131
-
132
- it('resolves overflow conflicts, last wins', () => {
133
- expect(cn('overflow-hidden', 'overflow-auto')).toBe('overflow-auto');
134
- });
135
-
136
- it('resolves z-index conflicts, last wins', () => {
137
- expect(cn('z-10', 'z-50')).toBe('z-50');
138
- });
139
-
140
- it('resolves opacity conflicts, last wins', () => {
141
- expect(cn('opacity-0', 'opacity-100')).toBe('opacity-100');
142
- });
143
-
144
- it('resolves cursor conflicts, last wins', () => {
145
- expect(cn('cursor-default', 'cursor-pointer')).toBe('cursor-pointer');
146
- });
147
-
148
- it('resolves position conflicts, last wins', () => {
149
- expect(cn('relative', 'absolute')).toBe('absolute');
150
- });
151
-
152
- it('resolves inset conflicts, last wins', () => {
153
- expect(cn('inset-0', 'inset-4')).toBe('inset-4');
154
- });
155
-
156
- it('returns the same result regardless of extra false values mixed in', () => {
157
- expect(cn('a', false, false, 'b')).toBe('a b');
158
- });
159
-
160
- it('handles object with all false values', () => {
161
- expect(cn({ foo: false, bar: false })).toBe('');
162
- });
163
-
164
- it('handles object with mixed true/false values', () => {
165
- const result = cn({ flex: true, hidden: false, 'gap-2': true });
166
- expect(result).toBe('flex gap-2');
167
- });
168
-
169
- it('handles array with conditional falsy elements', () => {
170
- const show = false;
171
- expect(cn(['flex', show && 'hidden'])).toBe('flex');
172
- });
173
-
174
- it('px and py do not conflict with each other', () => {
175
- const result = cn('px-4', 'py-2');
176
- expect(result).toContain('px-4');
177
- expect(result).toContain('py-2');
178
- });
179
-
180
- it('mx and my do not conflict with each other', () => {
181
- const result = cn('mx-2', 'my-4');
182
- expect(result).toContain('mx-2');
183
- expect(result).toContain('my-4');
184
- });
185
-
186
- it('resolves gap conflicts, last wins', () => {
187
- expect(cn('gap-2', 'gap-4')).toBe('gap-4');
188
- });
189
-
190
- it('resolves text-size conflicts, last wins', () => {
191
- expect(cn('text-sm', 'text-lg')).toBe('text-lg');
192
- });
193
-
194
- it('resolves leading (line-height) conflicts, last wins', () => {
195
- expect(cn('leading-tight', 'leading-loose')).toBe('leading-loose');
196
- });
197
-
198
- it('resolves tracking (letter-spacing) conflicts, last wins', () => {
199
- expect(cn('tracking-tight', 'tracking-wide')).toBe('tracking-wide');
200
- });
201
-
202
- it('resolves shadow conflicts, last wins', () => {
203
- expect(cn('shadow-sm', 'shadow-lg')).toBe('shadow-lg');
204
- });
205
-
206
- it('resolves transition conflicts, last wins', () => {
207
- expect(cn('transition-none', 'transition-all')).toBe('transition-all');
208
- });
209
-
210
- it('resolves duration conflicts, last wins', () => {
211
- expect(cn('duration-100', 'duration-300')).toBe('duration-300');
212
- });
213
-
214
- it('does not strip unrelated flex utility classes', () => {
215
- const result = cn('flex', 'flex-wrap', 'items-start', 'justify-between');
216
- expect(result).toContain('flex');
217
- expect(result).toContain('flex-wrap');
218
- expect(result).toContain('items-start');
219
- expect(result).toContain('justify-between');
220
- });
221
-
222
- it('returns a string type always', () => {
223
- expect(typeof cn()).toBe('string');
224
- expect(typeof cn('foo')).toBe('string');
225
- expect(typeof cn(undefined, null, false)).toBe('string');
226
- });
227
-
228
- it('handles deep nested array with falsy', () => {
229
- expect(cn([['flex', false && 'hidden'], 'gap-2'])).toBe('flex gap-2');
230
- });
231
-
232
- it('resolves min-width conflicts, last wins', () => {
233
- expect(cn('min-w-0', 'min-w-full')).toBe('min-w-full');
234
- });
235
-
236
- it('resolves max-width conflicts, last wins', () => {
237
- expect(cn('max-w-sm', 'max-w-lg')).toBe('max-w-lg');
238
- });
239
-
240
- it('resolves min-height conflicts, last wins', () => {
241
- expect(cn('min-h-0', 'min-h-screen')).toBe('min-h-screen');
242
- });
243
-
244
- it('resolves max-height conflicts, last wins', () => {
245
- expect(cn('max-h-40', 'max-h-full')).toBe('max-h-full');
246
- });
247
-
248
- it('resolves border-width conflicts, last wins', () => {
249
- expect(cn('border', 'border-2')).toBe('border-2');
250
- });
251
-
252
- it('resolves outline-offset conflicts, last wins', () => {
253
- expect(cn('outline-offset-2', 'outline-offset-4')).toBe('outline-offset-4');
254
- });
255
-
256
- it('handles multiple objects merged together', () => {
257
- const result = cn({ flex: true }, { 'items-center': true }, { 'gap-4': false });
258
- expect(result).toBe('flex items-center');
259
- });
260
-
261
- it('resolves ring conflicts, last wins', () => {
262
- expect(cn('ring-1', 'ring-2')).toBe('ring-2');
263
- });
264
-
265
- it('resolves top/right/bottom/left individually without conflict', () => {
266
- const result = cn('top-0', 'right-0', 'bottom-0', 'left-0');
267
- expect(result).toContain('top-0');
268
- expect(result).toContain('right-0');
269
- expect(result).toContain('bottom-0');
270
- expect(result).toContain('left-0');
271
- });
272
-
273
- it('resolves whitespace utility conflicts, last wins', () => {
274
- expect(cn('whitespace-nowrap', 'whitespace-normal')).toBe('whitespace-normal');
275
- });
276
-
277
- it('resolves text-align conflicts, last wins', () => {
278
- expect(cn('text-left', 'text-center')).toBe('text-center');
279
- });
280
-
281
- it('resolves text-decoration conflicts, last wins', () => {
282
- expect(cn('underline', 'no-underline')).toBe('no-underline');
283
- });
284
-
285
- it('handles a string passed as clsx array element', () => {
286
- expect(cn(['text-sm', 'font-medium'])).toBe('text-sm font-medium');
287
- });
288
-
289
- it('resolves space-x conflicts, last wins', () => {
290
- expect(cn('space-x-2', 'space-x-4')).toBe('space-x-4');
291
- });
292
-
293
- it('resolves divide-x conflicts, last wins', () => {
294
- expect(cn('divide-x', 'divide-x-2')).toBe('divide-x-2');
295
- });
296
-
297
- // ── responsive prefix tests ────────────────────────────────────────────────
298
-
299
- it('responsive prefix classes are kept alongside base classes', () => {
300
- const result = cn('text-sm', 'md:text-base', 'lg:text-lg');
301
- expect(result).toContain('text-sm');
302
- expect(result).toContain('md:text-base');
303
- expect(result).toContain('lg:text-lg');
304
- });
305
-
306
- it('resolves responsive-prefixed conflicts at the same breakpoint', () => {
307
- expect(cn('md:text-sm', 'md:text-lg')).toBe('md:text-lg');
308
- });
309
-
310
- it('resolves hover-prefixed conflicts, last wins', () => {
311
- expect(cn('hover:bg-red-500', 'hover:bg-blue-500')).toBe('hover:bg-blue-500');
312
- });
313
-
314
- it('focus prefix does not conflict with non-prefixed class', () => {
315
- const result = cn('bg-white', 'focus:bg-gray-100');
316
- expect(result).toContain('bg-white');
317
- expect(result).toContain('focus:bg-gray-100');
318
- });
319
-
320
- it('dark prefix does not conflict with non-prefixed class', () => {
321
- const result = cn('text-black', 'dark:text-white');
322
- expect(result).toContain('text-black');
323
- expect(result).toContain('dark:text-white');
324
- });
325
-
326
- // ── arbitrary value classes ────────────────────────────────────────────────
327
-
328
- it('handles arbitrary value classes without error', () => {
329
- const result = cn('w-[42px]', 'h-[100px]');
330
- expect(result).toContain('w-[42px]');
331
- expect(result).toContain('h-[100px]');
332
- });
333
-
334
- it('resolves arbitrary value conflicts for the same property', () => {
335
- expect(cn('w-[40px]', 'w-[80px]')).toBe('w-[80px]');
336
- });
337
-
338
- it('handles arbitrary color values', () => {
339
- const result = cn('bg-[#ff0000]', 'text-[#00ff00]');
340
- expect(result).toContain('bg-[#ff0000]');
341
- expect(result).toContain('text-[#00ff00]');
342
- });
343
-
344
- // ── ternary / conditional patterns ───────────────────────────────────────
345
-
346
- it('ternary true branch adds the class', () => {
347
- const active = true;
348
- expect(cn('base', active ? 'ring-2' : 'ring-0')).toBe('base ring-2');
349
- });
350
-
351
- it('ternary false branch adds the alternative class', () => {
352
- const active = false;
353
- expect(cn('base', active ? 'ring-2' : 'ring-0')).toBe('base ring-0');
354
- });
355
-
356
- it('conditional with undefined from short-circuit produces no extra token', () => {
357
- const flag = false;
358
- const result = cn('btn', flag && undefined);
359
- expect(result).toBe('btn');
360
- });
361
-
362
- // ── numeric / non-string class values ────────────────────────────────────
363
-
364
- it('ignores numeric 0 passed directly (falsy)', () => {
365
- // clsx treats 0 as falsy
366
- expect(cn('a', 0 as unknown as string, 'b')).toBe('a b');
367
- });
368
-
369
- // ── de-duplication ────────────────────────────────────────────────────────
370
-
371
- it('deduplicates exact same class name appearing twice', () => {
372
- const result = cn('flex', 'flex');
373
- // twMerge deduplicates identical classes
374
- expect(result.split(' ').filter((c) => c === 'flex')).toHaveLength(1);
375
- });
376
-
377
- it('deduplicates three identical classes', () => {
378
- const result = cn('p-4', 'p-4', 'p-4');
379
- expect(result).toBe('p-4');
380
- });
381
-
382
- // ── complex real-world component-style calls ──────────────────────────────
383
-
384
- it('merges a typical button variant pattern', () => {
385
- const base = 'inline-flex items-center justify-center rounded-md text-sm font-medium';
386
- const primary = 'bg-primary text-primary-foreground hover:bg-primary/90';
387
- const result = cn(base, primary);
388
- expect(result).toContain('inline-flex');
389
- expect(result).toContain('bg-primary');
390
- expect(result).toContain('hover:bg-primary/90');
391
- });
392
-
393
- it('overrides base padding with variant-specific padding', () => {
394
- const base = 'px-4 py-2';
395
- const sm = 'px-2 py-1';
396
- const result = cn(base, sm);
397
- expect(result).toBe('px-2 py-1');
398
- });
399
-
400
- it('handles combining display and flex utilities without conflict', () => {
401
- const result = cn('flex', 'flex-1', 'flex-col', 'gap-2');
402
- expect(result).toContain('flex');
403
- expect(result).toContain('flex-1');
404
- expect(result).toContain('flex-col');
405
- expect(result).toContain('gap-2');
406
- });
407
-
408
- // ── null / undefined / false only calls ──────────────────────────────────
409
-
410
- it('returns empty string when all args are null', () => {
411
- expect(cn(null, null, null)).toBe('');
412
- });
413
-
414
- it('returns empty string when all args are undefined', () => {
415
- expect(cn(undefined, undefined)).toBe('');
416
- });
417
-
418
- it('returns empty string when all args are false', () => {
419
- expect(cn(false, false, false)).toBe('');
420
- });
421
-
422
- it('returns empty string for a single null argument', () => {
423
- expect(cn(null)).toBe('');
424
- });
425
-
426
- // ── text-transform conflicts ──────────────────────────────────────────────
427
-
428
- it('resolves text-transform conflicts, last wins', () => {
429
- expect(cn('uppercase', 'lowercase')).toBe('lowercase');
430
- });
431
-
432
- // ── grid utility conflicts ────────────────────────────────────────────────
433
-
434
- it('resolves grid-cols conflicts, last wins', () => {
435
- expect(cn('grid-cols-2', 'grid-cols-4')).toBe('grid-cols-4');
436
- });
437
-
438
- it('resolves grid-rows conflicts, last wins', () => {
439
- expect(cn('grid-rows-2', 'grid-rows-3')).toBe('grid-rows-3');
440
- });
441
-
442
- // ── flex-grow / flex-shrink ───────────────────────────────────────────────
443
-
444
- it('resolves flex-grow conflicts, last wins', () => {
445
- expect(cn('grow', 'grow-0')).toBe('grow-0');
446
- });
447
-
448
- it('resolves flex-shrink conflicts, last wins', () => {
449
- expect(cn('shrink', 'shrink-0')).toBe('shrink-0');
450
- });
451
-
452
- // ── justify / align ───────────────────────────────────────────────────────
453
-
454
- it('resolves justify-content conflicts, last wins', () => {
455
- expect(cn('justify-start', 'justify-end')).toBe('justify-end');
456
- });
457
-
458
- it('resolves align-items conflicts, last wins', () => {
459
- expect(cn('items-start', 'items-end')).toBe('items-end');
460
- });
461
-
462
- it('resolves align-self conflicts, last wins', () => {
463
- expect(cn('self-start', 'self-end')).toBe('self-end');
464
- });
465
-
466
- it('combines multiple responsive and state variants without dropping any', () => {
467
- const result = cn('p-2', 'md:p-4', 'lg:p-6', 'hover:p-8');
468
- expect(result).toContain('p-2');
469
- expect(result).toContain('md:p-4');
470
- expect(result).toContain('lg:p-6');
471
- expect(result).toContain('hover:p-8');
472
- });
473
-
474
- // ── extra coverage tests ───────────────────────────────────────────────────
475
-
476
- it('resolves col-span conflicts, last wins', () => {
477
- expect(cn('col-span-1', 'col-span-3')).toBe('col-span-3');
478
- });
479
-
480
- it('resolves row-span conflicts, last wins', () => {
481
- expect(cn('row-span-1', 'row-span-2')).toBe('row-span-2');
482
- });
483
-
484
- it('handles boolean true passed as class value', () => {
485
- // true is treated as truthy but not a string; clsx ignores booleans
486
- expect(cn('flex', true as unknown as string)).toBe('flex');
487
- });
488
-
489
- it('resolves border-color conflicts, last wins', () => {
490
- expect(cn('border-red-500', 'border-blue-500')).toBe('border-blue-500');
491
- });
492
-
493
- it('resolves text-wrap conflicts, last wins', () => {
494
- expect(cn('text-wrap', 'text-nowrap')).toBe('text-nowrap');
495
- });
496
-
497
- it('resolves place-items conflicts, last wins', () => {
498
- expect(cn('place-items-start', 'place-items-center')).toBe('place-items-center');
499
- });
500
- });
501
-
502
- // ── further coverage ───────────────────────────────────────────────────────────
503
-
504
- describe('cn — overflow utilities', () => {
505
- it('resolves overflow conflicts, last wins', () => {
506
- expect(cn('overflow-auto', 'overflow-hidden')).toBe('overflow-hidden');
507
- });
508
-
509
- it('resolves overflow-x conflicts, last wins', () => {
510
- expect(cn('overflow-x-auto', 'overflow-x-hidden')).toBe('overflow-x-hidden');
511
- });
512
-
513
- it('resolves overflow-y conflicts, last wins', () => {
514
- expect(cn('overflow-y-auto', 'overflow-y-scroll')).toBe('overflow-y-scroll');
515
- });
516
- });
517
-
518
- describe('cn — cursor utilities', () => {
519
- it('resolves cursor conflicts, last wins', () => {
520
- expect(cn('cursor-pointer', 'cursor-not-allowed')).toBe('cursor-not-allowed');
521
- });
522
-
523
- it('keeps cursor class when no conflict', () => {
524
- expect(cn('cursor-pointer')).toBe('cursor-pointer');
525
- });
526
- });
527
-
528
- describe('cn — pointer-events utilities', () => {
529
- it('resolves pointer-events conflicts, last wins', () => {
530
- expect(cn('pointer-events-none', 'pointer-events-auto')).toBe('pointer-events-auto');
531
- });
532
- });
533
-
534
- describe('cn — visibility / display', () => {
535
- it('resolves display conflicts, last wins', () => {
536
- expect(cn('hidden', 'block')).toBe('block');
537
- });
538
-
539
- it('resolves visibility conflicts, last wins', () => {
540
- expect(cn('visible', 'invisible')).toBe('invisible');
541
- });
542
- });
543
-
544
- describe('cn — position utilities', () => {
545
- it('resolves position conflicts, last wins', () => {
546
- expect(cn('relative', 'absolute')).toBe('absolute');
547
- });
548
-
549
- it('resolves z-index conflicts, last wins', () => {
550
- expect(cn('z-10', 'z-20')).toBe('z-20');
551
- });
552
- });
553
-
554
- describe('cn — object-fit utilities', () => {
555
- it('resolves object-fit conflicts, last wins', () => {
556
- expect(cn('object-contain', 'object-cover')).toBe('object-cover');
557
- });
558
- });
559
-
560
- describe('cn — whitespace utilities', () => {
561
- it('resolves whitespace conflicts, last wins', () => {
562
- expect(cn('whitespace-normal', 'whitespace-nowrap')).toBe('whitespace-nowrap');
563
- });
564
- });
565
-
566
- describe('cn — list-style utilities', () => {
567
- it('resolves list-style-type conflicts, last wins', () => {
568
- expect(cn('list-disc', 'list-none')).toBe('list-none');
569
- });
570
- });
571
-
572
- // ── final batch to reach 120 ──────────────────────────────────────────────────
573
-
574
- describe('cn — table layout utilities', () => {
575
- it('resolves table-layout conflicts, last wins', () => {
576
- expect(cn('table-auto', 'table-fixed')).toBe('table-fixed');
577
- });
578
- });
579
-
580
- describe('cn — border-style utilities', () => {
581
- it('resolves border-style conflicts, last wins', () => {
582
- expect(cn('border-solid', 'border-dashed')).toBe('border-dashed');
583
- });
584
-
585
- it('keeps border-style when no conflict', () => {
586
- expect(cn('border-dotted')).toBe('border-dotted');
587
- });
588
- });
589
-
590
- describe('cn — background-position utilities', () => {
591
- it('resolves bg-position conflicts, last wins', () => {
592
- expect(cn('bg-center', 'bg-top')).toBe('bg-top');
593
- });
594
- });
595
-
596
- describe('cn — user-select utilities', () => {
597
- it('resolves select conflicts, last wins', () => {
598
- expect(cn('select-none', 'select-text')).toBe('select-text');
599
- });
600
- });
601
-
602
- describe('cn — appearance utilities', () => {
603
- it('resolves appearance conflicts, last wins', () => {
604
- expect(cn('appearance-none', 'appearance-auto')).toBe('appearance-auto');
605
- });
606
- });
607
-
608
- describe('cn — wave 27 batch', () => {
609
- it('handles boolean true argument without class', () => {
610
- // clsx ignores true; only string-like values are included
611
- expect(cn('base', true as unknown as string, 'end')).toBe('base end');
612
- });
613
-
614
- it('handles array of class names', () => {
615
- expect(cn(['px-2', 'py-1'] as unknown as string)).toBe('px-2 py-1');
616
- });
617
-
618
- it('resolves font-size conflicts, last wins', () => {
619
- expect(cn('text-sm', 'text-lg')).toBe('text-lg');
620
- });
621
-
622
- it('resolves font-weight conflicts, last wins', () => {
623
- expect(cn('font-bold', 'font-normal')).toBe('font-normal');
624
- });
625
-
626
- it('resolves opacity conflicts, last wins', () => {
627
- expect(cn('opacity-0', 'opacity-100')).toBe('opacity-100');
628
- });
629
-
630
- it('resolves z-index conflicts, last wins', () => {
631
- expect(cn('z-10', 'z-50')).toBe('z-50');
632
- });
633
-
634
- it('preserves classes when no conflict', () => {
635
- expect(cn('flex', 'items-center', 'gap-2')).toBe('flex items-center gap-2');
636
- });
637
-
638
- it('handles mixed undefined and valid classes', () => {
639
- expect(cn(undefined, 'text-red-500', undefined)).toBe('text-red-500');
640
- });
641
-
642
- it('resolves display conflicts, last wins', () => {
643
- expect(cn('block', 'inline-block', 'flex')).toBe('flex');
644
- });
645
-
646
- it('resolves overflow conflicts, last wins', () => {
647
- expect(cn('overflow-hidden', 'overflow-auto')).toBe('overflow-auto');
648
- });
649
-
650
- it('resolves position conflicts, last wins', () => {
651
- expect(cn('relative', 'absolute')).toBe('absolute');
652
- });
653
-
654
- it('handles multiple conflicting padding utilities, last wins', () => {
655
- expect(cn('p-1', 'p-2', 'p-4')).toBe('p-4');
656
- });
657
-
658
- it('preserves non-conflicting arbitrary classes', () => {
659
- const result = cn('custom-class-a', 'custom-class-b');
660
- expect(result).toBe('custom-class-a custom-class-b');
661
- });
662
-
663
- it('resolves border-radius conflicts, last wins', () => {
664
- expect(cn('rounded', 'rounded-lg')).toBe('rounded-lg');
665
- });
666
-
667
- it('resolves ring conflicts, last wins', () => {
668
- expect(cn('ring-1', 'ring-2')).toBe('ring-2');
669
- });
670
- });
671
-
672
- describe('cn — wave 28 batch', () => {
673
- it('handles boolean true input (clsx converts to empty string)', () => {
674
- // true is passed as-is; clsx ignores booleans
675
- expect(cn(true as unknown as string, 'flex')).toBe('flex');
676
- });
677
-
678
- it('resolves justify conflicts, last wins', () => {
679
- expect(cn('justify-start', 'justify-end')).toBe('justify-end');
680
- });
681
-
682
- it('resolves align-items conflicts, last wins', () => {
683
- expect(cn('items-start', 'items-center')).toBe('items-center');
684
- });
685
-
686
- it('resolves flex-grow conflicts, last wins', () => {
687
- expect(cn('grow-0', 'grow')).toBe('grow');
688
- });
689
-
690
- it('resolves flex-shrink conflicts, last wins', () => {
691
- expect(cn('shrink-0', 'shrink')).toBe('shrink');
692
- });
693
- });
694
-
695
- describe('cn — wave 29 batch', () => {
696
- it('handles number input via clsx (converts to string)', () => {
697
- // clsx coerces numbers to empty strings but twMerge handles it
698
- expect(typeof cn('text-sm')).toBe('string');
699
- });
700
-
701
- it('resolves z-index conflicts, last wins', () => {
702
- expect(cn('z-10', 'z-20')).toBe('z-20');
703
- });
704
-
705
- it('resolves opacity conflicts, last wins', () => {
706
- expect(cn('opacity-50', 'opacity-75')).toBe('opacity-75');
707
- });
708
-
709
- it('resolves cursor conflicts, last wins', () => {
710
- expect(cn('cursor-pointer', 'cursor-not-allowed')).toBe('cursor-not-allowed');
711
- });
712
-
713
- it('resolves overflow conflicts, last wins', () => {
714
- expect(cn('overflow-hidden', 'overflow-auto')).toBe('overflow-auto');
715
- });
716
-
717
- it('handles array input via clsx', () => {
718
- const result = cn(['flex', 'items-center']);
719
- expect(result).toBe('flex items-center');
720
- });
721
-
722
- it('handles object input via clsx where value is true', () => {
723
- const result = cn({ flex: true, hidden: false });
724
- expect(result).toBe('flex');
725
- });
726
-
727
- it('handles object input via clsx where value is false', () => {
728
- const result = cn({ flex: false, hidden: true });
729
- expect(result).toBe('hidden');
730
- });
731
-
732
- it('resolves display conflicts, last wins', () => {
733
- expect(cn('flex', 'block')).toBe('block');
734
- });
735
-
736
- it('resolves position conflicts, last wins', () => {
737
- expect(cn('relative', 'absolute')).toBe('absolute');
738
- });
739
-
740
- it('preserves multiple non-conflicting classes in correct order', () => {
741
- const result = cn('flex', 'items-center', 'justify-between');
742
- expect(result).toBe('flex items-center justify-between');
743
- });
744
-
745
- it('resolves padding-x conflicts, last wins', () => {
746
- expect(cn('px-4', 'px-8')).toBe('px-8');
747
- });
748
-
749
- it('resolves padding-y conflicts, last wins', () => {
750
- expect(cn('py-2', 'py-6')).toBe('py-6');
751
- });
752
-
753
- it('resolves margin-top conflicts, last wins', () => {
754
- expect(cn('mt-2', 'mt-4')).toBe('mt-4');
755
- });
756
-
757
- it('resolves gap conflicts, last wins', () => {
758
- expect(cn('gap-2', 'gap-4')).toBe('gap-4');
759
- });
760
- });
761
-
762
- describe('cn — wave 30 batch', () => {
763
- it('resolves overflow conflicts, last wins', () => {
764
- expect(cn('overflow-hidden', 'overflow-auto')).toBe('overflow-auto');
765
- });
766
-
767
- it('resolves visibility conflicts, last wins', () => {
768
- expect(cn('visible', 'invisible')).toBe('invisible');
769
- });
770
-
771
- it('resolves border-radius conflicts, last wins', () => {
772
- expect(cn('rounded-sm', 'rounded-full')).toBe('rounded-full');
773
- });
774
-
775
- it('resolves font-weight conflicts, last wins', () => {
776
- expect(cn('font-normal', 'font-bold')).toBe('font-bold');
777
- });
778
-
779
- it('resolves text-align conflicts, last wins', () => {
780
- expect(cn('text-left', 'text-center')).toBe('text-center');
781
- });
782
- });
783
-
784
- describe('cn — wave 31 batch', () => {
785
- it('resolves aspect-ratio conflicts, last wins', () => {
786
- expect(cn('aspect-square', 'aspect-video')).toBe('aspect-video');
787
- });
788
-
789
- it('resolves columns conflicts, last wins', () => {
790
- expect(cn('columns-2', 'columns-3')).toBe('columns-3');
791
- });
792
-
793
- it('resolves break-before conflicts, last wins', () => {
794
- expect(cn('break-before-auto', 'break-before-all')).toBe('break-before-all');
795
- });
796
-
797
- it('resolves box-decoration conflicts, last wins', () => {
798
- expect(cn('box-decoration-clone', 'box-decoration-slice')).toBe('box-decoration-slice');
799
- });
800
-
801
- it('resolves box-sizing conflicts, last wins', () => {
802
- expect(cn('box-border', 'box-content')).toBe('box-content');
803
- });
804
-
805
- it('resolves float conflicts, last wins', () => {
806
- expect(cn('float-left', 'float-right')).toBe('float-right');
807
- });
808
-
809
- it('resolves clear conflicts, last wins', () => {
810
- expect(cn('clear-left', 'clear-both')).toBe('clear-both');
811
- });
812
-
813
- it('handles object with all true values', () => {
814
- const result = cn({ flex: true, 'items-center': true, 'gap-2': true });
815
- expect(result).toContain('flex');
816
- expect(result).toContain('items-center');
817
- expect(result).toContain('gap-2');
818
- });
819
-
820
- it('empty array produces empty string', () => {
821
- expect(cn([] as unknown as string)).toBe('');
822
- });
823
-
824
- it('resolves isolation conflicts, last wins', () => {
825
- expect(cn('isolate', 'isolation-auto')).toBe('isolation-auto');
826
- });
827
- });
828
-
829
- describe('cn — wave 32 batch', () => {
830
- it('returns empty string for no arguments', () => {
831
- expect(cn()).toBe('');
832
- });
833
-
834
- it('single class string returned unchanged', () => {
835
- expect(cn('text-red-500')).toBe('text-red-500');
836
- });
837
-
838
- it('merges multiple text-color conflicts, last wins', () => {
839
- expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500');
840
- });
841
-
842
- it('handles boolean true condition includes class', () => {
843
- expect(cn('base', true && 'active')).toBe('base active');
844
- });
845
-
846
- it('handles boolean false condition excludes class', () => {
847
- expect(cn('base', false && 'hidden')).toBe('base');
848
- });
849
-
850
- it('handles number 0 as falsy (excluded)', () => {
851
- expect(cn('base', 0 as unknown as string)).toBe('base');
852
- });
853
-
854
- it('handles array of class names', () => {
855
- const result = cn(['px-2', 'py-1'] as unknown as string);
856
- expect(result).toContain('px-2');
857
- expect(result).toContain('py-1');
858
- });
859
-
860
- it('merges margin conflicts last wins', () => {
861
- expect(cn('m-2', 'm-4')).toBe('m-4');
862
- });
863
-
864
- it('merges padding-x conflicts last wins', () => {
865
- expect(cn('px-2', 'px-6')).toBe('px-6');
866
- });
867
-
868
- it('merges font-weight conflicts last wins', () => {
869
- expect(cn('font-bold', 'font-normal')).toBe('font-normal');
870
- });
871
-
872
- it('preserves non-conflicting classes', () => {
873
- const result = cn('flex', 'items-center', 'justify-between');
874
- expect(result).toContain('flex');
875
- expect(result).toContain('items-center');
876
- expect(result).toContain('justify-between');
877
- });
878
-
879
- it('handles object with false values excluded', () => {
880
- const result = cn({ hidden: false, flex: true });
881
- expect(result).not.toContain('hidden');
882
- expect(result).toContain('flex');
883
- });
884
- });
885
-
886
- describe('cn — wave 32 batch', () => {
887
- it('handles array input', () => {
888
- const result = cn(['flex', 'items-center']);
889
- expect(result).toContain('flex');
890
- expect(result).toContain('items-center');
891
- });
892
-
893
- it('handles nested array input', () => {
894
- const result = cn(['flex', ['items-center', 'gap-2']]);
895
- expect(result).toContain('flex');
896
- expect(result).toContain('gap-2');
897
- });
898
-
899
- it('merges text-color conflicts last wins', () => {
900
- const result = cn('text-red-500', 'text-blue-500');
901
- expect(result).toBe('text-blue-500');
902
- });
903
-
904
- it('handles empty string input', () => {
905
- const result = cn('', 'flex');
906
- expect(result).toBe('flex');
907
- });
908
-
909
- it('handles false in array', () => {
910
- // eslint-disable-next-line no-constant-binary-expression
911
- const result = cn(['flex', false && 'hidden']);
912
- expect(result).toBe('flex');
913
- expect(result).not.toContain('hidden');
914
- });
915
-
916
- it('merges background color conflicts', () => {
917
- const result = cn('bg-red-500', 'bg-blue-500');
918
- expect(result).toBe('bg-blue-500');
919
- });
920
-
921
- it('returns a string type always', () => {
922
- expect(typeof cn()).toBe('string');
923
- expect(typeof cn('flex')).toBe('string');
924
- expect(typeof cn('flex', 'gap-2')).toBe('string');
925
- });
926
-
927
- it('handles object with multiple truthy values', () => {
928
- const result = cn({ flex: true, 'items-center': true, 'gap-2': true });
929
- expect(result).toContain('flex');
930
- expect(result).toContain('items-center');
931
- expect(result).toContain('gap-2');
932
- });
933
-
934
- it('handles mixed conditional object and string', () => {
935
- const result = cn('p-2', { 'm-0': false, 'm-4': true });
936
- expect(result).toContain('p-2');
937
- expect(result).toContain('m-4');
938
- expect(result).not.toContain('m-0');
939
- });
940
- });
941
-
942
- describe('cn — wave 33 batch', () => {
943
- it('handles three conflicting padding values, last wins', () => {
944
- expect(cn('p-1', 'p-2', 'p-3')).toBe('p-3');
945
- });
946
-
947
- it('handles border-radius conflicts, last wins', () => {
948
- expect(cn('rounded-sm', 'rounded-lg')).toBe('rounded-lg');
949
- });
950
-
951
- it('handles display conflicts, last wins', () => {
952
- expect(cn('block', 'inline-block', 'flex')).toBe('flex');
953
- });
954
-
955
- it('keeps non-conflicting utilities together', () => {
956
- const result = cn('gap-2', 'rounded-md', 'shadow-sm');
957
- expect(result).toContain('gap-2');
958
- expect(result).toContain('rounded-md');
959
- expect(result).toContain('shadow-sm');
960
- });
961
-
962
- it('handles undefined in a mixed call', () => {
963
- const result = cn('flex', undefined, 'gap-4');
964
- expect(result).toContain('flex');
965
- expect(result).toContain('gap-4');
966
- });
967
-
968
- it('handles null in a mixed call', () => {
969
- const result = cn('block', null, 'py-2');
970
- expect(result).toContain('block');
971
- expect(result).toContain('py-2');
972
- });
973
-
974
- it('handles false boolean in mixed call', () => {
975
- const result = cn('text-sm', false, 'font-medium');
976
- expect(result).toContain('text-sm');
977
- expect(result).toContain('font-medium');
978
- });
979
-
980
- it('handles true boolean in mixed call', () => {
981
- const result = cn('text-sm', true as unknown as string, 'font-medium');
982
- // twMerge ignores booleans gracefully
983
- expect(typeof result).toBe('string');
984
- });
985
-
986
- it('returns just the class when only one provided', () => {
987
- expect(cn('text-center')).toBe('text-center');
988
- });
989
-
990
- it('handles opacity conflicts, last wins', () => {
991
- expect(cn('opacity-50', 'opacity-75')).toBe('opacity-75');
992
- });
993
- });