slot-variants 1.0.1 → 1.2.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 ADDED
@@ -0,0 +1,438 @@
1
+ # AGENTS.md - AI Agent Guide for slot-variants
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
+ ## Quick Reference
8
+
9
+ ```typescript
10
+ import { sv, cn, type VariantProps } from 'slot-variants';
11
+ ```
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
+
37
+ ## Best Practices
38
+
39
+ ### 1. Keep CSS Classes Mutually Exclusive
40
+
41
+ Define classes in base and variants such that they don't overlap. Use compound variants for intersecting classes to avoid duplication:
42
+
43
+ ```typescript
44
+ // GOOD - classes are mutually exclusive
45
+ const button = sv('btn', {
46
+ variants: {
47
+ size: { sm: 'text-sm', lg: 'text-lg' },
48
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
49
+ },
50
+ compoundVariants: [{ size: 'lg', intent: 'primary', class: 'font-bold' }]
51
+ });
52
+
53
+ // AVOID - duplicate classes across variants
54
+ const buttonBad = sv('btn font-medium', {
55
+ variants: {
56
+ size: { sm: 'text-sm font-medium', lg: 'text-lg font-medium' }
57
+ }
58
+ });
59
+ ```
60
+
61
+ ### 2. Use Boolean Shorthand for Simple States
62
+
63
+ For boolean variants (on/off), use shorthand to reduce boilerplate:
64
+
65
+ ```typescript
66
+ // GOOD - boolean shorthand
67
+ const button = sv('btn', {
68
+ variants: {
69
+ disabled: 'opacity-50 cursor-not-allowed',
70
+ loading: 'animate-spin pointer-events-none'
71
+ }
72
+ });
73
+
74
+ button({ disabled: true, loading: false });
75
+
76
+ // AVOID - verbose boolean record
77
+ const buttonVerbose = sv('btn', {
78
+ variants: {
79
+ disabled: {
80
+ true: 'opacity-50 cursor-not-allowed',
81
+ false: ''
82
+ }
83
+ }
84
+ });
85
+ ```
86
+
87
+ ### 3. Use Required Variants for Mandatory Props
88
+
89
+ When a variant must always be provided, use `requiredVariants`:
90
+
91
+ ```typescript
92
+ const button = sv('btn', {
93
+ variants: {
94
+ size: { sm: 'text-sm', lg: 'text-lg' },
95
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
96
+ },
97
+ requiredVariants: ['intent'] // intent must always be provided
98
+ });
99
+
100
+ // button() throws: Missing required variant: "intent"
101
+ button({ intent: 'primary' }); // OK
102
+ ```
103
+
104
+ ### 4. Use Slots for Multi-Element Components
105
+
106
+ For components with multiple elements (card, dialog, etc.), use slots:
107
+
108
+ ```typescript
109
+ const card = sv('border rounded-lg', {
110
+ slots: {
111
+ header: 'font-semibold px-4 py-2',
112
+ body: 'px-4 py-4',
113
+ footer: 'px-4 py-2 border-t'
114
+ }
115
+ });
116
+
117
+ const { base, header, body, footer } = card();
118
+ // Use: <div class={base}><header class={header}>...</div>
119
+ ```
120
+
121
+ ### 5. Use VariantProps Type for Component Props
122
+
123
+ Extract types for React/Preact/Vue component props:
124
+
125
+ ```typescript
126
+ const button = sv('btn', {
127
+ variants: {
128
+ size: { sm: 'text-sm', lg: 'text-lg' },
129
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
130
+ },
131
+ requiredVariants: ['intent']
132
+ });
133
+
134
+ type ButtonProps = VariantProps<typeof button>;
135
+ // { size?: 'sm' | 'lg' | undefined; intent: 'primary' | 'danger' }
136
+
137
+ // Exclude internal variants from props
138
+ type InternalButtonProps = VariantProps<typeof button, 'internalState'>;
139
+ ```
140
+
141
+ ### 6. Use Compound Variants for Conditional Combinations
142
+
143
+ Apply classes when multiple conditions are met:
144
+
145
+ ```typescript
146
+ const button = sv('btn', {
147
+ variants: {
148
+ size: { sm: 'text-sm', lg: 'text-lg' },
149
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
150
+ },
151
+ compoundVariants: [
152
+ { size: 'lg', intent: 'primary', class: 'font-bold uppercase' }
153
+ ]
154
+ });
155
+
156
+ button({ size: 'lg', intent: 'primary' });
157
+ // Includes: 'btn text-lg bg-blue-500 font-bold uppercase'
158
+ ```
159
+
160
+ ### 7. Use Function-Based Default Variants for Dynamic Defaults
161
+
162
+ For defaults that depend on other variant values:
163
+
164
+ ```typescript
165
+ const button = sv('btn', {
166
+ variants: {
167
+ size: { sm: 'text-sm', lg: 'text-lg' },
168
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
169
+ },
170
+ defaultVariants: {
171
+ size: 'sm',
172
+ intent: (props) => (props.size === 'lg' ? 'danger' : 'primary')
173
+ }
174
+ });
175
+ ```
176
+
177
+ ### 8. Use Post-Processing with tailwind-merge
178
+
179
+ For Tailwind projects, use `postProcess` to handle class conflicts:
180
+
181
+ ```typescript
182
+ import { sv } from 'slot-variants';
183
+ import { twMerge } from 'tailwind-merge';
184
+
185
+ const button = sv('px-4 py-2', {
186
+ variants: {
187
+ size: {
188
+ sm: 'px-2 py-1 text-sm',
189
+ lg: 'px-6 py-3 text-lg'
190
+ }
191
+ },
192
+ postProcess: twMerge
193
+ });
194
+
195
+ // tailwind-merge handles conflicting classes
196
+ button({ size: 'lg', class: 'px-2' });
197
+ // Result: 'px-2 py-3 text-lg' (not 'px-4 px-2 py-2')
198
+ ```
199
+
200
+ ### 9. Leverage Caching for Performance
201
+
202
+ The library caches results automatically (default 256 entries). Estimate the cache size based on the number of variant combinations and enlarge it accordingly:
203
+
204
+ ```typescript
205
+ // Estimate: size * intent * disabled = 2 * 2 * 2 = 8 combinations
206
+ const button = sv('btn', {
207
+ variants: {
208
+ size: { sm: 'text-sm', lg: 'text-lg' },
209
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' },
210
+ disabled: 'opacity-50 cursor-not-allowed'
211
+ },
212
+ cacheSize: 512 // increase cache for complex components
213
+ });
214
+
215
+ // Access cache methods
216
+ button.getCacheSize();
217
+ button.clearCache();
218
+ ```
219
+
220
+ Note: Using `class` or `className` prop bypasses caching.
221
+
222
+ ### 10. Use Presets for Reusable Variant Combinations
223
+
224
+ Define presets for frequently used combinations of variants (rarely needed):
225
+
226
+ ```typescript
227
+ const button = sv('btn', {
228
+ variants: {
229
+ size: { sm: 'text-sm', lg: 'text-lg' },
230
+ intent: { primary: 'bg-blue-500', danger: 'bg-red-500' },
231
+ rounded: { true: 'rounded-full', false: 'rounded-md' }
232
+ },
233
+ presets: {
234
+ primarySm: { size: 'sm', intent: 'primary', rounded: false },
235
+ cta: { size: 'lg', intent: 'primary', rounded: true }
236
+ }
237
+ });
238
+
239
+ button({ preset: 'cta' });
240
+ ```
241
+
242
+ ### 11. Use Introspection for Single Source of Truth
243
+
244
+ The returned function exposes configuration properties for runtime introspection. Use these to access variant/slot definitions and reuse them elsewhere, avoiding duplication:
245
+
246
+ ```typescript
247
+ const button = sv('btn', {
248
+ slots: {
249
+ icon: 'w-4 h-4'
250
+ },
251
+ variants: {
252
+ size: {
253
+ sm: 'text-sm',
254
+ lg: 'text-lg'
255
+ },
256
+ intent: {
257
+ primary: 'bg-blue-500',
258
+ danger: 'bg-red-500'
259
+ }
260
+ },
261
+ defaultVariants: {
262
+ size: 'sm'
263
+ },
264
+ requiredVariants: ['intent'],
265
+ presets: {
266
+ cta: { size: 'lg', intent: 'primary' }
267
+ }
268
+ });
269
+
270
+ button.variantKeys; // ['size', 'intent']
271
+ button.variants; // { size: { sm: 'text-sm', lg: 'text-lg' }, intent: { ... } }
272
+ button.slotKeys; // ['base', 'icon']
273
+ button.slots; // { icon: 'w-4 h-4' }
274
+ button.defaultVariants; // { size: 'sm' }
275
+ button.requiredVariants; // ['intent']
276
+ button.presetKeys; // ['cta']
277
+ button.presets; // { cta: { size: 'lg', intent: 'primary' } }
278
+ button.getVariantValues('size'); // ['sm', 'lg']
279
+ button.getVariantValues('intent'); // ['primary', 'danger']
280
+ ```
281
+
282
+ Use introspection to share variant/slot definitions with other parts of your codebase:
283
+
284
+ ```typescript
285
+ // variants.ts - centralize variant definitions
286
+ export const button = sv('btn font-medium rounded-lg', {
287
+ variants: {
288
+ size: {
289
+ sm: 'text-sm px-3 py-1.5',
290
+ md: 'text-base px-4 py-2',
291
+ lg: 'text-lg px-6 py-3'
292
+ },
293
+ intent: {
294
+ primary: 'bg-blue-500 text-white',
295
+ secondary: 'bg-gray-200 text-gray-800',
296
+ danger: 'bg-red-500 text-white'
297
+ }
298
+ },
299
+ defaultVariants: {
300
+ size: 'md',
301
+ intent: 'primary'
302
+ }
303
+ });
304
+
305
+ // Reuse variant keys for form validation
306
+ const validSizes = button.variantKeys.includes('size')
307
+ ? Object.keys(button.variants.size as Record<string, string>)
308
+ : [];
309
+ // validSizes: ['sm', 'md', 'lg']
310
+
311
+ // Reuse slot keys for component composition
312
+ const card = sv('card', {
313
+ slots: {
314
+ header: 'font-bold',
315
+ body: 'py-4'
316
+ }
317
+ });
318
+
319
+ // Dynamically render all slots
320
+ function renderCardWithSlots(slotClasses: ReturnType<typeof card>) {
321
+ return card.slotKeys.map((key) => (
322
+ <div key={key} className={slotClasses[key as keyof typeof slotClasses]}>
323
+ {key}
324
+ </div>
325
+ ));
326
+ }
327
+ ```
328
+
329
+ This approach ensures variant and slot definitions live in one place, making updates easy and reducing the risk of inconsistencies.
330
+
331
+ ## Common Patterns
332
+
333
+ ### React Component Pattern
334
+
335
+ ```typescript
336
+ // button.ts
337
+ import { sv, type VariantProps } from 'slot-variants';
338
+
339
+ const button = sv('btn font-medium rounded-lg', {
340
+ variants: {
341
+ size: {
342
+ sm: 'text-sm px-3 py-1.5',
343
+ md: 'text-base px-4 py-2',
344
+ lg: 'text-lg px-6 py-3'
345
+ },
346
+ intent: {
347
+ primary: 'bg-blue-500 text-white hover:bg-blue-600',
348
+ secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
349
+ danger: 'bg-red-500 text-white hover:bg-red-600'
350
+ },
351
+ disabled: {
352
+ true: 'opacity-50 cursor-not-allowed pointer-events-none'
353
+ }
354
+ },
355
+ defaultVariants: {
356
+ size: 'md',
357
+ intent: 'primary'
358
+ }
359
+ });
360
+
361
+ export type ButtonProps = VariantProps<typeof button>;
362
+
363
+ export const Button = ({ class: className, ...props }: ButtonProps & { class?: string }) => {
364
+ return <button className={button({ ...props, class: className })} />;
365
+ };
366
+ ```
367
+
368
+ ### Multi-Element Component Pattern
369
+
370
+ ```typescript
371
+ // card.ts
372
+ import { sv, type VariantProps } from 'slot-variants';
373
+
374
+ const card = sv('border rounded-lg shadow-sm', {
375
+ slots: {
376
+ header: 'font-semibold px-4 py-3 border-b',
377
+ body: 'px-4 py-4',
378
+ footer: 'px-4 py-3 border-t bg-gray-50'
379
+ },
380
+ variants: {
381
+ size: {
382
+ sm: { base: 'p-2', header: 'text-sm', body: 'text-sm' },
383
+ md: { base: 'p-4', header: 'text-base', body: 'text-base' },
384
+ lg: { base: 'p-6', header: 'text-lg', body: 'text-lg' }
385
+ },
386
+ elevated: {
387
+ true: { base: 'shadow-lg' }
388
+ }
389
+ },
390
+ compoundSlots: [{ slots: ['header', 'footer'], class: 'text-gray-600' }]
391
+ });
392
+
393
+ type CardProps = VariantProps<typeof card>;
394
+
395
+ export const Card = (props: CardProps) => {
396
+ const { base, header, body, footer } = card(props);
397
+ return { base, header, body, footer };
398
+ };
399
+ ```
400
+
401
+ ## Configuration Reference
402
+
403
+ | Option | Type | Description |
404
+ | ------------------ | -------------------------------- | --------------------------------- |
405
+ | `base` | `ClassValue` | Additional base classes |
406
+ | `variants` | `Record<string, VariantConfig>` | Variant definitions |
407
+ | `slots` | `Record<string, ClassValue>` | Named slot definitions |
408
+ | `compoundVariants` | `CompoundVariant[]` | Conditional class combinations |
409
+ | `compoundSlots` | `CompoundSlot[]` | Multi-slot conditional classes |
410
+ | `defaultVariants` | `Record<string, Value>` | Static or function-based defaults |
411
+ | `requiredVariants` | `string[]` | Mandatory variant names |
412
+ | `presets` | `Record<string, Partial<Props>>` | Named preset combinations |
413
+ | `postProcess` | `(className: string) => string` | Class transformation |
414
+ | `cacheSize` | `number` | Cache size (default: 256) |
415
+
416
+ ## Exported Types
417
+
418
+ - `ClassValue` - Valid input for `cn()` (string, array, object, boolean, null, undefined)
419
+ - `VariantProps<T, E>` - Extract variant props from an `sv()` return, optionally excluding keys
420
+ - `VariantValue<T, K>` - Extract the value union for a single variant key, without `undefined`
421
+ - `SlotClassProps<T>` - Extract the per-slot class injection shape from an `sv()` return type
422
+
423
+ ## Imports
424
+
425
+ ```typescript
426
+ // Default exports
427
+ import { sv, cn } from 'slot-variants';
428
+
429
+ // Types only
430
+ import type { VariantProps, VariantValue, SlotClassProps, ClassValue } from 'slot-variants';
431
+ ```
432
+
433
+ ## Performance Notes
434
+
435
+ 1. **Caching is automatic** - Results are cached by default
436
+ 2. **Class prop bypasses cache** - Dynamic classes at runtime should use `class` prop
437
+ 3. **Complex components benefit from larger cache** - Increase `cacheSize` for components with many variant combinations
438
+ 4. **Prefer static defaults** - Function-based defaults are called on every invocation