@classic-homes/theme-svelte 0.1.21 → 0.1.22
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/dist/lib/components/Combobox.svelte +194 -76
- package/dist/lib/components/Combobox.svelte.d.ts +4 -0
- package/dist/lib/components/DateTimePicker.svelte +280 -191
- package/dist/lib/components/DateTimePicker.svelte.d.ts +9 -1
- package/dist/lib/components/Input.svelte +1 -1
- package/dist/lib/components/MultiSelect.svelte +269 -108
- package/dist/lib/components/MultiSelect.svelte.d.ts +4 -0
- package/dist/lib/components/NumberInput.svelte +198 -78
- package/dist/lib/components/NumberInput.svelte.d.ts +4 -0
- package/dist/lib/components/OTPInput.svelte +125 -35
- package/dist/lib/components/OTPInput.svelte.d.ts +8 -0
- package/dist/lib/components/RadioGroup.svelte +31 -11
- package/dist/lib/components/RadioGroup.svelte.d.ts +2 -0
- package/dist/lib/components/Select.svelte +249 -103
- package/dist/lib/components/Select.svelte.d.ts +2 -0
- package/dist/lib/components/Slider.svelte +32 -2
- package/dist/lib/components/Slider.svelte.d.ts +4 -0
- package/dist/lib/composables/useForm.svelte.d.ts +6 -0
- package/dist/lib/composables/useForm.svelte.js +33 -0
- package/dist/lib/composables/usePersistedForm.svelte.js +3 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/utils/date.d.ts +64 -0
- package/dist/lib/utils/date.js +106 -0
- package/dist/lib/utils/form.d.ts +22 -0
- package/dist/lib/utils/form.js +31 -0
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Combobox as ComboboxPrimitive } from 'bits-ui';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
|
+
import { generateId, computeDescribedBy } from '../utils/form.js';
|
|
4
5
|
import Spinner from './Spinner.svelte';
|
|
6
|
+
import Label from './Label.svelte';
|
|
5
7
|
|
|
6
8
|
export interface ComboboxOption {
|
|
7
9
|
value: string;
|
|
@@ -28,6 +30,10 @@
|
|
|
28
30
|
name?: string;
|
|
29
31
|
/** Element ID */
|
|
30
32
|
id?: string;
|
|
33
|
+
/** Label text for the combobox */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Hint text displayed below the combobox */
|
|
36
|
+
hint?: string;
|
|
31
37
|
/** Error message to display */
|
|
32
38
|
error?: string;
|
|
33
39
|
/** Whether async data is loading */
|
|
@@ -50,6 +56,8 @@
|
|
|
50
56
|
required = false,
|
|
51
57
|
name,
|
|
52
58
|
id,
|
|
59
|
+
label,
|
|
60
|
+
hint,
|
|
53
61
|
error,
|
|
54
62
|
loading = false,
|
|
55
63
|
onSearch,
|
|
@@ -57,6 +65,15 @@
|
|
|
57
65
|
class: className,
|
|
58
66
|
}: Props = $props();
|
|
59
67
|
|
|
68
|
+
// Generate unique IDs for accessibility
|
|
69
|
+
const randomSuffix = generateId('combobox').split('-').pop();
|
|
70
|
+
const componentId = $derived(id || `combobox-${randomSuffix}`);
|
|
71
|
+
const hintId = $derived(`${componentId}-hint`);
|
|
72
|
+
const errorId = $derived(`${componentId}-error`);
|
|
73
|
+
|
|
74
|
+
// Compute aria-describedby based on hint and error
|
|
75
|
+
const describedBy = $derived(computeDescribedBy(hintId, errorId, !!hint, !!error));
|
|
76
|
+
|
|
60
77
|
let searchQuery = $state('');
|
|
61
78
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
62
79
|
let open = $state(false);
|
|
@@ -104,86 +121,187 @@
|
|
|
104
121
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
105
122
|
};
|
|
106
123
|
});
|
|
124
|
+
|
|
125
|
+
// Determine if we need a container wrapper
|
|
126
|
+
const hasWrapper = $derived(!!label || !!hint || !!error);
|
|
107
127
|
</script>
|
|
108
128
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
>
|
|
119
|
-
|
|
120
|
-
<ComboboxPrimitive.Input
|
|
121
|
-
{id}
|
|
122
|
-
placeholder={selectedLabel || placeholder}
|
|
123
|
-
oninput={handleSearchInput}
|
|
124
|
-
onfocus={() => (open = true)}
|
|
125
|
-
onclick={() => (open = true)}
|
|
126
|
-
class={cn(
|
|
127
|
-
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
128
|
-
error && 'border-destructive focus:ring-destructive',
|
|
129
|
-
className
|
|
130
|
-
)}
|
|
131
|
-
aria-invalid={error ? 'true' : undefined}
|
|
132
|
-
/>
|
|
133
|
-
</div>
|
|
129
|
+
{#if hasWrapper}
|
|
130
|
+
<div class="space-y-2">
|
|
131
|
+
{#if label}
|
|
132
|
+
<Label for={componentId} {disabled}>
|
|
133
|
+
{label}
|
|
134
|
+
{#if required}
|
|
135
|
+
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
|
136
|
+
<span class="sr-only">(required)</span>
|
|
137
|
+
{/if}
|
|
138
|
+
</Label>
|
|
139
|
+
{/if}
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
141
|
+
<ComboboxPrimitive.Root
|
|
142
|
+
type="single"
|
|
143
|
+
{disabled}
|
|
144
|
+
{required}
|
|
145
|
+
{name}
|
|
146
|
+
bind:open
|
|
147
|
+
onOpenChange={handleOpenChange}
|
|
148
|
+
onValueChange={handleValueChange}
|
|
149
|
+
{value}
|
|
150
|
+
>
|
|
151
|
+
<div class="relative">
|
|
152
|
+
<ComboboxPrimitive.Input
|
|
153
|
+
id={componentId}
|
|
154
|
+
placeholder={selectedLabel || placeholder}
|
|
155
|
+
oninput={handleSearchInput}
|
|
156
|
+
onfocus={() => (open = true)}
|
|
157
|
+
onclick={() => (open = true)}
|
|
158
|
+
class={cn(
|
|
159
|
+
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
160
|
+
error && 'border-destructive focus:ring-destructive',
|
|
161
|
+
className
|
|
162
|
+
)}
|
|
163
|
+
aria-invalid={error ? 'true' : undefined}
|
|
164
|
+
aria-describedby={describedBy}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<ComboboxPrimitive.Content
|
|
169
|
+
class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
170
|
+
sideOffset={-1}
|
|
171
|
+
collisionPadding={8}
|
|
172
|
+
avoidCollisions={true}
|
|
173
|
+
>
|
|
174
|
+
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
175
|
+
{#if loading}
|
|
176
|
+
<div class="flex items-center justify-center py-6">
|
|
177
|
+
<Spinner size="sm" />
|
|
178
|
+
<span class="ml-2 text-sm text-muted-foreground">Loading...</span>
|
|
179
|
+
</div>
|
|
180
|
+
{:else if filteredOptions.length === 0}
|
|
181
|
+
<div class="py-6 text-center text-sm text-muted-foreground">
|
|
182
|
+
{emptyMessage}
|
|
183
|
+
</div>
|
|
184
|
+
{:else}
|
|
185
|
+
{#each filteredOptions as option}
|
|
186
|
+
<ComboboxPrimitive.Item
|
|
187
|
+
value={option.value}
|
|
188
|
+
label={option.label}
|
|
189
|
+
disabled={option.disabled}
|
|
190
|
+
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
191
|
+
>
|
|
192
|
+
{#if option.value === value}
|
|
193
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
194
|
+
<svg
|
|
195
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
196
|
+
width="16"
|
|
197
|
+
height="16"
|
|
198
|
+
viewBox="0 0 24 24"
|
|
199
|
+
fill="none"
|
|
200
|
+
stroke="currentColor"
|
|
201
|
+
stroke-width="2"
|
|
202
|
+
stroke-linecap="round"
|
|
203
|
+
stroke-linejoin="round"
|
|
204
|
+
class="h-4 w-4"
|
|
205
|
+
>
|
|
206
|
+
<polyline points="20 6 9 17 4 12" />
|
|
207
|
+
</svg>
|
|
208
|
+
</span>
|
|
209
|
+
{/if}
|
|
210
|
+
{option.label}
|
|
211
|
+
</ComboboxPrimitive.Item>
|
|
212
|
+
{/each}
|
|
213
|
+
{/if}
|
|
150
214
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
215
|
+
</ComboboxPrimitive.Content>
|
|
216
|
+
</ComboboxPrimitive.Root>
|
|
217
|
+
|
|
218
|
+
{#if hint && !error}
|
|
219
|
+
<p id={hintId} class="text-sm text-muted-foreground">
|
|
220
|
+
{hint}
|
|
221
|
+
</p>
|
|
222
|
+
{/if}
|
|
223
|
+
|
|
224
|
+
{#if error}
|
|
225
|
+
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
226
|
+
{error}
|
|
227
|
+
</p>
|
|
228
|
+
{/if}
|
|
229
|
+
</div>
|
|
230
|
+
{:else}
|
|
231
|
+
<ComboboxPrimitive.Root
|
|
232
|
+
type="single"
|
|
233
|
+
{disabled}
|
|
234
|
+
{required}
|
|
235
|
+
{name}
|
|
236
|
+
bind:open
|
|
237
|
+
onOpenChange={handleOpenChange}
|
|
238
|
+
onValueChange={handleValueChange}
|
|
239
|
+
{value}
|
|
240
|
+
>
|
|
241
|
+
<div class="relative">
|
|
242
|
+
<ComboboxPrimitive.Input
|
|
243
|
+
id={componentId}
|
|
244
|
+
placeholder={selectedLabel || placeholder}
|
|
245
|
+
oninput={handleSearchInput}
|
|
246
|
+
onfocus={() => (open = true)}
|
|
247
|
+
onclick={() => (open = true)}
|
|
248
|
+
class={cn(
|
|
249
|
+
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
250
|
+
error && 'border-destructive focus:ring-destructive',
|
|
251
|
+
className
|
|
252
|
+
)}
|
|
253
|
+
aria-invalid={error ? 'true' : undefined}
|
|
254
|
+
aria-describedby={describedBy}
|
|
255
|
+
/>
|
|
181
256
|
</div>
|
|
182
|
-
</ComboboxPrimitive.Content>
|
|
183
|
-
</ComboboxPrimitive.Root>
|
|
184
257
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
258
|
+
<ComboboxPrimitive.Content
|
|
259
|
+
class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
260
|
+
sideOffset={-1}
|
|
261
|
+
collisionPadding={8}
|
|
262
|
+
avoidCollisions={true}
|
|
263
|
+
>
|
|
264
|
+
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
265
|
+
{#if loading}
|
|
266
|
+
<div class="flex items-center justify-center py-6">
|
|
267
|
+
<Spinner size="sm" />
|
|
268
|
+
<span class="ml-2 text-sm text-muted-foreground">Loading...</span>
|
|
269
|
+
</div>
|
|
270
|
+
{:else if filteredOptions.length === 0}
|
|
271
|
+
<div class="py-6 text-center text-sm text-muted-foreground">
|
|
272
|
+
{emptyMessage}
|
|
273
|
+
</div>
|
|
274
|
+
{:else}
|
|
275
|
+
{#each filteredOptions as option}
|
|
276
|
+
<ComboboxPrimitive.Item
|
|
277
|
+
value={option.value}
|
|
278
|
+
label={option.label}
|
|
279
|
+
disabled={option.disabled}
|
|
280
|
+
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
281
|
+
>
|
|
282
|
+
{#if option.value === value}
|
|
283
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
284
|
+
<svg
|
|
285
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
286
|
+
width="16"
|
|
287
|
+
height="16"
|
|
288
|
+
viewBox="0 0 24 24"
|
|
289
|
+
fill="none"
|
|
290
|
+
stroke="currentColor"
|
|
291
|
+
stroke-width="2"
|
|
292
|
+
stroke-linecap="round"
|
|
293
|
+
stroke-linejoin="round"
|
|
294
|
+
class="h-4 w-4"
|
|
295
|
+
>
|
|
296
|
+
<polyline points="20 6 9 17 4 12" />
|
|
297
|
+
</svg>
|
|
298
|
+
</span>
|
|
299
|
+
{/if}
|
|
300
|
+
{option.label}
|
|
301
|
+
</ComboboxPrimitive.Item>
|
|
302
|
+
{/each}
|
|
303
|
+
{/if}
|
|
304
|
+
</div>
|
|
305
|
+
</ComboboxPrimitive.Content>
|
|
306
|
+
</ComboboxPrimitive.Root>
|
|
189
307
|
{/if}
|
|
@@ -22,6 +22,10 @@ interface Props {
|
|
|
22
22
|
name?: string;
|
|
23
23
|
/** Element ID */
|
|
24
24
|
id?: string;
|
|
25
|
+
/** Label text for the combobox */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Hint text displayed below the combobox */
|
|
28
|
+
hint?: string;
|
|
25
29
|
/** Error message to display */
|
|
26
30
|
error?: string;
|
|
27
31
|
/** Whether async data is loading */
|