@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,67 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ interface Props {
6
+ /**
7
+ * ID of the target element to skip to (without the # prefix).
8
+ * @example targetId="main-content"
9
+ */
10
+ targetId: string;
11
+ /**
12
+ * Custom class name for the skip link.
13
+ * Note: The component uses the `.skip-link` class from @classic-homes/theme-styles
14
+ * which handles the visually hidden to visible transition on focus.
15
+ */
16
+ class?: string;
17
+ /**
18
+ * Content of the skip link. Defaults to "Skip to main content".
19
+ */
20
+ children?: Snippet;
21
+ /**
22
+ * Additional attributes
23
+ */
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ let { targetId, class: className, children, ...restProps }: Props = $props();
28
+
29
+ function handleClick(e: MouseEvent) {
30
+ e.preventDefault();
31
+ const target = document.getElementById(targetId);
32
+ if (target) {
33
+ // Set tabIndex to make the target focusable if it isn't already
34
+ if (target.tabIndex === -1) {
35
+ target.setAttribute('tabindex', '-1');
36
+ }
37
+ target.focus();
38
+ // Scroll into view
39
+ target.scrollIntoView();
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <!--
45
+ SkipLink - Accessible skip navigation link
46
+
47
+ Provides keyboard users a way to skip repetitive navigation and jump directly
48
+ to the main content. The link is visually hidden until focused.
49
+
50
+ @see https://www.w3.org/WAI/WCAG21/Techniques/general/G1
51
+
52
+ @example
53
+ // Basic usage - place at the very beginning of your page/layout
54
+ <SkipLink targetId="main-content">Skip to main content</SkipLink>
55
+
56
+ // Then add the target ID to your main content
57
+ <main id="main-content" tabindex="-1">
58
+ ...
59
+ </main>
60
+ -->
61
+ <a href={`#${targetId}`} class={cn('skip-link', className)} onclick={handleClick} {...restProps}>
62
+ {#if children}
63
+ {@render children()}
64
+ {:else}
65
+ Skip to main content
66
+ {/if}
67
+ </a>
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ /**
4
+ * ID of the target element to skip to (without the # prefix).
5
+ * @example targetId="main-content"
6
+ */
7
+ targetId: string;
8
+ /**
9
+ * Custom class name for the skip link.
10
+ * Note: The component uses the `.skip-link` class from @classic-homes/theme-styles
11
+ * which handles the visually hidden to visible transition on focus.
12
+ */
13
+ class?: string;
14
+ /**
15
+ * Content of the skip link. Defaults to "Skip to main content".
16
+ */
17
+ children?: Snippet;
18
+ /**
19
+ * Additional attributes
20
+ */
21
+ [key: string]: unknown;
22
+ }
23
+ declare const SkipLink: import("svelte").Component<Props, {}, "">;
24
+ type SkipLink = ReturnType<typeof SkipLink>;
25
+ export default SkipLink;
@@ -143,11 +143,15 @@
143
143
  aria-describedby={describedBy}
144
144
  >
145
145
  <SliderPrimitive.Range class={styles.range()} />
146
- {#each value as _, i}
146
+ {#each value as thumbValue, i}
147
147
  <SliderPrimitive.Thumb
148
148
  index={i}
149
149
  class={styles.thumb()}
150
- aria-label={value.length > 1 ? `Thumb ${i + 1}` : label || 'Slider'}
150
+ aria-label={value.length > 1 ? `${label || 'Slider'} thumb ${i + 1}` : label || 'Slider'}
151
+ aria-valuenow={thumbValue}
152
+ aria-valuemin={min}
153
+ aria-valuemax={max}
154
+ aria-valuetext={formatValue(thumbValue)}
151
155
  />
152
156
  {/each}
153
157
  </SliderPrimitive.Root>
@@ -10,6 +10,10 @@
10
10
  value?: string;
11
11
  id?: string;
12
12
  class?: string;
13
+ /** Accessible label for screen readers */
14
+ 'aria-label'?: string;
15
+ /** ID of element that labels this switch */
16
+ 'aria-labelledby'?: string;
13
17
  onchange?: (checked: boolean) => void;
14
18
  }
15
19
 
@@ -21,6 +25,8 @@
21
25
  value,
22
26
  id,
23
27
  class: className,
28
+ 'aria-label': ariaLabel,
29
+ 'aria-labelledby': ariaLabelledby,
24
30
  onchange,
25
31
  ...restProps
26
32
  }: Props = $props();
@@ -43,6 +49,8 @@
43
49
  {name}
44
50
  {value}
45
51
  {id}
52
+ aria-label={ariaLabel}
53
+ aria-labelledby={ariaLabelledby}
46
54
  onCheckedChange={handleCheckedChange}
47
55
  class={cn(
48
56
  'peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
@@ -6,6 +6,10 @@ interface Props {
6
6
  value?: string;
7
7
  id?: string;
8
8
  class?: string;
9
+ /** Accessible label for screen readers */
10
+ 'aria-label'?: string;
11
+ /** ID of element that labels this switch */
12
+ 'aria-labelledby'?: string;
9
13
  onchange?: (checked: boolean) => void;
10
14
  }
11
15
  declare const Switch: import("svelte").Component<Props, {}, "checked">;
@@ -11,6 +11,12 @@
11
11
  value?: string;
12
12
  /** Unique ID for ARIA relationships */
13
13
  id?: string;
14
+ /**
15
+ * Accessible label for the tab list.
16
+ * Describes the purpose of the tabs to screen reader users.
17
+ * @example label="Account settings"
18
+ */
19
+ label: string;
14
20
  /** Additional classes for container */
15
21
  class?: string;
16
22
  /** Tab panel content */
@@ -25,11 +31,15 @@
25
31
  tabs,
26
32
  value = $bindable(tabs[0]?.id ?? ''),
27
33
  id = generatedId,
34
+ label,
28
35
  class: className,
29
36
  children,
30
37
  onValueChange,
31
38
  }: Props = $props();
32
39
 
40
+ // Store references to tab button elements for direct focus management
41
+ let tabRefs: Record<string, HTMLButtonElement | undefined> = $state({});
42
+
33
43
  // Create a reactive object for context
34
44
  // Initialize with empty values - the effect will set them immediately
35
45
  const tabsState = $state({ activeTab: '', tabsId: '' });
@@ -80,9 +90,8 @@
80
90
  const newTab = enabledTabs[newIndex];
81
91
  if (newTab) {
82
92
  selectTab(newTab.id);
83
- // Focus the new tab
84
- const tabElement = document.getElementById(`${id}-tab-${newTab.id}`);
85
- tabElement?.focus();
93
+ // Focus the new tab using direct element reference
94
+ tabRefs[newTab.id]?.focus();
86
95
  }
87
96
  }
88
97
  </script>
@@ -91,11 +100,12 @@
91
100
  <!-- Tab List -->
92
101
  <div
93
102
  role="tablist"
94
- aria-label="Tabs"
103
+ aria-label={label}
95
104
  class="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground"
96
105
  >
97
106
  {#each tabs as tab, index (tab.id)}
98
107
  <button
108
+ bind:this={tabRefs[tab.id]}
99
109
  id={`${id}-tab-${tab.id}`}
100
110
  role="tab"
101
111
  type="button"
@@ -104,7 +114,7 @@
104
114
  tabindex={value === tab.id ? 0 : -1}
105
115
  disabled={tab.disabled}
106
116
  class={cn(
107
- 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
117
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
108
118
  value === tab.id
109
119
  ? 'bg-background text-foreground shadow-sm'
110
120
  : 'hover:bg-background-hover hover:text-foreground',
@@ -7,6 +7,12 @@ interface Props {
7
7
  value?: string;
8
8
  /** Unique ID for ARIA relationships */
9
9
  id?: string;
10
+ /**
11
+ * Accessible label for the tab list.
12
+ * Describes the purpose of the tabs to screen reader users.
13
+ * @example label="Account settings"
14
+ */
15
+ label: string;
10
16
  /** Additional classes for container */
11
17
  class?: string;
12
18
  /** Tab panel content */
@@ -11,6 +11,14 @@
11
11
  id?: string;
12
12
  rows?: number;
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 textarea */
17
+ 'aria-labelledby'?: string;
18
+ /** ID of element that describes this textarea (e.g., hint or error message) */
19
+ 'aria-describedby'?: string;
20
+ /** Whether the textarea has an error */
21
+ 'aria-invalid'?: boolean | 'true' | 'false';
14
22
  onValueChange?: (value: string) => void;
15
23
  [key: string]: unknown;
16
24
  }
@@ -25,6 +33,10 @@
25
33
  id,
26
34
  rows = 3,
27
35
  class: className,
36
+ 'aria-label': ariaLabel,
37
+ 'aria-labelledby': ariaLabelledby,
38
+ 'aria-describedby': ariaDescribedby,
39
+ 'aria-invalid': ariaInvalid,
28
40
  onValueChange,
29
41
  ...restProps
30
42
  }: Props = $props();
@@ -45,8 +57,15 @@
45
57
  {name}
46
58
  {id}
47
59
  {rows}
60
+ aria-label={ariaLabel}
61
+ aria-labelledby={ariaLabelledby}
62
+ aria-describedby={ariaDescribedby}
63
+ aria-invalid={ariaInvalid}
48
64
  class={cn(
49
65
  'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y',
66
+ ariaInvalid === true || ariaInvalid === 'true'
67
+ ? 'border-destructive focus-visible:ring-destructive'
68
+ : '',
50
69
  className
51
70
  )}
52
71
  oninput={handleInput}
@@ -8,6 +8,14 @@ interface Props {
8
8
  id?: string;
9
9
  rows?: number;
10
10
  class?: string;
11
+ /** Accessible label for screen readers (alternative to visible label) */
12
+ 'aria-label'?: string;
13
+ /** ID of element that labels this textarea */
14
+ 'aria-labelledby'?: string;
15
+ /** ID of element that describes this textarea (e.g., hint or error message) */
16
+ 'aria-describedby'?: string;
17
+ /** Whether the textarea has an error */
18
+ 'aria-invalid'?: boolean | 'true' | 'false';
11
19
  onValueChange?: (value: string) => void;
12
20
  [key: string]: unknown;
13
21
  }
@@ -92,7 +92,7 @@
92
92
  {#if onclose}
93
93
  <button
94
94
  type="button"
95
- class="flex-shrink-0 rounded-md p-1 hover:bg-muted-hover transition-colors"
95
+ class="flex-shrink-0 rounded-md p-1 hover:bg-muted-hover"
96
96
  onclick={onclose}
97
97
  aria-label="Close"
98
98
  >
@@ -119,7 +119,7 @@
119
119
  {#each footerLinks as link}
120
120
  <a
121
121
  href={link.href}
122
- class="text-sm text-muted-foreground transition-colors hover:text-foreground"
122
+ class="text-sm text-muted-foreground hover:text-foreground"
123
123
  target={link.external ? '_blank' : undefined}
124
124
  rel={link.external ? 'noopener noreferrer' : undefined}
125
125
  >
@@ -255,7 +255,7 @@
255
255
 
256
256
  <!-- Main Content Area -->
257
257
  <div
258
- class="flex flex-1 flex-col transition-all duration-300 motion-reduce:transition-none"
258
+ class="flex flex-1 flex-col transition-[margin] duration-300 motion-reduce:transition-none"
259
259
  style="margin-left: {sidebarWidth}px;"
260
260
  >
261
261
  {#if showHeader}
@@ -75,7 +75,7 @@
75
75
  <a
76
76
  href={item.href}
77
77
  class={cn(
78
- 'text-sm transition-colors',
78
+ 'text-sm',
79
79
  isDark
80
80
  ? 'text-white hover:text-primary'
81
81
  : 'text-muted-foreground hover:text-foreground'
@@ -97,7 +97,7 @@
97
97
  <a
98
98
  href={item.href}
99
99
  class={cn(
100
- 'text-sm font-bold uppercase transition-colors hover:text-primary',
100
+ 'text-sm font-bold uppercase hover:text-primary',
101
101
  item.active ? 'text-primary' : 'text-muted-foreground'
102
102
  )}
103
103
  target={item.external ? '_blank' : undefined}
@@ -122,7 +122,7 @@
122
122
  <a
123
123
  href={item.href}
124
124
  class={cn(
125
- 'block rounded-md px-3 py-2 text-sm font-bold uppercase transition-colors',
125
+ 'block rounded-md px-3 py-2 text-sm font-bold uppercase',
126
126
  item.active
127
127
  ? 'bg-primary/10 text-primary'
128
128
  : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
@@ -50,7 +50,7 @@
50
50
  <a
51
51
  href={link.href}
52
52
  class={cn(
53
- 'flex items-center transition-all duration-300 motion-reduce:transition-none overflow-hidden',
53
+ 'flex items-center overflow-hidden',
54
54
  // List mode styles
55
55
  display === 'list' && 'gap-3 rounded-md px-3 py-2 text-sm',
56
56
  display === 'list' &&
@@ -93,7 +93,7 @@
93
93
 
94
94
  <span
95
95
  class={cn(
96
- 'truncate whitespace-nowrap transition-all duration-300 motion-reduce:transition-none',
96
+ 'truncate whitespace-nowrap transition-[width,opacity] duration-300 motion-reduce:transition-none',
97
97
  display === 'icons' ? 'w-0 opacity-0 flex-none' : 'flex-1 opacity-100'
98
98
  )}
99
99
  >
@@ -111,7 +111,7 @@
111
111
  {#if link.external}
112
112
  <svg
113
113
  class={cn(
114
- 'h-4 w-4 shrink-0 transition-all duration-300',
114
+ 'h-4 w-4 shrink-0 transition-opacity duration-300',
115
115
  display === 'icons' ? 'hidden' : 'opacity-50'
116
116
  )}
117
117
  fill="none"
@@ -437,7 +437,7 @@
437
437
  <!-- Sidebar: z-50 positions above overlay and content, below modals (z-100+) -->
438
438
  <aside
439
439
  class={cn(
440
- 'fixed left-0 top-0 z-[var(--z-sidebar,50)] flex h-full flex-col transition-all duration-300 touch-pan-y',
440
+ 'fixed left-0 top-0 z-[var(--z-sidebar,50)] flex h-full flex-col transition-[width,transform] duration-300 motion-reduce:transition-none touch-pan-y',
441
441
  // Variant-based background and text colors
442
442
  isLight ? 'bg-background text-foreground' : 'bg-sidebar text-sidebar-foreground',
443
443
  strongBorder ? 'border-r-2 border-black' : 'border-r border-black',
@@ -110,7 +110,6 @@
110
110
  bind:this={flyoutElement}
111
111
  class={cn(
112
112
  'fixed min-w-48 rounded-md border shadow-lg z-[calc(var(--z-sidebar,50)+10)]',
113
- 'motion-reduce:transition-none',
114
113
  isLight ? 'bg-background border-border' : 'bg-sidebar border-sidebar-foreground/20'
115
114
  )}
116
115
  style="top: {top}px; left: {left}px;"
@@ -174,7 +173,7 @@
174
173
  <a
175
174
  href={child.disabled ? undefined : child.href}
176
175
  class={cn(
177
- 'flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors ml-2',
176
+ 'flex items-center gap-2 rounded-md px-3 py-2 text-sm ml-2',
178
177
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
179
178
  isLight
180
179
  ? 'text-foreground hover:bg-accent hover:text-accent-foreground'
@@ -84,11 +84,13 @@
84
84
 
85
85
  function getItemStateClasses(active?: boolean, disabled?: boolean): string {
86
86
  return cn(
87
- isLight && !active && 'text-foreground hover:bg-accent hover:text-accent-foreground',
87
+ isLight &&
88
+ !active &&
89
+ 'text-foreground bg-background hover:bg-accent hover:text-accent-foreground',
88
90
  isLight && active && 'bg-primary/10 text-primary',
89
91
  !isLight &&
90
92
  !active &&
91
- 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
93
+ 'text-sidebar-foreground bg-sidebar hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
92
94
  !isLight && active && 'bg-sidebar-primary text-sidebar-primary-foreground',
93
95
  disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
94
96
  );
@@ -172,13 +174,13 @@
172
174
  highlight: { before: string; match: string; after: string } | null
173
175
  )}
174
176
  {#if highlight}
175
- <span class="flex-1 truncate">
177
+ <span class="flex-1 min-w-0 truncate">
176
178
  {highlight.before}<mark class="bg-yellow-200 dark:bg-yellow-900 text-inherit rounded px-0.5"
177
179
  >{highlight.match}</mark
178
180
  >{highlight.after}
179
181
  </span>
180
182
  {:else}
181
- <span class="flex-1 truncate">{name}</span>
183
+ <span class="flex-1 min-w-0 truncate">{name}</span>
182
184
  {/if}
183
185
  {/snippet}
184
186
 
@@ -186,7 +188,7 @@
186
188
  <button
187
189
  type="button"
188
190
  class={cn(
189
- 'flex items-center justify-center p-1 rounded transition-all shrink-0',
191
+ 'flex items-center justify-center p-1 rounded shrink-0',
190
192
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
191
193
  // Show on hover for regular items, always show in pinned section
192
194
  isUnpin ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
@@ -247,10 +249,10 @@
247
249
  <div>
248
250
  <div
249
251
  class={cn(
250
- 'group flex w-full items-center rounded-md transition-colors overflow-hidden',
251
- isLight && !item.active && 'hover:bg-accent',
252
+ 'group flex w-full items-center rounded-md',
253
+ isLight && !item.active && 'bg-background hover:bg-accent',
252
254
  isLight && item.active && 'bg-primary/10',
253
- !isLight && !item.active && 'hover:bg-sidebar-accent',
255
+ !isLight && !item.active && 'bg-sidebar hover:bg-sidebar-accent',
254
256
  !isLight && item.active && 'bg-sidebar-primary'
255
257
  )}
256
258
  >
@@ -259,7 +261,7 @@
259
261
  <a
260
262
  href={item.disabled ? undefined : item.href}
261
263
  class={cn(
262
- 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
264
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium',
263
265
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
264
266
  'group-hover:underline underline-offset-2',
265
267
  isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
@@ -287,7 +289,7 @@
287
289
  <button
288
290
  type="button"
289
291
  class={cn(
290
- 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden text-left',
292
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium text-left',
291
293
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
292
294
  isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
293
295
  isLight && item.active && 'text-primary',
@@ -312,7 +314,7 @@
312
314
  <button
313
315
  type="button"
314
316
  class={cn(
315
- 'flex items-center justify-center p-2 transition-colors rounded-md',
317
+ 'flex items-center justify-center p-2 rounded-md',
316
318
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
317
319
  isLight
318
320
  ? 'hover:bg-accent-hover group-hover:text-accent-foreground'
@@ -353,16 +355,13 @@
353
355
  </div>
354
356
 
355
357
  {#if isExpanded}
356
- <ul
357
- class="mt-1 space-y-1 pl-4 motion-reduce:transition-none overflow-hidden"
358
- transition:slide={{ duration: 200 }}
359
- >
358
+ <ul class="mt-1 space-y-1 pl-4 overflow-hidden" transition:slide={{ duration: 200 }}>
360
359
  {#each item.children as child}
361
360
  <li>
362
361
  <a
363
362
  href={child.disabled ? undefined : child.href}
364
363
  class={cn(
365
- 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
364
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium',
366
365
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
367
366
  getItemStateClasses(child.active, child.disabled)
368
367
  )}
@@ -394,10 +393,10 @@
394
393
  <!-- Wrap in group div for pin button hover behavior -->
395
394
  <div
396
395
  class={cn(
397
- 'group flex w-full items-center rounded-md transition-colors overflow-hidden',
398
- isLight && !item.active && 'hover:bg-accent',
396
+ 'group flex w-full items-center rounded-md',
397
+ isLight && !item.active && 'bg-background hover:bg-accent',
399
398
  isLight && item.active && 'bg-primary/10',
400
- !isLight && !item.active && 'hover:bg-sidebar-accent',
399
+ !isLight && !item.active && 'bg-sidebar hover:bg-sidebar-accent',
401
400
  !isLight && item.active && 'bg-sidebar-primary'
402
401
  )}
403
402
  >
@@ -406,7 +405,7 @@
406
405
  <a
407
406
  href={item.disabled ? undefined : item.href}
408
407
  class={cn(
409
- 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden relative',
408
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium relative',
410
409
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
411
410
  isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
412
411
  isLight && item.active && 'text-primary',
@@ -453,7 +452,7 @@
453
452
  <button
454
453
  type="button"
455
454
  class={cn(
456
- 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden text-left relative',
455
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium text-left relative',
457
456
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
458
457
  isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
459
458
  isLight && item.active && 'text-primary',
@@ -67,7 +67,7 @@
67
67
  <button
68
68
  onclick={toggleSection}
69
69
  class={cn(
70
- 'flex w-full items-center justify-between mb-2 px-4 text-xs font-semibold uppercase tracking-wider transition-colors rounded-md overflow-hidden',
70
+ 'flex w-full items-center justify-between mb-2 px-4 text-xs font-semibold uppercase tracking-wider rounded-md overflow-hidden',
71
71
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
72
72
  isLight
73
73
  ? 'text-muted-foreground hover:text-foreground'
@@ -132,7 +132,7 @@
132
132
  {:else if isExpanded}
133
133
  <!-- Collapsible and expanded: animate -->
134
134
  <ul
135
- class="space-y-1 px-2 motion-reduce:transition-none overflow-hidden"
135
+ class="space-y-1 px-2 overflow-hidden"
136
136
  id={`sidebar-section-${section.id}`}
137
137
  transition:slide={{ duration: 200 }}
138
138
  >
@@ -81,7 +81,7 @@
81
81
  aria-selected={filter === filterItem.id}
82
82
  tabindex={filter === filterItem.id ? 0 : -1}
83
83
  class={cn(
84
- 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
84
+ 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
85
85
  filter === filterItem.id
86
86
  ? 'bg-background text-foreground shadow-sm'
87
87
  : 'hover:bg-background-hover hover:text-foreground'
@@ -66,9 +66,10 @@
66
66
  }
67
67
  </script>
68
68
 
69
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
69
70
  <div
70
71
  class={cn(
71
- 'group relative flex gap-3 rounded-lg border p-4 transition-colors',
72
+ 'group relative flex gap-3 rounded-lg border p-4',
72
73
  !notification.read && 'bg-blue-50/50 border-blue-200',
73
74
  notification.read && 'bg-card border-border',
74
75
  onClick && 'cursor-pointer hover:bg-muted/50',
@@ -24,3 +24,11 @@ export { useAsync, runAsync, type UseAsyncOptions, type UseAsyncReturn, } from '
24
24
  * ```
25
25
  */
26
26
  export { usePersistedForm, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, } from './usePersistedForm.svelte.js';
27
+ export { useFocusTrap, type UseFocusTrapOptions } from './useFocusTrap.svelte.js';
28
+ export { useFocusRef, type UseFocusRefOptions } from './useFocusRef.svelte.js';
29
+ export { useReducedMotion } from './useReducedMotion.svelte.js';
30
+ export { useArrowNavigation, type UseArrowNavigationOptions, type ArrowNavigationOrientation, } from './useArrowNavigation.svelte.js';
31
+ export { useRovingTabindex, type UseRovingTabindexOptions } from './useRovingTabindex.svelte.js';
32
+ export { useAnnounce, type UseAnnounceOptions, type UseAnnounceReturn, } from './useAnnounce.svelte.js';
33
+ export { useEscapeKey, type UseEscapeKeyOptions } from './useEscapeKey.svelte.js';
34
+ export { useClickOutside, clickOutsideAction, type UseClickOutsideOptions, } from './useClickOutside.svelte.js';
@@ -26,3 +26,12 @@ export { useAsync, runAsync, } from './useAsync.svelte.js';
26
26
  * ```
27
27
  */
28
28
  export { usePersistedForm, } from './usePersistedForm.svelte.js';
29
+ // Accessibility composables
30
+ export { useFocusTrap } from './useFocusTrap.svelte.js';
31
+ export { useFocusRef } from './useFocusRef.svelte.js';
32
+ export { useReducedMotion } from './useReducedMotion.svelte.js';
33
+ export { useArrowNavigation, } from './useArrowNavigation.svelte.js';
34
+ export { useRovingTabindex } from './useRovingTabindex.svelte.js';
35
+ export { useAnnounce, } from './useAnnounce.svelte.js';
36
+ export { useEscapeKey } from './useEscapeKey.svelte.js';
37
+ export { useClickOutside, clickOutsideAction, } from './useClickOutside.svelte.js';