@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Whether loading is active */
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
/** Which indicator to show */
|
|
6
|
+
variant?: 'spinner' | 'logo' | 'progress' | 'skeleton';
|
|
7
|
+
/** Positioning mode */
|
|
8
|
+
mode?: 'fullscreen' | 'overlay' | 'inline';
|
|
9
|
+
/** Indicator size */
|
|
10
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
/** Loading message text */
|
|
12
|
+
message?: string;
|
|
13
|
+
/** Progress value for progress variant */
|
|
14
|
+
progress?: number;
|
|
15
|
+
/** Backdrop blur (overlay/fullscreen) */
|
|
16
|
+
blur?: boolean;
|
|
17
|
+
/** Fully opaque background */
|
|
18
|
+
opaque?: boolean;
|
|
19
|
+
/** Delay in ms before showing */
|
|
20
|
+
delay?: number;
|
|
21
|
+
/** Additional CSS classes */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Content behind overlay */
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
declare const LoadingOverlay: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type LoadingOverlay = ReturnType<typeof LoadingOverlay>;
|
|
29
|
+
export default LoadingOverlay;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import LoadingLogo from './LoadingLogo.svelte';
|
|
4
|
+
import Spinner from './Spinner.svelte';
|
|
5
|
+
import ProgressBar from './ProgressBar.svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Whether loading is active */
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
/** Loading message text */
|
|
11
|
+
message?: string;
|
|
12
|
+
/** Determinate progress value */
|
|
13
|
+
progress?: number;
|
|
14
|
+
/** Show branded LoadingLogo */
|
|
15
|
+
showLogo?: boolean;
|
|
16
|
+
/** Logo color variant */
|
|
17
|
+
logoVariant?: 'light' | 'dark';
|
|
18
|
+
/** Delay before showing (prevents flash) */
|
|
19
|
+
delay?: number;
|
|
20
|
+
/** Additional CSS classes */
|
|
21
|
+
class?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
loading = true,
|
|
27
|
+
message,
|
|
28
|
+
progress,
|
|
29
|
+
showLogo = true,
|
|
30
|
+
logoVariant = 'dark',
|
|
31
|
+
delay = 200,
|
|
32
|
+
class: className,
|
|
33
|
+
...restProps
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
let visible = $state(false);
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (!loading) {
|
|
40
|
+
visible = false;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (delay === 0) {
|
|
45
|
+
visible = true;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
visible = true;
|
|
51
|
+
}, delay);
|
|
52
|
+
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
{#if visible}
|
|
58
|
+
<div
|
|
59
|
+
role="status"
|
|
60
|
+
aria-live="polite"
|
|
61
|
+
class={cn(
|
|
62
|
+
'fixed inset-0 z-50 flex flex-col items-center justify-center bg-background',
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...restProps}
|
|
66
|
+
>
|
|
67
|
+
{#if showLogo}
|
|
68
|
+
<LoadingLogo loading width={64} height={64} variant={logoVariant} />
|
|
69
|
+
{:else}
|
|
70
|
+
<Spinner size="xl" />
|
|
71
|
+
{/if}
|
|
72
|
+
|
|
73
|
+
{#if progress !== undefined}
|
|
74
|
+
<ProgressBar value={progress} class="mt-6 w-48 max-w-full" />
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
{#if message}
|
|
78
|
+
<p class="mt-4 text-sm text-muted-foreground animate-pulse-subtle">{message}</p>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
<span class="sr-only">
|
|
82
|
+
{message || 'Loading'}
|
|
83
|
+
{#if progress !== undefined}, {Math.round(progress)}% complete{/if}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Whether loading is active */
|
|
3
|
+
loading?: boolean;
|
|
4
|
+
/** Loading message text */
|
|
5
|
+
message?: string;
|
|
6
|
+
/** Determinate progress value */
|
|
7
|
+
progress?: number;
|
|
8
|
+
/** Show branded LoadingLogo */
|
|
9
|
+
showLogo?: boolean;
|
|
10
|
+
/** Logo color variant */
|
|
11
|
+
logoVariant?: 'light' | 'dark';
|
|
12
|
+
/** Delay before showing (prevents flash) */
|
|
13
|
+
delay?: number;
|
|
14
|
+
/** Additional CSS classes */
|
|
15
|
+
class?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
declare const LoadingPage: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type LoadingPage = ReturnType<typeof LoadingPage>;
|
|
20
|
+
export default LoadingPage;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import LoadingOverlay from './LoadingOverlay.svelte';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Whether loading is active */
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
/** Indicator type */
|
|
10
|
+
variant?: 'spinner' | 'skeleton' | 'logo' | 'progress';
|
|
11
|
+
/** Loading message */
|
|
12
|
+
message?: string;
|
|
13
|
+
/** Progress value */
|
|
14
|
+
progress?: number;
|
|
15
|
+
/** Minimum height to preserve layout */
|
|
16
|
+
minHeight?: string;
|
|
17
|
+
/** Backdrop blur */
|
|
18
|
+
blur?: boolean;
|
|
19
|
+
/** Delay before showing */
|
|
20
|
+
delay?: number;
|
|
21
|
+
/** Additional CSS classes */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Content being loaded */
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
loading = false,
|
|
30
|
+
variant = 'spinner',
|
|
31
|
+
message,
|
|
32
|
+
progress,
|
|
33
|
+
minHeight = '12rem',
|
|
34
|
+
blur = true,
|
|
35
|
+
delay = 0,
|
|
36
|
+
class: className,
|
|
37
|
+
children,
|
|
38
|
+
...restProps
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
class={cn('relative', className)}
|
|
44
|
+
style="min-height: {minHeight}"
|
|
45
|
+
aria-busy={loading}
|
|
46
|
+
{...restProps}
|
|
47
|
+
>
|
|
48
|
+
{#if children}
|
|
49
|
+
{@render children()}
|
|
50
|
+
{/if}
|
|
51
|
+
{#if loading}
|
|
52
|
+
<LoadingOverlay mode="overlay" {variant} {message} {progress} {blur} {delay} />
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Whether loading is active */
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
/** Indicator type */
|
|
6
|
+
variant?: 'spinner' | 'skeleton' | 'logo' | 'progress';
|
|
7
|
+
/** Loading message */
|
|
8
|
+
message?: string;
|
|
9
|
+
/** Progress value */
|
|
10
|
+
progress?: number;
|
|
11
|
+
/** Minimum height to preserve layout */
|
|
12
|
+
minHeight?: string;
|
|
13
|
+
/** Backdrop blur */
|
|
14
|
+
blur?: boolean;
|
|
15
|
+
/** Delay before showing */
|
|
16
|
+
delay?: number;
|
|
17
|
+
/** Additional CSS classes */
|
|
18
|
+
class?: string;
|
|
19
|
+
/** Content being loaded */
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare const LoadingSection: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type LoadingSection = ReturnType<typeof LoadingSection>;
|
|
25
|
+
export default LoadingSection;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { generateId, computeDescribedBy } from '../utils/form.js';
|
|
5
5
|
import Spinner from './Spinner.svelte';
|
|
6
6
|
import Label from './Label.svelte';
|
|
7
|
+
import LiveRegion from './LiveRegion.svelte';
|
|
7
8
|
|
|
8
9
|
export interface MultiSelectOption {
|
|
9
10
|
value: string;
|
|
@@ -138,6 +139,23 @@
|
|
|
138
139
|
|
|
139
140
|
// Determine if we need a container wrapper
|
|
140
141
|
const hasWrapper = $derived(!!label || !!hint || !!error);
|
|
142
|
+
|
|
143
|
+
// Generate announcement message for screen readers
|
|
144
|
+
const resultsAnnouncement = $derived.by(() => {
|
|
145
|
+
if (loading) return 'Loading options...';
|
|
146
|
+
if (!open) return '';
|
|
147
|
+
if (filteredOptions.length === 0) return emptyMessage;
|
|
148
|
+
const count = filteredOptions.length;
|
|
149
|
+
const selectedCount = value.length;
|
|
150
|
+
let message = `${count} ${count === 1 ? 'option' : 'options'} available`;
|
|
151
|
+
if (selectedCount > 0) {
|
|
152
|
+
message += `, ${selectedCount} selected`;
|
|
153
|
+
}
|
|
154
|
+
if (maxReached) {
|
|
155
|
+
message += `, maximum selections reached`;
|
|
156
|
+
}
|
|
157
|
+
return message;
|
|
158
|
+
});
|
|
141
159
|
</script>
|
|
142
160
|
|
|
143
161
|
{#if hasWrapper}
|
|
@@ -241,7 +259,7 @@
|
|
|
241
259
|
value={option.value}
|
|
242
260
|
label={option.label}
|
|
243
261
|
disabled={option.disabled || isDisabledByMax}
|
|
244
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none
|
|
262
|
+
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
245
263
|
>
|
|
246
264
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
247
265
|
{#if isSelected}
|
|
@@ -374,7 +392,7 @@
|
|
|
374
392
|
value={option.value}
|
|
375
393
|
label={option.label}
|
|
376
394
|
disabled={option.disabled || isDisabledByMax}
|
|
377
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none
|
|
395
|
+
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
378
396
|
>
|
|
379
397
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
380
398
|
{#if isSelected}
|
|
@@ -405,3 +423,6 @@
|
|
|
405
423
|
</ComboboxPrimitive.Root>
|
|
406
424
|
</div>
|
|
407
425
|
{/if}
|
|
426
|
+
|
|
427
|
+
<!-- Live region for screen reader announcements -->
|
|
428
|
+
<LiveRegion message={resultsAnnouncement} politeness="polite" />
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
onclick={decrement}
|
|
160
160
|
disabled={disabled || !canDecrement}
|
|
161
161
|
class={cn(
|
|
162
|
-
'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium
|
|
162
|
+
'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium 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 disabled:opacity-50',
|
|
163
163
|
error && 'border-destructive'
|
|
164
164
|
)}
|
|
165
165
|
aria-label="Decrease value"
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
onclick={increment}
|
|
209
209
|
disabled={disabled || !canIncrement}
|
|
210
210
|
class={cn(
|
|
211
|
-
'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium
|
|
211
|
+
'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium 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 disabled:opacity-50',
|
|
212
212
|
error && 'border-destructive'
|
|
213
213
|
)}
|
|
214
214
|
aria-label="Increase value"
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
onclick={decrement}
|
|
252
252
|
disabled={disabled || !canDecrement}
|
|
253
253
|
class={cn(
|
|
254
|
-
'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium
|
|
254
|
+
'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium 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 disabled:opacity-50',
|
|
255
255
|
error && 'border-destructive'
|
|
256
256
|
)}
|
|
257
257
|
aria-label="Decrease value"
|
|
@@ -300,7 +300,7 @@
|
|
|
300
300
|
onclick={increment}
|
|
301
301
|
disabled={disabled || !canIncrement}
|
|
302
302
|
class={cn(
|
|
303
|
-
'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium
|
|
303
|
+
'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium 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 disabled:opacity-50',
|
|
304
304
|
error && 'border-destructive'
|
|
305
305
|
)}
|
|
306
306
|
aria-label="Increase value"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cn } from '../utils.js';
|
|
3
3
|
import { generateId, computeDescribedBy } from '../utils/form.js';
|
|
4
4
|
import Label from './Label.svelte';
|
|
5
|
+
import LiveRegion from './LiveRegion.svelte';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
/** Current OTP value */
|
|
@@ -83,6 +84,9 @@
|
|
|
83
84
|
// Track if we've already fired onComplete for the current complete value
|
|
84
85
|
let lastCompletedValue = $state<string | null>(null);
|
|
85
86
|
|
|
87
|
+
// Screen reader announcement for paste feedback
|
|
88
|
+
let announcement = $state('');
|
|
89
|
+
|
|
86
90
|
function updateValue(newDigits: string[]) {
|
|
87
91
|
// Filter out empty strings and join to get actual value
|
|
88
92
|
const newValue = newDigits
|
|
@@ -188,6 +192,13 @@
|
|
|
188
192
|
// Focus the input after the last pasted character
|
|
189
193
|
const focusIndex = Math.min(chars.length, length - 1);
|
|
190
194
|
inputs[focusIndex]?.focus();
|
|
195
|
+
|
|
196
|
+
// Announce paste result to screen readers
|
|
197
|
+
if (chars.length === length) {
|
|
198
|
+
announcement = `Pasted ${chars.length} digits. Code complete.`;
|
|
199
|
+
} else {
|
|
200
|
+
announcement = `Pasted ${chars.length} of ${length} digits.`;
|
|
201
|
+
}
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
|
|
@@ -301,3 +312,6 @@
|
|
|
301
312
|
<input type="hidden" {name} {value} />
|
|
302
313
|
{/if}
|
|
303
314
|
{/if}
|
|
315
|
+
|
|
316
|
+
<!-- Live region for screen reader announcements -->
|
|
317
|
+
<LiveRegion message={announcement} politeness="polite" />
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
showLabel?: string;
|
|
18
18
|
/** Label for the toggle button when password is visible */
|
|
19
19
|
hideLabel?: string;
|
|
20
|
+
/** Screen reader announcement when password becomes visible */
|
|
21
|
+
visibleAnnouncement?: string;
|
|
22
|
+
/** Screen reader announcement when password becomes hidden */
|
|
23
|
+
hiddenAnnouncement?: string;
|
|
20
24
|
[key: string]: unknown;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -33,10 +37,13 @@
|
|
|
33
37
|
onValueChange,
|
|
34
38
|
showLabel = 'Show password',
|
|
35
39
|
hideLabel = 'Hide password',
|
|
40
|
+
visibleAnnouncement = 'Password is now visible',
|
|
41
|
+
hiddenAnnouncement = 'Password is now hidden',
|
|
36
42
|
...restProps
|
|
37
43
|
}: Props = $props();
|
|
38
44
|
|
|
39
45
|
let showPassword = $state(false);
|
|
46
|
+
let announcement = $state('');
|
|
40
47
|
|
|
41
48
|
function handleInput(e: Event) {
|
|
42
49
|
const target = e.target as HTMLInputElement;
|
|
@@ -46,6 +53,7 @@
|
|
|
46
53
|
|
|
47
54
|
function toggleVisibility() {
|
|
48
55
|
showPassword = !showPassword;
|
|
56
|
+
announcement = showPassword ? visibleAnnouncement : hiddenAnnouncement;
|
|
49
57
|
}
|
|
50
58
|
</script>
|
|
51
59
|
|
|
@@ -69,11 +77,16 @@
|
|
|
69
77
|
/>
|
|
70
78
|
<button
|
|
71
79
|
type="button"
|
|
72
|
-
class="absolute right-0 top-0 h-
|
|
80
|
+
class="absolute right-0 top-0 h-11 w-11 -mr-0.5 -mt-0.5 flex items-center justify-center text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded-r-md disabled:pointer-events-none disabled:opacity-50"
|
|
73
81
|
onclick={toggleVisibility}
|
|
74
82
|
{disabled}
|
|
75
83
|
aria-label={showPassword ? hideLabel : showLabel}
|
|
84
|
+
aria-pressed={showPassword}
|
|
76
85
|
>
|
|
77
86
|
<Icon name={showPassword ? 'eye-off' : 'eye'} size={18} />
|
|
78
87
|
</button>
|
|
88
|
+
<!-- Screen reader live region for announcing visibility changes -->
|
|
89
|
+
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
|
|
90
|
+
{announcement}
|
|
91
|
+
</div>
|
|
79
92
|
</div>
|
|
@@ -13,6 +13,10 @@ interface Props {
|
|
|
13
13
|
showLabel?: string;
|
|
14
14
|
/** Label for the toggle button when password is visible */
|
|
15
15
|
hideLabel?: string;
|
|
16
|
+
/** Screen reader announcement when password becomes visible */
|
|
17
|
+
visibleAnnouncement?: string;
|
|
18
|
+
/** Screen reader announcement when password becomes hidden */
|
|
19
|
+
hiddenAnnouncement?: string;
|
|
16
20
|
[key: string]: unknown;
|
|
17
21
|
}
|
|
18
22
|
declare const PasswordInput: import("svelte").Component<Props, {}, "value">;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
|
|
6
|
+
const progressBarVariants = tv({
|
|
7
|
+
base: 'w-full overflow-hidden rounded-full bg-muted',
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: 'h-1',
|
|
11
|
+
md: 'h-2',
|
|
12
|
+
lg: 'h-3',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: 'md',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const progressFillVariants = tv({
|
|
21
|
+
base: 'h-full rounded-full transition-all duration-300',
|
|
22
|
+
variants: {
|
|
23
|
+
variant: {
|
|
24
|
+
default: 'bg-primary',
|
|
25
|
+
success: 'bg-success',
|
|
26
|
+
warning: 'bg-warning',
|
|
27
|
+
destructive: 'bg-destructive',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: 'default',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
type ProgressBarSizeVariants = VariantProps<typeof progressBarVariants>;
|
|
36
|
+
type ProgressBarFillVariants = VariantProps<typeof progressFillVariants>;
|
|
37
|
+
|
|
38
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
39
|
+
/** Progress value 0-100. Omit for indeterminate */
|
|
40
|
+
value?: number;
|
|
41
|
+
/** Maximum value */
|
|
42
|
+
max?: number;
|
|
43
|
+
/** Bar height */
|
|
44
|
+
size?: ProgressBarSizeVariants['size'];
|
|
45
|
+
/** Fill color variant */
|
|
46
|
+
variant?: ProgressBarFillVariants['variant'];
|
|
47
|
+
/** Accessible label */
|
|
48
|
+
label?: string;
|
|
49
|
+
/** Show percentage text */
|
|
50
|
+
showValue?: boolean;
|
|
51
|
+
class?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
value,
|
|
56
|
+
max = 100,
|
|
57
|
+
size = 'md',
|
|
58
|
+
variant = 'default',
|
|
59
|
+
label = 'Progress',
|
|
60
|
+
showValue = false,
|
|
61
|
+
class: className,
|
|
62
|
+
...restProps
|
|
63
|
+
}: Props = $props();
|
|
64
|
+
|
|
65
|
+
const isIndeterminate = $derived(value === undefined);
|
|
66
|
+
const clampedValue = $derived(isIndeterminate ? 0 : Math.min(Math.max(0, value!), max));
|
|
67
|
+
const percentage = $derived(isIndeterminate ? 0 : Math.round((clampedValue / max) * 100));
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
role="progressbar"
|
|
72
|
+
aria-label={label}
|
|
73
|
+
aria-valuenow={isIndeterminate ? undefined : clampedValue}
|
|
74
|
+
aria-valuemin={0}
|
|
75
|
+
aria-valuemax={max}
|
|
76
|
+
class={cn('flex items-center gap-2', className)}
|
|
77
|
+
{...restProps}
|
|
78
|
+
>
|
|
79
|
+
<div class={cn(progressBarVariants({ size }))}>
|
|
80
|
+
<div
|
|
81
|
+
class={cn(
|
|
82
|
+
progressFillVariants({ variant }),
|
|
83
|
+
isIndeterminate && 'w-1/4 animate-progress-indeterminate'
|
|
84
|
+
)}
|
|
85
|
+
style={isIndeterminate ? undefined : `width: ${percentage}%`}
|
|
86
|
+
></div>
|
|
87
|
+
</div>
|
|
88
|
+
{#if showValue && !isIndeterminate}
|
|
89
|
+
<span class="text-sm tabular-nums text-muted-foreground">{percentage}%</span>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
declare const ProgressBar: import("svelte").Component<HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
/** Progress value 0-100. Omit for indeterminate */
|
|
4
|
+
value?: number;
|
|
5
|
+
/** Maximum value */
|
|
6
|
+
max?: number;
|
|
7
|
+
/** Bar height */
|
|
8
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
9
|
+
/** Fill color variant */
|
|
10
|
+
variant?: "default" | "destructive" | "warning" | "success" | undefined;
|
|
11
|
+
/** Accessible label */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Show percentage text */
|
|
14
|
+
showValue?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
}, {}, "">;
|
|
17
|
+
type ProgressBar = ReturnType<typeof ProgressBar>;
|
|
18
|
+
export default ProgressBar;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../utils.js';
|
|
4
|
-
import {
|
|
3
|
+
import { cn, useId } from '../utils.js';
|
|
4
|
+
import { computeDescribedBy } from '../utils/form.js';
|
|
5
5
|
import { validateNonEmptyArray } from '../validation.js';
|
|
6
6
|
import Label from './Label.svelte';
|
|
7
7
|
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
class: className,
|
|
52
52
|
}: Props = $props();
|
|
53
53
|
|
|
54
|
-
// Generate unique IDs for accessibility
|
|
55
|
-
const
|
|
56
|
-
const componentName = $derived(name ||
|
|
54
|
+
// Generate unique IDs for accessibility (sequential counter, SSR-safe)
|
|
55
|
+
const generatedId = useId('radio-group');
|
|
56
|
+
const componentName = $derived(name || generatedId);
|
|
57
57
|
const labelId = $derived(`${componentName}-label`);
|
|
58
58
|
const hintId = $derived(`${componentName}-hint`);
|
|
59
59
|
const errorId = $derived(`${componentName}-error`);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Select as SelectPrimitive } from 'bits-ui';
|
|
3
3
|
import { cn, useId } from '../utils.js';
|
|
4
4
|
import { validateNonEmptyArray } from '../validation.js';
|
|
5
|
+
import { DEV } from 'esm-env';
|
|
5
6
|
import Label from './Label.svelte';
|
|
6
7
|
|
|
7
8
|
export interface SelectOption {
|
|
@@ -76,6 +77,17 @@
|
|
|
76
77
|
validateNonEmptyArray(options, 'options', 'Select');
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
// Accessibility: warn if no label is associated in dev mode
|
|
81
|
+
if (DEV) {
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (!label && !id) {
|
|
84
|
+
console.warn(
|
|
85
|
+
'[Select] Missing accessible label. Provide a label prop or an id with a matching <label for="...">.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
function isGroup(item: SelectOption | SelectGroup): item is SelectGroup {
|
|
80
92
|
return 'options' in item;
|
|
81
93
|
}
|
|
@@ -131,7 +143,7 @@
|
|
|
131
143
|
|
|
132
144
|
// Common item classes
|
|
133
145
|
const itemClasses =
|
|
134
|
-
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none
|
|
146
|
+
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default';
|
|
135
147
|
</script>
|
|
136
148
|
|
|
137
149
|
{#snippet chevronIcon()}
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
labelText: 'text-sm font-medium leading-none',
|
|
29
29
|
modeToggle: 'inline-flex items-center rounded-lg bg-muted p-1',
|
|
30
30
|
modeButton:
|
|
31
|
-
'inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium
|
|
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
|
canvasContainer: 'relative w-full overflow-hidden rounded-lg border bg-background',
|
|
35
35
|
canvas: 'block w-full cursor-crosshair touch-none',
|
|
36
36
|
toolbar: 'flex flex-wrap items-center gap-2',
|
|
37
37
|
toolButton:
|
|
38
|
-
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm font-medium ring-offset-background
|
|
38
|
+
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm font-medium ring-offset-background 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 disabled:opacity-50',
|
|
39
39
|
toolGroup: 'flex items-center gap-1.5',
|
|
40
40
|
toolLabel: 'text-xs text-muted-foreground',
|
|
41
41
|
colorPicker:
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
'h-2 w-20 cursor-pointer appearance-none rounded-full bg-muted accent-primary',
|
|
45
45
|
typeInput: 'w-full',
|
|
46
46
|
fontPicker: 'flex flex-wrap gap-2',
|
|
47
|
-
fontButton: 'rounded-lg border-2 px-3 py-2 text-lg
|
|
47
|
+
fontButton: 'rounded-lg border-2 px-3 py-2 text-lg hover:border-primary',
|
|
48
48
|
fontButtonSelected: 'border-primary bg-primary/5',
|
|
49
49
|
fontButtonUnselected: 'border-input',
|
|
50
50
|
consent: 'flex items-start gap-2',
|
|
@@ -384,8 +384,8 @@
|
|
|
384
384
|
|
|
385
385
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
386
386
|
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
${backgroundColor === 'white' ? `<rect width="${width}" height="${height}" fill="white"/>` : ''}
|
|
388
|
+
${paths}
|
|
389
389
|
</svg>`;
|
|
390
390
|
}
|
|
391
391
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
5
|
|
|
6
6
|
const skeletonVariants = tv({
|
|
7
|
-
base: '
|
|
7
|
+
base: 'rounded-md bg-muted',
|
|
8
8
|
variants: {
|
|
9
9
|
variant: {
|
|
10
10
|
default: '',
|
|
@@ -24,12 +24,22 @@
|
|
|
24
24
|
|
|
25
25
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
26
26
|
variant?: SkeletonVariants['variant'];
|
|
27
|
+
/** Animation style for the skeleton */
|
|
28
|
+
animation?: 'pulse' | 'shimmer' | 'none';
|
|
27
29
|
class?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
let {
|
|
32
|
+
let {
|
|
33
|
+
variant = 'default',
|
|
34
|
+
animation = 'pulse',
|
|
35
|
+
class: className,
|
|
36
|
+
...restProps
|
|
37
|
+
}: Props = $props();
|
|
31
38
|
|
|
32
|
-
const
|
|
39
|
+
const animationClass = $derived(
|
|
40
|
+
animation === 'pulse' ? 'animate-pulse' : animation === 'shimmer' ? 'animate-shimmer' : ''
|
|
41
|
+
);
|
|
42
|
+
const classes = $derived(cn(skeletonVariants({ variant }), animationClass, className));
|
|
33
43
|
</script>
|
|
34
44
|
|
|
35
45
|
<div class={classes} {...restProps}></div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
declare const Skeleton: import("svelte").Component<HTMLAttributes<HTMLDivElement> & {
|
|
3
3
|
variant?: "default" | "button" | "text" | "title" | "avatar" | "card" | undefined;
|
|
4
|
+
/** Animation style for the skeleton */
|
|
5
|
+
animation?: "pulse" | "shimmer" | "none";
|
|
4
6
|
class?: string;
|
|
5
7
|
}, {}, "">;
|
|
6
8
|
type Skeleton = ReturnType<typeof Skeleton>;
|