@reshape-biotech/design-system 2.7.36 → 2.7.38

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 (40) hide show
  1. package/dist/components/activity/Activity.svelte +6 -4
  2. package/dist/components/button/Button.svelte +2 -1
  3. package/dist/components/card/Card.svelte +7 -5
  4. package/dist/components/card/CardWrapper.svelte +10 -13
  5. package/dist/components/checkbox/Checkbox.svelte +3 -2
  6. package/dist/components/combobox/components/combobox-content.svelte +2 -1
  7. package/dist/components/datepicker/DatePicker.svelte +8 -5
  8. package/dist/components/divider/Divider.svelte +7 -3
  9. package/dist/components/drawer/components/drawer-content.svelte +2 -1
  10. package/dist/components/drawer/components/drawer-title.svelte +2 -1
  11. package/dist/components/dropdown/components/dropdown-item.svelte +2 -1
  12. package/dist/components/dropdown/components/dropdown-sub-trigger.svelte +2 -1
  13. package/dist/components/graphs/scatterplot/Scatterplot.stories.svelte +119 -50
  14. package/dist/components/graphs/scatterplot/Scatterplot.svelte +99 -80
  15. package/dist/components/graphs/scatterplot/Scatterplot.svelte.d.ts +1 -1
  16. package/dist/components/icon-button/IconButton.svelte +3 -2
  17. package/dist/components/input/Input.svelte +2 -1
  18. package/dist/components/label/Label.svelte +3 -1
  19. package/dist/components/list/List.svelte +4 -1
  20. package/dist/components/markdown/Markdown.svelte +2 -1
  21. package/dist/components/segmented-control-buttons/ControlButton.svelte +7 -1
  22. package/dist/components/segmented-control-buttons/SegmentedControlButtons.svelte +2 -1
  23. package/dist/components/select/components/MultiSelectTrigger.svelte +5 -2
  24. package/dist/components/select/components/SelectItem.svelte +2 -1
  25. package/dist/components/select/components/SelectTrigger.svelte +2 -1
  26. package/dist/components/skeleton-loader/SkeletonLoader.svelte +6 -1
  27. package/dist/components/slider/Slider.svelte +6 -3
  28. package/dist/components/status-badge/StatusBadge.svelte +4 -2
  29. package/dist/components/status-badge/StatusBadge.svelte.d.ts +1 -0
  30. package/dist/components/table/Table.svelte +2 -1
  31. package/dist/components/table/components/TBody.svelte +2 -1
  32. package/dist/components/table/components/THead.svelte +2 -1
  33. package/dist/components/table/components/Td.svelte +2 -1
  34. package/dist/components/table/components/Th.svelte +2 -1
  35. package/dist/components/table/components/Tr.svelte +2 -1
  36. package/dist/components/tabs/components/Tab.svelte +2 -1
  37. package/dist/components/tabs/components/Tabs.svelte +2 -1
  38. package/dist/components/tag/Tag.svelte +3 -2
  39. package/dist/components/tooltip/Tooltip.svelte +2 -1
  40. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  import ActivityIcon from './ActivityIcon.svelte';
4
4
  import type { ActivityIconType } from './ActivityIcon.svelte';
5
5
  import { DateTime } from 'luxon';
6
+ import { twMerge } from 'tailwind-merge';
6
7
 
7
8
  type Activity = {
8
9
  activity_type?: string;
@@ -54,13 +55,14 @@
54
55
  let { class: className = '', activity }: Props = $props();
55
56
  </script>
56
57
 
57
- <div class={`group flex items-stretch gap-3 ${className}`}>
58
+ <div class={twMerge('group flex items-stretch gap-3', className)}>
58
59
  <div class="flex min-h-12 flex-col items-center gap-1.5">
59
60
  <div class="bg-neutral w-0.5 grow group-first:invisible"></div>
60
61
  <div
61
- class="text-secondary flex h-5 w-5 shrink-0 items-center justify-center rounded p-0.5 {ACTIVITY_BACKGROUND[
62
- activity.icon
63
- ]}"
62
+ class={twMerge(
63
+ 'text-secondary flex h-5 w-5 shrink-0 items-center justify-center rounded p-0.5',
64
+ ACTIVITY_BACKGROUND[activity.icon]
65
+ )}
64
66
  >
65
67
  <ActivityIcon icon={activity.icon} />
66
68
  </div>
@@ -12,6 +12,7 @@
12
12
  </script>
13
13
 
14
14
  <script lang="ts">
15
+ import { twMerge } from 'tailwind-merge';
15
16
  import Spinner from '../spinner/Spinner.svelte';
16
17
  import type { Snippet } from 'svelte';
17
18
 
@@ -66,7 +67,7 @@
66
67
  {id}
67
68
  {tabindex}
68
69
  class:cursor-wait={loading}
69
- class="button {variantClass} {sizeClass} {className}"
70
+ class={twMerge('button', variantClass, sizeClass, className)}
70
71
  data-testid={dataTestId}
71
72
  class:rounded
72
73
  class:disabled
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
 
4
5
  type Props = {
5
6
  Header?: Snippet;
@@ -19,19 +20,20 @@
19
20
  </script>
20
21
 
21
22
  <div
22
- class="overflow-hidden rounded-[10px] border border-static bg-surface shadow-container {additionalClasses}"
23
+ class={twMerge(
24
+ 'overflow-hidden rounded-[10px] border border-static bg-surface shadow-container',
25
+ additionalClasses
26
+ )}
23
27
  >
24
28
  {#if Header}
25
29
  <header
26
- class="flex min-h-12 items-center px-4"
27
- class:border-b={headerBorder}
28
- class:border-static={headerBorder}
30
+ class={twMerge('flex min-h-12 items-center px-4', headerBorder && 'border-b border-static')}
29
31
  >
30
32
  {@render Header()}
31
33
  </header>
32
34
  {/if}
33
35
  {#if children}
34
- <div class={`p-${padding}`}>
36
+ <div class={twMerge(`p-${padding}`)}>
35
37
  {@render children()}
36
38
  </div>
37
39
  {/if}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { ClassValue } from 'svelte/elements';
4
+ import { twMerge } from 'tailwind-merge';
4
5
 
5
6
  type Props = {
6
7
  Header?: Snippet;
@@ -19,28 +20,24 @@
19
20
  }: Props = $props();
20
21
  </script>
21
22
 
22
- <div class={['wrapper', variant, additionalClasses]}>
23
+ <div class={twMerge('wrapper', variant, additionalClasses)}>
23
24
  {#if Header}
24
25
  <header
25
- class={[
26
+ class={twMerge(
26
27
  'flex w-full items-center justify-between',
27
- {
28
- 'min-h-10 px-4': variant === 'default',
29
- 'min-h-8 px-3': variant === 'compact',
30
- },
31
- ]}
28
+ variant === 'default' && 'min-h-10 px-4',
29
+ variant === 'compact' && 'min-h-8 px-3'
30
+ )}
32
31
  >
33
32
  {@render Header()}
34
33
  </header>
35
34
  {/if}
36
35
  <div
37
- class={[
36
+ class={twMerge(
38
37
  'flex w-full flex-col !overflow-hidden [&>*]:w-full',
39
- {
40
- 'gap-1 [&>*]:rounded-xl': variant === 'default',
41
- 'gap-0.5 [&>*]:rounded-[10px]': variant === 'compact',
42
- },
43
- ]}
38
+ variant === 'default' && 'gap-1 [&>*]:rounded-xl',
39
+ variant === 'compact' && 'gap-0.5 [&>*]:rounded-[10px]'
40
+ )}
44
41
  >
45
42
  {@render children()}
46
43
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import Check from 'phosphor-svelte/lib/Check';
3
4
  import Minus from 'phosphor-svelte/lib/Minus';
4
5
  import { Icon } from '../icons/index.js';
@@ -6,13 +7,13 @@
6
7
 
7
8
  export type CheckboxProps = CheckboxRootProps;
8
9
 
9
- let { checked = $bindable(false), onCheckedChange, name, ...props }: CheckboxProps = $props();
10
+ let { checked = $bindable(false), onCheckedChange, name, class: className, ...props }: CheckboxProps = $props();
10
11
  </script>
11
12
 
12
13
  <Checkbox.Root bind:checked {onCheckedChange} {...props}>
13
14
  {#snippet children({ checked, indeterminate })}
14
15
  <div
15
- class={`flex items-center justify-center rounded border border-static ${checked ? 'bg-dark-accent-inverse-hover' : ''}`}
16
+ class={twMerge('flex items-center justify-center rounded border border-static', checked && 'bg-dark-accent-inverse-hover', className)}
16
17
  >
17
18
  {#if checked}
18
19
  <Icon class="text-primary-inverse" icon={Check} />
@@ -5,6 +5,7 @@
5
5
  import { Combobox } from 'bits-ui';
6
6
  import type { ComboboxContentProps } from '../types';
7
7
  import { fade } from 'svelte/transition';
8
+ import { twMerge } from 'tailwind-merge';
8
9
 
9
10
  let {
10
11
  children,
@@ -30,7 +31,7 @@
30
31
  <Icon icon={CaretUp} color="tertiary" />
31
32
  </Combobox.ScrollUpButton>
32
33
  <Combobox.Viewport class="overflow-y-auto">
33
- <div class="{paddedContent ? 'p-1' : ''} empty:p-0">
34
+ <div class={twMerge(paddedContent ? 'p-1' : '', 'empty:p-0')}>
34
35
  {@render children()}
35
36
  </div>
36
37
  </Combobox.Viewport>
@@ -3,6 +3,7 @@
3
3
  import CaretRight from 'phosphor-svelte/lib/CaretRight';
4
4
  import { Icon } from '../icons/index.js';
5
5
  import { run } from 'svelte/legacy';
6
+ import { twMerge } from 'tailwind-merge';
6
7
 
7
8
  import { DateTime, type MonthNumbers } from 'luxon';
8
9
 
@@ -125,11 +126,13 @@
125
126
  {#if dayOfMonth > 0}
126
127
  <div class="date-container">
127
128
  <p
128
- class={dayOfMonth === selectedDay &&
129
- selectedMonth === shownMonth &&
130
- selectedYear === shownYear
131
- ? 'date selected-date'
132
- : 'date'}
129
+ class={twMerge(
130
+ 'date',
131
+ dayOfMonth === selectedDay &&
132
+ selectedMonth === shownMonth &&
133
+ selectedYear === shownYear &&
134
+ 'selected-date'
135
+ )}
133
136
  >
134
137
  <button
135
138
  onclick={() => {
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
3
+
2
4
  interface Props {
3
5
  class?: string;
4
6
  vertical?: boolean;
@@ -9,7 +11,9 @@
9
11
  </script>
10
12
 
11
13
  <div
12
- class={`${
13
- vertical ? 'h-full w-0 border-l' : 'h-0 w-full border-b'
14
- } ${inverse ? 'border-static-inverse' : 'border-static'} ${className}`}
14
+ class={twMerge(
15
+ vertical ? 'h-full w-0 border-l' : 'h-0 w-full border-b',
16
+ inverse ? 'border-static-inverse' : 'border-static',
17
+ className
18
+ )}
15
19
  ></div>
@@ -3,6 +3,7 @@
3
3
  import { Icon } from '../../icons/index.js';
4
4
  import { Dialog } from 'bits-ui';
5
5
  import { fly } from 'svelte/transition';
6
+ import { twMerge } from 'tailwind-merge';
6
7
  import type { DrawerContentProps } from '../types';
7
8
  import Overlay from './drawer-overlay.svelte';
8
9
 
@@ -20,7 +21,7 @@
20
21
 
21
22
  const baseClasses = 'fixed top-0 h-screen z-50 w-[460px] flex flex-col bg-surface shadow-lg';
22
23
  const sideClasses = side === 'left' ? 'left-0' : 'right-0';
23
- const finalClasses = `${baseClasses} ${sideClasses} ${className}`;
24
+ const finalClasses = twMerge(baseClasses, sideClasses, className);
24
25
 
25
26
  const getTransitionParams = (side: 'left' | 'right') => ({
26
27
  x: side === 'left' ? -300 : 300,
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { Dialog } from 'bits-ui';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import type { DrawerTitleProps } from '../types';
4
5
 
5
6
  let { children, class: className = '', icon, ...restProps }: DrawerTitleProps = $props();
6
7
 
7
8
  const baseClasses = 'text-xl font-semibold tracking-tight text-primary';
8
- const finalClasses = `${baseClasses} ${className}`;
9
+ const finalClasses = twMerge(baseClasses, className);
9
10
  </script>
10
11
 
11
12
  <div class="space-y-6">
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { DropdownMenu } from 'bits-ui';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import type { DropdownItemProps } from '../types';
4
5
 
5
6
  let {
@@ -18,7 +19,7 @@
18
19
  {:else}
19
20
  <div
20
21
  {...props}
21
- class="dropdown-item flex gap-2 cursor-pointer items-center rounded-md px-2.5 py-2 min-h-10 text-sm outline-none transition-colors hover:bg-neutral focus:bg-neutral data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_a]:no-underline {className}"
22
+ class={twMerge('dropdown-item flex gap-2 cursor-pointer items-center rounded-md px-2.5 py-2 min-h-10 text-sm outline-none transition-colors hover:bg-neutral focus:bg-neutral data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_a]:no-underline', className)}
22
23
  >
23
24
  {@render children?.()}
24
25
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { DropdownMenu } from 'bits-ui';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import CaretRight from 'phosphor-svelte/lib/CaretRight';
4
5
  import CaretLeft from 'phosphor-svelte/lib/CaretLeft';
5
6
  import { Icon } from '../../icons/index.js';
@@ -17,7 +18,7 @@
17
18
  {:else}
18
19
  <div
19
20
  {...props}
20
- class="dropdown-sub-trigger flex h-9 cursor-pointer items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-neutral focus:bg-neutral data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_a]:no-underline {className}"
21
+ class={twMerge('dropdown-sub-trigger flex h-9 cursor-pointer items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-neutral focus:bg-neutral data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_a]:no-underline', className)}
21
22
  >
22
23
  {#if !hideChevron && side === 'left'}
23
24
  <Icon color="tertiary" icon={chevronIcon} size={14} class="mr-2" />
@@ -10,12 +10,12 @@
10
10
  });
11
11
 
12
12
  const data: DataPoint[] = [
13
- { value: [10.0, 8.04], metadata: { blip: '1', blop: 1, user_id: '1' } },
14
- { value: [8.07, 6.95], metadata: { blip: '2', blop: 2, user_id: '2' } },
15
- { value: [13.0, 7.58], metadata: { blip: '3', blop: 3, user_id: '3' } },
16
- { value: [9.05, 8.81], metadata: { blip: '4', blop: 4, user_id: '4' } },
17
- { value: [12.0, 8.33], metadata: { blip: '5', blop: 5, user_id: '5' } },
18
- { value: [14.0, 7.66], metadata: { blip: '6', blop: 6, user_id: '6' } },
13
+ { value: [10, 8], metadata: { id: '1' } },
14
+ { value: [8, 7], metadata: { id: '2' } },
15
+ { value: [13, 8], metadata: { id: '3' } },
16
+ { value: [9, 9], metadata: { id: '4' } },
17
+ { value: [12, 8], metadata: { id: '5' } },
18
+ { value: [14, 8], metadata: { id: '6' } },
19
19
  ];
20
20
 
21
21
  const errorBarsData: DataPoint[] = data.map((d) => ({
@@ -28,78 +28,147 @@
28
28
  [12, 14],
29
29
  ];
30
30
 
31
- function handleItemClick(params: echarts.ECElementEvent) {}
32
-
33
- function handleMouseOver(params: echarts.ECElementEvent) {}
34
-
35
- function handleMouseOut() {}
31
+ let tooltipState = $state({ visible: false, x: 0, y: 0, point: null as DataPoint | null });
36
32
  </script>
37
33
 
38
34
  <Story name="Base" asChild>
39
35
  <div class="h-[400px] w-full">
40
- <Scatterplot
41
- {data}
42
- xAxisName="Manual count"
43
- yAxisName="Analysis count"
44
- onitemclick={handleItemClick}
45
- onmouseover={handleMouseOver}
46
- onmouseout={handleMouseOut}
47
- />
36
+ <Scatterplot {data} xAxisName="X axis" yAxisName="Y axis" />
48
37
  </div>
49
38
  </Story>
50
39
 
51
- <Story name="With Line Data" asChild>
40
+ <Story name="With reference line" asChild>
52
41
  <div class="h-[400px] w-full">
53
- <Scatterplot
54
- {data}
55
- {lineData}
56
- xAxisName="Manual count"
57
- yAxisName="Analysis count"
58
- onitemclick={handleItemClick}
59
- onmouseover={handleMouseOver}
60
- onmouseout={handleMouseOut}
61
- />
42
+ <Scatterplot {data} {lineData} xAxisName="X axis" yAxisName="Y axis" />
62
43
  </div>
63
44
  </Story>
64
- <Story name="Error bars" asChild>
45
+
46
+ <Story name="With error bars" asChild>
47
+ <div class="h-[400px] w-full">
48
+ <Scatterplot data={errorBarsData} {lineData} xAxisName="X axis" yAxisName="Y axis" />
49
+ </div>
50
+ </Story>
51
+
52
+ <Story name="With confidence band" asChild>
65
53
  <div class="h-[400px] w-full">
66
54
  <Scatterplot
67
55
  data={errorBarsData}
68
56
  {lineData}
69
- xAxisName="Manual count"
70
- yAxisName="Analysis count"
71
- onitemclick={handleItemClick}
72
- onmouseover={handleMouseOver}
73
- onmouseout={handleMouseOut}
57
+ xAxisName="X axis"
58
+ yAxisName="Y axis"
59
+ showConfidenceBand
74
60
  />
75
61
  </div>
76
62
  </Story>
77
- <Story name="Error bars with confidence band" asChild>
63
+
64
+ <Story name="With legend" asChild>
78
65
  <div class="h-[400px] w-full">
79
66
  <Scatterplot
80
- data={errorBarsData}
67
+ {data}
81
68
  {lineData}
82
- xAxisName="Manual count"
83
- yAxisName="Analysis count"
69
+ xAxisName="X axis"
70
+ yAxisName="Y axis"
84
71
  showConfidenceBand
85
- onitemclick={handleItemClick}
86
- onmouseover={handleMouseOver}
87
- onmouseout={handleMouseOut}
72
+ showLegend
88
73
  />
89
74
  </div>
90
75
  </Story>
91
- <Story name="Confidence band and legend" asChild>
76
+
77
+ <Story name="With groups" asChild>
78
+ {@const data: DataPoint[] = [
79
+ { value: [8, 7], metadata: { id: 'A' } },
80
+ { value: [8, 10], metadata: { id: 'A' } },
81
+ { value: [15, 14], metadata: { id: 'B' } },
82
+ { value: [23, 21], metadata: { id: 'C' } },
83
+ { value: [23, 25], metadata: { id: 'C' } },
84
+ { value: [23, 22], metadata: { id: 'C' } },
85
+ { value: [35, 32], metadata: { id: 'D' } },
86
+ { value: [35, 39], metadata: { id: 'D' }, disagreement: true },
87
+ { value: [42, 41], metadata: { id: 'E' } },
88
+ { value: [42, 44], metadata: { id: 'E' } },
89
+ { value: [56, 58], metadata: { id: 'F' } },
90
+ ]}
91
+ {@const groups = [[0, 1], [2], [3, 4, 5], [6, 7], [8, 9], [10]]}
92
+ {@const lineData: [[number, number], [number, number]] = [[0, 0], [65, 65]]}
92
93
  <div class="h-[400px] w-full">
93
94
  <Scatterplot
94
95
  {data}
96
+ {groups}
95
97
  {lineData}
96
- xAxisName="Manual count"
97
- yAxisName="Analysis count"
98
+ xAxisName="X axis"
99
+ yAxisName="Y axis"
98
100
  showConfidenceBand
99
- showLegend
100
- onitemclick={handleItemClick}
101
- onmouseover={handleMouseOver}
102
- onmouseout={handleMouseOut}
103
- />
101
+ onmouseover={(params) => {
102
+ tooltipState.visible = true;
103
+ tooltipState.x = (params.event?.event as MouseEvent)?.offsetX ?? 0;
104
+ tooltipState.y = (params.event?.event as MouseEvent)?.offsetY ?? 0;
105
+ tooltipState.point = params.data;
106
+ }}
107
+ onmouseout={() => {
108
+ tooltipState.visible = false;
109
+ tooltipState.point = null;
110
+ }}
111
+ >
112
+ {#if tooltipState.visible && tooltipState.point}
113
+ <div
114
+ class="pointer-events-none absolute z-50 rounded border bg-white p-2 text-xs shadow-lg"
115
+ style="left: {tooltipState.x + 10}px; top: {tooltipState.y + 10}px;"
116
+ >
117
+ <div class="font-medium">ID: {tooltipState.point.metadata.id}</div>
118
+ <div>X: {tooltipState.point.value[0]}</div>
119
+ <div>Y: {tooltipState.point.value[1]}</div>
120
+ </div>
121
+ {/if}
122
+ </Scatterplot>
123
+ </div>
124
+ </Story>
125
+
126
+ <Story name="Overlapping groups" asChild>
127
+ {@const data: DataPoint[] = [
128
+ { value: [12, 9], metadata: { id: 'A' } },
129
+ { value: [12, 15], metadata: { id: 'A' } },
130
+ { value: [12, 12], metadata: { id: 'B' } },
131
+ { value: [12, 18], metadata: { id: 'B' } },
132
+ { value: [12, 14], metadata: { id: 'B' } },
133
+ { value: [31, 27], metadata: { id: 'C' } },
134
+ { value: [31, 34], metadata: { id: 'C' } },
135
+ { value: [31, 30], metadata: { id: 'D' } },
136
+ ]}
137
+ {@const groups = [
138
+ [0, 1],
139
+ [2, 3, 4],
140
+ [5, 6],
141
+ [7],
142
+ ]}
143
+ {@const lineData: [[number, number], [number, number]] = [[0, 0], [40, 40]]}
144
+ <div class="h-[400px] w-full">
145
+ <Scatterplot
146
+ {data}
147
+ {groups}
148
+ {lineData}
149
+ xAxisName="X axis"
150
+ yAxisName="Y axis"
151
+ onmouseover={(params) => {
152
+ tooltipState.visible = true;
153
+ tooltipState.x = (params.event?.event as MouseEvent)?.offsetX ?? 0;
154
+ tooltipState.y = (params.event?.event as MouseEvent)?.offsetY ?? 0;
155
+ tooltipState.point = params.data;
156
+ }}
157
+ onmouseout={() => {
158
+ tooltipState.visible = false;
159
+ tooltipState.point = null;
160
+ }}
161
+ >
162
+ {#if tooltipState.visible && tooltipState.point}
163
+ <div
164
+ class="pointer-events-none absolute z-50 rounded border bg-white p-2 text-xs shadow-lg"
165
+ style="left: {tooltipState.x + 10}px; top: {tooltipState.y + 10}px;"
166
+ >
167
+ <div class="font-medium">ID: {tooltipState.point.metadata.id}</div>
168
+ <div>X: {tooltipState.point.value[0]}</div>
169
+ <div>Y: {tooltipState.point.value[1]}</div>
170
+ </div>
171
+ {/if}
172
+ </Scatterplot>
104
173
  </div>
105
174
  </Story>
@@ -16,7 +16,6 @@
16
16
  export type DataPoint = {
17
17
  value: [number, number];
18
18
  metadata: any;
19
- error_value?: number;
20
19
  disagreement?: boolean | null;
21
20
  highlighted?: boolean | null;
22
21
  };
@@ -31,9 +30,15 @@
31
30
  type: 'point' | 'line' | 'area';
32
31
  };
33
32
 
33
+ type LineSegment = {
34
+ points: [number, number][];
35
+ disagreement?: boolean;
36
+ };
37
+
34
38
  type ScatterPlotProps = {
35
39
  data: DataPoint[];
36
40
  lineData?: [[number, number], [number, number]];
41
+ groups?: number[][];
37
42
  showConfidenceBand?: boolean;
38
43
  showLegend?: boolean;
39
44
  onitemclick?: (params: ScatterPlotEchartsEvent) => void;
@@ -58,10 +63,55 @@
58
63
  { label: 'Perfect agreement', color: backgroundColor['lilac-inverse'], type: 'line' },
59
64
  ],
60
65
  highlightIndex = null,
66
+ groups = [],
61
67
  children,
62
68
  ...props
63
69
  }: ScatterPlotProps = $props();
64
70
 
71
+ let hoveredGroupIndices = $state<number[] | null>(null);
72
+
73
+ const findGroupForIndex = (idx: number): number[] | null => {
74
+ if (groups.length === 0) return null;
75
+ return groups.find((g) => g.includes(idx)) ?? null;
76
+ };
77
+
78
+ const highlightedIndices = $derived.by((): number[] | null => {
79
+ if (hoveredGroupIndices !== null) return hoveredGroupIndices;
80
+ if (highlightIndex !== null) {
81
+ const group = findGroupForIndex(highlightIndex);
82
+ return group ?? [highlightIndex];
83
+ }
84
+ return null;
85
+ });
86
+
87
+ const groupLines = $derived.by((): LineSegment[] => {
88
+ if (groups.length === 0) return [];
89
+
90
+ return groups
91
+ .filter((g) => g.length > 1)
92
+ .map((groupIndices) => {
93
+ const points = groupIndices.map((i) => data[i]?.value).filter(Boolean) as [
94
+ number,
95
+ number,
96
+ ][];
97
+ const hasDisagreement = groupIndices.some((i) => data[i]?.disagreement);
98
+ return { points, disagreement: hasDisagreement };
99
+ });
100
+ });
101
+
102
+ const groupedDisagreement = $derived.by((): boolean[] => {
103
+ const result = data.map((d) => d.disagreement ?? false);
104
+ for (const groupIndices of groups) {
105
+ const hasDisagreement = groupIndices.some((i) => data[i]?.disagreement);
106
+ if (hasDisagreement) {
107
+ for (const i of groupIndices) {
108
+ result[i] = true;
109
+ }
110
+ }
111
+ }
112
+ return result;
113
+ });
114
+
65
115
  const displayedLegendItems: LegendItem[] = $derived([
66
116
  ...legendItems,
67
117
  ...(showConfidenceBand
@@ -82,75 +132,44 @@
82
132
  }
83
133
 
84
134
  function handleMouseOver(params: ScatterPlotEchartsEvent) {
135
+ const hoveredIdx = params.dataIndex;
136
+ if (hoveredIdx !== undefined && hoveredIdx >= 0) {
137
+ const group = findGroupForIndex(hoveredIdx);
138
+ hoveredGroupIndices = group ?? [hoveredIdx];
139
+ }
85
140
  onmouseover?.(params);
86
141
  }
87
142
 
88
143
  function handleMouseOut() {
144
+ hoveredGroupIndices = null;
89
145
  onmouseout?.();
90
146
  }
91
147
 
92
- // from https://echarts.apache.org/examples/en/editor.html?c=custom-error-bar
93
- function renderErrorBarItem(
94
- params: CustomSeriesRenderItemParams,
95
- api: CustomSeriesRenderItemAPI
96
- ) {
148
+ function renderLineItem(params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI) {
97
149
  const xValue = api.value(0) as number;
98
- const yValue = api.value(1) as number;
99
- const error = api.value(2) as number;
150
+ const yMin = api.value(1) as number;
151
+ const yMax = api.value(2) as number;
100
152
  const disagreement = api.value(3) as number;
101
153
 
102
- const highPoint = api.coord([xValue, yValue + error]);
103
- const lowPoint = api.coord([xValue, yValue - error]);
154
+ const topPoint = api.coord([xValue, yMax]);
155
+ const bottomPoint = api.coord([xValue, yMin]);
104
156
 
105
- if (!highPoint || !lowPoint) {
157
+ if (!topPoint || !bottomPoint) {
106
158
  return undefined;
107
159
  }
108
160
 
109
- // calculate a dynamic width for the caps based on the x-axis scale
110
- const sizeValue = api.size?.([1, 0]);
111
- const baseWidth = Array.isArray(sizeValue) ? sizeValue[0] : 10;
112
- const halfWidth = Math.min((baseWidth ?? 10) * 1.5, 3);
113
- const style = {
114
- stroke: disagreement ? textColor['icon-tertiary'] : backgroundColor['blue-inverse'],
115
- };
116
-
117
161
  return {
118
- type: 'group' as const,
119
- children: [
120
- {
121
- type: 'line' as const,
122
- transition: 'shape' as const,
123
- shape: {
124
- x1: highPoint[0] - halfWidth,
125
- y1: highPoint[1],
126
- x2: highPoint[0] + halfWidth,
127
- y2: highPoint[1],
128
- },
129
- style,
130
- },
131
- {
132
- type: 'line' as const,
133
- transition: 'shape' as const,
134
- shape: {
135
- x1: highPoint[0],
136
- y1: highPoint[1],
137
- x2: lowPoint[0],
138
- y2: lowPoint[1],
139
- },
140
- style,
141
- },
142
- {
143
- type: 'line' as const,
144
- transition: 'shape' as const,
145
- shape: {
146
- x1: lowPoint[0] - halfWidth,
147
- y1: lowPoint[1],
148
- x2: lowPoint[0] + halfWidth,
149
- y2: lowPoint[1],
150
- },
151
- style,
152
- },
153
- ],
162
+ type: 'line' as const,
163
+ shape: {
164
+ x1: topPoint[0],
165
+ y1: topPoint[1],
166
+ x2: bottomPoint[0],
167
+ y2: bottomPoint[1],
168
+ },
169
+ style: {
170
+ stroke: disagreement ? textColor['icon-tertiary'] : backgroundColor['blue-inverse'],
171
+ lineWidth: 1.5,
172
+ },
154
173
  };
155
174
  }
156
175
 
@@ -158,9 +177,6 @@
158
177
 
159
178
  function getChartOptions(): EChartsOption {
160
179
  const series: SeriesOption[] = [];
161
- const errorBarData = data
162
- .filter((d) => d.error_value != 0)
163
- .map((d) => [...d.value, d.error_value, d.disagreement ? 1 : 0]);
164
180
 
165
181
  if (props.lineData && props.lineData.length > 0) {
166
182
  const extendedLineData = [
@@ -226,36 +242,32 @@
226
242
  type: 'scatter',
227
243
  itemStyle: {
228
244
  color: (params: any) =>
229
- params?.data?.disagreement ? textColor['icon-tertiary'] : backgroundColor['blue-inverse'],
245
+ groupedDisagreement[params.dataIndex]
246
+ ? textColor['icon-tertiary']
247
+ : backgroundColor['blue-inverse'],
230
248
  opacity: 1,
231
249
  },
232
250
  emphasis: {
233
- itemStyle: {
234
- color: (params: any) => {
235
- const point = params?.data as DataPoint;
236
- return point?.disagreement
237
- ? textColor['icon-tertiary']
238
- : backgroundColor['blue-inverse'];
239
- },
240
- opacity: 1,
241
- },
251
+ disabled: true,
242
252
  },
243
253
  animation: false,
244
254
  });
245
255
 
246
- const p =
247
- highlightIndex !== null && highlightIndex >= 0 && highlightIndex < data.length
248
- ? data[highlightIndex]
249
- : null;
256
+ const highlightPoints =
257
+ highlightedIndices !== null
258
+ ? highlightedIndices
259
+ .filter((i) => i >= 0 && i < data.length)
260
+ .map((i) => ({ point: data[i], index: i }))
261
+ : [];
250
262
  series.push({
251
263
  id: 'highlight-overlay',
252
264
  type: 'scatter',
253
- data: [p],
265
+ data: highlightPoints.map((h) => h.point),
254
266
  symbolSize: 16,
255
267
  itemStyle: {
256
268
  color: (params: any) => {
257
- const point = params?.data as DataPoint;
258
- return point?.disagreement
269
+ const originalIndex = highlightPoints[params.dataIndex]?.index;
270
+ return groupedDisagreement[originalIndex]
259
271
  ? backgroundColor['neutral-hover']
260
272
  : backgroundColor['blue-hover'];
261
273
  },
@@ -264,16 +276,23 @@
264
276
  silent: true,
265
277
  });
266
278
 
267
- if (errorBarData.length > 0) {
279
+ if (groupLines.length > 0) {
280
+ const linesData = groupLines.map((line) => {
281
+ const yValues = line.points.map((p: [number, number]) => p[1]);
282
+ const xValue = line.points[0][0];
283
+ return [xValue, Math.min(...yValues), Math.max(...yValues), line.disagreement ? 1 : 0];
284
+ });
285
+
268
286
  series.push({
269
287
  type: 'custom',
270
- name: 'error',
288
+ name: 'lines',
271
289
  silent: true,
272
- itemStyle: { borderWidth: 1.5 },
273
- renderItem: renderErrorBarItem,
274
- data: errorBarData,
290
+ renderItem: renderLineItem,
291
+ data: linesData,
292
+ z: -1,
275
293
  });
276
294
  }
295
+
277
296
  return {
278
297
  xAxis: {
279
298
  type: 'value',
@@ -4,7 +4,6 @@ import type { Snippet } from 'svelte';
4
4
  export type DataPoint = {
5
5
  value: [number, number];
6
6
  metadata: any;
7
- error_value?: number;
8
7
  disagreement?: boolean | null;
9
8
  highlighted?: boolean | null;
10
9
  };
@@ -19,6 +18,7 @@ type LegendItem = {
19
18
  type ScatterPlotProps = {
20
19
  data: DataPoint[];
21
20
  lineData?: [[number, number], [number, number]];
21
+ groups?: number[][];
22
22
  showConfidenceBand?: boolean;
23
23
  showLegend?: boolean;
24
24
  onitemclick?: (params: ScatterPlotEchartsEvent) => void;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
  import { Spinner } from '../spinner/';
4
5
  import type { HTMLButtonAttributes } from 'svelte/elements';
@@ -50,7 +51,7 @@
50
51
  }}
51
52
  {type}
52
53
  {disabled}
53
- class="{sizeClass} {variantClass} disabled {className}"
54
+ class={twMerge(sizeClass, variantClass, 'disabled', className)}
54
55
  class:rounded
55
56
  {...rest}
56
57
  >
@@ -66,7 +67,7 @@
66
67
  {type}
67
68
  {disabled}
68
69
  {tabindex}
69
- class="{sizeClass} {variantClass} {className}"
70
+ class={twMerge(sizeClass, variantClass, className)}
70
71
  class:rounded
71
72
  {...rest}
72
73
  >
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import X from 'phosphor-svelte/lib/X';
3
4
  import { Icon } from '../icons/index.js';
4
5
  import type { Snippet } from 'svelte';
@@ -91,7 +92,7 @@
91
92
 
92
93
  <div class="flex-1">
93
94
  <div
94
- class="flex w-full items-center gap-1 size-{size} transition-colors"
95
+ class={twMerge('flex w-full items-center gap-1 transition-colors', `size-${size}`, className)}
95
96
  class:!border-error={!valid}
96
97
  class:primary={variant === 'primary'}
97
98
  class:secondary={variant === 'secondary'}
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
3
+
2
4
  interface Props {
3
5
  forId: string;
4
6
  text: string;
@@ -9,7 +11,7 @@
9
11
  const { forId, text, required = false, class: className }: Props = $props();
10
12
  </script>
11
13
 
12
- <label for={forId} class="block text-sm text-secondary {className ?? ''}">
14
+ <label for={forId} class={twMerge('block text-sm text-secondary', className)}>
13
15
  {text}
14
16
  {#if required}
15
17
  <span class="ml-0.5 text-danger">*</span>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" generics="T">
2
2
  import type { Overflow } from '../../tailwind';
3
3
  import type { Snippet } from 'svelte';
4
+ import { twMerge } from 'tailwind-merge';
4
5
 
5
6
  type Props = {
6
7
  items: T[];
@@ -11,7 +12,9 @@
11
12
  let { items, item, overflow = 'overflow-y-auto', variant = 'default' }: Props = $props();
12
13
  </script>
13
14
 
14
- <div class="{`listStyles ${variant === 'compact' ? 'compact' : 'default'} ${overflow} `}}">
15
+ <div
16
+ class={twMerge('listStyles', variant === 'compact' ? 'compact' : 'default', overflow)}
17
+ >
15
18
  {#each items as currentItem, i}
16
19
  <div class="item">
17
20
  {@render item(currentItem)}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { marked } from 'marked';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  interface Props {
4
5
  markdown: string;
5
6
  variant?: 'light' | 'dark';
@@ -8,6 +9,6 @@
8
9
  let { markdown, variant = 'light' }: Props = $props();
9
10
  </script>
10
11
 
11
- <div class="prose prose-slate text-sm {variant === 'dark' ? 'dark:prose-invert' : ''}">
12
+ <div class={twMerge('prose prose-slate text-sm', variant === 'dark' && 'dark:prose-invert')}>
12
13
  {@html marked.parse(markdown ?? '')}
13
14
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
 
4
5
  type Color = 'primary' | 'secondary';
5
6
 
@@ -32,7 +33,12 @@
32
33
  <div class="join-item flex h-full flex-1">
33
34
  <button
34
35
  {onclick}
35
- class={`control-button flex flex-1 items-center justify-center gap-2 rounded-md text-sm leading-4 ${colors[color]} ${className} ${color}`}
36
+ class={twMerge(
37
+ 'control-button flex flex-1 items-center justify-center gap-2 rounded-md text-sm leading-4',
38
+ colors[color],
39
+ color,
40
+ className
41
+ )}
36
42
  class:active
37
43
  type="button"
38
44
  {disabled}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import ControlButton from './ControlButton.svelte';
4
5
 
5
6
  interface Props {
@@ -16,6 +17,6 @@
16
17
  };
17
18
  </script>
18
19
 
19
- <div class={`join flex gap-1 rounded-lg bg-neutral p-1 ${sizes[size]} ${size} ${className}`}>
20
+ <div class={twMerge('join flex gap-1 rounded-lg bg-neutral p-1', sizes[size], size, className)}>
20
21
  {@render children?.({ ControlButton })}
21
22
  </div>
@@ -3,6 +3,7 @@
3
3
  import Icon from '../../icons/Icon.svelte';
4
4
  import * as Select from '../index';
5
5
  import IconButton from '../../icon-button/IconButton.svelte';
6
+ import { twMerge } from 'tailwind-merge';
6
7
 
7
8
  type ItemType = {
8
9
  value: string;
@@ -30,8 +31,10 @@
30
31
  </script>
31
32
 
32
33
  <Select.Trigger
33
- class={'flex h-fit min-h-10 w-full items-center justify-between rounded-lg border border-input bg-surface py-1 text-sm placeholder:text-tertiary focus:outline-none disabled:cursor-not-allowed disabled:border-transparent disabled:bg-neutral disabled:opacity-75' +
34
- (className || '')}
34
+ class={twMerge(
35
+ 'flex h-fit min-h-10 w-full items-center justify-between rounded-lg border border-input bg-surface py-1 text-sm placeholder:text-tertiary focus:outline-none disabled:cursor-not-allowed disabled:border-transparent disabled:bg-neutral disabled:opacity-75',
36
+ className
37
+ )}
35
38
  {...restProps}
36
39
  >
37
40
  {#if selectedValues.length === 0}
@@ -2,6 +2,7 @@
2
2
  import Check from 'phosphor-svelte/lib/Check';
3
3
  import { Icon } from '../../icons/index.js';
4
4
  import { Select as SelectPrimitive } from 'bits-ui';
5
+ import { twMerge } from 'tailwind-merge';
5
6
  import type { ItemProps, ItemSlotProps } from '../types';
6
7
 
7
8
  import type { Snippet } from 'svelte';
@@ -21,7 +22,7 @@
21
22
  `;
22
23
  </script>
23
24
 
24
- <SelectPrimitive.Item {...restProps} class={baseClasses + ' ' + className}>
25
+ <SelectPrimitive.Item {...restProps} class={twMerge(baseClasses, className)}>
25
26
  {#snippet children(params: ItemSlotProps)}
26
27
  {#if children}
27
28
  {@render children(params)}
@@ -2,6 +2,7 @@
2
2
  import CaretDown from 'phosphor-svelte/lib/CaretDown';
3
3
  import { Icon } from '../../icons/index.js';
4
4
  import { Select as SelectPrimitive } from 'bits-ui';
5
+ import { twMerge } from 'tailwind-merge';
5
6
  import type { TriggerProps, IconSnippet } from '../types';
6
7
 
7
8
  import type { Snippet } from 'svelte';
@@ -37,7 +38,7 @@
37
38
 
38
39
  <SelectPrimitive.Trigger
39
40
  {...restProps}
40
- class={`${baseClasses} ${className}`}
41
+ class={twMerge(baseClasses, className)}
41
42
  aria-label={placeholder}
42
43
  >
43
44
  <div class="flex-1 truncate text-left">
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import Skeleton from './components/Skeleton.svelte';
4
4
  import SkeletonImage from './components/SkeletonImage.svelte';
5
+ import { twMerge } from 'tailwind-merge';
5
6
 
6
7
  interface Props {
7
8
  class?: string;
@@ -12,6 +13,10 @@
12
13
  let { class: className = '', dataTestId = '', children }: Props = $props();
13
14
  </script>
14
15
 
15
- <div role="status" class={`h-full w-full animate-pulse ${className}`} data-testid={dataTestId}>
16
+ <div
17
+ role="status"
18
+ class={twMerge('h-full w-full animate-pulse', className)}
19
+ data-testid={dataTestId}
20
+ >
16
21
  {@render children?.({ Skeleton, SkeletonImage })}
17
22
  </div>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import CaretUpDown from 'phosphor-svelte/lib/CaretUpDown';
3
3
  import { Icon } from '../icons/index.js';
4
+ import { twMerge } from 'tailwind-merge';
4
5
 
5
6
  let className = '';
6
7
  export { className as class };
@@ -31,10 +32,12 @@
31
32
  }
32
33
  </script>
33
34
 
34
- <div class={`relative flex h-10 items-center ${className} ${disabled ? 'opacity-40' : ''}`}>
35
+ <div
36
+ class={twMerge('relative flex h-10 items-center', disabled && 'opacity-40', className)}
37
+ >
35
38
  <div class="track-overlay"></div>
36
39
  <div
37
- class="pointer-events-none absolute h-2.5 rounded-full {bufferColorClass}"
40
+ class={twMerge('pointer-events-none absolute h-2.5 rounded-full', bufferColorClass)}
38
41
  style="
39
42
  width: {calculatePosition(buffer - bufferMin)} + 1rem);
40
43
  left: {calculatePosition(bufferMin)}
@@ -42,7 +45,7 @@
42
45
  ></div>
43
46
  {#if showFilled}
44
47
  <div
45
- class="absolute h-2.5 rounded-full {filledColorClass}"
48
+ class={twMerge('absolute h-2.5 rounded-full', filledColorClass)}
46
49
  style="width: {calculatePosition(value)} + 1rem);left: 0;"
47
50
  ></div>
48
51
  {/if}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
 
4
5
  interface Props {
5
6
  // we can add dynamic classes because they are safelisted in tailwind-safelist.txt
@@ -9,6 +10,7 @@
9
10
  progress?: Snippet;
10
11
  icon?: Snippet;
11
12
  iconOnly?: boolean;
13
+ class?: string;
12
14
  }
13
15
 
14
16
  let {
@@ -18,14 +20,14 @@
18
20
  progress,
19
21
  icon,
20
22
  iconOnly: propIconOnly = false,
23
+ class: className,
21
24
  }: Props = $props();
22
25
 
23
26
  let iconOnly = $derived((content === undefined && progress === undefined) || propIconOnly);
24
27
  </script>
25
28
 
26
29
  <div
27
- class={`badge badge-${type} badge-${size}`}
28
- class:badge-icon-only={iconOnly}
30
+ class={twMerge(`badge badge-${type} badge-${size}`, iconOnly && 'badge-icon-only', className)}
29
31
  data-testid="status-badge"
30
32
  >
31
33
  {#if icon}
@@ -6,6 +6,7 @@ interface Props {
6
6
  progress?: Snippet;
7
7
  icon?: Snippet;
8
8
  iconOnly?: boolean;
9
+ class?: string;
9
10
  }
10
11
  declare const StatusBadge: import("svelte").Component<Props, {}, "">;
11
12
  type StatusBadge = ReturnType<typeof StatusBadge>;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
  import Td from './components/Td.svelte';
4
5
  import Th from './components/Th.svelte';
@@ -32,7 +33,7 @@
32
33
  </script>
33
34
 
34
35
  <div class="w-full rounded-xl border border-static bg-surface px-5 py-2 shadow-container">
35
- <table class={`w-full table-fixed sm:table-auto xl:table-${tableLayout}`}>
36
+ <table class={twMerge('w-full table-fixed sm:table-auto', `xl:table-${tableLayout}`)}>
36
37
  {@render children?.({ THead: THead, TBody: TBody, Tr: Tr, Th: Th, Td: Td })}
37
38
  </table>
38
39
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
5
  interface Props {
@@ -9,6 +10,6 @@
9
10
  let { class: className = '', children }: Props = $props();
10
11
  </script>
11
12
 
12
- <tbody class={`${className}`}>
13
+ <tbody class={twMerge(className)}>
13
14
  {@render children?.()}
14
15
  </tbody>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
5
  interface Props {
@@ -9,6 +10,6 @@
9
10
  let { class: className = '', children }: Props = $props();
10
11
  </script>
11
12
 
12
- <thead class={`${className}`}>
13
+ <thead class={twMerge(className)}>
13
14
  {@render children?.()}
14
15
  </thead>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
5
  interface Props {
@@ -10,6 +11,6 @@
10
11
  let { dataTestId = '', align = 'left', children }: Props = $props();
11
12
  </script>
12
13
 
13
- <td data-testid={dataTestId} class={`text-${align} text-sm font-normal`}>
14
+ <td data-testid={dataTestId} class={twMerge(`text-${align}`, 'text-sm font-normal')}>
14
15
  {@render children?.()}
15
16
  </td>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Width } from '../../../tailwind';
3
4
  import type { Snippet } from 'svelte';
4
5
 
@@ -10,6 +11,6 @@
10
11
  let { class: className = '', children }: Props = $props();
11
12
  </script>
12
13
 
13
- <th class={`text-left text-xs font-medium text-tertiary ${className}`}>
14
+ <th class={twMerge('text-left text-xs font-medium text-tertiary', className)}>
14
15
  {@render children?.()}
15
16
  </th>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { twMerge } from 'tailwind-merge';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
5
  interface Props {
@@ -10,7 +11,7 @@
10
11
  let { disabled = false, children, class: className = '' }: Props = $props();
11
12
  </script>
12
13
 
13
- <tr class={`border-static py-2 [&.disabled]:text-tertiary ${className}`} class:disabled>
14
+ <tr class={twMerge('border-static py-2 [&.disabled]:text-tertiary', className)} class:disabled>
14
15
  {@render children?.()}
15
16
  </tr>
16
17
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import Button from '../../button/Button.svelte';
4
5
 
5
6
  interface Props {
@@ -16,7 +17,7 @@
16
17
  {onclick}
17
18
  variant="transparent"
18
19
  size="sm"
19
- class={`tab !h-full ${className} ${active ? 'active' : ''}`}
20
+ class={twMerge('tab !h-full', active && 'active', className)}
20
21
  >
21
22
  {@render children?.()}
22
23
  </Button>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
 
4
5
  type Size = 'sm' | 'md' | 'lg';
5
6
  type Variant = 'primary' | 'secondary' | 'transparent';
@@ -29,7 +30,7 @@
29
30
 
30
31
  <div
31
32
  role="tablist"
32
- class={`flex ${sizes[size]} ${variant} ${className} ${fullWidth ? 'w-full [&>*]:flex-1' : 'w-fit [&>*]:flex-none'}`}
33
+ class={twMerge('flex', sizes[size], variant, fullWidth ? 'w-full [&>*]:flex-1' : 'w-fit [&>*]:flex-none', className)}
33
34
  >
34
35
  {@render children?.({ variant })}
35
36
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import { Tooltip } from '../tooltip/';
4
5
 
5
6
  interface Props {
@@ -82,14 +83,14 @@
82
83
  {#snippet renderTag()}
83
84
  {#if onclick}
84
85
  <button
85
- class="outer {variantClassName} {sizeClassName} {buttonClassVariant} hover:cursor-pointer {className} "
86
+ class={twMerge('outer', variantClassName, sizeClassName, buttonClassVariant, 'hover:cursor-pointer', className)}
86
87
  title={tooltip}
87
88
  {onclick}
88
89
  >
89
90
  {@render children()}
90
91
  </button>
91
92
  {:else}
92
- <div class="outer {variantClassName} {sizeClassName} no-underline {className}" title={tooltip}>
93
+ <div class={twMerge('outer', variantClassName, sizeClassName, 'no-underline', className)} title={tooltip}>
93
94
  {@render children()}
94
95
  </div>
95
96
  {/if}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Tooltip } from 'bits-ui';
3
+ import { twMerge } from 'tailwind-merge';
3
4
  import type { Snippet } from 'svelte';
4
5
  import { backgroundColor } from '../../tokens';
5
6
 
@@ -50,7 +51,7 @@
50
51
  {side}
51
52
  {align}
52
53
  sideOffset={8}
53
- class={`${className} z-50 max-w-xs text-wrap rounded-md bg-base-inverse px-2 py-1 text-left text-sm font-normal text-primary-inverse shadow-menu`}
54
+ class={twMerge('z-50 max-w-xs text-wrap rounded-md bg-base-inverse px-2 py-1 text-left text-sm font-normal text-primary-inverse shadow-menu', className)}
54
55
  >
55
56
  {#if content}
56
57
  {@render content()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reshape-biotech/design-system",
3
- "version": "2.7.36",
3
+ "version": "2.7.38",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build",