@finsweet/webflow-apps-utils 1.0.50 → 1.0.52

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.
@@ -17,6 +17,7 @@ export * from './section';
17
17
  export * from './select';
18
18
  export * from './shared';
19
19
  export * from './switch';
20
+ export * from './tags';
20
21
  export * from './text';
21
22
  export * from './tooltip';
22
23
  export { default as Loader } from './Loader.svelte';
@@ -17,6 +17,7 @@ export * from './section';
17
17
  export * from './select';
18
18
  export * from './shared';
19
19
  export * from './switch';
20
+ export * from './tags';
20
21
  export * from './text';
21
22
  export * from './tooltip';
22
23
  export { default as Loader } from './Loader.svelte';
@@ -108,3 +108,4 @@ export declare const InvalidState: Story;
108
108
  export declare const InvalidWithAlert: Story;
109
109
  export declare const ValidationStates: Story;
110
110
  export declare const FormValidationExample: Story;
111
+ export declare const WithFooter: Story;
@@ -1,5 +1,6 @@
1
1
  import { CheckIcon, UndoIcon } from '../../icons';
2
2
  import Select from './Select.svelte';
3
+ import SelectWithFooterStory from './SelectWithFooterStory.svelte';
3
4
  // Mock options for stories
4
5
  const basicOptions = [
5
6
  { label: 'Option 1', value: 'option1' },
@@ -567,3 +568,37 @@ export const FormValidationExample = {
567
568
  }
568
569
  }
569
570
  };
571
+ // Footer snippet example
572
+ const providerOptions = [
573
+ { label: 'Facebook', value: 'facebook' },
574
+ { label: 'Google', value: 'google' },
575
+ { label: 'Cloudflare', value: 'cloudflare' },
576
+ { label: 'Youtube', value: 'youtube' },
577
+ { label: 'Swiper', value: 'swiper' },
578
+ { label: 'GSAP', value: 'gsap' }
579
+ ];
580
+ export const WithFooter = {
581
+ render: () => ({
582
+ Component: SelectWithFooterStory,
583
+ props: {
584
+ options: providerOptions,
585
+ defaultText: 'Providers',
586
+ dropdownWidth: '250px',
587
+ dropdownHeight: '200px',
588
+ selected: 'facebook'
589
+ }
590
+ }),
591
+ args: {
592
+ options: providerOptions,
593
+ defaultText: 'Providers',
594
+ dropdownWidth: '250px',
595
+ dropdownHeight: '200px'
596
+ },
597
+ parameters: {
598
+ docs: {
599
+ description: {
600
+ story: 'Select with a sticky footer action. The footer stays visible while scrolling through options. Click the footer to trigger a custom action and close the dropdown.'
601
+ }
602
+ }
603
+ }
604
+ };
@@ -14,7 +14,12 @@
14
14
 
15
15
  import { Tooltip } from '..';
16
16
  import { Text } from '../text';
17
- import type { DropdownInstance, SelectInstanceManager, SelectProps } from './types.js';
17
+ import type {
18
+ DropdownInstance,
19
+ SelectFooterProps,
20
+ SelectInstanceManager,
21
+ SelectProps
22
+ } from './types.js';
18
23
 
19
24
  let {
20
25
  id = uuidv4(),
@@ -38,7 +43,8 @@
38
43
  invalid = false,
39
44
  className = '',
40
45
  onchange,
41
- children
46
+ children,
47
+ footer
42
48
  }: SelectProps = $props();
43
49
 
44
50
  // State variables
@@ -274,6 +280,17 @@
274
280
  lastHoveredItem = target;
275
281
  };
276
282
 
283
+ /**
284
+ * Clears the hover state when mouse leaves the items area.
285
+ */
286
+ const clearHoverState = (): void => {
287
+ if (lastHoveredItem) {
288
+ lastHoveredItem.classList.remove('hover-state');
289
+ lastHoveredItem.setAttribute('tabindex', '-1');
290
+ lastHoveredItem = null;
291
+ }
292
+ };
293
+
277
294
  type EventOption = [string, () => void];
278
295
 
279
296
  /**
@@ -403,14 +420,14 @@
403
420
  const getTooltipColor = (alertType: string) => {
404
421
  switch (alertType) {
405
422
  case 'error':
406
- return 'var(--redBackground, #ff4d4d)';
423
+ return 'var(--redBackground)';
407
424
  case 'warning':
408
- return 'var(--orangeBackground, #ff9933)';
425
+ return 'var(--orangeBackground)';
409
426
  case 'success':
410
- return 'var(--greenBackground, #00cc66)';
427
+ return 'var(--greenBackground)';
411
428
  case 'info':
412
429
  default:
413
- return 'var(--blueBackground, #4d9fff)';
430
+ return 'var(--actionPrimaryBackground)';
414
431
  }
415
432
  };
416
433
 
@@ -458,8 +475,9 @@
458
475
  <div
459
476
  tabindex={disabled || isOpen ? -1 : 0}
460
477
  class="dropdown-list"
478
+ class:has-footer={footer}
461
479
  role="listbox"
462
- style="width:{dropdownWidth}; max-height:{dropdownHeight};"
480
+ style="width:{dropdownWidth};"
463
481
  onkeydown={(e) => {
464
482
  e.stopPropagation();
465
483
  e.preventDefault();
@@ -467,85 +485,98 @@
467
485
  }}
468
486
  bind:this={dropdownItems}
469
487
  >
470
- {#if selectedLabel}
471
- <div class="selected">
472
- <div class="label">
473
- <Text label={selectedLabel} fontSize="normal" fontColor="var(--text1)" />
488
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
489
+ <div
490
+ class="dropdown-items-scroll"
491
+ style="max-height:{dropdownHeight};"
492
+ onmouseleave={clearHoverState}
493
+ >
494
+ {#if selectedLabel}
495
+ <div class="selected">
496
+ <div class="label">
497
+ <Text label={selectedLabel} fontSize="normal" fontColor="var(--text1)" />
498
+ </div>
474
499
  </div>
475
- </div>
476
- {/if}
477
-
478
- {#if enableSearch}
479
- <div class="search-container">
480
- <input
481
- type="text"
482
- placeholder="Search"
483
- oninput={(e) => {
500
+ {/if}
501
+
502
+ {#if enableSearch}
503
+ <div class="search-container">
504
+ <input
505
+ type="text"
506
+ placeholder="Search"
507
+ oninput={(e) => {
508
+ e.stopPropagation();
509
+ e.preventDefault();
510
+ handleSearch(e);
511
+ }}
512
+ onkeydown={(e) => e.stopPropagation()}
513
+ />
514
+ </div>
515
+ {/if}
516
+
517
+ {#each optionsStore?.length > 0 ? optionsStore : options as { label, value, className = null, description = null, labelIcon = null, descriptionTitle = null, isDisabled = false }, index (index)}
518
+ {@const indexId = index + 1}
519
+ {@const itemId = ref ? ref.replace(' ', '-') : 'dropdown'}
520
+ <button
521
+ aria-posinset={indexId}
522
+ aria-selected={value === selected && selected?.trim() !== '' ? 'true' : 'false'}
523
+ id={`${itemId}-list-${indexId}-${id}`}
524
+ data-value={value}
525
+ class="dropdown-item {isDisabled ? 'disabled' : ''} {className}"
526
+ role="option"
527
+ onclick={(e) => {
528
+ e.stopPropagation();
529
+ if (isDisabled) return;
530
+ handleSelect(value, label, e.currentTarget);
531
+ }}
532
+ onkeydown={(e) => {
484
533
  e.stopPropagation();
485
534
  e.preventDefault();
486
- handleSearch(e);
487
535
  }}
488
- onkeydown={(e) => e.stopPropagation()}
489
- />
536
+ onmouseenter={handleMouseEnter}
537
+ aria-hidden={!isOpen}
538
+ tabindex={value === selected ? 0 : -1}
539
+ style={description ? 'align-items:start;' : ''}
540
+ >
541
+ <div class="icon" aria-label={label}>
542
+ {#if value === selected && selected?.trim() !== ''}
543
+ <CheckIcon />
544
+ {/if}
545
+ </div>
546
+ <div class="label">
547
+ {#if description || descriptionTitle || labelIcon}
548
+ <div class="label-content">
549
+ <div class="label-name">
550
+ <Text {label} />
551
+ {#if labelIcon}
552
+ {@const IconComponent = labelIcon}
553
+ <IconComponent />
554
+ {/if}
555
+ </div>
556
+ <div class="label-description-title">
557
+ <Text
558
+ label={descriptionTitle || ''}
559
+ fontColor="var(--greenText)"
560
+ fontSize="10px"
561
+ />
562
+ </div>
563
+ <div class="label-description">
564
+ <Text label={description || ''} fontColor="var(--text2)" fontSize="10px" />
565
+ </div>
566
+ </div>
567
+ {:else}
568
+ <Text {label} fontSize="normal" />
569
+ {/if}
570
+ </div>
571
+ </button>
572
+ {/each}
573
+ </div>
574
+
575
+ {#if footer}
576
+ <div class="dropdown-footer">
577
+ {@render footer({ close: closeDropdown })}
490
578
  </div>
491
579
  {/if}
492
-
493
- {#each optionsStore?.length > 0 ? optionsStore : options as { label, value, className = null, description = null, labelIcon = null, descriptionTitle = null, isDisabled = false }, index (index)}
494
- {@const indexId = index + 1}
495
- {@const itemId = ref ? ref.replace(' ', '-') : 'dropdown'}
496
- <button
497
- aria-posinset={indexId}
498
- aria-selected={value === selected && selected?.trim() !== '' ? 'true' : 'false'}
499
- id={`${itemId}-list-${indexId}-${id}`}
500
- data-value={value}
501
- class="dropdown-item {isDisabled ? 'disabled' : ''} {className}"
502
- role="option"
503
- onclick={(e) => {
504
- e.stopPropagation();
505
- if (isDisabled) return;
506
- handleSelect(value, label, e.currentTarget);
507
- }}
508
- onkeydown={(e) => {
509
- e.stopPropagation();
510
- e.preventDefault();
511
- }}
512
- onmouseenter={handleMouseEnter}
513
- aria-hidden={!isOpen}
514
- tabindex={value === selected ? 0 : -1}
515
- style={description ? 'align-items:start;' : ''}
516
- >
517
- <div class="icon" aria-label={label}>
518
- {#if value === selected && selected?.trim() !== ''}
519
- <CheckIcon />
520
- {/if}
521
- </div>
522
- <div class="label">
523
- {#if description || descriptionTitle || labelIcon}
524
- <div class="label-content">
525
- <div class="label-name">
526
- <Text {label} />
527
- {#if labelIcon}
528
- {@const IconComponent = labelIcon}
529
- <IconComponent />
530
- {/if}
531
- </div>
532
- <div class="label-description-title">
533
- <Text
534
- label={descriptionTitle || ''}
535
- fontColor="var(--greenText)"
536
- fontSize="10px"
537
- />
538
- </div>
539
- <div class="label-description">
540
- <Text label={description || ''} fontColor="var(--text2)" fontSize="10px" />
541
- </div>
542
- </div>
543
- {:else}
544
- <Text {label} fontSize="normal" />
545
- {/if}
546
- </div>
547
- </button>
548
- {/each}
549
580
  </div>
550
581
  </div>
551
582
  </div>
@@ -675,14 +706,30 @@
675
706
  position: absolute;
676
707
  flex-direction: column;
677
708
  align-items: flex-start;
678
- gap: 4px;
709
+ gap: 0;
679
710
  border-radius: 4px;
680
711
  border: 1px solid var(--border1);
681
712
  background: var(--background3);
682
713
  box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.15);
714
+ z-index: 99999;
715
+ }
683
716
 
717
+ .dropdown-items-scroll {
718
+ display: flex;
719
+ flex-direction: column;
720
+ align-items: flex-start;
721
+ gap: 4px;
722
+ width: 100%;
684
723
  overflow-y: auto;
685
- z-index: 99999;
724
+ }
725
+
726
+ .dropdown-footer {
727
+ display: flex;
728
+ align-items: center;
729
+ width: 100%;
730
+ border-top: 1px solid var(--border1);
731
+ background: var(--background3);
732
+ flex-shrink: 0;
686
733
  }
687
734
  .dropdown-list .selected {
688
735
  display: flex;
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import Select from './Select.svelte';
3
+ import type { SelectOption } from './types.js';
4
+
5
+ interface Props {
6
+ options: SelectOption[];
7
+ defaultText?: string;
8
+ dropdownWidth?: string;
9
+ dropdownHeight?: string;
10
+ selected?: string | null;
11
+ }
12
+
13
+ let {
14
+ options,
15
+ defaultText = 'Select',
16
+ dropdownWidth = '200px',
17
+ dropdownHeight = '200px',
18
+ selected = $bindable(null)
19
+ }: Props = $props();
20
+
21
+ const handleFooterClick = (close: () => void) => {
22
+ console.log('Footer action clicked - adding a new provider manually');
23
+ close();
24
+ };
25
+ </script>
26
+
27
+ <Select {options} {defaultText} {dropdownWidth} {dropdownHeight} bind:selected>
28
+ {#snippet footer({ close })}
29
+ <button type="button" class="footer-action" onclick={() => handleFooterClick(close)}>
30
+ + Add manually a provider
31
+ </button>
32
+ {/snippet}
33
+ </Select>
34
+
35
+ <style>
36
+ .footer-action {
37
+ all: unset;
38
+ display: flex;
39
+ align-items: center;
40
+ padding: 8px;
41
+ width: 100%;
42
+ color: var(--blueText);
43
+ font-size: 11.5px;
44
+ font-weight: 400;
45
+ line-height: 16px;
46
+ letter-spacing: -0.115px;
47
+ cursor: pointer;
48
+ box-sizing: border-box;
49
+ }
50
+
51
+ .footer-action:hover {
52
+ background: var(--background5);
53
+ }
54
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { SelectOption } from './types.js';
2
+ interface Props {
3
+ options: SelectOption[];
4
+ defaultText?: string;
5
+ dropdownWidth?: string;
6
+ dropdownHeight?: string;
7
+ selected?: string | null;
8
+ }
9
+ declare const SelectWithFooterStory: import("svelte").Component<Props, {}, "selected">;
10
+ type SelectWithFooterStory = ReturnType<typeof SelectWithFooterStory>;
11
+ export default SelectWithFooterStory;
@@ -1,2 +1,2 @@
1
1
  export { default as Select } from './Select.svelte';
2
- export type { SelectOption, SelectProps, SelectChangeEvent, SelectChangeHandler, SelectState, DropdownInstance, DropdownConfig, SelectStyles, NavigationKey, KeyboardNavigationEvent, SearchConfig, FilterFunction, SelectElementRefs, SelectInstanceManager } from './types.js';
2
+ export type { SelectOption, SelectProps, SelectFooterProps, SelectChangeEvent, SelectChangeHandler, SelectState, DropdownInstance, DropdownConfig, SelectStyles, NavigationKey, KeyboardNavigationEvent, SearchConfig, FilterFunction, SelectElementRefs, SelectInstanceManager } from './types.js';
@@ -24,6 +24,9 @@ export interface SelectChangeEvent {
24
24
  value: string | null;
25
25
  }
26
26
  export type SelectChangeHandler = (event: SelectChangeEvent) => void;
27
+ export interface SelectFooterProps {
28
+ close: () => void;
29
+ }
27
30
  export interface SelectProps {
28
31
  id?: string;
29
32
  defaultText?: string;
@@ -53,6 +56,11 @@ export interface SelectProps {
53
56
  className?: string;
54
57
  onchange?: SelectChangeHandler;
55
58
  children?: Snippet;
59
+ /**
60
+ * Footer snippet for custom actions at the bottom of the dropdown.
61
+ * Receives { close } function to close the dropdown.
62
+ */
63
+ footer?: Snippet<[SelectFooterProps]>;
56
64
  }
57
65
  export interface SelectState {
58
66
  isOpen: boolean;
@@ -0,0 +1,103 @@
1
+ import type { StoryObj } from '@storybook/sveltekit';
2
+ declare const meta: {
3
+ title: string;
4
+ component: import("svelte").Component<import("./types.js").TagsInputProps, {}, "value">;
5
+ parameters: {
6
+ layout: string;
7
+ docs: {
8
+ description: {
9
+ component: string;
10
+ };
11
+ };
12
+ };
13
+ args: {
14
+ width: string;
15
+ height: string;
16
+ };
17
+ tags: string[];
18
+ argTypes: {
19
+ value: {
20
+ control: "object";
21
+ description: string;
22
+ };
23
+ placeholder: {
24
+ control: "text";
25
+ description: string;
26
+ };
27
+ disabled: {
28
+ control: "boolean";
29
+ description: string;
30
+ };
31
+ loading: {
32
+ control: "boolean";
33
+ description: string;
34
+ };
35
+ invalid: {
36
+ control: "boolean";
37
+ description: string;
38
+ };
39
+ readonly: {
40
+ control: "boolean";
41
+ description: string;
42
+ };
43
+ maxTags: {
44
+ control: "number";
45
+ description: string;
46
+ };
47
+ minTags: {
48
+ control: "number";
49
+ description: string;
50
+ };
51
+ maxTagLength: {
52
+ control: "number";
53
+ description: string;
54
+ };
55
+ allowDuplicates: {
56
+ control: "boolean";
57
+ description: string;
58
+ };
59
+ trimTags: {
60
+ control: "boolean";
61
+ description: string;
62
+ };
63
+ width: {
64
+ control: "text";
65
+ description: string;
66
+ };
67
+ height: {
68
+ control: "text";
69
+ description: string;
70
+ };
71
+ };
72
+ };
73
+ export default meta;
74
+ type Story = StoryObj<typeof meta>;
75
+ export declare const Default: Story;
76
+ export declare const WithInitialTags: Story;
77
+ export declare const WithPlaceholder: Story;
78
+ export declare const Disabled: Story;
79
+ export declare const Loading: Story;
80
+ export declare const Invalid: Story;
81
+ export declare const ReadOnly: Story;
82
+ export declare const MaxTags: Story;
83
+ export declare const MaxTagLength: Story;
84
+ export declare const NoDuplicates: Story;
85
+ export declare const AllowDuplicates: Story;
86
+ export declare const ErrorAlert: Story;
87
+ export declare const SuccessAlert: Story;
88
+ export declare const InfoAlert: Story;
89
+ export declare const WarningAlert: Story;
90
+ export declare const CustomWidth: Story;
91
+ export declare const CustomHeight: Story;
92
+ export declare const FullWidth: Story;
93
+ export declare const InteractiveExample: Story;
94
+ export declare const CustomValidation: Story;
95
+ export declare const EmailValidation: Story;
96
+ export declare const CategoryTags: Story;
97
+ export declare const SkillsTags: Story;
98
+ export declare const KeywordsTags: Story;
99
+ export declare const FormIntegration: Story;
100
+ export declare const ManyTags: Story;
101
+ export declare const LongTags: Story;
102
+ export declare const SpecialCharacters: Story;
103
+ export declare const UnicodeSupport: Story;