slot-variants 1.4.0 → 1.6.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 +154 -12
- package/SKILL.md +77 -395
- package/dist/eslint-plugin.cjs +148 -51
- package/dist/eslint-plugin.d.ts +2 -0
- package/dist/eslint-plugin.js +148 -51
- package/dist/index.cjs +128 -12
- package/dist/index.d.ts +66 -25
- package/dist/index.js +127 -12
- package/package.json +2 -2
- package/dist/src/cn.d.ts +0 -14
- package/dist/src/eslint-plugin.d.ts +0 -25
- package/dist/src/index.d.ts +0 -2
- package/dist/src/sv.d.ts +0 -189
- package/dist/test/cn.test.d.ts +0 -1
- package/dist/test/eslint-plugin.test.d.ts +0 -1
- package/dist/test/sv.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -12,10 +12,11 @@ npm install slot-variants
|
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
14
14
|
|
|
15
|
-
`slot-variants` exports
|
|
15
|
+
`slot-variants` exports three functions:
|
|
16
16
|
|
|
17
17
|
- **`sv()`** - creates variant-based class name generators with optional slots
|
|
18
18
|
- **`cn()`** - a utility for conditionally merging class names
|
|
19
|
+
- **`createSV()`** - builds a pre-configured `sv()` with shared config defaults
|
|
19
20
|
|
|
20
21
|
`sv()` is a drop-in replacement for [CVA](https://cva.style/) (just rename `cva` to `sv`) and covers the core feature set of [tailwind-variants](https://www.tailwind-variants.org/) (`tv`) with a simpler API. See [Migrating from CVA / tailwind-variants](#migrating-from-cva--tailwind-variants) for details.
|
|
21
22
|
|
|
@@ -68,7 +69,7 @@ cn(['foo', ['bar', 'baz']]); // 'foo bar baz'
|
|
|
68
69
|
cn({ foo: true, bar: false, baz: true }); // 'foo baz'
|
|
69
70
|
|
|
70
71
|
// Mixed
|
|
71
|
-
cn('base', ['responsive'], { active: true }); // 'base active
|
|
72
|
+
cn('base', ['responsive'], { active: true }); // 'base responsive active'
|
|
72
73
|
|
|
73
74
|
// Falsy values are filtered out
|
|
74
75
|
cn('foo', null, undefined, false, 'bar'); // 'foo bar'
|
|
@@ -384,6 +385,21 @@ button({ intent: 'primary', size: 'lg' }); // OK
|
|
|
384
385
|
button({ size: 'lg' }); // Throws: Missing required variant: "intent"
|
|
385
386
|
```
|
|
386
387
|
|
|
388
|
+
Pass `true` to make every variant required, or `false` to require none:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const button = sv('btn', {
|
|
392
|
+
variants: {
|
|
393
|
+
size: { sm: 'text-sm', lg: 'text-lg' },
|
|
394
|
+
intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
|
|
395
|
+
},
|
|
396
|
+
requiredVariants: true
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
button({ size: 'sm', intent: 'primary' }); // OK
|
|
400
|
+
button({ size: 'sm' }); // Throws: Missing required variant: "intent"
|
|
401
|
+
```
|
|
402
|
+
|
|
387
403
|
### Presets
|
|
388
404
|
|
|
389
405
|
Presets are predefined named combinations of variant values. Use them to create reusable variant shortcuts:
|
|
@@ -597,6 +613,58 @@ compoundSlots: [
|
|
|
597
613
|
]
|
|
598
614
|
```
|
|
599
615
|
|
|
616
|
+
### Multi Slots
|
|
617
|
+
|
|
618
|
+
By default each slot in the result object is a plain class string. The
|
|
619
|
+
`multiSlots` option turns the listed slots into reconfigurable functions
|
|
620
|
+
instead. A slot function accepts variant prop overrides and a
|
|
621
|
+
`class`/`className` override, and returns that slot's class string.
|
|
622
|
+
|
|
623
|
+
This is designed for cases where a single slot is rendered multiple times
|
|
624
|
+
with different props — for example a list of items where each item needs
|
|
625
|
+
its own variant values — so the same slot can be re-evaluated per use
|
|
626
|
+
without recreating the whole variant function:
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
const card = sv('border', {
|
|
630
|
+
slots: {
|
|
631
|
+
header: 'font-bold',
|
|
632
|
+
body: 'py-4'
|
|
633
|
+
},
|
|
634
|
+
variants: {
|
|
635
|
+
size: {
|
|
636
|
+
sm: { base: 'p-2', header: 'text-sm' },
|
|
637
|
+
lg: { base: 'p-6', header: 'text-lg' }
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
multiSlots: ['header']
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const result = card({ size: 'sm' });
|
|
644
|
+
// result.base -> 'border p-2' (plain string)
|
|
645
|
+
// result.body -> 'py-4' (plain string)
|
|
646
|
+
// result.header -> function
|
|
647
|
+
|
|
648
|
+
result.header(); // 'font-bold text-sm'
|
|
649
|
+
result.header({ size: 'lg' }); // 'font-bold text-lg'
|
|
650
|
+
result.header({ class: 'mt-2' }); // 'font-bold text-sm mt-2'
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Slots not listed in `multiSlots` stay plain strings. Pass `true` to make
|
|
654
|
+
every slot a function, or `false` (the default) to keep them all strings:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
const card = sv('border', {
|
|
658
|
+
slots: { header: 'font-bold', body: 'py-4' },
|
|
659
|
+
multiSlots: true
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const { base, header, body } = card();
|
|
663
|
+
base(); // 'border'
|
|
664
|
+
header(); // 'font-bold'
|
|
665
|
+
body(); // 'py-4'
|
|
666
|
+
```
|
|
667
|
+
|
|
600
668
|
### Class Override at Runtime
|
|
601
669
|
|
|
602
670
|
Append additional classes at call time using `class` or `className`:
|
|
@@ -666,10 +734,66 @@ const button = sv('px-4 py-2 bg-blue-500', {
|
|
|
666
734
|
|
|
667
735
|
The `postProcess` function is applied to each slot's final class string independently.
|
|
668
736
|
|
|
737
|
+
### Shared Defaults with `createSV()`
|
|
738
|
+
|
|
739
|
+
`createSV(defaults)` returns a pre-configured `sv()` that merges `defaults` into every config-based call. This avoids repeating the same options — most commonly `postProcess: twMerge` — across every component:
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
import { createSV } from 'slot-variants';
|
|
743
|
+
import { twMerge } from 'tailwind-merge';
|
|
744
|
+
|
|
745
|
+
export const customSV = createSV({
|
|
746
|
+
postProcess: twMerge,
|
|
747
|
+
cacheSize: 512
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// twMerge is applied without restating it per component
|
|
751
|
+
const button = customSV('px-4 py-2 bg-blue-500', {
|
|
752
|
+
variants: {
|
|
753
|
+
size: {
|
|
754
|
+
sm: 'px-2 py-1 text-sm',
|
|
755
|
+
lg: 'px-6 py-3 text-lg'
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`defaults` accepts **any** config option (`base`, `variants`, `slots`, `compoundVariants`, `cacheSize`, `introspection`, `postProcess`, …). The merge is shallow and **a per-call value always wins** over the matching default — there is no deep merging of variants or compound rules:
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
const customSV = createSV({ postProcess: twMerge });
|
|
765
|
+
|
|
766
|
+
// This component opts out of twMerge entirely
|
|
767
|
+
const raw = customSV('btn', {
|
|
768
|
+
variants: { size: { sm: 'text-sm' } },
|
|
769
|
+
postProcess: (className) => className // overrides the default
|
|
770
|
+
});
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
Calls with no config object are forwarded straight to `cn()`-style merging and never see the defaults:
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
const customSV = createSV({ postProcess: (c) => c.toUpperCase() });
|
|
777
|
+
|
|
778
|
+
customSV('flex', 'items-center'); // 'flex items-center' — defaults are skipped
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
A factory-level `introspection: true` is reflected in each component's return type, so the [introspection](#introspection) API is available without setting the flag on every config. A per-call `introspection: false` opts an individual component back out.
|
|
782
|
+
|
|
669
783
|
### Caching
|
|
670
784
|
|
|
671
785
|
Results are cached automatically for performance. The default cache size is **256** entries.
|
|
672
786
|
|
|
787
|
+
Each cache entry corresponds to one distinct combination of resolved variant values. The largest number of combinations a config can produce is the product, over every variant, of its value count plus one — the `+ 1` counts the variant being left unset:
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
maxEntries = (values₁ + 1) × (values₂ + 1) × ... × (valuesₙ + 1)
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
For example, four variants with three values each yield `(3 + 1) ** 4 = 256` combinations — exactly the default. A config whose `maxEntries` is at or below its `cacheSize` never evicts, so raising `cacheSize` past that point has no effect.
|
|
794
|
+
|
|
795
|
+
The `+ 1` is dropped for any variant that cannot actually be left unset — one listed in `requiredVariants` (or all of them when `requiredVariants` is `true`), or one with a static `defaultVariants` value that always fills it in. A function-based default keeps the `+ 1`, since it may return `undefined`. The `getMaxEntries()` introspection method computes this exact count for a given config — see [Introspection](#introspection).
|
|
796
|
+
|
|
673
797
|
```typescript
|
|
674
798
|
const button = sv('btn', {
|
|
675
799
|
variants: {
|
|
@@ -719,10 +843,12 @@ button.slotKeys; // ['base', 'icon']
|
|
|
719
843
|
button.slots; // { icon: 'w-4 h-4' }
|
|
720
844
|
button.defaultVariants; // { size: 'sm' }
|
|
721
845
|
button.requiredVariants; // ['intent']
|
|
846
|
+
button.multiSlots; // [] (slot names exposed as functions)
|
|
722
847
|
button.presetKeys; // ['cta']
|
|
723
848
|
button.presets; // { cta: { size: 'lg', intent: 'primary' } }
|
|
724
849
|
button.getVariantValues('size'); // ['sm', 'lg']
|
|
725
850
|
button.getVariantValues('intent'); // ['primary', 'danger']
|
|
851
|
+
button.getMaxEntries(); // 4 — distinct variant combinations
|
|
726
852
|
button.getCacheSize(); // current number of cached entries
|
|
727
853
|
button.clearCache(); // clear all cached entries
|
|
728
854
|
```
|
|
@@ -866,6 +992,7 @@ When used on an `sv()` definition without slots, `SlotClassProps` resolves to `{
|
|
|
866
992
|
| `VariantProps<T, E>` | Extracts variant props from an `sv()` return type, optionally excluding keys in `E` |
|
|
867
993
|
| `VariantValue<T, K>` | Extracts the value union for a single variant key `K`, without `undefined` |
|
|
868
994
|
| `SlotClassProps<T>` | Extracts the per-slot class injection shape from an `sv()` return type |
|
|
995
|
+
| `SV<DI>` | The shape of an `sv()` function, with the factory's introspection default `DI`; the return type of `createSV()` |
|
|
869
996
|
|
|
870
997
|
### Return Type
|
|
871
998
|
|
|
@@ -884,7 +1011,8 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
|
|
|
884
1011
|
| `compoundVariants` | `Array` | Additional classes applied when multiple variant conditions match |
|
|
885
1012
|
| `compoundSlots` | `Array` | Classes applied to multiple slots based on variant conditions |
|
|
886
1013
|
| `defaultVariants` | `Object` | Default values for variants (static values or functions) |
|
|
887
|
-
| `requiredVariants` | `string[]` | Variant names that must be provided at call time |
|
|
1014
|
+
| `requiredVariants` | `string[] \| boolean` | Variant names that must be provided at call time; `true` makes every variant required, `false` none |
|
|
1015
|
+
| `multiSlots` | `string[] \| boolean` | Slot names exposed as reconfigurable functions instead of strings; `true` makes every slot a function, `false` none |
|
|
888
1016
|
| `presets` | `Record<string, Partial<VariantProps>>` | Named combinations of variant values selectable via `preset` prop |
|
|
889
1017
|
| `postProcess` | `(className: string) => string` | Custom transformation applied to final class strings |
|
|
890
1018
|
| `cacheSize` | `number` | Maximum number of cached results (default: `256`) |
|
|
@@ -896,15 +1024,17 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
|
|
|
896
1024
|
|
|
897
1025
|
### Rules
|
|
898
1026
|
|
|
899
|
-
- **`slot-variants/no-conflicting-classes`** — flags class tokens that collide within the output of an `sv()` or `cn()` call: both exact-duplicate tokens that will appear more than once and distinct tokens that target the same Tailwind-style utility namespace (e.g. `w-100` and `w-200`). For `sv()`, detects collisions within `base`, across different variant keys, inside compound variants and compound slots, between `base` and a variant value, and within a single literal. For `cn()` (and the cn-style calling convention of `sv()` without a config), flags collisions across args, inside arrays, template literals, or within a single literal. Tokens with different variant prefixes (`w-100` vs `hover:w-200`) don't conflict,
|
|
1027
|
+
- **`slot-variants/no-conflicting-classes`** — flags class tokens that collide within the output of an `sv()` or `cn()` call: both exact-duplicate tokens that will appear more than once and distinct tokens that target the same Tailwind-style utility namespace (e.g. `w-100` and `w-200`). For `sv()`, detects collisions within `base`, across different variant keys, inside compound variants and compound slots, between `base` and a variant value, and within a single literal. For `cn()` (and the cn-style calling convention of `sv()` without a config), flags collisions across args, inside arrays, template literals, or within a single literal. Tokens with different variant prefixes (`w-100` vs `hover:w-200`) don't conflict, a leading or trailing `!` important marker (`!w-100`, `w-100!`) is ignored when computing the namespace, and tokens that only co-occur across mutually-exclusive variant values are skipped.
|
|
900
1028
|
|
|
901
|
-
- **`slot-variants/no-dynamic-classes`** — flags class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Only string literals, template literals without expressions, flat arrays of those in config, and explicit `undefined` config class values are accepted, and config objects must use static keys (no spreads, no computed keys). Identifiers, member access, calls, spreads, non-string literals, templates with expressions, nested config arrays, and runtime conditional records are reported. Non-class-bearing config keys (`defaultVariants`, `presets`, `requiredVariants`, `cacheSize`, `postProcess`, `introspection`) are not validated, and runtime variant matchers inside compound entries are left alone — only the `class`/`className` value (and the `slots` array of `compoundSlots`) is checked.
|
|
1029
|
+
- **`slot-variants/no-dynamic-classes`** — flags class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Only string literals, template literals without expressions, flat arrays of those in config, and explicit `undefined` config class values are accepted, and config objects must use static keys (no spreads, no computed keys). Identifiers, member access, calls, spreads, non-string literals, templates with expressions, nested config arrays, and runtime conditional records are reported. Non-class-bearing config keys (`defaultVariants`, `presets`, `requiredVariants`, `multiSlots`, `cacheSize`, `postProcess`, `introspection`) are not validated, and runtime variant matchers inside compound entries are left alone — only the `class`/`className` value (and the `slots` array of `compoundSlots`) is checked.
|
|
902
1030
|
|
|
903
|
-
- **`slot-variants/no-empty-classes`** — flags empty class values — empty strings, empty arrays, and empty objects — at any class-bearing position reachable from an `sv()` or `cn()` call, plus zero-argument `sv()` / `cn()` calls themselves (which always produce an empty class string). Reports empties in positional arguments (and inside arrays nested in those), in `base`, in variants including slot-keyed variant branches, in the `class`/`className` of `compoundVariants` and `compoundSlots` entries, and in the top-level `slots`, `variants`, `compoundVariants`, and `compoundSlots` containers themselves. Inside an `sv()` config, an empty string is still allowed as a direct `slots[key]` value — declaring a slot with no default classes is a valid use case.
|
|
1031
|
+
- **`slot-variants/no-empty-classes`** — flags empty class values — empty strings, empty arrays, and empty objects — at any class-bearing position reachable from an `sv()` or `cn()` call, plus zero-argument `sv()` / `cn()` calls themselves (which always produce an empty class string). Reports empties in positional arguments (and inside arrays nested in those), in `base`, in variants including slot-keyed variant branches, in the `class`/`className` of `compoundVariants` and `compoundSlots` entries, and in the top-level `slots`, `variants`, `compoundVariants`, and `compoundSlots` containers themselves. Inside an `sv()` config, an empty string is still allowed as a direct `slots[key]` value — declaring a slot with no default classes is a valid use case. **Partially auto-fixable**: `eslint --fix` removes an empty positional argument, an empty class-array element, or an empty top-level config property (`base`, `slots`, `variants`, `compoundVariants`, `compoundSlots`), along with its comma, when other items remain in that list or config; empties at other positions are reported without a fix.
|
|
904
1032
|
|
|
905
1033
|
- **`slot-variants/no-redundant-spaces`** — flags class strings whose whitespace isn't canonical. Inside a class string, whitespace is canonical only as a single ASCII space between two non-whitespace tokens, so leading or trailing whitespace, repeated spaces, and non-space whitespace (tabs, newlines, etc.) are reported. The rule walks every string and expressionless template literal reachable from a call's arguments — including values nested inside arrays and objects — and bails silently on dynamic expressions. **Auto-fixable**: `eslint --fix` rewrites each offending literal in place, preserving its original quote style.
|
|
906
1034
|
|
|
907
|
-
- **`slot-variants/no-shared-tokens`** — flags class tokens that appear in every value of an exhaustively-covered variant, where “exhaustive” means the variant has a statically defined default value
|
|
1035
|
+
- **`slot-variants/no-shared-tokens`** — flags class tokens that appear in every value of an exhaustively-covered variant, where “exhaustive” means the variant has a statically defined default value, is listed in `requiredVariants`, or `requiredVariants` is `true` (every variant required). Those tokens are constant in the rendered output, so they belong in `base` or the corresponding `slots[slot]` entry rather than being repeated in every variant value. The rule only analyzes `sv()` calls with a config, compares tokens per-slot, skips non-exhaustive variants, single-value variants, boolean shorthand, undefined or dynamic defaults, and dynamic or partially-analyzable variant value records, and reports every repeated occurrence that should be lifted out.
|
|
1036
|
+
|
|
1037
|
+
- **`slot-variants/require-top-level-config`** — flags `sv()` calls made with a config object that aren't at the module top level. The config form compiles the variant function and seeds its cache once; nesting it inside a function (a component body, an arrow, an object method, etc.) rebuilds that work — and throws away the variant cache — on every call, so the config form belongs at module scope. The cn-style calling convention of `sv()` (and every `cn()` call) carries no config and is left alone, as are calls inside top-level blocks or conditionals, which still run only at module load.
|
|
908
1038
|
|
|
909
1039
|
Only calls where `sv` or `cn` is a named import from `'slot-variants'` are analyzed. `no-conflicting-classes` skips dynamic inputs silently to avoid false positives; `no-dynamic-classes` is the opposite — it flags exactly those positions so the static analyzer can fully reason about every call. `no-shared-tokens` sits between them: it needs a fully statically analyzable, exhaustive variant before it can prove a token is constant across every value. `no-empty-classes` and `no-redundant-spaces` are independent and complement the structural rules: they cover empty and badly-shaped literals reachable from a call's arguments, regardless of whether the surrounding call is fully static.
|
|
910
1040
|
|
|
@@ -931,7 +1061,8 @@ export default [
|
|
|
931
1061
|
'slot-variants/no-dynamic-classes': 'error',
|
|
932
1062
|
'slot-variants/no-empty-classes': 'error',
|
|
933
1063
|
'slot-variants/no-redundant-spaces': 'error',
|
|
934
|
-
'slot-variants/no-shared-tokens': 'error'
|
|
1064
|
+
'slot-variants/no-shared-tokens': 'error',
|
|
1065
|
+
'slot-variants/require-top-level-config': 'error'
|
|
935
1066
|
}
|
|
936
1067
|
}
|
|
937
1068
|
];
|
|
@@ -947,7 +1078,8 @@ export default [
|
|
|
947
1078
|
"slot-variants/no-dynamic-classes": "error",
|
|
948
1079
|
"slot-variants/no-empty-classes": "error",
|
|
949
1080
|
"slot-variants/no-redundant-spaces": "error",
|
|
950
|
-
"slot-variants/no-shared-tokens": "error"
|
|
1081
|
+
"slot-variants/no-shared-tokens": "error",
|
|
1082
|
+
"slot-variants/require-top-level-config": "error"
|
|
951
1083
|
}
|
|
952
1084
|
}
|
|
953
1085
|
```
|
|
@@ -1111,11 +1243,11 @@ Key differences to be aware of:
|
|
|
1111
1243
|
|
|
1112
1244
|
| Feature | tailwind-variants | slot-variants |
|
|
1113
1245
|
| --- | --- | --- |
|
|
1114
|
-
| Slot return type |
|
|
1246
|
+
| Slot return type | Always functions: `slot({ class: '...' })` | Strings by default; functions for slots listed in `multiSlots` |
|
|
1115
1247
|
| `extend` (composition) | Supported | Not supported |
|
|
1116
|
-
| Built-in `twMerge` | Enabled by default | Use `postProcess: twMerge` |
|
|
1248
|
+
| Built-in `twMerge` | Enabled by default | Use `postProcess: twMerge`, globally via `createSV` |
|
|
1117
1249
|
|
|
1118
|
-
**Slot return type** is the most significant difference. In `tv()`, each slot returns a function that can accept additional props. In `sv()`, slots resolve to strings directly — use the `class` prop with a slot object for per-slot overrides:
|
|
1250
|
+
**Slot return type** is the most significant difference. In `tv()`, each slot returns a function that can accept additional props. In `sv()`, slots resolve to strings directly — use the `class` prop with a slot object for per-slot overrides, or list a slot in [`multiSlots`](#multi-slots) to expose it as a `tv`-style reconfigurable function:
|
|
1119
1251
|
|
|
1120
1252
|
```typescript
|
|
1121
1253
|
// tailwind-variants
|
|
@@ -1139,3 +1271,13 @@ const button = sv({
|
|
|
1139
1271
|
postProcess: twMerge
|
|
1140
1272
|
});
|
|
1141
1273
|
```
|
|
1274
|
+
|
|
1275
|
+
To enable it globally — the closest equivalent to `tv`'s default `twMerge` — wrap it once with [`createSV()`](#shared-defaults-with-createsv) and import the result everywhere:
|
|
1276
|
+
|
|
1277
|
+
```typescript
|
|
1278
|
+
// custom-sv.ts
|
|
1279
|
+
import { createSV } from 'slot-variants';
|
|
1280
|
+
import { twMerge } from 'tailwind-merge';
|
|
1281
|
+
|
|
1282
|
+
export const customSV = createSV({ postProcess: twMerge });
|
|
1283
|
+
```
|