slot-variants 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +139 -18
- package/README.md +402 -26
- package/dist/eslint-plugin.cjs +1067 -0
- package/dist/eslint-plugin.d.ts +26 -0
- package/dist/eslint-plugin.js +1042 -0
- package/dist/index.cjs +357 -181
- package/dist/index.d.ts +163 -48
- package/dist/index.js +357 -181
- package/dist/src/cn.d.ts +14 -0
- package/dist/src/eslint-plugin.d.ts +24 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/sv.d.ts +187 -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 +35 -16
package/AGENTS.md
CHANGED
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
import { sv, cn, type VariantProps } from 'slot-variants';
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
`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 (not functions like in `tv`). Features not in CVA/TV: `requiredVariants`, `presets`, `cacheSize`, `postProcess`, function-based `defaultVariants`, and variadic base args.
|
|
14
|
+
|
|
15
|
+
## Calling Conventions
|
|
16
|
+
|
|
17
|
+
`sv()` supports three calling conventions:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// 1. Config-only (like tailwind-variants' tv())
|
|
21
|
+
const button = sv({
|
|
22
|
+
base: 'btn font-medium',
|
|
23
|
+
variants: { size: { sm: 'text-sm', lg: 'text-lg' } }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 2. Base + config (like CVA's cva())
|
|
27
|
+
const button = sv('btn font-medium', {
|
|
28
|
+
variants: { size: { sm: 'text-sm', lg: 'text-lg' } }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// 3. Class name merging (like cn())
|
|
32
|
+
sv('flex', 'items-center', 'gap-2'); // 'flex items-center gap-2'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `base` config field merges with base arguments and `slots.base`: `cn(baseArgs..., config.base, slots.base)`.
|
|
36
|
+
|
|
13
37
|
## Best Practices
|
|
14
38
|
|
|
15
39
|
### 1. Keep CSS Classes Mutually Exclusive
|
|
@@ -187,12 +211,10 @@ const button = sv('btn', {
|
|
|
187
211
|
},
|
|
188
212
|
cacheSize: 512 // increase cache for complex components
|
|
189
213
|
});
|
|
190
|
-
|
|
191
|
-
// Access cache methods
|
|
192
|
-
button.getCacheSize();
|
|
193
|
-
button.clearCache();
|
|
194
214
|
```
|
|
195
215
|
|
|
216
|
+
Cache inspection methods (`getCacheSize`, `clearCache`) are only exposed when `introspection: true` is set — see rule 11.
|
|
217
|
+
|
|
196
218
|
Note: Using `class` or `className` prop bypasses caching.
|
|
197
219
|
|
|
198
220
|
### 10. Use Presets for Reusable Variant Combinations
|
|
@@ -217,7 +239,7 @@ button({ preset: 'cta' });
|
|
|
217
239
|
|
|
218
240
|
### 11. Use Introspection for Single Source of Truth
|
|
219
241
|
|
|
220
|
-
|
|
242
|
+
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:
|
|
221
243
|
|
|
222
244
|
```typescript
|
|
223
245
|
const button = sv('btn', {
|
|
@@ -240,19 +262,26 @@ const button = sv('btn', {
|
|
|
240
262
|
requiredVariants: ['intent'],
|
|
241
263
|
presets: {
|
|
242
264
|
cta: { size: 'lg', intent: 'primary' }
|
|
243
|
-
}
|
|
265
|
+
},
|
|
266
|
+
introspection: true
|
|
244
267
|
});
|
|
245
268
|
|
|
246
|
-
button.variantKeys;
|
|
247
|
-
button.variants;
|
|
248
|
-
button.slotKeys;
|
|
249
|
-
button.slots;
|
|
250
|
-
button.defaultVariants;
|
|
251
|
-
button.requiredVariants;
|
|
252
|
-
button.presetKeys;
|
|
253
|
-
button.presets;
|
|
269
|
+
button.variantKeys; // ['size', 'intent']
|
|
270
|
+
button.variants; // { size: { sm: 'text-sm', lg: 'text-lg' }, intent: { ... } }
|
|
271
|
+
button.slotKeys; // ['base', 'icon']
|
|
272
|
+
button.slots; // { icon: 'w-4 h-4' }
|
|
273
|
+
button.defaultVariants; // { size: 'sm' }
|
|
274
|
+
button.requiredVariants; // ['intent']
|
|
275
|
+
button.presetKeys; // ['cta']
|
|
276
|
+
button.presets; // { cta: { size: 'lg', intent: 'primary' } }
|
|
277
|
+
button.getVariantValues('size'); // ['sm', 'lg']
|
|
278
|
+
button.getVariantValues('intent'); // ['primary', 'danger']
|
|
279
|
+
button.getCacheSize(); // current cache size
|
|
280
|
+
button.clearCache(); // clear the cache
|
|
254
281
|
```
|
|
255
282
|
|
|
283
|
+
Without `introspection: true`, only the variant function itself is returned — accessing these properties is a type error.
|
|
284
|
+
|
|
256
285
|
Use introspection to share variant/slot definitions with other parts of your codebase:
|
|
257
286
|
|
|
258
287
|
```typescript
|
|
@@ -273,7 +302,8 @@ export const button = sv('btn font-medium rounded-lg', {
|
|
|
273
302
|
defaultVariants: {
|
|
274
303
|
size: 'md',
|
|
275
304
|
intent: 'primary'
|
|
276
|
-
}
|
|
305
|
+
},
|
|
306
|
+
introspection: true
|
|
277
307
|
});
|
|
278
308
|
|
|
279
309
|
// Reuse variant keys for form validation
|
|
@@ -287,7 +317,8 @@ const card = sv('card', {
|
|
|
287
317
|
slots: {
|
|
288
318
|
header: 'font-bold',
|
|
289
319
|
body: 'py-4'
|
|
290
|
-
}
|
|
320
|
+
},
|
|
321
|
+
introspection: true
|
|
291
322
|
});
|
|
292
323
|
|
|
293
324
|
// Dynamically render all slots
|
|
@@ -374,10 +405,13 @@ export const Card = (props: CardProps) => {
|
|
|
374
405
|
|
|
375
406
|
## Configuration Reference
|
|
376
407
|
|
|
408
|
+
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.
|
|
409
|
+
|
|
377
410
|
| Option | Type | Description |
|
|
378
411
|
| ------------------ | -------------------------------- | --------------------------------- |
|
|
412
|
+
| `base` | `string \| string[]` | Additional base classes |
|
|
379
413
|
| `variants` | `Record<string, VariantConfig>` | Variant definitions |
|
|
380
|
-
| `slots` | `Record<string,
|
|
414
|
+
| `slots` | `Record<string, string \| string[]>` | Named slot definitions |
|
|
381
415
|
| `compoundVariants` | `CompoundVariant[]` | Conditional class combinations |
|
|
382
416
|
| `compoundSlots` | `CompoundSlot[]` | Multi-slot conditional classes |
|
|
383
417
|
| `defaultVariants` | `Record<string, Value>` | Static or function-based defaults |
|
|
@@ -385,11 +419,14 @@ export const Card = (props: CardProps) => {
|
|
|
385
419
|
| `presets` | `Record<string, Partial<Props>>` | Named preset combinations |
|
|
386
420
|
| `postProcess` | `(className: string) => string` | Class transformation |
|
|
387
421
|
| `cacheSize` | `number` | Cache size (default: 256) |
|
|
422
|
+
| `introspection` | `boolean` | Expose introspection and cache methods (default: false) |
|
|
388
423
|
|
|
389
424
|
## Exported Types
|
|
390
425
|
|
|
391
426
|
- `ClassValue` - Valid input for `cn()` (string, array, object, boolean, null, undefined)
|
|
392
427
|
- `VariantProps<T, E>` - Extract variant props from an `sv()` return, optionally excluding keys
|
|
428
|
+
- `VariantValue<T, K>` - Extract the value union for a single variant key, without `undefined`
|
|
429
|
+
- `SlotClassProps<T>` - Extract the per-slot class injection shape from an `sv()` return type
|
|
393
430
|
|
|
394
431
|
## Imports
|
|
395
432
|
|
|
@@ -398,9 +435,93 @@ export const Card = (props: CardProps) => {
|
|
|
398
435
|
import { sv, cn } from 'slot-variants';
|
|
399
436
|
|
|
400
437
|
// Types only
|
|
401
|
-
import type { VariantProps, ClassValue } from 'slot-variants';
|
|
438
|
+
import type { VariantProps, VariantValue, SlotClassProps, ClassValue } from 'slot-variants';
|
|
402
439
|
```
|
|
403
440
|
|
|
441
|
+
## ESLint / oxlint Plugin
|
|
442
|
+
|
|
443
|
+
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.
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
// eslint.config.js
|
|
447
|
+
import svPlugin from 'slot-variants/eslint-plugin';
|
|
448
|
+
|
|
449
|
+
export default [{
|
|
450
|
+
plugins: { 'slot-variants': svPlugin },
|
|
451
|
+
rules: {
|
|
452
|
+
'slot-variants/no-duplicate-classes': 'error',
|
|
453
|
+
'slot-variants/no-dynamic-classes': 'error',
|
|
454
|
+
'slot-variants/no-empty-classes': 'error',
|
|
455
|
+
'slot-variants/no-redundant-spaces': 'error',
|
|
456
|
+
'slot-variants/no-shared-tokens': 'error'
|
|
457
|
+
}
|
|
458
|
+
}];
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```json
|
|
462
|
+
// .oxlintrc.json
|
|
463
|
+
{
|
|
464
|
+
"jsPlugins": ["slot-variants/eslint-plugin"],
|
|
465
|
+
"rules": {
|
|
466
|
+
"slot-variants/no-duplicate-classes": "error",
|
|
467
|
+
"slot-variants/no-dynamic-classes": "error",
|
|
468
|
+
"slot-variants/no-empty-classes": "error",
|
|
469
|
+
"slot-variants/no-redundant-spaces": "error",
|
|
470
|
+
"slot-variants/no-shared-tokens": "error"
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
### `slot-variants/no-duplicate-classes`
|
|
478
|
+
|
|
479
|
+
Reports class tokens that will appear more than once in the output of an `sv()` or `cn()` call.
|
|
480
|
+
|
|
481
|
+
- For `sv()` with a config, flags a token as duplicated 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.
|
|
482
|
+
- Does **not** flag the same token appearing in different values of the **same** variant key (those are mutually exclusive).
|
|
483
|
+
- For `cn()` (and `sv()` called without a config — the cn-style calling convention), flags any token that appears in more than one always-present source: across args, inside arrays, template literals without expressions, or within a single literal.
|
|
484
|
+
- 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.
|
|
485
|
+
|
|
486
|
+
### `slot-variants/no-dynamic-classes`
|
|
487
|
+
|
|
488
|
+
Reports class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Pair this rule with `no-duplicate-classes` to guarantee every call is fully analyzable.
|
|
489
|
+
|
|
490
|
+
- 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.
|
|
491
|
+
- 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.
|
|
492
|
+
- 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.
|
|
493
|
+
- 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.
|
|
494
|
+
- 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.
|
|
495
|
+
|
|
496
|
+
### `slot-variants/no-empty-classes`
|
|
497
|
+
|
|
498
|
+
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).
|
|
499
|
+
|
|
500
|
+
- 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.
|
|
501
|
+
- 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.
|
|
502
|
+
- 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.
|
|
503
|
+
- Reports `sv()` / `cn()` invocations with zero arguments — they always return an empty string and have no effect.
|
|
504
|
+
- Recurses into arrays but not into objects: values inside cn-style `{ cls: condition }` records are conditions, not class values, so they are left alone.
|
|
505
|
+
|
|
506
|
+
### `slot-variants/no-redundant-spaces`
|
|
507
|
+
|
|
508
|
+
Reports class strings whose whitespace isn't canonical — that is, whose value differs from `value.split(/\s+/).filter(Boolean).join(' ')`.
|
|
509
|
+
|
|
510
|
+
- 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.
|
|
511
|
+
- Walks recursively into arrays and objects, so values nested inside `slots`, `variants` records, `compoundVariants`, `compoundSlots`, `defaultVariants`, `presets`, etc. are all inspected.
|
|
512
|
+
- Bails silently on dynamic expressions and non-string literals — false positives are impossible by construction.
|
|
513
|
+
- Reports once per offending literal at the whole-node location. Fix by trimming and collapsing the string, or by splitting it into array entries.
|
|
514
|
+
|
|
515
|
+
### `slot-variants/no-shared-tokens`
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
- Only analyzes `sv()` calls with a config; `cn()` calls and cn-style `sv()` calls are ignored.
|
|
520
|
+
- Treats a variant as exhaustive when it has a `defaultVariants` entry or is listed in `requiredVariants`.
|
|
521
|
+
- Compares tokens per slot, so slot-based variants are checked against the specific slot they affect rather than only against `base`.
|
|
522
|
+
- Skips non-exhaustive variants, single-value variants, boolean shorthand, slot-keyed boolean shorthand, and dynamic or partially-uninspectable variant value records.
|
|
523
|
+
- Reports every repeated occurrence that should be lifted out of the variant so each value only contains classes that actually vary.
|
|
524
|
+
|
|
404
525
|
## Performance Notes
|
|
405
526
|
|
|
406
527
|
1. **Caching is automatic** - Results are cached by default
|