@casinogate/ui 1.10.8 → 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,7 @@
34
36
  variant = 'default',
35
37
  rounded = 'default',
36
38
  fullWidth = true,
37
- closeOnSelect = true,
39
+ closeOnSelect,
38
40
 
39
41
  maxContentHeight,
40
42
 
@@ -46,10 +48,18 @@
46
48
  searchDebounce = 300,
47
49
  dependsOn,
48
50
  clearOnDependencyChange = true,
51
+ onSelect,
49
52
 
50
53
  ...restProps
51
54
  }: ComboboxAsyncProps = $props();
52
55
 
56
+ const isMultiple = type === 'multiple';
57
+
58
+ // Initialize value based on type
59
+ if (value === undefined) {
60
+ value = isMultiple ? ([] as string[]) : '';
61
+ }
62
+
53
63
  let items = $state<CommandItem[]>(initialItems);
54
64
  let isLoading = $state(false);
55
65
  let isSearching = $state(false);
@@ -82,13 +92,32 @@
82
92
  const groupMap = $derived(new Map(collection.groups.map((g) => [g.value, g])));
83
93
  const groupedItems = $derived(collection.group());
84
94
 
95
+ const isSelected = (itemValue: string): boolean => {
96
+ if (Array.isArray(value)) return value.includes(itemValue);
97
+ return value === itemValue;
98
+ };
99
+
85
100
  const displayValue = $derived.by(() => {
86
- const val = items.find((item) => item.value === value)?.label ?? value;
87
- 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;
88
116
  });
89
117
 
90
118
  const isPlaceholder = $derived.by(() => {
91
- return value.trim() === '';
119
+ if (Array.isArray(value)) return value.length === 0;
120
+ return typeof value === 'string' && value.trim() === '';
92
121
  });
93
122
 
94
123
  const hasResults = $derived(collection.size > 0);
@@ -185,7 +214,7 @@
185
214
 
186
215
  // Clear selected value if enabled
187
216
  if (clearOnDependencyChange) {
188
- value = '';
217
+ value = isMultiple ? [] : '';
189
218
  }
190
219
 
191
220
  // Reload data
@@ -193,6 +222,34 @@
193
222
  }
194
223
  );
195
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
+
196
253
  const loadMore = () => {
197
254
  if (!isLoading && hasMore) {
198
255
  fetchData(currentPage + 1, searchValue, true);
@@ -230,16 +287,25 @@
230
287
  });
231
288
  };
232
289
 
233
- const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, onSelect?: AnyFn) => () => {
234
- onSelect?.();
290
+ const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, itemOnSelect?: AnyFn) => () => {
291
+ itemOnSelect?.();
235
292
 
236
- if (item.value === value) {
237
- 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
+ }
238
299
  } else {
239
- value = item.value;
300
+ if (item.value === value) {
301
+ value = allowDeselect ? '' : value;
302
+ } else {
303
+ value = item.value;
304
+ }
240
305
  }
241
306
 
242
- if (closeOnSelect) {
307
+ const shouldClose = closeOnSelect ?? !isMultiple;
308
+ if (shouldClose) {
243
309
  open = false;
244
310
  }
245
311
  };
@@ -250,7 +316,11 @@
250
316
  class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
251
317
  'data-placeholder': boolAttr(isPlaceholder),
252
318
  }}
253
- {#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}
254
324
  <PopoverPrimitive.Trigger {...triggerProps}>
255
325
  {#snippet child({ props })}
256
326
  {@render triggerSnippet?.({ props, displayValue })}
@@ -264,11 +334,11 @@
264
334
  {/snippet}
265
335
 
266
336
  {#snippet renderItem(item: CommandItem)}
267
- {@const { value: itemValue, label, icon: Icon, shortcut, onSelect, ...restItem } = item}
337
+ {@const { value: itemValue, label, icon: Icon, shortcut, onSelect: itemOnSelect, ...restItem } = item}
268
338
  {@const itemAttrs = {
269
339
  value: itemValue,
270
- onSelect: onSelectItem(item, onSelect),
271
- 'data-selected': boolAttr(itemValue === value),
340
+ onSelect: onSelectItem(item, itemOnSelect),
341
+ 'data-selected': boolAttr(isSelected(itemValue)),
272
342
  ...restItem,
273
343
  }}
274
344
 
@@ -283,7 +353,7 @@
283
353
  {#if Icon}
284
354
  <Icon width={16} height={16} />
285
355
  {/if}
286
- {label ?? value}
356
+ {label ?? itemValue}
287
357
  {#if shortcut && shortcut.length > 0}
288
358
  <Kbd.Group>
289
359
  {#each shortcut as shortcut (shortcut)}
@@ -291,7 +361,7 @@
291
361
  {/each}
292
362
  </Kbd.Group>
293
363
  {/if}
294
- {#if itemValue === value}
364
+ {#if isSelected(itemValue)}
295
365
  <span
296
366
  class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
297
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.8",
3
+ "version": "1.10.9",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",