@human-kit/svelte-components 1.0.0-alpha.1

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 (140) hide show
  1. package/dist/combobox/TODO.md +175 -0
  2. package/dist/combobox/button/combobox-button.svelte +57 -0
  3. package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
  4. package/dist/combobox/index.d.ts +14 -0
  5. package/dist/combobox/index.js +18 -0
  6. package/dist/combobox/index.parts.d.ts +10 -0
  7. package/dist/combobox/index.parts.js +11 -0
  8. package/dist/combobox/input/combobox-input.svelte +98 -0
  9. package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
  10. package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
  11. package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
  12. package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
  13. package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
  14. package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
  15. package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
  16. package/dist/combobox/list/combobox-listbox.svelte +76 -0
  17. package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
  18. package/dist/combobox/popover/combobox-popover.svelte +69 -0
  19. package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
  20. package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
  21. package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
  22. package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
  23. package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
  24. package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
  25. package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
  26. package/dist/combobox/root/combobox-test.svelte +43 -0
  27. package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
  28. package/dist/combobox/root/combobox.svelte +696 -0
  29. package/dist/combobox/root/combobox.svelte.d.ts +58 -0
  30. package/dist/combobox/root/context.d.ts +90 -0
  31. package/dist/combobox/root/context.js +15 -0
  32. package/dist/combobox/tag/combobox-tag.svelte +58 -0
  33. package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
  34. package/dist/combobox/tag/tag-context-provider.svelte +36 -0
  35. package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
  36. package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
  37. package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
  38. package/dist/combobox/tags/combobox-tags.svelte +50 -0
  39. package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
  40. package/dist/dialog/content/dialog-content.svelte +121 -0
  41. package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
  42. package/dist/dialog/index.d.ts +10 -0
  43. package/dist/dialog/index.js +15 -0
  44. package/dist/dialog/index.parts.d.ts +5 -0
  45. package/dist/dialog/index.parts.js +6 -0
  46. package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
  47. package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
  48. package/dist/dialog/portal/dialog-portal.svelte +32 -0
  49. package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
  50. package/dist/dialog/root/context.d.ts +25 -0
  51. package/dist/dialog/root/context.js +8 -0
  52. package/dist/dialog/root/dialog-root.svelte +99 -0
  53. package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
  54. package/dist/dialog/root/dialog-stack.d.ts +32 -0
  55. package/dist/dialog/root/dialog-stack.js +55 -0
  56. package/dist/dialog/root/dialog-test.svelte +38 -0
  57. package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
  58. package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
  59. package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
  60. package/dist/dialog/root/nested-dialog-test.svelte +63 -0
  61. package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
  62. package/dist/dialog/root/types.d.ts +10 -0
  63. package/dist/dialog/root/types.js +1 -0
  64. package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
  65. package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
  66. package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
  67. package/dist/hooks/use-virtual-focus.svelte.js +201 -0
  68. package/dist/index.d.ts +13 -0
  69. package/dist/index.js +19 -0
  70. package/dist/input/index.d.ts +3 -0
  71. package/dist/input/index.js +3 -0
  72. package/dist/input/input.svelte +19 -0
  73. package/dist/input/input.svelte.d.ts +8 -0
  74. package/dist/label/index.d.ts +3 -0
  75. package/dist/label/index.js +3 -0
  76. package/dist/label/label.svelte +21 -0
  77. package/dist/label/label.svelte.d.ts +8 -0
  78. package/dist/listbox/index.d.ts +6 -0
  79. package/dist/listbox/index.js +10 -0
  80. package/dist/listbox/index.parts.d.ts +2 -0
  81. package/dist/listbox/index.parts.js +3 -0
  82. package/dist/listbox/item/listbox-item.svelte +186 -0
  83. package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
  84. package/dist/listbox/root/context.d.ts +73 -0
  85. package/dist/listbox/root/context.js +249 -0
  86. package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
  87. package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
  88. package/dist/listbox/root/listbox-test.svelte +27 -0
  89. package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
  90. package/dist/listbox/root/listbox.svelte +146 -0
  91. package/dist/listbox/root/listbox.svelte.d.ts +54 -0
  92. package/dist/popover/content/popover-content-test.svelte +43 -0
  93. package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
  94. package/dist/popover/content/popover-content.svelte +167 -0
  95. package/dist/popover/content/popover-content.svelte.d.ts +38 -0
  96. package/dist/popover/index.d.ts +8 -0
  97. package/dist/popover/index.js +14 -0
  98. package/dist/popover/index.parts.d.ts +4 -0
  99. package/dist/popover/index.parts.js +5 -0
  100. package/dist/popover/root/context.d.ts +24 -0
  101. package/dist/popover/root/context.js +10 -0
  102. package/dist/popover/root/popover-root.svelte +87 -0
  103. package/dist/popover/root/popover-root.svelte.d.ts +20 -0
  104. package/dist/popover/root/popover-test.svelte +40 -0
  105. package/dist/popover/root/popover-test.svelte.d.ts +11 -0
  106. package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
  107. package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
  108. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
  109. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
  110. package/dist/popover/trigger/popover-trigger.svelte +71 -0
  111. package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
  112. package/dist/portal/index.d.ts +1 -0
  113. package/dist/portal/index.js +1 -0
  114. package/dist/portal/portal.svelte +44 -0
  115. package/dist/portal/portal.svelte.d.ts +10 -0
  116. package/dist/primitives/aria-hide-outside.d.ts +38 -0
  117. package/dist/primitives/aria-hide-outside.js +152 -0
  118. package/dist/primitives/click-outside.d.ts +26 -0
  119. package/dist/primitives/click-outside.js +66 -0
  120. package/dist/primitives/floating.d.ts +57 -0
  121. package/dist/primitives/floating.js +179 -0
  122. package/dist/primitives/focus-trap.d.ts +19 -0
  123. package/dist/primitives/focus-trap.js +102 -0
  124. package/dist/primitives/index.d.ts +6 -0
  125. package/dist/primitives/index.js +7 -0
  126. package/dist/primitives/keyboard-navigation.d.ts +88 -0
  127. package/dist/primitives/keyboard-navigation.js +274 -0
  128. package/dist/primitives/scroll-lock.d.ts +19 -0
  129. package/dist/primitives/scroll-lock.js +62 -0
  130. package/dist/test-mocks/app-environment.d.ts +7 -0
  131. package/dist/test-mocks/app-environment.js +7 -0
  132. package/dist/test-mocks/app-navigation.d.ts +11 -0
  133. package/dist/test-mocks/app-navigation.js +11 -0
  134. package/dist/test-mocks/app-stores.d.ts +16 -0
  135. package/dist/test-mocks/app-stores.js +18 -0
  136. package/dist/utils/cn.d.ts +2 -0
  137. package/dist/utils/cn.js +5 -0
  138. package/dist/utils/index.d.ts +1 -0
  139. package/dist/utils/index.js +1 -0
  140. package/package.json +99 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Focus trap primitive.
3
+ * Traps keyboard focus within a container element.
4
+ */
5
+ const FOCUSABLE_SELECTOR = [
6
+ 'a[href]',
7
+ 'button:not([disabled])',
8
+ 'input:not([disabled])',
9
+ 'select:not([disabled])',
10
+ 'textarea:not([disabled])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ '[contenteditable="true"]'
13
+ ].join(', ');
14
+ function getFocusableElements(container) {
15
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
16
+ }
17
+ /**
18
+ * Svelte action that traps focus within an element.
19
+ *
20
+ * @example
21
+ * ```svelte
22
+ * <div use:focusTrap={isOpen}>
23
+ * <button>First</button>
24
+ * <button>Last</button>
25
+ * </div>
26
+ * ```
27
+ */
28
+ export function focusTrap(node, enabled = true) {
29
+ let previousActiveElement = null;
30
+ function handleKeydown(event) {
31
+ if (event.key !== 'Tab')
32
+ return;
33
+ const focusableElements = getFocusableElements(node);
34
+ if (focusableElements.length === 0) {
35
+ event.preventDefault();
36
+ node.focus();
37
+ return;
38
+ }
39
+ const firstElement = focusableElements[0];
40
+ const lastElement = focusableElements[focusableElements.length - 1];
41
+ const focusIsInside = node.contains(document.activeElement);
42
+ if (!focusIsInside) {
43
+ event.preventDefault();
44
+ firstElement.focus();
45
+ return;
46
+ }
47
+ if (event.shiftKey) {
48
+ if (document.activeElement === firstElement || document.activeElement === node) {
49
+ event.preventDefault();
50
+ lastElement.focus();
51
+ }
52
+ }
53
+ else {
54
+ if (document.activeElement === lastElement || document.activeElement === node) {
55
+ event.preventDefault();
56
+ firstElement.focus();
57
+ }
58
+ }
59
+ }
60
+ function activate() {
61
+ previousActiveElement = document.activeElement;
62
+ if (!node.hasAttribute('tabindex')) {
63
+ node.setAttribute('tabindex', '-1');
64
+ }
65
+ // Focus first focusable element, or the container if none
66
+ requestAnimationFrame(() => {
67
+ const focusableElements = getFocusableElements(node);
68
+ if (focusableElements.length > 0) {
69
+ focusableElements[0].focus();
70
+ }
71
+ else {
72
+ node.focus();
73
+ }
74
+ });
75
+ document.addEventListener('keydown', handleKeydown, true);
76
+ }
77
+ function deactivate() {
78
+ document.removeEventListener('keydown', handleKeydown, true);
79
+ if (previousActiveElement && previousActiveElement.focus) {
80
+ previousActiveElement.focus();
81
+ }
82
+ }
83
+ if (enabled) {
84
+ activate();
85
+ }
86
+ return {
87
+ update(newEnabled) {
88
+ if (newEnabled && !enabled) {
89
+ activate();
90
+ }
91
+ else if (!newEnabled && enabled) {
92
+ deactivate();
93
+ }
94
+ enabled = newEnabled;
95
+ },
96
+ destroy() {
97
+ if (enabled) {
98
+ deactivate();
99
+ }
100
+ }
101
+ };
102
+ }
@@ -0,0 +1,6 @@
1
+ export * from './floating.js';
2
+ export * from './focus-trap.js';
3
+ export * from './keyboard-navigation.js';
4
+ export * from './scroll-lock.js';
5
+ export * from './aria-hide-outside.js';
6
+ export * from './click-outside.js';
@@ -0,0 +1,7 @@
1
+ // Primitives exports
2
+ export * from './floating.js';
3
+ export * from './focus-trap.js';
4
+ export * from './keyboard-navigation.js';
5
+ export * from './scroll-lock.js';
6
+ export * from './aria-hide-outside.js';
7
+ export * from './click-outside.js';
@@ -0,0 +1,88 @@
1
+ import { type Writable } from 'svelte/store';
2
+ export type KeyboardNavigationOptions = {
3
+ /**
4
+ * Orientation of the navigation
5
+ * - 'vertical': ArrowUp/ArrowDown for navigation
6
+ * - 'horizontal': ArrowLeft/ArrowRight for navigation
7
+ * - 'both': All arrow keys for navigation
8
+ */
9
+ orientation?: 'vertical' | 'horizontal' | 'both';
10
+ /**
11
+ * Whether navigation wraps around at the ends
12
+ */
13
+ loop?: boolean;
14
+ /**
15
+ * Selector for finding navigable items within the container
16
+ */
17
+ itemSelector?: string;
18
+ /**
19
+ * Callback when an item is selected (Enter/Space)
20
+ */
21
+ onSelect?: (id: string | number, element: HTMLElement) => void;
22
+ /**
23
+ * Callback when focused item changes
24
+ */
25
+ onFocusChange?: (id: string | number | null, element: HTMLElement | null) => void;
26
+ /**
27
+ * Callback for Ctrl+A (select all)
28
+ */
29
+ onSelectAll?: () => void;
30
+ /**
31
+ * Whether to handle Home/End keys
32
+ */
33
+ homeEndKeys?: boolean;
34
+ /**
35
+ * Whether to handle typeahead (character search)
36
+ */
37
+ typeahead?: boolean;
38
+ };
39
+ export type KeyboardNavigationState = {
40
+ focusedId: Writable<string | number | null>;
41
+ focusedElement: Writable<HTMLElement | null>;
42
+ };
43
+ export type KeyboardNavigationReturn = {
44
+ /** Current state stores */
45
+ state: KeyboardNavigationState;
46
+ /** Svelte action to attach to container */
47
+ action: (node: HTMLElement) => {
48
+ destroy: () => void;
49
+ };
50
+ /** Programmatic navigation methods */
51
+ focusNext: () => void;
52
+ focusPrevious: () => void;
53
+ focusFirst: () => void;
54
+ focusLast: () => void;
55
+ focusById: (id: string | number) => void;
56
+ /** Update items (call after DOM changes) */
57
+ updateItems: () => void;
58
+ };
59
+ /**
60
+ * Creates a keyboard navigation controller for list-like components.
61
+ * Implements WAI-ARIA patterns for keyboard navigation.
62
+ *
63
+ * @example
64
+ * ```svelte
65
+ * <script>
66
+ * import { createKeyboardNavigation } from './keyboard-navigation';
67
+ *
68
+ * const { action, state, focusNext } = createKeyboardNavigation({
69
+ * orientation: 'vertical',
70
+ * onSelect: (id) => console.log('Selected:', id)
71
+ * });
72
+ * </script>
73
+ *
74
+ * <div use:action>
75
+ * <div data-navigation-item data-item-id="1">Item 1</div>
76
+ * <div data-navigation-item data-item-id="2">Item 2</div>
77
+ * </div>
78
+ * ```
79
+ */
80
+ export declare function createKeyboardNavigation(options?: KeyboardNavigationOptions): KeyboardNavigationReturn;
81
+ /**
82
+ * Simple Svelte action for roving tabindex without state management.
83
+ * Use this for simpler cases where you don't need programmatic control.
84
+ */
85
+ export declare function rovingTabindex(container: HTMLElement, options?: Pick<KeyboardNavigationOptions, 'orientation' | 'loop' | 'itemSelector'>): {
86
+ update: () => void;
87
+ destroy: () => void;
88
+ };
@@ -0,0 +1,274 @@
1
+ import { writable } from 'svelte/store';
2
+ /**
3
+ * Creates a keyboard navigation controller for list-like components.
4
+ * Implements WAI-ARIA patterns for keyboard navigation.
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * import { createKeyboardNavigation } from './keyboard-navigation';
10
+ *
11
+ * const { action, state, focusNext } = createKeyboardNavigation({
12
+ * orientation: 'vertical',
13
+ * onSelect: (id) => console.log('Selected:', id)
14
+ * });
15
+ * </script>
16
+ *
17
+ * <div use:action>
18
+ * <div data-navigation-item data-item-id="1">Item 1</div>
19
+ * <div data-navigation-item data-item-id="2">Item 2</div>
20
+ * </div>
21
+ * ```
22
+ */
23
+ export function createKeyboardNavigation(options = {}) {
24
+ const { orientation = 'vertical', loop = false, itemSelector = '[data-navigation-item]:not([data-disabled])', onSelect, onFocusChange, onSelectAll, homeEndKeys = true, typeahead = false } = options;
25
+ const focusedId = writable(null);
26
+ const focusedElement = writable(null);
27
+ let container = null;
28
+ let items = [];
29
+ let typeaheadBuffer = '';
30
+ let typeaheadTimeout = null;
31
+ function getItems() {
32
+ if (!container)
33
+ return [];
34
+ return Array.from(container.querySelectorAll(itemSelector));
35
+ }
36
+ function updateItems() {
37
+ items = getItems();
38
+ // Note: tabIndex is now controlled by Svelte components via isFocused state
39
+ // The primitive only manages focus() calls and notifies about focus changes
40
+ }
41
+ function getItemId(element) {
42
+ const rawId = element.dataset.itemId;
43
+ if (rawId === undefined)
44
+ return null;
45
+ const idType = element.dataset.itemIdType;
46
+ if (idType === 'number') {
47
+ const parsed = Number(rawId);
48
+ return Number.isNaN(parsed) ? rawId : parsed;
49
+ }
50
+ return rawId;
51
+ }
52
+ function focusItem(element) {
53
+ if (!element) {
54
+ focusedId.set(null);
55
+ focusedElement.set(null);
56
+ onFocusChange?.(null, null);
57
+ return;
58
+ }
59
+ const id = getItemId(element);
60
+ focusedId.set(id);
61
+ focusedElement.set(element);
62
+ // Note: tabIndex is controlled by Svelte via isFocused state
63
+ // We just call focus() and notify - the component will react to onFocusChange
64
+ element.focus();
65
+ onFocusChange?.(id, element);
66
+ }
67
+ function getCurrentIndex() {
68
+ let currentElement = null;
69
+ focusedElement.subscribe((el) => (currentElement = el))();
70
+ if (!currentElement) {
71
+ const active = document.activeElement;
72
+ const idx = items.indexOf(active);
73
+ return idx;
74
+ }
75
+ return items.indexOf(currentElement);
76
+ }
77
+ function focusNext() {
78
+ items = getItems();
79
+ if (items.length === 0)
80
+ return;
81
+ const currentIdx = getCurrentIndex();
82
+ let nextIdx;
83
+ if (currentIdx === -1) {
84
+ nextIdx = 0;
85
+ }
86
+ else if (loop) {
87
+ nextIdx = (currentIdx + 1) % items.length;
88
+ }
89
+ else {
90
+ nextIdx = Math.min(currentIdx + 1, items.length - 1);
91
+ }
92
+ focusItem(items[nextIdx]);
93
+ }
94
+ function focusPrevious() {
95
+ items = getItems();
96
+ if (items.length === 0)
97
+ return;
98
+ const currentIdx = getCurrentIndex();
99
+ let prevIdx;
100
+ if (currentIdx === -1) {
101
+ prevIdx = items.length - 1;
102
+ }
103
+ else if (loop) {
104
+ prevIdx = (currentIdx - 1 + items.length) % items.length;
105
+ }
106
+ else {
107
+ prevIdx = Math.max(currentIdx - 1, 0);
108
+ }
109
+ focusItem(items[prevIdx]);
110
+ }
111
+ function focusFirst() {
112
+ items = getItems();
113
+ if (items.length === 0)
114
+ return;
115
+ focusItem(items[0]);
116
+ }
117
+ function focusLast() {
118
+ items = getItems();
119
+ if (items.length === 0)
120
+ return;
121
+ focusItem(items[items.length - 1]);
122
+ }
123
+ function focusById(id) {
124
+ items = getItems();
125
+ const element = items.find((el) => {
126
+ const itemId = getItemId(el);
127
+ return itemId === id || String(itemId) === String(id);
128
+ });
129
+ if (element) {
130
+ focusItem(element);
131
+ }
132
+ }
133
+ function handleTypeahead(char) {
134
+ if (!typeahead)
135
+ return;
136
+ if (typeaheadTimeout) {
137
+ clearTimeout(typeaheadTimeout);
138
+ }
139
+ typeaheadBuffer += char.toLowerCase();
140
+ items = getItems();
141
+ const match = items.find((el) => {
142
+ const text = el.textContent?.trim().toLowerCase() || '';
143
+ return text.startsWith(typeaheadBuffer);
144
+ });
145
+ if (match) {
146
+ focusItem(match);
147
+ }
148
+ typeaheadTimeout = setTimeout(() => {
149
+ typeaheadBuffer = '';
150
+ }, 500);
151
+ }
152
+ function handleKeydown(event) {
153
+ const { key, ctrlKey, metaKey } = event;
154
+ const nextKeys = orientation === 'horizontal'
155
+ ? ['ArrowRight']
156
+ : orientation === 'vertical'
157
+ ? ['ArrowDown']
158
+ : ['ArrowDown', 'ArrowRight'];
159
+ const prevKeys = orientation === 'horizontal'
160
+ ? ['ArrowLeft']
161
+ : orientation === 'vertical'
162
+ ? ['ArrowUp']
163
+ : ['ArrowUp', 'ArrowLeft'];
164
+ if (nextKeys.includes(key)) {
165
+ event.preventDefault();
166
+ focusNext();
167
+ return;
168
+ }
169
+ if (prevKeys.includes(key)) {
170
+ event.preventDefault();
171
+ focusPrevious();
172
+ return;
173
+ }
174
+ // For all other keys, block repeat (prevent scroll but don't act)
175
+ if (event.repeat) {
176
+ event.preventDefault();
177
+ return;
178
+ }
179
+ // Home/End - single press only
180
+ if (homeEndKeys) {
181
+ if (key === 'Home') {
182
+ event.preventDefault();
183
+ focusFirst();
184
+ return;
185
+ }
186
+ if (key === 'End') {
187
+ event.preventDefault();
188
+ focusLast();
189
+ return;
190
+ }
191
+ }
192
+ // Selection - single press only
193
+ if (key === 'Enter' || key === ' ') {
194
+ event.preventDefault();
195
+ const active = document.activeElement;
196
+ items = getItems();
197
+ if (active && items.includes(active)) {
198
+ const id = getItemId(active);
199
+ if (id !== null) {
200
+ onSelect?.(id, active);
201
+ }
202
+ }
203
+ return;
204
+ }
205
+ // Select all (Ctrl+A / Cmd+A)
206
+ if ((ctrlKey || metaKey) && (key === 'a' || key === 'A')) {
207
+ if (onSelectAll) {
208
+ event.preventDefault();
209
+ onSelectAll();
210
+ return;
211
+ }
212
+ }
213
+ // Typeahead (single printable character)
214
+ if (typeahead && key.length === 1 && !ctrlKey && !metaKey) {
215
+ handleTypeahead(key);
216
+ }
217
+ }
218
+ function action(node) {
219
+ container = node;
220
+ updateItems();
221
+ node.addEventListener('keydown', handleKeydown);
222
+ // Handle focus on container
223
+ function handleContainerFocus(event) {
224
+ // If focusing container directly (not an item), focus first item
225
+ if (event.target === node) {
226
+ items = getItems();
227
+ if (items.length > 0) {
228
+ // Focus the item with tabIndex 0 or first item
229
+ const tabbable = items.find((el) => el.tabIndex === 0) || items[0];
230
+ focusItem(tabbable);
231
+ }
232
+ }
233
+ }
234
+ node.addEventListener('focus', handleContainerFocus, true);
235
+ return {
236
+ destroy() {
237
+ node.removeEventListener('keydown', handleKeydown);
238
+ node.removeEventListener('focus', handleContainerFocus, true);
239
+ container = null;
240
+ if (typeaheadTimeout) {
241
+ clearTimeout(typeaheadTimeout);
242
+ }
243
+ }
244
+ };
245
+ }
246
+ return {
247
+ state: {
248
+ focusedId,
249
+ focusedElement
250
+ },
251
+ action,
252
+ focusNext,
253
+ focusPrevious,
254
+ focusFirst,
255
+ focusLast,
256
+ focusById,
257
+ updateItems
258
+ };
259
+ }
260
+ /**
261
+ * Simple Svelte action for roving tabindex without state management.
262
+ * Use this for simpler cases where you don't need programmatic control.
263
+ */
264
+ export function rovingTabindex(container, options = {}) {
265
+ if (typeof document === 'undefined') {
266
+ return { update: () => { }, destroy: () => { } };
267
+ }
268
+ const { action, updateItems } = createKeyboardNavigation(options);
269
+ const cleanup = action(container);
270
+ return {
271
+ update: updateItems,
272
+ destroy: cleanup.destroy
273
+ };
274
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Scroll lock primitive.
3
+ * Prevents scrolling of the document body.
4
+ */
5
+ /**
6
+ * Svelte action that locks scrolling on the document body.
7
+ * Handles multiple nested scroll locks correctly.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <div use:scrollLock={isOpen}>
12
+ * Modal content
13
+ * </div>
14
+ * ```
15
+ */
16
+ export declare function scrollLock(node: HTMLElement, enabled?: boolean): {
17
+ update(newEnabled: boolean): void;
18
+ destroy(): void;
19
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Scroll lock primitive.
3
+ * Prevents scrolling of the document body.
4
+ */
5
+ let lockCount = 0;
6
+ let originalOverflow = '';
7
+ let originalPaddingRight = '';
8
+ function getScrollbarWidth() {
9
+ return window.innerWidth - document.documentElement.clientWidth;
10
+ }
11
+ function lock() {
12
+ if (lockCount === 0) {
13
+ originalOverflow = document.body.style.overflow;
14
+ originalPaddingRight = document.body.style.paddingRight;
15
+ const scrollbarWidth = getScrollbarWidth();
16
+ document.body.style.overflow = 'hidden';
17
+ if (scrollbarWidth > 0) {
18
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
19
+ }
20
+ }
21
+ lockCount++;
22
+ }
23
+ function unlock() {
24
+ lockCount--;
25
+ if (lockCount === 0) {
26
+ document.body.style.overflow = originalOverflow;
27
+ document.body.style.paddingRight = originalPaddingRight;
28
+ }
29
+ lockCount = Math.max(0, lockCount);
30
+ }
31
+ /**
32
+ * Svelte action that locks scrolling on the document body.
33
+ * Handles multiple nested scroll locks correctly.
34
+ *
35
+ * @example
36
+ * ```svelte
37
+ * <div use:scrollLock={isOpen}>
38
+ * Modal content
39
+ * </div>
40
+ * ```
41
+ */
42
+ export function scrollLock(node, enabled = true) {
43
+ if (enabled) {
44
+ lock();
45
+ }
46
+ return {
47
+ update(newEnabled) {
48
+ if (newEnabled && !enabled) {
49
+ lock();
50
+ }
51
+ else if (!newEnabled && enabled) {
52
+ unlock();
53
+ }
54
+ enabled = newEnabled;
55
+ },
56
+ destroy() {
57
+ if (enabled) {
58
+ unlock();
59
+ }
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mock for $app/environment used in vitest tests
3
+ */
4
+ export declare const browser = true;
5
+ export declare const dev = true;
6
+ export declare const building = false;
7
+ export declare const version = "test";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mock for $app/environment used in vitest tests
3
+ */
4
+ export const browser = true;
5
+ export const dev = true;
6
+ export const building = false;
7
+ export const version = 'test';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Mock for $app/navigation used in vitest tests
3
+ */
4
+ export declare const goto: () => Promise<void>;
5
+ export declare const invalidate: () => Promise<void>;
6
+ export declare const invalidateAll: () => Promise<void>;
7
+ export declare const prefetch: () => Promise<void>;
8
+ export declare const prefetchRoutes: () => Promise<void>;
9
+ export declare const beforeNavigate: () => void;
10
+ export declare const afterNavigate: () => void;
11
+ export declare const onNavigate: () => void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Mock for $app/navigation used in vitest tests
3
+ */
4
+ export const goto = () => Promise.resolve();
5
+ export const invalidate = () => Promise.resolve();
6
+ export const invalidateAll = () => Promise.resolve();
7
+ export const prefetch = () => Promise.resolve();
8
+ export const prefetchRoutes = () => Promise.resolve();
9
+ export const beforeNavigate = () => { };
10
+ export const afterNavigate = () => { };
11
+ export const onNavigate = () => { };
@@ -0,0 +1,16 @@
1
+ export declare const page: import("svelte/store").Readable<{
2
+ url: URL;
3
+ params: {};
4
+ route: {
5
+ id: null;
6
+ };
7
+ status: number;
8
+ error: null;
9
+ data: {};
10
+ form: null;
11
+ }>;
12
+ export declare const navigating: import("svelte/store").Readable<null>;
13
+ export declare const updated: {
14
+ subscribe: (this: void, run: import("svelte/store").Subscriber<boolean>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
15
+ check: () => Promise<boolean>;
16
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Mock for $app/stores used in vitest tests
3
+ */
4
+ import { readable } from 'svelte/store';
5
+ export const page = readable({
6
+ url: new URL('http://localhost'),
7
+ params: {},
8
+ route: { id: null },
9
+ status: 200,
10
+ error: null,
11
+ data: {},
12
+ form: null
13
+ });
14
+ export const navigating = readable(null);
15
+ export const updated = {
16
+ subscribe: readable(false).subscribe,
17
+ check: () => Promise.resolve(false)
18
+ };
@@ -0,0 +1,2 @@
1
+ import type { ClassValue } from 'class-variance-authority/types';
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import clsx from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -0,0 +1 @@
1
+ export { cn } from './cn.js';
@@ -0,0 +1 @@
1
+ export { cn } from './cn.js';