@casinogate/ui 1.10.7 → 1.10.9

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.
@@ -19,14 +19,16 @@
19
19
 
20
20
  let {
21
21
  open = $bindable(false),
22
- value = $bindable(''),
22
+ value = $bindable(),
23
23
  searchValue = $bindable(''),
24
+ type,
24
25
  groups = [],
25
26
  empty,
26
27
  placeholder = 'Select an option',
27
28
  searchPlaceholder,
28
29
  trigger: triggerSnippet,
29
30
  item: itemSnippet,
31
+ label: labelSnippet,
30
32
  portalDisabled = false,
31
33
  allowDeselect = true,
32
34
  triggerClass,
@@ -34,7 +36,9 @@
34
36
  variant = 'default',
35
37
  rounded = 'default',
36
38
  fullWidth = true,
37
- closeOnSelect = true,
39
+ closeOnSelect,
40
+
41
+ maxContentHeight,
38
42
 
39
43
  loading: loadingSnippet,
40
44
  pageSize = 10,
@@ -44,10 +48,18 @@
44
48
  searchDebounce = 300,
45
49
  dependsOn,
46
50
  clearOnDependencyChange = true,
51
+ onSelect,
47
52
 
48
53
  ...restProps
49
54
  }: ComboboxAsyncProps = $props();
50
55
 
56
+ const isMultiple = type === 'multiple';
57
+
58
+ // Initialize value based on type
59
+ if (value === undefined) {
60
+ value = isMultiple ? ([] as string[]) : '';
61
+ }
62
+
51
63
  let items = $state<CommandItem[]>(initialItems);
52
64
  let isLoading = $state(false);
53
65
  let isSearching = $state(false);
@@ -80,13 +92,32 @@
80
92
  const groupMap = $derived(new Map(collection.groups.map((g) => [g.value, g])));
81
93
  const groupedItems = $derived(collection.group());
82
94
 
95
+ const isSelected = (itemValue: string): boolean => {
96
+ if (Array.isArray(value)) return value.includes(itemValue);
97
+ return value === itemValue;
98
+ };
99
+
83
100
  const displayValue = $derived.by(() => {
84
- const val = items.find((item) => item.value === value)?.label ?? value;
85
- return val.trim() !== '' ? val : placeholder;
101
+ if (typeof value === 'string' && value.trim() !== '') {
102
+ const item = items.find((i) => i.value === value);
103
+ return item?.label ?? value;
104
+ }
105
+
106
+ if (Array.isArray(value) && value.length > 0) {
107
+ return value
108
+ .map((v) => {
109
+ const item = items.find((i) => i.value === v);
110
+ return item?.label ?? v;
111
+ })
112
+ .join(', ');
113
+ }
114
+
115
+ return placeholder;
86
116
  });
87
117
 
88
118
  const isPlaceholder = $derived.by(() => {
89
- return value.trim() === '';
119
+ if (Array.isArray(value)) return value.length === 0;
120
+ return typeof value === 'string' && value.trim() === '';
90
121
  });
91
122
 
92
123
  const hasResults = $derived(collection.size > 0);
@@ -183,7 +214,7 @@
183
214
 
184
215
  // Clear selected value if enabled
185
216
  if (clearOnDependencyChange) {
186
- value = '';
217
+ value = isMultiple ? [] : '';
187
218
  }
188
219
 
189
220
  // Reload data
@@ -191,6 +222,34 @@
191
222
  }
192
223
  );
193
224
 
225
+ // onSelect callback
226
+ let previousValue = $state<string | string[]>(
227
+ isMultiple ? [...((value as string[]) ?? [])] : ((value as string) ?? '')
228
+ );
229
+
230
+ watch(
231
+ () => value,
232
+ (newValue) => {
233
+ if (!onSelect) {
234
+ previousValue = isMultiple ? [...((newValue as string[]) ?? [])] : ((newValue as string) ?? '');
235
+ return;
236
+ }
237
+
238
+ if (typeof newValue === 'string') {
239
+ const item = newValue ? (items.find((i) => i.value === newValue) ?? null) : null;
240
+ onSelect(newValue, item);
241
+ } else if (Array.isArray(newValue)) {
242
+ const prev = Array.isArray(previousValue) ? previousValue : [];
243
+ const added = newValue.find((v) => !prev.includes(v));
244
+ const item = added ? (items.find((i) => i.value === added) ?? null) : null;
245
+ onSelect(newValue, item);
246
+ }
247
+
248
+ previousValue = isMultiple ? [...((newValue as string[]) ?? [])] : ((newValue as string) ?? '');
249
+ },
250
+ { lazy: true }
251
+ );
252
+
194
253
  const loadMore = () => {
195
254
  if (!isLoading && hasMore) {
196
255
  fetchData(currentPage + 1, searchValue, true);
@@ -228,16 +287,25 @@
228
287
  });
229
288
  };
230
289
 
231
- const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, onSelect?: AnyFn) => () => {
232
- onSelect?.();
290
+ const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, itemOnSelect?: AnyFn) => () => {
291
+ itemOnSelect?.();
233
292
 
234
- if (item.value === value) {
235
- value = allowDeselect ? '' : value;
293
+ if (Array.isArray(value)) {
294
+ if (value.includes(item.value)) {
295
+ value = allowDeselect ? value.filter((v) => v !== item.value) : value;
296
+ } else {
297
+ value = [...value, item.value];
298
+ }
236
299
  } else {
237
- value = item.value;
300
+ if (item.value === value) {
301
+ value = allowDeselect ? '' : value;
302
+ } else {
303
+ value = item.value;
304
+ }
238
305
  }
239
306
 
240
- if (closeOnSelect) {
307
+ const shouldClose = closeOnSelect ?? !isMultiple;
308
+ if (shouldClose) {
241
309
  open = false;
242
310
  }
243
311
  };
@@ -248,7 +316,11 @@
248
316
  class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
249
317
  'data-placeholder': boolAttr(isPlaceholder),
250
318
  }}
251
- {#if triggerSnippet}
319
+ {#if labelSnippet}
320
+ <PopoverPrimitive.Trigger {...triggerProps}>
321
+ {@render labelSnippet({ placeholder: displayValue, value: isMultiple ? (value as string[]) : (value as string) })}
322
+ </PopoverPrimitive.Trigger>
323
+ {:else if triggerSnippet}
252
324
  <PopoverPrimitive.Trigger {...triggerProps}>
253
325
  {#snippet child({ props })}
254
326
  {@render triggerSnippet?.({ props, displayValue })}
@@ -262,11 +334,11 @@
262
334
  {/snippet}
263
335
 
264
336
  {#snippet renderItem(item: CommandItem)}
265
- {@const { value: itemValue, label, icon: Icon, shortcut, onSelect, ...restItem } = item}
337
+ {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: itemOnSelect, ...restItem } = item}
266
338
  {@const itemAttrs = {
267
339
  value: itemValue,
268
- onSelect: onSelectItem(item, onSelect),
269
- 'data-selected': boolAttr(itemValue === value),
340
+ onSelect: onSelectItem(item, itemOnSelect),
341
+ 'data-selected': boolAttr(isSelected(itemValue)),
270
342
  ...restItem,
271
343
  }}
272
344
 
@@ -281,7 +353,7 @@
281
353
  {#if Icon}
282
354
  <Icon width={16} height={16} />
283
355
  {/if}
284
- {label ?? value}
356
+ {label ?? itemValue}
285
357
  {#if shortcut && shortcut.length > 0}
286
358
  <Kbd.Group>
287
359
  {#each shortcut as shortcut (shortcut)}
@@ -289,7 +361,7 @@
289
361
  {/each}
290
362
  </Kbd.Group>
291
363
  {/if}
292
- {#if itemValue === value}
364
+ {#if isSelected(itemValue)}
293
365
  <span
294
366
  class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
295
367
  transition:fly={{ duration: 250, y: 4, easing: cubicInOut }}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { boolAttr } from '../../internal/utils/attrs.js';
3
3
  import { cn } from '../../internal/utils/common.js';
4
+ import { watch } from 'runed';
4
5
  import type { AnyFn } from 'svelte-toolbelt';
5
6
  import { cubicInOut } from 'svelte/easing';
6
7
  import { fly } from 'svelte/transition';
@@ -13,14 +14,16 @@
13
14
 
14
15
  let {
15
16
  open = $bindable(false),
16
- value = $bindable(''),
17
+ value = $bindable(),
17
18
  searchValue = $bindable(''),
19
+ type,
18
20
  collection,
19
21
  empty,
20
22
  placeholder = 'Select an option',
21
23
  searchPlaceholder,
22
24
  trigger: triggerSnippet,
23
25
  item: itemSnippet,
26
+ label: labelSnippet,
24
27
  portalDisabled = false,
25
28
  allowDeselect = true,
26
29
  triggerClass,
@@ -28,34 +31,97 @@
28
31
  variant = 'default',
29
32
  rounded = 'default',
30
33
  fullWidth = true,
31
- closeOnSelect = true,
34
+ closeOnSelect,
32
35
  maxContentHeight,
36
+ onSelect,
33
37
  ...restProps
34
38
  }: ComboboxProps = $props();
35
39
 
40
+ const isMultiple = $derived(type === 'multiple');
41
+
42
+ // Initialize value based on type
43
+ if (value === undefined) {
44
+ value = isMultiple ? ([] as string[]) : '';
45
+ }
46
+
36
47
  const groupMap = $derived(new Map(collection.groups.map((g) => [g.value, g])));
37
48
 
38
49
  const groupedItems = $derived(collection.group());
39
50
 
51
+ const isSelected = (itemValue: string): boolean => {
52
+ if (Array.isArray(value)) return value.includes(itemValue);
53
+ return value === itemValue;
54
+ };
55
+
40
56
  const displayValue = $derived.by(() => {
41
- const val = collection.items.find((item) => item.value === value)?.label ?? value;
42
- return val.trim() !== '' ? val : placeholder;
57
+ if (typeof value === 'string' && value.trim() !== '') {
58
+ const item = collection.items.find((i) => i.value === value);
59
+ return item?.label ?? value;
60
+ }
61
+
62
+ if (Array.isArray(value) && value.length > 0) {
63
+ return value
64
+ .map((v) => {
65
+ const item = collection.items.find((i) => i.value === v);
66
+ return item?.label ?? v;
67
+ })
68
+ .join(', ');
69
+ }
70
+
71
+ return placeholder;
43
72
  });
44
73
 
45
74
  const isPlaceholder = $derived.by(() => {
46
- return value.trim() === '';
75
+ if (Array.isArray(value)) return value.length === 0;
76
+ return typeof value === 'string' && value.trim() === '';
47
77
  });
48
78
 
49
- const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, onSelect?: AnyFn) => () => {
50
- onSelect?.();
51
-
52
- if (item.value === value) {
53
- value = allowDeselect ? '' : value;
79
+ let previousValue = $state<string | string[]>(
80
+ isMultiple ? [...((value as string[]) ?? [])] : ((value as string) ?? '')
81
+ );
82
+
83
+ watch(
84
+ () => value,
85
+ (newValue) => {
86
+ if (!onSelect) {
87
+ previousValue = isMultiple ? [...((newValue as string[]) ?? [])] : ((newValue as string) ?? '');
88
+ return;
89
+ }
90
+
91
+ if (typeof newValue === 'string') {
92
+ const item = newValue ? (collection.items.find((i) => i.value === newValue) ?? null) : null;
93
+ onSelect(newValue, item);
94
+ } else if (Array.isArray(newValue)) {
95
+ const prev = Array.isArray(previousValue) ? previousValue : [];
96
+ const added = newValue.find((v) => !prev.includes(v));
97
+ const item = added ? (collection.items.find((i) => i.value === added) ?? null) : null;
98
+ onSelect(newValue, item);
99
+ }
100
+
101
+ previousValue = isMultiple ? [...((newValue as string[]) ?? [])] : ((newValue as string) ?? '');
102
+ },
103
+ { lazy: true }
104
+ );
105
+
106
+ const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, itemOnSelect?: AnyFn) => () => {
107
+ itemOnSelect?.();
108
+
109
+ if (Array.isArray(value)) {
110
+ if (value.includes(item.value)) {
111
+ value = allowDeselect ? value.filter((v) => v !== item.value) : value;
112
+ } else {
113
+ value = [...value, item.value];
114
+ }
54
115
  } else {
55
- value = item.value;
116
+ if (item.value === value) {
117
+ value = allowDeselect ? '' : value;
118
+ } else {
119
+ value = item.value;
120
+ }
56
121
  }
57
122
 
58
- if (closeOnSelect) {
123
+ const shouldClose = closeOnSelect ?? !isMultiple;
124
+ if (shouldClose) {
59
125
  open = false;
60
126
  }
61
127
  };
@@ -66,7 +132,11 @@
66
132
  class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
67
133
  'data-placeholder': boolAttr(isPlaceholder),
68
134
  }}
69
- {#if triggerSnippet}
135
+ {#if labelSnippet}
136
+ <PopoverPrimitive.Trigger {...triggerProps}>
137
+ {@render labelSnippet({ placeholder: displayValue, value: isMultiple ? (value as string[]) : (value as string) })}
138
+ </PopoverPrimitive.Trigger>
139
+ {:else if triggerSnippet}
70
140
  <PopoverPrimitive.Trigger {...triggerProps}>
71
141
  {#snippet child({ props })}
72
142
  {@render triggerSnippet?.({ props, displayValue })}
@@ -80,11 +150,11 @@
80
150
  {/snippet}
81
151
 
82
152
  {#snippet renderItem(item: CommandItem)}
83
- {@const { value: itemValue, label, icon: Icon, shortcut, onSelect, ...restItem } = item}
153
+ {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: itemOnSelect, ...restItem } = item}
84
154
  {@const itemAttrs = {
85
155
  value: itemValue,
86
- onSelect: onSelectItem(item, onSelect),
87
- 'data-selected': boolAttr(itemValue === value),
156
+ onSelect: onSelectItem(item, itemOnSelect),
157
+ 'data-selected': boolAttr(isSelected(itemValue)),
88
158
  ...restItem,
89
159
  }}
90
160
 
@@ -99,7 +169,7 @@
99
169
  {#if Icon}
100
170
  <Icon width={16} height={16} />
101
171
  {/if}
102
- {label ?? value}
172
+ {label ?? itemValue}
103
173
  {#if shortcut && shortcut.length > 0}
104
174
  <Kbd.Group>
105
175
  {#each shortcut as shortcut (shortcut)}
@@ -107,7 +177,7 @@
107
177
  {/each}
108
178
  </Kbd.Group>
109
179
  {/if}
110
- {#if itemValue === value}
180
+ {#if isSelected(itemValue)}
111
181
  <span
112
182
  class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
113
183
  transition:fly={{ duration: 250, y: 4, easing: cubicInOut }}
@@ -3,9 +3,11 @@ import type { CommandGroup, CommandItem, CommandProps } from '../command/index.j
3
3
  import type { PrimitivePopoverRootProps } from '../popover/types.js';
4
4
  import type { ComboboxTriggerVariantsProps } from './styles.js';
5
5
  export type ComboboxRootProps = PrimitivePopoverRootProps;
6
- export type ComboboxProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsProps & {
6
+ /**
7
+ * Base props shared by both single and multiple selection modes
8
+ */
9
+ type ComboboxBaseProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsProps & {
7
10
  collection: CommandProps['collection'];
8
- value?: string;
9
11
  empty?: CommandProps['empty'];
10
12
  placeholder?: string;
11
13
  searchPlaceholder?: string;
@@ -20,7 +22,25 @@ export type ComboboxProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsP
20
22
  displayValue: string;
21
23
  }]>;
22
24
  item?: CommandProps['item'];
25
+ /** Custom label renderer for trigger display */
26
+ label?: Snippet<[{
27
+ placeholder: string;
28
+ value: string | string[];
29
+ }]>;
30
+ /** Callback when selection changes */
31
+ onSelect?: (value: string | string[], item: CommandItem | null) => void;
32
+ };
33
+ type ComboboxSingleProps = ComboboxBaseProps & {
34
+ /** Selection mode (default: 'single') */
35
+ type?: 'single';
36
+ value?: string;
37
+ };
38
+ type ComboboxMultipleProps = ComboboxBaseProps & {
39
+ /** Selection mode */
40
+ type: 'multiple';
41
+ value?: string[];
23
42
  };
43
+ export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
24
44
  /**
25
45
  * Async Combobox Types
26
46
  */
@@ -34,7 +54,7 @@ export type ComboboxAsyncCallbackResult = {
34
54
  hasMore: boolean;
35
55
  };
36
56
  export type ComboboxAsyncCallback = (params: ComboboxAsyncCallbackParams) => Promise<ComboboxAsyncCallbackResult>;
37
- export type ComboboxAsyncProps = Omit<ComboboxProps, 'collection'> & {
57
+ type ComboboxAsyncBaseProps = Omit<ComboboxBaseProps, 'collection'> & {
38
58
  /** Loading state renderer */
39
59
  loading?: Snippet;
40
60
  /** Current page (default: 1) */
@@ -62,3 +82,15 @@ export type ComboboxAsyncProps = Omit<ComboboxProps, 'collection'> & {
62
82
  */
63
83
  clearOnDependencyChange?: boolean;
64
84
  };
85
+ type ComboboxAsyncSingleProps = ComboboxAsyncBaseProps & {
86
+ /** Selection mode (default: 'single') */
87
+ type?: 'single';
88
+ value?: string;
89
+ };
90
+ type ComboboxAsyncMultipleProps = ComboboxAsyncBaseProps & {
91
+ /** Selection mode */
92
+ type: 'multiple';
93
+ value?: string[];
94
+ };
95
+ export type ComboboxAsyncProps = ComboboxAsyncSingleProps | ComboboxAsyncMultipleProps;
96
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casinogate/ui",
3
- "version": "1.10.7",
3
+ "version": "1.10.9",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",