@reshape-biotech/design-system 2.7.35 → 2.7.36

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 (35) hide show
  1. package/dist/app.css +2 -2
  2. package/dist/components/button/Button.stories.svelte +27 -7
  3. package/dist/components/button/Button.svelte +107 -24
  4. package/dist/components/combobox/Combobox.stories.svelte +386 -115
  5. package/dist/components/combobox/components/combobox-add.svelte +10 -4
  6. package/dist/components/combobox/components/combobox-content.svelte +35 -60
  7. package/dist/components/combobox/components/combobox-indicator.svelte +1 -1
  8. package/dist/components/combobox/types.d.ts +1 -0
  9. package/dist/components/dropdown/Dropdown.stories.svelte +17 -17
  10. package/dist/components/dropdown/components/dropdown-content.svelte +6 -3
  11. package/dist/components/dropdown/components/dropdown-item.svelte +8 -1
  12. package/dist/components/dropdown/components/dropdown-separator.svelte +10 -0
  13. package/dist/components/dropdown/components/dropdown-separator.svelte.d.ts +5 -0
  14. package/dist/components/dropdown/components/dropdown-sub-content.svelte +4 -2
  15. package/dist/components/dropdown/components/dropdown-sub-trigger.svelte +10 -3
  16. package/dist/components/dropdown/index.d.ts +2 -2
  17. package/dist/components/dropdown/index.js +2 -2
  18. package/dist/components/dropdown/types.d.ts +1 -0
  19. package/dist/components/icon-button/IconButton.svelte +1 -1
  20. package/dist/components/icons/AnalysisIcon.svelte +7 -7
  21. package/dist/components/modal/Modal.stories.svelte +3 -0
  22. package/dist/components/modal/components/modal-title.svelte +7 -2
  23. package/dist/components/modal/types.d.ts +1 -0
  24. package/dist/components/select/Select.stories.svelte +89 -14
  25. package/dist/components/select/components/SelectContent.svelte +1 -1
  26. package/dist/components/select/components/SelectGroupHeading.svelte +1 -1
  27. package/dist/components/select/components/SelectItem.svelte +1 -1
  28. package/dist/components/select/components/SelectTrigger.svelte +13 -5
  29. package/dist/components/select/components/SelectTrigger.svelte.d.ts +1 -0
  30. package/dist/components/stat-card/StatCard.stories.svelte +113 -47
  31. package/dist/components/stat-card/StatCard.svelte +27 -6
  32. package/dist/components/stat-card/StatCard.svelte.d.ts +4 -0
  33. package/dist/components/status-badge/StatusBadge.svelte +4 -3
  34. package/dist/components/stepper/components/stepper-step.svelte +3 -3
  35. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import Plus from 'phosphor-svelte/lib/Plus';
3
3
  import List from 'phosphor-svelte/lib/List';
4
+ import MagnifyingGlass from 'phosphor-svelte/lib/MagnifyingGlass';
4
5
  import { Icon } from '../icons/index.js';
5
6
  import { defineMeta } from '@storybook/addon-svelte-csf';
6
7
  import { userEvent, within } from '@storybook/test';
@@ -10,6 +11,8 @@
10
11
  import Divider from '../divider/Divider.svelte';
11
12
  import Tag from '../tag/Tag.svelte';
12
13
  import Button from '../button/Button.svelte';
14
+ import Input from '../input/Input.svelte';
15
+ import Checkbox from '../checkbox/Checkbox.svelte';
13
16
 
14
17
  const { Story } = defineMeta({
15
18
  component: ComboboxRootForMeta,
@@ -60,11 +63,47 @@
60
63
  let searchValueSingle = $state('');
61
64
  let searchValueGrouped = $state('');
62
65
  let searchValueCustom = $state('');
66
+ let searchValueWithoutWidth = $state('');
67
+ let searchValueWithWidth = $state('');
63
68
 
64
69
  let selected = $state<string[]>([]);
65
70
  let selectedSingle = $state<string>('');
66
71
  let selectedGrouped = $state<string[]>([]);
67
72
  let selectedCustom = $state<string[]>([]);
73
+ let selectedWithoutWidth = $state<string>('');
74
+ let selectedWithWidth = $state<string>('');
75
+
76
+ // Organism pattern story state
77
+ let selectedOrganisms = $state<string[]>([]);
78
+ let organismSearchValue = $state('');
79
+ let organismAnchor = $state<HTMLElement>(null!);
80
+
81
+ const organismItems = [
82
+ { value: 'e-coli', label: 'E. coli', level: 'species' },
83
+ { value: 's-aureus', label: 'S. aureus', level: 'species' },
84
+ { value: 'b-subtilis', label: 'B. subtilis', level: 'species' },
85
+ { value: 'p-aeruginosa', label: 'P. aeruginosa', level: 'species' },
86
+ ];
87
+ const recentItems = [
88
+ { value: 'e-coli', label: 'E. coli', level: 'species' },
89
+ { value: 's-aureus', label: 'S. aureus', level: 'species' },
90
+ ];
91
+
92
+ const filteredOrganismItems = $derived(
93
+ organismSearchValue === ''
94
+ ? organismItems
95
+ : organismItems.filter((item) =>
96
+ item.label.toLowerCase().includes(organismSearchValue.toLowerCase())
97
+ )
98
+ );
99
+ const isSearchingOrganisms = $derived(organismSearchValue.trim() !== '');
100
+ const hasExactOrganismMatch = $derived(
101
+ isSearchingOrganisms &&
102
+ organismItems.some(
103
+ (o) => o.label.toLowerCase() === organismSearchValue.trim().toLowerCase()
104
+ )
105
+ );
106
+ const shouldShowCreateNewOrganism = $derived(isSearchingOrganisms && !hasExactOrganismMatch);
68
107
 
69
108
  const filteredFruits = $derived(
70
109
  searchValue === ''
@@ -96,6 +135,22 @@
96
135
  )
97
136
  );
98
137
 
138
+ const filteredFruitsWithoutWidth = $derived(
139
+ searchValueWithoutWidth === ''
140
+ ? fruits
141
+ : fruits.filter((fruit) =>
142
+ fruit.label.toLowerCase().includes(searchValueWithoutWidth.toLowerCase())
143
+ )
144
+ );
145
+
146
+ const filteredFruitsWithWidth = $derived(
147
+ searchValueWithWidth === ''
148
+ ? fruits
149
+ : fruits.filter((fruit) =>
150
+ fruit.label.toLowerCase().includes(searchValueWithWidth.toLowerCase())
151
+ )
152
+ );
153
+
99
154
  const exactMatch = $derived(
100
155
  filteredFruits.find((fruit) => fruit.value.toLowerCase() === searchValue.toLowerCase())
101
156
  );
@@ -111,6 +166,8 @@
111
166
  let customAnchorSingle = $state<HTMLElement>(null!);
112
167
  let customAnchorGrouped = $state<HTMLElement>(null!);
113
168
  let customAnchorCustom = $state<HTMLElement>(null!);
169
+ let customAnchorWithoutWidth = $state<HTMLElement>(null!);
170
+ let customAnchorWithWidth = $state<HTMLElement>(null!);
114
171
 
115
172
  // Generate a long list of countries for scrolling demo
116
173
  const countries = [
@@ -186,58 +243,58 @@
186
243
  </Tag>
187
244
  {/each}
188
245
  </div>
189
- <Combobox.Trigger>
190
- <div bind:this={customAnchor}>
191
- <IconButton rounded={false}>
246
+ <div class="flex items-center justify-center">
247
+ <Combobox.Trigger>
248
+ <div bind:this={customAnchor}>
249
+ <IconButton rounded={false}>
192
250
  <Icon>
193
251
  {#snippet children(props)}
194
252
  <Plus {...props} />
195
253
  {/snippet}
196
254
  </Icon>
197
- </IconButton>
198
- </div>
199
- </Combobox.Trigger>
255
+ </IconButton>
256
+ </div>
257
+ </Combobox.Trigger>
258
+ </div>
200
259
  <Combobox.Content {customAnchor} class="flex flex-col justify-between">
201
- <div>
260
+ {#snippet header()}
202
261
  <Combobox.Input
203
262
  placeholder="Search a fruit"
204
263
  oninput={(e: Event) => (searchValue = (e.target as HTMLInputElement).value)}
205
264
  autofocus
206
265
  />
207
266
  <Divider />
208
- </div>
209
- <div class="flex flex-grow flex-col">
210
- {#if filteredFruits.length > 0}
211
- <Combobox.Group>
212
- <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
213
- {#each filteredFruits as fruit (fruit.value)}
214
- <Combobox.Item value={fruit.value} label={fruit.label}>
215
- {#snippet children({ selected })}
216
- {fruit.label}
217
- {#if selected}
218
- <Combobox.Indicator />
219
- {/if}
220
- {/snippet}
221
- </Combobox.Item>
222
- {:else}
223
- <span class="block px-5 py-2 text-sm text-muted-foreground"> No results found </span>
224
- {/each}
225
- </Combobox.Group>
226
- {/if}
227
- </div>
228
- {#if !exactMatch && searchValue !== ''}
229
- <Divider />
230
-
231
- <Combobox.Add
232
- onclick={() => {
233
- selected.push(searchValue);
234
- fruits.push({ value: searchValue, label: searchValue });
235
- searchValue = '';
236
- }}
237
- >
238
- <p>Add new fruit</p>
239
- </Combobox.Add>
267
+ {/snippet}
268
+ {#if filteredFruits.length > 0}
269
+ <Combobox.Group>
270
+ <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
271
+ {#each filteredFruits as fruit (fruit.value)}
272
+ <Combobox.Item value={fruit.value} label={fruit.label}>
273
+ {#snippet children({ selected })}
274
+ {fruit.label}
275
+ {#if selected}
276
+ <Combobox.Indicator />
277
+ {/if}
278
+ {/snippet}
279
+ </Combobox.Item>
280
+ {:else}
281
+ <span class="block px-5 py-2 text-sm text-secondary text-center"> No results found </span>
282
+ {/each}
283
+ </Combobox.Group>
240
284
  {/if}
285
+ {#snippet footer()}
286
+ {#if !exactMatch && searchValue !== ''}
287
+ <Combobox.Add
288
+ onclick={() => {
289
+ selected.push(searchValue);
290
+ fruits.push({ value: searchValue, label: searchValue });
291
+ searchValue = '';
292
+ }}
293
+ >
294
+ <p>Add new fruit</p>
295
+ </Combobox.Add>
296
+ {/if}
297
+ {/snippet}
241
298
  </Combobox.Content>
242
299
  </Combobox.Root>
243
300
  </Story>
@@ -252,13 +309,15 @@
252
309
  items={filteredFruitsSingle}
253
310
  bind:value={selectedSingle}
254
311
  >
255
- <Combobox.Trigger>
256
- <div bind:this={customAnchorSingle}>
257
- <Button variant="secondary" size="sm">
312
+ <div class="flex items-center justify-center">
313
+ <Combobox.Trigger>
314
+ <div bind:this={customAnchorSingle}>
315
+ <Button variant="secondary" size="sm">
258
316
  {selectedSingle ? selectedSingle : 'Select a fruit'}
259
317
  </Button>
260
- </div>
261
- </Combobox.Trigger>
318
+ </div>
319
+ </Combobox.Trigger>
320
+ </div>
262
321
  <Combobox.Content class="flex flex-col justify-between" customAnchor={customAnchorSingle}>
263
322
  <div class="flex flex-grow flex-col">
264
323
  {#if filteredFruitsSingle.length > 0}
@@ -274,7 +333,7 @@
274
333
  {/snippet}
275
334
  </Combobox.Item>
276
335
  {:else}
277
- <span class="block px-5 py-2 text-sm text-muted-foreground"> No results found </span>
336
+ <span class="block px-5 py-2 text-sm text-secondary text-center"> No results found </span>
278
337
  {/each}
279
338
  </Combobox.Group>
280
339
  {/if}
@@ -300,9 +359,10 @@
300
359
  </Tag>
301
360
  {/each}
302
361
  </div>
303
- <Combobox.Trigger>
304
- <div bind:this={customAnchorGrouped}>
305
- <Button variant="primary" size="sm">
362
+ <div class="flex items-center justify-center">
363
+ <Combobox.Trigger>
364
+ <div bind:this={customAnchorGrouped}>
365
+ <Button variant="primary" size="sm">
306
366
  <Icon>
307
367
  {#snippet children(props)}
308
368
  <List {...props} />
@@ -310,18 +370,18 @@
310
370
  </Icon>
311
371
  Select fruits by category
312
372
  </Button>
313
- </div>
314
- </Combobox.Trigger>
315
- <Combobox.Content class="flex flex-col justify-between" customAnchor={customAnchorGrouped}>
316
- <div>
373
+ </div>
374
+ </Combobox.Trigger>
375
+ </div>
376
+ <Combobox.Content class="flex flex-col justify-between" customAnchor={customAnchorGrouped} matchTriggerWidth>
377
+ {#snippet header()}
317
378
  <Combobox.Input
318
379
  placeholder="Search across categories"
319
380
  oninput={(e: Event) => (searchValueGrouped = (e.target as HTMLInputElement).value)}
320
381
  autofocus
321
382
  />
322
383
  <Divider />
323
- </div>
324
- <div class="flex flex-grow flex-col">
384
+ {/snippet}
325
385
  {#if searchValueGrouped === ''}
326
386
  {#each categories as category}
327
387
  <Combobox.Group>
@@ -358,7 +418,6 @@
358
418
  {:else}
359
419
  <span class="text-muted-foreground block px-5 py-2 text-sm"> No results found </span>
360
420
  {/if}
361
- </div>
362
421
  </Combobox.Content>
363
422
  </Combobox.Root>
364
423
  </Story>
@@ -380,9 +439,10 @@
380
439
  </Tag>
381
440
  {/each}
382
441
  </div>
383
- <Combobox.Trigger>
384
- <div bind:this={customAnchorCountries}>
385
- <Button variant="primary" size="sm">
442
+ <div class="flex items-center justify-center">
443
+ <Combobox.Trigger>
444
+ <div bind:this={customAnchorCountries}>
445
+ <Button variant="primary" size="sm">
386
446
  <Icon>
387
447
  {#snippet children(props)}
388
448
  <List {...props} />
@@ -390,39 +450,38 @@
390
450
  </Icon>
391
451
  Select countries
392
452
  </Button>
393
- </div>
394
- </Combobox.Trigger>
453
+ </div>
454
+ </Combobox.Trigger>
455
+ </div>
395
456
  <Combobox.Content
396
457
  class="flex flex-col justify-between"
397
458
  customAnchor={customAnchorCountries}
398
459
  >
399
- <div>
460
+ {#snippet header()}
400
461
  <Combobox.Input
401
462
  placeholder="Search for a country"
402
463
  oninput={(e: Event) => (searchValueCountries = (e.target as HTMLInputElement).value)}
403
464
  autofocus
404
465
  />
405
466
  <Divider />
406
- </div>
407
- <div class="flex flex-grow flex-col">
408
- {#if filteredCountries.length > 0}
409
- <Combobox.Group>
410
- <Combobox.GroupHeading>Countries</Combobox.GroupHeading>
411
- {#each filteredCountries as country (country.value)}
412
- <Combobox.Item value={country.value} label={country.label}>
413
- {#snippet children({ selected })}
414
- {country.label}
415
- {#if selected}
416
- <Combobox.Indicator />
417
- {/if}
418
- {/snippet}
419
- </Combobox.Item>
420
- {/each}
421
- </Combobox.Group>
422
- {:else}
423
- <span class="block px-5 py-2 text-sm text-muted-foreground"> No results found </span>
424
- {/if}
425
- </div>
467
+ {/snippet}
468
+ {#if filteredCountries.length > 0}
469
+ <Combobox.Group>
470
+ <Combobox.GroupHeading>Countries</Combobox.GroupHeading>
471
+ {#each filteredCountries as country (country.value)}
472
+ <Combobox.Item value={country.value} label={country.label}>
473
+ {#snippet children({ selected })}
474
+ {country.label}
475
+ {#if selected}
476
+ <Combobox.Indicator />
477
+ {/if}
478
+ {/snippet}
479
+ </Combobox.Item>
480
+ {/each}
481
+ </Combobox.Group>
482
+ {:else}
483
+ <span class="block px-5 py-2 text-sm text-secondary text-center"> No results found </span>
484
+ {/if}
426
485
  </Combobox.Content>
427
486
  <div class="mt-4 rounded bg-blue-50 p-2 text-xs text-blue-700">
428
487
  This story demonstrates the 80vh max-height with overflow scrolling for long lists (40 countries).
@@ -509,23 +568,25 @@
509
568
  </Tag>
510
569
  {/each}
511
570
  </div>
512
- <Combobox.Trigger data-testid="combobox-trigger">
513
- <div bind:this={customAnchor}>
514
- <IconButton rounded={false}>
571
+ <div class="flex items-center justify-center">
572
+ <Combobox.Trigger data-testid="combobox-trigger">
573
+ <div bind:this={customAnchor}>
574
+ <IconButton rounded={false}>
515
575
  <Icon>
516
576
  {#snippet children(props)}
517
577
  <Plus {...props} />
518
578
  {/snippet}
519
579
  </Icon>
520
580
  </IconButton>
521
- </div>
522
- </Combobox.Trigger>
581
+ </div>
582
+ </Combobox.Trigger>
583
+ </div>
523
584
  <Combobox.Content
524
585
  {customAnchor}
525
586
  class="flex flex-col justify-between"
526
587
  data-testid="combobox-content"
527
588
  >
528
- <div>
589
+ {#snippet header()}
529
590
  <Combobox.Input
530
591
  placeholder="Search a fruit"
531
592
  oninput={(e: Event) => (searchValue = (e.target as HTMLInputElement).value)}
@@ -533,17 +594,85 @@
533
594
  data-testid="combobox-input"
534
595
  />
535
596
  <Divider />
597
+ {/snippet}
598
+ {#if filteredFruits.length > 0}
599
+ <Combobox.Group>
600
+ <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
601
+ {#each filteredFruits as fruit (fruit.value)}
602
+ <Combobox.Item
603
+ value={fruit.value}
604
+ label={fruit.label}
605
+ data-testid={`fruit-option-${fruit.value}`}
606
+ >
607
+ {#snippet children({ selected })}
608
+ {fruit.label}
609
+ {#if selected}
610
+ <Combobox.Indicator />
611
+ {/if}
612
+ {/snippet}
613
+ </Combobox.Item>
614
+ {:else}
615
+ <span class="block px-5 py-2 text-sm text-secondary text-center">
616
+ No results found
617
+ </span>
618
+ {/each}
619
+ </Combobox.Group>
620
+ {/if}
621
+ {#snippet footer()}
622
+ {#if !exactMatch && searchValue !== ''}
623
+ <Combobox.Add
624
+ data-testid="add-new-fruit"
625
+ onclick={() => {
626
+ selected.push(searchValue);
627
+ fruits.push({ value: searchValue, label: searchValue });
628
+ searchValue = '';
629
+ }}
630
+ >
631
+ <p>Add new fruit</p>
632
+ </Combobox.Add>
633
+ {/if}
634
+ {/snippet}
635
+ </Combobox.Content>
636
+ </Combobox.Root>
637
+ </div>
638
+ </Story>
639
+
640
+ <Story name="Width Comparison" asChild>
641
+ <div class="flex gap-8 p-8">
642
+ <div class="flex flex-1 flex-col gap-4">
643
+ <p class="font-medium text-center text-secondary">matchTriggerWidth: false</p>
644
+ <Combobox.Root
645
+ onOpenChange={(o) => {
646
+ if (!o) searchValueWithoutWidth = '';
647
+ }}
648
+ type="single"
649
+ name="withoutWidth"
650
+ items={filteredFruitsWithoutWidth}
651
+ bind:value={selectedWithoutWidth}
652
+ >
653
+ <div class="flex w-full items-center justify-center">
654
+ <Combobox.Trigger class="w-full">
655
+ <div bind:this={customAnchorWithoutWidth} class="w-full">
656
+ <Button variant="secondary" size="sm" class="w-full">
657
+ Select a fruit
658
+ </Button>
659
+ </div>
660
+ </Combobox.Trigger>
536
661
  </div>
537
- <div class="flex flex-grow flex-col">
538
- {#if filteredFruits.length > 0}
662
+ <Combobox.Content customAnchor={customAnchorWithoutWidth}>
663
+ {#snippet header()}
664
+ <Combobox.Input
665
+ placeholder="Search a fruit"
666
+ oninput={(e: Event) => (searchValueWithoutWidth = (e.target as HTMLInputElement).value)}
667
+ autofocus
668
+ />
669
+ <Divider />
670
+ {/snippet}
671
+ {#if filteredFruitsWithoutWidth.length > 0}
539
672
  <Combobox.Group>
540
673
  <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
541
- {#each filteredFruits as fruit (fruit.value)}
542
- <Combobox.Item
543
- value={fruit.value}
544
- label={fruit.label}
545
- data-testid={`fruit-option-${fruit.value}`}
546
- >
674
+ {#each filteredFruitsWithoutWidth as fruit (fruit.value)}
675
+ <Combobox.Item value={fruit.value} label={fruit.label}>
547
676
  {#snippet children({ selected })}
548
677
  {fruit.label}
549
678
  {#if selected}
@@ -551,29 +680,171 @@
551
680
  {/if}
552
681
  {/snippet}
553
682
  </Combobox.Item>
554
- {:else}
555
- <span class="block px-5 py-2 text-sm text-muted-foreground">
556
- No results found
557
- </span>
558
683
  {/each}
559
684
  </Combobox.Group>
560
685
  {/if}
561
- </div>
562
- {#if !exactMatch && searchValue !== ''}
563
- <Divider />
686
+ </Combobox.Content>
687
+ </Combobox.Root>
688
+ </div>
564
689
 
565
- <Combobox.Add
566
- data-testid="add-new-fruit"
567
- onclick={() => {
568
- selected.push(searchValue);
569
- fruits.push({ value: searchValue, label: searchValue });
570
- searchValue = '';
571
- }}
572
- >
573
- <p>Add new fruit</p>
574
- </Combobox.Add>
575
- {/if}
576
- </Combobox.Content>
577
- </Combobox.Root>
690
+ <div class="flex flex-1 flex-col gap-4">
691
+ <p class="font-medium text-center text-secondary">matchTriggerWidth: true</p>
692
+ <Combobox.Root
693
+ onOpenChange={(o) => {
694
+ if (!o) searchValueWithWidth = '';
695
+ }}
696
+ type="single"
697
+ name="withWidth"
698
+ items={filteredFruitsWithWidth}
699
+ bind:value={selectedWithWidth}
700
+ >
701
+ <div class="flex w-full items-center justify-center">
702
+ <Combobox.Trigger class="w-full">
703
+ <div bind:this={customAnchorWithWidth} class="w-full">
704
+ <Button variant="secondary" size="sm" class="w-full">
705
+ Select a fruit
706
+ </Button>
707
+ </div>
708
+ </Combobox.Trigger>
709
+ </div>
710
+ <Combobox.Content matchTriggerWidth={true} customAnchor={customAnchorWithWidth}>
711
+ {#snippet header()}
712
+ <Combobox.Input
713
+ placeholder="Search a fruit"
714
+ oninput={(e: Event) => (searchValueWithWidth = (e.target as HTMLInputElement).value)}
715
+ autofocus
716
+ />
717
+ <Divider />
718
+ {/snippet}
719
+ {#if filteredFruitsWithWidth.length > 0}
720
+ <Combobox.Group>
721
+ <Combobox.GroupHeading>Fruits</Combobox.GroupHeading>
722
+ {#each filteredFruitsWithWidth as fruit (fruit.value)}
723
+ <Combobox.Item value={fruit.value} label={fruit.label}>
724
+ {#snippet children({ selected })}
725
+ {fruit.label}
726
+ {#if selected}
727
+ <Combobox.Indicator />
728
+ {/if}
729
+ {/snippet}
730
+ </Combobox.Item>
731
+ {/each}
732
+ </Combobox.Group>
733
+ {/if}
734
+ </Combobox.Content>
735
+ </Combobox.Root>
736
+ </div>
578
737
  </div>
579
738
  </Story>
739
+
740
+ <Story name="Organism Pattern (Simplified)" asChild>
741
+
742
+ <div class="flex items-center justify-center p-8">
743
+ <div class="w-full max-w-md">
744
+
745
+ <Combobox.Root
746
+ type="multiple"
747
+ value={selectedOrganisms}
748
+ name="organism"
749
+ onOpenChange={(open) => {
750
+ if (!open) {
751
+ organismSearchValue = '';
752
+ }
753
+ }}
754
+ onValueChange={(value) => {
755
+ selectedOrganisms = value;
756
+ }}
757
+ allowDeselect
758
+ >
759
+ <Combobox.Trigger
760
+ class="w-full rounded-lg border border-dashed border-input bg-transparent text-secondary hover:border-interactive hover:bg-neutral hover:text-primary"
761
+ >
762
+ <div bind:this={organismAnchor} class="flex w-full flex-row items-center justify-center gap-2 py-2">
763
+ <Icon icon={Plus} color="secondary" />
764
+ <span>Add organism</span>
765
+ </div>
766
+ </Combobox.Trigger>
767
+ <Combobox.Content
768
+ customAnchor={organismAnchor}
769
+ class="flex max-h-[400px] min-w-[var(--bits-combobox-anchor-width)] w-[var(--bits-combobox-anchor-width)] flex-col justify-between"
770
+ portalled={false}
771
+ align="start"
772
+ matchTriggerWidth={true}
773
+ >
774
+ {#snippet header()}
775
+ <Combobox.Input>
776
+ {#snippet child()}
777
+ <Input
778
+ autofocus
779
+ size="md"
780
+ variant="borderless"
781
+ clearable
782
+ placeholder="Search for organism..."
783
+ bind:value={organismSearchValue}
784
+ >
785
+ {#snippet prefix()}
786
+ <Icon icon={MagnifyingGlass} class="flex pl-2"/>
787
+ {/snippet}
788
+ </Input>
789
+ {/snippet}
790
+ </Combobox.Input>
791
+ <Divider />
792
+ {/snippet}
793
+
794
+ {#if isSearchingOrganisms}
795
+ {#if filteredOrganismItems.length > 0}
796
+ <Combobox.Group>
797
+ {#each filteredOrganismItems as organism}
798
+ <Combobox.Item value={organism.value} label={organism.label}>
799
+ {#snippet children({ selected })}
800
+ <div class="flex w-full flex-row items-center justify-between">
801
+ <span class="flex flex-row items-center gap-3">
802
+ <Checkbox checked={selected} />
803
+ <span>{organism.label}</span>
804
+ </span>
805
+ <Tag size="xs" variant="default">{organism.level}</Tag>
806
+ </div>
807
+ {/snippet}
808
+ </Combobox.Item>
809
+ {/each}
810
+ </Combobox.Group>
811
+ {/if}
812
+ {:else}
813
+ <Combobox.Group>
814
+ <Combobox.GroupHeading>Recently used</Combobox.GroupHeading>
815
+ {#each recentItems as organism}
816
+ <Combobox.Item value={organism.value} label={organism.label}>
817
+ {#snippet children({ selected })}
818
+ <div class="flex w-full flex-row items-center justify-between gap-2">
819
+ <span class="flex flex-row items-center gap-3">
820
+ <Checkbox checked={selected} />
821
+ {organism.label}
822
+ </span>
823
+ <Tag size="xs">{organism.level}</Tag>
824
+ </div>
825
+ {/snippet}
826
+ </Combobox.Item>
827
+ {/each}
828
+ </Combobox.Group>
829
+ {/if}
830
+
831
+ {#snippet footer()}
832
+ {#if shouldShowCreateNewOrganism}
833
+ <Combobox.Add>
834
+ <p>Create new organism: <span class="font-medium">"{organismSearchValue}"</span></p>
835
+ </Combobox.Add>
836
+ {/if}
837
+ {/snippet}
838
+ </Combobox.Content>
839
+ </Combobox.Root>
840
+
841
+ {#if selectedOrganisms.length > 0}
842
+ <div class="mt-4 flex flex-wrap gap-2">
843
+ {#each selectedOrganisms as value}
844
+ <Tag>{organismItems.find((o) => o.value === value)?.label || value}</Tag>
845
+ {/each}
846
+ </div>
847
+ {/if}
848
+ </div>
849
+ </div>
850
+ </Story>