@reshape-biotech/design-system 0.0.48 → 0.0.49

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.
Files changed (41) hide show
  1. package/dist/components/activity/Activity.stories.svelte +10 -4
  2. package/dist/components/activity/Activity.svelte +11 -12
  3. package/dist/components/activity/Activity.svelte.d.ts +1 -2
  4. package/dist/components/avatar/Avatar.stories.svelte +51 -0
  5. package/dist/components/avatar/Avatar.stories.svelte.d.ts +19 -0
  6. package/dist/components/banner/Banner.stories.svelte +9 -9
  7. package/dist/components/banner/Banner.svelte +3 -3
  8. package/dist/components/button/Button.stories.svelte +7 -1
  9. package/dist/components/collapsible/components/collapsible-trigger.svelte +2 -2
  10. package/dist/components/combobox/Combobox.stories.svelte +326 -5
  11. package/dist/components/combobox/components/combobox-add.svelte +2 -2
  12. package/dist/components/combobox/components/combobox-content.svelte +3 -3
  13. package/dist/components/combobox/components/combobox-indicator.svelte +2 -2
  14. package/dist/components/datepicker/DatePicker.svelte +8 -7
  15. package/dist/components/icon-button/IconButton.stories.svelte +17 -17
  16. package/dist/components/icons/AnalysisIcon.svelte +4 -1
  17. package/dist/components/icons/Icon.stories.svelte +127 -0
  18. package/dist/components/icons/Icon.stories.svelte.d.ts +19 -0
  19. package/dist/components/icons/Icon.svelte +6 -5
  20. package/dist/components/icons/index.d.ts +7 -5
  21. package/dist/components/icons/index.js +170 -4
  22. package/dist/components/input/Input.stories.svelte +7 -1
  23. package/dist/components/list/List.stories.svelte +3 -3
  24. package/dist/components/modal/Modal.svelte +2 -2
  25. package/dist/components/notification-popup/NotificationPopup.stories.svelte +2 -3
  26. package/dist/components/notification-popup/NotificationPopup.svelte +4 -4
  27. package/dist/components/pill/Pill.svelte +2 -2
  28. package/dist/components/segmented-control-buttons/SegmentedControlButtons.stories.svelte +10 -10
  29. package/dist/components/select/Select.svelte +5 -5
  30. package/dist/components/slider/Slider.svelte +4 -3
  31. package/dist/components/stat-card/StatCard.svelte +2 -2
  32. package/dist/components/status-badge/StatusBadge.stories.svelte +26 -34
  33. package/dist/components/table/Table.stories.svelte +6 -2
  34. package/dist/components/tag/Tag.stories.svelte +2 -2
  35. package/dist/components/tooltip/Tooltip.svelte +1 -1
  36. package/dist/components/tooltip/Tooltip.svelte.d.ts +1 -1
  37. package/dist/icons.d.ts +0 -0
  38. package/dist/icons.js +0 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +1 -1
  41. package/package.json +1 -1
@@ -2,13 +2,19 @@
2
2
  import Button from '../button/Button.svelte';
3
3
  import Activity from './Activity.svelte';
4
4
  import * as Collapsible from '../collapsible/index';
5
- import { Eye } from 'phosphor-svelte';
5
+ import { Icon } from '../icons';
6
6
 
7
7
  import { defineMeta } from '@storybook/addon-svelte-csf';
8
8
  const { Story } = defineMeta({
9
9
  component: Activity,
10
10
  title: 'Design System/Activity',
11
- tags: ['autodocs']
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ design: {
14
+ type: 'figma',
15
+ url: 'https://www.figma.com/design/VHTMNdy8PAXAMx43edNZGW/%F0%9F%92%A0--Reshape-Design-System%3A-V1?node-id=2248-7688&t=sCuBI8dX6K6NjNR6-0'
16
+ }
17
+ }
12
18
  });
13
19
 
14
20
  let activities = $state([
@@ -52,7 +58,7 @@
52
58
  </script>
53
59
 
54
60
  <Story name="Collapsible activity">
55
- <Collapsible.Root>
61
+ <Collapsible.Root open>
56
62
  <Collapsible.Trigger class="w-full pb-4">
57
63
  <h6>Activity</h6>
58
64
  </Collapsible.Trigger>
@@ -69,7 +75,7 @@
69
75
  size="sm"
70
76
  class="icon-secondary w-36 rounded-full"
71
77
  >
72
- <Eye />
78
+ <Icon iconName="Eye" />
73
79
  <p>Show more</p>
74
80
  </Button>
75
81
  </Collapsible.Content>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
+ import { Icon, type IconName } from '../icons';
2
3
  import { DateTime } from 'luxon';
3
- import { Plus, CheckFat, Pencil, Play, Pause, Stop, Trash, WarningCircle } from 'phosphor-svelte';
4
- import type { Component } from 'svelte';
5
4
 
6
5
  type ActivityIcon = 'add' | 'edit' | 'success' | 'start' | 'pause' | 'stop' | 'delete' | 'failed';
7
6
 
@@ -21,15 +20,15 @@
21
20
  class?: string;
22
21
  };
23
22
 
24
- const ACTIVITY_ICONS: Record<ActivityIcon, Component> = {
25
- add: Plus,
26
- edit: Pencil,
27
- success: CheckFat,
28
- start: Play,
29
- pause: Pause,
30
- stop: Stop,
31
- delete: Trash,
32
- failed: WarningCircle
23
+ const ACTIVITY_ICONS: Record<ActivityIcon, IconName> = {
24
+ add: 'Plus',
25
+ edit: 'Pencil',
26
+ success: 'CheckFat',
27
+ start: 'Play',
28
+ pause: 'Pause',
29
+ stop: 'Stop',
30
+ delete: 'Trash',
31
+ failed: 'WarningCircle'
33
32
  };
34
33
 
35
34
  function formatDateTime(timestamp: string): string {
@@ -57,7 +56,7 @@
57
56
  <div
58
57
  class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-neutral p-0.5 text-secondary"
59
58
  >
60
- <svelte:component this={ACTIVITY_ICONS[activity.icon]} />
59
+ <Icon iconName={ACTIVITY_ICONS[activity.icon]} />
61
60
  </div>
62
61
  <div class="w-0.5 grow bg-neutral group-last:invisible"></div>
63
62
  </div>
@@ -1,4 +1,3 @@
1
- import type { Component } from 'svelte';
2
1
  type ActivityIcon = 'add' | 'edit' | 'success' | 'start' | 'pause' | 'stop' | 'delete' | 'failed';
3
2
  type Activity = {
4
3
  activity_type?: string;
@@ -14,5 +13,5 @@ type Props = {
14
13
  activity: Activity;
15
14
  class?: string;
16
15
  };
17
- declare const Activity: Component<Props, {}, "">;
16
+ declare const Activity: import("svelte").Component<Props, {}, "">;
18
17
  export default Activity;
@@ -0,0 +1,51 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import Avatar from './Avatar.svelte';
4
+
5
+ const { Story } = defineMeta({
6
+ component: Avatar,
7
+ title: 'Design System/Avatar',
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ design: {
11
+ type: 'figma',
12
+ url: 'https://www.figma.com/design/VHTMNdy8PAXAMx43edNZGW/%F0%9F%92%A0--Reshape-Design-System%3A-V1?node-id=9-9253&t=sCuBI8dX6K6NjNR6-0'
13
+ }
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <Story name="Default">
19
+ <Avatar name="John Doe" />
20
+ </Story>
21
+
22
+ <Story name="Sizes">
23
+ <div class="flex gap-2">
24
+ <Avatar name="John Doe" size="sm" />
25
+ <Avatar name="John Doe" size="md" />
26
+ </div>
27
+ </Story>
28
+
29
+ <Story name="Email">
30
+ <Avatar name="john.doe@example.com" />
31
+ </Story>
32
+
33
+ <Story name="Single Name">
34
+ <Avatar name="John" />
35
+ </Story>
36
+
37
+ <Story name="No Name">
38
+ <Avatar name={null} />
39
+ </Story>
40
+
41
+ <Story name="Without Tooltip">
42
+ <Avatar name="John Doe" showTooltip={false} />
43
+ </Story>
44
+
45
+ <Story name="Multiple Avatars">
46
+ <div class="flex gap-2">
47
+ <Avatar name="John Doe" />
48
+ <Avatar name="Jane Smith" />
49
+ <Avatar name="Alex Johnson" />
50
+ </div>
51
+ </Story>
@@ -0,0 +1,19 @@
1
+ import Avatar from './Avatar.svelte';
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Avatar: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Avatar = InstanceType<typeof Avatar>;
19
+ export default Avatar;
@@ -1,5 +1,5 @@
1
1
  <script module lang="ts">
2
- import { Info, Warning as WarningIcon } from 'phosphor-svelte';
2
+ import { Icon } from '../icons';
3
3
  import Banner from './Banner.svelte';
4
4
  import Button from '../button/Button.svelte';
5
5
  import { defineMeta } from '@storybook/addon-svelte-csf';
@@ -16,7 +16,7 @@
16
16
  <Story name="Well finding warning">
17
17
  <Banner type="warning" closable={false}>
18
18
  {#snippet icon()}
19
- <WarningIcon size="100%" weight="bold" />
19
+ <Icon size={20} iconName="Warning" color="warning" weight="bold" />
20
20
  {/snippet}
21
21
  {#snippet content()}
22
22
  Our system couldn't find the wells in some plates. Analysis can't be done. For more help, <a
@@ -39,7 +39,7 @@
39
39
  <Story name="No toggle">
40
40
  <Banner type="progress" closable={false}>
41
41
  {#snippet icon()}
42
- <Info size="100%" weight="bold" />
42
+ <Icon size={20} iconName="Info" color="icon-blue" weight="bold" />
43
43
  {/snippet}
44
44
  {#snippet content()}
45
45
  This banner has no toggle.
@@ -55,7 +55,7 @@
55
55
 
56
56
  <Banner type="error" bind:show={showBanner}>
57
57
  {#snippet icon()}
58
- <Info size="100%" weight="bold" />
58
+ <Icon size={20} iconName="Info" color="danger" weight="bold" />
59
59
  {/snippet}
60
60
  {#snippet content()}
61
61
  This banner can be toggled off with the "x" to the right and back on with the button above.
@@ -70,7 +70,7 @@
70
70
  <h4>Neutral</h4>
71
71
  <Banner type="neutral">
72
72
  {#snippet icon()}
73
- <Info size="100%" weight="bold" />
73
+ <Icon size={20} iconName="Info" weight="bold" />
74
74
  {/snippet}
75
75
  {#snippet content()}
76
76
  This is a "neutral" banner.
@@ -82,7 +82,7 @@
82
82
  <h4>Success</h4>
83
83
  <Banner type="success">
84
84
  {#snippet icon()}
85
- <Info size="100%" weight="bold" />
85
+ <Icon size={20} iconName="CheckCircle" color="icon-success" weight="bold" />
86
86
  {/snippet}
87
87
  {#snippet content()}
88
88
  This is a "success" banner.
@@ -94,7 +94,7 @@
94
94
  <h4>Progress</h4>
95
95
  <Banner type="progress">
96
96
  {#snippet icon()}
97
- <Info size="100%" weight="bold" />
97
+ <Icon size={20} iconName="Circle" color="icon-blue" weight="bold" />
98
98
  {/snippet}
99
99
  {#snippet content()}
100
100
  This is a "progress" banner.
@@ -106,7 +106,7 @@
106
106
  <h4>Warning</h4>
107
107
  <Banner type="warning">
108
108
  {#snippet icon()}
109
- <Info size="100%" weight="bold" />
109
+ <Icon size={20} iconName="Warning" color="icon-warning" weight="bold" />
110
110
  {/snippet}
111
111
  {#snippet content()}
112
112
  This is a "warning" banner.
@@ -118,7 +118,7 @@
118
118
  <h4>Error</h4>
119
119
  <Banner type="error">
120
120
  {#snippet icon()}
121
- <Info size="100%" weight="bold" />
121
+ <Icon size={20} iconName="WarningCircle" color="icon-danger" weight="bold" />
122
122
  {/snippet}
123
123
  {#snippet content()}
124
124
  This is an "error" banner.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { X } from 'phosphor-svelte';
3
2
  import type { Snippet } from 'svelte';
4
3
  import { IconButton } from '../icon-button/';
4
+ import Icon from '../icons/Icon.svelte';
5
5
 
6
6
  interface Props {
7
7
  type?: 'neutral' | 'success' | 'progress' | 'warning' | 'error';
@@ -32,7 +32,7 @@
32
32
  <div class="rounded-lg bg-{color} flex h-11 w-full items-center justify-between p-3">
33
33
  <div class="inline-flex w-full items-center justify-start gap-3">
34
34
  {#if icon}
35
- <div class="size-5 text-icon-{color} ">
35
+ <div class="flex size-5 items-center justify-center text-icon-{color} ">
36
36
  {@render icon?.()}
37
37
  </div>
38
38
  {/if}
@@ -42,7 +42,7 @@
42
42
  </div>
43
43
  {#if closable}
44
44
  <IconButton size="sm" onclick={() => (show = false)}>
45
- <X class="text-icon-secondary" weight="bold" />
45
+ <Icon iconName="X" color="icon-secondary" weight="bold" />
46
46
  </IconButton>
47
47
  {/if}
48
48
  </div>
@@ -5,7 +5,13 @@
5
5
  const { Story } = defineMeta({
6
6
  component: Button,
7
7
  title: 'Design System/Button',
8
- tags: ['autodocs']
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ design: {
11
+ type: 'figma',
12
+ url: 'https://www.figma.com/design/VHTMNdy8PAXAMx43edNZGW/%F0%9F%92%A0--Reshape-Design-System%3A-V1?node-id=9-3068&t=sCuBI8dX6K6NjNR6-0'
13
+ }
14
+ }
9
15
  });
10
16
  </script>
11
17
 
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Collapsible } from 'bits-ui';
3
- import { CaretDown } from 'phosphor-svelte';
3
+ import { Icon } from '../../icons';
4
4
  import type { CollapsibleTriggerProps } from '../types';
5
5
 
6
6
  let { children, withIcon = true, ...props }: CollapsibleTriggerProps = $props();
@@ -10,7 +10,7 @@
10
10
  <div class="container">
11
11
  {@render children()}
12
12
  {#if withIcon}
13
- <CaretDown class="icon" />
13
+ <Icon iconName="CaretDown" class="icon" />
14
14
  {/if}
15
15
  </div>
16
16
  </Collapsible.Trigger>
@@ -1,15 +1,23 @@
1
1
  <script module lang="ts">
2
2
  import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import { expect, userEvent, within } from '@storybook/test';
3
4
  import * as Combobox from './index';
4
- import { Plus } from 'phosphor-svelte';
5
+ import { Icon } from '../icons';
5
6
  import IconButton from '../icon-button/IconButton.svelte';
6
7
  import Divider from '../divider/Divider.svelte';
7
8
  import Tag from '../tag/Tag.svelte';
9
+ import Button from '../button/Button.svelte';
8
10
 
9
11
  const { Story } = defineMeta({
10
12
  component: Combobox.Root,
11
13
  title: 'Design System/Combobox',
12
- tags: ['autodocs']
14
+ tags: ['autodocs'],
15
+ parameters: {
16
+ design: {
17
+ type: 'figma',
18
+ url: 'https://www.figma.com/design/VHTMNdy8PAXAMx43edNZGW/%F0%9F%92%A0--Reshape-Design-System%3A-V1?node-id=9-3294&t=sCuBI8dX6K6NjNR6-0'
19
+ }
20
+ }
13
21
  });
14
22
 
15
23
  const fruits = [
@@ -33,22 +41,76 @@
33
41
  { value: 'grapefruit', label: 'Grapefruit' }
34
42
  ];
35
43
 
44
+ const categories = [
45
+ { value: 'citrus', label: 'Citrus', items: ['orange', 'grapefruit', 'lemon'] },
46
+ {
47
+ value: 'berries',
48
+ label: 'Berries',
49
+ items: ['strawberry', 'blueberry', 'raspberry', 'blackberry']
50
+ },
51
+ { value: 'tropical', label: 'Tropical', items: ['mango', 'pineapple', 'banana', 'kiwi'] },
52
+ { value: 'stone', label: 'Stone Fruits', items: ['peach', 'cherry', 'plum', 'apricot'] },
53
+ { value: 'other', label: 'Other', items: ['apple', 'pear', 'watermelon', 'grape'] }
54
+ ];
55
+
36
56
  let searchValue = $state('');
57
+ let searchValueSingle = $state('');
58
+ let searchValueGrouped = $state('');
59
+ let searchValueCustom = $state('');
37
60
 
38
61
  let selected = $state<string[]>([]);
62
+ let selectedSingle = $state<string>('');
63
+ let selectedGrouped = $state<string[]>([]);
64
+ let selectedCustom = $state<string[]>([]);
39
65
 
40
66
  const filteredFruits = $derived(
41
67
  searchValue === ''
42
68
  ? fruits
43
69
  : fruits.filter((fruit) => fruit.label.toLowerCase().includes(searchValue.toLowerCase()))
44
70
  );
71
+
72
+ const filteredFruitsSingle = $derived(
73
+ searchValueSingle === ''
74
+ ? fruits
75
+ : fruits.filter((fruit) =>
76
+ fruit.label.toLowerCase().includes(searchValueSingle.toLowerCase())
77
+ )
78
+ );
79
+
80
+ const filteredFruitsGrouped = $derived(
81
+ searchValueGrouped === ''
82
+ ? fruits
83
+ : fruits.filter((fruit) =>
84
+ fruit.label.toLowerCase().includes(searchValueGrouped.toLowerCase())
85
+ )
86
+ );
87
+
88
+ const filteredFruitsCustom = $derived(
89
+ searchValueCustom === ''
90
+ ? fruits
91
+ : fruits.filter((fruit) =>
92
+ fruit.label.toLowerCase().includes(searchValueCustom.toLowerCase())
93
+ )
94
+ );
95
+
45
96
  const exactMatch = $derived(
46
97
  filteredFruits.find((fruit) => fruit.value.toLowerCase() === searchValue.toLowerCase())
47
98
  );
99
+
100
+ const exactMatchCustom = $derived(
101
+ filteredFruitsCustom.find(
102
+ (fruit) => fruit.value.toLowerCase() === searchValueCustom.toLowerCase()
103
+ )
104
+ );
105
+
48
106
  let customAnchor = $state<HTMLElement>(null!);
107
+ let customAnchorButton = $state<HTMLElement>(null!);
108
+ let customAnchorSingle = $state<HTMLElement>(null!);
109
+ let customAnchorGrouped = $state<HTMLElement>(null!);
110
+ let customAnchorCustom = $state<HTMLElement>(null!);
49
111
  </script>
50
112
 
51
- <Story name="Base">
113
+ <Story name="Multiple Selection">
52
114
  <Combobox.Root
53
115
  onOpenChange={(o) => {
54
116
  if (!o) searchValue = '';
@@ -68,7 +130,7 @@
68
130
  <Combobox.Trigger>
69
131
  <div bind:this={customAnchor}>
70
132
  <IconButton rounded={false}>
71
- <Plus size={16} />
133
+ <Icon iconName="Plus" />
72
134
  </IconButton>
73
135
  </div>
74
136
  </Combobox.Trigger>
@@ -104,7 +166,6 @@
104
166
  <Divider />
105
167
 
106
168
  <Combobox.Add
107
- class=""
108
169
  onclick={() => {
109
170
  selected.push(searchValue);
110
171
  fruits.push({ value: searchValue, label: searchValue });
@@ -117,3 +178,263 @@
117
178
  </Combobox.Content>
118
179
  </Combobox.Root>
119
180
  </Story>
181
+
182
+ <Story name="Single Selection">
183
+ <Combobox.Root
184
+ onOpenChange={(o) => {
185
+ if (!o) searchValueSingle = '';
186
+ }}
187
+ type="single"
188
+ name="singleFruit"
189
+ items={filteredFruitsSingle}
190
+ bind:value={selectedSingle}
191
+ >
192
+ <Combobox.Trigger>
193
+ <div bind:this={customAnchorSingle}>
194
+ <Button variant="secondary" size="sm">
195
+ {selectedSingle ? selectedSingle : 'Select a fruit'}
196
+ </Button>
197
+ </div>
198
+ </Combobox.Trigger>
199
+ <Combobox.Content class="flex flex-col justify-between" customAnchor={customAnchorSingle}>
200
+ <div class="flex flex-grow flex-col">
201
+ {#if filteredFruitsSingle.length > 0}
202
+ <Combobox.Group>
203
+ <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
204
+ {#each filteredFruitsSingle as fruit (fruit.value)}
205
+ <Combobox.Item value={fruit.value} label={fruit.label}>
206
+ {#snippet children({ selected })}
207
+ {fruit.label}
208
+ {#if selected}
209
+ <Combobox.Indicator />
210
+ {/if}
211
+ {/snippet}
212
+ </Combobox.Item>
213
+ {:else}
214
+ <span class="block px-5 py-2 text-sm text-muted-foreground"> No results found </span>
215
+ {/each}
216
+ </Combobox.Group>
217
+ {/if}
218
+ </div>
219
+ </Combobox.Content>
220
+ </Combobox.Root>
221
+ </Story>
222
+
223
+ <Story name="Grouped Items">
224
+ <Combobox.Root
225
+ onOpenChange={(o) => {
226
+ if (!o) searchValueGrouped = '';
227
+ }}
228
+ type="multiple"
229
+ name="groupedFruits"
230
+ items={filteredFruitsGrouped}
231
+ bind:value={selectedGrouped}
232
+ >
233
+ <div class="flex flex-wrap gap-2">
234
+ {#each selectedGrouped as fruit}
235
+ <Tag>
236
+ {fruit}
237
+ </Tag>
238
+ {/each}
239
+ </div>
240
+ <Combobox.Trigger>
241
+ <div bind:this={customAnchorGrouped}>
242
+ <Button variant="primary" size="sm">
243
+ <Icon iconName="List" />
244
+ Select fruits by category
245
+ </Button>
246
+ </div>
247
+ </Combobox.Trigger>
248
+ <Combobox.Content class="flex flex-col justify-between" customAnchor={customAnchorGrouped}>
249
+ <div>
250
+ <Combobox.Input
251
+ placeholder="Search across categories"
252
+ oninput={(e: Event) => (searchValueGrouped = (e.target as HTMLInputElement).value)}
253
+ autofocus
254
+ />
255
+ <Divider />
256
+ </div>
257
+ <div class="flex max-h-80 flex-grow flex-col overflow-y-auto">
258
+ {#if searchValueGrouped === ''}
259
+ {#each categories as category}
260
+ <Combobox.Group>
261
+ <Combobox.GroupHeading>{category.label}</Combobox.GroupHeading>
262
+ {#each category.items as item}
263
+ {@const fruit = fruits.find((f) => f.value === item)}
264
+ {#if fruit}
265
+ <Combobox.Item value={fruit.value} label={fruit.label}>
266
+ {#snippet children({ selected })}
267
+ {fruit.label}
268
+ {#if selected}
269
+ <Combobox.Indicator />
270
+ {/if}
271
+ {/snippet}
272
+ </Combobox.Item>
273
+ {/if}
274
+ {/each}
275
+ </Combobox.Group>
276
+ {/each}
277
+ {:else if filteredFruitsGrouped.length > 0}
278
+ <Combobox.Group>
279
+ <Combobox.GroupHeading>Search Results</Combobox.GroupHeading>
280
+ {#each filteredFruitsGrouped as fruit (fruit.value)}
281
+ <Combobox.Item value={fruit.value} label={fruit.label}>
282
+ {#snippet children({ selected })}
283
+ {fruit.label}
284
+ {#if selected}
285
+ <Combobox.Indicator />
286
+ {/if}
287
+ {/snippet}
288
+ </Combobox.Item>
289
+ {/each}
290
+ </Combobox.Group>
291
+ {:else}
292
+ <span class="text-muted-foreground block px-5 py-2 text-sm"> No results found </span>
293
+ {/if}
294
+ </div>
295
+ </Combobox.Content>
296
+ </Combobox.Root>
297
+ </Story>
298
+
299
+ <Story
300
+ name="Interaction Test"
301
+ play={async ({ canvasElement }) => {
302
+ // Get the canvas element
303
+ const canvas = within(canvasElement);
304
+
305
+ // Find and click the trigger button to open the combobox
306
+ const triggerButton = canvas.getByTestId('combobox-trigger');
307
+ await userEvent.click(triggerButton);
308
+
309
+ // Wait for the combobox content to appear in the document
310
+ await new Promise((resolve) => setTimeout(resolve, 300));
311
+
312
+ // Find the combobox content in the document
313
+ const comboboxContent = document.querySelector(
314
+ '[data-testid="combobox-content"]'
315
+ ) as HTMLElement;
316
+ if (!comboboxContent) {
317
+ console.log('Combobox content not found');
318
+ return;
319
+ }
320
+
321
+ // Use within to scope queries to the combobox content
322
+ const contentScope = within(comboboxContent);
323
+ const searchInput = contentScope.getByTestId('combobox-input');
324
+
325
+ // Type "berry" in the search input
326
+ await userEvent.type(searchInput, 'berry');
327
+
328
+ // Wait for the filtering to complete
329
+ await new Promise((resolve) => setTimeout(resolve, 300));
330
+
331
+ // Find and click on the Strawberry option
332
+ const strawberryOption = contentScope.getByText('Strawberry');
333
+ await userEvent.click(strawberryOption);
334
+
335
+ // Wait for selection to be processed
336
+
337
+ // Open the combobox again
338
+
339
+ // Get the search input again
340
+
341
+ // Clear the previous search and type "mango"
342
+ await userEvent.clear(searchInput);
343
+ await userEvent.type(searchInput, 'mango');
344
+
345
+ // Wait for filtering to complete
346
+ await new Promise((resolve) => setTimeout(resolve, 300));
347
+
348
+ // Find and click on the Mango option
349
+ const mangoOption = contentScope.getByText('Mango');
350
+ await userEvent.click(mangoOption);
351
+
352
+ await userEvent.keyboard('{Escape}');
353
+
354
+ // Test complete
355
+ console.log('Successfully selected Strawberry and Mango');
356
+ }}
357
+ >
358
+ <div data-testid="combobox-container">
359
+ <Combobox.Root
360
+ onOpenChange={(o) => {
361
+ if (!o) searchValue = '';
362
+ }}
363
+ type="multiple"
364
+ name="favoriteFruit"
365
+ items={filteredFruits}
366
+ bind:value={selected}
367
+ >
368
+ <div class="flex gap-2" data-testid="selected-tags">
369
+ {#each selected as fruit}
370
+ <Tag>
371
+ <div data-value={fruit}>
372
+ {fruit}
373
+ </div>
374
+ </Tag>
375
+ {/each}
376
+ </div>
377
+ <Combobox.Trigger data-testid="combobox-trigger">
378
+ <div bind:this={customAnchor}>
379
+ <IconButton rounded={false}>
380
+ <Icon iconName="Plus" />
381
+ </IconButton>
382
+ </div>
383
+ </Combobox.Trigger>
384
+ <Combobox.Content
385
+ {customAnchor}
386
+ class="flex flex-col justify-between"
387
+ data-testid="combobox-content"
388
+ >
389
+ <div>
390
+ <Combobox.Input
391
+ placeholder="Search a fruit"
392
+ oninput={(e: Event) => (searchValue = (e.target as HTMLInputElement).value)}
393
+ autofocus
394
+ data-testid="combobox-input"
395
+ />
396
+ <Divider />
397
+ </div>
398
+ <div class="flex flex-grow flex-col">
399
+ {#if filteredFruits.length > 0}
400
+ <Combobox.Group>
401
+ <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
402
+ {#each filteredFruits as fruit (fruit.value)}
403
+ <Combobox.Item
404
+ value={fruit.value}
405
+ label={fruit.label}
406
+ data-testid={`fruit-option-${fruit.value}`}
407
+ >
408
+ {#snippet children({ selected })}
409
+ {fruit.label}
410
+ {#if selected}
411
+ <Combobox.Indicator />
412
+ {/if}
413
+ {/snippet}
414
+ </Combobox.Item>
415
+ {:else}
416
+ <span class="block px-5 py-2 text-sm text-muted-foreground">
417
+ No results found
418
+ </span>
419
+ {/each}
420
+ </Combobox.Group>
421
+ {/if}
422
+ </div>
423
+ {#if !exactMatch && searchValue !== ''}
424
+ <Divider />
425
+
426
+ <Combobox.Add
427
+ data-testid="add-new-fruit"
428
+ onclick={() => {
429
+ selected.push(searchValue);
430
+ fruits.push({ value: searchValue, label: searchValue });
431
+ searchValue = '';
432
+ }}
433
+ >
434
+ <p>Add new fruit</p>
435
+ </Combobox.Add>
436
+ {/if}
437
+ </Combobox.Content>
438
+ </Combobox.Root>
439
+ </div>
440
+ </Story>