@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,249 @@
1
+ import { setContext, getContext } from 'svelte';
2
+ import { createKeyboardNavigation } from '../../primitives/keyboard-navigation';
3
+ const KEY = Symbol('listbox');
4
+ export function createListBoxContext(options = {}) {
5
+ const { selectionMode = 'single', selectionBehavior = 'toggle', disabledIds: initialDisabledIds, initialSelection = new Set(), onSelectionChange } = options;
6
+ const disabledIds = new Set(initialDisabledIds ?? []);
7
+ const items = new Map();
8
+ let selectedKeys = new Set(initialSelection);
9
+ const itemCallbacks = new Map();
10
+ // Item count tracking with subscription
11
+ const itemCountCallbacks = new Set();
12
+ function notifyItemCountChange() {
13
+ const count = items.size;
14
+ itemCountCallbacks.forEach((cb) => cb(count));
15
+ }
16
+ function registerItem(id, textValue, element) {
17
+ items.set(id, { textValue, element });
18
+ notifyItemCountChange();
19
+ }
20
+ function unregisterItem(id) {
21
+ items.delete(id);
22
+ itemCallbacks.delete(id);
23
+ notifyItemCountChange();
24
+ }
25
+ function getItemCount() {
26
+ return items.size;
27
+ }
28
+ function subscribeToItemCount(callback) {
29
+ itemCountCallbacks.add(callback);
30
+ // Immediately call with current count
31
+ callback(items.size);
32
+ return () => {
33
+ itemCountCallbacks.delete(callback);
34
+ };
35
+ }
36
+ function getItemIds() {
37
+ return Array.from(items.keys());
38
+ }
39
+ function isDisabled(id) {
40
+ return disabledIds.has(id);
41
+ }
42
+ function isSelected(id) {
43
+ return selectedKeys.has(id);
44
+ }
45
+ function getSelectedKeys() {
46
+ return new Set(selectedKeys);
47
+ }
48
+ function notifyItem(id, selected) {
49
+ const callbacks = itemCallbacks.get(id);
50
+ if (callbacks) {
51
+ callbacks.forEach((cb) => cb(selected));
52
+ }
53
+ }
54
+ let focusedId = null;
55
+ const focusCallbacks = new Map();
56
+ function getFocusedId() {
57
+ return focusedId;
58
+ }
59
+ function isFocused(id) {
60
+ return focusedId === id || String(focusedId) === String(id);
61
+ }
62
+ function notifyFocus(id, focused) {
63
+ const callbacks = focusCallbacks.get(id);
64
+ if (callbacks) {
65
+ callbacks.forEach((cb) => cb(focused));
66
+ }
67
+ }
68
+ function setFocusedId(newId) {
69
+ if (focusedId === newId)
70
+ return;
71
+ const previousId = focusedId;
72
+ focusedId = newId;
73
+ if (previousId !== null) {
74
+ notifyFocus(previousId, false);
75
+ }
76
+ if (newId !== null) {
77
+ notifyFocus(newId, true);
78
+ }
79
+ }
80
+ function subscribeToFocus(id, callback) {
81
+ if (!focusCallbacks.has(id)) {
82
+ focusCallbacks.set(id, new Set());
83
+ }
84
+ focusCallbacks.get(id).add(callback);
85
+ callback(isFocused(id));
86
+ return () => {
87
+ const callbacks = focusCallbacks.get(id);
88
+ if (callbacks) {
89
+ callbacks.delete(callback);
90
+ if (callbacks.size === 0) {
91
+ focusCallbacks.delete(id);
92
+ }
93
+ }
94
+ };
95
+ }
96
+ function subscribeToItem(id, callback) {
97
+ if (!itemCallbacks.has(id)) {
98
+ itemCallbacks.set(id, new Set());
99
+ }
100
+ itemCallbacks.get(id).add(callback);
101
+ callback(selectedKeys.has(id));
102
+ return () => {
103
+ const callbacks = itemCallbacks.get(id);
104
+ if (callbacks) {
105
+ callbacks.delete(callback);
106
+ if (callbacks.size === 0) {
107
+ itemCallbacks.delete(id);
108
+ }
109
+ }
110
+ };
111
+ }
112
+ function select(id) {
113
+ if (isDisabled(id))
114
+ return;
115
+ const wasSelected = selectedKeys.has(id);
116
+ const previouslySelected = new Set(selectedKeys);
117
+ if (selectionMode === 'single') {
118
+ if (selectionBehavior === 'toggle') {
119
+ selectedKeys = wasSelected ? new Set() : new Set([id]);
120
+ }
121
+ else {
122
+ selectedKeys = new Set([id]);
123
+ }
124
+ }
125
+ else {
126
+ selectedKeys = new Set(selectedKeys);
127
+ if (selectionBehavior === 'toggle') {
128
+ if (wasSelected) {
129
+ selectedKeys.delete(id);
130
+ }
131
+ else {
132
+ selectedKeys.add(id);
133
+ }
134
+ }
135
+ else {
136
+ selectedKeys.add(id);
137
+ }
138
+ }
139
+ previouslySelected.forEach((prevId) => {
140
+ if (!selectedKeys.has(prevId)) {
141
+ notifyItem(prevId, false);
142
+ }
143
+ });
144
+ selectedKeys.forEach((newId) => {
145
+ if (!previouslySelected.has(newId)) {
146
+ notifyItem(newId, true);
147
+ }
148
+ });
149
+ onSelectionChange?.(new Set(selectedKeys));
150
+ }
151
+ function selectAll() {
152
+ if (selectionMode !== 'multiple')
153
+ return;
154
+ const enabledIds = getItemIds().filter((id) => !isDisabled(id));
155
+ const previouslySelected = new Set(selectedKeys);
156
+ selectedKeys = new Set(enabledIds);
157
+ enabledIds.forEach((id) => {
158
+ if (!previouslySelected.has(id)) {
159
+ notifyItem(id, true);
160
+ }
161
+ });
162
+ onSelectionChange?.(new Set(selectedKeys));
163
+ }
164
+ function setSelection(newSelection) {
165
+ const previouslySelected = new Set(selectedKeys);
166
+ selectedKeys = new Set(newSelection);
167
+ previouslySelected.forEach((prevId) => {
168
+ if (!selectedKeys.has(prevId)) {
169
+ notifyItem(prevId, false);
170
+ }
171
+ });
172
+ selectedKeys.forEach((newId) => {
173
+ if (!previouslySelected.has(newId)) {
174
+ notifyItem(newId, true);
175
+ }
176
+ });
177
+ }
178
+ // Navigation helper functions for virtual focus (used by ComboBox)
179
+ const loop = false; // Same as keyboardNav loop setting
180
+ function getNextItemId(currentId) {
181
+ const itemIdsList = getItemIds();
182
+ if (itemIdsList.length === 0)
183
+ return null;
184
+ const currentIndex = currentId !== null ? itemIdsList.indexOf(currentId) : -1;
185
+ if (currentIndex < itemIdsList.length - 1) {
186
+ return itemIdsList[currentIndex + 1];
187
+ }
188
+ // At end - loop or stay
189
+ return loop ? itemIdsList[0] : null;
190
+ }
191
+ function getPreviousItemId(currentId) {
192
+ const itemIdsList = getItemIds();
193
+ if (itemIdsList.length === 0)
194
+ return null;
195
+ const currentIndex = currentId !== null ? itemIdsList.indexOf(currentId) : itemIdsList.length;
196
+ if (currentIndex > 0) {
197
+ return itemIdsList[currentIndex - 1];
198
+ }
199
+ // At start - loop or stay
200
+ return loop ? itemIdsList[itemIdsList.length - 1] : null;
201
+ }
202
+ const keyboardNav = createKeyboardNavigation({
203
+ orientation: 'vertical',
204
+ loop: loop,
205
+ itemSelector: '[data-navigation-item]:not([data-disabled])',
206
+ onSelect: (id) => {
207
+ select(id);
208
+ },
209
+ onFocusChange: (id) => {
210
+ setFocusedId(id);
211
+ },
212
+ onSelectAll: selectionMode === 'multiple' ? selectAll : undefined,
213
+ homeEndKeys: true
214
+ });
215
+ const ctx = {
216
+ selectionMode,
217
+ selectionBehavior,
218
+ disabledIds,
219
+ getSelectedKeys,
220
+ getFocusedId,
221
+ isSelected,
222
+ isDisabled,
223
+ isFocused,
224
+ keyboardNav,
225
+ items,
226
+ registerItem,
227
+ unregisterItem,
228
+ getItemIds,
229
+ getItemCount,
230
+ subscribeToItemCount,
231
+ select,
232
+ selectAll,
233
+ setSelection,
234
+ setFocusedId,
235
+ subscribeToItem,
236
+ subscribeToFocus,
237
+ getNextItemId,
238
+ getPreviousItemId
239
+ };
240
+ setContext(KEY, ctx);
241
+ return ctx;
242
+ }
243
+ export function useListBoxContext() {
244
+ const ctx = getContext(KEY);
245
+ if (!ctx) {
246
+ throw new Error('useListBoxContext must be used within a ListBox component');
247
+ }
248
+ return ctx;
249
+ }
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import ListBox from '../index';
3
+
4
+ let selected = $state<string | number | undefined>();
5
+
6
+ function handleChange(value: Set<string | number>) {
7
+ selected = Array.from(value)[0];
8
+ }
9
+ </script>
10
+
11
+ <ListBox.Root aria-label="Codes list" onChange={handleChange}>
12
+ <ListBox.Item id="01">Code 01</ListBox.Item>
13
+ <ListBox.Item id="02">Code 02</ListBox.Item>
14
+ <ListBox.Item id={3}>Code 3</ListBox.Item>
15
+ </ListBox.Root>
16
+
17
+ <div data-testid="selected">{selected === undefined ? '' : String(selected)}</div>
18
+ <div data-testid="selected-type">{selected === undefined ? '' : typeof selected}</div>
@@ -0,0 +1,3 @@
1
+ declare const ListboxNumericIdTest: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ListboxNumericIdTest = ReturnType<typeof ListboxNumericIdTest>;
3
+ export default ListboxNumericIdTest;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import ListBox from '../index';
3
+
4
+ type Props = {
5
+ selectionMode?: 'single' | 'multiple';
6
+ selectionBehavior?: 'toggle' | 'replace';
7
+ disabledIds?: Iterable<string | number>;
8
+ };
9
+
10
+ let { selectionMode = 'single', selectionBehavior = 'toggle', disabledIds }: Props = $props();
11
+
12
+ const fruits = [
13
+ { id: 'apple', name: 'Apple' },
14
+ { id: 'banana', name: 'Banana' },
15
+ { id: 'cherry', name: 'Cherry' },
16
+ { id: 'grape', name: 'Grape' },
17
+ { id: 'orange', name: 'Orange' }
18
+ ];
19
+ </script>
20
+
21
+ <ListBox.Root {selectionMode} {selectionBehavior} {disabledIds} aria-label="Fruits list">
22
+ {#each fruits as fruit (fruit.id)}
23
+ <ListBox.Item id={fruit.id} textValue={fruit.name}>
24
+ {fruit.name}
25
+ </ListBox.Item>
26
+ {/each}
27
+ </ListBox.Root>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ selectionMode?: 'single' | 'multiple';
3
+ selectionBehavior?: 'toggle' | 'replace';
4
+ disabledIds?: Iterable<string | number>;
5
+ };
6
+ declare const ListboxTest: import("svelte").Component<Props, {}, "">;
7
+ type ListboxTest = ReturnType<typeof ListboxTest>;
8
+ export default ListboxTest;
@@ -0,0 +1,146 @@
1
+ <script lang="ts" generics="T extends object = object">
2
+ import type { Snippet } from 'svelte';
3
+ import { createListBoxContext, type ListBoxContext } from './context';
4
+
5
+ /**
6
+ * Props for the ListBox component.
7
+ * @template T - The type of items when using dynamic rendering with the `items` prop.
8
+ */
9
+ type ListBoxProps = {
10
+ /** How selection behaves: 'toggle' allows deselection, 'replace' always selects. */
11
+ selectionBehavior?: 'toggle' | 'replace';
12
+ /** Content shown when the list is empty. Can be a string or a Snippet. */
13
+ emptyPlaceholder?: string | Snippet;
14
+ /** Iterable of items for dynamic rendering. Used with a snippet that receives each item. */
15
+ items?: Iterable<T>;
16
+ /** IDs of items that should be disabled and non-selectable. */
17
+ disabledIds?: Iterable<string | number>;
18
+ /** Selection mode: 'single' allows one selection, 'multiple' allows many. */
19
+ selectionMode?: 'single' | 'multiple';
20
+ /** Controlled value. When provided, the component is in controlled mode. */
21
+ value?: Iterable<string | number>;
22
+ /** Initial selection for uncontrolled mode. */
23
+ defaultValue?: Iterable<string | number>;
24
+ /** Content of the listbox. Can be static children or a snippet receiving items. */
25
+ children?: Snippet | Snippet<[T]>;
26
+ /** CSS class to apply to the listbox container. */
27
+ class?: string;
28
+ /** HTML id attribute for the listbox element. */
29
+ id?: string;
30
+ /** Accessible label for the listbox. Announced by screen readers. */
31
+ 'aria-label'?: string;
32
+ /** Callback fired when the selection changes. */
33
+ onChange?: (value: Set<string | number>) => void;
34
+ };
35
+
36
+ let {
37
+ selectionBehavior = 'toggle',
38
+ emptyPlaceholder = 'No items selected',
39
+ items,
40
+ disabledIds,
41
+ selectionMode = 'single',
42
+ value = $bindable(),
43
+ defaultValue,
44
+ children,
45
+ class: className = '',
46
+ id,
47
+ 'aria-label': ariaLabel,
48
+ onChange,
49
+ context = $bindable(),
50
+ element = $bindable()
51
+ }: ListBoxProps & { context?: ListBoxContext; element?: HTMLElement } = $props();
52
+
53
+ let listboxElement: HTMLElement;
54
+
55
+ // Expose element via bindable prop
56
+ $effect(() => {
57
+ element = listboxElement;
58
+ });
59
+
60
+ function parseSelection(val: Iterable<string | number> | undefined): Set<string | number> {
61
+ if (val === undefined) return new Set();
62
+ return new Set(val);
63
+ }
64
+
65
+ const isControlled = $derived(value !== undefined);
66
+
67
+ const ctx = createListBoxContext({
68
+ get selectionMode() {
69
+ return selectionMode;
70
+ },
71
+ get selectionBehavior() {
72
+ return selectionBehavior;
73
+ },
74
+ get disabledIds() {
75
+ return disabledIds;
76
+ },
77
+ // Use function to capture initial value only (not reactive)
78
+ initialSelection: (() => parseSelection(defaultValue))(),
79
+ onSelectionChange: (newSelection) => {
80
+ onChange?.(newSelection);
81
+ }
82
+ });
83
+
84
+ // Expose context via bindable prop
85
+ context = ctx;
86
+
87
+ const { action: keyboardAction } = ctx.keyboardNav;
88
+
89
+ $effect(() => {
90
+ if (isControlled && value !== undefined) {
91
+ const newSelection = parseSelection(value);
92
+ ctx.setSelection(newSelection);
93
+ }
94
+ });
95
+
96
+ $effect(() => {
97
+ ctx.disabledIds.clear();
98
+ if (disabledIds) {
99
+ for (const id of disabledIds) {
100
+ ctx.disabledIds.add(id);
101
+ }
102
+ }
103
+ });
104
+
105
+ let itemCount = $state(0);
106
+
107
+ // Subscribe to item count changes for reactive empty state
108
+ $effect(() => {
109
+ const unsubscribe = ctx.subscribeToItemCount((count) => {
110
+ itemCount = count;
111
+ });
112
+ return unsubscribe;
113
+ });
114
+
115
+ const itemsArray = $derived(items ? Array.from(items) : []);
116
+ const hasItems = $derived(itemsArray.length > 0 || itemCount > 0);
117
+ </script>
118
+
119
+ <div
120
+ bind:this={listboxElement}
121
+ role="listbox"
122
+ {id}
123
+ aria-multiselectable={selectionMode === 'multiple'}
124
+ aria-label={ariaLabel}
125
+ class={className}
126
+ tabindex="0"
127
+ use:keyboardAction
128
+ >
129
+ {#if items && children}
130
+ {#each itemsArray as item (item)}
131
+ {@render (children as Snippet<[T]>)(item)}
132
+ {/each}
133
+ {:else if children}
134
+ {@render (children as Snippet)()}
135
+ {/if}
136
+
137
+ {#if !hasItems && itemCount === 0}
138
+ {#if typeof emptyPlaceholder === 'string'}
139
+ <div role="option" aria-selected="false" aria-disabled="true" data-empty-placeholder>
140
+ {emptyPlaceholder}
141
+ </div>
142
+ {:else if emptyPlaceholder}
143
+ {@render emptyPlaceholder()}
144
+ {/if}
145
+ {/if}
146
+ </div>
@@ -0,0 +1,54 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type ListBoxContext } from './context';
3
+ declare function $$render<T extends object = object>(): {
4
+ props: {
5
+ /** How selection behaves: 'toggle' allows deselection, 'replace' always selects. */
6
+ selectionBehavior?: "toggle" | "replace";
7
+ /** Content shown when the list is empty. Can be a string or a Snippet. */
8
+ emptyPlaceholder?: string | Snippet;
9
+ /** Iterable of items for dynamic rendering. Used with a snippet that receives each item. */
10
+ items?: Iterable<T>;
11
+ /** IDs of items that should be disabled and non-selectable. */
12
+ disabledIds?: Iterable<string | number>;
13
+ /** Selection mode: 'single' allows one selection, 'multiple' allows many. */
14
+ selectionMode?: "single" | "multiple";
15
+ /** Controlled value. When provided, the component is in controlled mode. */
16
+ value?: Iterable<string | number>;
17
+ /** Initial selection for uncontrolled mode. */
18
+ defaultValue?: Iterable<string | number>;
19
+ /** Content of the listbox. Can be static children or a snippet receiving items. */
20
+ children?: Snippet | Snippet<[T]>;
21
+ /** CSS class to apply to the listbox container. */
22
+ class?: string;
23
+ /** HTML id attribute for the listbox element. */
24
+ id?: string;
25
+ /** Accessible label for the listbox. Announced by screen readers. */
26
+ 'aria-label'?: string;
27
+ /** Callback fired when the selection changes. */
28
+ onChange?: (value: Set<string | number>) => void;
29
+ } & {
30
+ context?: ListBoxContext;
31
+ element?: HTMLElement;
32
+ };
33
+ exports: {};
34
+ bindings: "value" | "context" | "element";
35
+ slots: {};
36
+ events: {};
37
+ };
38
+ declare class __sveltets_Render<T extends object = object> {
39
+ props(): ReturnType<typeof $$render<T>>['props'];
40
+ events(): ReturnType<typeof $$render<T>>['events'];
41
+ slots(): ReturnType<typeof $$render<T>>['slots'];
42
+ bindings(): "value" | "context" | "element";
43
+ exports(): {};
44
+ }
45
+ interface $$IsomorphicComponent {
46
+ new <T extends object = object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
47
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
48
+ } & ReturnType<__sveltets_Render<T>['exports']>;
49
+ <T extends object = object>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
50
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
51
+ }
52
+ declare const Listbox: $$IsomorphicComponent;
53
+ type Listbox<T extends object = object> = InstanceType<typeof Listbox<T>>;
54
+ export default Listbox;
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { Popover } from '../index';
3
+
4
+ type Props = {
5
+ open?: boolean;
6
+ defaultOpen?: boolean;
7
+ isNonModal?: boolean;
8
+ shouldCloseOnInteractOutside?: boolean;
9
+ shouldCloseOnEscape?: boolean;
10
+ shouldCloseOnBlur?: boolean;
11
+ onOpenChange?: (open: boolean) => void;
12
+ };
13
+
14
+ let {
15
+ open,
16
+ defaultOpen = false,
17
+ isNonModal = false,
18
+ shouldCloseOnInteractOutside = true,
19
+ shouldCloseOnEscape = true,
20
+ shouldCloseOnBlur,
21
+ onOpenChange
22
+ }: Props = $props();
23
+ </script>
24
+
25
+ <Popover.Root {open} {defaultOpen} {onOpenChange}>
26
+ <Popover.Trigger>
27
+ <button type="button">Open Popover</button>
28
+ </Popover.Trigger>
29
+
30
+ <Popover.Content
31
+ class="popover-content"
32
+ {isNonModal}
33
+ {shouldCloseOnInteractOutside}
34
+ {shouldCloseOnEscape}
35
+ {shouldCloseOnBlur}
36
+ >
37
+ <div class="popover-body">
38
+ <h3>Popover Title</h3>
39
+ <p>Popover content goes here.</p>
40
+ <button type="button" class="close-btn">Close</button>
41
+ </div>
42
+ </Popover.Content>
43
+ </Popover.Root>
@@ -0,0 +1,12 @@
1
+ type Props = {
2
+ open?: boolean;
3
+ defaultOpen?: boolean;
4
+ isNonModal?: boolean;
5
+ shouldCloseOnInteractOutside?: boolean;
6
+ shouldCloseOnEscape?: boolean;
7
+ shouldCloseOnBlur?: boolean;
8
+ onOpenChange?: (open: boolean) => void;
9
+ };
10
+ declare const PopoverContentTest: import("svelte").Component<Props, {}, "">;
11
+ type PopoverContentTest = ReturnType<typeof PopoverContentTest>;
12
+ export default PopoverContentTest;