@casinogate/ui 1.5.2 → 1.5.3

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.
@@ -1371,6 +1371,11 @@
1371
1371
  padding-right: calc(var(--cgui-spacing) * 14);
1372
1372
  }
1373
1373
  }
1374
+ .cgui\:data-\[highlighted\]\:bg-surface-lightest {
1375
+ &[data-highlighted] {
1376
+ background-color: var(--cg-ui-palette-neutral-10);
1377
+ }
1378
+ }
1374
1379
  .cgui\:data-\[orientation\=horizontal\]\:h-px {
1375
1380
  &[data-orientation="horizontal"] {
1376
1381
  height: 1px;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../../internal/utils/common.js';
3
3
  import { Select as SelectPrimitive } from 'bits-ui';
4
+ import type { Attachment } from 'svelte/attachments';
4
5
  import { SelectStylesContext } from '../styles.js';
5
6
  import type { SelectContentProps } from '../types.js';
6
7
 
@@ -11,6 +12,7 @@
11
12
  children,
12
13
  side = 'bottom',
13
14
  forceMount = false,
15
+ maxContentHeight,
14
16
  ...restProps
15
17
  }: SelectContentProps = $props();
16
18
 
@@ -24,8 +26,24 @@
24
26
  side,
25
27
  ...restProps,
26
28
  });
29
+
30
+ const contentStyle = $derived.by(() => {
31
+ return {
32
+ ...(maxContentHeight ? { '--cg-ui-max-content-height': maxContentHeight } : {}),
33
+ };
34
+ });
35
+
36
+ const contentMount: Attachment<HTMLDivElement> = (el) => {
37
+ $effect.pre(() => {
38
+ if (!el) return;
39
+
40
+ Object.entries(contentStyle).forEach(([key, value]) => {
41
+ el.style.setProperty(key, value);
42
+ });
43
+ });
44
+ };
27
45
  </script>
28
46
 
29
- <SelectPrimitive.Content bind:ref {...attrs}>
47
+ <SelectPrimitive.Content bind:ref {...attrs} {@attach contentMount}>
30
48
  {@render children?.()}
31
49
  </SelectPrimitive.Content>
@@ -1,4 +1,4 @@
1
- import { Select as SelectPrimitive } from 'bits-ui';
2
- declare const Select: import("svelte").Component<Omit<SelectPrimitive.ContentProps, "child">, {}, "ref">;
1
+ import type { SelectContentProps } from '../types.js';
2
+ declare const Select: import("svelte").Component<SelectContentProps, {}, "ref">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;
@@ -1 +1,2 @@
1
+ export { default as Async } from './select.async.svelte';
1
2
  export { default as Root } from './select.svelte';
@@ -1 +1,2 @@
1
+ export { default as Async } from './select.async.svelte';
1
2
  export { default as Root } from './select.svelte';
@@ -1,3 +1,3 @@
1
1
  export * as SelectPrimitive from './exports-primitive.js';
2
2
  export * as Select from './exports.js';
3
- export type { SelectContentProps, SelectGroupHeadingProps, SelectGroupProps, SelectItem, SelectItemData, SelectItemGroup, SelectItemProps, SelectRootProps, SelectTriggerProps, SelectViewportProps, } from './types.js';
3
+ export type { SelectAsyncCallback, SelectAsyncCallbackParams, SelectAsyncCallbackResult, SelectAsyncProps, SelectContentProps, SelectGroupHeadingProps, SelectGroupProps, SelectItem, SelectItemData, SelectItemGroup, SelectItemProps, SelectRootProps, SelectTriggerProps, SelectViewportProps, } from './types.js';
@@ -0,0 +1,204 @@
1
+ <script lang="ts">
2
+ import { Spinner } from '../spinner/index.js';
3
+ import Content from './components/select.content.svelte';
4
+ import GroupHeading from './components/select.group-heading.svelte';
5
+ import Group from './components/select.group.svelte';
6
+ import Item from './components/select.item.svelte';
7
+ import Portal from './components/select.portal.svelte';
8
+ import Root from './components/select.root.svelte';
9
+ import Trigger from './components/select.trigger.svelte';
10
+ import Viewport from './components/select.viewport.svelte';
11
+ import type { SelectAsyncProps, SelectData, SelectItem, SelectItemGroup } from './types.js';
12
+ import { getItemKey, getLabelFromValue } from './utils/index.js';
13
+
14
+ let {
15
+ value = $bindable(''),
16
+ open = $bindable(false),
17
+ empty = 'No results found',
18
+ maxContentHeight,
19
+ item,
20
+ trigger,
21
+ side,
22
+ sideOffset,
23
+ align,
24
+ alignOffset,
25
+ avoidCollisions,
26
+ collisionPadding,
27
+ customAnchor,
28
+ placeholder: placeholderProp,
29
+ loading,
30
+ pageSize = 10,
31
+ callback,
32
+ ...restProps
33
+ }: SelectAsyncProps = $props();
34
+
35
+ let data = $state<SelectData>([]);
36
+ let isLoading = $state(false);
37
+ let hasMore = $state(true);
38
+ let currentPage = $state(1);
39
+
40
+ let error = $state<string | null>(null);
41
+
42
+ const placeholder = $derived.by(() => {
43
+ if (typeof value === 'string' && value.trim() !== '') {
44
+ const label = getLabelFromValue(value, data);
45
+ return label ?? value;
46
+ }
47
+
48
+ if (Array.isArray(value) && value.length > 0) {
49
+ const labels = value.map((v) => getLabelFromValue(v, data) ?? v);
50
+ return labels.join(', ');
51
+ }
52
+
53
+ if (placeholderProp) return placeholderProp;
54
+
55
+ return '';
56
+ });
57
+
58
+ const fetchData = async (page: number, append = false) => {
59
+ isLoading = true;
60
+ error = null;
61
+
62
+ try {
63
+ const result = await callback({ page, pageSize });
64
+
65
+ if (append) {
66
+ data = [...data, ...(result.data as any)];
67
+ } else {
68
+ data = result.data;
69
+ }
70
+
71
+ currentPage = page;
72
+ hasMore = result.hasMore;
73
+ isLoading = false;
74
+ } catch (err) {
75
+ error = err instanceof Error ? err.message : 'Failed to fetch data';
76
+ } finally {
77
+ isLoading = false;
78
+ }
79
+ };
80
+
81
+ $effect(() => {
82
+ if (open && data.length === 0 && !isLoading) {
83
+ fetchData(currentPage);
84
+ }
85
+ });
86
+
87
+ $effect(() => {
88
+ return () => {
89
+ currentPage = 1;
90
+ };
91
+ });
92
+
93
+ const loadMore = () => {
94
+ if (!isLoading && hasMore) {
95
+ const nextPage = currentPage + 1;
96
+ fetchData(nextPage, true);
97
+ }
98
+ };
99
+
100
+ const handleScroll = (event: Event) => {
101
+ const viewport = event.target as HTMLElement;
102
+ const scrollBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
103
+
104
+ if (scrollBottom < 100 && hasMore && !isLoading) {
105
+ loadMore();
106
+ }
107
+ };
108
+
109
+ const hasResults = $derived.by(() => {
110
+ return data.length > 0;
111
+ });
112
+ </script>
113
+
114
+ {#snippet itemsString(item: string)}
115
+ <Item value={item} label={item} />
116
+ {/snippet}
117
+
118
+ {#snippet itemBasic(item: SelectItem)}
119
+ <Item value={item.value} label={item.label} />
120
+ {/snippet}
121
+
122
+ {#snippet itemGroup(group: SelectItemGroup)}
123
+ <Group>
124
+ <GroupHeading>
125
+ {group.group}
126
+ </GroupHeading>
127
+
128
+ {#each group.items as item (getItemKey(item))}
129
+ {#if typeof item === 'string'}
130
+ {@render itemsString(item)}
131
+ {:else}
132
+ {@render itemBasic(item)}
133
+ {/if}
134
+ {/each}
135
+ </Group>
136
+ {/snippet}
137
+
138
+ <Root bind:value={value as never} bind:open {...restProps as any}>
139
+ {#if trigger}
140
+ <Trigger>
141
+ {#snippet child({ props })}
142
+ {@render trigger?.({ props, label: placeholder })}
143
+ {/snippet}
144
+ </Trigger>
145
+ {:else}
146
+ <Trigger>
147
+ {placeholder}
148
+ </Trigger>
149
+ {/if}
150
+
151
+ <Portal>
152
+ <Content
153
+ {maxContentHeight}
154
+ {side}
155
+ {sideOffset}
156
+ {align}
157
+ {alignOffset}
158
+ {avoidCollisions}
159
+ {collisionPadding}
160
+ {customAnchor}
161
+ >
162
+ <Viewport onscroll={handleScroll}>
163
+ {#if error}
164
+ <div class="cgui:p-4 cgui:text-center cgui:text-body-2 cgui:text-fg-darkest cgui:text-destructive">
165
+ {error}
166
+ </div>
167
+ {:else if isLoading && !hasResults}
168
+ {#if loading}
169
+ {@render loading?.()}
170
+ {:else}
171
+ <div class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-fg-darkest">
172
+ <Spinner />
173
+ </div>
174
+ {/if}
175
+ {:else if hasResults}
176
+ {#each data as item (getItemKey(item))}
177
+ {#if typeof item === 'string'}
178
+ {@render itemsString(item)}
179
+ {:else if 'group' in item}
180
+ {@render itemGroup(item)}
181
+ {:else}
182
+ {@render itemBasic(item)}
183
+ {/if}
184
+ {/each}
185
+ {#if isLoading}
186
+ <div
187
+ class="cgui:p-2 cgui:flex cgui:items-center cgui:justify-center cgui:text-center cgui:text-body-2 cgui:text-fg-darkest"
188
+ >
189
+ <Spinner />
190
+ </div>
191
+ {/if}
192
+ {:else if typeof empty === 'string'}
193
+ <div
194
+ class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-center cgui:text-body-2 cgui:text-fg-regular"
195
+ >
196
+ {empty}
197
+ </div>
198
+ {:else}
199
+ {@render empty?.()}
200
+ {/if}
201
+ </Viewport>
202
+ </Content>
203
+ </Portal>
204
+ </Root>
@@ -0,0 +1,4 @@
1
+ import type { SelectAsyncProps } from './types.js';
2
+ declare const Select: import("svelte").Component<SelectAsyncProps, {}, "value" | "open">;
3
+ type Select = ReturnType<typeof Select>;
4
+ export default Select;
@@ -3,6 +3,7 @@
3
3
  import type { Parameters } from '@storybook/sveltekit';
4
4
  import type { ComponentProps } from 'svelte';
5
5
  import { Select, SelectPrimitive } from './index.js';
6
+ import type { SelectAsyncCallback } from './types.js';
6
7
 
7
8
  const parameters: Parameters = {
8
9
  controls: {
@@ -104,6 +105,38 @@
104
105
  [] as Array<{ label: string; items: typeof groupedItems }>
105
106
  )
106
107
  );
108
+
109
+ type CatData = {
110
+ id: string;
111
+ url: string;
112
+ width: number;
113
+ height: number;
114
+ };
115
+
116
+ const fetchCatData: SelectAsyncCallback = async (params) => {
117
+ const { page, pageSize } = params;
118
+
119
+ try {
120
+ const res = await fetch(`https://api.thecatapi.com/v1/images/search?limit=${pageSize}&page=${page}`, {
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ 'x-api-key': 'live_V1XvjJ84eY5LPP29tbKPJXDiRFPg0WgkDjRtCsocndgnNrVtiUDP25W9rp3QuwbX',
124
+ },
125
+ });
126
+
127
+ const data = (await res.json()) as CatData[];
128
+
129
+ return {
130
+ data: data.map((item) => ({
131
+ value: item.id,
132
+ label: item.id,
133
+ })),
134
+ hasMore: data.length > 0,
135
+ };
136
+ } catch (error) {
137
+ throw error;
138
+ }
139
+ };
107
140
  </script>
108
141
 
109
142
  <Story name="Basic" {args} {parameters}>
@@ -215,3 +248,16 @@
215
248
  </div>
216
249
  {/snippet}
217
250
  </Story>
251
+
252
+ <Story name="Async" {args} {parameters}>
253
+ {#snippet template(args: Args)}
254
+ <Select.Async
255
+ {...args}
256
+ type="single"
257
+ side="top"
258
+ maxContentHeight="150px"
259
+ callback={fetchCatData}
260
+ placeholder="Select Item"
261
+ />
262
+ {/snippet}
263
+ </Story>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import type { SelectItem, SelectItemData, SelectItemGroup, SelectProps } from './types.js';
2
+ import type { SelectItem, SelectItemGroup, SelectProps } from './types.js';
3
+ import { getItemKey, getLabelFromValue } from './utils/index.js';
3
4
 
4
- import type { Attachment } from 'svelte/attachments';
5
5
  import Content from './components/select.content.svelte';
6
6
  import GroupHeading from './components/select.group-heading.svelte';
7
7
  import Group from './components/select.group.svelte';
@@ -18,7 +18,6 @@
18
18
  empty = 'No results found',
19
19
 
20
20
  maxContentHeight,
21
-
22
21
  data,
23
22
  item,
24
23
  trigger,
@@ -35,33 +34,14 @@
35
34
  ...restProps
36
35
  }: SelectProps = $props();
37
36
 
38
- const getLabelFromValue = (searchValue: string): string | null => {
39
- for (const item of data) {
40
- if (typeof item === 'string') {
41
- if (item === searchValue) return item;
42
- } else if ('group' in item) {
43
- for (const groupItem of item.items) {
44
- if (typeof groupItem === 'string') {
45
- if (groupItem === searchValue) return groupItem;
46
- } else if (groupItem.value === searchValue) {
47
- return groupItem.label;
48
- }
49
- }
50
- } else if (item.value === searchValue) {
51
- return item.label;
52
- }
53
- }
54
- return null;
55
- };
56
-
57
37
  const placeholder = $derived.by(() => {
58
38
  if (typeof value === 'string' && value.trim() !== '') {
59
- const label = getLabelFromValue(value);
39
+ const label = getLabelFromValue(value, data);
60
40
  return label ?? value;
61
41
  }
62
42
 
63
43
  if (Array.isArray(value) && value.length > 0) {
64
- const labels = value.map((v) => getLabelFromValue(v) ?? v);
44
+ const labels = value.map((v) => getLabelFromValue(v, data) ?? v);
65
45
  return labels.join(', ');
66
46
  }
67
47
 
@@ -70,31 +50,9 @@
70
50
  return '';
71
51
  });
72
52
 
73
- const getItemKey = (item: SelectItemData) => {
74
- if (typeof item === 'string') return item;
75
- if ('group' in item) return item.group;
76
- return item.value;
77
- };
78
-
79
53
  const hasResults = $derived.by(() => {
80
54
  return data.length > 0;
81
55
  });
82
-
83
- const contentStyle = $derived.by(() => {
84
- return {
85
- ...(maxContentHeight ? { '--cg-ui-max-content-height': maxContentHeight } : {}),
86
- };
87
- });
88
-
89
- const contentMount: Attachment<HTMLDivElement> = (el) => {
90
- $effect.pre(() => {
91
- if (!el) return;
92
-
93
- Object.entries(contentStyle).forEach(([key, value]) => {
94
- el.style.setProperty(key, value);
95
- });
96
- });
97
- };
98
56
  </script>
99
57
 
100
58
  {#snippet itemsString(item: string)}
@@ -135,7 +93,16 @@
135
93
  {/if}
136
94
 
137
95
  <Portal>
138
- <Content {@attach contentMount} class="cgui:h-(--cg-ui-max-content-height)">
96
+ <Content
97
+ {maxContentHeight}
98
+ {side}
99
+ {sideOffset}
100
+ {align}
101
+ {alignOffset}
102
+ {avoidCollisions}
103
+ {collisionPadding}
104
+ {customAnchor}
105
+ >
139
106
  <Viewport>
140
107
  {#if hasResults}
141
108
  {#each data as item (getItemKey(item))}
@@ -148,7 +115,9 @@
148
115
  {/if}
149
116
  {/each}
150
117
  {:else if typeof empty === 'string'}
151
- <div class="cgui:p-4 cgui:text-center cgui:text-body-2 cgui:text-fg-darkest">
118
+ <div
119
+ class="cgui:p-4 cgui:flex cgui:items-center cgui:h-full cgui:justify-center cgui:text-center cgui:text-body-2 cgui:text-fg-regular"
120
+ >
152
121
  {empty}
153
122
  </div>
154
123
  {:else}
@@ -9,20 +9,23 @@ export const selectVariants = tv({
9
9
  'cgui:shadow-select cgui:bg-surface-white',
10
10
  'cgui:rounded-sm',
11
11
  'cgui:z-(--cg-ui-z-index-select)',
12
- 'cgui:max-h-(--bits-select-content-available-height) cgui:origin-(--bits-select-content-transform-origin)',
12
+ 'cgui:max-h-(--bits-select-content-available-height) cgui:origin-(--bits-select-content-transform-origin) cgui:h-(--cg-ui-max-content-height)',
13
13
  'cgui:data-[state=open]:animate-in cgui:data-[state=closed]:animate-out cgui:data-[state=closed]:fade-out-0 cgui:data-[state=open]:fade-in-0 cgui:data-[state=closed]:zoom-out-95 cgui:data-[state=open]:zoom-in-95 cgui:data-[side=bottom]:slide-in-from-top-2 cgui:data-[side=left]:slide-in-from-right-2 cgui:data-[side=right]:slide-in-from-left-2 cgui:data-[side=top]:slide-in-from-bottom-2 cgui:data-[side=bottom]:translate-y-1 cgui:data-[side=left]:-translate-x-1 cgui:data-[side=right]:translate-x-1 cgui:data-[side=top]:-translate-y-1',
14
14
  ],
15
15
  item: [
16
16
  'cgui:relative cgui:w-full',
17
17
  'cgui:flex cgui:items-center cgui:justify-between cgui:gap-2',
18
18
  'cgui:outline-hidden cgui:cursor-default cgui:select-none',
19
+ 'cgui:transition-all cgui:duration-250 cgui:ease-in-out',
20
+ 'cgui:rounded-xs',
19
21
  'cgui:p-2',
20
- 'cgui:text-body cgui:text-fg-dark',
22
+ 'cgui:text-body cgui:text-fg-medium',
21
23
  'cgui:data-[disabled]:pointer-events-none cgui:data-[disabled]:opacity-50',
22
24
  'cgui:[&_svg]:shrink-0 cgui:[&_svg]:pointer-events-none',
25
+ 'cgui:data-[highlighted]:bg-surface-lightest',
23
26
  ],
24
27
  viewport: [
25
- ' cgui:scrollbar-track-transparent cgui:scrollbar-thumb-stroke-default cgui:scrollbar-thumb-rounded-full',
28
+ 'cgui:scrollbar-track-transparent cgui:scrollbar-thumb-stroke-default cgui:scrollbar-thumb-rounded-full',
26
29
  'cgui:h-(--bits-select-anchor-height) cgui:min-w-(--bits-select-anchor-width) cgui:w-full cgui:scroll-my-1 cgui:p-1',
27
30
  ],
28
31
  group: [],
@@ -6,7 +6,9 @@ export type SelectRootProps = SelectRootPropsPrimitive & SelectVariantsProps;
6
6
  export type SelectTriggerProps = SelectTriggerPropsPrimitive & {
7
7
  hasChevron?: boolean;
8
8
  };
9
- export type SelectContentProps = WithoutChild<SelectContentPropsPrimitive>;
9
+ export type SelectContentProps = WithoutChild<SelectContentPropsPrimitive> & {
10
+ maxContentHeight?: string;
11
+ };
10
12
  export type SelectViewportProps = SelectViewportPropsPrimitive;
11
13
  export type SelectItemProps = WithoutChild<SelectItemPropsPrimitive>;
12
14
  export type SelectGroupProps = SelectGroupPropsPrimitive;
@@ -37,7 +39,7 @@ export type SelectProps = SelectRootProps & {
37
39
  label: string;
38
40
  }]>;
39
41
  empty?: Snippet | string;
40
- maxContentHeight?: string;
42
+ maxContentHeight?: SelectContentProps['maxContentHeight'];
41
43
  side?: SelectContentProps['side'];
42
44
  sideOffset?: SelectContentProps['sideOffset'];
43
45
  align?: SelectContentProps['align'];
@@ -46,3 +48,17 @@ export type SelectProps = SelectRootProps & {
46
48
  collisionPadding?: SelectContentProps['collisionPadding'];
47
49
  customAnchor?: SelectContentProps['customAnchor'];
48
50
  };
51
+ export type SelectAsyncCallbackParams = {
52
+ page: number;
53
+ pageSize: number;
54
+ };
55
+ export type SelectAsyncCallbackResult = {
56
+ data: SelectData;
57
+ hasMore: boolean;
58
+ };
59
+ export type SelectAsyncCallback = (params: SelectAsyncCallbackParams) => Promise<SelectAsyncCallbackResult>;
60
+ export type SelectAsyncProps = Omit<SelectProps, 'data'> & {
61
+ loading?: Snippet;
62
+ pageSize?: number;
63
+ callback: SelectAsyncCallback;
64
+ };
@@ -0,0 +1,2 @@
1
+ import type { SelectItemData } from '../types.js';
2
+ export declare const getItemKey: (item: SelectItemData) => string;
@@ -0,0 +1,7 @@
1
+ export const getItemKey = (item) => {
2
+ if (typeof item === 'string')
3
+ return item;
4
+ if ('group' in item)
5
+ return item.group;
6
+ return item.value;
7
+ };
@@ -0,0 +1,2 @@
1
+ import type { SelectData } from '../types.js';
2
+ export declare const getLabelFromValue: (searchValue: string, data: SelectData) => string | null;
@@ -0,0 +1,23 @@
1
+ export const getLabelFromValue = (searchValue, data) => {
2
+ for (const item of data) {
3
+ if (typeof item === 'string') {
4
+ if (item === searchValue)
5
+ return item;
6
+ }
7
+ else if ('group' in item) {
8
+ for (const groupItem of item.items) {
9
+ if (typeof groupItem === 'string') {
10
+ if (groupItem === searchValue)
11
+ return groupItem;
12
+ }
13
+ else if (groupItem.value === searchValue) {
14
+ return groupItem.label;
15
+ }
16
+ }
17
+ }
18
+ else if (item.value === searchValue) {
19
+ return item.label;
20
+ }
21
+ }
22
+ return null;
23
+ };
@@ -0,0 +1,2 @@
1
+ export { getItemKey } from './get-item-key.js';
2
+ export { getLabelFromValue } from './get-label-from-value.js';
@@ -0,0 +1,2 @@
1
+ export { getItemKey } from './get-item-key.js';
2
+ export { getLabelFromValue } from './get-label-from-value.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casinogate/ui",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "svelte": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",