@genspectrum/dashboard-components 0.1.4 → 0.1.5

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 (38) hide show
  1. package/custom-elements.json +213 -78
  2. package/dist/dashboard-components.js +303 -53
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +288 -69
  5. package/dist/style.css +142 -15
  6. package/package.json +3 -3
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +2 -0
  8. package/src/preact/aggregatedData/aggregate.tsx +9 -4
  9. package/src/preact/components/headline.stories.tsx +19 -1
  10. package/src/preact/components/headline.tsx +9 -1
  11. package/src/preact/components/info.stories.tsx +24 -3
  12. package/src/preact/components/info.tsx +49 -5
  13. package/src/preact/dateRangeSelector/date-range-selector.tsx +10 -10
  14. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -0
  15. package/src/preact/mutationComparison/mutation-comparison.tsx +3 -3
  16. package/src/preact/mutationFilter/mutation-filter.tsx +1 -1
  17. package/src/preact/mutations/mutations.stories.tsx +3 -0
  18. package/src/preact/mutations/mutations.tsx +9 -3
  19. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
  20. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +14 -4
  21. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +3 -0
  22. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +49 -4
  23. package/src/web-components/display/aggregate-component.stories.ts +3 -0
  24. package/src/web-components/display/aggregate-component.tsx +15 -1
  25. package/src/web-components/display/mutation-comparison-component.stories.ts +3 -0
  26. package/src/web-components/display/mutation-comparison-component.tsx +7 -0
  27. package/src/web-components/display/mutations-component.stories.ts +27 -7
  28. package/src/web-components/display/mutations-component.tsx +58 -4
  29. package/src/web-components/display/prevalence-over-time-component.stories.ts +24 -0
  30. package/src/web-components/display/prevalence-over-time-component.tsx +93 -5
  31. package/src/web-components/display/relative-growth-advantage-component.stories.ts +21 -0
  32. package/src/web-components/display/relative-growth-advantage-component.tsx +54 -3
  33. package/src/web-components/input/date-range-selector-component.stories.ts +17 -2
  34. package/src/web-components/input/date-range-selector-component.tsx +57 -5
  35. package/src/web-components/input/mutation-filter-component.stories.ts +13 -3
  36. package/src/web-components/input/mutation-filter-component.tsx +50 -2
  37. package/src/web-components/input/text-input-component.stories.ts +14 -3
  38. package/src/web-components/input/text-input-component.tsx +23 -1
package/dist/style.css CHANGED
@@ -1065,7 +1065,6 @@ html {
1065
1065
  transition-duration: 200ms;
1066
1066
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1067
1067
  border-width: var(--border-btn, 1px);
1068
- animation: button-pop var(--animation-btn, 0.25s) ease-out;
1069
1068
  transition-property: color, background-color, border-color, opacity, box-shadow, transform;
1070
1069
  --tw-text-opacity: 1;
1071
1070
  color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
@@ -1540,6 +1539,14 @@ html {
1540
1539
  z-index: 3;
1541
1540
  opacity: 1;
1542
1541
  }
1542
+ .steps {
1543
+ display: inline-grid;
1544
+ grid-auto-flow: column;
1545
+ overflow: hidden;
1546
+ overflow-x: auto;
1547
+ counter-reset: step;
1548
+ grid-auto-columns: 1fr;
1549
+ }
1543
1550
  .steps .step {
1544
1551
  display: grid;
1545
1552
  grid-template-columns: repeat(1, minmax(0, 1fr));
@@ -1554,8 +1561,7 @@ html {
1554
1561
  display: grid;
1555
1562
  align-items: flex-end;
1556
1563
  }
1557
- .tabs-lifted:has(.tab-content[class^="rounded-"]) .tab:first-child:not(.tab-active),
1558
- .tabs-lifted:has(.tab-content[class*=" rounded-"]) .tab:first-child:not(.tab-active) {
1564
+ .tabs-lifted:has(.tab-content[class^="rounded-"]) .tab:first-child:not(:is(.tab-active, [aria-selected="true"])), .tabs-lifted:has(.tab-content[class*=" rounded-"]) .tab:first-child:not(:is(.tab-active, [aria-selected="true"])) {
1559
1565
  border-bottom-color: transparent;
1560
1566
  }
1561
1567
  .tab {
@@ -1600,7 +1606,7 @@ html {
1600
1606
  grid-column-start: span 9999;
1601
1607
  }
1602
1608
  input.tab:checked + .tab-content,
1603
- .tab-active + .tab-content {
1609
+ :is(.tab-active, [aria-selected="true"]) + .tab-content {
1604
1610
  display: block;
1605
1611
  }
1606
1612
  .table {
@@ -1645,6 +1651,12 @@ input.tab:checked + .tab-content,
1645
1651
  font-size: 1rem;
1646
1652
  line-height: 1.5rem;
1647
1653
  }
1654
+ @media (prefers-reduced-motion: no-preference) {
1655
+
1656
+ .btn {
1657
+ animation: button-pop var(--animation-btn, 0.25s) ease-out;
1658
+ }
1659
+ }
1648
1660
  .btn:active:hover,
1649
1661
  .btn:active:focus {
1650
1662
  animation: button-pop 0s ease-out;
@@ -2222,12 +2234,79 @@ input.tab:checked + .tab-content,
2222
2234
  .steps .step[data-content]:after {
2223
2235
  content: attr(data-content);
2224
2236
  }
2237
+ .steps .step-neutral + .step-neutral:before,
2238
+ .steps .step-neutral:after {
2239
+ --tw-bg-opacity: 1;
2240
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
2241
+ --tw-text-opacity: 1;
2242
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
2243
+ }
2244
+ .steps .step-primary + .step-primary:before,
2245
+ .steps .step-primary:after {
2246
+ --tw-bg-opacity: 1;
2247
+ background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
2248
+ --tw-text-opacity: 1;
2249
+ color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
2250
+ }
2251
+ .steps .step-secondary + .step-secondary:before,
2252
+ .steps .step-secondary:after {
2253
+ --tw-bg-opacity: 1;
2254
+ background-color: var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));
2255
+ --tw-text-opacity: 1;
2256
+ color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
2257
+ }
2258
+ .steps .step-accent + .step-accent:before,
2259
+ .steps .step-accent:after {
2260
+ --tw-bg-opacity: 1;
2261
+ background-color: var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));
2262
+ --tw-text-opacity: 1;
2263
+ color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)));
2264
+ }
2265
+ .steps .step-info + .step-info:before {
2266
+ --tw-bg-opacity: 1;
2267
+ background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));
2268
+ }
2269
+ .steps .step-info:after {
2270
+ --tw-bg-opacity: 1;
2271
+ background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));
2272
+ --tw-text-opacity: 1;
2273
+ color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));
2274
+ }
2275
+ .steps .step-success + .step-success:before {
2276
+ --tw-bg-opacity: 1;
2277
+ background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));
2278
+ }
2279
+ .steps .step-success:after {
2280
+ --tw-bg-opacity: 1;
2281
+ background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));
2282
+ --tw-text-opacity: 1;
2283
+ color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));
2284
+ }
2285
+ .steps .step-warning + .step-warning:before {
2286
+ --tw-bg-opacity: 1;
2287
+ background-color: var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));
2288
+ }
2289
+ .steps .step-warning:after {
2290
+ --tw-bg-opacity: 1;
2291
+ background-color: var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));
2292
+ --tw-text-opacity: 1;
2293
+ color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
2294
+ }
2295
+ .steps .step-error + .step-error:before {
2296
+ --tw-bg-opacity: 1;
2297
+ background-color: var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));
2298
+ }
2299
+ .steps .step-error:after {
2300
+ --tw-bg-opacity: 1;
2301
+ background-color: var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));
2302
+ --tw-text-opacity: 1;
2303
+ color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
2304
+ }
2225
2305
  .tabs-lifted > .tab:focus-visible {
2226
2306
  border-end-end-radius: 0;
2227
2307
  border-end-start-radius: 0;
2228
2308
  }
2229
- .tab.tab-active:not(.tab-disabled):not([disabled]),
2230
- .tab:is(input:checked) {
2309
+ .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]), .tab:is(input:checked) {
2231
2310
  border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));
2232
2311
  --tw-border-opacity: 1;
2233
2312
  --tw-text-opacity: 1;
@@ -2262,8 +2341,7 @@ input.tab:checked + .tab-content,
2262
2341
  padding-inline-end: var(--tab-padding, 1rem);
2263
2342
  padding-top: var(--tab-border, 1px);
2264
2343
  }
2265
- .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]),
2266
- .tabs-lifted > .tab:is(input:checked) {
2344
+ .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]), .tabs-lifted > .tab:is(input:checked) {
2267
2345
  background-color: var(--tab-bg);
2268
2346
  border-width: var(--tab-border, 1px) var(--tab-border, 1px) 0 var(--tab-border, 1px);
2269
2347
  border-inline-start-color: var(--tab-border-color);
@@ -2274,7 +2352,7 @@ input.tab:checked + .tab-content,
2274
2352
  padding-bottom: var(--tab-border, 1px);
2275
2353
  padding-top: 0;
2276
2354
  }
2277
- .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked):before {
2355
+ .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked):before {
2278
2356
  z-index: 1;
2279
2357
  content: "";
2280
2358
  display: block;
@@ -2303,26 +2381,26 @@ input.tab:checked + .tab-content,
2303
2381
  );
2304
2382
  background-image: var(--radius-start), var(--radius-end);
2305
2383
  }
2306
- .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before, .tabs-lifted > .tab:is(input:checked):first-child:before {
2384
+ .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):first-child:before, .tabs-lifted > .tab:is(input:checked):first-child:before {
2307
2385
  background-image: var(--radius-end);
2308
2386
  background-position: top right;
2309
2387
  }
2310
- [dir="rtl"] .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):first-child:before {
2388
+ [dir="rtl"] .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):first-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):first-child:before {
2311
2389
  background-image: var(--radius-start);
2312
2390
  background-position: top left;
2313
2391
  }
2314
- .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before, .tabs-lifted > .tab:is(input:checked):last-child:before {
2392
+ .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):last-child:before, .tabs-lifted > .tab:is(input:checked):last-child:before {
2315
2393
  background-image: var(--radius-start);
2316
2394
  background-position: top left;
2317
2395
  }
2318
- [dir="rtl"] .tabs-lifted > .tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):last-child:before {
2396
+ [dir="rtl"] .tabs-lifted > .tab:is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):last-child:before, [dir="rtl"] .tabs-lifted > .tab:is(input:checked):last-child:before {
2319
2397
  background-image: var(--radius-end);
2320
2398
  background-position: top right;
2321
2399
  }
2322
2400
  .tabs-lifted
2323
- > .tab-active:not(.tab-disabled):not([disabled])
2401
+ > :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled])
2324
2402
  + .tabs-lifted
2325
- .tab-active:not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked) + .tabs-lifted .tab:is(input:checked):before {
2403
+ :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):before, .tabs-lifted > .tab:is(input:checked) + .tabs-lifted .tab:is(input:checked):before {
2326
2404
  background-image: var(--radius-end);
2327
2405
  background-position: top right;
2328
2406
  }
@@ -2643,9 +2721,21 @@ input.tab:checked + .tab-content,
2643
2721
  .static {
2644
2722
  position: static;
2645
2723
  }
2724
+ .absolute {
2725
+ position: absolute;
2726
+ }
2646
2727
  .relative {
2647
2728
  position: relative;
2648
2729
  }
2730
+ .right-6 {
2731
+ right: 1.5rem;
2732
+ }
2733
+ .top-8 {
2734
+ top: 2rem;
2735
+ }
2736
+ .z-50 {
2737
+ z-index: 50;
2738
+ }
2649
2739
  .z-\[1\] {
2650
2740
  z-index: 1;
2651
2741
  }
@@ -2682,6 +2772,9 @@ input.tab:checked + .tab-content,
2682
2772
  .mt-2 {
2683
2773
  margin-top: 0.5rem;
2684
2774
  }
2775
+ .mt-4 {
2776
+ margin-top: 1rem;
2777
+ }
2685
2778
  .inline {
2686
2779
  display: inline;
2687
2780
  }
@@ -2742,6 +2835,9 @@ input.tab:checked + .tab-content,
2742
2835
  .items-center {
2743
2836
  align-items: center;
2744
2837
  }
2838
+ .justify-end {
2839
+ justify-content: flex-end;
2840
+ }
2745
2841
  .justify-center {
2746
2842
  justify-content: center;
2747
2843
  }
@@ -2797,6 +2893,10 @@ input.tab:checked + .tab-content,
2797
2893
  .border-b-2 {
2798
2894
  border-bottom-width: 2px;
2799
2895
  }
2896
+ .border-black {
2897
+ --tw-border-opacity: 1;
2898
+ border-color: rgb(0 0 0 / var(--tw-border-opacity));
2899
+ }
2800
2900
  .border-error {
2801
2901
  --tw-border-opacity: 1;
2802
2902
  border-color: var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));
@@ -2856,6 +2956,13 @@ input.tab:checked + .tab-content,
2856
2956
  padding-top: 0.5rem;
2857
2957
  padding-bottom: 0.5rem;
2858
2958
  }
2959
+ .text-justify {
2960
+ text-align: justify;
2961
+ }
2962
+ .text-base {
2963
+ font-size: 1rem;
2964
+ line-height: 1.5rem;
2965
+ }
2859
2966
  .text-lg {
2860
2967
  font-size: 1.125rem;
2861
2968
  line-height: 1.75rem;
@@ -2881,15 +2988,31 @@ input.tab:checked + .tab-content,
2881
2988
  .leading-5 {
2882
2989
  line-height: 1.25rem;
2883
2990
  }
2991
+ .text-blue-600 {
2992
+ --tw-text-opacity: 1;
2993
+ color: rgb(37 99 235 / var(--tw-text-opacity));
2994
+ }
2884
2995
  .text-gray-600 {
2885
2996
  --tw-text-opacity: 1;
2886
2997
  color: rgb(75 85 99 / var(--tw-text-opacity));
2887
2998
  }
2999
+ .underline {
3000
+ text-decoration-line: underline;
3001
+ }
2888
3002
  .shadow {
2889
3003
  --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
2890
3004
  --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
2891
3005
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
2892
3006
  }
3007
+ .shadow-lg {
3008
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
3009
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
3010
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
3011
+ }
3012
+ .blur {
3013
+ --tw-blur: blur(8px);
3014
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
3015
+ }
2893
3016
  .filter {
2894
3017
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
2895
3018
  }
@@ -2905,6 +3028,10 @@ input.tab:checked + .tab-content,
2905
3028
  --tw-bg-opacity: 1;
2906
3029
  background-color: rgb(243 244 246 / var(--tw-bg-opacity));
2907
3030
  }
3031
+ .hover\:text-blue-800:hover {
3032
+ --tw-text-opacity: 1;
3033
+ color: rgb(30 64 175 / var(--tw-text-opacity));
3034
+ }
2908
3035
  .hover\:text-gray-700:hover {
2909
3036
  --tw-text-opacity: 1;
2910
3037
  color: rgb(55 65 81 / var(--tw-text-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -70,7 +70,7 @@
70
70
  "zod": "^3.23.0"
71
71
  },
72
72
  "devDependencies": {
73
- "@custom-elements-manifest/analyzer": "^0.9.4",
73
+ "@custom-elements-manifest/analyzer": "^0.10.2",
74
74
  "@playwright/test": "^1.43.1",
75
75
  "@storybook/addon-actions": "^8.0.9",
76
76
  "@storybook/addon-essentials": "^8.0.9",
@@ -80,7 +80,7 @@
80
80
  "@storybook/preact": "^8.0.9",
81
81
  "@storybook/preact-vite": "^8.0.9",
82
82
  "@storybook/test": "^8.0.0",
83
- "@storybook/test-runner": "^0.17.0",
83
+ "@storybook/test-runner": "^0.18.0",
84
84
  "@storybook/types": "^8.0.9",
85
85
  "@storybook/web-components": "^8.0.9",
86
86
  "@storybook/web-components-vite": "^8.0.9",
@@ -11,6 +11,7 @@ const meta: Meta<AggregateProps> = {
11
11
  argTypes: {
12
12
  fields: [{ control: 'object' }],
13
13
  size: [{ control: 'object' }],
14
+ headline: { control: 'text' },
14
15
  },
15
16
  parameters: {
16
17
  fetchMock: {
@@ -49,5 +50,6 @@ export const Default: StoryObj<AggregateProps> = {
49
50
  country: 'USA',
50
51
  },
51
52
  size: { width: '100%', height: '70vh' },
53
+ headline: 'Aggregate',
52
54
  },
53
55
  };
@@ -22,17 +22,22 @@ export interface AggregateProps {
22
22
  fields: string[];
23
23
  views: View[];
24
24
  size?: Size;
25
+ headline?: string;
25
26
  }
26
27
 
27
- export const Aggregate: FunctionComponent<AggregateProps> = ({ fields, views, filter, size }) => {
28
+ export const Aggregate: FunctionComponent<AggregateProps> = ({
29
+ fields,
30
+ views,
31
+ filter,
32
+ size,
33
+ headline = 'Aggregate',
34
+ }) => {
28
35
  const lapis = useContext(LapisUrlContext);
29
36
 
30
37
  const { data, error, isLoading } = useQuery(async () => {
31
38
  return queryAggregateData(filter, fields, lapis);
32
39
  }, [filter, fields, lapis]);
33
40
 
34
- const headline = 'Aggregate';
35
-
36
41
  if (isLoading) {
37
42
  return (
38
43
  <Headline heading={headline}>
@@ -96,7 +101,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
96
101
  return (
97
102
  <div class='flex flex-row'>
98
103
  <CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
99
- <Info className='mx-1' content='Info for aggregate' />
104
+ <Info>Info for aggregate</Info>
100
105
  </div>
101
106
  );
102
107
  };
@@ -3,10 +3,13 @@ import { expect, within } from '@storybook/test';
3
3
 
4
4
  import Headline, { type HeadlineProps } from './headline';
5
5
 
6
- const meta: Meta<typeof Headline> = {
6
+ const meta: Meta<HeadlineProps> = {
7
7
  title: 'Component/Headline',
8
8
  component: Headline,
9
9
  parameters: { fetchMock: {} },
10
+ argTypes: {
11
+ heading: { control: 'text' },
12
+ },
10
13
  };
11
14
 
12
15
  export default meta;
@@ -27,3 +30,18 @@ export const HeadlineStory: StoryObj<HeadlineProps> = {
27
30
  await expect(canvas.getByText('Some Content')).toBeInTheDocument();
28
31
  },
29
32
  };
33
+
34
+ export const NoHeadlineStory: StoryObj<HeadlineProps> = {
35
+ render: (args) => (
36
+ <Headline {...args}>
37
+ <div class='flex justify-center px-4 py-16 bg-base-200'>Some Content</div>
38
+ </Headline>
39
+ ),
40
+ args: {},
41
+ play: async ({ canvasElement }) => {
42
+ const canvas = within(canvasElement);
43
+
44
+ await expect(canvas.queryByText('My Headline')).not.toBeInTheDocument();
45
+ await expect(canvas.getByText('Some Content')).toBeInTheDocument();
46
+ },
47
+ };
@@ -2,10 +2,18 @@ import { type FunctionComponent } from 'preact';
2
2
  import { useEffect, useRef, useState } from 'preact/hooks';
3
3
 
4
4
  export interface HeadlineProps {
5
- heading: string;
5
+ heading?: string;
6
6
  }
7
7
 
8
8
  const Headline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
9
+ if (!heading) {
10
+ return <>{children}</>;
11
+ }
12
+
13
+ return <ResizingHeadline heading={heading}>{children}</ResizingHeadline>;
14
+ };
15
+
16
+ const ResizingHeadline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
9
17
  const ref = useRef<HTMLHeadingElement>(null);
10
18
 
11
19
  const [h1Height, setH1Height] = useState('2rem');
@@ -1,4 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fireEvent, waitFor, within } from '@storybook/test';
2
3
 
3
4
  import Info, { type InfoProps } from './info';
4
5
 
@@ -7,16 +8,36 @@ const meta: Meta<InfoProps> = {
7
8
  component: Info,
8
9
  parameters: { fetchMock: {} },
9
10
  args: {
10
- content: 'This is a tooltip which shows some information.',
11
+ size: { width: '400px', height: '100px' },
11
12
  },
12
13
  };
13
14
 
14
15
  export default meta;
15
16
 
17
+ const tooltipText = 'This is a tooltip which shows some information.';
18
+
16
19
  export const InfoStory: StoryObj<InfoProps> = {
17
20
  render: (args) => (
18
- <div class='flex justify-center px-4 py-16 bg-base-200'>
19
- <Info {...args} />
21
+ <div class='flex justify-center px-4 py-16'>
22
+ <Info {...args}>{tooltipText}</Info>
20
23
  </div>
21
24
  ),
22
25
  };
26
+
27
+ export const ShowsInfoOnClick: StoryObj<InfoProps> = {
28
+ ...InfoStory,
29
+ play: async ({ canvasElement }) => {
30
+ const canvas = within(canvasElement);
31
+ const loading = canvas.getByRole('button', { name: '?' });
32
+
33
+ await waitFor(() => expect(loading).toBeInTheDocument());
34
+
35
+ await fireEvent.click(loading);
36
+
37
+ await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeInTheDocument());
38
+
39
+ await fireEvent.click(canvas.getByRole('button', { name: 'Close' }));
40
+
41
+ await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeInTheDocument());
42
+ },
43
+ };
@@ -1,16 +1,60 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useState } from 'preact/hooks';
2
3
 
3
4
  export interface InfoProps {
4
- content: string;
5
- className?: string;
5
+ size?: {
6
+ height?: string;
7
+ width?: string;
8
+ };
6
9
  }
7
10
 
8
- const Info: FunctionComponent<InfoProps> = ({ content, className }) => {
11
+ const Info: FunctionComponent<InfoProps> = ({ children, size }) => {
12
+ const [showHelp, setShowHelp] = useState(false);
13
+
14
+ const toggleHelp = () => {
15
+ setShowHelp(!showHelp);
16
+ };
17
+
9
18
  return (
10
- <div class={`${className} tooltip`} data-tip={content}>
11
- <button class='btn btn-xs'>?</button>
19
+ <div className='relative'>
20
+ <button className='btn btn-xs' onClick={toggleHelp}>
21
+ ?
22
+ </button>
23
+ {showHelp && (
24
+ <div
25
+ className='absolute top-8 right-6 bg-white p-2 border border-black flex flex-col overflow-auto shadow-lg rounded z-50'
26
+ style={size}
27
+ >
28
+ <div className='flex flex-col'>{children}</div>
29
+ <div className='flex justify-end'>
30
+ <button className='text-sm underline mt-2' onClick={toggleHelp}>
31
+ Close
32
+ </button>
33
+ </div>
34
+ </div>
35
+ )}
12
36
  </div>
13
37
  );
14
38
  };
15
39
 
40
+ export const InfoHeadline1: FunctionComponent = ({ children }) => {
41
+ return <h1 className='text-lg font-bold'>{children}</h1>;
42
+ };
43
+
44
+ export const InfoHeadline2: FunctionComponent = ({ children }) => {
45
+ return <h2 className='text-base font-bold mt-4'>{children}</h2>;
46
+ };
47
+
48
+ export const InfoParagraph: FunctionComponent = ({ children }) => {
49
+ return <p className='text-justify my-1'>{children}</p>;
50
+ };
51
+
52
+ export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
53
+ return (
54
+ <a className='text-blue-600 hover:text-blue-800' href={href} target='_blank' rel='noopener noreferrer'>
55
+ {children}
56
+ </a>
57
+ );
58
+ };
59
+
16
60
  export default Info;
@@ -39,8 +39,8 @@ export const DateRangeSelector = <CustomLabel extends string>({
39
39
  earliestDate = '1900-01-01',
40
40
  initialValue,
41
41
  }: DateRangeSelectorProps<CustomLabel>) => {
42
- const datePickerRef = useRef<HTMLInputElement>(null);
43
- const endDatePickerRef = useRef<HTMLInputElement>(null);
42
+ const fromDatePickerRef = useRef<HTMLInputElement>(null);
43
+ const toDatePickerRef = useRef<HTMLInputElement>(null);
44
44
  const divRef = useRef<HTMLDivElement>(null);
45
45
  const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
46
46
  const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
@@ -64,18 +64,18 @@ export const DateRangeSelector = <CustomLabel extends string>({
64
64
  dateFormat: 'Y-m-d',
65
65
  };
66
66
 
67
- if (datePickerRef.current) {
67
+ if (fromDatePickerRef.current) {
68
68
  setDateFromPicker(
69
- flatpickr(datePickerRef.current, {
69
+ flatpickr(fromDatePickerRef.current, {
70
70
  ...commonConfig,
71
71
  defaultDate: selectedDates.dateFrom,
72
72
  }),
73
73
  );
74
74
  }
75
75
 
76
- if (endDatePickerRef.current) {
76
+ if (toDatePickerRef.current) {
77
77
  setDateToPicker(
78
- flatpickr(endDatePickerRef.current, {
78
+ flatpickr(toDatePickerRef.current, {
79
79
  ...commonConfig,
80
80
  defaultDate: selectedDates.dateTo,
81
81
  }),
@@ -87,7 +87,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
87
87
  dateToPicker?.destroy();
88
88
  };
89
89
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
- }, [datePickerRef, endDatePickerRef]);
90
+ }, [fromDatePickerRef, toDatePickerRef]);
91
91
 
92
92
  const onSelectChange = (value: CustomLabel | PresetOptionValues) => {
93
93
  setSelectedDateRange(value);
@@ -167,7 +167,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
167
167
  class='input input-bordered rounded-none join-item'
168
168
  type='text'
169
169
  placeholder='Date from'
170
- ref={datePickerRef}
170
+ ref={fromDatePickerRef}
171
171
  onChange={onChangeDateFrom}
172
172
  onBlur={onChangeDateFrom}
173
173
  />
@@ -175,9 +175,9 @@ export const DateRangeSelector = <CustomLabel extends string>({
175
175
  class='input input-bordered rounded-none join-item'
176
176
  type='text'
177
177
  placeholder='Date to'
178
- ref={endDatePickerRef}
178
+ ref={toDatePickerRef}
179
179
  onChange={onChangeDateTo}
180
- onBlur={onChangeDateFrom}
180
+ onBlur={onChangeDateTo}
181
181
  />
182
182
  </div>
183
183
  );
@@ -28,6 +28,7 @@ const meta: Meta<MutationComparisonProps> = {
28
28
  control: { type: 'check' },
29
29
  },
30
30
  size: [{ control: 'object' }],
31
+ headline: { control: 'text' },
31
32
  },
32
33
  parameters: {
33
34
  fetchMock: {
@@ -81,6 +82,7 @@ const Template: StoryObj<MutationComparisonProps> = {
81
82
  sequenceType={args.sequenceType}
82
83
  views={args.views}
83
84
  size={args.size}
85
+ headline={args.headline}
84
86
  />
85
87
  </ReferenceGenomeContext.Provider>
86
88
  </LapisUrlContext.Provider>
@@ -108,6 +110,7 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
108
110
  sequenceType: 'nucleotide',
109
111
  views: ['table', 'venn'],
110
112
  size: { width: '100%', height: '700px' },
113
+ headline: 'Mutation comparison',
111
114
  },
112
115
  };
113
116
 
@@ -33,6 +33,7 @@ export interface MutationComparisonProps {
33
33
  sequenceType: SequenceType;
34
34
  views: View[];
35
35
  size?: Size;
36
+ headline?: string;
36
37
  }
37
38
 
38
39
  export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
@@ -40,6 +41,7 @@ export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
40
41
  sequenceType,
41
42
  views,
42
43
  size,
44
+ headline = 'Mutation comparison',
43
45
  }) => {
44
46
  const lapis = useContext(LapisUrlContext);
45
47
 
@@ -47,8 +49,6 @@ export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
47
49
  return queryMutationData(variants, sequenceType, lapis);
48
50
  }, [variants, sequenceType, lapis]);
49
51
 
50
- const headline = 'Mutation comparison';
51
-
52
52
  if (isLoading) {
53
53
  return (
54
54
  <Headline heading={headline}>
@@ -182,7 +182,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
182
182
  getData={() => getMutationComparisonTableData({ content: filteredData }, proportionInterval)}
183
183
  filename='mutation_comparison.csv'
184
184
  />
185
- <Info className='mx-1' content='Info for mutation comparison' />
185
+ <Info>Info for mutation comparison</Info>
186
186
  </div>
187
187
  );
188
188
  };
@@ -90,7 +90,7 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initial
90
90
  setSelectedFilters={setSelectedFilters}
91
91
  fireChangeEvent={fireChangeEvent}
92
92
  />
93
- <Info className='mx-1' content='Info for mutation filter' />
93
+ <Info>Info for mutation filter</Info>
94
94
  </div>
95
95
 
96
96
  <form className='mt-2 w-full' onSubmit={handleSubmit} ref={formRef}>