@casinogate/ui 1.10.6 → 1.10.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.
@@ -369,6 +369,9 @@
369
369
  .cgui\:h-\(--cg-ui-max-content-height\) {
370
370
  height: var(--cg-ui-max-content-height);
371
371
  }
372
+ .cgui\:h-1 {
373
+ height: calc(var(--cgui-spacing) * 1);
374
+ }
372
375
  .cgui\:h-3 {
373
376
  height: calc(var(--cgui-spacing) * 3);
374
377
  }
@@ -450,6 +453,9 @@
450
453
  .cgui\:max-h-75 {
451
454
  max-height: calc(var(--cgui-spacing) * 75);
452
455
  }
456
+ .cgui\:max-h-80 {
457
+ max-height: calc(var(--cgui-spacing) * 80);
458
+ }
453
459
  .cgui\:min-h-7\.5 {
454
460
  min-height: calc(var(--cgui-spacing) * 7.5);
455
461
  }
@@ -4,9 +4,10 @@
4
4
 
5
5
  import { boolAttr } from '../../internal/utils/attrs.js';
6
6
  import { cn } from '../../internal/utils/common.js';
7
- import { watch } from 'runed';
7
+ import { Debounced, watch } from 'runed';
8
8
  import type { AnyFn } from 'svelte-toolbelt';
9
9
  import { onMountEffect } from 'svelte-toolbelt';
10
+ import type { Attachment } from 'svelte/attachments';
10
11
  import { cubicInOut } from 'svelte/easing';
11
12
  import { fly } from 'svelte/transition';
12
13
  import { CommandApi, CommandPrimitive } from '../command/index.js';
@@ -49,13 +50,14 @@
49
50
 
50
51
  let items = $state<CommandItem[]>(initialItems);
51
52
  let isLoading = $state(false);
52
- let hasMore = $state(true);
53
+ let isSearching = $state(false);
54
+ let hasMore = $state(false);
53
55
  let currentPage = $state(1);
54
56
  let error = $state<string | null>(null);
55
- let viewportRef = $state<HTMLElement | null>(null);
56
- let searchTimeout = $state<ReturnType<typeof setTimeout> | null>(null);
57
57
  let lastSearch = $state('');
58
58
 
59
+ let listRef = $state<HTMLElement | null>(null);
60
+
59
61
  const createCollection = (itemList: CommandItem[]): CommandCollection => {
60
62
  const groupOrderMap = new Map<string, number>();
61
63
  groups.forEach((g, index) => {
@@ -93,6 +95,7 @@
93
95
  if (isLoading) return;
94
96
 
95
97
  isLoading = true;
98
+ isSearching = !append;
96
99
  error = null;
97
100
 
98
101
  try {
@@ -107,53 +110,34 @@
107
110
  currentPage = page;
108
111
  hasMore = result.hasMore;
109
112
  lastSearch = search;
110
-
111
- // Check if viewport needs more items after render
112
- requestAnimationFrame(() => {
113
- checkNeedMoreItems();
114
- });
115
113
  } catch (err) {
116
114
  error = err instanceof Error ? err.message : 'Failed to fetch data';
117
115
  } finally {
118
116
  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();
117
+ isSearching = false;
131
118
  }
132
119
  };
133
120
 
134
121
  // Debounced search handler
135
- const debouncedSearch = (search: string) => {
136
- if (searchTimeout) {
137
- clearTimeout(searchTimeout);
138
- }
122
+ const debouncedSearchValue = new Debounced(() => searchValue, searchDebounce);
139
123
 
140
- searchTimeout = setTimeout(() => {
141
- currentPage = 1;
142
- fetchData(1, search, false);
143
- }, searchDebounce);
124
+ const search = (search: string) => {
125
+ currentPage = 1;
126
+ fetchData(1, search, false);
144
127
  };
145
128
 
146
129
  // Watch for search value changes
147
130
  watch(
148
- () => searchValue,
131
+ () => debouncedSearchValue.current,
149
132
  () => {
150
- if (searchValue !== lastSearch) {
151
- debouncedSearch(searchValue);
152
- }
133
+ search(debouncedSearchValue.current);
134
+ },
135
+ {
136
+ lazy: true,
153
137
  }
154
138
  );
155
139
 
156
- // Fetch on first open or check viewport fill
140
+ // Fetch on first open
157
141
  watch(
158
142
  () => open,
159
143
  () => {
@@ -164,11 +148,6 @@
164
148
  fetchData(1, searchValue);
165
149
  return;
166
150
  }
167
-
168
- // If already have items, check if viewport needs more after it renders
169
- requestAnimationFrame(() => {
170
- checkNeedMoreItems();
171
- });
172
151
  }
173
152
  );
174
153
 
@@ -181,9 +160,6 @@
181
160
  $effect(() => {
182
161
  return () => {
183
162
  currentPage = 1;
184
- if (searchTimeout) {
185
- clearTimeout(searchTimeout);
186
- }
187
163
  };
188
164
  });
189
165
 
@@ -221,13 +197,21 @@
221
197
  }
222
198
  };
223
199
 
224
- const handleScroll = (event: Event) => {
225
- const viewport = event.target as HTMLElement;
226
- const scrollBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
200
+ const anchorAttachment: Attachment = (el) => {
201
+ const observer = new IntersectionObserver(
202
+ (entries) => {
203
+ if (entries[0]?.isIntersecting) {
204
+ loadMore();
205
+ }
206
+ },
207
+ { threshold: 0 }
208
+ );
227
209
 
228
- if (scrollBottom < 100 && hasMore && !isLoading) {
229
- loadMore();
230
- }
210
+ observer.observe(el);
211
+
212
+ return () => {
213
+ observer.disconnect();
214
+ };
231
215
  };
232
216
 
233
217
  const retry = () => {
@@ -236,9 +220,9 @@
236
220
  };
237
221
 
238
222
  const scrollToTop = () => {
239
- if (!viewportRef) return;
223
+ if (!listRef) return;
240
224
 
241
- viewportRef.scrollTo({
225
+ listRef.scrollTo({
242
226
  top: 0,
243
227
  behavior: 'smooth',
244
228
  });
@@ -343,14 +327,14 @@
343
327
 
344
328
  <PopoverPrimitive.Portal disabled={portalDisabled}>
345
329
  <PopoverPrimitive.Content class="cgui:overflow-hidden cgui:rounded-md">
346
- <CommandPrimitive.Root shouldFilter={false}>
330
+ <CommandPrimitive.Root shouldFilter={false} class="cgui:max-h-80">
347
331
  <CommandPrimitive.Input bind:value={searchValue} placeholder={searchPlaceholder} />
348
332
 
349
- <CommandPrimitive.List class="cgui:rounded-inherit">
350
- <CommandPrimitive.Viewport onscroll={handleScroll} bind:ref={viewportRef}>
333
+ <CommandPrimitive.List class="cgui:rounded-inherit" bind:ref={listRef} style={{ maxHeight: maxContentHeight }}>
334
+ <CommandPrimitive.Viewport class={cn('cgui:relative')}>
351
335
  {#if error}
352
336
  {@render errorState()}
353
- {:else if isLoading && !hasResults}
337
+ {:else if isSearching}
354
338
  {#if loadingSnippet}
355
339
  {@render loadingSnippet()}
356
340
  {:else}
@@ -380,22 +364,29 @@
380
364
  {/if}
381
365
  {/each}
382
366
 
383
- {#if isLoading}
384
- {@render loadingMore()}
367
+ {#if hasMore}
368
+ {#key items.length}
369
+ <div class="cgui:h-1 cgui:w-full" {@attach anchorAttachment}></div>
370
+ {/key}
385
371
  {/if}
386
372
 
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>
373
+ {#if isLoading}
374
+ {@render loadingMore()}
394
375
  {/if}
395
376
  {:else}
396
377
  <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
397
378
  {/if}
398
379
  </CommandPrimitive.Viewport>
380
+
381
+ {#if !hasMore && items.length > pageSize}
382
+ <button
383
+ type="button"
384
+ 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"
385
+ onclick={scrollToTop}
386
+ >
387
+ <IconComponent.ChevronUp />
388
+ </button>
389
+ {/if}
399
390
  </CommandPrimitive.List>
400
391
  </CommandPrimitive.Root>
401
392
  </PopoverPrimitive.Content>
@@ -29,6 +29,7 @@
29
29
  rounded = 'default',
30
30
  fullWidth = true,
31
31
  closeOnSelect = true,
32
+ maxContentHeight,
32
33
  ...restProps
33
34
  }: ComboboxProps = $props();
34
35
 
@@ -126,7 +127,7 @@
126
127
  <CommandPrimitive.Root>
127
128
  <CommandPrimitive.Input bind:value={searchValue} placeholder={searchPlaceholder} />
128
129
 
129
- <CommandPrimitive.List class="cgui:rounded-inherit">
130
+ <CommandPrimitive.List class="cgui:rounded-inherit" style={{ maxHeight: maxContentHeight }}>
130
131
  <CommandPrimitive.Viewport>
131
132
  <CommandPrimitive.Empty>No results found</CommandPrimitive.Empty>
132
133
 
@@ -14,6 +14,7 @@ export type ComboboxProps = PrimitivePopoverRootProps & ComboboxTriggerVariantsP
14
14
  allowDeselect?: boolean;
15
15
  triggerClass?: string;
16
16
  closeOnSelect?: boolean;
17
+ maxContentHeight?: string;
17
18
  trigger?: Snippet<[{
18
19
  props: Record<string, unknown>;
19
20
  displayValue: string;
@@ -7,6 +7,7 @@
7
7
  import { cn } from '../../internal/utils/common.js';
8
8
  import { watch } from 'runed';
9
9
  import { onMountEffect } from 'svelte-toolbelt';
10
+ import type { Attachment } from 'svelte/attachments';
10
11
  import { cubicInOut } from 'svelte/easing';
11
12
  import { fly } from 'svelte/transition';
12
13
  import Content from './components/select.content.svelte';
@@ -208,15 +209,32 @@
208
209
  }
209
210
  };
210
211
 
211
- const handleScroll = (event: Event) => {
212
- const viewport = event.target as HTMLElement;
213
- const scrollBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
212
+ const anchorAttachment: Attachment = (el) => {
213
+ const observer = new IntersectionObserver(
214
+ (entries) => {
215
+ if (entries[0]?.isIntersecting) {
216
+ loadMore();
217
+ }
218
+ },
219
+ { threshold: 0 }
220
+ );
214
221
 
215
- if (scrollBottom < 100 && hasMore && !isLoading) {
216
- loadMore();
217
- }
222
+ observer.observe(el);
223
+
224
+ return () => {
225
+ observer.disconnect();
226
+ };
218
227
  };
219
228
 
229
+ // const handleScroll = (event: Event) => {
230
+ // const viewport = event.target as HTMLElement;
231
+ // const scrollBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
232
+
233
+ // if (scrollBottom < 100 && hasMore && !isLoading) {
234
+ // loadMore();
235
+ // }
236
+ // };
237
+
220
238
  const retry = () => {
221
239
  error = null;
222
240
  fetchData(currentPage);
@@ -272,6 +290,12 @@
272
290
  </div>
273
291
  {/snippet}
274
292
 
293
+ {#if hasMore}
294
+ {#key items.length}
295
+ <div class="cgui:h-1 cgui:w-full" {@attach anchorAttachment}></div>
296
+ {/key}
297
+ {/if}
298
+
275
299
  {#snippet loadingMore()}
276
300
  <div class="cgui:p-2 cgui:flex cgui:items-center cgui:justify-center cgui:text-icon-default">
277
301
  <Spinner />
@@ -302,7 +326,7 @@
302
326
 
303
327
  <Portal>
304
328
  <Content bind:ref={contentRef} class={cn(contentClass)} {...contentProps}>
305
- <Viewport onscroll={handleScroll} bind:ref={viewportRef}>
329
+ <Viewport bind:ref={viewportRef}>
306
330
  {#if error}
307
331
  {@render errorState()}
308
332
  {:else if isLoading && !hasResults}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casinogate/ui",
3
- "version": "1.10.6",
3
+ "version": "1.10.7",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",