@casinogate/ui 1.9.5 → 1.9.7
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.
- package/dist/assets/css/root.css +3 -0
- package/dist/components/combobox/combobox.async.svelte +403 -0
- package/dist/components/combobox/combobox.async.svelte.d.ts +4 -0
- package/dist/components/combobox/exports.d.ts +1 -0
- package/dist/components/combobox/exports.js +1 -0
- package/dist/components/combobox/types.d.ts +42 -1
- package/dist/components/dialog/types.d.ts +2 -4
- package/dist/components/number-input/components/number-input.root.svelte +2 -0
- package/dist/components/select/index.d.ts +1 -1
- package/dist/components/select/select.async.svelte +58 -3
- package/dist/components/select/types.d.ts +13 -1
- package/package.json +1 -1
package/dist/assets/css/root.css
CHANGED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CommandCollection, CommandItem } from '../command/index.js';
|
|
3
|
+
import type { ComboboxAsyncProps } from './types.js';
|
|
4
|
+
|
|
5
|
+
import { boolAttr } from '../../internal/utils/attrs.js';
|
|
6
|
+
import { cn } from '../../internal/utils/common.js';
|
|
7
|
+
import { watch } from 'runed';
|
|
8
|
+
import type { AnyFn } from 'svelte-toolbelt';
|
|
9
|
+
import { onMountEffect } from 'svelte-toolbelt';
|
|
10
|
+
import { cubicInOut } from 'svelte/easing';
|
|
11
|
+
import { fly } from 'svelte/transition';
|
|
12
|
+
import { CommandApi, CommandPrimitive } from '../command/index.js';
|
|
13
|
+
import { Icon as IconComponent } from '../icons/index.js';
|
|
14
|
+
import { Kbd } from '../kbd/index.js';
|
|
15
|
+
import { PopoverPrimitive } from '../popover/index.js';
|
|
16
|
+
import { Spinner } from '../spinner/index.js';
|
|
17
|
+
import { comboboxTriggerVariants } from './styles.js';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
open = $bindable(false),
|
|
21
|
+
value = $bindable(''),
|
|
22
|
+
searchValue = $bindable(''),
|
|
23
|
+
groups = [],
|
|
24
|
+
empty,
|
|
25
|
+
placeholder = 'Select an option',
|
|
26
|
+
searchPlaceholder,
|
|
27
|
+
trigger: triggerSnippet,
|
|
28
|
+
item: itemSnippet,
|
|
29
|
+
portalDisabled = false,
|
|
30
|
+
allowDeselect = true,
|
|
31
|
+
triggerClass,
|
|
32
|
+
size = 'default',
|
|
33
|
+
variant = 'default',
|
|
34
|
+
rounded = 'default',
|
|
35
|
+
fullWidth = true,
|
|
36
|
+
closeOnSelect = true,
|
|
37
|
+
|
|
38
|
+
loading: loadingSnippet,
|
|
39
|
+
pageSize = 10,
|
|
40
|
+
callback,
|
|
41
|
+
initialItems = [],
|
|
42
|
+
loadImmediate = true,
|
|
43
|
+
searchDebounce = 300,
|
|
44
|
+
dependsOn,
|
|
45
|
+
clearOnDependencyChange = true,
|
|
46
|
+
|
|
47
|
+
...restProps
|
|
48
|
+
}: ComboboxAsyncProps = $props();
|
|
49
|
+
|
|
50
|
+
let items = $state<CommandItem[]>(initialItems);
|
|
51
|
+
let isLoading = $state(false);
|
|
52
|
+
let hasMore = $state(true);
|
|
53
|
+
let currentPage = $state(1);
|
|
54
|
+
let error = $state<string | null>(null);
|
|
55
|
+
let viewportRef = $state<HTMLElement | null>(null);
|
|
56
|
+
let searchTimeout = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
57
|
+
let lastSearch = $state('');
|
|
58
|
+
|
|
59
|
+
const createCollection = (itemList: CommandItem[]): CommandCollection => {
|
|
60
|
+
const groupOrderMap = new Map<string, number>();
|
|
61
|
+
groups.forEach((g, index) => {
|
|
62
|
+
groupOrderMap.set(g.value, g.order ?? index);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return CommandApi.createCollection({
|
|
66
|
+
items: itemList,
|
|
67
|
+
groups: groups,
|
|
68
|
+
groupSort: (a, b) => {
|
|
69
|
+
const orderA = groupOrderMap.get(a) ?? Infinity;
|
|
70
|
+
const orderB = groupOrderMap.get(b) ?? Infinity;
|
|
71
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
72
|
+
return a.localeCompare(b);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const collection = $derived(createCollection(items));
|
|
78
|
+
const groupMap = $derived(new Map(collection.groups.map((g) => [g.value, g])));
|
|
79
|
+
const groupedItems = $derived(collection.group());
|
|
80
|
+
|
|
81
|
+
const displayValue = $derived.by(() => {
|
|
82
|
+
const val = items.find((item) => item.value === value)?.label ?? value;
|
|
83
|
+
return val.trim() !== '' ? val : placeholder;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const isPlaceholder = $derived.by(() => {
|
|
87
|
+
return value.trim() === '';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const hasResults = $derived(collection.size > 0);
|
|
91
|
+
|
|
92
|
+
const fetchData = async (page: number, search: string, append = false) => {
|
|
93
|
+
if (isLoading) return;
|
|
94
|
+
|
|
95
|
+
isLoading = true;
|
|
96
|
+
error = null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await callback({ page, pageSize, search });
|
|
100
|
+
|
|
101
|
+
if (append) {
|
|
102
|
+
items = [...items, ...result.items];
|
|
103
|
+
} else {
|
|
104
|
+
items = result.items;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
currentPage = page;
|
|
108
|
+
hasMore = result.hasMore;
|
|
109
|
+
lastSearch = search;
|
|
110
|
+
|
|
111
|
+
// Check if viewport needs more items after render
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
checkNeedMoreItems();
|
|
114
|
+
});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
error = err instanceof Error ? err.message : 'Failed to fetch data';
|
|
117
|
+
} finally {
|
|
118
|
+
isLoading = false;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Auto-fill viewport if content doesn't overflow
|
|
123
|
+
const checkNeedMoreItems = () => {
|
|
124
|
+
if (!viewportRef || isLoading || !hasMore) return;
|
|
125
|
+
|
|
126
|
+
const { scrollHeight, clientHeight } = viewportRef;
|
|
127
|
+
|
|
128
|
+
// If content doesn't fill viewport, load more
|
|
129
|
+
if (scrollHeight <= clientHeight) {
|
|
130
|
+
loadMore();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Debounced search handler
|
|
135
|
+
const debouncedSearch = (search: string) => {
|
|
136
|
+
if (searchTimeout) {
|
|
137
|
+
clearTimeout(searchTimeout);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
searchTimeout = setTimeout(() => {
|
|
141
|
+
currentPage = 1;
|
|
142
|
+
fetchData(1, search, false);
|
|
143
|
+
}, searchDebounce);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Watch for search value changes
|
|
147
|
+
watch(
|
|
148
|
+
() => searchValue,
|
|
149
|
+
() => {
|
|
150
|
+
if (searchValue !== lastSearch) {
|
|
151
|
+
debouncedSearch(searchValue);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Fetch on first open or check viewport fill
|
|
157
|
+
watch(
|
|
158
|
+
() => open,
|
|
159
|
+
() => {
|
|
160
|
+
if (!open) return;
|
|
161
|
+
|
|
162
|
+
// If no items yet and not loading, fetch first page
|
|
163
|
+
if (items.length === 0 && !isLoading && !loadImmediate) {
|
|
164
|
+
fetchData(1, searchValue);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If already have items, check if viewport needs more after it renders
|
|
169
|
+
requestAnimationFrame(() => {
|
|
170
|
+
checkNeedMoreItems();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Fetch immediate data on mount
|
|
176
|
+
onMountEffect(() => {
|
|
177
|
+
if (loadImmediate) fetchData(1, searchValue);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Reset page on unmount
|
|
181
|
+
$effect(() => {
|
|
182
|
+
return () => {
|
|
183
|
+
currentPage = 1;
|
|
184
|
+
if (searchTimeout) {
|
|
185
|
+
clearTimeout(searchTimeout);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Watch for dependency changes and reset
|
|
191
|
+
let previousDependency = $state<unknown>(dependsOn?.());
|
|
192
|
+
watch(
|
|
193
|
+
() => dependsOn?.(),
|
|
194
|
+
(newValue) => {
|
|
195
|
+
if (previousDependency === undefined && newValue === undefined) return;
|
|
196
|
+
if (previousDependency === newValue) return;
|
|
197
|
+
|
|
198
|
+
previousDependency = newValue;
|
|
199
|
+
|
|
200
|
+
// Reset state
|
|
201
|
+
items = [];
|
|
202
|
+
currentPage = 1;
|
|
203
|
+
hasMore = true;
|
|
204
|
+
error = null;
|
|
205
|
+
searchValue = '';
|
|
206
|
+
lastSearch = '';
|
|
207
|
+
|
|
208
|
+
// Clear selected value if enabled
|
|
209
|
+
if (clearOnDependencyChange) {
|
|
210
|
+
value = '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Reload data
|
|
214
|
+
fetchData(1, '');
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const loadMore = () => {
|
|
219
|
+
if (!isLoading && hasMore) {
|
|
220
|
+
fetchData(currentPage + 1, searchValue, true);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleScroll = (event: Event) => {
|
|
225
|
+
const viewport = event.target as HTMLElement;
|
|
226
|
+
const scrollBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
|
227
|
+
|
|
228
|
+
if (scrollBottom < 100 && hasMore && !isLoading) {
|
|
229
|
+
loadMore();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const retry = () => {
|
|
234
|
+
error = null;
|
|
235
|
+
fetchData(currentPage, searchValue);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const scrollToTop = () => {
|
|
239
|
+
if (!viewportRef) return;
|
|
240
|
+
|
|
241
|
+
viewportRef.scrollTo({
|
|
242
|
+
top: 0,
|
|
243
|
+
behavior: 'smooth',
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onSelectItem = (item: Omit<CommandItem, 'onSelect'>, onSelect?: AnyFn) => () => {
|
|
248
|
+
onSelect?.();
|
|
249
|
+
|
|
250
|
+
if (item.value === value) {
|
|
251
|
+
value = allowDeselect ? '' : value;
|
|
252
|
+
} else {
|
|
253
|
+
value = item.value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (closeOnSelect) {
|
|
257
|
+
open = false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
</script>
|
|
261
|
+
|
|
262
|
+
{#snippet renderTrigger()}
|
|
263
|
+
{@const triggerProps = {
|
|
264
|
+
class: cn(comboboxTriggerVariants({ variant, size, rounded, fullWidth, class: triggerClass })),
|
|
265
|
+
'data-placeholder': boolAttr(isPlaceholder),
|
|
266
|
+
}}
|
|
267
|
+
{#if triggerSnippet}
|
|
268
|
+
<PopoverPrimitive.Trigger {...triggerProps}>
|
|
269
|
+
{#snippet child({ props })}
|
|
270
|
+
{@render triggerSnippet?.({ props, displayValue })}
|
|
271
|
+
{/snippet}
|
|
272
|
+
</PopoverPrimitive.Trigger>
|
|
273
|
+
{:else}
|
|
274
|
+
<PopoverPrimitive.Trigger {...triggerProps}>
|
|
275
|
+
{displayValue}
|
|
276
|
+
</PopoverPrimitive.Trigger>
|
|
277
|
+
{/if}
|
|
278
|
+
{/snippet}
|
|
279
|
+
|
|
280
|
+
{#snippet renderItem(item: CommandItem)}
|
|
281
|
+
{@const { value: itemValue, label, icon: Icon, shortcut, onSelect, ...restItem } = item}
|
|
282
|
+
{@const itemAttrs = {
|
|
283
|
+
value: itemValue,
|
|
284
|
+
onSelect: onSelectItem(item, onSelect),
|
|
285
|
+
'data-selected': boolAttr(itemValue === value),
|
|
286
|
+
...restItem,
|
|
287
|
+
}}
|
|
288
|
+
|
|
289
|
+
{#if itemSnippet}
|
|
290
|
+
<CommandPrimitive.Item {...itemAttrs}>
|
|
291
|
+
{#snippet child({ props })}
|
|
292
|
+
{@render itemSnippet?.({ item, props })}
|
|
293
|
+
{/snippet}
|
|
294
|
+
</CommandPrimitive.Item>
|
|
295
|
+
{:else}
|
|
296
|
+
<CommandPrimitive.Item {...itemAttrs}>
|
|
297
|
+
{#if Icon}
|
|
298
|
+
<Icon width={16} height={16} />
|
|
299
|
+
{/if}
|
|
300
|
+
{label ?? value}
|
|
301
|
+
{#if shortcut && shortcut.length > 0}
|
|
302
|
+
<Kbd.Group>
|
|
303
|
+
{#each shortcut as shortcut (shortcut)}
|
|
304
|
+
<Kbd.Root>{shortcut}</Kbd.Root>
|
|
305
|
+
{/each}
|
|
306
|
+
</Kbd.Group>
|
|
307
|
+
{/if}
|
|
308
|
+
{#if itemValue === value}
|
|
309
|
+
<span
|
|
310
|
+
class="cgui:ms-auto cgui:text-icon-regular cgui:size-4 cgui:flex cgui:items-center cgui:justify-center cgui:shrink-0"
|
|
311
|
+
transition:fly={{ duration: 250, y: 4, easing: cubicInOut }}
|
|
312
|
+
>
|
|
313
|
+
<IconComponent.Checkmark class="cgui:ms-auto" width={16} height={16} />
|
|
314
|
+
</span>
|
|
315
|
+
{/if}
|
|
316
|
+
</CommandPrimitive.Item>
|
|
317
|
+
{/if}
|
|
318
|
+
{/snippet}
|
|
319
|
+
|
|
320
|
+
{#snippet loadingSpinner()}
|
|
321
|
+
<div class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-icon-default">
|
|
322
|
+
<Spinner />
|
|
323
|
+
</div>
|
|
324
|
+
{/snippet}
|
|
325
|
+
|
|
326
|
+
{#snippet loadingMore()}
|
|
327
|
+
<div class="cgui:p-2 cgui:flex cgui:items-center cgui:justify-center cgui:text-icon-default">
|
|
328
|
+
<Spinner />
|
|
329
|
+
</div>
|
|
330
|
+
{/snippet}
|
|
331
|
+
|
|
332
|
+
{#snippet errorState()}
|
|
333
|
+
<div class="cgui:p-4 cgui:text-center cgui:text-body-2">
|
|
334
|
+
<p class="cgui:text-destructive cgui:mb-2">{error}</p>
|
|
335
|
+
<button type="button" class="cgui:text-sm cgui:text-primary cgui:underline cgui:hover:no-underline" onclick={retry}>
|
|
336
|
+
Try again
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
{/snippet}
|
|
340
|
+
|
|
341
|
+
<PopoverPrimitive.Root bind:open {...restProps}>
|
|
342
|
+
{@render renderTrigger()}
|
|
343
|
+
|
|
344
|
+
<PopoverPrimitive.Portal disabled={portalDisabled}>
|
|
345
|
+
<PopoverPrimitive.Content class="cgui:overflow-hidden cgui:rounded-md">
|
|
346
|
+
<CommandPrimitive.Root shouldFilter={false}>
|
|
347
|
+
<CommandPrimitive.Input bind:value={searchValue} placeholder={searchPlaceholder} />
|
|
348
|
+
|
|
349
|
+
<CommandPrimitive.List class="cgui:rounded-inherit">
|
|
350
|
+
<CommandPrimitive.Viewport onscroll={handleScroll} bind:ref={viewportRef}>
|
|
351
|
+
{#if error}
|
|
352
|
+
{@render errorState()}
|
|
353
|
+
{:else if isLoading && !hasResults}
|
|
354
|
+
{#if loadingSnippet}
|
|
355
|
+
{@render loadingSnippet()}
|
|
356
|
+
{:else}
|
|
357
|
+
{@render loadingSpinner()}
|
|
358
|
+
{/if}
|
|
359
|
+
{:else if hasResults}
|
|
360
|
+
{#each groupedItems as [groupKey, groupItems] (groupKey || '__ungrouped__')}
|
|
361
|
+
{#if groupKey}
|
|
362
|
+
{@const group = groupMap.get(groupKey)}
|
|
363
|
+
<CommandPrimitive.Group value={group?.value}>
|
|
364
|
+
<CommandPrimitive.GroupHeading>{group?.label ?? groupKey}</CommandPrimitive.GroupHeading>
|
|
365
|
+
|
|
366
|
+
<CommandPrimitive.GroupItems>
|
|
367
|
+
{#each groupItems as item (item.value)}
|
|
368
|
+
{@render renderItem(item)}
|
|
369
|
+
{/each}
|
|
370
|
+
</CommandPrimitive.GroupItems>
|
|
371
|
+
|
|
372
|
+
{#if group?.separator}
|
|
373
|
+
<CommandPrimitive.Separator />
|
|
374
|
+
{/if}
|
|
375
|
+
</CommandPrimitive.Group>
|
|
376
|
+
{:else}
|
|
377
|
+
{#each groupItems as item (item.value)}
|
|
378
|
+
{@render renderItem(item)}
|
|
379
|
+
{/each}
|
|
380
|
+
{/if}
|
|
381
|
+
{/each}
|
|
382
|
+
|
|
383
|
+
{#if isLoading}
|
|
384
|
+
{@render loadingMore()}
|
|
385
|
+
{/if}
|
|
386
|
+
|
|
387
|
+
{#if !hasMore && items.length > pageSize}
|
|
388
|
+
<button
|
|
389
|
+
class="cgui:w-full cgui:flex cgui:justify-center cgui:cursor-pointer cgui:items-center cgui:text-xs cgui:text-icon-default cgui:underline cgui:hover:no-underline"
|
|
390
|
+
onclick={scrollToTop}
|
|
391
|
+
>
|
|
392
|
+
<IconComponent.ChevronUp />
|
|
393
|
+
</button>
|
|
394
|
+
{/if}
|
|
395
|
+
{:else}
|
|
396
|
+
<CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
|
|
397
|
+
{/if}
|
|
398
|
+
</CommandPrimitive.Viewport>
|
|
399
|
+
</CommandPrimitive.List>
|
|
400
|
+
</CommandPrimitive.Root>
|
|
401
|
+
</PopoverPrimitive.Content>
|
|
402
|
+
</PopoverPrimitive.Portal>
|
|
403
|
+
</PopoverPrimitive.Root>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { CommandProps } from '../command/index.js';
|
|
2
|
+
import type { CommandGroup, CommandItem, CommandProps } from '../command/index.js';
|
|
3
3
|
import type { PrimitivePopoverRootProps } from '../popover/types.js';
|
|
4
4
|
import type { ComboboxTriggerVariantsProps } from './styles.js';
|
|
5
5
|
export type ComboboxRootProps = PrimitivePopoverRootProps;
|
|
@@ -20,3 +20,44 @@ export type ComboboxProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsP
|
|
|
20
20
|
}]>;
|
|
21
21
|
item?: CommandProps['item'];
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Async Combobox Types
|
|
25
|
+
*/
|
|
26
|
+
export type ComboboxAsyncCallbackParams = {
|
|
27
|
+
page: number;
|
|
28
|
+
pageSize: number;
|
|
29
|
+
search: string;
|
|
30
|
+
};
|
|
31
|
+
export type ComboboxAsyncCallbackResult = {
|
|
32
|
+
items: CommandItem[];
|
|
33
|
+
hasMore: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type ComboboxAsyncCallback = (params: ComboboxAsyncCallbackParams) => Promise<ComboboxAsyncCallbackResult>;
|
|
36
|
+
export type ComboboxAsyncProps = Omit<ComboboxProps, 'collection'> & {
|
|
37
|
+
/** Loading state renderer */
|
|
38
|
+
loading?: Snippet;
|
|
39
|
+
/** Current page (default: 1) */
|
|
40
|
+
page?: number;
|
|
41
|
+
/** Page size for pagination (default: 20) */
|
|
42
|
+
pageSize?: number;
|
|
43
|
+
/** Async data callback */
|
|
44
|
+
callback: ComboboxAsyncCallback;
|
|
45
|
+
/** Initial items (optional) */
|
|
46
|
+
initialItems?: CommandItem[];
|
|
47
|
+
/** Load immediate data when combobox is mounted (default: true) */
|
|
48
|
+
loadImmediate?: boolean;
|
|
49
|
+
/** Group definitions (for labels/ordering) */
|
|
50
|
+
groups?: CommandGroup[];
|
|
51
|
+
/** Debounce delay for search in milliseconds (default: 300ms) */
|
|
52
|
+
searchDebounce?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Getter function for dependency tracking. When the returned value changes,
|
|
55
|
+
* the component will reset items, page to 1, clear selected value, and reload data.
|
|
56
|
+
* @example dependsOn={() => parentSelectValue}
|
|
57
|
+
*/
|
|
58
|
+
dependsOn?: () => unknown;
|
|
59
|
+
/**
|
|
60
|
+
* Whether to clear the selected value when dependsOn changes **(default: true)**
|
|
61
|
+
*/
|
|
62
|
+
clearOnDependencyChange?: boolean;
|
|
63
|
+
};
|
|
@@ -17,11 +17,9 @@ export type DialogHostProps = {
|
|
|
17
17
|
export type DialogComponent = Component<any>;
|
|
18
18
|
export interface RegisteredDialogsOverride {
|
|
19
19
|
}
|
|
20
|
-
export type RegisteredDialogCollectionOverwritten = RegisteredDialogsOverride extends {
|
|
20
|
+
export type RegisteredDialogCollectionOverwritten = keyof RegisteredDialogsOverride extends never ? {
|
|
21
21
|
dialogs: Record<string, DialogComponent>;
|
|
22
|
-
}
|
|
23
|
-
dialogs: Record<string, DialogComponent>;
|
|
24
|
-
};
|
|
22
|
+
} : RegisteredDialogsOverride;
|
|
25
23
|
export type RegisteredDialogCollection = RegisteredDialogCollectionOverwritten['dialogs'];
|
|
26
24
|
export type RegisteredDialogKeys = keyof RegisteredDialogCollectionOverwritten['dialogs'];
|
|
27
25
|
export type DialogRegistrationValue<TKey extends string = string, TComponent extends DialogComponent = DialogComponent> = {
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
<ButtonGroupPrimitive.Root class={cn('cgui:w-full', containerClass)}>
|
|
81
81
|
<Button
|
|
82
|
+
type="button"
|
|
82
83
|
variant="clean"
|
|
83
84
|
w="clean"
|
|
84
85
|
class="cgui:border cgui:w-9 cgui:border-r-0 cgui:border-stroke-default cgui:text-fg-dark cgui:bg-surface-lightest"
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
<Input bind:value={internalValueBox.current} class={cn('cgui:border-r cgui:border-l!', className)} {...restProps} />
|
|
92
93
|
|
|
93
94
|
<Button
|
|
95
|
+
type="button"
|
|
94
96
|
variant="clean"
|
|
95
97
|
w="clean"
|
|
96
98
|
class="cgui:border cgui:w-9 cgui:border-stroke-default cgui:text-fg-dark cgui:bg-surface-lightest"
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * as SelectPrimitive from './exports-primitive.js';
|
|
2
2
|
export * as Select from './exports.js';
|
|
3
|
-
export type
|
|
3
|
+
export type * from './types.js';
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
callback,
|
|
40
40
|
initialItems = [],
|
|
41
41
|
loadImmediate = true,
|
|
42
|
+
dependsOn,
|
|
43
|
+
clearOnDependencyChange = true,
|
|
42
44
|
|
|
43
45
|
...restProps
|
|
44
46
|
}: SelectAsyncProps = $props();
|
|
@@ -112,6 +114,11 @@
|
|
|
112
114
|
|
|
113
115
|
currentPage = page;
|
|
114
116
|
hasMore = result.hasMore;
|
|
117
|
+
|
|
118
|
+
// Check if viewport needs more items after render
|
|
119
|
+
requestAnimationFrame(() => {
|
|
120
|
+
checkNeedMoreItems();
|
|
121
|
+
});
|
|
115
122
|
} catch (err) {
|
|
116
123
|
error = err instanceof Error ? err.message : 'Failed to fetch data';
|
|
117
124
|
} finally {
|
|
@@ -119,12 +126,34 @@
|
|
|
119
126
|
}
|
|
120
127
|
};
|
|
121
128
|
|
|
122
|
-
//
|
|
129
|
+
// Auto-fill viewport if content doesn't overflow
|
|
130
|
+
const checkNeedMoreItems = () => {
|
|
131
|
+
if (!viewportRef || isLoading || !hasMore) return;
|
|
132
|
+
|
|
133
|
+
const { scrollHeight, clientHeight } = viewportRef;
|
|
134
|
+
|
|
135
|
+
// If content doesn't fill viewport, load more
|
|
136
|
+
if (scrollHeight <= clientHeight) {
|
|
137
|
+
loadMore();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Fetch on first open or check viewport fill
|
|
123
142
|
watch(
|
|
124
143
|
() => open,
|
|
125
144
|
() => {
|
|
126
|
-
if (!open
|
|
127
|
-
|
|
145
|
+
if (!open) return;
|
|
146
|
+
|
|
147
|
+
// If no items yet and not loading, fetch first page
|
|
148
|
+
if (items.length === 0 && !isLoading && !loadImmediate) {
|
|
149
|
+
fetchData(1);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If already have items, check if viewport needs more after it renders
|
|
154
|
+
requestAnimationFrame(() => {
|
|
155
|
+
checkNeedMoreItems();
|
|
156
|
+
});
|
|
128
157
|
}
|
|
129
158
|
);
|
|
130
159
|
|
|
@@ -140,6 +169,32 @@
|
|
|
140
169
|
};
|
|
141
170
|
});
|
|
142
171
|
|
|
172
|
+
// Watch for dependency changes and reset
|
|
173
|
+
let previousDependency = $state<unknown>(dependsOn?.());
|
|
174
|
+
watch(
|
|
175
|
+
() => dependsOn?.(),
|
|
176
|
+
(newValue) => {
|
|
177
|
+
if (previousDependency === undefined && newValue === undefined) return;
|
|
178
|
+
if (previousDependency === newValue) return;
|
|
179
|
+
|
|
180
|
+
previousDependency = newValue;
|
|
181
|
+
|
|
182
|
+
// Reset state
|
|
183
|
+
items = [];
|
|
184
|
+
currentPage = 1;
|
|
185
|
+
hasMore = true;
|
|
186
|
+
error = null;
|
|
187
|
+
|
|
188
|
+
// Clear selected value if enabled
|
|
189
|
+
if (clearOnDependencyChange) {
|
|
190
|
+
value = '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Reload data
|
|
194
|
+
fetchData(1);
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
143
198
|
const loadMore = () => {
|
|
144
199
|
if (!isLoading && hasMore) {
|
|
145
200
|
fetchData(currentPage + 1, true);
|
|
@@ -102,6 +102,18 @@ export type SelectAsyncProps = Omit<SelectProps, 'collection'> & {
|
|
|
102
102
|
callback: SelectAsyncCallback;
|
|
103
103
|
/** Initial items (optional) */
|
|
104
104
|
initialItems?: SelectItem[];
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Load immediate data when select was mounted **(default: true)**
|
|
107
|
+
*/
|
|
106
108
|
loadImmediate?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Getter function for dependency tracking. When the returned value changes,
|
|
111
|
+
* the component will reset items, page to 1, clear selected value, and reload data.
|
|
112
|
+
* @example dependsOn={() => parentSelectValue}
|
|
113
|
+
*/
|
|
114
|
+
dependsOn?: () => unknown;
|
|
115
|
+
/**
|
|
116
|
+
* Whether to clear the selected value when dependsOn changes **(default: true)**
|
|
117
|
+
*/
|
|
118
|
+
clearOnDependencyChange?: boolean;
|
|
107
119
|
};
|