@miozu/jera 0.0.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +443 -0
- package/README.md +211 -1
- package/llms.txt +64 -0
- package/package.json +44 -14
- package/src/actions/index.js +375 -0
- package/src/components/feedback/EmptyState.svelte +179 -0
- package/src/components/feedback/ProgressBar.svelte +116 -0
- package/src/components/feedback/Skeleton.svelte +107 -0
- package/src/components/feedback/Spinner.svelte +77 -0
- package/src/components/feedback/Toast.svelte +297 -0
- package/src/components/forms/Checkbox.svelte +147 -0
- package/src/components/forms/Dropzone.svelte +248 -0
- package/src/components/forms/FileUpload.svelte +266 -0
- package/src/components/forms/IconInput.svelte +184 -0
- package/src/components/forms/Input.svelte +121 -0
- package/src/components/forms/NumberInput.svelte +225 -0
- package/src/components/forms/PinInput.svelte +169 -0
- package/src/components/forms/Radio.svelte +143 -0
- package/src/components/forms/RadioGroup.svelte +62 -0
- package/src/components/forms/RangeSlider.svelte +212 -0
- package/src/components/forms/SearchInput.svelte +175 -0
- package/src/components/forms/Select.svelte +326 -0
- package/src/components/forms/Switch.svelte +159 -0
- package/src/components/forms/Textarea.svelte +122 -0
- package/src/components/navigation/Accordion.svelte +65 -0
- package/src/components/navigation/AccordionItem.svelte +146 -0
- package/src/components/navigation/Tabs.svelte +239 -0
- package/src/components/overlays/ConfirmDialog.svelte +272 -0
- package/src/components/overlays/Dropdown.svelte +153 -0
- package/src/components/overlays/DropdownDivider.svelte +23 -0
- package/src/components/overlays/DropdownItem.svelte +97 -0
- package/src/components/overlays/Modal.svelte +232 -0
- package/src/components/overlays/Popover.svelte +206 -0
- package/src/components/primitives/Avatar.svelte +132 -0
- package/src/components/primitives/Badge.svelte +118 -0
- package/src/components/primitives/Button.svelte +262 -0
- package/src/components/primitives/Card.svelte +104 -0
- package/src/components/primitives/Divider.svelte +105 -0
- package/src/components/primitives/LazyImage.svelte +104 -0
- package/src/components/primitives/Link.svelte +122 -0
- package/src/components/primitives/StatusBadge.svelte +122 -0
- package/src/index.js +128 -0
- package/src/tokens/colors.css +189 -0
- package/src/tokens/effects.css +128 -0
- package/src/tokens/index.css +81 -0
- package/src/tokens/spacing.css +49 -0
- package/src/tokens/typography.css +79 -0
- package/src/utils/cn.svelte.js +175 -0
- package/src/utils/index.js +17 -0
- package/src/utils/reactive.svelte.js +239 -0
- package/jera.js +0 -135
- package/www/components/jera/Input/Input.svelte +0 -63
- package/www/components/jera/Input/index.js +0 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Select
|
|
3
|
+
|
|
4
|
+
An accessible dropdown select component with keyboard navigation.
|
|
5
|
+
Demonstrates advanced Svelte 5 patterns:
|
|
6
|
+
|
|
7
|
+
- $state for local UI state
|
|
8
|
+
- $derived for computed values
|
|
9
|
+
- $effect for side effects with cleanup
|
|
10
|
+
- $bindable for two-way value binding
|
|
11
|
+
- Actions for click-outside behavior
|
|
12
|
+
- Keyboard navigation (ArrowUp/Down, Enter, Escape)
|
|
13
|
+
- ARIA attributes for accessibility
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
<Select
|
|
17
|
+
options={[
|
|
18
|
+
{ value: 'opt1', label: 'Option 1' },
|
|
19
|
+
{ value: 'opt2', label: 'Option 2' }
|
|
20
|
+
]}
|
|
21
|
+
bind:value={selected}
|
|
22
|
+
placeholder="Select an option"
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
// With custom keys
|
|
26
|
+
<Select
|
|
27
|
+
options={users}
|
|
28
|
+
bind:value={selectedUserId}
|
|
29
|
+
labelKey="name"
|
|
30
|
+
valueKey="id"
|
|
31
|
+
/>
|
|
32
|
+
-->
|
|
33
|
+
<script>
|
|
34
|
+
import { cn, cv } from '../../utils/cn.svelte.js';
|
|
35
|
+
import { clickOutside } from '../../actions/index.js';
|
|
36
|
+
import { generateId } from '../../utils/reactive.svelte.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {{ [key: string]: any }} Option
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
options = [],
|
|
44
|
+
value = $bindable(null),
|
|
45
|
+
placeholder = 'Select...',
|
|
46
|
+
labelKey = 'label',
|
|
47
|
+
valueKey = 'value',
|
|
48
|
+
disabled = false,
|
|
49
|
+
error = false,
|
|
50
|
+
class: className = '',
|
|
51
|
+
onchange
|
|
52
|
+
} = $props();
|
|
53
|
+
|
|
54
|
+
// Generate unique IDs for accessibility
|
|
55
|
+
const triggerId = generateId();
|
|
56
|
+
const listboxId = generateId();
|
|
57
|
+
|
|
58
|
+
// Local state
|
|
59
|
+
let isOpen = $state(false);
|
|
60
|
+
let highlightedIndex = $state(-1);
|
|
61
|
+
let triggerRef = $state(null);
|
|
62
|
+
let listRef = $state(null);
|
|
63
|
+
|
|
64
|
+
// Derived values
|
|
65
|
+
const selectedOption = $derived(
|
|
66
|
+
options.find(opt => opt[valueKey] === value) ?? null
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const selectedLabel = $derived(
|
|
70
|
+
selectedOption ? selectedOption[labelKey] : placeholder
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const hasValue = $derived(value !== null && value !== undefined);
|
|
74
|
+
|
|
75
|
+
// Keyboard navigation effect
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (!isOpen) {
|
|
78
|
+
highlightedIndex = -1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Focus the listbox when opened
|
|
83
|
+
listRef?.focus();
|
|
84
|
+
|
|
85
|
+
// Set initial highlight to selected option
|
|
86
|
+
const selectedIdx = options.findIndex(opt => opt[valueKey] === value);
|
|
87
|
+
highlightedIndex = selectedIdx >= 0 ? selectedIdx : 0;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Scroll highlighted option into view
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (!isOpen || highlightedIndex < 0) return;
|
|
93
|
+
|
|
94
|
+
const listbox = listRef;
|
|
95
|
+
const highlighted = listbox?.querySelector(`[data-index="${highlightedIndex}"]`);
|
|
96
|
+
highlighted?.scrollIntoView({ block: 'nearest' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Handlers
|
|
100
|
+
function toggle() {
|
|
101
|
+
if (disabled) return;
|
|
102
|
+
isOpen = !isOpen;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function close() {
|
|
106
|
+
isOpen = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function select(option) {
|
|
110
|
+
value = option[valueKey];
|
|
111
|
+
onchange?.(option);
|
|
112
|
+
close();
|
|
113
|
+
triggerRef?.focus();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleKeydown(event) {
|
|
117
|
+
if (disabled) return;
|
|
118
|
+
|
|
119
|
+
switch (event.key) {
|
|
120
|
+
case 'ArrowDown':
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
if (!isOpen) {
|
|
123
|
+
isOpen = true;
|
|
124
|
+
} else {
|
|
125
|
+
highlightedIndex = Math.min(highlightedIndex + 1, options.length - 1);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'ArrowUp':
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
if (!isOpen) {
|
|
132
|
+
isOpen = true;
|
|
133
|
+
} else {
|
|
134
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'Enter':
|
|
139
|
+
case ' ':
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
if (isOpen && highlightedIndex >= 0) {
|
|
142
|
+
select(options[highlightedIndex]);
|
|
143
|
+
} else {
|
|
144
|
+
toggle();
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'Escape':
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
close();
|
|
151
|
+
triggerRef?.focus();
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'Home':
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
highlightedIndex = 0;
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'End':
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
highlightedIndex = options.length - 1;
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'Tab':
|
|
165
|
+
close();
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Styles
|
|
171
|
+
const triggerStyles = cv({
|
|
172
|
+
base: [
|
|
173
|
+
'relative w-full flex items-center justify-between gap-2',
|
|
174
|
+
'h-10 px-3 rounded-lg border',
|
|
175
|
+
'text-sm text-left',
|
|
176
|
+
'transition-colors duration-150',
|
|
177
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
|
178
|
+
'disabled:opacity-50 disabled:cursor-not-allowed'
|
|
179
|
+
].join(' '),
|
|
180
|
+
|
|
181
|
+
variants: {
|
|
182
|
+
error: {
|
|
183
|
+
true: 'border-error focus-visible:ring-error/50',
|
|
184
|
+
false: 'border-border hover:border-muted focus-visible:ring-primary/50'
|
|
185
|
+
},
|
|
186
|
+
open: {
|
|
187
|
+
true: 'border-primary ring-2 ring-primary/20',
|
|
188
|
+
false: ''
|
|
189
|
+
},
|
|
190
|
+
hasValue: {
|
|
191
|
+
true: 'text-text-strong',
|
|
192
|
+
false: 'text-muted'
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
defaults: {
|
|
197
|
+
error: false,
|
|
198
|
+
open: false,
|
|
199
|
+
hasValue: false
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<div
|
|
205
|
+
class={cn('relative', className)}
|
|
206
|
+
use:clickOutside={close}
|
|
207
|
+
>
|
|
208
|
+
<!-- Trigger Button -->
|
|
209
|
+
<button
|
|
210
|
+
bind:this={triggerRef}
|
|
211
|
+
id={triggerId}
|
|
212
|
+
type="button"
|
|
213
|
+
class={triggerStyles({ error, open: isOpen, hasValue })}
|
|
214
|
+
{disabled}
|
|
215
|
+
aria-haspopup="listbox"
|
|
216
|
+
aria-expanded={isOpen}
|
|
217
|
+
aria-controls={listboxId}
|
|
218
|
+
aria-invalid={error || undefined}
|
|
219
|
+
onclick={toggle}
|
|
220
|
+
onkeydown={handleKeydown}
|
|
221
|
+
>
|
|
222
|
+
<span class="truncate flex-1">
|
|
223
|
+
{selectedLabel}
|
|
224
|
+
</span>
|
|
225
|
+
|
|
226
|
+
<!-- Chevron Icon -->
|
|
227
|
+
<svg
|
|
228
|
+
class={cn(
|
|
229
|
+
'w-4 h-4 shrink-0 text-muted transition-transform duration-200',
|
|
230
|
+
isOpen && 'rotate-180'
|
|
231
|
+
)}
|
|
232
|
+
viewBox="0 0 24 24"
|
|
233
|
+
fill="none"
|
|
234
|
+
stroke="currentColor"
|
|
235
|
+
stroke-width="2"
|
|
236
|
+
stroke-linecap="round"
|
|
237
|
+
stroke-linejoin="round"
|
|
238
|
+
>
|
|
239
|
+
<path d="m6 9 6 6 6-6" />
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
|
|
243
|
+
<!-- Dropdown Listbox -->
|
|
244
|
+
{#if isOpen}
|
|
245
|
+
<div
|
|
246
|
+
bind:this={listRef}
|
|
247
|
+
id={listboxId}
|
|
248
|
+
role="listbox"
|
|
249
|
+
tabindex="-1"
|
|
250
|
+
aria-labelledby={triggerId}
|
|
251
|
+
aria-activedescendant={highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : undefined}
|
|
252
|
+
class={cn(
|
|
253
|
+
'absolute z-50 w-full mt-1',
|
|
254
|
+
'max-h-60 overflow-auto',
|
|
255
|
+
'bg-surface border border-border rounded-lg shadow-lg',
|
|
256
|
+
'py-1',
|
|
257
|
+
'animate-in fade-in slide-in-from-top-2 duration-150'
|
|
258
|
+
)}
|
|
259
|
+
onkeydown={handleKeydown}
|
|
260
|
+
>
|
|
261
|
+
{#if options.length === 0}
|
|
262
|
+
<div class="px-3 py-2 text-sm text-muted text-center">
|
|
263
|
+
No options available
|
|
264
|
+
</div>
|
|
265
|
+
{:else}
|
|
266
|
+
{#each options as option, index (option[valueKey])}
|
|
267
|
+
{@const isSelected = option[valueKey] === value}
|
|
268
|
+
{@const isHighlighted = index === highlightedIndex}
|
|
269
|
+
|
|
270
|
+
<button
|
|
271
|
+
id="{listboxId}-option-{index}"
|
|
272
|
+
type="button"
|
|
273
|
+
role="option"
|
|
274
|
+
data-index={index}
|
|
275
|
+
aria-selected={isSelected}
|
|
276
|
+
class={cn(
|
|
277
|
+
'w-full flex items-center gap-2 px-3 py-2',
|
|
278
|
+
'text-sm text-left',
|
|
279
|
+
'transition-colors duration-75',
|
|
280
|
+
'focus:outline-none',
|
|
281
|
+
isHighlighted && 'bg-hover',
|
|
282
|
+
isSelected && 'text-primary font-medium',
|
|
283
|
+
!isSelected && 'text-text'
|
|
284
|
+
)}
|
|
285
|
+
onclick={() => select(option)}
|
|
286
|
+
onmouseenter={() => highlightedIndex = index}
|
|
287
|
+
>
|
|
288
|
+
<!-- Check Icon for Selected -->
|
|
289
|
+
<span class={cn('w-4 h-4 shrink-0', !isSelected && 'invisible')}>
|
|
290
|
+
<svg
|
|
291
|
+
viewBox="0 0 24 24"
|
|
292
|
+
fill="none"
|
|
293
|
+
stroke="currentColor"
|
|
294
|
+
stroke-width="2.5"
|
|
295
|
+
stroke-linecap="round"
|
|
296
|
+
stroke-linejoin="round"
|
|
297
|
+
>
|
|
298
|
+
<path d="M20 6 9 17l-5-5" />
|
|
299
|
+
</svg>
|
|
300
|
+
</span>
|
|
301
|
+
|
|
302
|
+
<span class="truncate flex-1">
|
|
303
|
+
{option[labelKey]}
|
|
304
|
+
</span>
|
|
305
|
+
</button>
|
|
306
|
+
{/each}
|
|
307
|
+
{/if}
|
|
308
|
+
</div>
|
|
309
|
+
{/if}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<style>
|
|
313
|
+
@keyframes fade-in {
|
|
314
|
+
from { opacity: 0; }
|
|
315
|
+
to { opacity: 1; }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@keyframes slide-in-from-top-2 {
|
|
319
|
+
from { transform: translateY(-0.5rem); }
|
|
320
|
+
to { transform: translateY(0); }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.animate-in {
|
|
324
|
+
animation: fade-in 150ms ease-out, slide-in-from-top-2 150ms ease-out;
|
|
325
|
+
}
|
|
326
|
+
</style>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Switch
|
|
3
|
+
|
|
4
|
+
An accessible toggle switch for boolean values.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Switch bind:checked={enabled}>Enable notifications</Switch>
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
<Switch bind:checked={darkMode} size="lg" />
|
|
11
|
+
-->
|
|
12
|
+
<script>
|
|
13
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
14
|
+
import { generateId } from '../../utils/reactive.svelte.js';
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
checked = $bindable(false),
|
|
18
|
+
disabled = false,
|
|
19
|
+
size = 'md',
|
|
20
|
+
name = '',
|
|
21
|
+
id,
|
|
22
|
+
class: className = '',
|
|
23
|
+
children,
|
|
24
|
+
onchange,
|
|
25
|
+
...rest
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const inputId = id || generateId();
|
|
29
|
+
|
|
30
|
+
// Size classes map to CSS classes defined below
|
|
31
|
+
const sizeClass = $derived(`switch-${size}`);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<label
|
|
35
|
+
class={cn('switch-wrapper', disabled && 'switch-disabled', className)}
|
|
36
|
+
for={inputId}
|
|
37
|
+
>
|
|
38
|
+
<input
|
|
39
|
+
type="checkbox"
|
|
40
|
+
role="switch"
|
|
41
|
+
id={inputId}
|
|
42
|
+
{name}
|
|
43
|
+
{disabled}
|
|
44
|
+
bind:checked
|
|
45
|
+
{onchange}
|
|
46
|
+
class="switch-input"
|
|
47
|
+
aria-checked={checked}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<span class={cn('switch-track', sizeClass, checked && 'switch-track-checked')}>
|
|
52
|
+
<span class={cn('switch-thumb', checked && 'switch-thumb-checked')} />
|
|
53
|
+
</span>
|
|
54
|
+
|
|
55
|
+
{#if children}
|
|
56
|
+
<span class="switch-label">
|
|
57
|
+
{@render children()}
|
|
58
|
+
</span>
|
|
59
|
+
{/if}
|
|
60
|
+
</label>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.switch-wrapper {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: var(--space-2);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
user-select: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.switch-disabled {
|
|
72
|
+
opacity: 0.5;
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.switch-input {
|
|
77
|
+
position: absolute;
|
|
78
|
+
width: 1px;
|
|
79
|
+
height: 1px;
|
|
80
|
+
padding: 0;
|
|
81
|
+
margin: -1px;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
clip: rect(0, 0, 0, 0);
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
border: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.switch-track {
|
|
89
|
+
position: relative;
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
border-radius: var(--radius-full);
|
|
94
|
+
background-color: var(--color-border);
|
|
95
|
+
transition: var(--transition-colors);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.switch-track-checked {
|
|
99
|
+
background-color: var(--color-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.switch-input:focus-visible + .switch-track {
|
|
103
|
+
outline: 2px solid var(--color-primary);
|
|
104
|
+
outline-offset: 2px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.switch-thumb {
|
|
108
|
+
position: absolute;
|
|
109
|
+
left: 2px;
|
|
110
|
+
border-radius: var(--radius-full);
|
|
111
|
+
background-color: white;
|
|
112
|
+
box-shadow: var(--shadow-sm);
|
|
113
|
+
transition: transform var(--duration-fast) var(--ease-out);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.switch-label {
|
|
117
|
+
font-size: var(--text-sm);
|
|
118
|
+
color: var(--color-text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Size variants - Small */
|
|
122
|
+
.switch-sm {
|
|
123
|
+
width: 2rem;
|
|
124
|
+
height: 1rem;
|
|
125
|
+
}
|
|
126
|
+
.switch-sm .switch-thumb {
|
|
127
|
+
width: 0.75rem;
|
|
128
|
+
height: 0.75rem;
|
|
129
|
+
}
|
|
130
|
+
.switch-sm .switch-thumb-checked {
|
|
131
|
+
transform: translateX(1rem);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Size variants - Medium (default) */
|
|
135
|
+
.switch-md {
|
|
136
|
+
width: 2.5rem;
|
|
137
|
+
height: 1.25rem;
|
|
138
|
+
}
|
|
139
|
+
.switch-md .switch-thumb {
|
|
140
|
+
width: 1rem;
|
|
141
|
+
height: 1rem;
|
|
142
|
+
}
|
|
143
|
+
.switch-md .switch-thumb-checked {
|
|
144
|
+
transform: translateX(1.25rem);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Size variants - Large */
|
|
148
|
+
.switch-lg {
|
|
149
|
+
width: 3rem;
|
|
150
|
+
height: 1.5rem;
|
|
151
|
+
}
|
|
152
|
+
.switch-lg .switch-thumb {
|
|
153
|
+
width: 1.25rem;
|
|
154
|
+
height: 1.25rem;
|
|
155
|
+
}
|
|
156
|
+
.switch-lg .switch-thumb-checked {
|
|
157
|
+
transform: translateX(1.5rem);
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Textarea
|
|
3
|
+
|
|
4
|
+
A multi-line text input component.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Textarea bind:value={description} placeholder="Enter description" rows={4} />
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
// Auto-resize based on content
|
|
11
|
+
<Textarea bind:value={notes} autoResize />
|
|
12
|
+
-->
|
|
13
|
+
<script>
|
|
14
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
value = $bindable(''),
|
|
18
|
+
ref = $bindable(),
|
|
19
|
+
placeholder = '',
|
|
20
|
+
disabled = false,
|
|
21
|
+
required = false,
|
|
22
|
+
name = '',
|
|
23
|
+
id = '',
|
|
24
|
+
rows = 3,
|
|
25
|
+
maxlength,
|
|
26
|
+
minlength,
|
|
27
|
+
autoResize = false,
|
|
28
|
+
class: className = '',
|
|
29
|
+
unstyled = false,
|
|
30
|
+
error = false,
|
|
31
|
+
oninput,
|
|
32
|
+
onchange,
|
|
33
|
+
onkeydown,
|
|
34
|
+
onfocus,
|
|
35
|
+
onblur,
|
|
36
|
+
...rest
|
|
37
|
+
} = $props();
|
|
38
|
+
|
|
39
|
+
const textareaClass = $derived(
|
|
40
|
+
unstyled ? className : cn(
|
|
41
|
+
'textarea-base',
|
|
42
|
+
autoResize && 'textarea-auto-resize',
|
|
43
|
+
error && 'textarea-error',
|
|
44
|
+
className
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function handleInput(e) {
|
|
49
|
+
if (autoResize && ref) {
|
|
50
|
+
ref.style.height = 'auto';
|
|
51
|
+
ref.style.height = ref.scrollHeight + 'px';
|
|
52
|
+
}
|
|
53
|
+
oninput?.(e);
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<textarea
|
|
58
|
+
class={textareaClass}
|
|
59
|
+
bind:this={ref}
|
|
60
|
+
bind:value
|
|
61
|
+
{id}
|
|
62
|
+
{name}
|
|
63
|
+
{rows}
|
|
64
|
+
{placeholder}
|
|
65
|
+
{disabled}
|
|
66
|
+
{required}
|
|
67
|
+
{maxlength}
|
|
68
|
+
{minlength}
|
|
69
|
+
aria-invalid={error || undefined}
|
|
70
|
+
oninput={handleInput}
|
|
71
|
+
{onchange}
|
|
72
|
+
{onkeydown}
|
|
73
|
+
{onfocus}
|
|
74
|
+
{onblur}
|
|
75
|
+
{...rest}
|
|
76
|
+
></textarea>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.textarea-base {
|
|
80
|
+
width: 100%;
|
|
81
|
+
padding: var(--space-2) var(--space-3);
|
|
82
|
+
font-size: var(--text-sm);
|
|
83
|
+
line-height: 1.5;
|
|
84
|
+
color: var(--color-text-strong);
|
|
85
|
+
background-color: var(--color-bg);
|
|
86
|
+
border: 1px solid var(--color-border-muted);
|
|
87
|
+
border-radius: var(--radius-lg);
|
|
88
|
+
transition: var(--transition-colors);
|
|
89
|
+
resize: vertical;
|
|
90
|
+
font-family: inherit;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.textarea-base::placeholder {
|
|
94
|
+
color: var(--color-text-muted);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.textarea-base:focus {
|
|
98
|
+
outline: none;
|
|
99
|
+
border-color: var(--color-primary);
|
|
100
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.textarea-base:disabled {
|
|
104
|
+
opacity: 0.5;
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
resize: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.textarea-auto-resize {
|
|
110
|
+
resize: none;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.textarea-error {
|
|
115
|
+
border-color: var(--color-error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.textarea-error:focus {
|
|
119
|
+
border-color: var(--color-error);
|
|
120
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 20%, transparent);
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Accordion
|
|
3
|
+
|
|
4
|
+
Collapsible content sections.
|
|
5
|
+
|
|
6
|
+
@example Single item
|
|
7
|
+
<Accordion>
|
|
8
|
+
<AccordionItem title="Section 1">
|
|
9
|
+
Content for section 1
|
|
10
|
+
</AccordionItem>
|
|
11
|
+
<AccordionItem title="Section 2">
|
|
12
|
+
Content for section 2
|
|
13
|
+
</AccordionItem>
|
|
14
|
+
</Accordion>
|
|
15
|
+
|
|
16
|
+
@example Controlled
|
|
17
|
+
<Accordion bind:expanded={openSections} multiple>
|
|
18
|
+
...
|
|
19
|
+
</Accordion>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
import { setContext } from 'svelte';
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
expanded = $bindable([]),
|
|
26
|
+
multiple = false,
|
|
27
|
+
children,
|
|
28
|
+
class: className = ''
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
function toggle(id) {
|
|
32
|
+
if (multiple) {
|
|
33
|
+
if (expanded.includes(id)) {
|
|
34
|
+
expanded = expanded.filter(i => i !== id);
|
|
35
|
+
} else {
|
|
36
|
+
expanded = [...expanded, id];
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
expanded = expanded.includes(id) ? [] : [id];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isExpanded(id) {
|
|
44
|
+
return expanded.includes(id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setContext('accordion', {
|
|
48
|
+
toggle,
|
|
49
|
+
isExpanded: (id) => expanded.includes(id)
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<div class="accordion {className}">
|
|
54
|
+
{@render children?.()}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.accordion {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
border: 1px solid var(--color-border);
|
|
62
|
+
border-radius: 0.5rem;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
</style>
|