slot-variants 1.4.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 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`:
@@ -670,6 +737,16 @@ The `postProcess` function is applied to each slot's final class string independ
670
737
 
671
738
  Results are cached automatically for performance. The default cache size is **256** entries.
672
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).
749
+
673
750
  ```typescript
674
751
  const button = sv('btn', {
675
752
  variants: {
@@ -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,15 +976,15 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
896
976
 
897
977
  ### Rules
898
978
 
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.
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
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 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.
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
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
 
@@ -1111,11 +1191,11 @@ Key differences to be aware of:
1111
1191
 
1112
1192
  | Feature | tailwind-variants | slot-variants |
1113
1193
  | --- | --- | --- |
1114
- | Slot return type | Functions: `slot({ class: '...' })` | Strings: `slot` is already a `string` |
1194
+ | Slot return type | Always functions: `slot({ class: '...' })` | Strings by default; functions for slots listed in `multiSlots` |
1115
1195
  | `extend` (composition) | Supported | Not supported |
1116
1196
  | Built-in `twMerge` | Enabled by default | Use `postProcess: twMerge` |
1117
1197
 
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:
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:
1119
1199
 
1120
1200
  ```typescript
1121
1201
  // tailwind-variants