@casinogate/ui 1.11.12 → 1.11.13

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 => {
@@ -132,109 +131,61 @@
132
131
 
133
132
  const hasResults = $derived(collection.size > 0);
134
133
 
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;
134
+ const resolvedLoadItems = $derived.by(() => {
135
+ const loader = loadItems ?? callback;
136
+ if (!loader) {
137
+ throw new Error('Combobox.Async requires either loadItems or callback');
159
138
  }
160
- };
161
139
 
162
- // Debounced search handler
163
- const debouncedSearchValue = new Debounced(() => searchValue, searchDebounce);
140
+ return loader;
141
+ });
164
142
 
165
- const search = (search: string) => {
166
- currentPage = 1;
167
- fetchData(1, search, false);
168
- };
143
+ const asyncData = useAsyncComboboxData<TItem>({
144
+ items: boxWith(
145
+ () => items,
146
+ (next) => {
147
+ items = next;
148
+ }
149
+ ),
150
+ loadItems: boxWith(() => resolvedLoadItems),
151
+ search: boxWith(
152
+ () => searchValue,
153
+ (next) => {
154
+ searchValue = next;
155
+ }
156
+ ),
157
+ pageSize: boxWith(() => pageSize),
158
+ debounceMs: boxWith(() => debounceMs ?? searchDebounce),
159
+ loadImmediate: boxWith(() => loadImmediate),
160
+ dependency: boxWith(() => dependsOn?.()),
161
+ onDependencyReset: (dependency) => {
162
+ onDependencyChange?.(dependency);
169
163
 
170
- // Watch for search value changes
171
- watch(
172
- () => debouncedSearchValue.current,
173
- () => {
174
- search(debouncedSearchValue.current);
164
+ if (clearOnDependencyChange) {
165
+ value = isMultiple ? [] : '';
166
+ selectedItems = [];
167
+ }
175
168
  },
176
- {
177
- lazy: true,
178
- }
179
- );
169
+ });
170
+
171
+ const isLoading = $derived(asyncData.isLoading);
172
+ const isSearching = $derived(asyncData.isSearching);
173
+ const hasMore = $derived(asyncData.hasMore);
174
+ const error = $derived(asyncData.error);
180
175
 
181
- // Fetch on first open
182
176
  watch(
183
177
  () => open,
184
178
  () => {
185
179
  if (!open) return;
180
+ if (items.length > 0 || isLoading || loadImmediate) return;
186
181
 
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, '');
182
+ asyncData.reload(searchValue);
232
183
  }
233
184
  );
234
185
 
235
186
  const loadMore = () => {
236
187
  if (!isLoading && hasMore) {
237
- fetchData(currentPage + 1, searchValue, true);
188
+ asyncData.loadMore();
238
189
  }
239
190
  };
240
191
 
@@ -256,8 +207,7 @@
256
207
  };
257
208
 
258
209
  const retry = () => {
259
- error = null;
260
- fetchData(currentPage, searchValue);
210
+ asyncData.retry();
261
211
  };
262
212
 
263
213
  const scrollToTop = () => {
@@ -268,24 +218,45 @@
268
218
  behavior: 'smooth',
269
219
  });
270
220
  };
271
- function onSelectItem(item: CommandItem) {
221
+
222
+ const cacheSelectedItem = (item: TItem) => {
223
+ selectedItems = [...selectedItems.filter((selectedItem) => selectedItem.value !== item.value), item];
224
+ };
225
+
226
+ const removeSelectedItem = (itemValue: string) => {
227
+ selectedItems = selectedItems.filter((selectedItem) => selectedItem.value !== itemValue);
228
+ };
229
+
230
+ function onSelectItem(item: ComboboxItem) {
231
+ const typedItem = item as TItem;
232
+
272
233
  if (Array.isArray(value)) {
273
234
  let result = value;
274
235
  if (value.includes(item.value)) {
275
- result = allowDeselect ? value.filter((v) => v !== item.value) : value;
236
+ if (allowDeselect) {
237
+ result = value.filter((v) => v !== item.value);
238
+ removeSelectedItem(item.value);
239
+ }
276
240
  } else {
277
241
  result = [...value, item.value];
242
+ cacheSelectedItem(typedItem);
278
243
  }
279
- onSelect(result, item as TItem);
244
+ onSelect(result, typedItem);
280
245
  value = result;
281
246
  } else {
282
- let result = value;
247
+ let result = typeof value === 'string' ? value : '';
283
248
  if (item.value === value) {
284
- result = allowDeselect ? '' : value;
249
+ if (allowDeselect) {
250
+ result = '';
251
+ selectedItems = [];
252
+ } else {
253
+ cacheSelectedItem(typedItem);
254
+ }
285
255
  } else {
286
256
  result = item.value;
257
+ selectedItems = [typedItem];
287
258
  }
288
- onSelect(result, item as TItem);
259
+ onSelect(result, typedItem);
289
260
  value = result;
290
261
  }
291
262
 
@@ -307,11 +278,15 @@
307
278
  {#snippet clearBtn()}
308
279
  {#if clearable && isNotEmpty}
309
280
  <button
281
+ aria-label="Clear selection"
310
282
  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
283
  type="button"
312
284
  onclick={(event) => {
313
285
  event.stopPropagation();
314
- value = isMultiple ? [] : '';
286
+ const nextValue = isMultiple ? [] : '';
287
+ value = nextValue;
288
+ selectedItems = [];
289
+ onSelect(nextValue, null);
315
290
  }}
316
291
  >
317
292
  <Icon.Cross class="cgui:ms-auto" width={16} height={16} />
@@ -323,13 +298,15 @@
323
298
  {@const triggerProps = {
324
299
  class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
325
300
  'data-placeholder': boolAttr(isPlaceholder),
301
+ 'aria-expanded': open,
302
+ 'aria-busy': isLoading,
326
303
  }}
327
304
  {#if labelSnippet}
328
305
  <PopoverPrimitive.Trigger {...triggerProps}>
329
306
  {@render labelSnippet({
330
307
  placeholder: displayValue.current,
331
308
  value: isMultiple ? (value as string[]) : (value as string),
332
- selectedItems: selectedItems.current,
309
+ selectedItems: resolvedSelectedItems.current,
333
310
  })}
334
311
 
335
312
  {@render clearBtn()}
@@ -349,7 +326,7 @@
349
326
  {/if}
350
327
  {/snippet}
351
328
 
352
- {#snippet renderItem(item: CommandItem)}
329
+ {#snippet renderItem(item: ComboboxItem)}
353
330
  {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
354
331
  {@const itemAttrs = {
355
332
  value: itemValue,
@@ -388,8 +365,22 @@
388
365
  {/if}
389
366
  {/snippet}
390
367
 
368
+ {#snippet renderEmpty()}
369
+ {#if typeof empty === 'string'}
370
+ <CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
371
+ {:else if empty}
372
+ {@render empty({ props: {} })}
373
+ {:else}
374
+ <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
375
+ {/if}
376
+ {/snippet}
377
+
391
378
  {#snippet loadingSpinner()}
392
- <div class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default">
379
+ <div
380
+ role="status"
381
+ aria-live="polite"
382
+ class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default"
383
+ >
393
384
  <Spinner />
394
385
  </div>
395
386
  {/snippet}
@@ -401,7 +392,7 @@
401
392
  {/snippet}
402
393
 
403
394
  {#snippet errorState()}
404
- <div class="cgui:p-4 cgui:text-center cgui:text-body-2">
395
+ <div role="alert" class="cgui:p-4 cgui:text-center cgui:text-body-2">
405
396
  <p class="cgui:text-destructive cgui:mb-2">{error}</p>
406
397
  <button type="button" class="cgui:text-sm cgui:text-primary cgui:underline cgui:hover:no-underline" onclick={retry}>
407
398
  Try again
@@ -463,7 +454,7 @@
463
454
  {@render loadingMore()}
464
455
  {/if}
465
456
  {:else}
466
- <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
457
+ {@render renderEmpty()}
467
458
  {/if}
468
459
  </CommandPrimitive.Viewport>
469
460
 
@@ -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)) {
@@ -123,11 +123,14 @@
123
123
  {#snippet clearBtn()}
124
124
  {#if clearable && isNotEmpty}
125
125
  <button
126
+ aria-label="Clear selection"
126
127
  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
128
  type="button"
128
129
  onclick={(event) => {
129
130
  event.stopPropagation();
130
- value = isMultiple ? [] : '';
131
+ const nextValue = isMultiple ? [] : '';
132
+ value = nextValue;
133
+ onSelect(nextValue, null);
131
134
  }}
132
135
  >
133
136
  <Icon.Cross class="cgui:ms-auto" width={16} height={16} />
@@ -166,7 +169,7 @@
166
169
  {/if}
167
170
  {/snippet}
168
171
 
169
- {#snippet renderItem(item: CommandItem)}
172
+ {#snippet renderItem(item: ComboboxItem)}
170
173
  {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: _, ...restItem } = item}
171
174
  {@const itemAttrs = {
172
175
  value: itemValue,
@@ -205,6 +208,16 @@
205
208
  {/if}
206
209
  {/snippet}
207
210
 
211
+ {#snippet renderEmpty()}
212
+ {#if typeof empty === 'string'}
213
+ <CommandPrimitive.Empty>{empty}</CommandPrimitive.Empty>
214
+ {:else if empty}
215
+ {@render empty({ props: {} })}
216
+ {:else}
217
+ <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
218
+ {/if}
219
+ {/snippet}
220
+
208
221
  <PopoverPrimitive.Root bind:open {...restProps}>
209
222
  {@render renderTrigger()}
210
223
 
@@ -215,7 +228,7 @@
215
228
 
216
229
  <CommandPrimitive.List class="cgui:rounded-inherit" style={{ maxHeight: maxContentHeight }}>
217
230
  <CommandPrimitive.Viewport>
218
- <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
231
+ {@render renderEmpty()}
219
232
 
220
233
  {#each groupedItems.items.current as [groupKey, items] (groupKey || '__ungrouped__')}
221
234
  {#if groupKey}
@@ -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,7 @@ 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;
7
8
  /**
8
9
  * Base props shared by both single and multiple selection modes
9
10
  */
@@ -29,10 +30,10 @@ type ComboboxBaseProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsProp
29
30
  label?: Snippet<[{
30
31
  placeholder: string;
31
32
  value: string | string[];
32
- selectedItems: CommandItem[];
33
+ selectedItems: ComboboxItem[];
33
34
  }]>;
34
35
  /** Callback when selection changes */
35
- onSelect?: (value: string | string[], item: CommandItem | null) => void;
36
+ onSelect?: (value: string | string[], item: ComboboxItem | null) => void;
36
37
  };
37
38
  type ComboboxSingleProps = ComboboxBaseProps & {
38
39
  type: typeof SELECTION_TYPE.SINGLE;
@@ -46,58 +47,63 @@ export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
46
47
  /**
47
48
  * Async Combobox Types
48
49
  */
49
- export type ComboboxAsyncCallbackParams = {
50
+ export type ComboboxAsyncLoadParams = {
50
51
  page: number;
51
52
  pageSize: number;
52
53
  search: string;
54
+ signal: AbortSignal;
55
+ dependency?: unknown;
53
56
  };
54
- export type ComboboxAsyncCallbackResult<TItem extends CommandItem> = {
57
+ export type ComboboxAsyncLoadResult<TItem extends ComboboxItem> = {
55
58
  items: TItem[];
56
59
  hasMore: boolean;
57
60
  };
58
- export type ComboboxAsyncCallback<TItem extends CommandItem = CommandItem> = (params: ComboboxAsyncCallbackParams) => Promise<ComboboxAsyncCallbackResult<TItem>>;
59
- type ComboboxAsyncBaseProps<TItem extends CommandItem> = Omit<ComboboxBaseProps, 'collection' | 'item' | 'onSelect'> & {
61
+ export type ComboboxAsyncLoader<TItem extends ComboboxItem = ComboboxItem> = (params: ComboboxAsyncLoadParams) => Promise<ComboboxAsyncLoadResult<TItem>>;
62
+ /** @deprecated Use ComboboxAsyncLoadParams instead. */
63
+ export type ComboboxAsyncCallbackParams = ComboboxAsyncLoadParams;
64
+ /** @deprecated Use ComboboxAsyncLoadResult instead. */
65
+ export type ComboboxAsyncCallbackResult<TItem extends ComboboxItem> = ComboboxAsyncLoadResult<TItem>;
66
+ /** @deprecated Use ComboboxAsyncLoader instead. */
67
+ export type ComboboxAsyncCallback<TItem extends ComboboxItem = ComboboxItem> = ComboboxAsyncLoader<TItem>;
68
+ type ComboboxAsyncLoadProp<TItem extends ComboboxItem> = {
69
+ /** Preferred async loader API */
70
+ loadItems: ComboboxAsyncLoader<TItem>;
71
+ callback?: never;
72
+ } | {
73
+ /** @deprecated Use loadItems instead. Kept for backwards compatibility. */
74
+ callback: ComboboxAsyncLoader<TItem>;
75
+ loadItems?: never;
76
+ };
77
+ type ComboboxAsyncBaseProps<TItem extends ComboboxItem> = Omit<ComboboxBaseProps, 'collection' | 'item' | 'onSelect'> & ComboboxAsyncLoadProp<TItem> & {
60
78
  items?: TItem[];
79
+ /** Cache of selected item objects, useful when selected values are not in the currently loaded async page. */
80
+ selectedItems?: TItem[];
61
81
  item?: Snippet<[{
62
82
  item: TItem;
63
83
  props: Record<string, unknown>;
64
84
  }]>;
65
- /** Callback when selection changes */
66
85
  onSelect?: (value: string | string[], item: TItem | null) => void;
67
- /** Loading state renderer */
68
86
  loading?: Snippet;
69
- /** Current page (default: 1) */
70
- page?: number;
71
- /** Page size for pagination (default: 20) */
72
87
  pageSize?: number;
73
- /** Async data callback */
74
- callback: ComboboxAsyncCallback<TItem>;
75
- /** Load immediate data when combobox is mounted (default: true) */
76
88
  loadImmediate?: boolean;
77
- /** Group definitions (for labels/ordering) */
78
89
  groups?: CommandGroup[];
79
- /** Debounce delay for search in milliseconds (default: 300ms) */
90
+ /** Preferred name */
91
+ debounceMs?: number;
92
+ /** @deprecated Use debounceMs instead. */
80
93
  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
94
  dependsOn?: () => unknown;
87
- /**
88
- * Whether to clear the selected value when dependsOn changes **(default: true)**
89
- */
90
95
  clearOnDependencyChange?: boolean;
96
+ onDependencyChange?: (value: unknown) => void;
91
97
  };
92
- type ComboboxAsyncSingleProps<TItem extends CommandItem> = ComboboxAsyncBaseProps<TItem> & {
98
+ type ComboboxAsyncSingleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
93
99
  /** Selection mode (default: 'single') */
94
100
  type?: 'single';
95
101
  value?: string;
96
102
  };
97
- type ComboboxAsyncMultipleProps<TItem extends CommandItem> = ComboboxAsyncBaseProps<TItem> & {
103
+ type ComboboxAsyncMultipleProps<TItem extends ComboboxItem> = ComboboxAsyncBaseProps<TItem> & {
98
104
  /** Selection mode */
99
105
  type: 'multiple';
100
106
  value?: string[];
101
107
  };
102
- export type ComboboxAsyncProps<TItem extends CommandItem> = ComboboxAsyncSingleProps<TItem> | ComboboxAsyncMultipleProps<TItem>;
108
+ export type ComboboxAsyncProps<TItem extends ComboboxItem> = ComboboxAsyncSingleProps<TItem> | ComboboxAsyncMultipleProps<TItem>;
103
109
  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.13",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",