@svelte-atoms/core 1.0.0-alpha.27 → 1.0.0-alpha.28
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 +856 -645
- package/dist/components/accordion/accordion-root.svelte +61 -61
- package/dist/components/accordion/item/accordion-item-body.svelte +42 -42
- package/dist/components/accordion/item/accordion-item-header.svelte +50 -50
- package/dist/components/accordion/item/accordion-item-indicator.svelte +50 -50
- package/dist/components/accordion/item/accordion-item-root.svelte +65 -65
- package/dist/components/alert/alert-actions.svelte +2 -1
- package/dist/components/alert/alert-close-button.svelte +18 -20
- package/dist/components/alert/alert-content.svelte +2 -1
- package/dist/components/alert/alert-description.svelte +2 -1
- package/dist/components/alert/alert-icon.svelte +2 -1
- package/dist/components/alert/alert-root.svelte +3 -2
- package/dist/components/alert/alert-title.svelte +2 -1
- package/dist/components/alert/alert.stories.svelte +401 -40
- package/dist/components/alert/alert.stories.svelte.d.ts +2 -5
- package/dist/components/atom/html-atom.svelte +205 -201
- package/dist/components/atom/snippet-renderer.svelte +5 -0
- package/dist/components/atom/snippet-renderer.svelte.d.ts +5 -0
- package/dist/components/avatar/avatar.stories.svelte.d.ts +1 -1
- package/dist/components/badge/badge.stories.svelte.d.ts +1 -1
- package/dist/components/breadcrumb/breadcrumb.stories.svelte.d.ts +1 -1
- package/dist/components/button/button.stories.svelte +60 -57
- package/dist/components/calendar/atoms.d.ts +5 -0
- package/dist/components/calendar/atoms.js +5 -0
- package/dist/components/calendar/bond.svelte.d.ts +72 -0
- package/dist/components/calendar/bond.svelte.js +132 -0
- package/dist/components/calendar/calendar-body.svelte +107 -0
- package/dist/components/calendar/calendar-body.svelte.d.ts +8 -0
- package/dist/components/calendar/calendar-day.svelte +97 -0
- package/dist/components/calendar/calendar-day.svelte.d.ts +4 -0
- package/dist/components/calendar/calendar-header.svelte +33 -0
- package/dist/components/calendar/calendar-header.svelte.d.ts +7 -0
- package/dist/components/calendar/calendar-root.svelte +208 -0
- package/dist/components/calendar/calendar-root.svelte.d.ts +8 -0
- package/dist/components/calendar/calendar-week-day.svelte +34 -0
- package/dist/components/calendar/calendar-week-day.svelte.d.ts +9 -0
- package/dist/components/calendar/calendar.css +26 -0
- package/dist/components/calendar/calendar.stories.svelte +36 -0
- package/dist/components/calendar/calendar.stories.svelte.d.ts +6 -0
- package/dist/components/calendar/index.d.ts +4 -0
- package/dist/components/calendar/index.js +4 -0
- package/dist/components/calendar/runes.svelte.d.ts +3 -0
- package/dist/components/calendar/runes.svelte.js +25 -0
- package/dist/components/calendar/types.d.ts +62 -0
- package/dist/components/calendar/types.js +1 -0
- package/dist/components/card/card-body.svelte +39 -39
- package/dist/components/card/card-description.svelte +41 -41
- package/dist/components/card/card-footer.svelte +41 -41
- package/dist/components/card/card-header.svelte +41 -41
- package/dist/components/card/card-media.svelte +41 -41
- package/dist/components/card/card-root.svelte +91 -91
- package/dist/components/card/card-subtitle.svelte +41 -41
- package/dist/components/card/card-title.svelte +45 -45
- package/dist/components/collapsible/collapsible-body.svelte +39 -39
- package/dist/components/collapsible/collapsible-header.svelte +39 -39
- package/dist/components/collapsible/collapsible-indicator.svelte +50 -50
- package/dist/components/collapsible/collapsible-root.svelte +66 -66
- package/dist/components/combobox/combobox-root.svelte +65 -65
- package/dist/components/container/container.stories.svelte.d.ts +1 -1
- package/dist/components/contextmenu/contextmenu-trigger.svelte.d.ts +1 -1
- package/dist/components/datagrid/bond.svelte.d.ts +2 -2
- package/dist/components/datagrid/datagrid-body.svelte +37 -37
- package/dist/components/datagrid/datagrid-checkbox.svelte +101 -101
- package/dist/components/datagrid/datagrid-footer.svelte +34 -34
- package/dist/components/datagrid/datagrid-header.svelte +49 -49
- package/dist/components/datagrid/datagrid-root.svelte +59 -59
- package/dist/components/datagrid/td/datagrid-td.svelte +66 -66
- package/dist/components/datagrid/th/datagrid-th.svelte +106 -106
- package/dist/components/datagrid/tr/datagrid-tr.svelte +88 -88
- package/dist/components/date-picker/atoms.d.ts +7 -0
- package/dist/components/date-picker/atoms.js +7 -0
- package/dist/components/date-picker/bond.svelte.d.ts +67 -0
- package/dist/components/date-picker/bond.svelte.js +174 -0
- package/dist/components/date-picker/date-picker-calendar.svelte +42 -0
- package/dist/components/date-picker/date-picker-calendar.svelte.d.ts +7 -0
- package/dist/components/date-picker/date-picker-header.svelte +105 -0
- package/dist/components/date-picker/date-picker-header.svelte.d.ts +7 -0
- package/dist/components/date-picker/date-picker-months.svelte +150 -0
- package/dist/components/date-picker/date-picker-months.svelte.d.ts +7 -0
- package/dist/components/date-picker/date-picker-root.svelte +94 -0
- package/dist/components/date-picker/date-picker-root.svelte.d.ts +17 -0
- package/dist/components/date-picker/date-picker-years.svelte +214 -0
- package/dist/components/date-picker/date-picker-years.svelte.d.ts +7 -0
- package/dist/components/date-picker/date-picker.stories.svelte +51 -0
- package/dist/components/date-picker/date-picker.stories.svelte.d.ts +3 -0
- package/dist/components/date-picker/index.d.ts +3 -0
- package/dist/components/date-picker/index.js +3 -0
- package/dist/components/date-picker/types.d.ts +1 -0
- package/dist/components/date-picker/types.js +1 -0
- package/dist/components/dialog/dialog-body.svelte +39 -39
- package/dist/components/dialog/dialog-close-button.svelte +58 -58
- package/dist/components/dialog/dialog-content.svelte +62 -62
- package/dist/components/dialog/dialog-description.svelte +40 -40
- package/dist/components/dialog/dialog-footer.svelte +39 -39
- package/dist/components/dialog/dialog-header.svelte +39 -39
- package/dist/components/dialog/dialog-root.svelte +110 -110
- package/dist/components/dialog/dialog-title.svelte +41 -41
- package/dist/components/drawer/drawer-backdrop.svelte +38 -38
- package/dist/components/drawer/drawer-body.svelte +42 -42
- package/dist/components/drawer/drawer-content.svelte +42 -42
- package/dist/components/drawer/drawer-description.svelte +44 -44
- package/dist/components/drawer/drawer-footer.svelte +41 -41
- package/dist/components/drawer/drawer-header.svelte +43 -43
- package/dist/components/drawer/drawer-root.svelte +93 -93
- package/dist/components/drawer/drawer-title.svelte +44 -44
- package/dist/components/dropdown/dropdown-query.svelte +54 -54
- package/dist/components/dropdown/dropdown-root.svelte +59 -59
- package/dist/components/dropdown/dropdown-trigger.svelte +41 -41
- package/dist/components/dropdown/dropdown-value.svelte +60 -60
- package/dist/components/element/html-element.svelte +85 -85
- package/dist/components/form/bond.svelte.d.ts +1 -1
- package/dist/components/form/field/field-control.svelte +48 -48
- package/dist/components/form/field/field-label.svelte +24 -24
- package/dist/components/form/field/field-root.svelte +59 -59
- package/dist/components/icon/icon.svelte +44 -44
- package/dist/components/image/image.stories.svelte.d.ts +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/input/input-control.svelte +103 -103
- package/dist/components/label/label.svelte +25 -25
- package/dist/components/popover/popover-arrow.svelte +111 -111
- package/dist/components/popover/popover-content.svelte +46 -7
- package/dist/components/popover/popover-root.svelte +48 -49
- package/dist/components/popover/popover.stories.svelte +52 -67
- package/dist/components/portal/portal-root.svelte +83 -83
- package/dist/components/portal/teleport.svelte +50 -50
- package/dist/components/qr-code/index.d.ts +1 -0
- package/dist/components/qr-code/index.js +1 -0
- package/dist/components/qr-code/qr-code.stories.svelte +24 -0
- package/dist/components/qr-code/qr-code.stories.svelte.d.ts +26 -0
- package/dist/components/qr-code/qr-code.svelte +25 -0
- package/dist/components/qr-code/qr-code.svelte.d.ts +6 -0
- package/dist/components/radio/radio.svelte +109 -109
- package/dist/components/radio/types.svelte.d.ts +1 -1
- package/dist/components/scrollable/scrollable-container.svelte +82 -82
- package/dist/components/scrollable/scrollable-content.svelte +41 -41
- package/dist/components/scrollable/scrollable-root.svelte +100 -100
- package/dist/components/scrollable/scrollable-thumb.svelte +75 -75
- package/dist/components/scrollable/scrollable-track.svelte +59 -59
- package/dist/components/scrollable/scrollable.stories.svelte.d.ts +1 -1
- package/dist/components/tabs/tab/tab-body.svelte +52 -52
- package/dist/components/tabs/tab/tab-description.svelte +41 -41
- package/dist/components/tabs/tab/tab-header.svelte +71 -71
- package/dist/components/tabs/tab/tab-root.svelte +86 -86
- package/dist/components/toast/toast-description.svelte +38 -38
- package/dist/components/toast/toast-root.svelte +61 -61
- package/dist/components/toast/toast-title.svelte +35 -35
- package/dist/components/tree/tree-body.svelte +39 -39
- package/dist/components/tree/tree-header.svelte +54 -54
- package/dist/components/tree/tree-indicator.svelte +40 -40
- package/dist/components/tree/tree-root.svelte +65 -65
- package/dist/components/virtual/virtual-root.svelte +239 -239
- package/dist/context/preset.svelte.d.ts +1 -1
- package/dist/icons/icon-arrow-down.svelte.d.ts +1 -1
- package/dist/icons/icon-checkmark.svelte.d.ts +1 -1
- package/dist/icons/icon-close.svelte.d.ts +1 -1
- package/dist/icons/icon-more-vert.svelte.d.ts +1 -1
- package/dist/runes/container.svelte.d.ts +2 -2
- package/dist/shared/bond.svelte.d.ts +1 -1
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +2 -1
- package/llm/variants.md +1261 -712
- package/package.json +464 -437
package/llm/variants.md
CHANGED
|
@@ -1,712 +1,1261 @@
|
|
|
1
|
-
# Variant System
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
The variant system in @svelte-atoms/core provides a powerful way to define component styling variations. It's deeply integrated with the preset system, allowing both global theming and local customization.
|
|
6
|
-
|
|
7
|
-
## Problem Statement
|
|
8
|
-
|
|
9
|
-
Currently, creating component variants (size, variant, appearance, etc.) requires:
|
|
10
|
-
|
|
11
|
-
```svelte
|
|
12
|
-
<script>
|
|
13
|
-
let { variant = 'primary', size = 'md' } = $props();
|
|
14
|
-
|
|
15
|
-
const variantClasses = {
|
|
16
|
-
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
17
|
-
secondary: 'bg-gray-500 text-white hover:bg-gray-600',
|
|
18
|
-
danger: 'bg-red-500 text-white hover:bg-red-600'
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const sizeClasses = {
|
|
22
|
-
sm: 'px-2 py-1 text-sm',
|
|
23
|
-
md: 'px-4 py-2 text-base',
|
|
24
|
-
lg: 'px-6 py-3 text-lg'
|
|
25
|
-
};
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<HtmlAtom class={`${variantClasses[variant]} ${sizeClasses[size]}`} />
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**Pain points:**
|
|
32
|
-
|
|
33
|
-
- Repeated boilerplate in every component
|
|
34
|
-
- No access to component state (bond) for reactive variants
|
|
35
|
-
- Can't return attributes, only classes
|
|
36
|
-
- Not type-safe
|
|
37
|
-
|
|
38
|
-
## Integration with Preset System
|
|
39
|
-
|
|
40
|
-
**Key Feature:** Variants are now integrated with the preset system, enabling global variant definitions that can be overridden locally.
|
|
41
|
-
|
|
42
|
-
### Preset Structure
|
|
43
|
-
|
|
44
|
-
Presets support the full variant structure for comprehensive theming:
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
// Preset: Full variant support
|
|
48
|
-
{
|
|
49
|
-
class: 'base-classes',
|
|
50
|
-
variants: {
|
|
51
|
-
variant: { primary: '...', secondary: '...' },
|
|
52
|
-
size: { sm: '...', md: '...', lg: '...' }
|
|
53
|
-
},
|
|
54
|
-
compounds: [
|
|
55
|
-
{ variant: 'primary', size: 'lg', class: 'shadow-lg' }
|
|
56
|
-
],
|
|
57
|
-
defaults: {
|
|
58
|
-
variant: 'primary',
|
|
59
|
-
size: 'md'
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Component: Same structure, overrides/extends preset
|
|
64
|
-
{
|
|
65
|
-
class: 'component-base',
|
|
66
|
-
variants: { ... },
|
|
67
|
-
compoundss: [...],
|
|
68
|
-
defaults: { ... }
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**Merge Behavior:**
|
|
73
|
-
|
|
74
|
-
- `variants`: Deep merged (component extends preset)
|
|
75
|
-
- `compounds`: Concatenated (preset first, then component compounds)
|
|
76
|
-
- `defaults`: Deep merged (component overrides preset)
|
|
77
|
-
|
|
78
|
-
### Architecture
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
Preset (Global Theme)
|
|
82
|
-
├─ class: ClassValue (base styling)
|
|
83
|
-
├─ variants: Record<string, Record<string, ClassValue>>
|
|
84
|
-
│ ├─ variant: { primary: '...', secondary: '...' }
|
|
85
|
-
│ └─ size: { sm: '...', md: '...', lg: '...' }
|
|
86
|
-
├─ compounds: Array<CompoundVariant> (conditional styling)
|
|
87
|
-
├─ defaults: Record<string, string> (default values)
|
|
88
|
-
├─ base: Component (component override)
|
|
89
|
-
├─ as: string (element type)
|
|
90
|
-
└─ ...other props
|
|
91
|
-
|
|
92
|
-
Component (Local)
|
|
93
|
-
├─ variants: VariantDefinition (extends/overrides preset)
|
|
94
|
-
│ ├─ class: ClassValue (component-specific base)
|
|
95
|
-
│ ├─ variants: { ... } (extends/overrides preset variants)
|
|
96
|
-
│ ├─ compounds: [...] (appended to preset)
|
|
97
|
-
│ └─ defaults: {...} (overrides preset defaults)
|
|
98
|
-
└─ variant props (size, variant, etc.)
|
|
99
|
-
|
|
100
|
-
Final Output = merge(preset, component)
|
|
101
|
-
- variants: deep merged
|
|
102
|
-
- compounds: concatenated (preset + component)
|
|
103
|
-
- defaults: deep merged (component overrides)
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Hierarchy
|
|
107
|
-
|
|
108
|
-
The system merges variants in this order (later overrides earlier):
|
|
109
|
-
|
|
110
|
-
1. **Preset variants** - Global theme variants
|
|
111
|
-
2. **Component variants** - Local component-specific variants
|
|
112
|
-
3. **Props** - Runtime variant prop values
|
|
113
|
-
|
|
114
|
-
### Example: Global + Local Variants
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
// +layout.svelte - Global theme
|
|
118
|
-
import { setPreset } from '@svelte-atoms/core/context';
|
|
119
|
-
|
|
120
|
-
setPreset({
|
|
121
|
-
button: () => ({
|
|
122
|
-
class: 'font-medium transition-colors rounded-md focus:outline-none focus:ring-2',
|
|
123
|
-
variants: {
|
|
124
|
-
variant: {
|
|
125
|
-
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
126
|
-
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90',
|
|
127
|
-
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
128
|
-
},
|
|
129
|
-
size: {
|
|
130
|
-
sm: 'h-8 px-3 text-xs',
|
|
131
|
-
md: 'h-10 px-4 text-sm',
|
|
132
|
-
lg: 'h-12 px-6 text-base'
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
compounds: [
|
|
136
|
-
{
|
|
137
|
-
variant: 'primary',
|
|
138
|
-
size: 'lg',
|
|
139
|
-
class: 'shadow-lg font-semibold'
|
|
140
|
-
}
|
|
141
|
-
],
|
|
142
|
-
defaults: {
|
|
143
|
-
variant: 'primary',
|
|
144
|
-
size: 'md'
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
```svelte
|
|
151
|
-
<!-- Button.svelte - Local override -->
|
|
152
|
-
<script>
|
|
153
|
-
import { HtmlAtom } from '@svelte-atoms/core';
|
|
154
|
-
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
155
|
-
|
|
156
|
-
// Extend preset variants with additional local variants
|
|
157
|
-
const localVariants = defineVariants({
|
|
158
|
-
variants: {
|
|
159
|
-
variant: {
|
|
160
|
-
// Add new variant not in preset
|
|
161
|
-
ghost: 'hover:bg-accent hover:text-accent-foreground'
|
|
162
|
-
},
|
|
163
|
-
// Add new variant key
|
|
164
|
-
loading: {
|
|
165
|
-
true: 'opacity-50 cursor-wait',
|
|
166
|
-
false: ''
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
let {
|
|
172
|
-
variant = 'primary',
|
|
173
|
-
size = 'md',
|
|
174
|
-
loading = false,
|
|
175
|
-
...props
|
|
176
|
-
} = $props();
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<HtmlAtom
|
|
180
|
-
preset="button" <!-- Uses global preset -->
|
|
181
|
-
variants={localVariants} <!-- Merges with preset -->
|
|
182
|
-
{variant}
|
|
183
|
-
{size}
|
|
184
|
-
{loading}
|
|
185
|
-
{...props}
|
|
186
|
-
>
|
|
187
|
-
{@render children?.()}
|
|
188
|
-
</HtmlAtom>
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Result:** The button will have:
|
|
192
|
-
|
|
193
|
-
- Base classes from preset
|
|
194
|
-
- Preset variant classes (primary, sm/md/lg)
|
|
195
|
-
- Local variant classes (ghost, loading)
|
|
196
|
-
- Merged intelligently - local overrides preset
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
{
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1
|
+
# Variant System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The variant system in @svelte-atoms/core provides a powerful way to define component styling variations. It's deeply integrated with the preset system, allowing both global theming and local customization.
|
|
6
|
+
|
|
7
|
+
## Problem Statement
|
|
8
|
+
|
|
9
|
+
Currently, creating component variants (size, variant, appearance, etc.) requires:
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<script>
|
|
13
|
+
let { variant = 'primary', size = 'md' } = $props();
|
|
14
|
+
|
|
15
|
+
const variantClasses = {
|
|
16
|
+
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
17
|
+
secondary: 'bg-gray-500 text-white hover:bg-gray-600',
|
|
18
|
+
danger: 'bg-red-500 text-white hover:bg-red-600'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const sizeClasses = {
|
|
22
|
+
sm: 'px-2 py-1 text-sm',
|
|
23
|
+
md: 'px-4 py-2 text-base',
|
|
24
|
+
lg: 'px-6 py-3 text-lg'
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<HtmlAtom class={`${variantClasses[variant]} ${sizeClasses[size]}`} />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Pain points:**
|
|
32
|
+
|
|
33
|
+
- Repeated boilerplate in every component
|
|
34
|
+
- No access to component state (bond) for reactive variants
|
|
35
|
+
- Can't return attributes, only classes
|
|
36
|
+
- Not type-safe
|
|
37
|
+
|
|
38
|
+
## Integration with Preset System
|
|
39
|
+
|
|
40
|
+
**Key Feature:** Variants are now integrated with the preset system, enabling global variant definitions that can be overridden locally.
|
|
41
|
+
|
|
42
|
+
### Preset Structure
|
|
43
|
+
|
|
44
|
+
Presets support the full variant structure for comprehensive theming:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Preset: Full variant support
|
|
48
|
+
{
|
|
49
|
+
class: 'base-classes',
|
|
50
|
+
variants: {
|
|
51
|
+
variant: { primary: '...', secondary: '...' },
|
|
52
|
+
size: { sm: '...', md: '...', lg: '...' }
|
|
53
|
+
},
|
|
54
|
+
compounds: [
|
|
55
|
+
{ variant: 'primary', size: 'lg', class: 'shadow-lg' }
|
|
56
|
+
],
|
|
57
|
+
defaults: {
|
|
58
|
+
variant: 'primary',
|
|
59
|
+
size: 'md'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Component: Same structure, overrides/extends preset
|
|
64
|
+
{
|
|
65
|
+
class: 'component-base',
|
|
66
|
+
variants: { ... },
|
|
67
|
+
compoundss: [...],
|
|
68
|
+
defaults: { ... }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Merge Behavior:**
|
|
73
|
+
|
|
74
|
+
- `variants`: Deep merged (component extends preset)
|
|
75
|
+
- `compounds`: Concatenated (preset first, then component compounds)
|
|
76
|
+
- `defaults`: Deep merged (component overrides preset)
|
|
77
|
+
|
|
78
|
+
### Architecture
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Preset (Global Theme)
|
|
82
|
+
├─ class: ClassValue (base styling)
|
|
83
|
+
├─ variants: Record<string, Record<string, ClassValue>>
|
|
84
|
+
│ ├─ variant: { primary: '...', secondary: '...' }
|
|
85
|
+
│ └─ size: { sm: '...', md: '...', lg: '...' }
|
|
86
|
+
├─ compounds: Array<CompoundVariant> (conditional styling)
|
|
87
|
+
├─ defaults: Record<string, string> (default values)
|
|
88
|
+
├─ base: Component (component override)
|
|
89
|
+
├─ as: string (element type)
|
|
90
|
+
└─ ...other props
|
|
91
|
+
|
|
92
|
+
Component (Local)
|
|
93
|
+
├─ variants: VariantDefinition (extends/overrides preset)
|
|
94
|
+
│ ├─ class: ClassValue (component-specific base)
|
|
95
|
+
│ ├─ variants: { ... } (extends/overrides preset variants)
|
|
96
|
+
│ ├─ compounds: [...] (appended to preset)
|
|
97
|
+
│ └─ defaults: {...} (overrides preset defaults)
|
|
98
|
+
└─ variant props (size, variant, etc.)
|
|
99
|
+
|
|
100
|
+
Final Output = merge(preset, component)
|
|
101
|
+
- variants: deep merged
|
|
102
|
+
- compounds: concatenated (preset + component)
|
|
103
|
+
- defaults: deep merged (component overrides)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Hierarchy
|
|
107
|
+
|
|
108
|
+
The system merges variants in this order (later overrides earlier):
|
|
109
|
+
|
|
110
|
+
1. **Preset variants** - Global theme variants
|
|
111
|
+
2. **Component variants** - Local component-specific variants
|
|
112
|
+
3. **Props** - Runtime variant prop values
|
|
113
|
+
|
|
114
|
+
### Example: Global + Local Variants
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// +layout.svelte - Global theme
|
|
118
|
+
import { setPreset } from '@svelte-atoms/core/context';
|
|
119
|
+
|
|
120
|
+
setPreset({
|
|
121
|
+
button: () => ({
|
|
122
|
+
class: 'font-medium transition-colors rounded-md focus:outline-none focus:ring-2',
|
|
123
|
+
variants: {
|
|
124
|
+
variant: {
|
|
125
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
126
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90',
|
|
127
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
128
|
+
},
|
|
129
|
+
size: {
|
|
130
|
+
sm: 'h-8 px-3 text-xs',
|
|
131
|
+
md: 'h-10 px-4 text-sm',
|
|
132
|
+
lg: 'h-12 px-6 text-base'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
compounds: [
|
|
136
|
+
{
|
|
137
|
+
variant: 'primary',
|
|
138
|
+
size: 'lg',
|
|
139
|
+
class: 'shadow-lg font-semibold'
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
defaults: {
|
|
143
|
+
variant: 'primary',
|
|
144
|
+
size: 'md'
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```svelte
|
|
151
|
+
<!-- Button.svelte - Local override -->
|
|
152
|
+
<script>
|
|
153
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
154
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
155
|
+
|
|
156
|
+
// Extend preset variants with additional local variants
|
|
157
|
+
const localVariants = defineVariants({
|
|
158
|
+
variants: {
|
|
159
|
+
variant: {
|
|
160
|
+
// Add new variant not in preset
|
|
161
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground'
|
|
162
|
+
},
|
|
163
|
+
// Add new variant key
|
|
164
|
+
loading: {
|
|
165
|
+
true: 'opacity-50 cursor-wait',
|
|
166
|
+
false: ''
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let {
|
|
172
|
+
variant = 'primary',
|
|
173
|
+
size = 'md',
|
|
174
|
+
loading = false,
|
|
175
|
+
...props
|
|
176
|
+
} = $props();
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<HtmlAtom
|
|
180
|
+
preset="button" <!-- Uses global preset -->
|
|
181
|
+
variants={localVariants} <!-- Merges with preset -->
|
|
182
|
+
{variant}
|
|
183
|
+
{size}
|
|
184
|
+
{loading}
|
|
185
|
+
{...props}
|
|
186
|
+
>
|
|
187
|
+
{@render children?.()}
|
|
188
|
+
</HtmlAtom>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Result:** The button will have:
|
|
192
|
+
|
|
193
|
+
- Base classes from preset
|
|
194
|
+
- Preset variant classes (primary, sm/md/lg)
|
|
195
|
+
- Local variant classes (ghost, loading)
|
|
196
|
+
- Merged intelligently - local overrides preset
|
|
197
|
+
|
|
198
|
+
## Basic Usage
|
|
199
|
+
|
|
200
|
+
**Key Pattern:** `defineVariants()` returns a **function** that you pass to `HtmlAtom`. The component calls this function internally with bond and props.
|
|
201
|
+
|
|
202
|
+
```svelte
|
|
203
|
+
<script lang="ts">
|
|
204
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
205
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
206
|
+
|
|
207
|
+
// defineVariants returns a FUNCTION
|
|
208
|
+
const variants = defineVariants({
|
|
209
|
+
class: 'rounded-md font-medium transition-colors',
|
|
210
|
+
variants: {
|
|
211
|
+
variant: {
|
|
212
|
+
primary: {
|
|
213
|
+
class: 'bg-blue-500 text-white hover:bg-blue-600'
|
|
214
|
+
},
|
|
215
|
+
secondary: {
|
|
216
|
+
class: 'bg-gray-500 text-white hover:bg-gray-600'
|
|
217
|
+
},
|
|
218
|
+
danger: {
|
|
219
|
+
class: 'bg-red-500 text-white hover:bg-red-600'
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
size: {
|
|
223
|
+
sm: 'px-2 py-1 text-sm',
|
|
224
|
+
md: 'px-4 py-2 text-base',
|
|
225
|
+
lg: 'px-6 py-3 text-lg'
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
compounds: [
|
|
229
|
+
{
|
|
230
|
+
variant: 'primary',
|
|
231
|
+
size: 'lg',
|
|
232
|
+
class: 'shadow-lg' // Applied when both conditions match
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
defaults: {
|
|
236
|
+
variant: 'primary',
|
|
237
|
+
size: 'md'
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
let { variant, size, ...props } = $props();
|
|
242
|
+
</script>
|
|
243
|
+
|
|
244
|
+
<!-- Pass the variant FUNCTION to HtmlAtom -->
|
|
245
|
+
<!-- HtmlAtom will call it internally: variants(bond, { variant, size }) -->
|
|
246
|
+
<HtmlAtom {variants} {variant} {size} {...props}>
|
|
247
|
+
{@render children?.()}
|
|
248
|
+
</HtmlAtom>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**How it works:**
|
|
252
|
+
|
|
253
|
+
1. `defineVariants(config)` → returns a function
|
|
254
|
+
2. You pass this function to `HtmlAtom` via `{variants}`
|
|
255
|
+
3. `HtmlAtom` internally calls `variants(bond, restProps)`
|
|
256
|
+
4. The function returns `{ class: [...], ...attributes }`
|
|
257
|
+
5. `HtmlAtom` applies these to the rendered element
|
|
258
|
+
|
|
259
|
+
## Key Features
|
|
260
|
+
|
|
261
|
+
### 1. Access to Component State (Bond)
|
|
262
|
+
|
|
263
|
+
Variant values can be functions that receive the component's bond for reactive styling:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const accordionVariants = defineVariants({
|
|
267
|
+
class: 'border rounded-md transition-all',
|
|
268
|
+
variants: {
|
|
269
|
+
state: {
|
|
270
|
+
open: (bond) => ({
|
|
271
|
+
class: bond?.state?.isOpen ? 'bg-blue-50 border-blue-200' : 'bg-white',
|
|
272
|
+
'aria-expanded': bond?.state?.isOpen,
|
|
273
|
+
'data-state': bond?.state?.isOpen ? 'open' : 'closed'
|
|
274
|
+
}),
|
|
275
|
+
disabled: (bond) => ({
|
|
276
|
+
class: bond?.state?.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
|
277
|
+
'aria-disabled': bond?.state?.disabled
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Usage in component:
|
|
284
|
+
let { state = 'open', ...props } = $props();
|
|
285
|
+
|
|
286
|
+
<HtmlAtom {variants} {state} {...props}>
|
|
287
|
+
{@render children?.()}
|
|
288
|
+
</HtmlAtom>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 2. Return Both Classes and Attributes
|
|
292
|
+
|
|
293
|
+
Variants can return not just classes, but any HTML attributes:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const buttonVariants = defineVariants({
|
|
297
|
+
variants: {
|
|
298
|
+
variant: {
|
|
299
|
+
primary: {
|
|
300
|
+
class: 'bg-blue-500 text-white',
|
|
301
|
+
'aria-label': 'Primary action',
|
|
302
|
+
'data-variant': 'primary'
|
|
303
|
+
},
|
|
304
|
+
danger: (bond) => ({
|
|
305
|
+
class: bond?.state?.disabled ? 'bg-red-300' : 'bg-red-500',
|
|
306
|
+
'aria-disabled': bond?.state?.disabled,
|
|
307
|
+
role: 'button'
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// When called by HtmlAtom, returns: { class: [...], 'aria-label': '...', 'data-variant': '...', ... }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### 3. Compound Variants
|
|
317
|
+
|
|
318
|
+
Apply additional styling when multiple conditions match:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const alertVariants = defineVariants({
|
|
322
|
+
class: 'rounded-lg p-4 border',
|
|
323
|
+
variants: {
|
|
324
|
+
variant: {
|
|
325
|
+
error: 'bg-red-50 border-red-200 text-red-900'
|
|
326
|
+
},
|
|
327
|
+
size: {
|
|
328
|
+
lg: 'text-lg'
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
compounds: [
|
|
332
|
+
{
|
|
333
|
+
variant: 'error',
|
|
334
|
+
size: 'lg',
|
|
335
|
+
class: 'font-bold', // Only applied when both variant=error AND size=lg
|
|
336
|
+
role: 'alert'
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### 4. Type Safety
|
|
343
|
+
|
|
344
|
+
Full TypeScript support with automatic type inference:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
type ButtonVariants = VariantPropsType<typeof buttonVariants>;
|
|
348
|
+
// Inferred type: { variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg' }
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 5. Default Variants
|
|
352
|
+
|
|
353
|
+
Specify default values that are used when no variant is provided:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const buttonVariants = defineVariants({
|
|
357
|
+
class: 'rounded-md',
|
|
358
|
+
variants: {
|
|
359
|
+
variant: {
|
|
360
|
+
primary: 'bg-blue-500',
|
|
361
|
+
secondary: 'bg-gray-500'
|
|
362
|
+
},
|
|
363
|
+
size: {
|
|
364
|
+
sm: 'text-sm',
|
|
365
|
+
md: 'text-base',
|
|
366
|
+
lg: 'text-lg'
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
defaults: {
|
|
370
|
+
variant: 'primary', // Used if variant prop is undefined
|
|
371
|
+
size: 'md' // Used if size prop is undefined
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Pass to HtmlAtom - uses defaults when props not provided
|
|
376
|
+
<HtmlAtom {variants} {...props}>
|
|
377
|
+
{@render children?.()}
|
|
378
|
+
</HtmlAtom>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Complete Examples
|
|
382
|
+
|
|
383
|
+
### Example 1: Preset-based Button (Recommended)
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// Theme.svelte - Define global button variants
|
|
387
|
+
import { setPreset } from '@svelte-atoms/core/context';
|
|
388
|
+
|
|
389
|
+
setPreset({
|
|
390
|
+
button: () => ({
|
|
391
|
+
class:
|
|
392
|
+
'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
393
|
+
variants: {
|
|
394
|
+
variant: {
|
|
395
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
396
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
397
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
398
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
399
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
400
|
+
link: 'text-primary underline-offset-4 hover:underline'
|
|
401
|
+
},
|
|
402
|
+
size: {
|
|
403
|
+
default: 'h-10 px-4 py-2',
|
|
404
|
+
sm: 'h-9 rounded-md px-3',
|
|
405
|
+
lg: 'h-11 rounded-md px-8',
|
|
406
|
+
icon: 'h-10 w-10'
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
compounds: [
|
|
410
|
+
{
|
|
411
|
+
variant: 'default',
|
|
412
|
+
size: 'lg',
|
|
413
|
+
class: 'text-base font-semibold'
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
defaults: {
|
|
417
|
+
variant: 'default',
|
|
418
|
+
size: 'default'
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```svelte
|
|
425
|
+
<!-- Button.svelte - Use preset variants -->
|
|
426
|
+
<script lang="ts">
|
|
427
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
428
|
+
|
|
429
|
+
type ButtonProps = {
|
|
430
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
431
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
432
|
+
disabled?: boolean;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
let { variant, size, disabled = false, class: klass = '', ...props }: ButtonProps = $props();
|
|
436
|
+
</script>
|
|
437
|
+
|
|
438
|
+
<HtmlAtom preset="button" as="button" {variant} {size} {disabled} class={klass} {...props}>
|
|
439
|
+
{@render children?.()}
|
|
440
|
+
</HtmlAtom>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Example 2: Local Variants with HtmlAtom
|
|
444
|
+
|
|
445
|
+
```svelte
|
|
446
|
+
<script lang="ts">
|
|
447
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
448
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
449
|
+
|
|
450
|
+
const buttonVariants = defineVariants({
|
|
451
|
+
class: 'inline-flex items-center justify-center rounded-md font-medium',
|
|
452
|
+
variants: {
|
|
453
|
+
variant: {
|
|
454
|
+
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
455
|
+
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
|
456
|
+
ghost: 'hover:bg-gray-100'
|
|
457
|
+
},
|
|
458
|
+
size: {
|
|
459
|
+
sm: 'h-8 px-3 text-sm',
|
|
460
|
+
md: 'h-10 px-4',
|
|
461
|
+
lg: 'h-12 px-6 text-lg'
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
defaults: {
|
|
465
|
+
variant: 'primary',
|
|
466
|
+
size: 'md'
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
let { variant, size, ...props } = $props();
|
|
471
|
+
</script>
|
|
472
|
+
|
|
473
|
+
<HtmlAtom {variants} as="button" {variant} {size} {...props}>
|
|
474
|
+
{@render children?.()}
|
|
475
|
+
</HtmlAtom>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Example 3: Extending Preset with Local Variants
|
|
479
|
+
|
|
480
|
+
Combine global preset variants with component-specific variants:
|
|
481
|
+
|
|
482
|
+
```svelte
|
|
483
|
+
<!-- special-button.svelte -->
|
|
484
|
+
<script>
|
|
485
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
486
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
487
|
+
|
|
488
|
+
// Local variants that extend/override preset
|
|
489
|
+
const localVariants = defineVariants({
|
|
490
|
+
variants: {
|
|
491
|
+
variant: {
|
|
492
|
+
// Add new variants not in preset
|
|
493
|
+
gradient: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white',
|
|
494
|
+
neon: 'bg-black text-neon-green border-2 border-neon-green'
|
|
495
|
+
},
|
|
496
|
+
// Add completely new variant key
|
|
497
|
+
animated: {
|
|
498
|
+
true: 'animate-pulse',
|
|
499
|
+
false: ''
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
let {
|
|
505
|
+
variant,
|
|
506
|
+
size,
|
|
507
|
+
animated = false,
|
|
508
|
+
...props
|
|
509
|
+
} = $props();
|
|
510
|
+
</script>
|
|
511
|
+
|
|
512
|
+
<HtmlAtom
|
|
513
|
+
preset="button" <!-- Gets base variants -->
|
|
514
|
+
variants={localVariants} <!-- Merges/extends -->
|
|
515
|
+
{variant}
|
|
516
|
+
{size}
|
|
517
|
+
{animated}
|
|
518
|
+
{...props}
|
|
519
|
+
>
|
|
520
|
+
{@render children?.()}
|
|
521
|
+
</HtmlAtom>
|
|
522
|
+
````
|
|
523
|
+
|
|
524
|
+
### Example 4: Alert Component with Compound Variants
|
|
525
|
+
|
|
526
|
+
```svelte
|
|
527
|
+
<script lang="ts">
|
|
528
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
529
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
530
|
+
|
|
531
|
+
const alertVariants = defineVariants({
|
|
532
|
+
class: 'rounded-lg p-4 border',
|
|
533
|
+
variants: {
|
|
534
|
+
variant: {
|
|
535
|
+
info: 'bg-blue-50 border-blue-200 text-blue-900',
|
|
536
|
+
success: 'bg-green-50 border-green-200 text-green-900',
|
|
537
|
+
warning: 'bg-yellow-50 border-yellow-200 text-yellow-900',
|
|
538
|
+
error: 'bg-red-50 border-red-200 text-red-900'
|
|
539
|
+
},
|
|
540
|
+
size: {
|
|
541
|
+
sm: 'text-sm p-2',
|
|
542
|
+
md: 'text-base p-4',
|
|
543
|
+
lg: 'text-lg p-6'
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
compounds: [
|
|
547
|
+
{
|
|
548
|
+
variant: 'error',
|
|
549
|
+
size: 'lg',
|
|
550
|
+
class: 'font-bold',
|
|
551
|
+
role: 'alert',
|
|
552
|
+
'aria-live': 'assertive'
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
defaults: {
|
|
556
|
+
variant: 'info',
|
|
557
|
+
size: 'md'
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
let { variant, size, ...props } = $props();
|
|
562
|
+
</script>
|
|
563
|
+
|
|
564
|
+
<HtmlAtom {variants} {variant} {size} {...props}>
|
|
565
|
+
{@render children?.()}
|
|
566
|
+
</HtmlAtom>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
````
|
|
570
|
+
|
|
571
|
+
### Example 3: Accordion with Reactive Bond State
|
|
572
|
+
|
|
573
|
+
```svelte
|
|
574
|
+
<!-- accordion-item.svelte -->
|
|
575
|
+
<script>
|
|
576
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
577
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
578
|
+
import { AccordionBond } from './bond.svelte';
|
|
579
|
+
|
|
580
|
+
const accordionVariants = defineVariants({
|
|
581
|
+
class: 'border rounded-md transition-all duration-200',
|
|
582
|
+
variants: {
|
|
583
|
+
state: {
|
|
584
|
+
// Reactive: changes when bond.state.isOpen changes
|
|
585
|
+
open: (bond) => ({
|
|
586
|
+
class: bond?.state?.isOpen
|
|
587
|
+
? 'bg-blue-50 border-blue-200'
|
|
588
|
+
: 'bg-white border-gray-200',
|
|
589
|
+
'aria-expanded': bond?.state?.isOpen,
|
|
590
|
+
'data-state': bond?.state?.isOpen ? 'open' : 'closed'
|
|
591
|
+
}),
|
|
592
|
+
disabled: (bond) => ({
|
|
593
|
+
class: bond?.state?.disabled
|
|
594
|
+
? 'opacity-50 cursor-not-allowed'
|
|
595
|
+
: 'cursor-pointer',
|
|
596
|
+
'aria-disabled': bond?.state?.disabled
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
let { state = 'open', ...props } = $props();
|
|
603
|
+
</script>
|
|
604
|
+
|
|
605
|
+
<HtmlAtom {variants} {state} {...props}>
|
|
606
|
+
{@render children?.()}
|
|
607
|
+
</HtmlAtom>
|
|
608
|
+
````
|
|
609
|
+
|
|
610
|
+
## Migration Guide
|
|
611
|
+
|
|
612
|
+
### Before: Manual Variant Management
|
|
613
|
+
|
|
614
|
+
```svelte
|
|
615
|
+
<script>
|
|
616
|
+
let { variant = 'primary', size = 'md' } = $props();
|
|
617
|
+
|
|
618
|
+
const variantClasses = {
|
|
619
|
+
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
620
|
+
secondary: 'bg-gray-500 text-white hover:bg-gray-600',
|
|
621
|
+
danger: 'bg-red-500 text-white hover:bg-red-600'
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const sizeClasses = {
|
|
625
|
+
sm: 'px-2 py-1 text-sm',
|
|
626
|
+
md: 'px-4 py-2 text-base',
|
|
627
|
+
lg: 'px-6 py-3 text-lg'
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const classes = `rounded-md ${variantClasses[variant]} ${sizeClasses[size]}`;
|
|
631
|
+
</script>
|
|
632
|
+
|
|
633
|
+
<button class={classes}> Click me </button>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### After: defineVariants
|
|
637
|
+
|
|
638
|
+
```svelte
|
|
639
|
+
<script>
|
|
640
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
641
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
642
|
+
|
|
643
|
+
const buttonVariants = defineVariants({
|
|
644
|
+
class: 'rounded-md',
|
|
645
|
+
variants: {
|
|
646
|
+
variant: {
|
|
647
|
+
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
648
|
+
secondary: 'bg-gray-500 text-white hover:bg-gray-600',
|
|
649
|
+
danger: 'bg-red-500 text-white hover:bg-red-600'
|
|
650
|
+
},
|
|
651
|
+
size: {
|
|
652
|
+
sm: 'px-2 py-1 text-sm',
|
|
653
|
+
md: 'px-4 py-2 text-base',
|
|
654
|
+
lg: 'px-6 py-3 text-lg'
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
defaults: {
|
|
658
|
+
variant: 'primary',
|
|
659
|
+
size: 'md'
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
let { variant, size, ...props } = $props();
|
|
664
|
+
</script>
|
|
665
|
+
|
|
666
|
+
<!-- Pass the variant function to HtmlAtom -->
|
|
667
|
+
<HtmlAtom {variants} as="button" {variant} {size} {...props}>
|
|
668
|
+
Click me
|
|
669
|
+
</HtmlAtom>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Benefits After Migration
|
|
673
|
+
|
|
674
|
+
✅ **Less boilerplate** - Define once, use everywhere
|
|
675
|
+
✅ **Type safety** - Automatic type inference
|
|
676
|
+
✅ **Compound variants** - Complex styling combinations
|
|
677
|
+
✅ **Default values** - No need for fallback logic
|
|
678
|
+
✅ **Bond integration** - Access component state for reactive variants
|
|
679
|
+
✅ **Return attributes** - Not just classes, any HTML attributes
|
|
680
|
+
|
|
681
|
+
## Best Practices
|
|
682
|
+
|
|
683
|
+
### 1. Organize Variants in Separate Files
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// variants.ts
|
|
687
|
+
import { defineVariants } from '@svelte-atoms/core/utils';
|
|
688
|
+
|
|
689
|
+
export const buttonVariants = defineVariants({
|
|
690
|
+
class: 'rounded-md font-medium transition-colors',
|
|
691
|
+
variants: {
|
|
692
|
+
/* ... */
|
|
693
|
+
},
|
|
694
|
+
defaults: {
|
|
695
|
+
/* ... */
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
export const cardVariants = defineVariants({
|
|
700
|
+
class: 'rounded-lg border',
|
|
701
|
+
variants: {
|
|
702
|
+
/* ... */
|
|
703
|
+
},
|
|
704
|
+
defaults: {
|
|
705
|
+
/* ... */
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### 2. Pass Variants Function to Components
|
|
711
|
+
|
|
712
|
+
Always pass the variant function (not the result) to `HtmlAtom`:
|
|
713
|
+
|
|
714
|
+
```svelte
|
|
715
|
+
<script>
|
|
716
|
+
const variants = defineVariants({ ... });
|
|
717
|
+
|
|
718
|
+
let { variant, size, ...props } = $props();
|
|
719
|
+
</script>
|
|
720
|
+
|
|
721
|
+
<!-- Correct: Pass the function -->
|
|
722
|
+
<HtmlAtom {variants} {variant} {size} {...props}>
|
|
723
|
+
{@render children?.()}
|
|
724
|
+
</HtmlAtom>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### 3. Extend with Additional Classes
|
|
728
|
+
|
|
729
|
+
Merge variant classes with custom classes:
|
|
730
|
+
|
|
731
|
+
```svelte
|
|
732
|
+
<script>
|
|
733
|
+
let { variant, size, class: klass = '', ...props } = $props();
|
|
734
|
+
</script>
|
|
735
|
+
|
|
736
|
+
<HtmlAtom {variants} {variant} {size} class={['custom-class', klass]} {...props}>
|
|
737
|
+
{@render children?.()}
|
|
738
|
+
</HtmlAtom>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### 4. Type Props from Variants
|
|
742
|
+
|
|
743
|
+
Extract variant types for component props:
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
import { VariantPropsType } from '@svelte-atoms/core/utils';
|
|
747
|
+
import { buttonVariants } from './variants';
|
|
748
|
+
|
|
749
|
+
type ButtonProps = VariantPropsType<typeof buttonVariants> & {
|
|
750
|
+
disabled?: boolean;
|
|
751
|
+
// other props...
|
|
752
|
+
};
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
## Creating Variants: Local vs Global
|
|
756
|
+
|
|
757
|
+
### Local Variants (Component-Specific)
|
|
758
|
+
|
|
759
|
+
Create variants directly in your component file when they're only used in one place:
|
|
760
|
+
|
|
761
|
+
```svelte
|
|
762
|
+
<!-- my-button.svelte -->
|
|
763
|
+
<script lang="ts">
|
|
764
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
765
|
+
import { defineVariants, type VariantPropsType } from '@svelte-atoms/core/utils';
|
|
766
|
+
|
|
767
|
+
// Define variants locally
|
|
768
|
+
const buttonVariants = defineVariants({
|
|
769
|
+
class: 'inline-flex items-center justify-center rounded-md font-medium',
|
|
770
|
+
variants: {
|
|
771
|
+
variant: {
|
|
772
|
+
primary: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
773
|
+
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
|
774
|
+
ghost: 'hover:bg-gray-100'
|
|
775
|
+
},
|
|
776
|
+
size: {
|
|
777
|
+
sm: 'h-8 px-3 text-sm',
|
|
778
|
+
md: 'h-10 px-4',
|
|
779
|
+
lg: 'h-12 px-6 text-lg'
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
compounds: [
|
|
783
|
+
{
|
|
784
|
+
variant: 'primary',
|
|
785
|
+
size: 'lg',
|
|
786
|
+
class: 'shadow-md font-semibold'
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
variant: 'secondary',
|
|
790
|
+
size: 'sm',
|
|
791
|
+
class: 'text-xs'
|
|
792
|
+
}
|
|
793
|
+
],
|
|
794
|
+
defaults: {
|
|
795
|
+
variant: 'primary',
|
|
796
|
+
size: 'md'
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Extract variant prop types
|
|
801
|
+
type ButtonVariantProps = VariantPropsType<typeof buttonVariants>;
|
|
802
|
+
|
|
803
|
+
// Define component props extending variant props
|
|
804
|
+
type ButtonProps = ButtonVariantProps & {
|
|
805
|
+
disabled?: boolean;
|
|
806
|
+
onclick?: (e: MouseEvent) => void;
|
|
807
|
+
class?: string;
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
let { variant, size, disabled = false, class: klass = '', ...props }: ButtonProps = $props();
|
|
811
|
+
|
|
812
|
+
const variants = buttonVariants; // The variant function
|
|
813
|
+
</script>
|
|
814
|
+
|
|
815
|
+
<HtmlAtom {variants} as="button" {variant} {size} {disabled} class={klass} {...props}>
|
|
816
|
+
{@render children?.()}
|
|
817
|
+
</HtmlAtom>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Use local variants when:**
|
|
821
|
+
|
|
822
|
+
- The component is unique and won't be reused elsewhere
|
|
823
|
+
- You need quick prototyping
|
|
824
|
+
- Variants are tightly coupled to the component logic
|
|
825
|
+
|
|
826
|
+
### Global Variants (Preset-Based)
|
|
827
|
+
|
|
828
|
+
Define variants globally in your theme/preset configuration for reusable components:
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// +layout.svelte or theme.svelte
|
|
832
|
+
import { setPreset } from '@svelte-atoms/core/context';
|
|
833
|
+
|
|
834
|
+
setPreset({
|
|
835
|
+
// Global button variants
|
|
836
|
+
button: () => ({
|
|
837
|
+
class:
|
|
838
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
|
|
839
|
+
variants: {
|
|
840
|
+
variant: {
|
|
841
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
842
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
843
|
+
outline: 'border border-input bg-background hover:bg-accent',
|
|
844
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
845
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
846
|
+
link: 'text-primary underline-offset-4 hover:underline'
|
|
847
|
+
},
|
|
848
|
+
size: {
|
|
849
|
+
default: 'h-10 px-4 py-2',
|
|
850
|
+
sm: 'h-9 px-3',
|
|
851
|
+
lg: 'h-11 px-8',
|
|
852
|
+
icon: 'h-10 w-10'
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
compounds: [
|
|
856
|
+
{
|
|
857
|
+
variant: 'default',
|
|
858
|
+
size: 'lg',
|
|
859
|
+
class: 'text-base font-semibold'
|
|
860
|
+
}
|
|
861
|
+
],
|
|
862
|
+
defaults: {
|
|
863
|
+
variant: 'default',
|
|
864
|
+
size: 'default'
|
|
865
|
+
}
|
|
866
|
+
}),
|
|
867
|
+
|
|
868
|
+
// Global card variants
|
|
869
|
+
card: () => ({
|
|
870
|
+
class: 'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
871
|
+
variants: {
|
|
872
|
+
variant: {
|
|
873
|
+
default: 'border-border',
|
|
874
|
+
elevated: 'shadow-lg',
|
|
875
|
+
outlined: 'border-2'
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
defaults: {
|
|
879
|
+
variant: 'default'
|
|
880
|
+
}
|
|
881
|
+
})
|
|
882
|
+
});
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
```svelte
|
|
886
|
+
<!-- button.svelte - Consume global variants -->
|
|
887
|
+
<script lang="ts">
|
|
888
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
889
|
+
|
|
890
|
+
// Type-safe props based on preset
|
|
891
|
+
type ButtonProps = {
|
|
892
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
893
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
894
|
+
disabled?: boolean;
|
|
895
|
+
class?: string;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
let {
|
|
899
|
+
variant,
|
|
900
|
+
size,
|
|
901
|
+
disabled = false,
|
|
902
|
+
class: klass = '',
|
|
903
|
+
...props
|
|
904
|
+
}: ButtonProps = $props();
|
|
905
|
+
</script>
|
|
906
|
+
|
|
907
|
+
<HtmlAtom
|
|
908
|
+
preset="button" <!-- Uses global preset variants -->
|
|
909
|
+
as="button"
|
|
910
|
+
{variant}
|
|
911
|
+
{size}
|
|
912
|
+
{disabled}
|
|
913
|
+
class={klass}
|
|
914
|
+
{...props}
|
|
915
|
+
>
|
|
916
|
+
{@render children?.()}
|
|
917
|
+
</HtmlAtom>
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Use global variants when:**
|
|
921
|
+
|
|
922
|
+
- Building a design system with consistent styling
|
|
923
|
+
- Components are used across multiple pages/features
|
|
924
|
+
- You want centralized theme control
|
|
925
|
+
- You need to support theme switching
|
|
926
|
+
|
|
927
|
+
### Extending Global Variants Locally
|
|
928
|
+
|
|
929
|
+
Combine the best of both worlds - use global presets as a base and extend with local variants:
|
|
930
|
+
|
|
931
|
+
```svelte
|
|
932
|
+
<!-- special-button.svelte -->
|
|
933
|
+
<script lang="ts">
|
|
934
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
935
|
+
import { defineVariants, type VariantPropsType } from '@svelte-atoms/core/utils';
|
|
936
|
+
|
|
937
|
+
// Local variants that extend the global preset
|
|
938
|
+
const extendedVariants = defineVariants({
|
|
939
|
+
variants: {
|
|
940
|
+
variant: {
|
|
941
|
+
// Add new variants not in preset
|
|
942
|
+
gradient: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600',
|
|
943
|
+
neon: 'bg-black text-green-400 border-2 border-green-400 hover:bg-green-400 hover:text-black',
|
|
944
|
+
glass: 'bg-white/10 backdrop-blur-lg border border-white/20 hover:bg-white/20'
|
|
945
|
+
},
|
|
946
|
+
// Add new variant dimension
|
|
947
|
+
animated: {
|
|
948
|
+
true: 'animate-pulse',
|
|
949
|
+
false: ''
|
|
950
|
+
},
|
|
951
|
+
shadow: {
|
|
952
|
+
none: '',
|
|
953
|
+
sm: 'shadow-sm',
|
|
954
|
+
md: 'shadow-md',
|
|
955
|
+
lg: 'shadow-lg'
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
defaults: {
|
|
959
|
+
animated: false,
|
|
960
|
+
shadow: 'none'
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Extract extended variant types
|
|
965
|
+
type ExtendedVariantProps = VariantPropsType<typeof extendedVariants>;
|
|
966
|
+
|
|
967
|
+
// Combine preset variants with extended variants
|
|
968
|
+
type SpecialButtonProps = {
|
|
969
|
+
// Preset variant types
|
|
970
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'gradient' | 'neon' | 'glass';
|
|
971
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
972
|
+
// Extended variant types
|
|
973
|
+
animated?: boolean;
|
|
974
|
+
shadow?: 'none' | 'sm' | 'md' | 'lg';
|
|
975
|
+
disabled?: boolean;
|
|
976
|
+
class?: string;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
let {
|
|
980
|
+
variant,
|
|
981
|
+
size,
|
|
982
|
+
animated,
|
|
983
|
+
shadow,
|
|
984
|
+
disabled = false,
|
|
985
|
+
class: klass = '',
|
|
986
|
+
...props
|
|
987
|
+
}: SpecialButtonProps = $props();
|
|
988
|
+
</script>
|
|
989
|
+
|
|
990
|
+
<HtmlAtom
|
|
991
|
+
preset="button" <!-- Gets base global variants -->
|
|
992
|
+
variants={extendedVariants} <!-- Extends with local variants -->
|
|
993
|
+
as="button"
|
|
994
|
+
{variant}
|
|
995
|
+
{size}
|
|
996
|
+
{animated}
|
|
997
|
+
{shadow}
|
|
998
|
+
{disabled}
|
|
999
|
+
class={klass}
|
|
1000
|
+
{...props}
|
|
1001
|
+
>
|
|
1002
|
+
{@render children?.()}
|
|
1003
|
+
</HtmlAtom>
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
## Type-Safe Component Props with Variants
|
|
1007
|
+
|
|
1008
|
+
### Method 1: Manual Type Definition (Preset-based)
|
|
1009
|
+
|
|
1010
|
+
When using presets, manually define the variant types based on your preset configuration:
|
|
1011
|
+
|
|
1012
|
+
```svelte
|
|
1013
|
+
<script lang="ts">
|
|
1014
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
1015
|
+
|
|
1016
|
+
// Manually typed based on preset definition
|
|
1017
|
+
type ButtonProps = {
|
|
1018
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
1019
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
1020
|
+
disabled?: boolean;
|
|
1021
|
+
class?: string;
|
|
1022
|
+
onclick?: (e: MouseEvent) => void;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
let { variant, size, disabled = false, class: klass = '', ...props }: ButtonProps = $props();
|
|
1026
|
+
</script>
|
|
1027
|
+
|
|
1028
|
+
<HtmlAtom preset="button" as="button" {variant} {size} {disabled} class={klass} {...props}>
|
|
1029
|
+
{@render children?.()}
|
|
1030
|
+
</HtmlAtom>
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### Method 2: Extract Types from defineVariants
|
|
1034
|
+
|
|
1035
|
+
For local variants, use `VariantPropsType` to automatically extract types:
|
|
1036
|
+
|
|
1037
|
+
```svelte
|
|
1038
|
+
<script lang="ts">
|
|
1039
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
1040
|
+
import { defineVariants, type VariantPropsType } from '@svelte-atoms/core/utils';
|
|
1041
|
+
|
|
1042
|
+
const buttonVariants = defineVariants({
|
|
1043
|
+
class: 'rounded-md',
|
|
1044
|
+
variants: {
|
|
1045
|
+
variant: {
|
|
1046
|
+
primary: 'bg-blue-500',
|
|
1047
|
+
secondary: 'bg-gray-500',
|
|
1048
|
+
danger: 'bg-red-500'
|
|
1049
|
+
},
|
|
1050
|
+
size: {
|
|
1051
|
+
sm: 'text-sm',
|
|
1052
|
+
md: 'text-base',
|
|
1053
|
+
lg: 'text-lg'
|
|
1054
|
+
},
|
|
1055
|
+
fullWidth: {
|
|
1056
|
+
true: 'w-full',
|
|
1057
|
+
false: 'w-auto'
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
defaults: {
|
|
1061
|
+
variant: 'primary',
|
|
1062
|
+
size: 'md',
|
|
1063
|
+
fullWidth: false
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// Automatically extract variant prop types
|
|
1068
|
+
type ButtonVariantProps = VariantPropsType<typeof buttonVariants>;
|
|
1069
|
+
// Result: { variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; fullWidth?: boolean }
|
|
1070
|
+
|
|
1071
|
+
// Extend with additional props
|
|
1072
|
+
type ButtonProps = ButtonVariantProps & {
|
|
1073
|
+
disabled?: boolean;
|
|
1074
|
+
onclick?: (e: MouseEvent) => void;
|
|
1075
|
+
class?: string;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
let {
|
|
1079
|
+
variant,
|
|
1080
|
+
size,
|
|
1081
|
+
fullWidth,
|
|
1082
|
+
disabled = false,
|
|
1083
|
+
class: klass = '',
|
|
1084
|
+
...props
|
|
1085
|
+
}: ButtonProps = $props();
|
|
1086
|
+
</script>
|
|
1087
|
+
|
|
1088
|
+
<HtmlAtom {variants} as="button" {variant} {size} {fullWidth} {disabled} class={klass} {...props}>
|
|
1089
|
+
{@render children?.()}
|
|
1090
|
+
</HtmlAtom>
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
### Method 3: Shared Variant Types
|
|
1094
|
+
|
|
1095
|
+
Create reusable variant type definitions:
|
|
1096
|
+
|
|
1097
|
+
```typescript
|
|
1098
|
+
// types/button.ts
|
|
1099
|
+
import { defineVariants, type VariantPropsType } from '@svelte-atoms/core/utils';
|
|
1100
|
+
|
|
1101
|
+
export const buttonVariants = defineVariants({
|
|
1102
|
+
class: 'inline-flex items-center justify-center rounded-md font-medium',
|
|
1103
|
+
variants: {
|
|
1104
|
+
variant: {
|
|
1105
|
+
primary: 'bg-blue-500 text-white',
|
|
1106
|
+
secondary: 'bg-gray-200 text-gray-900',
|
|
1107
|
+
ghost: 'hover:bg-gray-100'
|
|
1108
|
+
},
|
|
1109
|
+
size: {
|
|
1110
|
+
sm: 'h-8 px-3 text-sm',
|
|
1111
|
+
md: 'h-10 px-4',
|
|
1112
|
+
lg: 'h-12 px-6 text-lg'
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
defaults: {
|
|
1116
|
+
variant: 'primary',
|
|
1117
|
+
size: 'md'
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// Export the variant types
|
|
1122
|
+
export type ButtonVariantProps = VariantPropsType<typeof buttonVariants>;
|
|
1123
|
+
|
|
1124
|
+
// Export full component props
|
|
1125
|
+
export type ButtonProps = ButtonVariantProps & {
|
|
1126
|
+
disabled?: boolean;
|
|
1127
|
+
onclick?: (e: MouseEvent) => void;
|
|
1128
|
+
class?: string;
|
|
1129
|
+
};
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
```svelte
|
|
1133
|
+
<!-- button.svelte -->
|
|
1134
|
+
<script lang="ts">
|
|
1135
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
1136
|
+
import { buttonVariants, type ButtonProps } from './types/button';
|
|
1137
|
+
|
|
1138
|
+
const variants = buttonVariants; // The function from the shared file
|
|
1139
|
+
|
|
1140
|
+
let { variant, size, disabled = false, class: klass = '', ...props }: ButtonProps = $props();
|
|
1141
|
+
</script>
|
|
1142
|
+
|
|
1143
|
+
<HtmlAtom {variants} as="button" {variant} {size} {disabled} class={klass} {...props}>
|
|
1144
|
+
{@render children?.()}
|
|
1145
|
+
</HtmlAtom>
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
### Method 4: Component with Bond State Types
|
|
1149
|
+
|
|
1150
|
+
For components using the Bond pattern:
|
|
1151
|
+
|
|
1152
|
+
```typescript
|
|
1153
|
+
// accordion-item.svelte
|
|
1154
|
+
<script lang="ts">
|
|
1155
|
+
import { HtmlAtom } from '@svelte-atoms/core';
|
|
1156
|
+
import { defineVariants, type VariantPropsType } from '@svelte-atoms/core/utils';
|
|
1157
|
+
import { AccordionBond } from './bond.svelte';
|
|
1158
|
+
|
|
1159
|
+
const accordionVariants = defineVariants({
|
|
1160
|
+
class: 'border rounded-md',
|
|
1161
|
+
variants: {
|
|
1162
|
+
state: {
|
|
1163
|
+
open: (bond) => ({
|
|
1164
|
+
class: bond?.state?.isOpen ? 'bg-blue-50' : 'bg-white',
|
|
1165
|
+
'aria-expanded': bond?.state?.isOpen
|
|
1166
|
+
})
|
|
1167
|
+
},
|
|
1168
|
+
bordered: {
|
|
1169
|
+
true: 'border-2',
|
|
1170
|
+
false: 'border'
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
defaults: {
|
|
1174
|
+
bordered: false
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Extract variant types
|
|
1179
|
+
type AccordionVariantProps = VariantPropsType<typeof accordionVariants>;
|
|
1180
|
+
|
|
1181
|
+
// Extend with component-specific props
|
|
1182
|
+
type AccordionItemProps = Omit<AccordionVariantProps, 'state'> & {
|
|
1183
|
+
value: string;
|
|
1184
|
+
disabled?: boolean;
|
|
1185
|
+
class?: string;
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
let {
|
|
1189
|
+
value,
|
|
1190
|
+
bordered,
|
|
1191
|
+
disabled = false,
|
|
1192
|
+
class: klass = '',
|
|
1193
|
+
...props
|
|
1194
|
+
}: AccordionItemProps = $props();
|
|
1195
|
+
|
|
1196
|
+
const variants = accordionVariants; // The variant function
|
|
1197
|
+
</script>
|
|
1198
|
+
|
|
1199
|
+
<HtmlAtom {variants} {bordered} class={klass} {...props}>
|
|
1200
|
+
{@render children?.()}
|
|
1201
|
+
</HtmlAtom>
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
### Best Practices for Typed Variants
|
|
1205
|
+
|
|
1206
|
+
1. **Always extract types from defineVariants**
|
|
1207
|
+
|
|
1208
|
+
```typescript
|
|
1209
|
+
const variants = defineVariants({...});
|
|
1210
|
+
type VariantProps = VariantPropsType<typeof variants>;
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
2. **Extend variant types with component props**
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
type ComponentProps = VariantProps & {
|
|
1217
|
+
disabled?: boolean;
|
|
1218
|
+
onclick?: () => void;
|
|
1219
|
+
};
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
3. **Use Omit for bond-driven variants**
|
|
1223
|
+
|
|
1224
|
+
```typescript
|
|
1225
|
+
// Remove 'state' from props since it's driven by bond
|
|
1226
|
+
type Props = Omit<VariantProps, 'state'> & { ... };
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
4. **Share types across related components**
|
|
1230
|
+
|
|
1231
|
+
```typescript
|
|
1232
|
+
// types/card.ts
|
|
1233
|
+
export type CardVariantProps = VariantPropsType<typeof cardVariants>;
|
|
1234
|
+
export type CardHeaderProps = { class?: string };
|
|
1235
|
+
export type CardBodyProps = { class?: string };
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
5. **Document variant options in JSDoc**
|
|
1239
|
+
```typescript
|
|
1240
|
+
/**
|
|
1241
|
+
* Button component with multiple variants
|
|
1242
|
+
* @param variant - Visual style: 'primary' | 'secondary' | 'ghost'
|
|
1243
|
+
* @param size - Size variant: 'sm' | 'md' | 'lg'
|
|
1244
|
+
*/
|
|
1245
|
+
type ButtonProps = VariantPropsType<typeof buttonVariants> & {...};
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
## Summary
|
|
1249
|
+
|
|
1250
|
+
`defineVariants()` provides:
|
|
1251
|
+
|
|
1252
|
+
✅ **Single function** - One API for all variant needs
|
|
1253
|
+
✅ **Type-safe** - Automatic TypeScript inference
|
|
1254
|
+
✅ **Reactive** - Access bond state for dynamic styling
|
|
1255
|
+
✅ **Powerful** - Base classes, compound variants, defaults
|
|
1256
|
+
✅ **Flexible** - Return both classes and attributes
|
|
1257
|
+
✅ **Clean** - No manual object merging or conditionals
|
|
1258
|
+
✅ **Extensible** - Combine global presets with local variants
|
|
1259
|
+
✅ **Type extraction** - Use `VariantPropsType` for automatic type inference
|
|
1260
|
+
|
|
1261
|
+
Inspired by Class Variance Authority but integrated with @svelte-atoms/core's bond system.
|