@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,605 +1,488 @@
1
- <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
3
- import { untrack } from "svelte";
4
- import {
5
- createVirtualizer,
6
- type SvelteVirtualizer,
7
- } from "@tanstack/svelte-virtual";
8
- import {
9
- registerOverlay,
10
- } from "../OverlayStack";
11
- import Portal from "../Portal/Portal.svelte";
12
- import type { BoxerItem, BoxerProps } from "./types";
13
- import { createBoxerStore } from "./store";
14
- import BoxerTarget from "./BoxerTarget.svelte";
15
-
16
- let {
17
- adapter,
18
- clearable = true,
19
- data = [],
20
- dataSource: dataSourceProp = undefined,
21
- debounceMs = 300,
22
- disablePortal = false,
23
- disabled,
24
- error,
25
- label,
26
- leftSection,
27
- mah = 200,
28
- multiSelect,
29
- onAPICall: onAPICallProp = undefined,
30
- onBufferChange,
31
- onChange,
32
- onResolveValues: onResolveValuesProp = undefined,
33
- openOnClear,
34
- pageSize = 50,
35
- placeholder,
36
- rightSection,
37
- searchColumns,
38
- selectFirst,
39
- showAll,
40
- value = $bindable<any>(undefined),
41
- }: BoxerProps = $props();
42
-
43
- const instanceId = `boxer-${Math.random().toString(36).slice(2, 10)}`;
44
- const inputId = `${instanceId}-input`;
45
- const listboxId = `${instanceId}-listbox`;
46
-
47
- // Derive effective dataSource, onAPICall, resolver from adapter when not explicitly provided.
48
- const dataSource = dataSourceProp ?? (adapter ? "server" : "local");
49
- const onAPICall =
50
- onAPICallProp ??
51
- (adapter
52
- ? (params: {
53
- page: number;
54
- pageSize: number;
55
- search?: string;
56
- signal?: AbortSignal;
57
- }) => adapter!.fetch(params)
58
- : undefined);
59
- const onResolveValues =
60
- onResolveValuesProp ??
61
- (adapter?.resolveByValue
62
- ? (values: any[], signal?: AbortSignal) =>
63
- adapter!.resolveByValue!(values, signal)
64
- : undefined);
65
-
66
- // Create store once with initial props
67
- const store = createBoxerStore({
68
- clearable,
69
- data,
70
- dataSource,
71
- disabled,
72
- error,
73
- label,
74
- mah,
75
- multiSelect,
76
- onAPICall,
77
- onBufferChange,
78
- onChange,
79
- onResolveValues,
80
- openOnClear,
81
- pageSize,
82
- placeholder,
83
- searchColumns,
84
- selectFirst,
85
- showAll,
86
- value,
87
- });
88
-
89
- // Fine-grained derived values so $effect blocks only re-run when the
90
- // specific field they care about actually changes (not on every store update).
91
- const storeSearch = $derived($store.search);
92
- const storeOpened = $derived($store.opened);
93
- const storeBoxerData = $derived($store.boxerData);
94
- const storeSelectedItems = $derived($store.selectedItems);
95
- const storeInput = $derived($store.input);
96
- const boxerDataLength = $derived($store.boxerData.length);
97
-
98
- // Sync value prop changes into store
99
- $effect(() => {
100
- store.updateProps({ value });
101
- });
102
-
103
- // Virtualizer
104
- let parentEl = $state<HTMLDivElement | undefined>(undefined);
105
- let targetRef = $state<{ focus: () => void } | undefined>(undefined);
106
- let anchorEl = $state<HTMLDivElement | undefined>(undefined);
107
- let dropdownEl = $state<HTMLDivElement | undefined>(undefined);
108
- let popupTop = $state(0);
109
- let popupLeft = $state(0);
110
- let popupWidth = $state(0);
111
- let activeOptionIndex = $state<number | null>(null);
112
- let backdropZ = $state(1090);
113
- let dropdownZ = $state(1100);
114
- let pointerInteractingWithDropdown = $state(false);
115
- let insideDialog = $state(false);
116
- let suppressOpenOnFocus = $state(false);
117
- const effectiveDisablePortal = $derived(disablePortal || insideDialog);
118
- // Plain variable NOT $state to avoid deep proxy on the complex virtualizer object.
119
- let rawVirtualizer: SvelteVirtualizer<
120
- HTMLDivElement,
121
- HTMLDivElement
122
- > | null = null;
123
- // Virtualizer outputs as $state — updated directly by the subscribe callback.
124
- let virtualItems = $state.raw<
125
- ReturnType<
126
- SvelteVirtualizer<HTMLDivElement, HTMLDivElement>["getVirtualItems"]
127
- >
128
- >([]);
129
- let totalSize = $state(0);
130
-
131
- // Create virtualizer only when parentEl changes (dropdown mount/unmount).
132
- $effect(() => {
133
- if (!parentEl) {
134
- rawVirtualizer = null;
135
- virtualItems = [];
136
- totalSize = 0;
137
- return;
138
- }
139
- const initialCount = untrack(() => boxerDataLength);
140
- const v = createVirtualizer<HTMLDivElement, HTMLDivElement>({
141
- count: initialCount,
142
- estimateSize: () => 36,
143
- getScrollElement: () => parentEl!,
144
- });
145
- return v.subscribe((instance) => {
146
- rawVirtualizer = instance;
147
- virtualItems = instance.getVirtualItems();
148
- totalSize = instance.getTotalSize();
149
- });
150
- });
151
-
152
- // Update virtualizer count when data length actually changes (preserves scroll).
153
- $effect(() => {
154
- const count = boxerDataLength;
155
- if (rawVirtualizer && parentEl) {
156
- rawVirtualizer.setOptions({
157
- count,
158
- estimateSize: () => 36,
159
- getScrollElement: () => parentEl!,
160
- });
161
- }
162
- });
163
-
164
- // Debounced search only reacts to actual search text changes.
165
- let searchTimeout: ReturnType<typeof setTimeout> | undefined;
166
- let didInitialFetch = false;
167
-
168
- $effect(() => {
169
- const search = storeSearch;
170
- // Skip the initial tick: the initial-fetch effect below handles first load.
171
- if (!didInitialFetch) return;
172
- clearTimeout(searchTimeout);
173
- searchTimeout = setTimeout(() => {
174
- if (search !== undefined && storeOpened) {
175
- store.fetchData(search, true);
176
- parentEl?.scrollTo?.({ top: 0 });
177
- }
178
- }, debounceMs);
179
- return () => clearTimeout(searchTimeout);
180
- });
181
-
182
- // Initial data fetch + resolve labels for preloaded value(s).
183
- $effect(() => {
184
- didInitialFetch = true;
185
- store.fetchData("", true);
186
- resolvePreloadedValue(value);
187
- return () => {
188
- clearTimeout(searchTimeout);
189
- store.cancel();
190
- };
191
- });
192
-
193
- let lastResolvedKey: string | null = null;
194
- async function resolvePreloadedValue(val: unknown) {
195
- if (val == null || val === "" || (Array.isArray(val) && val.length === 0)) return;
196
- if (!onResolveValues) return;
197
-
198
- const values = Array.isArray(val) ? val : [val];
199
- const key = JSON.stringify(values);
200
- if (key === lastResolvedKey) return;
201
- lastResolvedKey = key;
202
-
203
- try {
204
- const items = await onResolveValues(values);
205
- if (!items || items.length === 0) return;
206
- if (multiSelect) {
207
- store.setSelectedItems(items);
208
- } else {
209
- store.setSelectedItems(items.slice(0, 1));
210
- }
211
- } catch (err) {
212
- console.error("Boxer resolveByValue error:", err);
213
- }
214
- }
215
-
216
- // When value prop changes from outside, resolve any new labels we don't yet have.
217
- $effect(() => {
218
- void value;
219
- untrack(() => {
220
- if (value == null) {
221
- store.clearSelectedItems();
222
- return;
223
- }
224
- const knownKeys = new Set(
225
- $store.selectedItems.map((i) => JSON.stringify(i.value)),
226
- );
227
- const needed = (Array.isArray(value) ? value : [value]).filter(
228
- (v) => !knownKeys.has(JSON.stringify(v)),
229
- );
230
- if (needed.length === 0) {
231
- if (!multiSelect) {
232
- // Keep the single-item list in sync with the chosen value.
233
- const match = $store.selectedItems.find(
234
- (i) => JSON.stringify(i.value) === JSON.stringify(value),
235
- );
236
- if (match) store.setSelectedItems([match]);
237
- } else {
238
- // Drop any selected items whose value is no longer in `value`.
239
- const keep = new Set((value as any[]).map((v) => JSON.stringify(v)));
240
- const filtered = $store.selectedItems.filter((i) =>
241
- keep.has(JSON.stringify(i.value)),
242
- );
243
- if (filtered.length !== $store.selectedItems.length) {
244
- store.setSelectedItems(filtered);
245
- }
246
- }
247
- return;
248
- }
249
- // Fall back to boxerData, then to the resolver.
250
- const fromBoxer = $store.boxerData.filter((i) =>
251
- needed.some((v) => JSON.stringify(v) === JSON.stringify(i.value)),
252
- );
253
- fromBoxer.forEach((i) => store.addSelectedItem(i));
254
- const stillMissing = needed.filter(
255
- (v) => !fromBoxer.some((i) => JSON.stringify(i.value) === JSON.stringify(v)),
256
- );
257
- if (stillMissing.length > 0) resolvePreloadedValue(stillMissing);
258
- });
259
- });
260
-
261
- // Sync displayed input + emit onBufferChange from selectedItems (single source of truth).
262
- $effect(() => {
263
- const selected = storeSelectedItems;
264
- const opened = storeOpened;
265
- const currentInput = storeInput;
266
-
267
- if (multiSelect) {
268
- const labels = selected.map((i) => i.label).join(", ");
269
- if (!opened && currentInput !== labels) store.setInput(labels);
270
- onBufferChange?.(selected);
271
- } else {
272
- const item = selected[0] ?? null;
273
- if (item && currentInput !== item.label && !opened) {
274
- store.setInput(item.label);
275
- } else if (!item && !opened && currentInput !== "") {
276
- store.setInput("");
277
- }
278
- onBufferChange?.(item);
279
- }
280
- });
281
-
282
- // When data arrives, opportunistically enrich selectedItems for multiselect
283
- // (adds labels for items that just loaded).
284
- $effect(() => {
285
- const boxerData = storeBoxerData;
286
- if (!multiSelect || !Array.isArray(value) || value.length === 0) return;
287
- untrack(() => {
288
- const known = new Set(
289
- $store.selectedItems.map((i) => JSON.stringify(i.value)),
290
- );
291
- for (const v of value as any[]) {
292
- if (known.has(JSON.stringify(v))) continue;
293
- const match = boxerData.find(
294
- (i) => JSON.stringify(i.value) === JSON.stringify(v),
295
- );
296
- if (match) store.addSelectedItem(match);
297
- }
298
- });
299
- });
300
-
301
- // Select first
302
- $effect(() => {
303
- const boxerData = storeBoxerData;
304
- if (selectFirst && boxerData.length > 0 && !multiSelect && !value) {
305
- onOptionSubmit(0);
306
- }
307
- });
308
-
309
- function onOptionSubmit(index: number) {
310
- const option = $store.boxerData?.[index];
311
- if (!option) return;
312
-
313
- activeOptionIndex = index;
314
-
315
- if (multiSelect) {
316
- const currentValues = Array.isArray(value) ? value : [];
317
- const isSelected = currentValues.includes(option.value);
318
- if (isSelected) {
319
- value = currentValues.filter((v: unknown) => v !== option.value);
320
- store.removeSelectedItem(option.value);
321
- } else {
322
- value = [...currentValues, option.value];
323
- store.addSelectedItem(option);
324
- }
325
- onChange?.(value);
326
- } else {
327
- value = option.value;
328
- store.setSelectedItems([option]);
329
- onChange?.(option.value);
330
- store.setSearch("");
331
- store.setInput(option.label);
332
- store.setOpened(false);
333
- activeOptionIndex = null;
334
- }
335
- }
336
-
337
- function onClear() {
338
- if (showAll && selectFirst) {
339
- onOptionSubmit(0);
340
- } else {
341
- value = multiSelect ? [] : null;
342
- store.clearSelectedItems();
343
- onChange?.(value);
344
- store.setSearch("");
345
- store.setInput("");
346
- activeOptionIndex = null;
347
- targetRef?.focus();
348
- }
349
- if (openOnClear) {
350
- store.setOpened(true);
351
- }
352
- }
353
-
354
- function focusDropdownItem(index: number) {
355
- const length = $store.boxerData.length;
356
- if (length === 0) return;
357
- const clamped = Math.max(0, Math.min(index, length - 1));
358
- activeOptionIndex = clamped;
359
- rawVirtualizer?.scrollToIndex(clamped, { align: "auto" });
360
- requestAnimationFrame(() => {
361
- const el = parentEl?.querySelector<HTMLDivElement>(
362
- `[data-index="${clamped}"]`,
363
- );
364
- if (el) {
365
- el.scrollIntoView({ block: "nearest" });
366
- el.focus();
367
- }
368
- });
369
- }
370
-
371
- function onInputKeydown(e: KeyboardEvent) {
372
- switch (e.key) {
373
- case "ArrowDown":
374
- e.preventDefault();
375
- if (!$store.opened) store.setOpened(true);
376
- requestAnimationFrame(() => {
377
- if ($store.boxerData.length > 0) focusDropdownItem(0);
378
- });
379
- break;
380
- case "Enter":
381
- e.preventDefault();
382
- if (!$store.opened) {
383
- store.setOpened(true);
384
- } else if (!multiSelect) {
385
- const selectedIndex = $store.boxerData.findIndex(
386
- (item: BoxerItem) => item.value === value,
387
- );
388
- if (selectedIndex >= 0) {
389
- store.setOpened(false);
390
- activeOptionIndex = null;
391
- } else {
392
- onOptionSubmit(0);
393
- }
394
- } else {
395
- store.setOpened(false);
396
- activeOptionIndex = null;
397
- }
398
- break;
399
- case "Escape":
400
- if ($store.opened) {
401
- e.preventDefault();
402
- store.setOpened(false);
403
- activeOptionIndex = null;
404
- }
405
- break;
406
- case "Tab":
407
- if (multiSelect && $store.opened && $store.boxerData.length > 0) {
408
- e.preventDefault();
409
- onOptionSubmit(0);
410
- }
411
- break;
412
- }
413
- }
414
-
415
- function onDropdownItemKeydown(e: KeyboardEvent, index: number) {
416
- const length = $store.boxerData.length;
417
- const pageSizeNav = Math.max(1, Math.floor(mah / 36));
418
-
419
- switch (e.key) {
420
- case "ArrowDown":
421
- e.preventDefault();
422
- if (index < length - 1) focusDropdownItem(index + 1);
423
- break;
424
- case "ArrowUp":
425
- e.preventDefault();
426
- if (index === 0) {
427
- activeOptionIndex = null;
428
- targetRef?.focus();
429
- } else {
430
- focusDropdownItem(index - 1);
431
- }
432
- break;
433
- case "Home":
434
- e.preventDefault();
435
- focusDropdownItem(0);
436
- break;
437
- case "End":
438
- e.preventDefault();
439
- focusDropdownItem(length - 1);
440
- break;
441
- case "PageDown":
442
- e.preventDefault();
443
- focusDropdownItem(Math.min(length - 1, index + pageSizeNav));
444
- break;
445
- case "PageUp":
446
- e.preventDefault();
447
- focusDropdownItem(Math.max(0, index - pageSizeNav));
448
- break;
449
- case "Enter":
450
- case " ":
451
- e.preventDefault();
452
- onOptionSubmit(index);
453
- if (!multiSelect) targetRef?.focus();
454
- break;
455
- case "Escape":
456
- e.preventDefault();
457
- store.setOpened(false);
458
- activeOptionIndex = null;
459
- suppressOpenOnFocus = true;
460
- targetRef?.focus();
461
- break;
462
- }
463
- }
464
-
465
- function updatePopoverPosition() {
466
- if (!anchorEl) return;
467
- const rect = anchorEl.getBoundingClientRect();
468
- const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
469
- const horizontalPadding = 8;
470
- const maxWidth = Math.max(160, viewportWidth - horizontalPadding * 2);
471
- const nextWidth = Math.min(Math.max(220, rect.width), maxWidth);
472
- const maxLeft = viewportWidth - nextWidth - horizontalPadding;
473
-
474
- popupTop = rect.bottom + 4;
475
- popupWidth = nextWidth;
476
- popupLeft = Math.min(
477
- Math.max(rect.left, horizontalPadding),
478
- Math.max(horizontalPadding, maxLeft),
479
- );
480
- }
481
-
482
- function markDropdownPointerInteraction() {
483
- pointerInteractingWithDropdown = true;
484
- setTimeout(() => {
485
- pointerInteractingWithDropdown = false;
486
- }, 0);
487
- }
488
-
489
- function onBackdropPointerDown(e: PointerEvent) {
490
- // Consume the pointer sequence so it cannot activate elements behind the backdrop.
491
- e.preventDefault();
492
- e.stopPropagation();
493
- store.setOpened(false);
494
- }
495
-
496
- function onOptionPointerDown(e: PointerEvent, index: number) {
497
- // Commit selection before blur/focusout closes the popover on some browsers.
498
- e.preventDefault();
499
- e.stopPropagation();
500
- onOptionSubmit(index);
501
- }
502
-
503
- function syncInsideDialogFlag() {
504
- insideDialog = !!anchorEl?.closest('dialog, [role="dialog"]');
505
- }
506
-
507
- // Public API via bind:this
508
- export function clear() {
509
- onClear();
510
- }
511
- export function close() {
512
- store.setOpened(false);
513
- activeOptionIndex = null;
514
- }
515
- export function focus() {
516
- suppressOpenOnFocus = false;
517
- targetRef?.focus();
518
- }
519
- export function getValue() {
520
- return value;
521
- }
522
- export function open() {
523
- if (disabled) return;
524
- store.setOpened(true);
525
- activeOptionIndex = null;
526
- }
527
- export function refetch() {
528
- return store.refetch();
529
- }
530
- export function setValue(val: unknown) {
531
- value = val;
532
- onChange?.(val);
533
- }
534
-
535
- $effect(() => {
536
- if (!$store.opened || effectiveDisablePortal || disabled) {
537
- return;
538
- }
539
-
540
- updatePopoverPosition();
541
-
542
- if (typeof window === "undefined") {
543
- return;
544
- }
545
-
546
- const onWindowChange = () => {
547
- updatePopoverPosition();
548
- };
549
-
550
- window.addEventListener("resize", onWindowChange);
551
- window.addEventListener("scroll", onWindowChange, true);
552
-
553
- return () => {
554
- window.removeEventListener("resize", onWindowChange);
555
- window.removeEventListener("scroll", onWindowChange, true);
556
- };
557
- });
558
-
559
- $effect(() => {
560
- if (!$store.opened || disabled) {
561
- return;
562
- }
563
-
564
- const overlay = registerOverlay({
565
- kind: "popover",
566
- mount: effectiveDisablePortal ? "inline" : "portal",
567
- });
568
- backdropZ = overlay.layer.backdrop;
569
- dropdownZ = overlay.layer.content;
570
-
571
- return () => {
572
- overlay.unregister();
573
- backdropZ = 1090;
574
- dropdownZ = 1100;
575
- };
576
- });
577
-
578
- $effect(() => {
579
- void anchorEl;
580
- void storeOpened;
581
- syncInsideDialogFlag();
582
- });
583
-
584
- $effect(() => {
585
- if (!$store.opened || !effectiveDisablePortal || disabled || typeof window === "undefined") {
586
- return;
587
- }
588
-
589
- const onDocumentPointerDown = (event: PointerEvent) => {
590
- const target = event.target as Node | null;
591
- if (!target) return;
592
-
593
- const insideAnchor = !!anchorEl?.contains(target);
594
- const insideDropdown = !!dropdownEl?.contains(target);
595
- if (!insideAnchor && !insideDropdown) {
596
- store.setOpened(false);
597
- }
598
- };
599
-
600
- window.addEventListener("pointerdown", onDocumentPointerDown, true);
601
- return () => window.removeEventListener("pointerdown", onDocumentPointerDown, true);
602
- });
1
+ <script lang="ts">/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
2
+ import { untrack } from "svelte";
3
+ import { createVirtualizer } from "@tanstack/svelte-virtual";
4
+ import { registerOverlay } from "../OverlayStack";
5
+ import Portal from "../Portal/Portal.svelte";
6
+ import { createBoxerStore } from "./store";
7
+ import BoxerTarget from "./BoxerTarget.svelte";
8
+ let { adapter, clearable = true, data = [], dataSource: dataSourceProp = undefined, debounceMs = 300, disablePortal = false, disabled, error, id, label, leftSection, mah = 200, multiSelect, name, onAPICall: onAPICallProp = undefined, onBufferChange, onChange, onResolveValues: onResolveValuesProp = undefined, openOnClear, pageSize = 50, placeholder, rightSection, searchColumns, selectFirst, showAll, value = $bindable(undefined) } = $props();
9
+ const instanceId = `boxer-${Math.random().toString(36).slice(2, 10)}`;
10
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
11
+ // svelte-ignore state_referenced_locally -- id is intentionally captured once for a stable inputId
12
+ const inputId = id ?? `${instanceId}-input`;
13
+ const listboxId = `${instanceId}-listbox`;
14
+ // Derive effective dataSource, onAPICall, resolver from adapter when not explicitly provided.
15
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
16
+ // svelte-ignore state_referenced_locally -- adapter/dataSourceProp/onAPICallProp/onResolveValuesProp captured once at mount
17
+ const dataSource = dataSourceProp ?? (adapter ? "server" : "local");
18
+ // svelte-ignore state_referenced_locally
19
+ const onAPICall = onAPICallProp ?? (adapter ? (params) => adapter.fetch(params) : undefined);
20
+ // svelte-ignore state_referenced_locally
21
+ const onResolveValues = onResolveValuesProp ?? (adapter?.resolveByValue ? (values, signal) => adapter.resolveByValue(values, signal) : undefined);
22
+ // Create store once with initial props
23
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
24
+ // svelte-ignore state_referenced_locally -- all props captured at mount for store initialisation
25
+ const store = createBoxerStore({
26
+ clearable,
27
+ data,
28
+ dataSource,
29
+ disabled,
30
+ error,
31
+ label,
32
+ mah,
33
+ multiSelect,
34
+ onAPICall,
35
+ onBufferChange,
36
+ onChange,
37
+ onResolveValues,
38
+ openOnClear,
39
+ pageSize,
40
+ placeholder,
41
+ searchColumns,
42
+ selectFirst,
43
+ showAll,
44
+ value
45
+ });
46
+ // Fine-grained derived values so $effect blocks only re-run when the
47
+ // specific field they care about actually changes (not on every store update).
48
+ const storeSearch = $derived($store.search);
49
+ const storeOpened = $derived($store.opened);
50
+ const storeBoxerData = $derived($store.boxerData);
51
+ const storeSelectedItems = $derived($store.selectedItems);
52
+ const storeInput = $derived($store.input);
53
+ const boxerDataLength = $derived($store.boxerData.length);
54
+ // Sync value prop changes into store
55
+ $effect(() => {
56
+ store.updateProps({ value });
57
+ });
58
+ // Virtualizer
59
+ let parentEl = $state(undefined);
60
+ let targetRef = $state(undefined);
61
+ let anchorEl = $state(undefined);
62
+ let dropdownEl = $state(undefined);
63
+ let popupTop = $state(0);
64
+ let popupLeft = $state(0);
65
+ let popupWidth = $state(0);
66
+ let activeOptionIndex = $state(null);
67
+ let backdropZ = $state(1090);
68
+ let dropdownZ = $state(1100);
69
+ let pointerInteractingWithDropdown = $state(false);
70
+ let insideDialog = $state(false);
71
+ let suppressOpenOnFocus = $state(false);
72
+ const effectiveDisablePortal = $derived(disablePortal || insideDialog);
73
+ // Plain variable — NOT $state to avoid deep proxy on the complex virtualizer object.
74
+ let rawVirtualizer = null;
75
+ // Virtualizer outputs as $state — updated directly by the subscribe callback.
76
+ let virtualItems = $state.raw([]);
77
+ let totalSize = $state(0);
78
+ // Create virtualizer only when parentEl changes (dropdown mount/unmount).
79
+ $effect(() => {
80
+ if (!parentEl) {
81
+ rawVirtualizer = null;
82
+ virtualItems = [];
83
+ totalSize = 0;
84
+ return;
85
+ }
86
+ const initialCount = untrack(() => boxerDataLength);
87
+ const v = createVirtualizer({
88
+ count: initialCount,
89
+ estimateSize: () => 36,
90
+ getScrollElement: () => parentEl
91
+ });
92
+ return v.subscribe((instance) => {
93
+ rawVirtualizer = instance;
94
+ virtualItems = instance.getVirtualItems();
95
+ totalSize = instance.getTotalSize();
96
+ });
97
+ });
98
+ // Update virtualizer count when data length actually changes (preserves scroll).
99
+ $effect(() => {
100
+ const count = boxerDataLength;
101
+ if (rawVirtualizer && parentEl) {
102
+ rawVirtualizer.setOptions({
103
+ count,
104
+ estimateSize: () => 36,
105
+ getScrollElement: () => parentEl
106
+ });
107
+ }
108
+ });
109
+ // Debounced search — only reacts to actual search text changes.
110
+ let searchTimeout;
111
+ let didInitialFetch = false;
112
+ $effect(() => {
113
+ const search = storeSearch;
114
+ // Skip the initial tick: the initial-fetch effect below handles first load.
115
+ if (!didInitialFetch) return;
116
+ clearTimeout(searchTimeout);
117
+ searchTimeout = setTimeout(() => {
118
+ if (search !== undefined && storeOpened) {
119
+ store.fetchData(search, true);
120
+ parentEl?.scrollTo?.({ top: 0 });
121
+ }
122
+ }, debounceMs);
123
+ return () => clearTimeout(searchTimeout);
124
+ });
125
+ // Initial data fetch + resolve labels for preloaded value(s).
126
+ $effect(() => {
127
+ didInitialFetch = true;
128
+ store.fetchData("", true);
129
+ resolvePreloadedValue(value);
130
+ return () => {
131
+ clearTimeout(searchTimeout);
132
+ store.cancel();
133
+ };
134
+ });
135
+ let lastResolvedKey = null;
136
+ async function resolvePreloadedValue(val) {
137
+ if (val == null || val === "" || Array.isArray(val) && val.length === 0) return;
138
+ if (!onResolveValues) return;
139
+ const values = Array.isArray(val) ? val : [val];
140
+ const key = JSON.stringify(values);
141
+ if (key === lastResolvedKey) return;
142
+ lastResolvedKey = key;
143
+ try {
144
+ const items = await onResolveValues(values);
145
+ if (!items || items.length === 0) return;
146
+ if (multiSelect) {
147
+ store.setSelectedItems(items);
148
+ } else {
149
+ store.setSelectedItems(items.slice(0, 1));
150
+ }
151
+ } catch (err) {
152
+ console.error("Boxer resolveByValue error:", err);
153
+ }
154
+ }
155
+ // When value prop changes from outside, resolve any new labels we don't yet have.
156
+ $effect(() => {
157
+ void value;
158
+ untrack(() => {
159
+ if (value == null) {
160
+ store.clearSelectedItems();
161
+ return;
162
+ }
163
+ const knownKeys = new Set($store.selectedItems.map((i) => JSON.stringify(i.value)));
164
+ const needed = (Array.isArray(value) ? value : [value]).filter((v) => !knownKeys.has(JSON.stringify(v)));
165
+ if (needed.length === 0) {
166
+ if (!multiSelect) {
167
+ // Keep the single-item list in sync with the chosen value.
168
+ const match = $store.selectedItems.find((i) => JSON.stringify(i.value) === JSON.stringify(value));
169
+ if (match) store.setSelectedItems([match]);
170
+ } else {
171
+ // Drop any selected items whose value is no longer in `value`.
172
+ const keep = new Set(value.map((v) => JSON.stringify(v)));
173
+ const filtered = $store.selectedItems.filter((i) => keep.has(JSON.stringify(i.value)));
174
+ if (filtered.length !== $store.selectedItems.length) {
175
+ store.setSelectedItems(filtered);
176
+ }
177
+ }
178
+ return;
179
+ }
180
+ // Fall back to boxerData, then to the resolver.
181
+ const fromBoxer = $store.boxerData.filter((i) => needed.some((v) => JSON.stringify(v) === JSON.stringify(i.value)));
182
+ fromBoxer.forEach((i) => store.addSelectedItem(i));
183
+ const stillMissing = needed.filter((v) => !fromBoxer.some((i) => JSON.stringify(i.value) === JSON.stringify(v)));
184
+ if (stillMissing.length > 0) resolvePreloadedValue(stillMissing);
185
+ });
186
+ });
187
+ // Sync displayed input + emit onBufferChange from selectedItems (single source of truth).
188
+ $effect(() => {
189
+ const selected = storeSelectedItems;
190
+ const opened = storeOpened;
191
+ const currentInput = storeInput;
192
+ if (multiSelect) {
193
+ const labels = selected.map((i) => i.label).join(", ");
194
+ if (!opened && currentInput !== labels) store.setInput(labels);
195
+ onBufferChange?.(selected);
196
+ } else {
197
+ const item = selected[0] ?? null;
198
+ if (item && currentInput !== item.label && !opened) {
199
+ store.setInput(item.label);
200
+ } else if (!item && !opened && currentInput !== "") {
201
+ store.setInput("");
202
+ }
203
+ onBufferChange?.(item);
204
+ }
205
+ });
206
+ // When data arrives, opportunistically enrich selectedItems for multiselect
207
+ // (adds labels for items that just loaded).
208
+ $effect(() => {
209
+ const boxerData = storeBoxerData;
210
+ if (!multiSelect || !Array.isArray(value) || value.length === 0) return;
211
+ untrack(() => {
212
+ const known = new Set($store.selectedItems.map((i) => JSON.stringify(i.value)));
213
+ for (const v of value) {
214
+ if (known.has(JSON.stringify(v))) continue;
215
+ const match = boxerData.find((i) => JSON.stringify(i.value) === JSON.stringify(v));
216
+ if (match) store.addSelectedItem(match);
217
+ }
218
+ });
219
+ });
220
+ // Select first
221
+ $effect(() => {
222
+ const boxerData = storeBoxerData;
223
+ if (selectFirst && boxerData.length > 0 && !multiSelect && !value) {
224
+ onOptionSubmit(0);
225
+ }
226
+ });
227
+ function onOptionSubmit(index) {
228
+ const option = $store.boxerData?.[index];
229
+ if (!option) return;
230
+ activeOptionIndex = index;
231
+ if (multiSelect) {
232
+ const currentValues = Array.isArray(value) ? value : [];
233
+ const isSelected = currentValues.includes(option.value);
234
+ if (isSelected) {
235
+ value = currentValues.filter((v) => v !== option.value);
236
+ store.removeSelectedItem(option.value);
237
+ } else {
238
+ value = [...currentValues, option.value];
239
+ store.addSelectedItem(option);
240
+ }
241
+ onChange?.(value);
242
+ } else {
243
+ value = option.value;
244
+ store.setSelectedItems([option]);
245
+ onChange?.(option.value);
246
+ store.setSearch("");
247
+ store.setInput(option.label);
248
+ store.setOpened(false);
249
+ activeOptionIndex = null;
250
+ }
251
+ }
252
+ function onClear() {
253
+ if (showAll && selectFirst) {
254
+ onOptionSubmit(0);
255
+ } else {
256
+ value = multiSelect ? [] : null;
257
+ store.clearSelectedItems();
258
+ onChange?.(value);
259
+ store.setSearch("");
260
+ store.setInput("");
261
+ activeOptionIndex = null;
262
+ targetRef?.focus();
263
+ }
264
+ if (openOnClear) {
265
+ store.setOpened(true);
266
+ }
267
+ }
268
+ function focusDropdownItem(index) {
269
+ const length = $store.boxerData.length;
270
+ if (length === 0) return;
271
+ const clamped = Math.max(0, Math.min(index, length - 1));
272
+ activeOptionIndex = clamped;
273
+ rawVirtualizer?.scrollToIndex(clamped, { align: "auto" });
274
+ requestAnimationFrame(() => {
275
+ const el = parentEl?.querySelector(`[data-index="${clamped}"]`);
276
+ if (el) {
277
+ el.scrollIntoView({ block: "nearest" });
278
+ el.focus();
279
+ }
280
+ });
281
+ }
282
+ function onInputKeydown(e) {
283
+ switch (e.key) {
284
+ case "ArrowDown":
285
+ e.preventDefault();
286
+ if (!$store.opened) store.setOpened(true);
287
+ requestAnimationFrame(() => {
288
+ if ($store.boxerData.length > 0) focusDropdownItem(0);
289
+ });
290
+ break;
291
+ case "Enter":
292
+ e.preventDefault();
293
+ if (!$store.opened) {
294
+ store.setOpened(true);
295
+ } else if (!multiSelect) {
296
+ const selectedIndex = $store.boxerData.findIndex((item) => item.value === value);
297
+ if (selectedIndex >= 0) {
298
+ store.setOpened(false);
299
+ activeOptionIndex = null;
300
+ } else {
301
+ onOptionSubmit(0);
302
+ }
303
+ } else {
304
+ store.setOpened(false);
305
+ activeOptionIndex = null;
306
+ }
307
+ break;
308
+ case "Escape":
309
+ if ($store.opened) {
310
+ e.preventDefault();
311
+ store.setOpened(false);
312
+ activeOptionIndex = null;
313
+ }
314
+ break;
315
+ case "Tab":
316
+ if (multiSelect && $store.opened && $store.boxerData.length > 0) {
317
+ e.preventDefault();
318
+ onOptionSubmit(0);
319
+ }
320
+ break;
321
+ }
322
+ }
323
+ function onDropdownItemKeydown(e, index) {
324
+ const length = $store.boxerData.length;
325
+ const pageSizeNav = Math.max(1, Math.floor(mah / 36));
326
+ switch (e.key) {
327
+ case "ArrowDown":
328
+ e.preventDefault();
329
+ if (index < length - 1) focusDropdownItem(index + 1);
330
+ break;
331
+ case "ArrowUp":
332
+ e.preventDefault();
333
+ if (index === 0) {
334
+ activeOptionIndex = null;
335
+ targetRef?.focus();
336
+ } else {
337
+ focusDropdownItem(index - 1);
338
+ }
339
+ break;
340
+ case "Home":
341
+ e.preventDefault();
342
+ focusDropdownItem(0);
343
+ break;
344
+ case "End":
345
+ e.preventDefault();
346
+ focusDropdownItem(length - 1);
347
+ break;
348
+ case "PageDown":
349
+ e.preventDefault();
350
+ focusDropdownItem(Math.min(length - 1, index + pageSizeNav));
351
+ break;
352
+ case "PageUp":
353
+ e.preventDefault();
354
+ focusDropdownItem(Math.max(0, index - pageSizeNav));
355
+ break;
356
+ case "Enter":
357
+ case " ":
358
+ e.preventDefault();
359
+ onOptionSubmit(index);
360
+ if (!multiSelect) targetRef?.focus();
361
+ break;
362
+ case "Escape":
363
+ e.preventDefault();
364
+ store.setOpened(false);
365
+ activeOptionIndex = null;
366
+ suppressOpenOnFocus = true;
367
+ targetRef?.focus();
368
+ break;
369
+ }
370
+ }
371
+ function updatePopoverPosition() {
372
+ if (!anchorEl) return;
373
+ const rect = anchorEl.getBoundingClientRect();
374
+ const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
375
+ const horizontalPadding = 8;
376
+ const maxWidth = Math.max(160, viewportWidth - horizontalPadding * 2);
377
+ const nextWidth = Math.min(Math.max(220, rect.width), maxWidth);
378
+ const maxLeft = viewportWidth - nextWidth - horizontalPadding;
379
+ popupTop = rect.bottom + 4;
380
+ popupWidth = nextWidth;
381
+ popupLeft = Math.min(Math.max(rect.left, horizontalPadding), Math.max(horizontalPadding, maxLeft));
382
+ }
383
+ function markDropdownPointerInteraction() {
384
+ pointerInteractingWithDropdown = true;
385
+ setTimeout(() => {
386
+ pointerInteractingWithDropdown = false;
387
+ }, 0);
388
+ }
389
+ function onBackdropPointerDown(e) {
390
+ // Consume the pointer sequence so it cannot activate elements behind the backdrop.
391
+ e.preventDefault();
392
+ e.stopPropagation();
393
+ store.setOpened(false);
394
+ }
395
+ function onOptionPointerDown(e, index) {
396
+ // Commit selection before blur/focusout closes the popover on some browsers.
397
+ e.preventDefault();
398
+ e.stopPropagation();
399
+ onOptionSubmit(index);
400
+ }
401
+ function syncInsideDialogFlag() {
402
+ insideDialog = !!anchorEl?.closest("dialog, [role=\"dialog\"]");
403
+ }
404
+ // Public API via bind:this
405
+ export function clear() {
406
+ onClear();
407
+ }
408
+ export function close() {
409
+ store.setOpened(false);
410
+ activeOptionIndex = null;
411
+ }
412
+ export function focus() {
413
+ suppressOpenOnFocus = false;
414
+ targetRef?.focus();
415
+ }
416
+ export function getValue() {
417
+ return value;
418
+ }
419
+ export function open() {
420
+ if (disabled) return;
421
+ store.setOpened(true);
422
+ activeOptionIndex = null;
423
+ }
424
+ export function refetch() {
425
+ return store.refetch();
426
+ }
427
+ export function setValue(val) {
428
+ value = val;
429
+ onChange?.(val);
430
+ }
431
+ $effect(() => {
432
+ if (!$store.opened || effectiveDisablePortal || disabled) {
433
+ return;
434
+ }
435
+ updatePopoverPosition();
436
+ if (typeof window === "undefined") {
437
+ return;
438
+ }
439
+ const onWindowChange = () => {
440
+ updatePopoverPosition();
441
+ };
442
+ window.addEventListener("resize", onWindowChange);
443
+ window.addEventListener("scroll", onWindowChange, true);
444
+ return () => {
445
+ window.removeEventListener("resize", onWindowChange);
446
+ window.removeEventListener("scroll", onWindowChange, true);
447
+ };
448
+ });
449
+ $effect(() => {
450
+ if (!$store.opened || disabled) {
451
+ return;
452
+ }
453
+ const overlay = registerOverlay({
454
+ kind: "popover",
455
+ mount: effectiveDisablePortal ? "inline" : "portal"
456
+ });
457
+ backdropZ = overlay.layer.backdrop;
458
+ dropdownZ = overlay.layer.content;
459
+ return () => {
460
+ overlay.unregister();
461
+ backdropZ = 1090;
462
+ dropdownZ = 1100;
463
+ };
464
+ });
465
+ $effect(() => {
466
+ void anchorEl;
467
+ void storeOpened;
468
+ syncInsideDialogFlag();
469
+ });
470
+ $effect(() => {
471
+ if (!$store.opened || !effectiveDisablePortal || disabled || typeof window === "undefined") {
472
+ return;
473
+ }
474
+ const onDocumentPointerDown = (event) => {
475
+ const target = event.target;
476
+ if (!target) return;
477
+ const insideAnchor = !!anchorEl?.contains(target);
478
+ const insideDropdown = !!dropdownEl?.contains(target);
479
+ if (!insideAnchor && !insideDropdown) {
480
+ store.setOpened(false);
481
+ }
482
+ };
483
+ window.addEventListener("pointerdown", onDocumentPointerDown, true);
484
+ return () => window.removeEventListener("pointerdown", onDocumentPointerDown, true);
485
+ });
603
486
  </script>
604
487
 
605
488
  <div
@@ -631,6 +514,7 @@
631
514
  {inputId}
632
515
  {label}
633
516
  {leftSection}
517
+ {name}
634
518
  {placeholder}
635
519
  isFetching={$store.isFetching}
636
520
  search={$store.input}
@@ -657,6 +541,7 @@
657
541
  <div
658
542
  class="fixed inset-0"
659
543
  data-testid="boxer-backdrop"
544
+ role="presentation"
660
545
  onpointerdown={onBackdropPointerDown}
661
546
  style:z-index={backdropZ}
662
547
  ></div>
@@ -670,6 +555,7 @@
670
555
  data-testid="boxer-dropdown"
671
556
  id={listboxId}
672
557
  role="listbox"
558
+ tabindex="-1"
673
559
  aria-label={label ?? "Options"}
674
560
  onpointerdown={markDropdownPointerInteraction}
675
561
  style:z-index={dropdownZ}