@casinogate/ui 1.11.12 → 1.11.14

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.
@@ -1,4 +1,4 @@
1
- /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
1
+ /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
2
2
  @layer properties;
3
3
  @layer theme, base, components, utilities;
4
4
  @layer theme {
@@ -1,12 +1,12 @@
1
- <script lang="ts" generics="TItem extends CommandItem">
2
- import type { CommandCollection, CommandItem } from '../../../command/index.js';
3
- import type { ComboboxAsyncProps } from '../../types.js';
1
+ <script lang="ts" generics="TItem extends ComboboxItem">
2
+ import type { CommandCollection } from '../../../command/index.js';
3
+ import type { ComboboxAsyncProps, ComboboxItem } from '../../types.js';
4
4
 
5
5
  import { SELECTION_TYPE } from '../../../../internal/constants/selection-type.js';
6
6
  import { boolAttr } from '../../../../internal/utils/attrs.js';
7
7
  import { cn, noop } from '../../../../internal/utils/common.js';
8
- import { Debounced, watch } from 'runed';
9
- import { afterTick, boxWith, onMountEffect } from 'svelte-toolbelt';
8
+ import { watch } from 'runed';
9
+ import { afterTick, boxWith } from 'svelte-toolbelt';
10
10
  import type { Attachment } from 'svelte/attachments';
11
11
  import { cubicInOut } from 'svelte/easing';
12
12
  import { fly } from 'svelte/transition';
@@ -19,6 +19,7 @@
19
19
  import { useDisplayValue } from '../use-display-value.svelte.js';
20
20
  import { useGroupedItems } from '../use-grouped-items.svelte.js';
21
21
  import { useSelectedItems } from '../use-selected-items.svelte.js';
22
+ import { useAsyncComboboxData } from './use-async-combobox-data.svelte.js';
22
23
 
23
24
  let {
24
25
  open = $bindable(false),
@@ -44,48 +45,44 @@
44
45
  clearable = false,
45
46
 
46
47
  items = $bindable([]),
48
+ selectedItems = $bindable([]),
47
49
 
48
50
  maxContentHeight,
49
51
 
50
52
  loading: loadingSnippet,
51
53
  pageSize = 10,
52
54
  callback,
55
+ loadItems,
53
56
  loadImmediate = true,
57
+ debounceMs,
54
58
  searchDebounce = 300,
55
59
  dependsOn,
56
60
  clearOnDependencyChange = true,
61
+ onDependencyChange,
57
62
  onSelect = noop,
58
63
 
59
64
  ...restProps
60
65
  }: ComboboxAsyncProps<TItem> = $props();
61
66
 
62
- const isMultiple = type === 'multiple';
67
+ const getDefaultValue = () => (type === SELECTION_TYPE.MULTIPLE ? [] : '');
68
+ const isMultiple = $derived.by(() => type === SELECTION_TYPE.MULTIPLE);
63
69
 
64
70
  // Initialize value based on type
65
71
  if (value === undefined) {
66
- const defaultValue = type === SELECTION_TYPE.SINGLE ? '' : [];
67
- value = defaultValue;
72
+ value = getDefaultValue();
68
73
  }
69
74
 
70
75
  watch.pre(
71
76
  () => value,
72
77
  () => {
73
78
  if (value !== undefined) return;
74
- value = type === SELECTION_TYPE.SINGLE ? '' : [];
79
+ value = getDefaultValue();
75
80
  }
76
81
  );
77
82
 
78
- // let items = $state<CommandItem[]>(initialItems);
79
- let isLoading = $state(false);
80
- let isSearching = $state(false);
81
- let hasMore = $state(false);
82
- let currentPage = $state(1);
83
- let error = $state<string | null>(null);
84
- let lastSearch = $state('');
85
-
86
83
  let listRef = $state<HTMLElement | null>(null);
87
84
 
88
- const createCollection = (itemList: CommandItem[]): CommandCollection => {
85
+ const createCollection = (itemList: ComboboxItem[]): CommandCollection => {
89
86
  const groupOrderMap = new Map<string, number>();
90
87
  groups.forEach((g, index) => {
91
88
  groupOrderMap.set(g.value, g.order ?? index);
@@ -113,11 +110,13 @@
113
110
  value: boxWith(() => value!),
114
111
  collection: boxWith(() => collection),
115
112
  placeholder: boxWith(() => placeholder),
113
+ selectedItems: boxWith(() => selectedItems),
116
114
  });
117
115
 
118
- const selectedItems = useSelectedItems({
116
+ const resolvedSelectedItems = useSelectedItems({
119
117
  value: boxWith(() => value!),
120
118
  collection: boxWith(() => collection),
119
+ selectedItems: boxWith(() => selectedItems),
121
120
  });
122
121
 
123
122
  const isSelected = (itemValue: string): boolean => {
@@ -125,6 +124,8 @@
125
124
  return value === itemValue;
126
125
  };
127
126
 
127
+ const getItemKey = (item: ComboboxItem) => item.key ?? item.value;
128
+
128
129
  const isPlaceholder = $derived.by(() => {
129
130
  if (Array.isArray(value)) return value.length === 0;
130
131
  return typeof value === 'string' && value.trim() === '';
@@ -132,109 +133,61 @@
132
133
 
133
134
  const hasResults = $derived(collection.size > 0);
134
135
 
135
- const fetchData = async (page: number, search: string, append = false) => {
136
- if (isLoading) return;
137
-
138
- isLoading = true;
139
- isSearching = !append;
140
- error = null;
141
-
142
- try {
143
- const result = await callback({ page, pageSize, search });
144
-
145
- if (append) {
146
- items = [...items, ...result.items];
147
- } else {
148
- items = result.items;
149
- }
150
-
151
- currentPage = page;
152
- hasMore = result.hasMore;
153
- lastSearch = search;
154
- } catch (err) {
155
- error = err instanceof Error ? err.message : 'Failed to fetch data';
156
- } finally {
157
- isLoading = false;
158
- isSearching = false;
136
+ const resolvedLoadItems = $derived.by(() => {
137
+ const loader = loadItems ?? callback;
138
+ if (!loader) {
139
+ throw new Error('Combobox.Async requires either loadItems or callback');
159
140
  }
160
- };
161
141
 
162
- // Debounced search handler
163
- const debouncedSearchValue = new Debounced(() => searchValue, searchDebounce);
142
+ return loader;
143
+ });
164
144
 
165
- const search = (search: string) => {
166
- currentPage = 1;
167
- fetchData(1, search, false);
168
- };
145
+ const asyncData = useAsyncComboboxData<TItem>({
146
+ items: boxWith(
147
+ () => items,
148
+ (next) => {
149
+ items = next;
150
+ }
151
+ ),
152
+ loadItems: boxWith(() => resolvedLoadItems),
153
+ search: boxWith(
154
+ () => searchValue,
155
+ (next) => {
156
+ searchValue = next;
157
+ }
158
+ ),
159
+ pageSize: boxWith(() => pageSize),
160
+ debounceMs: boxWith(() => debounceMs ?? searchDebounce),
161
+ loadImmediate: boxWith(() => loadImmediate),
162
+ dependency: boxWith(() => dependsOn?.()),
163
+ onDependencyReset: (dependency) => {
164
+ onDependencyChange?.(dependency);
169
165
 
170
- // Watch for search value changes
171
- watch(
172
- () => debouncedSearchValue.current,
173
- () => {
174
- search(debouncedSearchValue.current);
166
+ if (clearOnDependencyChange) {
167
+ value = isMultiple ? [] : '';
168
+ selectedItems = [];
169
+ }
175
170
  },
176
- {
177
- lazy: true,
178
- }
179
- );
171
+ });
172
+
173
+ const isLoading = $derived(asyncData.isLoading);
174
+ const isSearching = $derived(asyncData.isSearching);
175
+ const hasMore = $derived(asyncData.hasMore);
176
+ const error = $derived(asyncData.error);
180
177
 
181
- // Fetch on first open
182
178
  watch(
183
179
  () => open,
184
180
  () => {
185
181
  if (!open) return;
182
+ if (items.length > 0 || isLoading || loadImmediate) return;
186
183
 
187
- // If no items yet and not loading, fetch first page
188
- if (items.length === 0 && !isLoading && !loadImmediate) {
189
- fetchData(1, searchValue);
190
- return;
191
- }
192
- }
193
- );
194
-
195
- // Fetch immediate data on mount
196
- onMountEffect(() => {
197
- if (loadImmediate) fetchData(1, searchValue);
198
- });
199
-
200
- // Reset page on unmount
201
- $effect(() => {
202
- return () => {
203
- currentPage = 1;
204
- };
205
- });
206
-
207
- // Watch for dependency changes and reset
208
- let previousDependency = $state<unknown>(dependsOn?.());
209
- watch(
210
- () => dependsOn?.(),
211
- (newValue) => {
212
- if (previousDependency === undefined && newValue === undefined) return;
213
- if (previousDependency === newValue) return;
214
-
215
- previousDependency = newValue;
216
-
217
- // Reset state
218
- items = [];
219
- currentPage = 1;
220
- hasMore = true;
221
- error = null;
222
- searchValue = '';
223
- lastSearch = '';
224
-
225
- // Clear selected value if enabled
226
- if (clearOnDependencyChange) {
227
- value = isMultiple ? [] : '';
228
- }
229
-
230
- // Reload data
231
- fetchData(1, '');
184
+ asyncData.reload(searchValue);
232
185
  }
233
186
  );
234
187
 
235
188
  const loadMore = () => {
236
189
  if (!isLoading && hasMore) {
237
- fetchData(currentPage + 1, searchValue, true);
190
+ asyncData.loadMore();
238
191
  }
239
192
  };
240
193
 
@@ -256,8 +209,7 @@
256
209
  };
257
210
 
258
211
  const retry = () => {
259
- error = null;
260
- fetchData(currentPage, searchValue);
212
+ asyncData.retry();
261
213
  };
262
214
 
263
215
  const scrollToTop = () => {
@@ -268,24 +220,45 @@
268
220
  behavior: 'smooth',
269
221
  });
270
222
  };
271
- function onSelectItem(item: CommandItem) {
223
+
224
+ const cacheSelectedItem = (item: TItem) => {
225
+ selectedItems = [...selectedItems.filter((selectedItem) => selectedItem.value !== item.value), item];
226
+ };
227
+
228
+ const removeSelectedItem = (itemValue: string) => {
229
+ selectedItems = selectedItems.filter((selectedItem) => selectedItem.value !== itemValue);
230
+ };
231
+
232
+ function onSelectItem(item: ComboboxItem) {
233
+ const typedItem = item as TItem;
234
+
272
235
  if (Array.isArray(value)) {
273
236
  let result = value;
274
237
  if (value.includes(item.value)) {
275
- result = allowDeselect ? value.filter((v) => v !== item.value) : value;
238
+ if (allowDeselect) {
239
+ result = value.filter((v) => v !== item.value);
240
+ removeSelectedItem(item.value);
241
+ }
276
242
  } else {
277
243
  result = [...value, item.value];
244
+ cacheSelectedItem(typedItem);
278
245
  }
279
- onSelect(result, item as TItem);
246
+ onSelect(result, typedItem);
280
247
  value = result;
281
248
  } else {
282
- let result = value;
249
+ let result = typeof value === 'string' ? value : '';
283
250
  if (item.value === value) {
284
- result = allowDeselect ? '' : value;
251
+ if (allowDeselect) {
252
+ result = '';
253
+ selectedItems = [];
254
+ } else {
255
+ cacheSelectedItem(typedItem);
256
+ }
285
257
  } else {
286
258
  result = item.value;
259
+ selectedItems = [typedItem];
287
260
  }
288
- onSelect(result, item as TItem);
261
+ onSelect(result, typedItem);
289
262
  value = result;
290
263
  }
291
264
 
@@ -307,11 +280,15 @@
307
280
  {#snippet clearBtn()}
308
281
  {#if clearable && isNotEmpty}
309
282
  <button
283
+ aria-label="Clear selection"
310
284
  class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:cursor-pointer cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
311
285
  type="button"
312
286
  onclick={(event) => {
313
287
  event.stopPropagation();
314
- value = isMultiple ? [] : '';
288
+ const nextValue = isMultiple ? [] : '';
289
+ value = nextValue;
290
+ selectedItems = [];
291
+ onSelect(nextValue, null);
315
292
  }}
316
293
  >
317
294
  <Icon.Cross class="cgui:ms-auto" width={16} height={16} />
@@ -323,13 +300,15 @@
323
300
  {@const triggerProps = {
324
301
  class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
325
302
  'data-placeholder': boolAttr(isPlaceholder),
303
+ 'aria-expanded': open,
304
+ 'aria-busy': isLoading,
326
305
  }}
327
306
  {#if labelSnippet}
328
307
  <PopoverPrimitive.Trigger {...triggerProps}>
329
308
  {@render labelSnippet({
330
309
  placeholder: displayValue.current,
331
310
  value: isMultiple ? (value as string[]) : (value as string),
332
- selectedItems: selectedItems.current,
311
+ selectedItems: resolvedSelectedItems.current,
333
312
  })}
334
313
 
335
314
  {@render clearBtn()}
@@ -349,7 +328,7 @@
349
328
  {/if}
350
329
  {/snippet}
351
330
 
352
- {#snippet renderItem(item: CommandItem)}
331
+ {#snippet renderItem(item: ComboboxItem)}
353
332
  {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
354
333
  {@const itemAttrs = {
355
334
  value: itemValue,
@@ -388,8 +367,22 @@
388
367
  {/if}
389
368
  {/snippet}
390
369
 
370
+ {#snippet renderEmpty()}
371
+ {#if typeof empty === 'string'}
372
+ <CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
373
+ {:else if empty}
374
+ {@render empty({ props: {} })}
375
+ {:else}
376
+ <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
377
+ {/if}
378
+ {/snippet}
379
+
391
380
  {#snippet loadingSpinner()}
392
- <div class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default">
381
+ <div
382
+ role="status"
383
+ aria-live="polite"
384
+ class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default"
385
+ >
393
386
  <Spinner />
394
387
  </div>
395
388
  {/snippet}
@@ -401,7 +394,7 @@
401
394
  {/snippet}
402
395
 
403
396
  {#snippet errorState()}
404
- <div class="cgui:p-4 cgui:text-center cgui:text-body-2">
397
+ <div role="alert" class="cgui:p-4 cgui:text-center cgui:text-body-2">
405
398
  <p class="cgui:text-destructive cgui:mb-2">{error}</p>
406
399
  <button type="button" class="cgui:text-sm cgui:text-primary cgui:underline cgui:hover:no-underline" onclick={retry}>
407
400
  Try again
@@ -435,7 +428,7 @@
435
428
  <CommandPrimitive.GroupHeading>{group?.label ?? groupKey}</CommandPrimitive.GroupHeading>
436
429
 
437
430
  <CommandPrimitive.GroupItems>
438
- {#each groupItems as item (item.value)}
431
+ {#each groupItems as item (getItemKey(item))}
439
432
  {@render renderItem(item)}
440
433
  {/each}
441
434
  </CommandPrimitive.GroupItems>
@@ -446,7 +439,7 @@
446
439
  </CommandPrimitive.Group>
447
440
  {:else}
448
441
  <CommandPrimitive.Group value="__ungrouped__">
449
- {#each groupItems as item (item.value)}
442
+ {#each groupItems as item (getItemKey(item))}
450
443
  {@render renderItem(item)}
451
444
  {/each}
452
445
  </CommandPrimitive.Group>
@@ -463,7 +456,7 @@
463
456
  {@render loadingMore()}
464
457
  {/if}
465
458
  {:else}
466
- <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
459
+ {@render renderEmpty()}
467
460
  {/if}
468
461
  </CommandPrimitive.Viewport>
469
462
 
@@ -1,26 +1,25 @@
1
- import type { CommandItem } from '../../../command/index.js';
2
- import type { ComboboxAsyncProps } from '../../types.js';
3
- declare function $$render<TItem extends CommandItem>(): {
1
+ import type { ComboboxAsyncProps, ComboboxItem } from '../../types.js';
2
+ declare function $$render<TItem extends ComboboxItem>(): {
4
3
  props: ComboboxAsyncProps<TItem>;
5
4
  exports: {};
6
- bindings: "value" | "open" | "items" | "searchValue";
5
+ bindings: "value" | "open" | "items" | "searchValue" | "selectedItems";
7
6
  slots: {};
8
7
  events: {};
9
8
  };
10
- declare class __sveltets_Render<TItem extends CommandItem> {
9
+ declare class __sveltets_Render<TItem extends ComboboxItem> {
11
10
  props(): ReturnType<typeof $$render<TItem>>['props'];
12
11
  events(): ReturnType<typeof $$render<TItem>>['events'];
13
12
  slots(): ReturnType<typeof $$render<TItem>>['slots'];
14
- bindings(): "value" | "open" | "items" | "searchValue";
13
+ bindings(): "value" | "open" | "items" | "searchValue" | "selectedItems";
15
14
  exports(): {};
16
15
  }
17
16
  interface $$IsomorphicComponent {
18
- new <TItem extends CommandItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TItem>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TItem>['props']>, ReturnType<__sveltets_Render<TItem>['events']>, ReturnType<__sveltets_Render<TItem>['slots']>> & {
17
+ new <TItem extends ComboboxItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TItem>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TItem>['props']>, ReturnType<__sveltets_Render<TItem>['events']>, ReturnType<__sveltets_Render<TItem>['slots']>> & {
19
18
  $$bindings?: ReturnType<__sveltets_Render<TItem>['bindings']>;
20
19
  } & ReturnType<__sveltets_Render<TItem>['exports']>;
21
- <TItem extends CommandItem>(internal: unknown, props: ReturnType<__sveltets_Render<TItem>['props']> & {}): ReturnType<__sveltets_Render<TItem>['exports']>;
20
+ <TItem extends ComboboxItem>(internal: unknown, props: ReturnType<__sveltets_Render<TItem>['props']> & {}): ReturnType<__sveltets_Render<TItem>['exports']>;
22
21
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
23
22
  }
24
23
  declare const Combobox: $$IsomorphicComponent;
25
- type Combobox<TItem extends CommandItem> = InstanceType<typeof Combobox<TItem>>;
24
+ type Combobox<TItem extends ComboboxItem> = InstanceType<typeof Combobox<TItem>>;
26
25
  export default Combobox;
@@ -0,0 +1,36 @@
1
+ import type { ComboboxAsyncLoader, ComboboxItem } from '../../types.js';
2
+ import type { ReadableBoxedValues, WritableBoxedValues } from 'svelte-toolbelt';
3
+ export declare const ASYNC_STATUS: {
4
+ readonly IDLE: "idle";
5
+ readonly LOADING: "loading";
6
+ readonly SEARCHING: "searching";
7
+ readonly LOADING_MORE: "loadingMore";
8
+ readonly ERROR: "error";
9
+ };
10
+ export type AsyncStatus = (typeof ASYNC_STATUS)[keyof typeof ASYNC_STATUS];
11
+ type Options<TItem extends ComboboxItem> = WritableBoxedValues<{
12
+ items: TItem[];
13
+ search: string;
14
+ }> & ReadableBoxedValues<{
15
+ loadItems: ComboboxAsyncLoader<TItem>;
16
+ pageSize: number;
17
+ debounceMs: number;
18
+ loadImmediate: boolean;
19
+ dependency: unknown;
20
+ }> & {
21
+ onDependencyReset?: (value: unknown) => void;
22
+ };
23
+ export declare function useAsyncComboboxData<TItem extends ComboboxItem>(opts: Options<TItem>): {
24
+ readonly status: AsyncStatus;
25
+ readonly error: string | null;
26
+ readonly hasMore: boolean;
27
+ readonly currentPage: number;
28
+ readonly isLoading: boolean;
29
+ readonly isSearching: boolean;
30
+ readonly isLoadingMore: boolean;
31
+ reload: (search?: string) => Promise<void>;
32
+ loadMore: () => Promise<void>;
33
+ retry: () => Promise<void>;
34
+ abort: () => void;
35
+ };
36
+ export {};
@@ -0,0 +1,113 @@
1
+ import { Debounced, watch } from 'runed';
2
+ import { onMount } from 'svelte';
3
+ export const ASYNC_STATUS = {
4
+ IDLE: 'idle',
5
+ LOADING: 'loading',
6
+ SEARCHING: 'searching',
7
+ LOADING_MORE: 'loadingMore',
8
+ ERROR: 'error',
9
+ };
10
+ export function useAsyncComboboxData(opts) {
11
+ let status = $state(ASYNC_STATUS.IDLE);
12
+ let error = $state(null);
13
+ let hasMore = $state(false);
14
+ let currentPage = $state(1);
15
+ let committedSearch = $state('');
16
+ let requestId = 0;
17
+ let controller = null;
18
+ const abortCurrent = () => {
19
+ requestId += 1;
20
+ controller?.abort();
21
+ controller = null;
22
+ };
23
+ const run = async (page, search, append = false) => {
24
+ if (append && (status === ASYNC_STATUS.LOADING_MORE || !hasMore))
25
+ return;
26
+ abortCurrent();
27
+ const id = requestId;
28
+ const localController = new AbortController();
29
+ controller = localController;
30
+ status = append
31
+ ? ASYNC_STATUS.LOADING_MORE
32
+ : opts.items.current.length > 0
33
+ ? ASYNC_STATUS.SEARCHING
34
+ : ASYNC_STATUS.LOADING;
35
+ error = null;
36
+ try {
37
+ const result = await opts.loadItems.current({
38
+ page,
39
+ pageSize: opts.pageSize.current,
40
+ search,
41
+ signal: localController.signal,
42
+ dependency: opts.dependency.current,
43
+ });
44
+ if (localController.signal.aborted || id !== requestId)
45
+ return;
46
+ opts.items.current = append ? [...opts.items.current, ...result.items] : result.items;
47
+ currentPage = page;
48
+ committedSearch = search;
49
+ hasMore = result.hasMore;
50
+ status = ASYNC_STATUS.IDLE;
51
+ }
52
+ catch (err) {
53
+ if (localController.signal.aborted || id !== requestId)
54
+ return;
55
+ error = err instanceof Error ? err.message : 'Failed to fetch data';
56
+ status = ASYNC_STATUS.ERROR;
57
+ }
58
+ };
59
+ const reload = (search = opts.search.current) => run(1, search, false);
60
+ const loadMore = () => run(currentPage + 1, committedSearch, true);
61
+ const retry = () => run(currentPage, committedSearch, currentPage > 1);
62
+ const debouncedSearch = new Debounced(() => opts.search.current, opts.debounceMs.current);
63
+ watch(() => debouncedSearch.current, (search) => {
64
+ reload(search);
65
+ }, { lazy: true });
66
+ let previousDependency = $state(opts.dependency.current);
67
+ watch(() => $state.snapshot(opts.dependency.current), (dependency) => {
68
+ if (Object.is(previousDependency, dependency))
69
+ return;
70
+ previousDependency = dependency;
71
+ abortCurrent();
72
+ opts.items.current = [];
73
+ opts.search.current = '';
74
+ opts.onDependencyReset?.(dependency);
75
+ error = null;
76
+ hasMore = false;
77
+ currentPage = 1;
78
+ committedSearch = '';
79
+ reload('');
80
+ }, { lazy: true });
81
+ onMount(() => {
82
+ if (opts.loadImmediate.current)
83
+ reload(opts.search.current);
84
+ return () => abortCurrent();
85
+ });
86
+ return {
87
+ get status() {
88
+ return status;
89
+ },
90
+ get error() {
91
+ return error;
92
+ },
93
+ get hasMore() {
94
+ return hasMore;
95
+ },
96
+ get currentPage() {
97
+ return currentPage;
98
+ },
99
+ get isLoading() {
100
+ return (status === ASYNC_STATUS.LOADING || status === ASYNC_STATUS.SEARCHING || status === ASYNC_STATUS.LOADING_MORE);
101
+ },
102
+ get isSearching() {
103
+ return status === ASYNC_STATUS.LOADING || status === ASYNC_STATUS.SEARCHING;
104
+ },
105
+ get isLoadingMore() {
106
+ return status === ASYNC_STATUS.LOADING_MORE;
107
+ },
108
+ reload,
109
+ loadMore,
110
+ retry,
111
+ abort: abortCurrent,
112
+ };
113
+ }
@@ -6,12 +6,12 @@
6
6
  import { afterTick, boxWith } from 'svelte-toolbelt';
7
7
  import { cubicInOut } from 'svelte/easing';
8
8
  import { fly } from 'svelte/transition';
9
- import { CommandPrimitive, type CommandItem } from '../../../command/index.js';
9
+ import { CommandPrimitive } from '../../../command/index.js';
10
10
  import { Icon, Icon as IconComponent } from '../../../icons/index.js';
11
11
  import { Kbd } from '../../../kbd/index.js';
12
12
  import { PopoverPrimitive } from '../../../popover/index.js';
13
13
  import { comboboxTriggerVariants } from '../../styles.js';
14
- import type { ComboboxProps } from '../../types.js';
14
+ import type { ComboboxItem, ComboboxProps } from '../../types.js';
15
15
  import { useDisplayValue } from '../use-display-value.svelte.js';
16
16
  import { useGroupedItems } from '../use-grouped-items.svelte.js';
17
17
  import { useSelectedItems } from '../use-selected-items.svelte.js';
@@ -84,7 +84,7 @@
84
84
  return typeof value === 'string' && value.trim() !== '';
85
85
  });
86
86
 
87
- function onSelectItem(item: CommandItem) {
87
+ function onSelectItem(item: ComboboxItem) {
88
88
  if (Array.isArray(value)) {
89
89
  let result = value;
90
90
  if (value.includes(item.value)) {
@@ -118,16 +118,21 @@
118
118
  if (Array.isArray(value)) return value.includes(itemValue);
119
119
  return value === itemValue;
120
120
  }
121
+
122
+ const getItemKey = (item: ComboboxItem) => item.key ?? item.value;
121
123
  </script>
122
124
 
123
125
  {#snippet clearBtn()}
124
126
  {#if clearable && isNotEmpty}
125
127
  <button
128
+ aria-label="Clear selection"
126
129
  class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:cursor-pointer cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
127
130
  type="button"
128
131
  onclick={(event) => {
129
132
  event.stopPropagation();
130
- value = isMultiple ? [] : '';
133
+ const nextValue = isMultiple ? [] : '';
134
+ value = nextValue;
135
+ onSelect(nextValue, null);
131
136
  }}
132
137
  >
133
138
  <Icon.Cross class="cgui:ms-auto" width={16} height={16} />
@@ -166,7 +171,7 @@
166
171
  {/if}
167
172
  {/snippet}
168
173
 
169
- {#snippet renderItem(item: CommandItem)}
174
+ {#snippet renderItem(item: ComboboxItem)}
170
175
  {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
171
176
  {@const itemAttrs = {
172
177
  value: itemValue,
@@ -205,6 +210,16 @@
205
210
  {/if}
206
211
  {/snippet}
207
212
 
213
+ {#snippet renderEmpty()}
214
+ {#if typeof empty === 'string'}
215
+ <CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
216
+ {:else if empty}
217
+ {@render empty({ props: {} })}
218
+ {:else}
219
+ <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
220
+ {/if}
221
+ {/snippet}
222
+
208
223
  <PopoverPrimitive.Root bind:open {...restProps}>
209
224
  {@render renderTrigger()}
210
225
 
@@ -215,7 +230,7 @@
215
230
 
216
231
  <CommandPrimitive.List class="cgui:rounded-inherit" style={{ maxHeight: maxContentHeight }}>
217
232
  <CommandPrimitive.Viewport>
218
- <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
233
+ {@render renderEmpty()}
219
234
 
220
235
  {#each groupedItems.items.current as [groupKey, items] (groupKey || '__ungrouped__')}
221
236
  {#if groupKey}
@@ -224,7 +239,7 @@
224
239
  <CommandPrimitive.GroupHeading>{group?.label ?? groupKey}</CommandPrimitive.GroupHeading>
225
240
 
226
241
  <CommandPrimitive.GroupItems>
227
- {#each items as item (item.value)}
242
+ {#each items as item (getItemKey(item))}
228
243
  {@render renderItem(item)}
229
244
  {/each}
230
245
  </CommandPrimitive.GroupItems>
@@ -235,7 +250,7 @@
235
250
  </CommandPrimitive.Group>
236
251
  {:else}
237
252
  <CommandPrimitive.Group value="__ungrouped__">
238
- {#each items as item (item.value)}
253
+ {#each items as item (getItemKey(item))}
239
254
  {@render renderItem(item)}
240
255
  {/each}
241
256
  </CommandPrimitive.Group>
@@ -1,9 +1,12 @@
1
1
  import type { CommandCollection } from '../../command/types.js';
2
- import { type ReadableBoxedValues } from 'svelte-toolbelt';
2
+ import type { ComboboxItem } from '../types.js';
3
+ import { type ReadableBox, type ReadableBoxedValues } from 'svelte-toolbelt';
3
4
  type Opts = ReadableBoxedValues<{
4
5
  value: string | string[];
5
6
  collection: CommandCollection;
6
7
  placeholder: string;
7
- }>;
8
- export declare const useDisplayValue: (opts: Opts) => import("svelte-toolbelt").ReadableBox<string>;
8
+ }> & {
9
+ selectedItems?: ReadableBox<ComboboxItem[]>;
10
+ };
11
+ export declare const useDisplayValue: (opts: Opts) => ReadableBox<string>;
9
12
  export {};
@@ -2,14 +2,18 @@ import { boxWith } from 'svelte-toolbelt';
2
2
  export const useDisplayValue = (opts) => {
3
3
  const res = $derived.by(() => {
4
4
  const { value, collection, placeholder } = opts;
5
+ const findItem = (itemValue) => {
6
+ return (collection.current.items.find((item) => item.value === itemValue) ??
7
+ opts.selectedItems?.current.find((item) => item.value === itemValue));
8
+ };
5
9
  if (typeof value.current === 'string' && value.current.trim() !== '') {
6
- const item = collection.current.items.find((i) => i.value === value.current);
10
+ const item = findItem(value.current);
7
11
  return item?.label ?? value.current;
8
12
  }
9
13
  if (Array.isArray(value.current) && value.current.length > 0) {
10
14
  return value.current
11
15
  .map((v) => {
12
- const item = collection.current.items.find((i) => i.value === v);
16
+ const item = findItem(v);
13
17
  return item?.label ?? v;
14
18
  })
15
19
  .join(', ');
@@ -1,8 +1,11 @@
1
- import type { CommandCollection, CommandItem } from '../../command/types.js';
2
- import { type ReadableBoxedValues } from 'svelte-toolbelt';
1
+ import type { CommandCollection } from '../../command/types.js';
2
+ import type { ComboboxItem } from '../types.js';
3
+ import { type ReadableBox, type ReadableBoxedValues } from 'svelte-toolbelt';
3
4
  type Opts = ReadableBoxedValues<{
4
5
  value: string | string[];
5
6
  collection: CommandCollection;
6
- }>;
7
- export declare const useSelectedItems: (opts: Opts) => import("svelte-toolbelt").ReadableBox<CommandItem[]>;
7
+ }> & {
8
+ selectedItems?: ReadableBox<ComboboxItem[]>;
9
+ };
10
+ export declare const useSelectedItems: (opts: Opts) => ReadableBox<import("../../command/types.js").CommandItem[]>;
8
11
  export {};
@@ -2,14 +2,16 @@ import { boxWith } from 'svelte-toolbelt';
2
2
  export const useSelectedItems = (opts) => {
3
3
  const { value, collection } = opts;
4
4
  const res = $derived.by(() => {
5
+ const findItem = (itemValue) => {
6
+ return (collection.current.items.find((item) => item.value === itemValue) ??
7
+ opts.selectedItems?.current.find((item) => item.value === itemValue));
8
+ };
5
9
  if (typeof value.current === 'string' && value.current.trim() !== '') {
6
- const item = collection.current.items.find((i) => i.value === value.current);
10
+ const item = findItem(value.current);
7
11
  return item ? [item] : [];
8
12
  }
9
13
  if (Array.isArray(value.current) && value.current.length > 0) {
10
- return value.current
11
- .map((v) => collection.current.items.find((i) => i.value === v))
12
- .filter((i) => i !== undefined);
14
+ return value.current.map((v) => findItem(v)).filter((i) => i !== undefined);
13
15
  }
14
16
  return [];
15
17
  });
@@ -4,6 +4,9 @@ import type { CommandGroup, CommandItem, CommandProps } from '../command/index.j
4
4
  import type { PrimitivePopoverRootProps } from '../popover/types.js';
5
5
  import type { ComboboxTriggerVariantsProps } from './styles.js';
6
6
  export type ComboboxRootProps = PrimitivePopoverRootProps;
7
+ export type ComboboxItem = CommandItem & {
8
+ key?: string | number;
9
+ };
7
10
  /**
8
11
  * Base props shared by both single and multiple selection modes
9
12
  */
@@ -29,10 +32,10 @@ type ComboboxBaseProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsProp
29
32
  label?: Snippet<[{
30
33
  placeholder: string;
31
34
  value: string | string[];
32
- selectedItems: CommandItem[];
35
+ selectedItems: ComboboxItem[];
33
36
  }]>;
34
37
  /** Callback when selection changes */
35
- onSelect?: (value: string | string[], item: CommandItem | null) => void;
38
+ onSelect?: (value: string | string[], item: ComboboxItem | null) => void;
36
39
  };
37
40
  type ComboboxSingleProps = ComboboxBaseProps & {
38
41
  type: typeof SELECTION_TYPE.SINGLE;
@@ -46,58 +49,63 @@ export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
46
49
  /**
47
50
  * Async Combobox Types
48
51
  */
49
- export type ComboboxAsyncCallbackParams = {
52
+ export type ComboboxAsyncLoadParams = {
50
53
  page: number;
51
54
  pageSize: number;
52
55
  search: string;
56
+ signal: AbortSignal;
57
+ dependency?: unknown;
53
58
  };
54
- export type ComboboxAsyncCallbackResult<TItem extends CommandItem> = {
59
+ export type ComboboxAsyncLoadResult<TItem extends ComboboxItem> = {
55
60
  items: TItem[];
56
61
  hasMore: boolean;
57
62
  };
58
- export type ComboboxAsyncCallback<TItem extends CommandItem = CommandItem> = (params: ComboboxAsyncCallbackParams) => Promise<ComboboxAsyncCallbackResult<TItem>>;
59
- type ComboboxAsyncBaseProps<TItem extends CommandItem> = Omit<ComboboxBaseProps, 'collection' | 'item' | 'onSelect'> & {
63
+ export type ComboboxAsyncLoader<TItem extends ComboboxItem = ComboboxItem> = (params: ComboboxAsyncLoadParams) => Promise<ComboboxAsyncLoadResult<TItem>>;
64
+ /** @deprecated Use ComboboxAsyncLoadParams instead. */
65
+ export type ComboboxAsyncCallbackParams = ComboboxAsyncLoadParams;
66
+ /** @deprecated Use ComboboxAsyncLoadResult instead. */
67
+ export type ComboboxAsyncCallbackResult<TItem extends ComboboxItem> = ComboboxAsyncLoadResult<TItem>;
68
+ /** @deprecated Use ComboboxAsyncLoader instead. */
69
+ export type ComboboxAsyncCallback<TItem extends ComboboxItem = ComboboxItem> = ComboboxAsyncLoader<TItem>;
70
+ type ComboboxAsyncLoadProp<TItem extends ComboboxItem> = {
71
+ /** Preferred async loader API */
72
+ loadItems: ComboboxAsyncLoader<TItem>;
73
+ callback?: never;
74
+ } | {
75
+ /** @deprecated Use loadItems instead. Kept for backwards compatibility. */
76
+ callback: ComboboxAsyncLoader<TItem>;
77
+ loadItems?: never;
78
+ };
79
+ type ComboboxAsyncBaseProps<TItem extends ComboboxItem> = Omit<ComboboxBaseProps, 'collection' | 'item' | 'onSelect'> & ComboboxAsyncLoadProp<TItem> & {
60
80
  items?: TItem[];
81
+ /** Cache of selected item objects, useful when selected values are not in the currently loaded async page. */
82
+ selectedItems?: TItem[];
61
83
  item?: Snippet<[{
62
84
  item: TItem;
63
85
  props: Record<string, unknown>;
64
86
  }]>;
65
- /** Callback when selection changes */
66
87
  onSelect?: (value: string | string[], item: TItem | null) => void;
67
- /** Loading state renderer */
68
88
  loading?: Snippet;
69
- /** Current page (default: 1) */
70
- page?: number;
71
- /** Page size for pagination (default: 20) */
72
89
  pageSize?: number;
73
- /** Async data callback */
74
- callback: ComboboxAsyncCallback<TItem>;
75
- /** Load immediate data when combobox is mounted (default: true) */
76
90
  loadImmediate?: boolean;
77
- /** Group definitions (for labels/ordering) */
78
91
  groups?: CommandGroup[];
79
- /** Debounce delay for search in milliseconds (default: 300ms) */
92
+ /** Preferred name */
93
+ debounceMs?: number;
94
+ /** @deprecated Use debounceMs instead. */
80
95
  searchDebounce?: number;
81
- /**
82
- * Getter function for dependency tracking. When the returned value changes,
83
- * the component will reset items, page to 1, clear selected value, and reload data.
84
- * @example dependsOn={() => parentSelectValue}
85
- */
86
96
  dependsOn?: () => unknown;
87
- /**
88
- * Whether to clear the selected value when dependsOn changes **(default: true)**
89
- */
90
97
  clearOnDependencyChange?: boolean;
98
+ onDependencyChange?: (value: unknown) => void;
91
99
  };
92
- type ComboboxAsyncSingleProps<TItem extends CommandItem> = ComboboxAsyncBaseProps<TItem> & {
100
+ type ComboboxAsyncSingleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
93
101
  /** Selection mode (default: 'single') */
94
102
  type?: 'single';
95
103
  value?: string;
96
104
  };
97
- type ComboboxAsyncMultipleProps<TItem extends CommandItem> = ComboboxAsyncBaseProps<TItem> & {
105
+ type ComboboxAsyncMultipleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
98
106
  /** Selection mode */
99
107
  type: 'multiple';
100
108
  value?: string[];
101
109
  };
102
- export type ComboboxAsyncProps<TItem extends CommandItem> = ComboboxAsyncSingleProps<TItem> | ComboboxAsyncMultipleProps<TItem>;
110
+ export type ComboboxAsyncProps<TItem extends ComboboxItem> = ComboboxAsyncSingleProps<TItem> | ComboboxAsyncMultipleProps<TItem>;
103
111
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casinogate/ui",
3
- "version": "1.11.12",
3
+ "version": "1.11.14",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",