@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.
- package/dist/components/activity/Activity.stories.svelte +10 -4
- package/dist/components/activity/Activity.svelte +11 -12
- package/dist/components/activity/Activity.svelte.d.ts +1 -2
- package/dist/components/avatar/Avatar.stories.svelte +51 -0
- package/dist/components/avatar/Avatar.stories.svelte.d.ts +19 -0
- package/dist/components/banner/Banner.stories.svelte +9 -9
- package/dist/components/banner/Banner.svelte +3 -3
- package/dist/components/button/Button.stories.svelte +7 -1
- package/dist/components/collapsible/components/collapsible-trigger.svelte +2 -2
- package/dist/components/combobox/Combobox.stories.svelte +326 -5
- package/dist/components/combobox/components/combobox-add.svelte +2 -2
- package/dist/components/combobox/components/combobox-content.svelte +3 -3
- package/dist/components/combobox/components/combobox-indicator.svelte +2 -2
- package/dist/components/datepicker/DatePicker.svelte +8 -7
- package/dist/components/icon-button/IconButton.stories.svelte +17 -17
- package/dist/components/icons/AnalysisIcon.svelte +4 -1
- package/dist/components/icons/Icon.stories.svelte +127 -0
- package/dist/components/icons/Icon.stories.svelte.d.ts +19 -0
- package/dist/components/icons/Icon.svelte +6 -5
- package/dist/components/icons/index.d.ts +7 -5
- package/dist/components/icons/index.js +170 -4
- package/dist/components/input/Input.stories.svelte +7 -1
- package/dist/components/list/List.stories.svelte +3 -3
- package/dist/components/modal/Modal.svelte +2 -2
- package/dist/components/notification-popup/NotificationPopup.stories.svelte +2 -3
- package/dist/components/notification-popup/NotificationPopup.svelte +4 -4
- package/dist/components/pill/Pill.svelte +2 -2
- package/dist/components/segmented-control-buttons/SegmentedControlButtons.stories.svelte +10 -10
- package/dist/components/select/Select.svelte +5 -5
- package/dist/components/slider/Slider.svelte +4 -3
- package/dist/components/stat-card/StatCard.svelte +2 -2
- package/dist/components/status-badge/StatusBadge.stories.svelte +26 -34
- package/dist/components/table/Table.stories.svelte +6 -2
- package/dist/components/tag/Tag.stories.svelte +2 -2
- package/dist/components/tooltip/Tooltip.svelte +1 -1
- package/dist/components/tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/icons.d.ts +0 -0
- package/dist/icons.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 {
|
|
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,
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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 {
|
|
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 {
|
|
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="
|
|
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
|
-
<
|
|
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>
|