@finsweet/webflow-apps-utils 1.0.51 → 1.0.53

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.
@@ -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;
@@ -60,6 +60,14 @@ declare const meta: {
60
60
  control: "boolean";
61
61
  description: string;
62
62
  };
63
+ showRemoveIcon: {
64
+ control: "boolean";
65
+ description: string;
66
+ };
67
+ expandOnClick: {
68
+ control: "boolean";
69
+ description: string;
70
+ };
63
71
  width: {
64
72
  control: "text";
65
73
  description: string;
@@ -99,5 +107,8 @@ export declare const KeywordsTags: Story;
99
107
  export declare const FormIntegration: Story;
100
108
  export declare const ManyTags: Story;
101
109
  export declare const LongTags: Story;
110
+ export declare const ShowRemoveIcon: Story;
111
+ export declare const ExpandOnClick: Story;
112
+ export declare const ShowRemoveIconAndExpandOnClick: Story;
102
113
  export declare const SpecialCharacters: Story;
103
114
  export declare const UnicodeSupport: Story;
@@ -60,6 +60,14 @@ const meta = {
60
60
  control: 'boolean',
61
61
  description: 'Whether to trim whitespace from tags'
62
62
  },
63
+ showRemoveIcon: {
64
+ control: 'boolean',
65
+ description: 'Whether to always show the remove icon on tags'
66
+ },
67
+ expandOnClick: {
68
+ control: 'boolean',
69
+ description: 'Whether clicking a tag expands it to show full content'
70
+ },
63
71
  width: {
64
72
  control: 'text',
65
73
  description: 'Custom width for the component'
@@ -407,6 +415,53 @@ export const LongTags = {
407
415
  }
408
416
  }
409
417
  };
418
+ export const ShowRemoveIcon = {
419
+ args: {
420
+ value: ['JavaScript', 'TypeScript', 'Svelte'],
421
+ showRemoveIcon: true,
422
+ placeholder: 'Remove icon always visible...'
423
+ },
424
+ parameters: {
425
+ docs: {
426
+ description: {
427
+ story: 'When showRemoveIcon is true, the X button is always visible inline with 4px gap.'
428
+ }
429
+ }
430
+ }
431
+ };
432
+ export const ExpandOnClick = {
433
+ args: {
434
+ value: [
435
+ 'This is a very long tag that will be truncated',
436
+ 'Another long tag content here',
437
+ 'Short'
438
+ ],
439
+ expandOnClick: true,
440
+ placeholder: 'Click tags to expand...'
441
+ },
442
+ parameters: {
443
+ docs: {
444
+ description: {
445
+ story: 'When expandOnClick is true, clicking a tag expands it to show full content.'
446
+ }
447
+ }
448
+ }
449
+ };
450
+ export const ShowRemoveIconAndExpandOnClick = {
451
+ args: {
452
+ value: ['This is a very long tag name', 'TypeScript', 'Click me to expand'],
453
+ showRemoveIcon: true,
454
+ expandOnClick: true,
455
+ placeholder: 'Both features enabled...'
456
+ },
457
+ parameters: {
458
+ docs: {
459
+ description: {
460
+ story: 'Both showRemoveIcon and expandOnClick enabled together.'
461
+ }
462
+ }
463
+ }
464
+ };
410
465
  export const SpecialCharacters = {
411
466
  args: {
412
467
  value: ['C++', 'C#', '.NET', '@angular', '#svelte'],
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+
2
4
  import { TimesIcon } from '../../icons';
3
5
 
4
6
  import Loader from '../Loader.svelte';
@@ -21,6 +23,8 @@
21
23
  allowDuplicates = false,
22
24
  validateTag,
23
25
  trimTags = true,
26
+ showRemoveIcon = false,
27
+ expandOnClick = false,
24
28
  width = '100%',
25
29
  height = 'auto',
26
30
  class: className = '',
@@ -39,6 +43,7 @@
39
43
  let inputElement: HTMLInputElement | undefined = $state();
40
44
  let inputValue = $state('');
41
45
  let isFocused = $state(false);
46
+ let expandedTags = new SvelteSet<number>();
42
47
 
43
48
  // Derived states
44
49
  let isDisabled = $derived(disabled || loading);
@@ -52,20 +57,55 @@
52
57
  return value.length < minTags;
53
58
  });
54
59
 
60
+ // Derived alert state for styling
61
+ let alertType = $derived(alert?.type || null);
62
+ let isErrorAlert = $derived(alertType === 'error' || alertType === 'warning');
63
+ let isSuccessAlert = $derived(alertType === 'success');
64
+
55
65
  // CSS classes
56
66
  let wrapperClasses = $derived(
57
67
  `
58
68
  tags-input-wrapper
59
69
  ${isDisabled ? 'disabled' : ''}
60
- ${invalid || hasAlert || isMinTagsInvalid ? 'invalid' : ''}
70
+ ${readonly ? 'readonly' : ''}
71
+ ${invalid || isErrorAlert || isMinTagsInvalid ? 'invalid' : ''}
72
+ ${isSuccessAlert ? 'success' : ''}
61
73
  ${isFocused ? 'focused' : ''}
62
74
  ${loading ? 'loading' : ''}
75
+ ${showRemoveIcon ? 'show-remove-icon' : ''}
63
76
  ${className}
64
77
  `
65
78
  .trim()
66
79
  .replace(/\s+/g, ' ')
67
80
  );
68
81
 
82
+ /**
83
+ * Collapses all expanded tags
84
+ */
85
+ const collapseAllTags = () => {
86
+ expandedTags.clear();
87
+ };
88
+
89
+ /**
90
+ * Toggles the expanded state of a tag
91
+ */
92
+ const toggleTagExpand = (index: number) => {
93
+ if (!expandOnClick) return;
94
+
95
+ if (expandedTags.has(index)) {
96
+ expandedTags.delete(index);
97
+ } else {
98
+ expandedTags.add(index);
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Checks if a tag is expanded
104
+ */
105
+ const isTagExpanded = (index: number): boolean => {
106
+ return expandedTags.has(index);
107
+ };
108
+
69
109
  /**
70
110
  * Focus the input element
71
111
  */
@@ -225,9 +265,13 @@
225
265
  };
226
266
 
227
267
  /**
228
- * Handles wrapper click to focus input
268
+ * Handles wrapper click to focus input and collapse expanded tags
229
269
  */
230
270
  const handleWrapperClick = () => {
271
+ // Collapse all expanded tags when clicking on the wrapper area
272
+ if (expandOnClick && expandedTags.size > 0) {
273
+ collapseAllTags();
274
+ }
231
275
  focusInput();
232
276
  };
233
277
 
@@ -237,14 +281,14 @@
237
281
  const getTooltipColor = (alertType: string) => {
238
282
  switch (alertType) {
239
283
  case 'error':
240
- return 'var(--redBackground, #cf313b)';
284
+ return 'var(--redBackground)';
241
285
  case 'warning':
242
- return 'var(--orangeBackground, #bf4704)';
286
+ return 'var(--orangeBackground)';
243
287
  case 'success':
244
- return 'var(--greenBackground, #007a41)';
288
+ return 'var(--greenBackground)';
245
289
  case 'info':
246
290
  default:
247
- return 'var(--actionPrimaryBackground, #006acc)';
291
+ return 'var(--actionPrimaryBackground)';
248
292
  }
249
293
  };
250
294
  </script>
@@ -260,8 +304,25 @@
260
304
  onkeydown={(e) => e.key === 'Enter' && handleWrapperClick()}
261
305
  >
262
306
  <div class="tags-input-content">
263
- {#each value as tag, index (tag)}
264
- <span class="tag" role="listitem">
307
+ {#each value as tag, index (`${index}-${tag}`)}
308
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
309
+ <span
310
+ class="tag {expandOnClick ? 'expandable' : ''} {isTagExpanded(index) ? 'expanded' : ''}"
311
+ role="listitem"
312
+ onclick={(e) => {
313
+ if (expandOnClick) {
314
+ e.stopPropagation();
315
+ toggleTagExpand(index);
316
+ }
317
+ }}
318
+ onkeydown={(e) => {
319
+ if (expandOnClick && e.key === 'Enter') {
320
+ e.stopPropagation();
321
+ toggleTagExpand(index);
322
+ }
323
+ }}
324
+ tabindex={expandOnClick ? 0 : undefined}
325
+ >
265
326
  <span class="tag-text">{tag}</span>
266
327
  {#if !readonly && !isDisabled}
267
328
  <button
@@ -337,14 +398,14 @@
337
398
 
338
399
  .tags-input-wrapper {
339
400
  position: relative;
340
- border: 1px solid var(--border3, rgba(255, 255, 255, 0.19));
341
- border-radius: var(--border-radius, 4px);
401
+ border: 1px solid var(--border3);
402
+ border-radius: var(--border-radius);
342
403
  padding: 4px;
343
404
  display: flex;
344
405
  flex-wrap: wrap;
345
406
  align-items: flex-start;
346
407
  align-content: flex-start;
347
- background: var(--background1, #1e1e1e);
408
+ background: var(--background1);
348
409
  min-height: 32px;
349
410
  box-shadow:
350
411
  0px 16px 16px -16px rgba(0, 0, 0, 0.13) inset,
@@ -357,17 +418,22 @@
357
418
  }
358
419
 
359
420
  .tags-input-wrapper.focused {
360
- border-color: var(--blueBorder, #007df0);
421
+ border-color: var(--blueBorder);
361
422
  }
362
423
 
363
424
  .tags-input-wrapper.invalid {
364
- border-color: var(--redBorder, #e42f3a);
425
+ border-color: var(--redBorder);
426
+ }
427
+
428
+ .tags-input-wrapper.success {
429
+ border-color: var(--greenBorder);
365
430
  }
366
431
 
367
- .tags-input-wrapper.disabled {
432
+ .tags-input-wrapper.disabled,
433
+ .tags-input-wrapper.readonly {
368
434
  cursor: not-allowed;
369
- opacity: 0.5;
370
- border-color: var(--border1, rgba(255, 255, 255, 0.1));
435
+ opacity: 0.7;
436
+ border-color: var(--border1);
371
437
  }
372
438
 
373
439
  .tags-input-wrapper.loading {
@@ -379,34 +445,56 @@
379
445
  flex-wrap: wrap;
380
446
  align-items: flex-start;
381
447
  align-content: flex-start;
382
- gap: 4px;
448
+ gap: 0;
383
449
  width: 100%;
384
450
  }
385
451
 
452
+ .tags-input-wrapper.show-remove-icon .tags-input-content {
453
+ gap: 4px;
454
+ }
455
+
386
456
  .tag {
387
457
  position: relative;
388
458
  display: flex;
389
459
  padding: 4px 8px;
390
460
  justify-content: center;
391
461
  align-items: center;
392
- border-radius: var(--border-radius, 4px);
462
+ border-radius: var(--border-radius);
393
463
  background: var(--actionSecondaryBackground);
394
- color: var(--text1, #ebebeb);
395
- font-size: var(--font-size-small, 11.5px);
396
- font-weight: var(--font-weight-normal, 400);
464
+ color: var(--text1);
465
+ font-size: var(--font-size-small);
466
+ font-weight: var(--font-weight-normal);
397
467
  line-height: 16px;
398
468
  letter-spacing: -0.115px;
399
469
  box-shadow:
400
470
  0 0.5px 1px 0 #000,
401
471
  0 0.5px 0.5px 0 rgba(255, 255, 255, 0.12) inset;
402
472
  user-select: none;
473
+ max-width: 100%;
474
+ min-width: 0;
475
+ margin: 2px;
476
+ }
477
+
478
+ .tags-input-wrapper.show-remove-icon .tag {
479
+ margin: 0;
480
+ }
481
+
482
+ .tag.expandable {
483
+ cursor: pointer;
484
+ }
485
+
486
+ .tag.expanded .tag-text {
487
+ overflow: visible;
488
+ text-overflow: unset;
489
+ white-space: normal;
490
+ word-break: break-word;
403
491
  }
404
492
 
405
493
  .tag-text {
406
- max-width: 150px;
407
494
  overflow: hidden;
408
495
  text-overflow: ellipsis;
409
496
  white-space: nowrap;
497
+ min-width: 0;
410
498
  }
411
499
 
412
500
  .tag-remove {
@@ -423,7 +511,7 @@
423
511
  background: #464646;
424
512
  color: var(--text2);
425
513
  cursor: pointer;
426
- border-radius: 0 var(--border-radius, 4px) var(--border-radius, 4px) 0;
514
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
427
515
  opacity: 0;
428
516
  pointer-events: none;
429
517
  }
@@ -433,8 +521,22 @@
433
521
  pointer-events: auto;
434
522
  }
435
523
 
524
+ /* Show remove icon mode - inline button */
525
+ .tags-input-wrapper.show-remove-icon .tag-remove {
526
+ position: relative;
527
+ top: auto;
528
+ right: auto;
529
+ bottom: auto;
530
+ padding: 0;
531
+ margin-left: 4px;
532
+ background: transparent;
533
+ opacity: 1;
534
+ pointer-events: auto;
535
+ border-radius: 0;
536
+ }
537
+
436
538
  .tag-remove:not(:disabled) {
437
- color: var(--text1, #ebebeb);
539
+ color: var(--text1);
438
540
  }
439
541
 
440
542
  .tag-remove:disabled {
@@ -453,8 +555,8 @@
453
555
  padding: 4px 8px;
454
556
  border: none;
455
557
  background: transparent;
456
- color: var(--text1, #ebebeb);
457
- font-size: var(--font-size-small, 11.5px);
558
+ color: var(--text1);
559
+ font-size: var(--font-size-small);
458
560
  font-family: inherit;
459
561
  line-height: 16px;
460
562
  letter-spacing: -0.115px;
@@ -462,7 +564,7 @@
462
564
  }
463
565
 
464
566
  .tags-input-field::placeholder {
465
- color: var(--text3, #a3a3a3);
567
+ color: var(--text3);
466
568
  }
467
569
 
468
570
  .tags-input-field:disabled {
@@ -62,6 +62,18 @@ export interface TagsInputProps {
62
62
  * Whether to trim whitespace from tags (default: true)
63
63
  */
64
64
  trimTags?: boolean;
65
+ /**
66
+ * Whether to always show the remove icon on tags (default: false)
67
+ * When true: remove button is inline, 4px gap, no padding on close button
68
+ * When false: remove button is absolute positioned, appears on hover
69
+ */
70
+ showRemoveIcon?: boolean;
71
+ /**
72
+ * Whether clicking a tag expands it to show full content (default: false)
73
+ * When true: clicking a tag removes ellipsis and shows full text
74
+ * When false: long tags are always truncated with ellipsis
75
+ */
76
+ expandOnClick?: boolean;
65
77
  /**
66
78
  * Custom width for the component
67
79
  */
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 12 12" fill="none">
2
2
  <path
3
3
  fill-rule="evenodd"
4
4
  clip-rule="evenodd"
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 14 14" fill="none">
2
2
  <path
3
3
  opacity="0.4"
4
4
  fill-rule="evenodd"
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16" fill="none">
2
2
  <path
3
3
  d="M8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0ZM11.53 10.47L10.47 11.53L8 9.06L5.53 11.53L4.47 10.47L6.94 8L4.47 5.53L5.53 4.47L8 6.94L10.47 4.47L11.53 5.53L9.06 8L11.53 10.47Z"
4
4
  fill="currentColor"
@@ -42,7 +42,3 @@
42
42
  </script>
43
43
 
44
44
  {#if children}{@render children()}{/if}
45
-
46
- <style>
47
- /* Global provider styles - currently using display: contents */
48
- </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {