slot-variants 1.3.0 → 1.5.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 +106 -16
- package/SKILL.md +200 -0
- package/dist/eslint-plugin.cjs +278 -97
- package/dist/eslint-plugin.d.ts +6 -5
- package/dist/eslint-plugin.js +278 -97
- package/dist/index.cjs +442 -192
- package/dist/index.d.ts +50 -29
- package/dist/index.js +442 -192
- package/package.json +17 -13
- package/AGENTS.md +0 -530
- package/dist/src/cn.d.ts +0 -14
- package/dist/src/eslint-plugin.d.ts +0 -24
- package/dist/src/index.d.ts +0 -2
- package/dist/src/sv.d.ts +0 -187
- 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
|
@@ -384,6 +384,21 @@ button({ intent: 'primary', size: 'lg' }); // OK
|
|
|
384
384
|
button({ size: 'lg' }); // Throws: Missing required variant: "intent"
|
|
385
385
|
```
|
|
386
386
|
|
|
387
|
+
Pass `true` to make every variant required, or `false` to require none:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const button = sv('btn', {
|
|
391
|
+
variants: {
|
|
392
|
+
size: { sm: 'text-sm', lg: 'text-lg' },
|
|
393
|
+
intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
|
|
394
|
+
},
|
|
395
|
+
requiredVariants: true
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
button({ size: 'sm', intent: 'primary' }); // OK
|
|
399
|
+
button({ size: 'sm' }); // Throws: Missing required variant: "intent"
|
|
400
|
+
```
|
|
401
|
+
|
|
387
402
|
### Presets
|
|
388
403
|
|
|
389
404
|
Presets are predefined named combinations of variant values. Use them to create reusable variant shortcuts:
|
|
@@ -597,6 +612,58 @@ compoundSlots: [
|
|
|
597
612
|
]
|
|
598
613
|
```
|
|
599
614
|
|
|
615
|
+
### Multi Slots
|
|
616
|
+
|
|
617
|
+
By default each slot in the result object is a plain class string. The
|
|
618
|
+
`multiSlots` option turns the listed slots into reconfigurable functions
|
|
619
|
+
instead. A slot function accepts variant prop overrides and a
|
|
620
|
+
`class`/`className` override, and returns that slot's class string.
|
|
621
|
+
|
|
622
|
+
This is designed for cases where a single slot is rendered multiple times
|
|
623
|
+
with different props — for example a list of items where each item needs
|
|
624
|
+
its own variant values — so the same slot can be re-evaluated per use
|
|
625
|
+
without recreating the whole variant function:
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
const card = sv('border', {
|
|
629
|
+
slots: {
|
|
630
|
+
header: 'font-bold',
|
|
631
|
+
body: 'py-4'
|
|
632
|
+
},
|
|
633
|
+
variants: {
|
|
634
|
+
size: {
|
|
635
|
+
sm: { base: 'p-2', header: 'text-sm' },
|
|
636
|
+
lg: { base: 'p-6', header: 'text-lg' }
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
multiSlots: ['header']
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const result = card({ size: 'sm' });
|
|
643
|
+
// result.base -> 'border p-2' (plain string)
|
|
644
|
+
// result.body -> 'py-4' (plain string)
|
|
645
|
+
// result.header -> function
|
|
646
|
+
|
|
647
|
+
result.header(); // 'font-bold text-sm'
|
|
648
|
+
result.header({ size: 'lg' }); // 'font-bold text-lg'
|
|
649
|
+
result.header({ class: 'mt-2' }); // 'font-bold text-sm mt-2'
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Slots not listed in `multiSlots` stay plain strings. Pass `true` to make
|
|
653
|
+
every slot a function, or `false` (the default) to keep them all strings:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
const card = sv('border', {
|
|
657
|
+
slots: { header: 'font-bold', body: 'py-4' },
|
|
658
|
+
multiSlots: true
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const { base, header, body } = card();
|
|
662
|
+
base(); // 'border'
|
|
663
|
+
header(); // 'font-bold'
|
|
664
|
+
body(); // 'py-4'
|
|
665
|
+
```
|
|
666
|
+
|
|
600
667
|
### Class Override at Runtime
|
|
601
668
|
|
|
602
669
|
Append additional classes at call time using `class` or `className`:
|
|
@@ -668,7 +735,17 @@ The `postProcess` function is applied to each slot's final class string independ
|
|
|
668
735
|
|
|
669
736
|
### Caching
|
|
670
737
|
|
|
671
|
-
Results are cached automatically for performance. The default cache size is **256** entries.
|
|
738
|
+
Results are cached automatically for performance. The default cache size is **256** entries.
|
|
739
|
+
|
|
740
|
+
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:
|
|
741
|
+
|
|
742
|
+
```
|
|
743
|
+
maxEntries = (values₁ + 1) × (values₂ + 1) × ... × (valuesₙ + 1)
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
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.
|
|
747
|
+
|
|
748
|
+
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).
|
|
672
749
|
|
|
673
750
|
```typescript
|
|
674
751
|
const button = sv('btn', {
|
|
@@ -719,10 +796,12 @@ button.slotKeys; // ['base', 'icon']
|
|
|
719
796
|
button.slots; // { icon: 'w-4 h-4' }
|
|
720
797
|
button.defaultVariants; // { size: 'sm' }
|
|
721
798
|
button.requiredVariants; // ['intent']
|
|
799
|
+
button.multiSlots; // [] (slot names exposed as functions)
|
|
722
800
|
button.presetKeys; // ['cta']
|
|
723
801
|
button.presets; // { cta: { size: 'lg', intent: 'primary' } }
|
|
724
802
|
button.getVariantValues('size'); // ['sm', 'lg']
|
|
725
803
|
button.getVariantValues('intent'); // ['primary', 'danger']
|
|
804
|
+
button.getMaxEntries(); // 4 — distinct variant combinations
|
|
726
805
|
button.getCacheSize(); // current number of cached entries
|
|
727
806
|
button.clearCache(); // clear all cached entries
|
|
728
807
|
```
|
|
@@ -884,7 +963,8 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
|
|
|
884
963
|
| `compoundVariants` | `Array` | Additional classes applied when multiple variant conditions match |
|
|
885
964
|
| `compoundSlots` | `Array` | Classes applied to multiple slots based on variant conditions |
|
|
886
965
|
| `defaultVariants` | `Object` | Default values for variants (static values or functions) |
|
|
887
|
-
| `requiredVariants` | `string[]` | Variant names that must be provided at call time |
|
|
966
|
+
| `requiredVariants` | `string[] \| boolean` | Variant names that must be provided at call time; `true` makes every variant required, `false` none |
|
|
967
|
+
| `multiSlots` | `string[] \| boolean` | Slot names exposed as reconfigurable functions instead of strings; `true` makes every slot a function, `false` none |
|
|
888
968
|
| `presets` | `Record<string, Partial<VariantProps>>` | Named combinations of variant values selectable via `preset` prop |
|
|
889
969
|
| `postProcess` | `(className: string) => string` | Custom transformation applied to final class strings |
|
|
890
970
|
| `cacheSize` | `number` | Maximum number of cached results (default: `256`) |
|
|
@@ -896,20 +976,30 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
|
|
|
896
976
|
|
|
897
977
|
### Rules
|
|
898
978
|
|
|
899
|
-
- **`slot-variants/no-
|
|
979
|
+
- **`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
980
|
|
|
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.
|
|
981
|
+
- **`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
982
|
|
|
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.
|
|
983
|
+
- **`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
984
|
|
|
905
|
-
- **`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.
|
|
985
|
+
- **`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
986
|
|
|
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
|
|
987
|
+
- **`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.
|
|
908
988
|
|
|
909
|
-
Only calls where `sv` or `cn` is a named import from `'slot-variants'` are analyzed. `no-
|
|
989
|
+
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
990
|
|
|
911
991
|
### ESLint (flat config)
|
|
912
992
|
|
|
993
|
+
Use the `recommended` preset to enable every rule at `error` in one line:
|
|
994
|
+
|
|
995
|
+
```js
|
|
996
|
+
import svPlugin from 'slot-variants/eslint-plugin';
|
|
997
|
+
|
|
998
|
+
export default [svPlugin.configs.recommended];
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Or wire each rule by hand if you want per-rule control:
|
|
1002
|
+
|
|
913
1003
|
```js
|
|
914
1004
|
import svPlugin from 'slot-variants/eslint-plugin';
|
|
915
1005
|
|
|
@@ -917,7 +1007,7 @@ export default [
|
|
|
917
1007
|
{
|
|
918
1008
|
plugins: { 'slot-variants': svPlugin },
|
|
919
1009
|
rules: {
|
|
920
|
-
'slot-variants/no-
|
|
1010
|
+
'slot-variants/no-conflicting-classes': 'error',
|
|
921
1011
|
'slot-variants/no-dynamic-classes': 'error',
|
|
922
1012
|
'slot-variants/no-empty-classes': 'error',
|
|
923
1013
|
'slot-variants/no-redundant-spaces': 'error',
|
|
@@ -933,7 +1023,7 @@ export default [
|
|
|
933
1023
|
{
|
|
934
1024
|
"jsPlugins": ["slot-variants/eslint-plugin"],
|
|
935
1025
|
"rules": {
|
|
936
|
-
"slot-variants/no-
|
|
1026
|
+
"slot-variants/no-conflicting-classes": "error",
|
|
937
1027
|
"slot-variants/no-dynamic-classes": "error",
|
|
938
1028
|
"slot-variants/no-empty-classes": "error",
|
|
939
1029
|
"slot-variants/no-redundant-spaces": "error",
|
|
@@ -960,7 +1050,7 @@ const button = sv({
|
|
|
960
1050
|
cn('flex items-center', 'flex'); // 'flex' duplicated across args
|
|
961
1051
|
```
|
|
962
1052
|
|
|
963
|
-
`no-
|
|
1053
|
+
`no-conflicting-classes` reports `flex` on the `base` literal and on both variant values; for the `cn()` call, both occurrences of `'flex'` are flagged. Move the shared class into `base` — or use compound variants — so each class has a single source.
|
|
964
1054
|
|
|
965
1055
|
```typescript
|
|
966
1056
|
import { sv, cn } from 'slot-variants';
|
|
@@ -1000,7 +1090,7 @@ sv({ slots: { body: 'p-4 ' } }); // trailing space
|
|
|
1000
1090
|
cn(`flex\titems-center`); // tab between tokens
|
|
1001
1091
|
```
|
|
1002
1092
|
|
|
1003
|
-
`no-redundant-spaces` reports each literal whose whitespace deviates from the canonical "tokens separated by exactly one space" form. Trim and collapse the strings — or split them into array entries — so the stored class output is byte-stable and easy to scan in diffs.
|
|
1093
|
+
`no-redundant-spaces` reports each literal whose whitespace deviates from the canonical "tokens separated by exactly one space" form. Trim and collapse the strings — or split them into array entries — so the stored class output is byte-stable and easy to scan in diffs. Run `eslint --fix` to apply the canonical form automatically; the fixer rewrites the literal in place using the original quote style.
|
|
1004
1094
|
|
|
1005
1095
|
```typescript
|
|
1006
1096
|
import { sv } from 'slot-variants';
|
|
@@ -1070,7 +1160,7 @@ module.exports = {
|
|
|
1070
1160
|
size: { sm: 'text-sm', lg: 'text-lg' },
|
|
1071
1161
|
intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
|
|
1072
1162
|
},
|
|
1073
|
-
defaultVariants: { size: '
|
|
1163
|
+
defaultVariants: { size: 'sm' },
|
|
1074
1164
|
compoundVariants: [
|
|
1075
1165
|
{ size: 'lg', intent: 'primary', class: 'uppercase' }
|
|
1076
1166
|
]
|
|
@@ -1093,7 +1183,7 @@ Everything else works identically — the config shape, `class`/`className` over
|
|
|
1093
1183
|
variants: {
|
|
1094
1184
|
size: { sm: 'text-sm', lg: 'text-lg' }
|
|
1095
1185
|
},
|
|
1096
|
-
defaultVariants: { size: '
|
|
1186
|
+
defaultVariants: { size: 'sm' }
|
|
1097
1187
|
});
|
|
1098
1188
|
```
|
|
1099
1189
|
|
|
@@ -1101,11 +1191,11 @@ Key differences to be aware of:
|
|
|
1101
1191
|
|
|
1102
1192
|
| Feature | tailwind-variants | slot-variants |
|
|
1103
1193
|
| --- | --- | --- |
|
|
1104
|
-
| Slot return type |
|
|
1194
|
+
| Slot return type | Always functions: `slot({ class: '...' })` | Strings by default; functions for slots listed in `multiSlots` |
|
|
1105
1195
|
| `extend` (composition) | Supported | Not supported |
|
|
1106
1196
|
| Built-in `twMerge` | Enabled by default | Use `postProcess: twMerge` |
|
|
1107
1197
|
|
|
1108
|
-
**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:
|
|
1198
|
+
**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:
|
|
1109
1199
|
|
|
1110
1200
|
```typescript
|
|
1111
1201
|
// tailwind-variants
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# slot-variants — AI Agent Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`slot-variants` is a lightweight, zero-dependency, type-safe library for managing CSS class name variants with slots support.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { sv, cn, type VariantProps } from 'slot-variants';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`sv()` is a drop-in replacement for CVA (`cva` → `sv`) and covers the core feature set of tailwind-variants (`tv`) with a simpler API. Slots return strings directly by default, or reconfigurable functions (like in `tv`) when listed in `multiSlots`. Features not in CVA/TV: `requiredVariants`, `presets`, `cacheSize`, `postProcess`, function-based `defaultVariants`, and variadic base args.
|
|
12
|
+
|
|
13
|
+
## Calling Conventions
|
|
14
|
+
|
|
15
|
+
`sv()` supports three calling conventions:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// 1. Config-only
|
|
19
|
+
sv({ base: 'btn', variants: { size: { sm: 'text-sm', lg: 'text-lg' } } });
|
|
20
|
+
|
|
21
|
+
// 2. Base + config
|
|
22
|
+
sv('btn', { variants: { size: { sm: 'text-sm', lg: 'text-lg' } } });
|
|
23
|
+
|
|
24
|
+
// 3. Class name merging (like cn())
|
|
25
|
+
sv('flex', 'items-center', 'gap-2'); // 'flex items-center gap-2'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `base` config field merges with base arguments and `slots.base`: `cn(baseArgs..., config.base, slots.base)`.
|
|
29
|
+
|
|
30
|
+
## Best Practices
|
|
31
|
+
|
|
32
|
+
### 1. Keep CSS Classes Mutually Exclusive
|
|
33
|
+
|
|
34
|
+
Define classes in base and variants so they don't overlap; use compound variants for intersecting classes. Don't repeat a token across every variant value — lift it into `base`.
|
|
35
|
+
|
|
36
|
+
### 2. Use Boolean Shorthand for Simple States
|
|
37
|
+
|
|
38
|
+
For boolean variants (on/off), use shorthand instead of a verbose `{ true, false }` record:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const button = sv('btn', {
|
|
42
|
+
variants: { disabled: 'opacity-50 cursor-not-allowed' }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
button({ disabled: true });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Use Required Variants for Mandatory Props
|
|
49
|
+
|
|
50
|
+
List a variant in `requiredVariants: ['intent']` to make it mandatory — `button()` throws when it is missing.
|
|
51
|
+
|
|
52
|
+
### 4. Use Slots for Multi-Element Components
|
|
53
|
+
|
|
54
|
+
For components with multiple elements (card, dialog, etc.), use slots:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const card = sv('border rounded-lg', {
|
|
58
|
+
slots: { header: 'font-semibold px-4', body: 'px-4 py-4', footer: 'px-4 border-t' }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { base, header, body, footer } = card();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Variant values can target slots: `size: { sm: { base: 'p-2', header: 'text-sm' } }`. Use `compoundSlots` to apply classes to multiple slots at once.
|
|
65
|
+
|
|
66
|
+
### 5. Use VariantProps Type for Component Props
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
type ButtonProps = VariantProps<typeof button>;
|
|
70
|
+
// Exclude internal variants from props:
|
|
71
|
+
type InternalButtonProps = VariantProps<typeof button, 'internalState'>;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 6. Use Compound Variants for Conditional Combinations
|
|
75
|
+
|
|
76
|
+
Apply classes when multiple conditions are met:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
compoundVariants: [
|
|
80
|
+
{ size: 'lg', intent: 'primary', class: 'font-bold uppercase' }
|
|
81
|
+
]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 7. Use Function-Based Default Variants for Dynamic Defaults
|
|
85
|
+
|
|
86
|
+
A default can be a function of the other variant values:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
defaultVariants: {
|
|
90
|
+
size: 'sm',
|
|
91
|
+
intent: (props) => {
|
|
92
|
+
if (props.size === 'lg') return 'danger';
|
|
93
|
+
return 'primary';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Prefer static defaults — function-based defaults run on every invocation.
|
|
99
|
+
|
|
100
|
+
### 8. Use Post-Processing with tailwind-merge
|
|
101
|
+
|
|
102
|
+
For Tailwind projects, pass `postProcess` to resolve class conflicts:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { twMerge } from 'tailwind-merge';
|
|
106
|
+
|
|
107
|
+
sv('px-4 py-2', { variants: { size: { lg: 'px-6 py-3' } }, postProcess: twMerge });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 9. Leverage Caching for Performance
|
|
111
|
+
|
|
112
|
+
The library caches results automatically (default 256 entries). Each cache entry is one distinct combination of resolved variant values:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
maxEntries = (values₁ + 1) × (values₂ + 1) × ... × (valuesₙ + 1)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `+ 1` counts the variant being left unset. Raise `cacheSize` only when `maxEntries` exceeds 256 — below that the cache never evicts. Cache inspection methods (`getCacheSize`, `clearCache`) are only exposed when `introspection: true`.
|
|
119
|
+
|
|
120
|
+
### 10. Use Presets for Reusable Variant Combinations
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const button = sv('btn', {
|
|
124
|
+
variants: { size: { sm: 'text-sm', lg: 'text-lg' }, intent: { primary: 'bg-blue-500', danger: 'bg-red-500' } },
|
|
125
|
+
presets: { cta: { size: 'lg', intent: 'primary' } }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
button({ preset: 'cta' }); // applies size: 'lg', intent: 'primary'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 11. Use Introspection for Single Source of Truth
|
|
132
|
+
|
|
133
|
+
Set `introspection: true` to expose configuration and cache members on the returned function (off by default): `variantKeys`, `variants`, `slotKeys`, `slots`, `defaultVariants`, `requiredVariants`, `presetKeys`, `presets`, `getVariantValues(key)`, `getCacheSize()`, and `clearCache()`.
|
|
134
|
+
|
|
135
|
+
Without `introspection: true`, accessing these is a type error. Use it to centralize variant/slot definitions and reuse them across the codebase.
|
|
136
|
+
|
|
137
|
+
## Common Patterns
|
|
138
|
+
|
|
139
|
+
### React Component Pattern
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { sv, type VariantProps } from 'slot-variants';
|
|
143
|
+
|
|
144
|
+
const button = sv('btn font-medium rounded-lg', {
|
|
145
|
+
variants: {
|
|
146
|
+
size: { sm: 'text-sm px-3', md: 'text-base px-4', lg: 'text-lg px-6' },
|
|
147
|
+
intent: { primary: 'bg-blue-500 text-white', danger: 'bg-red-500 text-white' }
|
|
148
|
+
},
|
|
149
|
+
defaultVariants: { size: 'md', intent: 'primary' }
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export type ButtonProps = VariantProps<typeof button>;
|
|
153
|
+
|
|
154
|
+
export const Button = ({ class: className, ...props }: ButtonProps & { class?: string }) => {
|
|
155
|
+
return <button className={button({ ...props, class: className })} />;
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Multi-Element Component Pattern
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const card = sv('border rounded-lg', {
|
|
163
|
+
slots: { header: 'font-semibold px-4 border-b', body: 'px-4 py-4', footer: 'px-4 border-t' },
|
|
164
|
+
variants: {
|
|
165
|
+
size: { sm: { base: 'p-2', header: 'text-sm' }, md: { base: 'p-4', header: 'text-base' } },
|
|
166
|
+
elevated: { true: { base: 'shadow-lg' }, false: { base: 'shadow-sm' } }
|
|
167
|
+
},
|
|
168
|
+
compoundSlots: [{ slots: ['header', 'footer'], class: 'text-gray-600' }]
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const { base, header, body, footer } = card({ size: 'md' });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Configuration Reference
|
|
175
|
+
|
|
176
|
+
Class values inside the config (`base`, `variants` values, `slots` values, and `compound*` `class`/`className`) accept only `string`, `string[]`, or `undefined`. Dynamic class values (objects, booleans, nested arrays) belong on the `class`/`className` runtime prop, not in the config.
|
|
177
|
+
|
|
178
|
+
| Option | Type | Description |
|
|
179
|
+
| ------------------ | ------------------------------------ | --------------------------------- |
|
|
180
|
+
| `base` | `string \| string[]` | Additional base classes |
|
|
181
|
+
| `variants` | `Record<string, VariantConfig>` | Variant definitions |
|
|
182
|
+
| `slots` | `Record<string, string \| string[]>` | Named slot definitions |
|
|
183
|
+
| `compoundVariants` | `CompoundVariant[]` | Conditional class combinations |
|
|
184
|
+
| `compoundSlots` | `CompoundSlot[]` | Multi-slot conditional classes |
|
|
185
|
+
| `defaultVariants` | `Record<string, Value>` | Static or function-based defaults |
|
|
186
|
+
| `requiredVariants` | `string[] \| boolean` | Mandatory variant names |
|
|
187
|
+
| `multiSlots` | `string[] \| boolean` | Slots exposed as reconfigurable functions |
|
|
188
|
+
| `presets` | `Record<string, Partial<Props>>` | Named preset combinations |
|
|
189
|
+
| `postProcess` | `(className: string) => string` | Class transformation |
|
|
190
|
+
| `cacheSize` | `number` | Cache size (default: 256) |
|
|
191
|
+
| `introspection` | `boolean` | Expose introspection and cache methods (default: false) |
|
|
192
|
+
|
|
193
|
+
## Exported Types
|
|
194
|
+
|
|
195
|
+
- `ClassValue` — Valid input for `cn()` (string, array, object, boolean, null, undefined)
|
|
196
|
+
- `VariantProps<T, E>` — Extract variant props from an `sv()` return, optionally excluding keys
|
|
197
|
+
- `VariantValue<T, K>` — Extract the value union for a single variant key, without `undefined`
|
|
198
|
+
- `SlotClassProps<T>` — Extract the per-slot class injection shape from an `sv()` return type
|
|
199
|
+
|
|
200
|
+
Functions are imported as named values; types via `import type { ... } from 'slot-variants'`.
|