slot-variants 1.2.0 → 1.4.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 +197 -17
- package/{AGENTS.md → SKILL.md} +104 -14
- package/dist/eslint-plugin.cjs +1186 -0
- package/dist/eslint-plugin.d.ts +27 -0
- package/dist/eslint-plugin.js +1161 -0
- package/dist/index.cjs +514 -228
- package/dist/index.d.ts +172 -52
- package/dist/index.js +514 -228
- package/dist/src/cn.d.ts +14 -0
- package/dist/src/eslint-plugin.d.ts +25 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/sv.d.ts +189 -0
- package/dist/test/cn.test.d.ts +1 -0
- package/dist/test/eslint-plugin.test.d.ts +1 -0
- package/dist/test/sv.test.d.ts +1 -0
- package/package.json +44 -21
package/README.md
CHANGED
|
@@ -172,15 +172,14 @@ badge({ color: 'green', size: 'sm' });
|
|
|
172
172
|
// 'badge bg-green-100 text-green-800 text-xs px-2 py-0.5'
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
Variant values
|
|
175
|
+
Variant values accept a string or an array of strings:
|
|
176
176
|
|
|
177
177
|
```typescript
|
|
178
178
|
const button = sv('btn', {
|
|
179
179
|
variants: {
|
|
180
180
|
size: {
|
|
181
|
-
sm: ['px-2', 'py-1', 'text-sm'],
|
|
182
|
-
lg: 'px-6 py-3 text-lg'
|
|
183
|
-
xl: { 'px-8': true, 'py-4': true, 'text-xl': true } // object
|
|
181
|
+
sm: ['px-2', 'py-1', 'text-sm'], // array of strings
|
|
182
|
+
lg: 'px-6 py-3 text-lg' // string
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
});
|
|
@@ -669,7 +668,7 @@ The `postProcess` function is applied to each slot's final class string independ
|
|
|
669
668
|
|
|
670
669
|
### Caching
|
|
671
670
|
|
|
672
|
-
Results are cached automatically for performance. The default cache size is **256** entries.
|
|
671
|
+
Results are cached automatically for performance. The default cache size is **256** entries.
|
|
673
672
|
|
|
674
673
|
```typescript
|
|
675
674
|
const button = sv('btn', {
|
|
@@ -679,16 +678,15 @@ const button = sv('btn', {
|
|
|
679
678
|
lg: 'text-lg'
|
|
680
679
|
}
|
|
681
680
|
},
|
|
682
|
-
cacheSize: 512
|
|
681
|
+
cacheSize: 512 // customize the cache size
|
|
683
682
|
});
|
|
684
|
-
|
|
685
|
-
button.getCacheSize(); // current number of cached entries
|
|
686
|
-
button.clearCache(); // clear all cached entries
|
|
687
683
|
```
|
|
688
684
|
|
|
685
|
+
Cache inspection and control methods (`getCacheSize`, `clearCache`) are exposed on the returned function only when `introspection: true` is set — see [Introspection](#introspection).
|
|
686
|
+
|
|
689
687
|
### Introspection
|
|
690
688
|
|
|
691
|
-
|
|
689
|
+
Set `introspection: true` in the config to expose configuration properties and cache controls on the returned function for runtime inspection. Introspection is **disabled by default** to keep the returned function lean; opt in only when you need it:
|
|
692
690
|
|
|
693
691
|
```typescript
|
|
694
692
|
const button = sv('btn', {
|
|
@@ -711,7 +709,8 @@ const button = sv('btn', {
|
|
|
711
709
|
requiredVariants: ['intent'],
|
|
712
710
|
presets: {
|
|
713
711
|
cta: { size: 'lg', intent: 'primary' }
|
|
714
|
-
}
|
|
712
|
+
},
|
|
713
|
+
introspection: true
|
|
715
714
|
});
|
|
716
715
|
|
|
717
716
|
button.variantKeys; // ['size', 'intent']
|
|
@@ -724,8 +723,12 @@ button.presetKeys; // ['cta']
|
|
|
724
723
|
button.presets; // { cta: { size: 'lg', intent: 'primary' } }
|
|
725
724
|
button.getVariantValues('size'); // ['sm', 'lg']
|
|
726
725
|
button.getVariantValues('intent'); // ['primary', 'danger']
|
|
726
|
+
button.getCacheSize(); // current number of cached entries
|
|
727
|
+
button.clearCache(); // clear all cached entries
|
|
727
728
|
```
|
|
728
729
|
|
|
730
|
+
Without `introspection: true`, only the variant function itself is returned — accessing introspection or cache properties is a type error.
|
|
731
|
+
|
|
729
732
|
## TypeScript
|
|
730
733
|
|
|
731
734
|
`slot-variants` is fully typed. Variant props are inferred from your config:
|
|
@@ -871,11 +874,13 @@ When used on an `sv()` definition without slots, `SlotClassProps` resolves to `{
|
|
|
871
874
|
|
|
872
875
|
## Config Reference
|
|
873
876
|
|
|
877
|
+
Class values inside the config (`base`, `variants`, `slots`, and `compound*` `class`/`className`) accept `string`, `string[]`, or `undefined`. Dynamic class values (objects, booleans, nested arrays) are only accepted at call time via the `class`/`className` prop.
|
|
878
|
+
|
|
874
879
|
| Option | Type | Description |
|
|
875
880
|
| --- | --- | --- |
|
|
876
|
-
| `base` | `
|
|
877
|
-
| `variants` | `
|
|
878
|
-
| `slots` | `
|
|
881
|
+
| `base` | `string \| string[]` | Additional base classes merged with the base argument and `slots.base` |
|
|
882
|
+
| `variants` | `Record<string, Record<string \| number, string \| string[]>>` | Variant definitions mapping variant names to their possible values |
|
|
883
|
+
| `slots` | `Record<string, string \| string[]>` | Named slot definitions for multi-element components |
|
|
879
884
|
| `compoundVariants` | `Array` | Additional classes applied when multiple variant conditions match |
|
|
880
885
|
| `compoundSlots` | `Array` | Classes applied to multiple slots based on variant conditions |
|
|
881
886
|
| `defaultVariants` | `Object` | Default values for variants (static values or functions) |
|
|
@@ -883,6 +888,181 @@ When used on an `sv()` definition without slots, `SlotClassProps` resolves to `{
|
|
|
883
888
|
| `presets` | `Record<string, Partial<VariantProps>>` | Named combinations of variant values selectable via `preset` prop |
|
|
884
889
|
| `postProcess` | `(className: string) => string` | Custom transformation applied to final class strings |
|
|
885
890
|
| `cacheSize` | `number` | Maximum number of cached results (default: `256`) |
|
|
891
|
+
| `introspection` | `boolean` | When `true`, exposes variant/slot/preset introspection and cache methods on the returned function (default: `false`) |
|
|
892
|
+
|
|
893
|
+
## ESLint / oxlint Plugin
|
|
894
|
+
|
|
895
|
+
`slot-variants` ships an ESLint-compatible plugin at the `slot-variants/eslint-plugin` subpath. It runs under ESLint v9+ (flat config) and under [oxlint](https://oxc.rs/docs/guide/usage/linter/js-plugins) via its `jsPlugins` API. The plugin is a separate entry point with no runtime imports — consuming it doesn't pull any library code into your bundle.
|
|
896
|
+
|
|
897
|
+
### Rules
|
|
898
|
+
|
|
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, the trailing `!` important marker is ignored when computing the namespace, and tokens that only co-occur across mutually-exclusive variant values are skipped.
|
|
900
|
+
|
|
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.
|
|
902
|
+
|
|
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.
|
|
904
|
+
|
|
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. **Auto-fixable**: `eslint --fix` rewrites each offending literal in place, preserving its original quote style.
|
|
906
|
+
|
|
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 or is listed in `requiredVariants`. 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
|
+
|
|
909
|
+
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
|
+
|
|
911
|
+
### ESLint (flat config)
|
|
912
|
+
|
|
913
|
+
Use the `recommended` preset to enable every rule at `error` in one line:
|
|
914
|
+
|
|
915
|
+
```js
|
|
916
|
+
import svPlugin from 'slot-variants/eslint-plugin';
|
|
917
|
+
|
|
918
|
+
export default [svPlugin.configs.recommended];
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Or wire each rule by hand if you want per-rule control:
|
|
922
|
+
|
|
923
|
+
```js
|
|
924
|
+
import svPlugin from 'slot-variants/eslint-plugin';
|
|
925
|
+
|
|
926
|
+
export default [
|
|
927
|
+
{
|
|
928
|
+
plugins: { 'slot-variants': svPlugin },
|
|
929
|
+
rules: {
|
|
930
|
+
'slot-variants/no-conflicting-classes': 'error',
|
|
931
|
+
'slot-variants/no-dynamic-classes': 'error',
|
|
932
|
+
'slot-variants/no-empty-classes': 'error',
|
|
933
|
+
'slot-variants/no-redundant-spaces': 'error',
|
|
934
|
+
'slot-variants/no-shared-tokens': 'error'
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
];
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### oxlint
|
|
941
|
+
|
|
942
|
+
```json
|
|
943
|
+
{
|
|
944
|
+
"jsPlugins": ["slot-variants/eslint-plugin"],
|
|
945
|
+
"rules": {
|
|
946
|
+
"slot-variants/no-conflicting-classes": "error",
|
|
947
|
+
"slot-variants/no-dynamic-classes": "error",
|
|
948
|
+
"slot-variants/no-empty-classes": "error",
|
|
949
|
+
"slot-variants/no-redundant-spaces": "error",
|
|
950
|
+
"slot-variants/no-shared-tokens": "error"
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Example
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
import { sv, cn } from 'slot-variants';
|
|
959
|
+
|
|
960
|
+
const button = sv({
|
|
961
|
+
base: 'flex items-center',
|
|
962
|
+
variants: {
|
|
963
|
+
orientation: {
|
|
964
|
+
row: ['flex', 'flex-row'], // 'flex' duplicates base
|
|
965
|
+
col: ['flex', 'flex-col'] // 'flex' duplicates base
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
cn('flex items-center', 'flex'); // 'flex' duplicated across args
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
`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.
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
import { sv, cn } from 'slot-variants';
|
|
977
|
+
|
|
978
|
+
const extra = getDynamicClass();
|
|
979
|
+
|
|
980
|
+
sv({ base: extra }); // dynamic base
|
|
981
|
+
sv({ base: `text-sm ${size}` }); // template with expression
|
|
982
|
+
sv({ ...rest, variants: {} }); // spread inside config
|
|
983
|
+
sv({ variants: { [key]: 'x' } }); // computed variant key
|
|
984
|
+
sv({ slots: { body: ['p-4', ...rest] } }); // spread inside slot array
|
|
985
|
+
cn(extra, 'flex'); // identifier argument
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
`no-dynamic-classes` reports each of the dynamic positions above. Replace dynamic class strings with static ones (or move them to the runtime `class` / `className` prop on the returned function, which is intentionally outside the analyzer's scope) so every call can be statically verified.
|
|
989
|
+
|
|
990
|
+
```typescript
|
|
991
|
+
import { sv, cn } from 'slot-variants';
|
|
992
|
+
|
|
993
|
+
sv({ base: '' }); // empty base
|
|
994
|
+
sv({ base: [] }); // empty array
|
|
995
|
+
sv({ variants: { size: { sm: '' } } }); // empty variant value
|
|
996
|
+
sv({ compoundVariants: [{ size: 'lg', class: '' }] }); // empty compound class
|
|
997
|
+
cn('flex', ''); // empty cn arg
|
|
998
|
+
sv(); // zero-arg call
|
|
999
|
+
cn(); // zero-arg call
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
`no-empty-classes` reports each empty class value — strings, arrays, or objects — plus zero-argument `sv()` / `cn()` calls (they always produce an empty string). The one exception is a direct empty string at `slots[key]`, which is allowed because declaring a slot with no default classes is a real use case (`sv({ slots: { extra: '' } })`). Either remove the empty value or replace it with a meaningful class string.
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
import { sv, cn } from 'slot-variants';
|
|
1006
|
+
|
|
1007
|
+
sv({ base: ' flex items-center' }); // leading space
|
|
1008
|
+
sv({ base: 'flex items-center' }); // double space
|
|
1009
|
+
sv({ slots: { body: 'p-4 ' } }); // trailing space
|
|
1010
|
+
cn(`flex\titems-center`); // tab between tokens
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
`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.
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
import { sv } from 'slot-variants';
|
|
1017
|
+
|
|
1018
|
+
const button = sv({
|
|
1019
|
+
variants: {
|
|
1020
|
+
size: {
|
|
1021
|
+
sm: 'rounded text-sm',
|
|
1022
|
+
lg: 'rounded text-lg'
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
defaultVariants: { size: 'sm' }
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const card = sv({
|
|
1029
|
+
slots: { root: 'flex', body: 'p-4' },
|
|
1030
|
+
variants: {
|
|
1031
|
+
size: {
|
|
1032
|
+
sm: { root: 'rounded text-sm', body: 'p-1' },
|
|
1033
|
+
lg: { root: 'rounded text-lg', body: 'p-2' }
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
requiredVariants: ['size']
|
|
1037
|
+
});
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
`no-shared-tokens` reports `rounded` in both `button` variant values and in both `card` `root` slot values, because the token is present in every value of an exhaustive variant. Lift that class into `base` — or into `slots.root` for slot-based variants — so each variant value contains only the classes that actually vary.
|
|
1041
|
+
|
|
1042
|
+
## IntelliSense Setup (Optional)
|
|
1043
|
+
|
|
1044
|
+
If you're using Tailwind CSS, you can opt into class autocompletion and automatic class sorting inside `sv()` and `cn()` calls.
|
|
1045
|
+
|
|
1046
|
+
### VSCode
|
|
1047
|
+
|
|
1048
|
+
The [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension recognizes calls listed in `tailwindCSS.classFunctions`. Add `cn` and `sv` to your workspace or user settings:
|
|
1049
|
+
|
|
1050
|
+
```json
|
|
1051
|
+
{
|
|
1052
|
+
"tailwindCSS.classFunctions": ["cn", "sv"]
|
|
1053
|
+
}
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### Prettier
|
|
1057
|
+
|
|
1058
|
+
The [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) plugin sorts Tailwind classes inside the functions listed in `tailwindFunctions`:
|
|
1059
|
+
|
|
1060
|
+
```js
|
|
1061
|
+
module.exports = {
|
|
1062
|
+
plugins: [require('prettier-plugin-tailwindcss')],
|
|
1063
|
+
tailwindFunctions: ['cn', 'sv']
|
|
1064
|
+
};
|
|
1065
|
+
```
|
|
886
1066
|
|
|
887
1067
|
## Migrating from CVA / tailwind-variants
|
|
888
1068
|
|
|
@@ -900,7 +1080,7 @@ When used on an `sv()` definition without slots, `SlotClassProps` resolves to `{
|
|
|
900
1080
|
size: { sm: 'text-sm', lg: 'text-lg' },
|
|
901
1081
|
intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
|
|
902
1082
|
},
|
|
903
|
-
defaultVariants: { size: '
|
|
1083
|
+
defaultVariants: { size: 'sm' },
|
|
904
1084
|
compoundVariants: [
|
|
905
1085
|
{ size: 'lg', intent: 'primary', class: 'uppercase' }
|
|
906
1086
|
]
|
|
@@ -923,7 +1103,7 @@ Everything else works identically — the config shape, `class`/`className` over
|
|
|
923
1103
|
variants: {
|
|
924
1104
|
size: { sm: 'text-sm', lg: 'text-lg' }
|
|
925
1105
|
},
|
|
926
|
-
defaultVariants: { size: '
|
|
1106
|
+
defaultVariants: { size: 'sm' }
|
|
927
1107
|
});
|
|
928
1108
|
```
|
|
929
1109
|
|
|
@@ -958,4 +1138,4 @@ const button = sv({
|
|
|
958
1138
|
variants: { size: { sm: 'px-2 py-1' } },
|
|
959
1139
|
postProcess: twMerge
|
|
960
1140
|
});
|
|
961
|
-
```
|
|
1141
|
+
```
|
package/{AGENTS.md → SKILL.md}
RENAMED
|
@@ -211,13 +211,9 @@ const button = sv('btn', {
|
|
|
211
211
|
},
|
|
212
212
|
cacheSize: 512 // increase cache for complex components
|
|
213
213
|
});
|
|
214
|
-
|
|
215
|
-
// Access cache methods
|
|
216
|
-
button.getCacheSize();
|
|
217
|
-
button.clearCache();
|
|
218
214
|
```
|
|
219
215
|
|
|
220
|
-
|
|
216
|
+
Cache inspection methods (`getCacheSize`, `clearCache`) are only exposed when `introspection: true` is set — see rule 11.
|
|
221
217
|
|
|
222
218
|
### 10. Use Presets for Reusable Variant Combinations
|
|
223
219
|
|
|
@@ -241,7 +237,7 @@ button({ preset: 'cta' });
|
|
|
241
237
|
|
|
242
238
|
### 11. Use Introspection for Single Source of Truth
|
|
243
239
|
|
|
244
|
-
|
|
240
|
+
Set `introspection: true` in the config to expose configuration properties and cache methods on the returned function. Introspection is **off by default** — opt in only when you need runtime access to variant/slot definitions or cache controls:
|
|
245
241
|
|
|
246
242
|
```typescript
|
|
247
243
|
const button = sv('btn', {
|
|
@@ -264,7 +260,8 @@ const button = sv('btn', {
|
|
|
264
260
|
requiredVariants: ['intent'],
|
|
265
261
|
presets: {
|
|
266
262
|
cta: { size: 'lg', intent: 'primary' }
|
|
267
|
-
}
|
|
263
|
+
},
|
|
264
|
+
introspection: true
|
|
268
265
|
});
|
|
269
266
|
|
|
270
267
|
button.variantKeys; // ['size', 'intent']
|
|
@@ -277,8 +274,12 @@ button.presetKeys; // ['cta']
|
|
|
277
274
|
button.presets; // { cta: { size: 'lg', intent: 'primary' } }
|
|
278
275
|
button.getVariantValues('size'); // ['sm', 'lg']
|
|
279
276
|
button.getVariantValues('intent'); // ['primary', 'danger']
|
|
277
|
+
button.getCacheSize(); // current cache size
|
|
278
|
+
button.clearCache(); // clear the cache
|
|
280
279
|
```
|
|
281
280
|
|
|
281
|
+
Without `introspection: true`, only the variant function itself is returned — accessing these properties is a type error.
|
|
282
|
+
|
|
282
283
|
Use introspection to share variant/slot definitions with other parts of your codebase:
|
|
283
284
|
|
|
284
285
|
```typescript
|
|
@@ -299,7 +300,8 @@ export const button = sv('btn font-medium rounded-lg', {
|
|
|
299
300
|
defaultVariants: {
|
|
300
301
|
size: 'md',
|
|
301
302
|
intent: 'primary'
|
|
302
|
-
}
|
|
303
|
+
},
|
|
304
|
+
introspection: true
|
|
303
305
|
});
|
|
304
306
|
|
|
305
307
|
// Reuse variant keys for form validation
|
|
@@ -313,7 +315,8 @@ const card = sv('card', {
|
|
|
313
315
|
slots: {
|
|
314
316
|
header: 'font-bold',
|
|
315
317
|
body: 'py-4'
|
|
316
|
-
}
|
|
318
|
+
},
|
|
319
|
+
introspection: true
|
|
317
320
|
});
|
|
318
321
|
|
|
319
322
|
// Dynamically render all slots
|
|
@@ -400,11 +403,13 @@ export const Card = (props: CardProps) => {
|
|
|
400
403
|
|
|
401
404
|
## Configuration Reference
|
|
402
405
|
|
|
406
|
+
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.
|
|
407
|
+
|
|
403
408
|
| Option | Type | Description |
|
|
404
409
|
| ------------------ | -------------------------------- | --------------------------------- |
|
|
405
|
-
| `base` | `
|
|
410
|
+
| `base` | `string \| string[]` | Additional base classes |
|
|
406
411
|
| `variants` | `Record<string, VariantConfig>` | Variant definitions |
|
|
407
|
-
| `slots` | `Record<string,
|
|
412
|
+
| `slots` | `Record<string, string \| string[]>` | Named slot definitions |
|
|
408
413
|
| `compoundVariants` | `CompoundVariant[]` | Conditional class combinations |
|
|
409
414
|
| `compoundSlots` | `CompoundSlot[]` | Multi-slot conditional classes |
|
|
410
415
|
| `defaultVariants` | `Record<string, Value>` | Static or function-based defaults |
|
|
@@ -412,6 +417,7 @@ export const Card = (props: CardProps) => {
|
|
|
412
417
|
| `presets` | `Record<string, Partial<Props>>` | Named preset combinations |
|
|
413
418
|
| `postProcess` | `(className: string) => string` | Class transformation |
|
|
414
419
|
| `cacheSize` | `number` | Cache size (default: 256) |
|
|
420
|
+
| `introspection` | `boolean` | Expose introspection and cache methods (default: false) |
|
|
415
421
|
|
|
416
422
|
## Exported Types
|
|
417
423
|
|
|
@@ -430,9 +436,93 @@ import { sv, cn } from 'slot-variants';
|
|
|
430
436
|
import type { VariantProps, VariantValue, SlotClassProps, ClassValue } from 'slot-variants';
|
|
431
437
|
```
|
|
432
438
|
|
|
439
|
+
## ESLint / oxlint Plugin
|
|
440
|
+
|
|
441
|
+
Subpath export `slot-variants/eslint-plugin` ships five rules that statically analyze `sv()` and `cn()` calls. Works under ESLint v9+ (flat config) and under oxlint via its `jsPlugins` config. The plugin is a separate entry point — it adds no runtime code to the library bundle.
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
// eslint.config.js
|
|
445
|
+
import svPlugin from 'slot-variants/eslint-plugin';
|
|
446
|
+
|
|
447
|
+
export default [{
|
|
448
|
+
plugins: { 'slot-variants': svPlugin },
|
|
449
|
+
rules: {
|
|
450
|
+
'slot-variants/no-conflicting-classes': 'error',
|
|
451
|
+
'slot-variants/no-dynamic-classes': 'error',
|
|
452
|
+
'slot-variants/no-empty-classes': 'error',
|
|
453
|
+
'slot-variants/no-redundant-spaces': 'error',
|
|
454
|
+
'slot-variants/no-shared-tokens': 'error'
|
|
455
|
+
}
|
|
456
|
+
}];
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
// .oxlintrc.json
|
|
461
|
+
{
|
|
462
|
+
"jsPlugins": ["slot-variants/eslint-plugin"],
|
|
463
|
+
"rules": {
|
|
464
|
+
"slot-variants/no-conflicting-classes": "error",
|
|
465
|
+
"slot-variants/no-dynamic-classes": "error",
|
|
466
|
+
"slot-variants/no-empty-classes": "error",
|
|
467
|
+
"slot-variants/no-redundant-spaces": "error",
|
|
468
|
+
"slot-variants/no-shared-tokens": "error"
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
All rules only analyze calls where `sv` or `cn` is a **named import** from `'slot-variants'`. Default, namespace, and aliased-to-other-identifier imports are ignored.
|
|
474
|
+
|
|
475
|
+
### `slot-variants/no-conflicting-classes`
|
|
476
|
+
|
|
477
|
+
Reports class tokens that collide within the output of an `sv()` or `cn()` call: 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`).
|
|
478
|
+
|
|
479
|
+
- For `sv()` with a config, flags a collision when two of its sources can both be active at runtime: base + any variant, base + compound, two variants with different keys, two compound entries, or the same literal token repeated inside a single source.
|
|
480
|
+
- Does **not** flag tokens that only co-occur across different values of the **same** variant key (those are mutually exclusive at runtime).
|
|
481
|
+
- For `cn()` (and `sv()` called without a config — the cn-style calling convention), flags collisions across args, inside arrays, template literals without expressions, or within a single literal.
|
|
482
|
+
- Tokens with different variant prefixes (`w-100` vs `hover:w-200`) don't conflict; the trailing `!` important marker is ignored when computing the namespace; single-word utilities (no `-`) participate only in exact-duplicate detection.
|
|
483
|
+
- Bails out silently on dynamic inputs (identifiers, spreads, computed keys, template literals with expressions, cn-style `{ cls: condition }` records) — no false positives for code it can't statically resolve.
|
|
484
|
+
|
|
485
|
+
### `slot-variants/no-dynamic-classes`
|
|
486
|
+
|
|
487
|
+
Reports class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Pair this rule with `no-conflicting-classes` to guarantee every call is fully analyzable.
|
|
488
|
+
|
|
489
|
+
- Accepts only string literals, template literals without expressions, and arrays of those as class values. Identifiers, member access, calls, spreads, non-string literals, templates with expressions, and object records are reported.
|
|
490
|
+
- For `sv()` config, validates `base`, `slots` values, `variants` values (both record form and boolean shorthand), and the `class` / `className` of `compoundVariants` / `compoundSlots` entries. The `slots` array of `compoundSlots` must be a static array of string literals.
|
|
491
|
+
- Top-level config keys must be statically known — spreads and computed keys cause the call to fall through to the cn-style path, which then reports the entire object as dynamic.
|
|
492
|
+
- Non-class-bearing config keys (`defaultVariants`, `presets`, `requiredVariants`, `cacheSize`, `postProcess`, `introspection`) are not validated. Runtime variant matchers inside compound entries are also left alone — only the class value (and `compoundSlots`' `slots` array) is checked.
|
|
493
|
+
- Move dynamic class strings to the runtime `class` / `className` prop on the function returned by `sv()` — that call site is intentionally outside the analyzer's scope.
|
|
494
|
+
|
|
495
|
+
### `slot-variants/no-empty-classes`
|
|
496
|
+
|
|
497
|
+
Reports 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 (which always produce an empty class string).
|
|
498
|
+
|
|
499
|
+
- Reports empty literals as positional arguments to `cn()` and `sv()` (cn-style or as base args alongside a config), as well as empty literals nested inside class arrays.
|
|
500
|
+
- Inside an `sv()` config, reports empty values at `base`, in `variants` value records and boolean-shorthand values, and in the `class`/`className` of `compoundVariants` / `compoundSlots` entries. Also reports empty top-level `slots`, `variants`, `compoundVariants`, and `compoundSlots` containers.
|
|
501
|
+
- A direct empty string at `slots[key]` is allowed — declaring a slot with no default classes is a real use case (`sv({ slots: { extra: '' } })`). Empty strings inside slot-value arrays are still reported.
|
|
502
|
+
- Reports `sv()` / `cn()` invocations with zero arguments — they always return an empty string and have no effect.
|
|
503
|
+
- Recurses into arrays but not into objects: values inside cn-style `{ cls: condition }` records are conditions, not class values, so they are left alone.
|
|
504
|
+
|
|
505
|
+
### `slot-variants/no-redundant-spaces`
|
|
506
|
+
|
|
507
|
+
Reports class strings whose whitespace isn't canonical — that is, whose value differs from `value.split(/\s+/).filter(Boolean).join(' ')`.
|
|
508
|
+
|
|
509
|
+
- Flags leading or trailing whitespace, repeated spaces, and non-space whitespace (tabs, newlines, etc.) inside any string literal or expressionless template literal reachable from the call's arguments.
|
|
510
|
+
- Walks recursively into arrays and objects, so values nested inside `slots`, `variants` records, `compoundVariants`, `compoundSlots`, `defaultVariants`, `presets`, etc. are all inspected.
|
|
511
|
+
- Bails silently on dynamic expressions and non-string literals — false positives are impossible by construction.
|
|
512
|
+
- Reports once per offending literal at the whole-node location. Fix by trimming and collapsing the string, or by splitting it into array entries.
|
|
513
|
+
|
|
514
|
+
### `slot-variants/no-shared-tokens`
|
|
515
|
+
|
|
516
|
+
Reports class tokens that appear in every value of an exhaustively-covered variant — the token is constant in the rendered output and belongs in `base` or the corresponding `slots[slot]` entry rather than being repeated across every variant value.
|
|
517
|
+
|
|
518
|
+
- Only analyzes `sv()` calls with a config; `cn()` calls and cn-style `sv()` calls are ignored.
|
|
519
|
+
- Treats a variant as exhaustive when it has a `defaultVariants` entry or is listed in `requiredVariants`.
|
|
520
|
+
- Compares tokens per slot, so slot-based variants are checked against the specific slot they affect rather than only against `base`.
|
|
521
|
+
- Skips non-exhaustive variants, single-value variants, boolean shorthand, slot-keyed boolean shorthand, and dynamic or partially-uninspectable variant value records.
|
|
522
|
+
- Reports every repeated occurrence that should be lifted out of the variant so each value only contains classes that actually vary.
|
|
523
|
+
|
|
433
524
|
## Performance Notes
|
|
434
525
|
|
|
435
526
|
1. **Caching is automatic** - Results are cached by default
|
|
436
|
-
2. **
|
|
437
|
-
3. **
|
|
438
|
-
4. **Prefer static defaults** - Function-based defaults are called on every invocation
|
|
527
|
+
2. **Complex components benefit from larger cache** - Increase `cacheSize` for components with many variant combinations
|
|
528
|
+
3. **Prefer static defaults** - Function-based defaults are called on every invocation
|