@classic-homes/theme-svelte 0.1.52 → 0.1.54
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/__tests__/a11y/Alert.a11y.test.js +84 -0
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +126 -0
- package/dist/__tests__/a11y/Button.a11y.test.js +66 -0
- package/dist/__tests__/a11y/Card.a11y.test.js +77 -0
- package/dist/__tests__/a11y/Combobox.a11y.test.js +209 -0
- package/dist/__tests__/a11y/Dialog.a11y.test.js +103 -0
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +104 -0
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +158 -0
- package/dist/__tests__/a11y/Icon.a11y.test.js +119 -0
- package/dist/__tests__/a11y/Input.a11y.test.js +96 -0
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +102 -0
- package/dist/__tests__/a11y/Switch.a11y.test.js +95 -0
- package/dist/__tests__/a11y/Tabs.a11y.test.js +97 -0
- package/dist/__tests__/a11y/Textarea.a11y.test.js +111 -0
- package/dist/__tests__/setup.js +6 -0
- package/dist/lib/components/Alert.svelte +1 -1
- package/dist/lib/components/AlertDialog.svelte +1 -1
- package/dist/lib/components/AlertDialog.svelte.d.ts +2 -2
- package/dist/lib/components/Badge.svelte +49 -12
- package/dist/lib/components/Badge.svelte.d.ts +9 -0
- package/dist/lib/components/Button.svelte +14 -2
- package/dist/lib/components/CardTitle.svelte +9 -3
- package/dist/lib/components/CardTitle.svelte.d.ts +2 -0
- package/dist/lib/components/Combobox.svelte +19 -2
- package/dist/lib/components/DataPanelHeader.svelte +4 -4
- package/dist/lib/components/DataPanelTab.svelte +1 -1
- package/dist/lib/components/DataTable.svelte +7 -7
- package/dist/lib/components/DataTable.svelte.d.ts +5 -5
- package/dist/lib/components/DateTimePicker.svelte +7 -4
- package/dist/lib/components/Dialog.svelte +6 -6
- package/dist/lib/components/DropdownMenu.svelte +14 -10
- package/dist/lib/components/FileUpload.svelte +5 -10
- package/dist/lib/components/FormField.svelte +7 -1
- package/dist/lib/components/HeaderSearch.svelte +9 -9
- package/dist/lib/components/Icon.svelte +5 -5
- package/dist/lib/components/Input.svelte +32 -0
- package/dist/lib/components/Input.svelte.d.ts +8 -0
- package/dist/lib/components/LiveRegion.svelte +61 -0
- package/dist/lib/components/LiveRegion.svelte.d.ts +18 -0
- package/dist/lib/components/LoadingOverlay.svelte +148 -0
- package/dist/lib/components/LoadingOverlay.svelte.d.ts +29 -0
- package/dist/lib/components/LoadingPage.svelte +86 -0
- package/dist/lib/components/LoadingPage.svelte.d.ts +20 -0
- package/dist/lib/components/LoadingSection.svelte +54 -0
- package/dist/lib/components/LoadingSection.svelte.d.ts +25 -0
- package/dist/lib/components/MultiSelect.svelte +23 -2
- package/dist/lib/components/NumberInput.svelte +4 -4
- package/dist/lib/components/OTPInput.svelte +14 -0
- package/dist/lib/components/PasswordInput.svelte +14 -1
- package/dist/lib/components/PasswordInput.svelte.d.ts +4 -0
- package/dist/lib/components/ProgressBar.svelte +91 -0
- package/dist/lib/components/ProgressBar.svelte.d.ts +18 -0
- package/dist/lib/components/RadioGroup.svelte +5 -5
- package/dist/lib/components/Select.svelte +13 -1
- package/dist/lib/components/Signature.svelte +5 -5
- package/dist/lib/components/Skeleton.svelte +13 -3
- package/dist/lib/components/Skeleton.svelte.d.ts +2 -0
- package/dist/lib/components/SkipLink.svelte +67 -0
- package/dist/lib/components/SkipLink.svelte.d.ts +25 -0
- package/dist/lib/components/Slider.svelte +6 -2
- package/dist/lib/components/Switch.svelte +8 -0
- package/dist/lib/components/Switch.svelte.d.ts +4 -0
- package/dist/lib/components/Tabs.svelte +15 -5
- package/dist/lib/components/Tabs.svelte.d.ts +6 -0
- package/dist/lib/components/Textarea.svelte +19 -0
- package/dist/lib/components/Textarea.svelte.d.ts +8 -0
- package/dist/lib/components/Toast.svelte +1 -1
- package/dist/lib/components/layout/AuthLayout.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +1 -1
- package/dist/lib/components/layout/Footer.svelte +1 -1
- package/dist/lib/components/layout/PublicLayout.svelte +2 -2
- package/dist/lib/components/layout/QuickLinks.svelte +3 -3
- package/dist/lib/components/layout/Sidebar.svelte +1 -1
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +1 -2
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +20 -21
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +2 -2
- package/dist/lib/components/notifications/NotificationFilters.svelte +1 -1
- package/dist/lib/components/notifications/NotificationItem.svelte +2 -1
- package/dist/lib/composables/index.d.ts +8 -0
- package/dist/lib/composables/index.js +9 -0
- package/dist/lib/composables/useAnnounce.svelte.d.ts +65 -0
- package/dist/lib/composables/useAnnounce.svelte.js +76 -0
- package/dist/lib/composables/useArrowNavigation.svelte.d.ts +42 -0
- package/dist/lib/composables/useArrowNavigation.svelte.js +138 -0
- package/dist/lib/composables/useClickOutside.svelte.d.ts +88 -0
- package/dist/lib/composables/useClickOutside.svelte.js +100 -0
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +48 -0
- package/dist/lib/composables/useEscapeKey.svelte.js +50 -0
- package/dist/lib/composables/useFocusRef.svelte.d.ts +46 -0
- package/dist/lib/composables/useFocusRef.svelte.js +92 -0
- package/dist/lib/composables/useFocusTrap.svelte.d.ts +42 -0
- package/dist/lib/composables/useFocusTrap.svelte.js +136 -0
- package/dist/lib/composables/useReducedMotion.svelte.d.ts +47 -0
- package/dist/lib/composables/useReducedMotion.svelte.js +67 -0
- package/dist/lib/composables/useRovingTabindex.svelte.d.ts +54 -0
- package/dist/lib/composables/useRovingTabindex.svelte.js +128 -0
- package/dist/lib/index.d.ts +7 -1
- package/dist/lib/index.js +11 -1
- package/dist/lib/stores/theme.svelte.d.ts +1 -1
- package/dist/lib/utils.d.ts +4 -2
- package/dist/lib/utils.js +5 -11
- package/package.json +11 -5
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
<button
|
|
483
483
|
type="button"
|
|
484
484
|
onclick={prevMonth}
|
|
485
|
-
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent
|
|
485
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent hover:bg-accent hover:text-accent-foreground"
|
|
486
486
|
aria-label="Previous month"
|
|
487
487
|
>
|
|
488
488
|
<svg
|
|
@@ -506,7 +506,7 @@
|
|
|
506
506
|
<button
|
|
507
507
|
type="button"
|
|
508
508
|
onclick={nextMonth}
|
|
509
|
-
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent
|
|
509
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent hover:bg-accent hover:text-accent-foreground"
|
|
510
510
|
aria-label="Next month"
|
|
511
511
|
>
|
|
512
512
|
<svg
|
|
@@ -547,7 +547,7 @@
|
|
|
547
547
|
onclick={() => selectDate(day)}
|
|
548
548
|
disabled={isDateDisabled(day)}
|
|
549
549
|
class={cn(
|
|
550
|
-
'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm bg-transparent
|
|
550
|
+
'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm bg-transparent',
|
|
551
551
|
'hover:bg-accent hover:text-accent-foreground',
|
|
552
552
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
553
553
|
'disabled:pointer-events-none disabled:opacity-50',
|
|
@@ -569,6 +569,7 @@
|
|
|
569
569
|
<select
|
|
570
570
|
bind:value={hour}
|
|
571
571
|
onchange={updateTime}
|
|
572
|
+
aria-label="Hour"
|
|
572
573
|
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
573
574
|
>
|
|
574
575
|
{#each hourOptions as h}
|
|
@@ -577,10 +578,11 @@
|
|
|
577
578
|
</option>
|
|
578
579
|
{/each}
|
|
579
580
|
</select>
|
|
580
|
-
<span class="text-muted-foreground">:</span>
|
|
581
|
+
<span class="text-muted-foreground" aria-hidden="true">:</span>
|
|
581
582
|
<select
|
|
582
583
|
bind:value={minute}
|
|
583
584
|
onchange={updateTime}
|
|
585
|
+
aria-label="Minute"
|
|
584
586
|
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
585
587
|
>
|
|
586
588
|
{#each minuteOptions as m}
|
|
@@ -591,6 +593,7 @@
|
|
|
591
593
|
<select
|
|
592
594
|
bind:value={period}
|
|
593
595
|
onchange={updateTime}
|
|
596
|
+
aria-label="AM or PM"
|
|
594
597
|
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
595
598
|
>
|
|
596
599
|
<option value="AM">AM</option>
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
|
|
79
79
|
<DialogPrimitive.Portal>
|
|
80
80
|
<DialogPrimitive.Overlay
|
|
81
|
-
class="fixed inset-0 z-50 bg-
|
|
81
|
+
class="fixed inset-0 z-50 bg-foreground/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
82
82
|
/>
|
|
83
83
|
<DialogPrimitive.Content
|
|
84
84
|
class={cn(
|
|
85
|
-
'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-
|
|
85
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg overflow-hidden',
|
|
86
86
|
sizeClasses[size],
|
|
87
87
|
className
|
|
88
88
|
)}
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
<!-- Header section with title and actions -->
|
|
91
91
|
{#if header || title || description || headerActions || showClose}
|
|
92
92
|
<div
|
|
93
|
-
class="flex items-start justify-between gap-4 border-b border-
|
|
93
|
+
class="flex items-start justify-between gap-4 border-b border-border bg-muted/30 px-6 py-4"
|
|
94
94
|
>
|
|
95
95
|
<!-- Title section -->
|
|
96
96
|
<div class="flex flex-col space-y-1.5 min-w-0 flex-1 overflow-hidden">
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
{/if}
|
|
118
118
|
{#if showClose}
|
|
119
119
|
<DialogPrimitive.Close
|
|
120
|
-
class="flex h-9 w-9 items-center justify-center rounded-md bg-transparent text-muted-foreground
|
|
120
|
+
class="flex h-9 w-9 items-center justify-center rounded-md bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none"
|
|
121
121
|
aria-label="Close dialog"
|
|
122
122
|
>
|
|
123
123
|
<svg
|
|
@@ -131,11 +131,11 @@
|
|
|
131
131
|
stroke-linecap="round"
|
|
132
132
|
stroke-linejoin="round"
|
|
133
133
|
class="h-5 w-5"
|
|
134
|
+
aria-hidden="true"
|
|
134
135
|
>
|
|
135
136
|
<path d="M18 6 6 18" />
|
|
136
137
|
<path d="m6 6 12 12" />
|
|
137
138
|
</svg>
|
|
138
|
-
<span class="sr-only">Close</span>
|
|
139
139
|
</DialogPrimitive.Close>
|
|
140
140
|
{/if}
|
|
141
141
|
</div>
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
<!-- Footer section -->
|
|
151
151
|
{#if footer}
|
|
152
152
|
<div
|
|
153
|
-
class="flex flex-col-reverse border-t border-
|
|
153
|
+
class="flex flex-col-reverse border-t border-border bg-muted/30 px-6 py-4 sm:flex-row sm:justify-end sm:space-x-2"
|
|
154
154
|
>
|
|
155
155
|
{@render footer()}
|
|
156
156
|
</div>
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
<DropdownMenuPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
59
59
|
<DropdownMenuPrimitive.Trigger>
|
|
60
60
|
{#snippet child({ props })}
|
|
61
|
-
<
|
|
61
|
+
<button type="button" {...props}>
|
|
62
62
|
{@render trigger()}
|
|
63
|
-
</
|
|
63
|
+
</button>
|
|
64
64
|
{/snippet}
|
|
65
65
|
</DropdownMenuPrimitive.Trigger>
|
|
66
66
|
|
|
@@ -80,15 +80,15 @@
|
|
|
80
80
|
{#if isGroup(item)}
|
|
81
81
|
<DropdownMenuPrimitive.Group>
|
|
82
82
|
{#if item.label}
|
|
83
|
-
<
|
|
83
|
+
<DropdownMenuPrimitive.GroupHeading class="px-2 py-1.5 text-sm font-semibold">
|
|
84
84
|
{item.label}
|
|
85
|
-
</
|
|
85
|
+
</DropdownMenuPrimitive.GroupHeading>
|
|
86
86
|
{/if}
|
|
87
87
|
{#each item.items as subItem}
|
|
88
88
|
{#if subItem.type === 'separator'}
|
|
89
89
|
<DropdownMenuPrimitive.Separator class="-mx-1 my-1 h-px bg-muted" />
|
|
90
90
|
{:else if subItem.type === 'label'}
|
|
91
|
-
<div class="px-2 py-1.5 text-sm font-semibold">
|
|
91
|
+
<div role="presentation" class="px-2 py-1.5 text-sm font-semibold">
|
|
92
92
|
{subItem.label}
|
|
93
93
|
</div>
|
|
94
94
|
{:else}
|
|
@@ -96,15 +96,17 @@
|
|
|
96
96
|
disabled={subItem.disabled}
|
|
97
97
|
onSelect={() => subItem.onSelect?.()}
|
|
98
98
|
class={cn(
|
|
99
|
-
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none
|
|
99
|
+
'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',
|
|
100
100
|
subItem.destructive && 'text-destructive data-[highlighted]:text-destructive'
|
|
101
101
|
)}
|
|
102
|
+
aria-keyshortcuts={subItem.shortcut || undefined}
|
|
102
103
|
>
|
|
103
104
|
{subItem.label}
|
|
104
105
|
{#if subItem.shortcut}
|
|
105
|
-
<span class="ml-auto text-xs tracking-widest opacity-60">
|
|
106
|
+
<span class="ml-auto text-xs tracking-widest opacity-60" aria-hidden="true">
|
|
106
107
|
{subItem.shortcut}
|
|
107
108
|
</span>
|
|
109
|
+
<span class="sr-only">, keyboard shortcut {subItem.shortcut}</span>
|
|
108
110
|
{/if}
|
|
109
111
|
</DropdownMenuPrimitive.Item>
|
|
110
112
|
{/if}
|
|
@@ -113,7 +115,7 @@
|
|
|
113
115
|
{:else if item.type === 'separator'}
|
|
114
116
|
<DropdownMenuPrimitive.Separator class="-mx-1 my-1 h-px bg-muted" />
|
|
115
117
|
{:else if item.type === 'label'}
|
|
116
|
-
<div class="px-2 py-1.5 text-sm font-semibold">
|
|
118
|
+
<div role="presentation" class="px-2 py-1.5 text-sm font-semibold">
|
|
117
119
|
{item.label}
|
|
118
120
|
</div>
|
|
119
121
|
{:else}
|
|
@@ -121,15 +123,17 @@
|
|
|
121
123
|
disabled={item.disabled}
|
|
122
124
|
onSelect={() => item.onSelect?.()}
|
|
123
125
|
class={cn(
|
|
124
|
-
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none
|
|
126
|
+
'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',
|
|
125
127
|
item.destructive && 'text-destructive data-[highlighted]:text-destructive'
|
|
126
128
|
)}
|
|
129
|
+
aria-keyshortcuts={item.shortcut || undefined}
|
|
127
130
|
>
|
|
128
131
|
{item.label}
|
|
129
132
|
{#if item.shortcut}
|
|
130
|
-
<span class="ml-auto text-xs tracking-widest opacity-60">
|
|
133
|
+
<span class="ml-auto text-xs tracking-widest opacity-60" aria-hidden="true">
|
|
131
134
|
{item.shortcut}
|
|
132
135
|
</span>
|
|
136
|
+
<span class="sr-only">, keyboard shortcut {item.shortcut}</span>
|
|
133
137
|
{/if}
|
|
134
138
|
</DropdownMenuPrimitive.Item>
|
|
135
139
|
{/if}
|
|
@@ -297,15 +297,13 @@
|
|
|
297
297
|
</label>
|
|
298
298
|
{/if}
|
|
299
299
|
|
|
300
|
-
<!-- Drop Zone -->
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
tabindex={disabled ? -1 : 0}
|
|
304
|
-
aria-disabled={disabled}
|
|
300
|
+
<!-- Drop Zone - using label to wrap the hidden input for better accessibility -->
|
|
301
|
+
<label
|
|
302
|
+
for={id}
|
|
305
303
|
aria-describedby={displayError ? `${id}-error` : undefined}
|
|
306
304
|
class={cn(
|
|
307
305
|
'relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors',
|
|
308
|
-
'cursor-pointer focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
306
|
+
'cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
|
309
307
|
isDragging && !disabled && 'border-primary bg-primary/5',
|
|
310
308
|
disabled && 'cursor-not-allowed opacity-50',
|
|
311
309
|
displayError && 'border-destructive',
|
|
@@ -318,8 +316,6 @@
|
|
|
318
316
|
ondragleave={handleDragLeave}
|
|
319
317
|
ondragover={handleDragOver}
|
|
320
318
|
ondrop={handleDrop}
|
|
321
|
-
onclick={handleClick}
|
|
322
|
-
onkeydown={handleKeyDown}
|
|
323
319
|
>
|
|
324
320
|
<!-- Cloud Upload Icon -->
|
|
325
321
|
<svg
|
|
@@ -354,9 +350,8 @@
|
|
|
354
350
|
{disabled}
|
|
355
351
|
class="sr-only"
|
|
356
352
|
onchange={handleInputChange}
|
|
357
|
-
aria-label="File upload"
|
|
358
353
|
/>
|
|
359
|
-
</
|
|
354
|
+
</label>
|
|
360
355
|
|
|
361
356
|
<!-- Hint text -->
|
|
362
357
|
{#if hint && !displayError}
|
|
@@ -122,7 +122,13 @@
|
|
|
122
122
|
|
|
123
123
|
<!-- Error message (takes precedence over hint) -->
|
|
124
124
|
{#if error}
|
|
125
|
-
<p
|
|
125
|
+
<p
|
|
126
|
+
id={errorId}
|
|
127
|
+
class="text-sm text-destructive"
|
|
128
|
+
role="alert"
|
|
129
|
+
aria-live="polite"
|
|
130
|
+
aria-atomic="true"
|
|
131
|
+
>
|
|
126
132
|
{error}
|
|
127
133
|
</p>
|
|
128
134
|
{:else if hint}
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
* @example Basic usage:
|
|
16
16
|
* ```svelte
|
|
17
17
|
* <HeaderSearch
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* enabled={true}
|
|
19
|
+
* placeholder="Search..."
|
|
20
|
+
* onSearch={(query) => handleSearch(query)}
|
|
21
|
+
* onSelect={(item) => goto(item.href)}
|
|
22
|
+
* results={searchResults}
|
|
23
|
+
* loading={isSearching}
|
|
24
24
|
* />
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
type="button"
|
|
176
176
|
onclick={() => (open = true)}
|
|
177
177
|
class={cn(
|
|
178
|
-
'inline-flex items-center justify-center rounded-md border border-input bg-background text-muted-foreground
|
|
178
|
+
'inline-flex items-center justify-center rounded-md border border-input bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
179
179
|
size === 'sm' ? 'h-8 w-8' : 'h-9 w-9',
|
|
180
180
|
className
|
|
181
181
|
)}
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
type="button"
|
|
198
198
|
onclick={() => (open = true)}
|
|
199
199
|
class={cn(
|
|
200
|
-
'flex items-center gap-2 rounded-md border border-input bg-background px-3 text-sm text-muted-foreground
|
|
200
|
+
'flex items-center gap-2 rounded-md border border-input bg-background px-3 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
201
201
|
size === 'sm' ? 'h-8' : 'h-9',
|
|
202
202
|
className
|
|
203
203
|
)}
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
type="button"
|
|
288
288
|
onclick={() => handleSelect(item)}
|
|
289
289
|
class={cn(
|
|
290
|
-
'flex w-full cursor-pointer items-start gap-3 rounded-md px-3 py-2 text-left text-sm
|
|
290
|
+
'flex w-full cursor-pointer items-start gap-3 rounded-md px-3 py-2 text-left text-sm',
|
|
291
291
|
index === selectedIndex
|
|
292
292
|
? 'bg-accent text-accent-foreground'
|
|
293
293
|
: 'hover:bg-accent-hover'
|
|
@@ -895,7 +895,7 @@
|
|
|
895
895
|
{#if name === 'loading'}
|
|
896
896
|
<!-- Loading spinner with animation -->
|
|
897
897
|
<svg
|
|
898
|
-
aria-hidden=
|
|
898
|
+
aria-hidden="true"
|
|
899
899
|
width={size}
|
|
900
900
|
height={size}
|
|
901
901
|
viewBox="0 0 24 24"
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
{:else if name === 'more'}
|
|
912
912
|
<!-- Vertical dots -->
|
|
913
913
|
<svg
|
|
914
|
-
aria-hidden=
|
|
914
|
+
aria-hidden="true"
|
|
915
915
|
width={size}
|
|
916
916
|
height={size}
|
|
917
917
|
viewBox="0 0 24 24"
|
|
@@ -926,7 +926,7 @@
|
|
|
926
926
|
{:else if name === 'more-horizontal'}
|
|
927
927
|
<!-- Horizontal dots -->
|
|
928
928
|
<svg
|
|
929
|
-
aria-hidden=
|
|
929
|
+
aria-hidden="true"
|
|
930
930
|
width={size}
|
|
931
931
|
height={size}
|
|
932
932
|
viewBox="0 0 24 24"
|
|
@@ -941,7 +941,7 @@
|
|
|
941
941
|
{:else if name === 'grip'}
|
|
942
942
|
<!-- Grip handle (6 dots for drag) -->
|
|
943
943
|
<svg
|
|
944
|
-
aria-hidden=
|
|
944
|
+
aria-hidden="true"
|
|
945
945
|
width={size}
|
|
946
946
|
height={size}
|
|
947
947
|
viewBox="0 0 24 24"
|
|
@@ -959,7 +959,7 @@
|
|
|
959
959
|
{:else if iconDef}
|
|
960
960
|
<!-- Standard path-based icons -->
|
|
961
961
|
<svg
|
|
962
|
-
aria-hidden=
|
|
962
|
+
aria-hidden="true"
|
|
963
963
|
width={size}
|
|
964
964
|
height={size}
|
|
965
965
|
viewBox="0 0 24 24"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../utils.js';
|
|
3
|
+
import { DEV } from 'esm-env';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
type?: string;
|
|
@@ -14,6 +15,14 @@
|
|
|
14
15
|
/** Input mode hint for mobile keyboards */
|
|
15
16
|
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
|
|
16
17
|
class?: string;
|
|
18
|
+
/** Accessible label for screen readers (alternative to visible label) */
|
|
19
|
+
'aria-label'?: string;
|
|
20
|
+
/** ID of element that labels this input */
|
|
21
|
+
'aria-labelledby'?: string;
|
|
22
|
+
/** ID of element that describes this input (e.g., hint or error message) */
|
|
23
|
+
'aria-describedby'?: string;
|
|
24
|
+
/** Whether the input has an error */
|
|
25
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
17
26
|
onValueChange?: (value: string) => void;
|
|
18
27
|
[key: string]: unknown;
|
|
19
28
|
}
|
|
@@ -30,10 +39,25 @@
|
|
|
30
39
|
autocomplete,
|
|
31
40
|
inputmode,
|
|
32
41
|
class: className,
|
|
42
|
+
'aria-label': ariaLabel,
|
|
43
|
+
'aria-labelledby': ariaLabelledby,
|
|
44
|
+
'aria-describedby': ariaDescribedby,
|
|
45
|
+
'aria-invalid': ariaInvalid,
|
|
33
46
|
onValueChange,
|
|
34
47
|
...restProps
|
|
35
48
|
}: Props = $props();
|
|
36
49
|
|
|
50
|
+
// Accessibility: warn if no label is associated in dev mode
|
|
51
|
+
if (DEV) {
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!ariaLabel && !ariaLabelledby && !id) {
|
|
54
|
+
console.warn(
|
|
55
|
+
'[Input] Missing accessible label. Provide aria-label, aria-labelledby, or an id with a matching <label for="...">.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
function handleInput(e: Event) {
|
|
38
62
|
const target = e.target as HTMLInputElement;
|
|
39
63
|
value = target.value;
|
|
@@ -52,8 +76,16 @@
|
|
|
52
76
|
{id}
|
|
53
77
|
{autocomplete}
|
|
54
78
|
{inputmode}
|
|
79
|
+
aria-label={ariaLabel}
|
|
80
|
+
aria-labelledby={ariaLabelledby}
|
|
81
|
+
aria-describedby={ariaDescribedby}
|
|
82
|
+
aria-invalid={ariaInvalid}
|
|
83
|
+
aria-required={required}
|
|
55
84
|
class={cn(
|
|
56
85
|
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
86
|
+
ariaInvalid === true || ariaInvalid === 'true'
|
|
87
|
+
? 'border-destructive focus-visible:ring-destructive'
|
|
88
|
+
: '',
|
|
57
89
|
className
|
|
58
90
|
)}
|
|
59
91
|
oninput={handleInput}
|
|
@@ -11,6 +11,14 @@ interface Props {
|
|
|
11
11
|
/** Input mode hint for mobile keyboards */
|
|
12
12
|
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
|
|
13
13
|
class?: string;
|
|
14
|
+
/** Accessible label for screen readers (alternative to visible label) */
|
|
15
|
+
'aria-label'?: string;
|
|
16
|
+
/** ID of element that labels this input */
|
|
17
|
+
'aria-labelledby'?: string;
|
|
18
|
+
/** ID of element that describes this input (e.g., hint or error message) */
|
|
19
|
+
'aria-describedby'?: string;
|
|
20
|
+
/** Whether the input has an error */
|
|
21
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
14
22
|
onValueChange?: (value: string) => void;
|
|
15
23
|
[key: string]: unknown;
|
|
16
24
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Text to announce to screen readers */
|
|
7
|
+
message?: string;
|
|
8
|
+
/** Interruption level - 'polite' waits for idle, 'assertive' interrupts immediately */
|
|
9
|
+
politeness?: 'polite' | 'assertive';
|
|
10
|
+
/** Whether to announce the entire region on updates (default: true) */
|
|
11
|
+
atomic?: boolean;
|
|
12
|
+
/** Whether to hide the region visually (default: true) */
|
|
13
|
+
visuallyHidden?: boolean;
|
|
14
|
+
/** Additional class names */
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Child content to render in the live region */
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
message,
|
|
22
|
+
politeness = 'polite',
|
|
23
|
+
atomic = true,
|
|
24
|
+
visuallyHidden = true,
|
|
25
|
+
class: className,
|
|
26
|
+
children,
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!--
|
|
31
|
+
LiveRegion announces dynamic content changes to screen readers.
|
|
32
|
+
|
|
33
|
+
Use this component when content updates need to be communicated to assistive technology users,
|
|
34
|
+
such as form validation messages, status updates, or notifications.
|
|
35
|
+
|
|
36
|
+
@example
|
|
37
|
+
<LiveRegion message={errorMessage} politeness="assertive" />
|
|
38
|
+
|
|
39
|
+
@example with children
|
|
40
|
+
<LiveRegion politeness="polite">
|
|
41
|
+
<span>Status: {status}</span>
|
|
42
|
+
</LiveRegion>
|
|
43
|
+
|
|
44
|
+
@example visible
|
|
45
|
+
<LiveRegion visuallyHidden={false} message="Loading..." />
|
|
46
|
+
-->
|
|
47
|
+
<div
|
|
48
|
+
role="status"
|
|
49
|
+
aria-live={politeness}
|
|
50
|
+
aria-atomic={atomic}
|
|
51
|
+
class={cn(
|
|
52
|
+
visuallyHidden && 'sr-only absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0',
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{#if message}
|
|
57
|
+
{message}
|
|
58
|
+
{:else if children}
|
|
59
|
+
{@render children()}
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Text to announce to screen readers */
|
|
4
|
+
message?: string;
|
|
5
|
+
/** Interruption level - 'polite' waits for idle, 'assertive' interrupts immediately */
|
|
6
|
+
politeness?: 'polite' | 'assertive';
|
|
7
|
+
/** Whether to announce the entire region on updates (default: true) */
|
|
8
|
+
atomic?: boolean;
|
|
9
|
+
/** Whether to hide the region visually (default: true) */
|
|
10
|
+
visuallyHidden?: boolean;
|
|
11
|
+
/** Additional class names */
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Child content to render in the live region */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const LiveRegion: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type LiveRegion = ReturnType<typeof LiveRegion>;
|
|
18
|
+
export default LiveRegion;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import Spinner from './Spinner.svelte';
|
|
4
|
+
import LoadingLogo from './LoadingLogo.svelte';
|
|
5
|
+
import ProgressBar from './ProgressBar.svelte';
|
|
6
|
+
import Skeleton from './Skeleton.svelte';
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Whether loading is active */
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
/** Which indicator to show */
|
|
13
|
+
variant?: 'spinner' | 'logo' | 'progress' | 'skeleton';
|
|
14
|
+
/** Positioning mode */
|
|
15
|
+
mode?: 'fullscreen' | 'overlay' | 'inline';
|
|
16
|
+
/** Indicator size */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
18
|
+
/** Loading message text */
|
|
19
|
+
message?: string;
|
|
20
|
+
/** Progress value for progress variant */
|
|
21
|
+
progress?: number;
|
|
22
|
+
/** Backdrop blur (overlay/fullscreen) */
|
|
23
|
+
blur?: boolean;
|
|
24
|
+
/** Fully opaque background */
|
|
25
|
+
opaque?: boolean;
|
|
26
|
+
/** Delay in ms before showing */
|
|
27
|
+
delay?: number;
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
class?: string;
|
|
30
|
+
/** Content behind overlay */
|
|
31
|
+
children?: Snippet;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const logoSizeMap = { sm: 32, md: 40, lg: 56, xl: 80 } as const;
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
loading = true,
|
|
39
|
+
variant = 'spinner',
|
|
40
|
+
mode = 'overlay',
|
|
41
|
+
size = 'md',
|
|
42
|
+
message,
|
|
43
|
+
progress,
|
|
44
|
+
blur = true,
|
|
45
|
+
opaque = false,
|
|
46
|
+
delay = 0,
|
|
47
|
+
class: className,
|
|
48
|
+
children,
|
|
49
|
+
...restProps
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let visible = $state(false);
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (!loading) {
|
|
56
|
+
visible = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (delay === 0) {
|
|
61
|
+
visible = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
visible = true;
|
|
67
|
+
}, delay);
|
|
68
|
+
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const bgClass = $derived(
|
|
73
|
+
opaque ? 'bg-background' : mode === 'fullscreen' ? 'bg-background/80' : 'bg-background/60'
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const positionClass = $derived(
|
|
77
|
+
mode === 'fullscreen' ? 'fixed inset-0 z-50' : 'absolute inset-0 z-10'
|
|
78
|
+
);
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{#if mode === 'inline'}
|
|
82
|
+
{#if visible}
|
|
83
|
+
<div
|
|
84
|
+
role="status"
|
|
85
|
+
aria-live="polite"
|
|
86
|
+
class={cn('inline-flex items-center gap-2', className)}
|
|
87
|
+
{...restProps}
|
|
88
|
+
>
|
|
89
|
+
{#if variant === 'spinner'}
|
|
90
|
+
<Spinner {size} />
|
|
91
|
+
{:else if variant === 'logo'}
|
|
92
|
+
<LoadingLogo loading width={logoSizeMap[size]} height={logoSizeMap[size]} />
|
|
93
|
+
{:else if variant === 'progress'}
|
|
94
|
+
<ProgressBar value={progress} class="w-48 max-w-full" />
|
|
95
|
+
{:else if variant === 'skeleton'}
|
|
96
|
+
<div class="w-full space-y-3">
|
|
97
|
+
<Skeleton variant="title" />
|
|
98
|
+
<Skeleton variant="text" />
|
|
99
|
+
<Skeleton variant="text" />
|
|
100
|
+
<Skeleton variant="text" class="w-2/3" />
|
|
101
|
+
</div>
|
|
102
|
+
{/if}
|
|
103
|
+
{#if message}
|
|
104
|
+
<p class="mt-2 text-sm text-muted-foreground animate-pulse-subtle">{message}</p>
|
|
105
|
+
{:else}
|
|
106
|
+
<span class="sr-only">Loading</span>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
110
|
+
{:else}
|
|
111
|
+
{#if children}
|
|
112
|
+
{@render children()}
|
|
113
|
+
{/if}
|
|
114
|
+
{#if visible}
|
|
115
|
+
<div
|
|
116
|
+
role="status"
|
|
117
|
+
aria-live="polite"
|
|
118
|
+
class={cn(
|
|
119
|
+
positionClass,
|
|
120
|
+
bgClass,
|
|
121
|
+
blur && 'backdrop-blur-sm',
|
|
122
|
+
'flex flex-col items-center justify-center',
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
{...restProps}
|
|
126
|
+
>
|
|
127
|
+
{#if variant === 'spinner'}
|
|
128
|
+
<Spinner {size} />
|
|
129
|
+
{:else if variant === 'logo'}
|
|
130
|
+
<LoadingLogo loading width={logoSizeMap[size]} height={logoSizeMap[size]} />
|
|
131
|
+
{:else if variant === 'progress'}
|
|
132
|
+
<ProgressBar value={progress} class="w-48 max-w-full" />
|
|
133
|
+
{:else if variant === 'skeleton'}
|
|
134
|
+
<div class="w-full space-y-3">
|
|
135
|
+
<Skeleton variant="title" />
|
|
136
|
+
<Skeleton variant="text" />
|
|
137
|
+
<Skeleton variant="text" />
|
|
138
|
+
<Skeleton variant="text" class="w-2/3" />
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
{#if message}
|
|
142
|
+
<p class="mt-2 text-sm text-muted-foreground animate-pulse-subtle">{message}</p>
|
|
143
|
+
{:else}
|
|
144
|
+
<span class="sr-only">Loading</span>
|
|
145
|
+
{/if}
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
{/if}
|