@classic-homes/theme-svelte 1.22.1 → 1.22.3
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 +2 -2
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- package/dist/lib/components/layout/Sidebar.svelte +28 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +66 -50
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +2 -0
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
- package/dist/lib/stores/sidebar.svelte.js +15 -0
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- package/package.json +1 -1
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
collisionPadding={8}
|
|
187
187
|
avoidCollisions={true}
|
|
188
188
|
>
|
|
189
|
-
<div class="p-1 max-h-
|
|
189
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
190
190
|
{#if loading}
|
|
191
191
|
<div class="flex items-center justify-center py-6">
|
|
192
192
|
<Spinner size="sm" />
|
|
@@ -282,7 +282,7 @@
|
|
|
282
282
|
collisionPadding={8}
|
|
283
283
|
avoidCollisions={true}
|
|
284
284
|
>
|
|
285
|
-
<div class="p-1 max-h-
|
|
285
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
286
286
|
{#if loading}
|
|
287
287
|
<div class="flex items-center justify-center py-6">
|
|
288
288
|
<Spinner size="sm" />
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
rowSelection?: Set<string>;
|
|
86
86
|
/** Callback when selection changes */
|
|
87
87
|
onRowSelectionChange?: (selection: Set<string>) => void;
|
|
88
|
-
/** Function to get unique row ID (defaults to index) */
|
|
88
|
+
/** Function to get unique row ID (defaults to the row's index in `data`) */
|
|
89
89
|
getRowId?: (row: T, index: number) => string;
|
|
90
90
|
|
|
91
91
|
// === Visibility Props ===
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
const table = createDataTable<T>(() => ({
|
|
240
240
|
data,
|
|
241
241
|
columns,
|
|
242
|
-
getRowId
|
|
242
|
+
getRowId,
|
|
243
243
|
initialSortColumn: sortColumn,
|
|
244
244
|
initialSortDirection: sortDirection,
|
|
245
245
|
initialPageSize: pageSize,
|
|
@@ -248,6 +248,8 @@
|
|
|
248
248
|
manualSorting: !!onSort,
|
|
249
249
|
manualFiltering,
|
|
250
250
|
manualPagination,
|
|
251
|
+
// Without the pager every row must render, not just the first page.
|
|
252
|
+
enablePagination,
|
|
251
253
|
pageCount,
|
|
252
254
|
onSortChange: (col, dir) => {
|
|
253
255
|
sortColumn = col ?? undefined;
|
|
@@ -324,9 +326,14 @@
|
|
|
324
326
|
table.toggleAllRows();
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
// Keyboard activation for clickable rows; ignore keys aimed at the
|
|
330
|
+
// row's own controls (the selection checkbox handles its own Space).
|
|
331
|
+
function handleRowKeydown(e: KeyboardEvent, row: T) {
|
|
332
|
+
if (e.target !== e.currentTarget) return;
|
|
333
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
334
|
+
e.preventDefault();
|
|
335
|
+
handleRowClick(row);
|
|
336
|
+
}
|
|
330
337
|
}
|
|
331
338
|
|
|
332
339
|
// Literal class strings so Tailwind's content scan generates them.
|
|
@@ -442,7 +449,9 @@
|
|
|
442
449
|
onRowClick && 'cursor-pointer hover:bg-muted-hover',
|
|
443
450
|
row.isSelected && 'bg-primary/5'
|
|
444
451
|
)}
|
|
452
|
+
tabindex={onRowClick ? 0 : undefined}
|
|
445
453
|
onclick={() => handleRowClick(row.original)}
|
|
454
|
+
onkeydown={onRowClick ? (e) => handleRowKeydown(e, row.original) : undefined}
|
|
446
455
|
>
|
|
447
456
|
{#if enableRowSelection}
|
|
448
457
|
<td class="px-4 py-3">
|
|
@@ -451,7 +460,7 @@
|
|
|
451
460
|
checked={row.isSelected}
|
|
452
461
|
onCheckedChange={() => table.toggleRowSelection(row.id)}
|
|
453
462
|
aria-label={`Select row ${index + 1}`}
|
|
454
|
-
onclick={(e: MouseEvent) =>
|
|
463
|
+
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
455
464
|
/>
|
|
456
465
|
{/if}
|
|
457
466
|
</td>
|
|
@@ -66,7 +66,7 @@ declare function $$render<T extends object>(): {
|
|
|
66
66
|
rowSelection?: Set<string>;
|
|
67
67
|
/** Callback when selection changes */
|
|
68
68
|
onRowSelectionChange?: (selection: Set<string>) => void;
|
|
69
|
-
/** Function to get unique row ID (defaults to index) */
|
|
69
|
+
/** Function to get unique row ID (defaults to the row's index in `data`) */
|
|
70
70
|
getRowId?: (row: T, index: number) => string;
|
|
71
71
|
/** Enable column visibility toggling */
|
|
72
72
|
enableColumnVisibility?: boolean;
|
|
@@ -75,8 +75,9 @@
|
|
|
75
75
|
const hintId = $derived(`${componentId}-hint`);
|
|
76
76
|
const errorId = $derived(`${componentId}-error`);
|
|
77
77
|
|
|
78
|
-
// Sync stringValue -> value when stringValue changes externally
|
|
79
|
-
|
|
78
|
+
// Sync stringValue -> value when stringValue changes externally. Starts at
|
|
79
|
+
// null so an initial stringValue is adopted before value -> stringValue runs.
|
|
80
|
+
let lastStringValue = $state<string | null>(null);
|
|
80
81
|
$effect(() => {
|
|
81
82
|
if (stringValue !== lastStringValue) {
|
|
82
83
|
lastStringValue = stringValue;
|
|
@@ -117,7 +118,8 @@
|
|
|
117
118
|
// Compute aria-describedby based on hint and error
|
|
118
119
|
const describedBy = $derived.by(() => {
|
|
119
120
|
const ids: string[] = [];
|
|
120
|
-
|
|
121
|
+
// The hint is not rendered while an error shows
|
|
122
|
+
if (hint && !displayError) ids.push(hintId);
|
|
121
123
|
if (displayError) ids.push(errorId);
|
|
122
124
|
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
123
125
|
});
|
|
@@ -412,9 +414,6 @@
|
|
|
412
414
|
];
|
|
413
415
|
|
|
414
416
|
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
415
|
-
|
|
416
|
-
// Determine if we need a container wrapper
|
|
417
|
-
const hasWrapper = $derived(!!label || !!hint || !!error || !!parseError);
|
|
418
417
|
</script>
|
|
419
418
|
|
|
420
419
|
{#snippet pickerContent()}
|
|
@@ -633,32 +632,30 @@
|
|
|
633
632
|
{/if}
|
|
634
633
|
{/snippet}
|
|
635
634
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
{#if hint && !displayError}
|
|
651
|
-
<p id={hintId} class="text-sm text-muted-foreground">
|
|
652
|
-
{hint}
|
|
653
|
-
</p>
|
|
654
|
-
{/if}
|
|
655
|
-
|
|
656
|
-
{#if displayError}
|
|
657
|
-
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
658
|
-
{displayError}
|
|
659
|
-
</p>
|
|
660
|
-
{/if}
|
|
661
|
-
</div>
|
|
662
|
-
{:else}
|
|
635
|
+
<!-- Always wrapped: toggling the wrapper on a parse error would re-mount the
|
|
636
|
+
input and drop focus. -->
|
|
637
|
+
<div class="space-y-2">
|
|
638
|
+
{#if label}
|
|
639
|
+
<Label for={componentId} {disabled}>
|
|
640
|
+
{label}
|
|
641
|
+
{#if required}
|
|
642
|
+
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
|
643
|
+
<span class="sr-only">(required)</span>
|
|
644
|
+
{/if}
|
|
645
|
+
</Label>
|
|
646
|
+
{/if}
|
|
647
|
+
|
|
663
648
|
{@render pickerContent()}
|
|
664
|
-
|
|
649
|
+
|
|
650
|
+
{#if hint && !displayError}
|
|
651
|
+
<p id={hintId} class="text-sm text-muted-foreground">
|
|
652
|
+
{hint}
|
|
653
|
+
</p>
|
|
654
|
+
{/if}
|
|
655
|
+
|
|
656
|
+
{#if displayError}
|
|
657
|
+
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
658
|
+
{displayError}
|
|
659
|
+
</p>
|
|
660
|
+
{/if}
|
|
661
|
+
</div>
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
icon?: IconName;
|
|
18
18
|
/** Render the item as a link to this URL (selection still fires `onSelect`) */
|
|
19
19
|
href?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Make this a checkbox item (`menuitemcheckbox`) showing this state.
|
|
22
|
+
* Selecting it fires `onSelect` and keeps the menu open.
|
|
23
|
+
*/
|
|
24
|
+
checked?: boolean;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface DropdownMenuGroup {
|
|
@@ -102,6 +107,19 @@
|
|
|
102
107
|
<div role="presentation" class="px-2 py-1.5 text-sm font-semibold">
|
|
103
108
|
{item.label}
|
|
104
109
|
</div>
|
|
110
|
+
{:else if item.checked !== undefined}
|
|
111
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
112
|
+
checked={item.checked}
|
|
113
|
+
disabled={item.disabled}
|
|
114
|
+
closeOnSelect={false}
|
|
115
|
+
onSelect={() => item.onSelect?.()}
|
|
116
|
+
class="relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none bg-popover data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
117
|
+
>
|
|
118
|
+
{#if item.checked}
|
|
119
|
+
<Icon name="check" size={16} class="absolute left-2 shrink-0" aria-hidden="true" />
|
|
120
|
+
{/if}
|
|
121
|
+
{@render itemContent(item)}
|
|
122
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
105
123
|
{:else}
|
|
106
124
|
{@const itemClass = cn(
|
|
107
125
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none bg-popover data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default',
|
|
@@ -151,34 +169,38 @@
|
|
|
151
169
|
{/snippet}
|
|
152
170
|
</DropdownMenuPrimitive.Trigger>
|
|
153
171
|
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
172
|
+
<!-- Portal so the menu is not clipped by an overflow/transformed ancestor
|
|
173
|
+
(a Dialog, or the DataTable's scroll wrapper). -->
|
|
174
|
+
<DropdownMenuPrimitive.Portal>
|
|
175
|
+
<DropdownMenuPrimitive.Content
|
|
176
|
+
{side}
|
|
177
|
+
{align}
|
|
178
|
+
sideOffset={4}
|
|
179
|
+
collisionPadding={8}
|
|
180
|
+
avoidCollisions={true}
|
|
181
|
+
class={cn(
|
|
182
|
+
'isolate z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md 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',
|
|
183
|
+
className
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<div class="max-h-72 overflow-y-auto overflow-x-hidden">
|
|
187
|
+
{#each items as item}
|
|
188
|
+
{#if isGroup(item)}
|
|
189
|
+
<DropdownMenuPrimitive.Group>
|
|
190
|
+
{#if item.label}
|
|
191
|
+
<DropdownMenuPrimitive.GroupHeading class="px-2 py-1.5 text-sm font-semibold">
|
|
192
|
+
{item.label}
|
|
193
|
+
</DropdownMenuPrimitive.GroupHeading>
|
|
194
|
+
{/if}
|
|
195
|
+
{#each item.items as subItem}
|
|
196
|
+
{@render entry(subItem)}
|
|
197
|
+
{/each}
|
|
198
|
+
</DropdownMenuPrimitive.Group>
|
|
199
|
+
{:else}
|
|
200
|
+
{@render entry(item)}
|
|
201
|
+
{/if}
|
|
202
|
+
{/each}
|
|
203
|
+
</div>
|
|
204
|
+
</DropdownMenuPrimitive.Content>
|
|
205
|
+
</DropdownMenuPrimitive.Portal>
|
|
184
206
|
</DropdownMenuPrimitive.Root>
|
|
@@ -13,6 +13,11 @@ export interface DropdownMenuItem {
|
|
|
13
13
|
icon?: IconName;
|
|
14
14
|
/** Render the item as a link to this URL (selection still fires `onSelect`) */
|
|
15
15
|
href?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Make this a checkbox item (`menuitemcheckbox`) showing this state.
|
|
18
|
+
* Selecting it fires `onSelect` and keeps the menu open.
|
|
19
|
+
*/
|
|
20
|
+
checked?: boolean;
|
|
16
21
|
}
|
|
17
22
|
export interface DropdownMenuGroup {
|
|
18
23
|
label?: string;
|
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
|
|
87
87
|
let isDragging = $state(false);
|
|
88
88
|
let internalError = $state<string | undefined>(undefined);
|
|
89
|
-
let fileInput: HTMLInputElement;
|
|
90
89
|
let lightboxOpen = $state(false);
|
|
91
90
|
let lightboxIndex = $state(0);
|
|
92
91
|
|
|
@@ -420,13 +419,6 @@
|
|
|
420
419
|
}
|
|
421
420
|
}
|
|
422
421
|
|
|
423
|
-
// Handle click to browse
|
|
424
|
-
function handleClick() {
|
|
425
|
-
if (!disabled) {
|
|
426
|
-
fileInput?.click();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
422
|
// Handle file input change
|
|
431
423
|
function handleInputChange(e: Event) {
|
|
432
424
|
const input = e.target as HTMLInputElement;
|
|
@@ -492,20 +484,15 @@
|
|
|
492
484
|
|
|
493
485
|
const usePreviewGrid = $derived(previewLayout === 'grid' && previewItems.length > 0);
|
|
494
486
|
|
|
487
|
+
// The focusable file input carries the description, not the drop-zone label
|
|
488
|
+
const describedBy = $derived(displayError ? `${id}-error` : hint ? `${id}-hint` : undefined);
|
|
489
|
+
|
|
495
490
|
function openPreview(fileId: string) {
|
|
496
491
|
const previewIndex = previewItems.findIndex((item) => item.id === fileId);
|
|
497
492
|
if (previewIndex === -1) return;
|
|
498
493
|
lightboxIndex = previewIndex;
|
|
499
494
|
lightboxOpen = true;
|
|
500
495
|
}
|
|
501
|
-
|
|
502
|
-
// Handle keyboard activation
|
|
503
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
504
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
505
|
-
e.preventDefault();
|
|
506
|
-
handleClick();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
496
|
</script>
|
|
510
497
|
|
|
511
498
|
<div class={cn('space-y-2', className)}>
|
|
@@ -518,7 +505,6 @@
|
|
|
518
505
|
<!-- Drop Zone - using label to wrap the hidden input for better accessibility -->
|
|
519
506
|
<label
|
|
520
507
|
for={id}
|
|
521
|
-
aria-describedby={displayError ? `${id}-error` : undefined}
|
|
522
508
|
class={cn(
|
|
523
509
|
'relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-6',
|
|
524
510
|
'cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
|
@@ -560,12 +546,12 @@
|
|
|
560
546
|
|
|
561
547
|
<!-- Hidden file input -->
|
|
562
548
|
<input
|
|
563
|
-
bind:this={fileInput}
|
|
564
549
|
type="file"
|
|
565
550
|
{id}
|
|
566
551
|
multiple={maxFiles > 1}
|
|
567
552
|
accept={acceptedTypes.join(',')}
|
|
568
553
|
{disabled}
|
|
554
|
+
aria-describedby={describedBy}
|
|
569
555
|
class="sr-only"
|
|
570
556
|
onchange={handleInputChange}
|
|
571
557
|
/>
|
|
@@ -573,7 +559,7 @@
|
|
|
573
559
|
|
|
574
560
|
<!-- Hint text -->
|
|
575
561
|
{#if hint && !displayError}
|
|
576
|
-
<p class="text-sm text-muted-foreground">{hint}</p>
|
|
562
|
+
<p id={`${id}-hint`} class="text-sm text-muted-foreground">{hint}</p>
|
|
577
563
|
{/if}
|
|
578
564
|
|
|
579
565
|
<!-- Error message -->
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
collisionPadding={8}
|
|
245
245
|
avoidCollisions={true}
|
|
246
246
|
>
|
|
247
|
-
<div class="p-1 max-h-
|
|
247
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
248
248
|
{#if loading}
|
|
249
249
|
<div class="flex items-center justify-center py-6">
|
|
250
250
|
<Spinner size="sm" />
|
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
collisionPadding={8}
|
|
382
382
|
avoidCollisions={true}
|
|
383
383
|
>
|
|
384
|
-
<div class="p-1 max-h-
|
|
384
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
385
385
|
{#if loading}
|
|
386
386
|
<div class="flex items-center justify-center py-6">
|
|
387
387
|
<Spinner size="sm" />
|
|
@@ -101,10 +101,23 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Decimal places of a number, e.g. 0.25 -> 2, 1e-7 -> 7
|
|
105
|
+
function decimalPlaces(n: number): number {
|
|
106
|
+
const [mantissa, exponent] = String(n).split('e');
|
|
107
|
+
const fraction = mantissa.split('.')[1]?.length ?? 0;
|
|
108
|
+
return Math.max(0, fraction - (exponent ? parseInt(exponent) : 0));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add in floating point, then round away artifacts (0.2 + 0.1 -> 0.3)
|
|
112
|
+
function addStep(current: number, delta: number): number {
|
|
113
|
+
const places = Math.min(20, Math.max(decimalPlaces(current), decimalPlaces(delta)));
|
|
114
|
+
return Number((current + delta).toFixed(places));
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
function increment() {
|
|
105
118
|
if (disabled || readonly) return;
|
|
106
119
|
const currentValue = value ?? min ?? 0;
|
|
107
|
-
const newValue = clampValue(currentValue
|
|
120
|
+
const newValue = clampValue(addStep(currentValue, step));
|
|
108
121
|
value = newValue;
|
|
109
122
|
onValueChange?.(newValue);
|
|
110
123
|
}
|
|
@@ -112,7 +125,7 @@
|
|
|
112
125
|
function decrement() {
|
|
113
126
|
if (disabled || readonly) return;
|
|
114
127
|
const currentValue = value ?? max ?? 0;
|
|
115
|
-
const newValue = clampValue(currentValue -
|
|
128
|
+
const newValue = clampValue(addStep(currentValue, -step));
|
|
116
129
|
value = newValue;
|
|
117
130
|
onValueChange?.(newValue);
|
|
118
131
|
}
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
error?: string;
|
|
42
42
|
/** Hint text displayed below the select */
|
|
43
43
|
hint?: string;
|
|
44
|
+
/** Accessible name for the trigger, when there is no visible label */
|
|
45
|
+
'aria-label'?: string;
|
|
46
|
+
/** ID of an element that labels the trigger */
|
|
47
|
+
'aria-labelledby'?: string;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
let {
|
|
@@ -56,6 +60,8 @@
|
|
|
56
60
|
id,
|
|
57
61
|
error,
|
|
58
62
|
hint,
|
|
63
|
+
'aria-label': ariaLabelProp,
|
|
64
|
+
'aria-labelledby': ariaLabelledby,
|
|
59
65
|
}: Props = $props();
|
|
60
66
|
|
|
61
67
|
// Generate unique IDs for accessibility using sequential counter
|
|
@@ -68,11 +74,16 @@
|
|
|
68
74
|
// Compute aria-describedby based on hint and error
|
|
69
75
|
const describedBy = $derived.by(() => {
|
|
70
76
|
const ids: string[] = [];
|
|
71
|
-
|
|
77
|
+
// The hint is not rendered while an error shows
|
|
78
|
+
if (hint && !error) ids.push(hintId);
|
|
72
79
|
if (error) ids.push(errorId);
|
|
73
80
|
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
74
81
|
});
|
|
75
82
|
|
|
83
|
+
const fallbackAriaLabel = $derived(
|
|
84
|
+
ariaLabelProp ?? (!label && !id && !ariaLabelledby ? placeholder : undefined)
|
|
85
|
+
);
|
|
86
|
+
|
|
76
87
|
// Validate props in development
|
|
77
88
|
$effect(() => {
|
|
78
89
|
validateNonEmptyArray(options, 'options', 'Select');
|
|
@@ -81,7 +92,7 @@
|
|
|
81
92
|
// Accessibility: warn if no label is associated in dev mode
|
|
82
93
|
if (DEV) {
|
|
83
94
|
$effect(() => {
|
|
84
|
-
if (!label && !id) {
|
|
95
|
+
if (!label && !id && !ariaLabelProp && !ariaLabelledby) {
|
|
85
96
|
console.warn(
|
|
86
97
|
'[Select] Missing accessible label. Provide a label prop or an id with a matching <label for="...">.'
|
|
87
98
|
);
|
|
@@ -209,7 +220,7 @@
|
|
|
209
220
|
collisionPadding={8}
|
|
210
221
|
avoidCollisions={true}
|
|
211
222
|
>
|
|
212
|
-
<div class="p-1 max-h-
|
|
223
|
+
<div class="p-1 max-h-72 overflow-y-auto">
|
|
213
224
|
{#each options as item}
|
|
214
225
|
{#if isGroup(item)}
|
|
215
226
|
<SelectPrimitive.Group>
|
|
@@ -229,6 +240,8 @@
|
|
|
229
240
|
</SelectPrimitive.Portal>
|
|
230
241
|
{/snippet}
|
|
231
242
|
|
|
243
|
+
<!-- The placeholder is only a last-resort name: an aria-label would override
|
|
244
|
+
an external <label for={id}> or aria-labelledby. -->
|
|
232
245
|
{#snippet selectRoot(ariaLabel: string | undefined)}
|
|
233
246
|
<SelectPrimitive.Root
|
|
234
247
|
type="single"
|
|
@@ -243,6 +256,7 @@
|
|
|
243
256
|
id={componentId}
|
|
244
257
|
class={triggerClasses}
|
|
245
258
|
aria-label={ariaLabel}
|
|
259
|
+
aria-labelledby={ariaLabelledby}
|
|
246
260
|
aria-invalid={error ? 'true' : undefined}
|
|
247
261
|
aria-describedby={describedBy}
|
|
248
262
|
>
|
|
@@ -268,7 +282,7 @@
|
|
|
268
282
|
</Label>
|
|
269
283
|
{/if}
|
|
270
284
|
|
|
271
|
-
{@render selectRoot(
|
|
285
|
+
{@render selectRoot(fallbackAriaLabel)}
|
|
272
286
|
|
|
273
287
|
{#if hint && !error}
|
|
274
288
|
<p id={hintId} class="text-sm text-muted-foreground">
|
|
@@ -283,5 +297,5 @@
|
|
|
283
297
|
{/if}
|
|
284
298
|
</div>
|
|
285
299
|
{:else}
|
|
286
|
-
{@render selectRoot(
|
|
300
|
+
{@render selectRoot(fallbackAriaLabel)}
|
|
287
301
|
{/if}
|
|
@@ -32,6 +32,10 @@ interface Props {
|
|
|
32
32
|
error?: string;
|
|
33
33
|
/** Hint text displayed below the select */
|
|
34
34
|
hint?: string;
|
|
35
|
+
/** Accessible name for the trigger, when there is no visible label */
|
|
36
|
+
'aria-label'?: string;
|
|
37
|
+
/** ID of an element that labels the trigger */
|
|
38
|
+
'aria-labelledby'?: string;
|
|
35
39
|
}
|
|
36
40
|
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
37
41
|
type Select = ReturnType<typeof Select>;
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
'inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium',
|
|
32
32
|
modeButtonActive: 'bg-background text-foreground shadow-sm',
|
|
33
33
|
modeButtonInactive: 'text-muted-foreground hover:text-foreground',
|
|
34
|
-
|
|
34
|
+
// A white pad, like the exported image, so the default black ink stays
|
|
35
|
+
// visible in dark mode.
|
|
36
|
+
canvasContainer: 'relative w-full overflow-hidden rounded-lg border bg-white',
|
|
35
37
|
canvas: 'block w-full cursor-crosshair touch-none',
|
|
36
38
|
toolbar: 'flex flex-wrap items-center gap-2',
|
|
37
39
|
toolButton:
|
|
@@ -88,7 +90,7 @@
|
|
|
88
90
|
/** Default typed name for type mode */
|
|
89
91
|
typedName?: string;
|
|
90
92
|
|
|
91
|
-
/** Stroke color (default:
|
|
93
|
+
/** Stroke color (default: #000000, drawn on a white pad to match the export) */
|
|
92
94
|
strokeColor?: string;
|
|
93
95
|
/** Stroke width in pixels */
|
|
94
96
|
strokeWidth?: number;
|
|
@@ -222,8 +224,11 @@
|
|
|
222
224
|
);
|
|
223
225
|
let fontsLoaded = $state<Set<string>>(new Set());
|
|
224
226
|
|
|
225
|
-
// Consent state
|
|
227
|
+
// Consent state, kept in step with the bindable prop both ways
|
|
226
228
|
let consented = $state(untrack(() => initialConsented));
|
|
229
|
+
$effect(() => {
|
|
230
|
+
consented = initialConsented;
|
|
231
|
+
});
|
|
227
232
|
|
|
228
233
|
// Validation state
|
|
229
234
|
let validationResult = $state<SignatureValidationResult>({ valid: false });
|
|
@@ -661,7 +666,10 @@
|
|
|
661
666
|
function handlePointerUp(e: PointerEvent): void {
|
|
662
667
|
if (!isDrawing || !currentStroke || disabled) return;
|
|
663
668
|
|
|
664
|
-
|
|
669
|
+
// After pointercancel the capture is already gone and release would throw
|
|
670
|
+
if (canvas.hasPointerCapture?.(e.pointerId)) {
|
|
671
|
+
canvas.releasePointerCapture(e.pointerId);
|
|
672
|
+
}
|
|
665
673
|
isDrawing = false;
|
|
666
674
|
currentStroke.endTime = Date.now();
|
|
667
675
|
|
|
@@ -732,6 +740,8 @@
|
|
|
732
740
|
function handleConsentChange(e: Event): void {
|
|
733
741
|
const target = e.target as HTMLInputElement;
|
|
734
742
|
consented = target.checked;
|
|
743
|
+
// Write back so bind:consented reflects the checkbox
|
|
744
|
+
initialConsented = consented;
|
|
735
745
|
onConsentChange?.(consented);
|
|
736
746
|
updateValue();
|
|
737
747
|
}
|
|
@@ -915,6 +925,7 @@
|
|
|
915
925
|
onpointerdown={handlePointerDown}
|
|
916
926
|
onpointermove={handlePointerMove}
|
|
917
927
|
onpointerup={handlePointerUp}
|
|
928
|
+
onpointercancel={handlePointerUp}
|
|
918
929
|
onpointerleave={handlePointerLeave}
|
|
919
930
|
onkeydown={handleKeyDown}
|
|
920
931
|
></canvas>
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
showModeToggle?: boolean;
|
|
15
15
|
/** Default typed name for type mode */
|
|
16
16
|
typedName?: string;
|
|
17
|
-
/** Stroke color (default:
|
|
17
|
+
/** Stroke color (default: #000000, drawn on a white pad to match the export) */
|
|
18
18
|
strokeColor?: string;
|
|
19
19
|
/** Stroke width in pixels */
|
|
20
20
|
strokeWidth?: number;
|
|
@@ -7,23 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
const sliderVariants = tv({
|
|
9
9
|
slots: {
|
|
10
|
-
root: 'relative flex w-full touch-none select-none items-center',
|
|
11
|
-
|
|
10
|
+
root: 'relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:flex-col',
|
|
11
|
+
// Vertical: bits-ui positions the range by top/bottom, so drop h-full
|
|
12
|
+
range:
|
|
13
|
+
'absolute h-full bg-primary rounded-full data-[orientation=vertical]:h-auto data-[orientation=vertical]:w-full',
|
|
12
14
|
thumb:
|
|
13
15
|
"block rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 relative after:absolute after:left-1/2 after:top-1/2 after:h-11 after:w-11 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:content-['']",
|
|
14
16
|
},
|
|
15
17
|
variants: {
|
|
16
18
|
size: {
|
|
17
19
|
sm: {
|
|
18
|
-
root: 'h-1.5',
|
|
20
|
+
root: 'h-1.5 data-[orientation=vertical]:w-1.5',
|
|
19
21
|
thumb: 'h-4 w-4',
|
|
20
22
|
},
|
|
21
23
|
md: {
|
|
22
|
-
root: 'h-2',
|
|
24
|
+
root: 'h-2 data-[orientation=vertical]:w-2',
|
|
23
25
|
thumb: 'h-5 w-5',
|
|
24
26
|
},
|
|
25
27
|
lg: {
|
|
26
|
-
root: 'h-3',
|
|
28
|
+
root: 'h-3 data-[orientation=vertical]:w-3',
|
|
27
29
|
thumb: 'h-6 w-6',
|
|
28
30
|
},
|
|
29
31
|
},
|
|
@@ -140,7 +142,6 @@
|
|
|
140
142
|
onValueChange={handleValueChange}
|
|
141
143
|
onValueCommit={handleValueCommit}
|
|
142
144
|
class={cn(styles.root(), 'rounded-full bg-muted')}
|
|
143
|
-
aria-describedby={describedBy}
|
|
144
145
|
>
|
|
145
146
|
<SliderPrimitive.Range class={styles.range()} />
|
|
146
147
|
{#each value as thumbValue, i}
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
aria-valuemin={min}
|
|
153
154
|
aria-valuemax={max}
|
|
154
155
|
aria-valuetext={formatValue(thumbValue)}
|
|
156
|
+
aria-describedby={describedBy}
|
|
155
157
|
/>
|
|
156
158
|
{/each}
|
|
157
159
|
</SliderPrimitive.Root>
|