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 +438 -0
- package/README.md +313 -14
- package/dist/index.cjs +323 -1
- package/dist/index.d.ts +20 -8
- package/dist/index.js +295 -1
- package/logo.svg +4 -0
- package/package.json +5 -4
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
|