nestable-tailwind-variants 0.1.3 → 0.2.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.
package/README.md CHANGED
@@ -1,8 +1,67 @@
1
1
  # nestable-tailwind-variants
2
2
 
3
- A variant styling library for Tailwind CSS that supports nested condition definitions. Express complex style combinations intuitively through nested objects instead of flat `compoundVariants` patterns.
3
+ Tailwind CSS variant library with nestable conditions for compound styling instead of flat compoundVariants.
4
4
 
5
- Inspired by [React Spectrum's `style` macro](https://react-spectrum.adobe.com/styling), with some ideas from [tailwind-variants](https://www.tailwind-variants.org/).
5
+ Inspired by [React Spectrum's style macro](https://github.com/adobe/react-spectrum/tree/main/packages/%40react-spectrum/s2) and [Tailwind Variants](https://www.tailwind-variants.org/).
6
+
7
+ ## Why nestable-tailwind-variants?
8
+
9
+ With [Tailwind Variants](https://www.tailwind-variants.org/), combining variants with [React Aria Components render props](https://react-aria.adobe.com/styling#render-props) like `isHovered` and `isPressed` requires `compoundVariants`. This quickly becomes verbose as combinations grow:
10
+
11
+ ```tsx
12
+ import { tv } from 'tailwind-variants';
13
+
14
+ const button = tv({
15
+ base: 'px-4 py-2 rounded',
16
+ variants: {
17
+ variant: {
18
+ primary: 'bg-blue-500 text-white',
19
+ secondary: 'bg-gray-200 text-gray-800',
20
+ },
21
+ isHovered: { true: '' },
22
+ isPressed: { true: '' },
23
+ },
24
+ compoundVariants: [
25
+ { variant: 'primary', isHovered: true, class: 'bg-blue-600' },
26
+ { variant: 'primary', isPressed: true, class: 'bg-blue-700' },
27
+ { variant: 'secondary', isHovered: true, class: 'bg-gray-300' },
28
+ // ... one entry for every variant × condition combination
29
+ ],
30
+ });
31
+
32
+ button({ variant: 'primary', isHovered: true });
33
+ // => 'px-4 py-2 rounded bg-blue-500 text-white bg-blue-600'
34
+ ```
35
+
36
+ nestable-tailwind-variants solves this by letting you nest conditions directly inside variants, keeping related logic together:
37
+
38
+ ```tsx
39
+ import { ntv } from 'nestable-tailwind-variants';
40
+
41
+ interface ButtonProps {
42
+ variant: 'primary' | 'secondary';
43
+ isHovered: boolean;
44
+ isPressed: boolean;
45
+ }
46
+
47
+ const button = ntv<ButtonProps>({
48
+ $base: 'px-4 py-2 rounded',
49
+ variant: {
50
+ primary: {
51
+ $default: 'bg-blue-500 text-white',
52
+ isHovered: 'bg-blue-600',
53
+ isPressed: 'bg-blue-700',
54
+ },
55
+ secondary: {
56
+ $default: 'bg-gray-200 text-gray-800',
57
+ isHovered: 'bg-gray-300',
58
+ },
59
+ },
60
+ });
61
+
62
+ button({ variant: 'primary', isHovered: true });
63
+ // => 'px-4 py-2 rounded bg-blue-600'
64
+ ```
6
65
 
7
66
  ## Installation
8
67
 
@@ -10,208 +69,546 @@ Inspired by [React Spectrum's `style` macro](https://react-spectrum.adobe.com/st
10
69
  npm install nestable-tailwind-variants
11
70
  ```
12
71
 
13
- ## Basic Usage
72
+ ## Core Concepts
14
73
 
15
- Define variants with string union types:
74
+ ### `$base` - Always-applied styles
16
75
 
17
- ```tsx
18
- import { ntv } from 'nestable-tailwind-variants';
76
+ The `$base` property defines styles that are always included in the output:
19
77
 
20
- const button = ntv<{ variant?: 'primary' | 'secondary' }>({
21
- default: 'px-4 py-2 rounded font-medium',
78
+ ```ts
79
+ interface CardProps {
80
+ variant: 'elevated' | 'flat';
81
+ }
82
+
83
+ const card = ntv<CardProps>({
84
+ $base: 'rounded-lg shadow-md p-4',
22
85
  variant: {
23
- primary: 'bg-blue-500 text-white',
24
- secondary: 'bg-gray-200 text-gray-800',
86
+ elevated: 'shadow-xl',
87
+ flat: 'shadow-none',
25
88
  },
26
89
  });
27
90
 
28
- button({ variant: 'primary' });
29
- // => 'px-4 py-2 rounded font-medium bg-blue-500 text-white'
91
+ card();
92
+ // => 'rounded-lg shadow-md p-4'
93
+
94
+ card({ variant: 'elevated' });
95
+ // => 'rounded-lg shadow-xl p-4'
96
+ // Note: shadow-md is replaced by shadow-xl via tailwind-merge
30
97
  ```
31
98
 
32
- Class conflicts are automatically resolved by [tailwind-merge](https://github.com/dcastil/tailwind-merge).
99
+ ### Variants - String-based selection
33
100
 
34
- Boolean conditions starting with `is` or `allows` can be used directly without nesting:
101
+ Define variants as objects mapping variant values to class names:
35
102
 
36
- ```tsx
37
- const button = ntv<{ isDisabled?: boolean }>({
38
- default: 'bg-blue-500',
39
- isDisabled: 'bg-gray-300 cursor-not-allowed',
103
+ ```ts
104
+ interface BadgeProps {
105
+ size: 'sm' | 'md' | 'lg';
106
+ color: 'info' | 'success' | 'warning';
107
+ }
108
+
109
+ const badge = ntv<BadgeProps>({
110
+ $base: 'px-2 py-1 rounded text-sm',
111
+ size: {
112
+ sm: 'text-xs px-1.5',
113
+ md: 'text-sm px-2',
114
+ lg: 'text-base px-3',
115
+ },
116
+ color: {
117
+ info: 'bg-blue-100 text-blue-800',
118
+ success: 'bg-green-100 text-green-800',
119
+ warning: 'bg-yellow-100 text-yellow-800',
120
+ },
40
121
  });
41
122
 
42
- button({ isDisabled: true });
43
- // => 'bg-gray-300 cursor-not-allowed'
123
+ badge({ size: 'lg', color: 'success' });
124
+ // => 'py-1 rounded text-base px-3 bg-green-100 text-green-800'
44
125
  ```
45
126
 
46
- ## Nested Conditions
127
+ ### Boolean Conditions - `is[A-Z]` / `allows[A-Z]` patterns
47
128
 
48
- Nest conditions to define styles that apply when multiple conditions are true.
129
+ Keys matching `is[A-Z]*` or `allows[A-Z]*` are treated as boolean conditions:
49
130
 
50
- ```tsx
51
- interface CardStyleProps {
52
- variant?: 'elevated';
53
- isHovered?: boolean;
131
+ ```ts
132
+ interface InputProps {
133
+ isFocused: boolean;
134
+ isDisabled: boolean;
135
+ isInvalid: boolean;
136
+ allowsClearing: boolean;
137
+ }
138
+
139
+ const input = ntv<InputProps>({
140
+ $base: 'border rounded px-3 py-2',
141
+ isFocused: 'ring-2 ring-blue-500 border-blue-500',
142
+ isDisabled: 'bg-gray-100 text-gray-400 cursor-not-allowed',
143
+ isInvalid: 'border-red-500 text-red-600',
144
+ allowsClearing: 'pr-8',
145
+ });
146
+
147
+ input({ isFocused: true });
148
+ // => 'border rounded px-3 py-2 ring-2 ring-blue-500 border-blue-500'
149
+
150
+ input({ isDisabled: true, isInvalid: true });
151
+ // => 'border rounded px-3 py-2 bg-gray-100 text-gray-400 cursor-not-allowed border-red-500 text-red-600'
152
+ ```
153
+
154
+ ### `$default` - Fallback styles
155
+
156
+ Use `$default` for styles applied when no conditions match:
157
+
158
+ ```ts
159
+ interface TextProps {
160
+ variant: 'primary' | 'danger';
54
161
  }
55
162
 
56
- const card = ntv<CardStyleProps>({
163
+ const text = ntv<TextProps>({
164
+ $default: 'text-gray-500',
57
165
  variant: {
58
- elevated: {
59
- default: 'shadow-md',
60
- isHovered: 'shadow-xl',
166
+ primary: 'text-blue-600',
167
+ danger: 'text-red-600',
168
+ },
169
+ });
170
+
171
+ text();
172
+ // => 'text-gray-500'
173
+
174
+ text({ variant: 'primary' });
175
+ // => 'text-blue-600'
176
+ ```
177
+
178
+ When `$default` is nested inside variants, they accumulate only when no conditions match at each level:
179
+
180
+ ```ts
181
+ interface Props {
182
+ variant: 'primary';
183
+ size: 'large';
184
+ }
185
+
186
+ const styles = ntv<Props>({
187
+ $base: 'base',
188
+ $default: 'root-default',
189
+ variant: {
190
+ $default: 'variant-default',
191
+ primary: {
192
+ size: {
193
+ $default: 'size-default',
194
+ large: 'size-large',
195
+ },
61
196
  },
62
197
  },
63
198
  });
64
199
 
65
- card({ variant: 'elevated' });
66
- // => 'shadow-md'
200
+ styles();
201
+ // => 'base root-default variant-default'
202
+ // No conditions matched, so $defaults accumulate
203
+
204
+ styles({ variant: 'primary' });
205
+ // => 'base size-default'
206
+ // variant matched, so only nested $default is applied
67
207
 
68
- card({ variant: 'elevated', isHovered: true });
69
- // => 'shadow-xl'
208
+ styles({ variant: 'primary', size: 'large' });
209
+ // => 'base size-large'
210
+ // Both matched, so no $defaults are applied
70
211
  ```
71
212
 
72
- When a condition matches at the same level, `default` is skipped. Conditions can be nested to any depth.
213
+ ## Nested Conditions
73
214
 
74
- ## Comparison with tailwind-variants
215
+ The core feature of nestable-tailwind-variants is the ability to nest conditions inside variants.
75
216
 
76
- With tailwind-variants, compound conditions require `compoundVariants`.
217
+ ### Boolean conditions inside variants
77
218
 
78
- **tailwind-variants:**
219
+ ```ts
220
+ interface ChipProps {
221
+ variant: 'filled' | 'outlined';
222
+ isSelected: boolean;
223
+ }
79
224
 
80
- ```tsx
81
- const button = tv({
82
- base: 'rounded font-medium',
83
- variants: {
84
- variant: {
85
- primary: 'bg-blue-500 text-white',
86
- secondary: 'bg-gray-200 text-gray-800',
225
+ const chip = ntv<ChipProps>({
226
+ $base: 'inline-flex items-center rounded-full px-3 py-1',
227
+ variant: {
228
+ filled: {
229
+ $default: 'bg-gray-200 text-gray-800',
230
+ isSelected: 'bg-blue-500 text-white',
231
+ },
232
+ outlined: {
233
+ $default: 'border border-gray-300 text-gray-800',
234
+ isSelected: 'border-blue-500 text-blue-500',
87
235
  },
88
- isHovered: { true: '' },
89
- isPressed: { true: '' },
90
236
  },
91
- compoundVariants: [
92
- { variant: 'primary', isHovered: true, class: 'bg-blue-600' },
93
- { variant: 'primary', isPressed: true, class: 'bg-blue-700' },
94
- { variant: 'secondary', isHovered: true, class: 'bg-gray-300' },
95
- { variant: 'secondary', isPressed: true, class: 'bg-gray-400' },
96
- ],
97
237
  });
238
+
239
+ chip({ variant: 'filled' });
240
+ // => 'inline-flex items-center rounded-full px-3 py-1 bg-gray-200 text-gray-800'
241
+
242
+ chip({ variant: 'filled', isSelected: true });
243
+ // => 'inline-flex items-center rounded-full px-3 py-1 bg-blue-500 text-white'
244
+
245
+ chip({ variant: 'outlined', isSelected: true });
246
+ // => 'inline-flex items-center rounded-full px-3 py-1 border-blue-500 text-blue-500'
98
247
  ```
99
248
 
100
- **nestable-tailwind-variants:**
249
+ ### Multi-level nesting
250
+
251
+ You can nest conditions to any depth:
252
+
253
+ ```ts
254
+ interface ButtonProps {
255
+ variant: 'primary';
256
+ isHovered: boolean;
257
+ isPressed: boolean;
258
+ isDisabled: boolean;
259
+ }
260
+
261
+ const button = ntv<ButtonProps>({
262
+ $base: 'px-4 py-2 rounded font-medium transition-colors',
263
+ variant: {
264
+ primary: {
265
+ $default: 'bg-blue-500 text-white',
266
+ isHovered: 'bg-blue-600',
267
+ isPressed: 'bg-blue-700',
268
+ isDisabled: {
269
+ $default: 'bg-blue-300 cursor-not-allowed',
270
+ isHovered: 'bg-blue-300', // Prevent hover effect when disabled
271
+ },
272
+ },
273
+ },
274
+ });
275
+
276
+ button({ variant: 'primary' });
277
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-500 text-white'
278
+
279
+ button({ variant: 'primary', isHovered: true });
280
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-600'
281
+
282
+ button({ variant: 'primary', isDisabled: true });
283
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-300 cursor-not-allowed'
284
+
285
+ button({ variant: 'primary', isDisabled: true, isHovered: true });
286
+ // => 'px-4 py-2 rounded font-medium transition-colors bg-blue-300'
287
+ ```
288
+
289
+ ## Composing Styles
290
+
291
+ Merge multiple style functions into one. Later functions take precedence:
292
+
293
+ ```ts
294
+ import { ntv, mergeNtv } from 'nestable-tailwind-variants';
295
+
296
+ interface BaseButtonProps {
297
+ size: 'sm' | 'md';
298
+ }
299
+
300
+ const baseButton = ntv<BaseButtonProps>({
301
+ $base: 'rounded font-medium transition-colors',
302
+ size: {
303
+ sm: 'px-3 py-1.5 text-sm',
304
+ md: 'px-4 py-2 text-base',
305
+ },
306
+ });
307
+
308
+ interface ColoredButtonProps {
309
+ variant: 'primary' | 'secondary';
310
+ }
311
+
312
+ const coloredButton = ntv<ColoredButtonProps>({
313
+ variant: {
314
+ primary: 'bg-blue-500 text-white hover:bg-blue-600',
315
+ secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
316
+ },
317
+ });
318
+
319
+ const button = mergeNtv(baseButton, coloredButton);
320
+
321
+ button({ size: 'md', variant: 'primary' });
322
+ // => 'rounded font-medium transition-colors px-4 py-2 text-base bg-blue-500 text-white hover:bg-blue-600'
323
+ ```
324
+
325
+ ## Passing Additional Classes
326
+
327
+ Pass additional classes using `class` or `className`:
328
+
329
+ ```ts
330
+ interface BoxProps {
331
+ variant: 'primary' | 'secondary';
332
+ }
333
+
334
+ const box = ntv<BoxProps>({
335
+ $base: 'p-4 rounded',
336
+ variant: {
337
+ primary: 'bg-blue-500',
338
+ secondary: 'bg-gray-200',
339
+ },
340
+ });
341
+
342
+ box({ variant: 'primary', class: 'mt-4 p-8' });
343
+ // => 'rounded bg-blue-500 mt-4 p-8'
344
+ // Note: p-8 overrides p-4 via tailwind-merge
345
+
346
+ box({ variant: 'secondary', className: 'shadow-lg' });
347
+ // => 'p-4 rounded bg-gray-200 shadow-lg'
348
+ ```
349
+
350
+ ## React Aria Components Integration
351
+
352
+ nestable-tailwind-variants is designed to work seamlessly with [React Aria Components render props](https://react-aria.adobe.com/styling#render-props):
101
353
 
102
354
  ```tsx
355
+ import {
356
+ Button as RACButton,
357
+ composeRenderProps,
358
+ type ButtonProps as RACButtonProps,
359
+ type ButtonRenderProps,
360
+ } from 'react-aria-components';
361
+ import { ntv } from 'nestable-tailwind-variants';
362
+
103
363
  interface ButtonStyleProps {
104
364
  variant?: 'primary' | 'secondary';
105
- isHovered?: boolean;
106
- isPressed?: boolean;
365
+ size?: 'sm' | 'md' | 'lg';
107
366
  }
108
367
 
109
- const button = ntv<ButtonStyleProps>({
110
- default: 'rounded font-medium',
368
+ const button = ntv<ButtonRenderProps & ButtonStyleProps>({
369
+ $base:
370
+ 'inline-flex items-center justify-center rounded-md px-4 py-2 font-medium transition-colors focus:outline-none',
111
371
  variant: {
112
372
  primary: {
113
- default: 'bg-blue-500 text-white',
373
+ $default: 'bg-blue-500 text-white',
114
374
  isHovered: 'bg-blue-600',
115
375
  isPressed: 'bg-blue-700',
376
+ isFocusVisible: 'ring-2 ring-blue-500 ring-offset-2',
116
377
  },
117
378
  secondary: {
118
- default: 'bg-gray-200 text-gray-800',
379
+ $default: 'bg-gray-200 text-gray-800',
119
380
  isHovered: 'bg-gray-300',
120
381
  isPressed: 'bg-gray-400',
382
+ isFocusVisible: 'ring-2 ring-gray-500 ring-offset-2',
121
383
  },
122
384
  },
385
+ size: {
386
+ sm: 'h-8 px-3 text-sm',
387
+ md: 'h-10 px-4 text-base',
388
+ lg: 'h-12 px-6 text-lg',
389
+ },
390
+ isDisabled: 'opacity-50 cursor-not-allowed',
123
391
  });
124
- ```
125
392
 
126
- Nesting groups related styles together, reflecting the logical hierarchy of conditions in your code.
393
+ function Button({
394
+ variant = 'primary',
395
+ size = 'md',
396
+ children,
397
+ ...props
398
+ }: RACButtonProps & ButtonStyleProps) {
399
+ return (
400
+ <RACButton
401
+ {...props}
402
+ className={composeRenderProps(props.className, (className, renderProps) =>
403
+ // renderProps includes isHovered, isPressed, isFocusVisible, isDisabled
404
+ button({ ...renderProps, variant, size, className }),
405
+ )}
406
+ >
407
+ {children}
408
+ </RACButton>
409
+ );
410
+ }
411
+ ```
127
412
 
128
- ## Composing Styles
413
+ ## Type Safety
129
414
 
130
- Combine multiple style functions using `composeNtv`:
415
+ The recommended approach is to define a props type and pass it as a type argument to `ntv`. This provides clear documentation, better IDE support, and ensures type safety:
131
416
 
132
- ```tsx
133
- import { ntv, composeNtv } from 'nestable-tailwind-variants';
417
+ ```ts
418
+ interface ButtonProps {
419
+ variant: 'primary' | 'secondary';
420
+ size: 'sm' | 'md' | 'lg';
421
+ isDisabled: boolean;
422
+ }
134
423
 
135
- const baseButton = ntv<{ size?: 'sm' | 'lg' }>({
136
- default: 'rounded font-medium',
424
+ const button = ntv<ButtonProps>({
425
+ $base: 'rounded',
426
+ variant: {
427
+ primary: 'bg-blue-500',
428
+ secondary: 'bg-gray-200',
429
+ },
137
430
  size: {
138
- sm: 'px-2 py-1 text-sm',
139
- lg: 'px-4 py-2 text-lg',
431
+ sm: 'text-sm',
432
+ md: 'text-base',
433
+ lg: 'text-lg',
140
434
  },
435
+ isDisabled: 'opacity-50',
141
436
  });
142
437
 
143
- const coloredButton = ntv<{ variant?: 'primary' | 'secondary' }>({
144
- variant: {
145
- primary: 'bg-blue-500 text-white',
146
- secondary: 'bg-gray-200 text-gray-800',
147
- },
148
- });
438
+ button({ variant: 'primary', size: 'lg' }); // ✅ OK
439
+ button({ variant: 'tertiary' }); // ❌ Error: 'tertiary' is not assignable
440
+ ```
149
441
 
150
- const button = composeNtv(baseButton, coloredButton);
442
+ Omitting the type argument when the scheme has keys disables type checking. Provide explicit type arguments when possible for better type safety.
151
443
 
152
- button({ size: 'lg', variant: 'primary' });
153
- // => 'rounded font-medium px-4 py-2 text-lg bg-blue-500 text-white'
154
- ```
444
+ ## Options
155
445
 
156
- ## API
446
+ `ntv` accepts an options object as the second argument. For `mergeNtv`, use `mergeNtvWithOptions` to pass options.
157
447
 
158
- ### `ntv<Props>(style)`
448
+ ### Disabling tailwind-merge
159
449
 
160
- Creates a style function from a nested style definition.
450
+ By default, [tailwind-merge](https://github.com/dcastil/tailwind-merge) is used to resolve class conflicts. To disable it, pass `{ twMerge: false }`.
161
451
 
162
- - `style` - Style definition object
163
- - `default` - Base styles (skipped when other conditions match at the same level)
164
- - `[variantKey]` - Style definitions for each variant value
165
- - `is*` / `allows*` - Boolean condition styles
166
- - Returns `(props: Partial<Props>) => string`
452
+ For `ntv`:
167
453
 
168
- ### `composeNtv(...fns)`
454
+ ```ts
455
+ const styles = ntv(
456
+ {
457
+ $base: 'p-4',
458
+ },
459
+ { twMerge: false },
460
+ );
169
461
 
170
- Composes multiple style functions into a single function.
462
+ styles({ class: 'p-8' });
463
+ // => 'p-4 p-8' (no merge, both classes kept)
464
+ ```
171
465
 
172
- - `fns` - Style functions to compose
173
- - Returns `(props: Partial<Props>) => string`
466
+ For `mergeNtvWithOptions`:
174
467
 
175
- ### `createNTV(options)`
468
+ ```ts
469
+ const styles = mergeNtvWithOptions(baseStyles, overrideStyles)({ twMerge: false });
470
+ ```
176
471
 
177
- Creates a customized `ntv` function.
472
+ ### Custom tailwind-merge configuration
178
473
 
179
- - `options.twMerge` (`boolean`, default: `true`) - Enable tailwind-merge
180
- - `options.twMergeConfig` (`object`) - Custom tailwind-merge configuration
181
- - Returns customized `ntv` function
474
+ If you've extended Tailwind with custom classes (e.g., custom font sizes via `@theme`), you need to tell tailwind-merge about them so it can resolve conflicts correctly:
182
475
 
183
- ```tsx
184
- const ntvNoMerge = createNTV({ twMerge: false });
476
+ ```css
477
+ /* app.css */
478
+ @theme {
479
+ --font-size-huge: 4rem;
480
+ }
481
+ ```
185
482
 
186
- const customNTV = createNTV({
483
+ For `ntv`:
484
+
485
+ ```ts
486
+ const heading = ntv(
487
+ { $base: 'text-huge' },
488
+ {
489
+ twMergeConfig: {
490
+ extend: {
491
+ classGroups: {
492
+ 'font-size': [{ text: ['huge'] }],
493
+ },
494
+ },
495
+ },
496
+ },
497
+ );
498
+
499
+ heading({ class: 'text-6xl' });
500
+ // => 'text-6xl' (text-huge is correctly replaced by text-6xl)
501
+ ```
502
+
503
+ For `mergeNtvWithOptions`:
504
+
505
+ ```ts
506
+ const styles = mergeNtvWithOptions(
507
+ baseStyles,
508
+ overrideStyles,
509
+ )({
187
510
  twMergeConfig: {
188
511
  extend: {
189
- theme: {
190
- shadow: ['100', '200', '300'],
512
+ classGroups: {
513
+ 'font-size': [{ text: ['huge'] }],
191
514
  },
192
515
  },
193
516
  },
194
517
  });
195
518
  ```
196
519
 
197
- ### `createComposeNtv(options)`
520
+ Without this configuration, tailwind-merge wouldn't recognize `text-huge` as a font-size class and would keep both `text-huge` and `text-6xl`.
198
521
 
199
- Creates a customized `composeNtv` function.
522
+ ### Pre-configured factories
200
523
 
201
- - `options.twMerge` (`boolean`, default: `true`) - Enable tailwind-merge
202
- - `options.twMergeConfig` (`object`) - Custom tailwind-merge configuration
203
- - Returns customized `composeNtv` function
524
+ Use `createNtv` or `createMergeNtv` to create functions with shared options:
204
525
 
205
- ```tsx
206
- const composeNtvNoMerge = createComposeNtv({ twMerge: false });
526
+ ```ts
527
+ import { createNtv, createMergeNtv, type TwMergeConfig } from 'nestable-tailwind-variants';
207
528
 
208
- const customComposeNtv = createComposeNtv({
209
- twMergeConfig: {
210
- extend: {
211
- theme: {
212
- shadow: ['100', '200', '300'],
529
+ const twMergeConfig: TwMergeConfig = {
530
+ extend: {
531
+ classGroups: {
532
+ 'font-size': [{ text: ['huge', 'tiny'] }],
533
+ },
534
+ },
535
+ };
536
+
537
+ const ntv = createNtv({ twMergeConfig });
538
+ const mergeNtv = createMergeNtv({ twMergeConfig });
539
+ ```
540
+
541
+ ## Tooling
542
+
543
+ ### Tailwind CSS IntelliSense
544
+
545
+ Add to `.vscode/settings.json` for [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) class autocomplete:
546
+
547
+ ```json
548
+ {
549
+ "tailwindCSS.experimental.classRegex": ["ntv\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
550
+ }
551
+ ```
552
+
553
+ ### prettier-plugin-tailwindcss
554
+
555
+ Add to your Prettier config for [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) automatic class sorting:
556
+
557
+ ```json
558
+ {
559
+ "tailwindFunctions": ["ntv"]
560
+ }
561
+ ```
562
+
563
+ ### eslint-plugin-better-tailwindcss
564
+
565
+ Add to your ESLint config for [eslint-plugin-better-tailwindcss](https://github.com/schoero/eslint-plugin-better-tailwindcss) linting:
566
+
567
+ ```js
568
+ import { getDefaultCallees } from 'eslint-plugin-better-tailwindcss/defaults';
569
+
570
+ export default [
571
+ {
572
+ settings: {
573
+ 'better-tailwindcss': {
574
+ callees: [...getDefaultCallees(), ['ntv', [{ match: 'objectValues' }]]],
213
575
  },
214
576
  },
215
577
  },
216
- });
578
+ ];
217
579
  ```
580
+
581
+ ## API Reference
582
+
583
+ ### Functions
584
+
585
+ | Function | Description |
586
+ | -------------------------------------------- | ------------------------------------------------------- |
587
+ | `ntv(scheme, options?)` | Create a style function from a scheme |
588
+ | `createNtv(options)` | Create a pre-configured `ntv` with default options |
589
+ | `mergeNtv(...styleFns)` | Merge multiple style functions into one |
590
+ | `mergeNtvWithOptions(...styleFns)(options?)` | Merge style functions with custom options |
591
+ | `createMergeNtv(options)` | Create a pre-configured `mergeNtv` with default options |
592
+
593
+ ### Types
594
+
595
+ | Type | Description |
596
+ | --------------- | ------------------------------------------------------------------------------------------- |
597
+ | `ClassValue` | Valid class value (string, array, or object) |
598
+ | `ClassProp` | Props for runtime class override (`{ class?: ClassValue }` or `{ className?: ClassValue }`) |
599
+ | `NtvOptions` | Options for ntv functions (`{ twMerge?: boolean; twMergeConfig?: TwMergeConfig }`) |
600
+ | `TwMergeConfig` | Configuration object for tailwind-merge |
601
+
602
+ ### Scheme Properties
603
+
604
+ | Property | Description |
605
+ | -------------- | ---------------------------------------------------- |
606
+ | `$base` | Classes always applied (top-level only) |
607
+ | `$default` | Fallback classes when no conditions match |
608
+ | `is[A-Z]*` | Boolean condition (e.g., `isSelected`, `isDisabled`) |
609
+ | `allows[A-Z]*` | Boolean condition (e.g., `allowsRemoving`) |
610
+ | `[key]` | Variant object mapping values to classes |
611
+
612
+ ## License
613
+
614
+ MIT