@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.
Files changed (102) hide show
  1. package/dist/__tests__/a11y/Alert.a11y.test.js +84 -0
  2. package/dist/__tests__/a11y/AlertDialog.a11y.test.js +126 -0
  3. package/dist/__tests__/a11y/Button.a11y.test.js +66 -0
  4. package/dist/__tests__/a11y/Card.a11y.test.js +77 -0
  5. package/dist/__tests__/a11y/Combobox.a11y.test.js +209 -0
  6. package/dist/__tests__/a11y/Dialog.a11y.test.js +103 -0
  7. package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +104 -0
  8. package/dist/__tests__/a11y/FileUpload.a11y.test.js +158 -0
  9. package/dist/__tests__/a11y/Icon.a11y.test.js +119 -0
  10. package/dist/__tests__/a11y/Input.a11y.test.js +96 -0
  11. package/dist/__tests__/a11y/LiveRegion.a11y.test.js +102 -0
  12. package/dist/__tests__/a11y/Switch.a11y.test.js +95 -0
  13. package/dist/__tests__/a11y/Tabs.a11y.test.js +97 -0
  14. package/dist/__tests__/a11y/Textarea.a11y.test.js +111 -0
  15. package/dist/__tests__/setup.js +6 -0
  16. package/dist/lib/components/Alert.svelte +1 -1
  17. package/dist/lib/components/AlertDialog.svelte +1 -1
  18. package/dist/lib/components/AlertDialog.svelte.d.ts +2 -2
  19. package/dist/lib/components/Badge.svelte +49 -12
  20. package/dist/lib/components/Badge.svelte.d.ts +9 -0
  21. package/dist/lib/components/Button.svelte +14 -2
  22. package/dist/lib/components/CardTitle.svelte +9 -3
  23. package/dist/lib/components/CardTitle.svelte.d.ts +2 -0
  24. package/dist/lib/components/Combobox.svelte +19 -2
  25. package/dist/lib/components/DataPanelHeader.svelte +4 -4
  26. package/dist/lib/components/DataPanelTab.svelte +1 -1
  27. package/dist/lib/components/DataTable.svelte +7 -7
  28. package/dist/lib/components/DataTable.svelte.d.ts +5 -5
  29. package/dist/lib/components/DateTimePicker.svelte +7 -4
  30. package/dist/lib/components/Dialog.svelte +6 -6
  31. package/dist/lib/components/DropdownMenu.svelte +14 -10
  32. package/dist/lib/components/FileUpload.svelte +5 -10
  33. package/dist/lib/components/FormField.svelte +7 -1
  34. package/dist/lib/components/HeaderSearch.svelte +9 -9
  35. package/dist/lib/components/Icon.svelte +5 -5
  36. package/dist/lib/components/Input.svelte +32 -0
  37. package/dist/lib/components/Input.svelte.d.ts +8 -0
  38. package/dist/lib/components/LiveRegion.svelte +61 -0
  39. package/dist/lib/components/LiveRegion.svelte.d.ts +18 -0
  40. package/dist/lib/components/LoadingOverlay.svelte +148 -0
  41. package/dist/lib/components/LoadingOverlay.svelte.d.ts +29 -0
  42. package/dist/lib/components/LoadingPage.svelte +86 -0
  43. package/dist/lib/components/LoadingPage.svelte.d.ts +20 -0
  44. package/dist/lib/components/LoadingSection.svelte +54 -0
  45. package/dist/lib/components/LoadingSection.svelte.d.ts +25 -0
  46. package/dist/lib/components/MultiSelect.svelte +23 -2
  47. package/dist/lib/components/NumberInput.svelte +4 -4
  48. package/dist/lib/components/OTPInput.svelte +14 -0
  49. package/dist/lib/components/PasswordInput.svelte +14 -1
  50. package/dist/lib/components/PasswordInput.svelte.d.ts +4 -0
  51. package/dist/lib/components/ProgressBar.svelte +91 -0
  52. package/dist/lib/components/ProgressBar.svelte.d.ts +18 -0
  53. package/dist/lib/components/RadioGroup.svelte +5 -5
  54. package/dist/lib/components/Select.svelte +13 -1
  55. package/dist/lib/components/Signature.svelte +5 -5
  56. package/dist/lib/components/Skeleton.svelte +13 -3
  57. package/dist/lib/components/Skeleton.svelte.d.ts +2 -0
  58. package/dist/lib/components/SkipLink.svelte +67 -0
  59. package/dist/lib/components/SkipLink.svelte.d.ts +25 -0
  60. package/dist/lib/components/Slider.svelte +6 -2
  61. package/dist/lib/components/Switch.svelte +8 -0
  62. package/dist/lib/components/Switch.svelte.d.ts +4 -0
  63. package/dist/lib/components/Tabs.svelte +15 -5
  64. package/dist/lib/components/Tabs.svelte.d.ts +6 -0
  65. package/dist/lib/components/Textarea.svelte +19 -0
  66. package/dist/lib/components/Textarea.svelte.d.ts +8 -0
  67. package/dist/lib/components/Toast.svelte +1 -1
  68. package/dist/lib/components/layout/AuthLayout.svelte +1 -1
  69. package/dist/lib/components/layout/DashboardLayout.svelte +1 -1
  70. package/dist/lib/components/layout/Footer.svelte +1 -1
  71. package/dist/lib/components/layout/PublicLayout.svelte +2 -2
  72. package/dist/lib/components/layout/QuickLinks.svelte +3 -3
  73. package/dist/lib/components/layout/Sidebar.svelte +1 -1
  74. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +1 -2
  75. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +20 -21
  76. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +2 -2
  77. package/dist/lib/components/notifications/NotificationFilters.svelte +1 -1
  78. package/dist/lib/components/notifications/NotificationItem.svelte +2 -1
  79. package/dist/lib/composables/index.d.ts +8 -0
  80. package/dist/lib/composables/index.js +9 -0
  81. package/dist/lib/composables/useAnnounce.svelte.d.ts +65 -0
  82. package/dist/lib/composables/useAnnounce.svelte.js +76 -0
  83. package/dist/lib/composables/useArrowNavigation.svelte.d.ts +42 -0
  84. package/dist/lib/composables/useArrowNavigation.svelte.js +138 -0
  85. package/dist/lib/composables/useClickOutside.svelte.d.ts +88 -0
  86. package/dist/lib/composables/useClickOutside.svelte.js +100 -0
  87. package/dist/lib/composables/useEscapeKey.svelte.d.ts +48 -0
  88. package/dist/lib/composables/useEscapeKey.svelte.js +50 -0
  89. package/dist/lib/composables/useFocusRef.svelte.d.ts +46 -0
  90. package/dist/lib/composables/useFocusRef.svelte.js +92 -0
  91. package/dist/lib/composables/useFocusTrap.svelte.d.ts +42 -0
  92. package/dist/lib/composables/useFocusTrap.svelte.js +136 -0
  93. package/dist/lib/composables/useReducedMotion.svelte.d.ts +47 -0
  94. package/dist/lib/composables/useReducedMotion.svelte.js +67 -0
  95. package/dist/lib/composables/useRovingTabindex.svelte.d.ts +54 -0
  96. package/dist/lib/composables/useRovingTabindex.svelte.js +128 -0
  97. package/dist/lib/index.d.ts +7 -1
  98. package/dist/lib/index.js +11 -1
  99. package/dist/lib/stores/theme.svelte.d.ts +1 -1
  100. package/dist/lib/utils.d.ts +4 -2
  101. package/dist/lib/utils.js +5 -11
  102. 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 transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
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 transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
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 transition-colors 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',
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 transition-colors 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',
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 transition-colors 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',
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 transition-colors 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',
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-10 w-10 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 rounded-r-md disabled:pointer-events-none disabled:opacity-50"
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 { generateId, computeDescribedBy } from '../utils/form.js';
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 randomSuffix = generateId('radio-group').split('-').pop();
56
- const componentName = $derived(name || `radio-group-${randomSuffix}`);
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 transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default';
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 transition-colors',
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 transition-colors 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',
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 transition-colors hover:border-primary',
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
- ${backgroundColor === 'white' ? `<rect width="${width}" height="${height}" fill="white"/>` : ''}
388
- ${paths}
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: 'animate-pulse rounded-md bg-muted',
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 { variant = 'default', class: className, ...restProps }: Props = $props();
32
+ let {
33
+ variant = 'default',
34
+ animation = 'pulse',
35
+ class: className,
36
+ ...restProps
37
+ }: Props = $props();
31
38
 
32
- const classes = $derived(cn(skeletonVariants({ variant }), className));
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>;