@finsweet/webflow-apps-utils 1.0.33 → 1.0.37

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.
@@ -59,6 +59,8 @@
59
59
  main?: Snippet;
60
60
  /** Preview bar content snippet */
61
61
  previewBar?: Snippet;
62
+ /** Custom tabs snippet to override default tab rendering */
63
+ customTabs?: Snippet;
62
64
  /** Footer content snippet */
63
65
  footer?: Snippet;
64
66
  }
@@ -73,6 +75,7 @@
73
75
  showFooter = true,
74
76
  showSidebar = true,
75
77
  showTabs = true,
78
+ customTabs,
76
79
  showPreviewBar = true,
77
80
  sidebarWidth = '274px',
78
81
  containerMode = false,
@@ -164,60 +167,64 @@
164
167
  >
165
168
  {#if showTabs}
166
169
  <div class="navbar" data-area="navbar">
167
- {#each tabs as tab (tab.path)}
168
- {@const Icon = tab.icon}
169
- {@const notification = getNotification(tab.path)}
170
- <button
171
- class="tab"
172
- class:isActive={activeTab === tab.path}
173
- class:warning={notification && !notification?.success}
174
- class:success={notification && notification?.success}
175
- onclick={() => switchTab(tab.path)}
176
- >
177
- <Icon />
178
- <span
179
- class="tab-text"
180
- style="color: {activeTab !== tab.path ? 'var(--text2)' : 'var(--actionPrimaryText)'}"
170
+ {#if customTabs}
171
+ {@render customTabs()}
172
+ {:else}
173
+ {#each tabs as tab (tab.path)}
174
+ {@const Icon = tab.icon}
175
+ {@const notification = getNotification(tab.path)}
176
+ <button
177
+ class="tab"
178
+ class:isActive={activeTab === tab.path}
179
+ class:warning={notification && !notification?.success}
180
+ class:success={notification && notification?.success}
181
+ onclick={() => switchTab(tab.path)}
181
182
  >
182
- {tab.name}
183
- </span>
184
-
185
- {#if notification?.showNotification}
186
- {#if notification?.success}
187
- <span class="notification-pill success">
188
- <CheckCircleOutlinedIcon />
189
- </span>
190
- {:else}
191
- <Tooltip
192
- message={notification?.message}
193
- placement="right"
194
- offsetVal={8}
195
- position="fixed"
196
- width="max-content"
197
- >
198
- {#snippet target()}
199
- <div class="notification-pill warning-tooltip">
200
- <WarningCircleOutlineIcon />
201
- </div>
202
- {/snippet}
203
- </Tooltip>
183
+ <Icon />
184
+ <span
185
+ class="tab-text"
186
+ style="color: {activeTab !== tab.path ? 'var(--text2)' : 'var(--actionPrimaryText)'}"
187
+ >
188
+ {tab.name}
189
+ </span>
190
+
191
+ {#if notification?.showNotification}
192
+ {#if notification?.success}
193
+ <span class="notification-pill success">
194
+ <CheckCircleOutlinedIcon />
195
+ </span>
196
+ {:else}
197
+ <Tooltip
198
+ message={notification?.message}
199
+ placement="right"
200
+ offsetVal={8}
201
+ position="fixed"
202
+ width="max-content"
203
+ >
204
+ {#snippet target()}
205
+ <div class="notification-pill warning-tooltip">
206
+ <WarningCircleOutlineIcon />
207
+ </div>
208
+ {/snippet}
209
+ </Tooltip>
210
+ {/if}
204
211
  {/if}
205
- {/if}
206
- </button>
207
- {/each}
208
- </div>
209
- {/if}
210
-
211
- {#if showPreviewBar && showSidebar}
212
- <div class="preview-bar" data-area="preview-bar">
213
- {#if previewBar}
214
- {@render previewBar()}
215
- {:else}
216
- <div class="preview-bar-content">
217
- <span>Preview: {activeTab} tab content</span>
218
- </div>
212
+ </button>
213
+ {/each}
219
214
  {/if}
220
215
  </div>
216
+
217
+ {#if showPreviewBar && showSidebar}
218
+ <div class="preview-bar" data-area="preview-bar">
219
+ {#if previewBar}
220
+ {@render previewBar()}
221
+ {:else}
222
+ <div class="preview-bar-content">
223
+ <span>Preview: {activeTab} tab content</span>
224
+ </div>
225
+ {/if}
226
+ </div>
227
+ {/if}
221
228
  {/if}
222
229
 
223
230
  {#if showSidebar}
@@ -47,6 +47,8 @@ interface LayoutProps extends HTMLAttributes<HTMLDivElement> {
47
47
  main?: Snippet;
48
48
  /** Preview bar content snippet */
49
49
  previewBar?: Snippet;
50
+ /** Custom tabs snippet to override default tab rendering */
51
+ customTabs?: Snippet;
50
52
  /** Footer content snippet */
51
53
  footer?: Snippet;
52
54
  }
@@ -126,6 +126,10 @@
126
126
  searchPlaceholder: {
127
127
  control: 'text',
128
128
  description: 'Placeholder text for search input'
129
+ },
130
+ hide: {
131
+ control: 'object',
132
+ description: 'Array of group keys or region codes to hide from display'
129
133
  }
130
134
  },
131
135
  args: {
@@ -381,3 +385,93 @@
381
385
  }
382
386
  }}
383
387
  />
388
+
389
+ <Story
390
+ name="Hide Global Group"
391
+ args={{
392
+ regionGroups: dummyRegionGroups,
393
+ selectedRegions: [],
394
+ usedRegions: emptyUsedRegions,
395
+ hide: ['global-parent']
396
+ }}
397
+ parameters={{
398
+ docs: {
399
+ description: {
400
+ story:
401
+ 'Demonstrates hiding the Global group using the parent key. The Global option will not be visible.'
402
+ }
403
+ }
404
+ }}
405
+ />
406
+
407
+ <Story
408
+ name="Hide by Region Code"
409
+ args={{
410
+ regionGroups: dummyRegionGroups,
411
+ selectedRegions: [],
412
+ usedRegions: emptyUsedRegions,
413
+ hide: ['Global', 'EU']
414
+ }}
415
+ parameters={{
416
+ docs: {
417
+ description: {
418
+ story:
419
+ 'Shows hiding multiple groups by their region codes. Both Global and EU groups are hidden.'
420
+ }
421
+ }
422
+ }}
423
+ />
424
+
425
+ <Story
426
+ name="Hide Multiple Groups"
427
+ args={{
428
+ regionGroups: dummyRegionGroups,
429
+ selectedRegions: ['CA', 'MX'],
430
+ usedRegions: emptyUsedRegions,
431
+ hide: ['global-parent', 'europe', 'us']
432
+ }}
433
+ parameters={{
434
+ docs: {
435
+ description: {
436
+ story:
437
+ 'Demonstrates hiding multiple groups at once. Only the Countries group remains visible, with some regions pre-selected.'
438
+ }
439
+ }
440
+ }}
441
+ />
442
+
443
+ <Story
444
+ name="Three State Checkboxes"
445
+ args={{
446
+ regionGroups: dummyRegionGroups,
447
+ selectedRegions: ['US-CA', 'US-AZ'],
448
+ usedRegions: emptyUsedRegions,
449
+ showSelectionDisplay: true
450
+ }}
451
+ parameters={{
452
+ docs: {
453
+ description: {
454
+ story:
455
+ 'Demonstrates the three-state checkbox system: empty (no selections), minus icon (partial selections), and checkmark (all selections). The United States group shows a minus icon because only some states are selected (California and Arizona). Try selecting more states to see how the checkbox changes.'
456
+ }
457
+ }
458
+ }}
459
+ />
460
+
461
+ <Story
462
+ name="Three State with Mixed Groups"
463
+ args={{
464
+ regionGroups: dummyRegionGroups,
465
+ selectedRegions: ['US-CA', 'US-AZ', 'US-FL', 'CA', 'MX'],
466
+ usedRegions: emptyUsedRegions,
467
+ showSelectionDisplay: true
468
+ }}
469
+ parameters={{
470
+ docs: {
471
+ description: {
472
+ story:
473
+ 'Shows three-state checkboxes with mixed selections across multiple groups. The "Select Countries" parent group shows a minus icon because it contains both the US subgroup (partially selected) and individual countries (Canada, Mexico). This demonstrates how parent groups intelligently show partial selection state when their children have mixed selection states.'
474
+ }
475
+ }
476
+ }}
477
+ />
@@ -1,7 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { SvelteSet } from 'svelte/reactivity';
3
3
 
4
+ import { SubtractIcon } from '../../icons';
5
+
6
+ import Button from '../button/Button.svelte';
4
7
  import { Checkbox } from '../checkbox';
8
+ import { Tooltip } from '../tooltip';
5
9
  import type { ExtendedRegionGroup, Region, RegionGroup, RegionSelectorProps } from './types.js';
6
10
 
7
11
  let {
@@ -18,6 +22,8 @@
18
22
  error,
19
23
  maxVisibleBadges = 2,
20
24
  searchPlaceholder = 'Search regions...',
25
+ showSelectionDisplay = false,
26
+ hide = [],
21
27
  class: className = '',
22
28
  ...restProps
23
29
  }: RegionSelectorProps = $props();
@@ -31,13 +37,11 @@
31
37
  * Sync with prop changes and trigger callback
32
38
  */
33
39
  $effect(() => {
34
- // Sync from prop to internal state when prop changes externally
35
40
  if (JSON.stringify(selectedRegions) !== JSON.stringify(previousSelectedRegions)) {
36
41
  tempSelectedRegions = [...selectedRegions];
37
42
  previousSelectedRegions = [...selectedRegions];
38
43
  }
39
44
 
40
- // Trigger callback when internal state changes
41
45
  if (JSON.stringify(tempSelectedRegions) !== JSON.stringify(selectedRegions)) {
42
46
  onRegionsChange?.(tempSelectedRegions);
43
47
  }
@@ -170,11 +174,9 @@
170
174
  */
171
175
  $effect(() => {
172
176
  if (searchQuery) {
173
- // Clear first
174
177
  expandedGroups.clear();
175
178
 
176
179
  restructuredGroups.forEach((group) => {
177
- // Check if this group has matching content
178
180
  const hasMatchingRegions = group.regions.some(
179
181
  (region) =>
180
182
  region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -267,18 +269,15 @@
267
269
  if (tempSelectedRegions.includes('Global')) {
268
270
  tempSelectedRegions = [];
269
271
  } else {
270
- // Clear all and select only Global
271
272
  tempSelectedRegions = ['Global'];
272
273
  }
273
274
  } else if (code === 'EU') {
274
275
  if (tempSelectedRegions.includes('EU')) {
275
276
  tempSelectedRegions = [];
276
277
  } else {
277
- // Clear all and select only EU
278
278
  tempSelectedRegions = ['EU'];
279
279
  }
280
280
  } else {
281
- // Selecting a country - clear Global and EU
282
281
  const filteredRegions = tempSelectedRegions.filter((c) => c !== 'Global' && c !== 'EU');
283
282
 
284
283
  if (filteredRegions.includes(code)) {
@@ -352,6 +351,50 @@
352
351
  return selectedCount > 0 && selectedCount < group.regions.length;
353
352
  };
354
353
 
354
+ /**
355
+ * Check if extended group (with subgroups) is partially selected
356
+ */
357
+ const isExtendedGroupPartiallySelected = (group: ExtendedRegionGroup): boolean => {
358
+ const allRegions = [...group.regions, ...(group.subGroups?.flatMap((sg) => sg.regions) || [])];
359
+
360
+ const availableRegions = allRegions.filter((region) => !usedRegions.regions.has(region.code));
361
+ const selectedCount = availableRegions.filter((region) =>
362
+ tempSelectedRegions.includes(region.code)
363
+ ).length;
364
+
365
+ return selectedCount > 0 && selectedCount < availableRegions.length;
366
+ };
367
+
368
+ /**
369
+ * Check if extended group (with subgroups) is fully selected
370
+ */
371
+ const isExtendedGroupFullySelected = (group: ExtendedRegionGroup): boolean => {
372
+ const allRegions = [...group.regions, ...(group.subGroups?.flatMap((sg) => sg.regions) || [])];
373
+
374
+ const availableRegions = allRegions.filter((region) => !usedRegions.regions.has(region.code));
375
+ return (
376
+ availableRegions.length > 0 &&
377
+ availableRegions.every((region) => tempSelectedRegions.includes(region.code))
378
+ );
379
+ };
380
+
381
+ /**
382
+ * Get checkbox state for a group (none, partial, full)
383
+ */
384
+ const getGroupCheckboxState = (
385
+ group: ExtendedRegionGroup | RegionGroup
386
+ ): 'none' | 'partial' | 'full' => {
387
+ if ('subGroups' in group && group.subGroups) {
388
+ if (isExtendedGroupFullySelected(group)) return 'full';
389
+ if (isExtendedGroupPartiallySelected(group)) return 'partial';
390
+ return 'none';
391
+ } else {
392
+ if (isGroupFullySelected(group as RegionGroup)) return 'full';
393
+ if (isGroupPartiallySelected(group as RegionGroup)) return 'partial';
394
+ return 'none';
395
+ }
396
+ };
397
+
355
398
  /**
356
399
  * Toggle entire group selection
357
400
  */
@@ -363,7 +406,6 @@
363
406
  if (tempSelectedRegions.includes('Global')) {
364
407
  tempSelectedRegions = [];
365
408
  } else {
366
- // Clear all and select only Global
367
409
  tempSelectedRegions = ['Global'];
368
410
  }
369
411
  return;
@@ -373,54 +415,44 @@
373
415
  if (tempSelectedRegions.includes('EU')) {
374
416
  tempSelectedRegions = [];
375
417
  } else {
376
- // Clear all and select only EU
377
418
  tempSelectedRegions = ['EU'];
378
419
  }
379
420
  return;
380
421
  }
381
422
 
382
423
  if (group.isMainParent) {
383
- // This is the "Select Countries" group
384
424
  const allGroupAndSubGroupRegions = [
385
425
  ...group.regions.map((r) => r.code),
386
426
  ...(group.subGroups?.flatMap((sg) => sg.regions.map((r) => r.code)) || [])
387
427
  ];
388
428
 
389
- // Filter out regions that are already in use
390
429
  const availableRegions = allGroupAndSubGroupRegions.filter(
391
430
  (code) => !usedRegions.regions.has(code)
392
431
  );
393
432
 
394
- // Clear Global and EU when selecting countries
395
433
  const currentCountrySelections = tempSelectedRegions.filter(
396
434
  (code) => code !== 'Global' && code !== 'EU'
397
435
  );
398
436
 
399
- // Check if all available (non-in-use) regions are selected
400
437
  const allAvailableSelected = availableRegions.every((code) =>
401
438
  currentCountrySelections.includes(code)
402
439
  );
403
440
 
404
441
  if (allAvailableSelected) {
405
- // Deselect all available countries
406
442
  tempSelectedRegions = currentCountrySelections.filter(
407
443
  (code) => !availableRegions.includes(code)
408
444
  );
409
445
  } else {
410
- // Select all available countries (excluding Global, EU, and in-use regions)
411
446
  tempSelectedRegions = [...new Set([...currentCountrySelections, ...availableRegions])];
412
447
  }
413
448
  return;
414
449
  }
415
450
 
416
- // Sub-group toggle (like United States)
417
451
  const isFullySelected = isGroupFullySelected(group);
418
452
  const groupCodes = group.regions.map((r) => r.code);
419
453
 
420
- // Filter out regions that are already in use
421
454
  const availableGroupCodes = groupCodes.filter((code) => !usedRegions.regions.has(code));
422
455
 
423
- // Clear Global and EU when selecting countries
424
456
  const currentCountrySelections = tempSelectedRegions.filter(
425
457
  (code) => code !== 'Global' && code !== 'EU'
426
458
  );
@@ -428,11 +460,59 @@
428
460
  if (isFullySelected) {
429
461
  tempSelectedRegions = currentCountrySelections.filter((code) => !groupCodes.includes(code));
430
462
  } else {
431
- // Only select available (non-in-use) regions
432
463
  tempSelectedRegions = [...new Set([...currentCountrySelections, ...availableGroupCodes])];
433
464
  }
434
465
  };
435
466
 
467
+ /**
468
+ * Gets the instance name for a region from the usedRegions mapping
469
+ */
470
+ const getInstanceName = (regionCode: string): string => {
471
+ return usedRegions.instanceNames?.get(regionCode) || 'another Instance';
472
+ };
473
+
474
+ /**
475
+ * Creates tooltip message for regions in use
476
+ */
477
+ const getInUseTooltipMessage = (regionCode?: string): string => {
478
+ if (!regionCode) return 'Currently in use on another instance';
479
+
480
+ const instanceName = getInstanceName(regionCode);
481
+ return `Currently in use on ${instanceName}`;
482
+ };
483
+
484
+ /**
485
+ * Check if a group should be hidden based on hide prop
486
+ */
487
+ const shouldHideGroup = (group: ExtendedRegionGroup): boolean => {
488
+ if (hide.length === 0) return false;
489
+
490
+ if (hide.includes(group.key) || (group.parentKey && hide.includes(group.parentKey))) {
491
+ return true;
492
+ }
493
+
494
+ const hasHiddenRegion = group.regions.some((region) => hide.includes(region.code));
495
+ if (hasHiddenRegion) return true;
496
+
497
+ return false;
498
+ };
499
+
500
+ /**
501
+ * Check if a subgroup should be hidden based on hide prop
502
+ */
503
+ const shouldHideSubGroup = (subGroup: RegionGroup): boolean => {
504
+ if (hide.length === 0) return false;
505
+
506
+ if (hide.includes(subGroup.key)) {
507
+ return true;
508
+ }
509
+
510
+ const hasHiddenRegion = subGroup.regions.some((region) => hide.includes(region.code));
511
+ if (hasHiddenRegion) return true;
512
+
513
+ return false;
514
+ };
515
+
436
516
  /**
437
517
  * Apply selections and trigger callback
438
518
  */
@@ -441,6 +521,18 @@
441
521
  };
442
522
  </script>
443
523
 
524
+ {#snippet customCheckbox(state: 'none' | 'partial' | 'full', disabled: boolean = false)}
525
+ {#if state === 'full'}
526
+ <Checkbox checked={true} {disabled} />
527
+ {:else if state === 'partial'}
528
+ <div class="partially-selected-box" class:disabled>
529
+ <SubtractIcon />
530
+ </div>
531
+ {:else}
532
+ <Checkbox checked={false} {disabled} />
533
+ {/if}
534
+ {/snippet}
535
+
444
536
  <div class="region-selector {className}" {...restProps}>
445
537
  <div class="search-actions-row">
446
538
  <div class="search-section">
@@ -452,11 +544,11 @@
452
544
  />
453
545
  </div>
454
546
  {#if tempSelectedRegions.length > 0}
455
- <button type="button" class="clear-btn" onclick={clearAll}>Clear all</button>
547
+ <Button text="Clear all" variant="secondary" onclick={clearAll} class="clear-btn" />
456
548
  {/if}
457
549
  </div>
458
550
 
459
- {#if tempSelectedRegions.length > 0}
551
+ {#if showSelectionDisplay && tempSelectedRegions.length > 0}
460
552
  <div class="selection-display">
461
553
  <div class="selected-items">
462
554
  {#each visibleBadges as region (region.code)}
@@ -519,7 +611,12 @@
519
611
  {@const isGroupUsedByOther = isGlobalGroupUsed || isEUGroupUsed || allGroupRegionsInUse}
520
612
  {@const isGroupDisabled = isGroupUsedByOther}
521
613
 
522
- <div class="option-group" class:disabled={isGroupDisabled} class:main-parent={isMainParent}>
614
+ <div
615
+ class="option-group"
616
+ class:disabled={isGroupDisabled}
617
+ class:main-parent={isMainParent}
618
+ class:hidden={shouldHideGroup(group)}
619
+ >
523
620
  {#if isMainParent}
524
621
  <button
525
622
  type="button"
@@ -579,18 +676,29 @@
579
676
  }
580
677
  }}
581
678
  >
582
- <Checkbox
583
- checked={isThisMainParentSelected}
584
- variant="radio"
585
- disabled={isGroupDisabled}
586
- />
679
+ <Tooltip
680
+ message={getInUseTooltipMessage()}
681
+ placement="top"
682
+ listener="hover"
683
+ offsetVal={6}
684
+ width="auto"
685
+ position="fixed"
686
+ padding="6px 8px"
687
+ disabled={!isGroupDisabled}
688
+ >
689
+ {#snippet target()}
690
+ {@render customCheckbox(getGroupCheckboxState(group), isGroupDisabled)}
691
+ {/snippet}
692
+ </Tooltip>
693
+ </div>
694
+ <div class="group-label-text-wrapper">
695
+ <span class="group-label-text" class:in-use={isGroupUsedByOther}>
696
+ <span class="group-name">{group.name}</span>
697
+ {#if isGroupUsedByOther}
698
+ <span class="in-use-badge">In use</span>
699
+ {/if}
700
+ </span>
587
701
  </div>
588
- <span class="group-label-text">
589
- {group.name}
590
- {#if isGroupUsedByOther}
591
- <span class="in-use-badge">(in-use)</span>
592
- {/if}
593
- </span>
594
702
  {#if !isNonExpandable}
595
703
  <span class="group-chevron">
596
704
  <svg viewBox="0 0 20 20" fill="currentColor">
@@ -614,7 +722,7 @@
614
722
  {@const isSubGroupPartiallySelected = isGroupPartiallySelected(subGroup)}
615
723
  {@const isSubGroupExpanded = isGroupExpanded(subGroup.key)}
616
724
 
617
- <div class="sub-group">
725
+ <div class="sub-group" class:hidden={shouldHideSubGroup(subGroup)}>
618
726
  <button
619
727
  type="button"
620
728
  class="group-label sub-group-label"
@@ -654,22 +762,19 @@
654
762
  }
655
763
  }}
656
764
  >
657
- {#if isSubGroupPartiallySelected}
658
- <div class="partially-selected-box">
659
- <svg viewBox="0 0 20 20" fill="currentColor">
660
- <path d="M5 10h10v1H5z" />
661
- </svg>
662
- </div>
663
- {:else}
664
- <Checkbox checked={isSubGroupFullySelected} disabled={isSubGroupDisabled} />
665
- {/if}
765
+ {@render customCheckbox(getGroupCheckboxState(subGroup), isSubGroupDisabled)}
766
+ </div>
767
+ <div class="group-label-text-wrapper">
768
+ <span
769
+ class="group-label-text sub-group-text"
770
+ class:in-use={allSubGroupRegionsInUse}
771
+ >
772
+ <span class="group-name">{subGroup.name}</span>
773
+ {#if allSubGroupRegionsInUse}
774
+ <span class="in-use-badge">In use</span>
775
+ {/if}
776
+ </span>
666
777
  </div>
667
- <span class="group-label-text sub-group-text">
668
- {subGroup.name}
669
- {#if allSubGroupRegionsInUse}
670
- <span class="in-use-badge">(in-use)</span>
671
- {/if}
672
- </span>
673
778
  <span class="group-chevron">
674
779
  <svg viewBox="0 0 20 20" fill="currentColor">
675
780
  <path
@@ -686,32 +791,51 @@
686
791
  {@const isRegionInUse = usedRegions.regions.has(region.code)}
687
792
  {@const isRegionDisabled = isRegionInUse}
688
793
 
689
- <button
690
- type="button"
691
- class="option-item sub-group-item"
692
- class:disabled={isRegionDisabled}
693
- class:in-use={isRegionInUse}
694
- onclick={() => {
695
- if (!isRegionDisabled) {
696
- toggleRegion(region.code);
697
- }
698
- }}
699
- disabled={isRegionDisabled}
700
- >
701
- <Checkbox
702
- checked={tempSelectedRegions.includes(region.code)}
703
- disabled={isRegionDisabled}
704
- />
705
- <span class="option-label">
706
- {region.name}
707
- <span class="region-code-badge">
708
- {region.code}
709
- {#if isRegionInUse}
710
- <span class="in-use-badge">(in-use)</span>
711
- {/if}
794
+ {#if isRegionDisabled}
795
+ <button
796
+ type="button"
797
+ class="option-item sub-group-item"
798
+ class:disabled={isRegionDisabled}
799
+ class:in-use={isRegionInUse}
800
+ style="cursor: not-allowed;"
801
+ >
802
+ <Tooltip
803
+ message={getInUseTooltipMessage(region.code)}
804
+ placement="top"
805
+ listener="hover"
806
+ offsetVal={6}
807
+ width="auto"
808
+ position="fixed"
809
+ padding="6px 8px"
810
+ >
811
+ {#snippet target()}
812
+ <Checkbox
813
+ checked={tempSelectedRegions.includes(region.code)}
814
+ disabled={isRegionDisabled}
815
+ />
816
+ {/snippet}
817
+ </Tooltip>
818
+ <span
819
+ class="option-label"
820
+ class:in-use={isRegionInUse}
821
+ class:disabled={isRegionDisabled}
822
+ >
823
+ <span class="region-name">{region.name}</span>
824
+ <span class="in-use-badge">In use</span>
825
+ </span>
826
+ </button>
827
+ {:else}
828
+ <button
829
+ type="button"
830
+ class="option-item sub-group-item"
831
+ onclick={() => toggleRegion(region.code)}
832
+ >
833
+ <Checkbox checked={tempSelectedRegions.includes(region.code)} />
834
+ <span class="option-label">
835
+ <span class="region-name">{region.name}</span>
712
836
  </span>
713
- </span>
714
- </button>
837
+ </button>
838
+ {/if}
715
839
  {/each}
716
840
  {/if}
717
841
  </div>
@@ -723,33 +847,53 @@
723
847
  {@const isRegionInUse = usedRegions.regions.has(region.code)}
724
848
  {@const isRegionDisabled = isRegionInUse}
725
849
 
726
- <button
727
- type="button"
728
- class="option-item"
729
- class:indented={isMainParent}
730
- class:disabled={isRegionDisabled}
731
- class:in-use={isRegionInUse}
732
- onclick={() => {
733
- if (!isRegionDisabled) {
734
- toggleRegion(region.code);
735
- }
736
- }}
737
- disabled={isRegionDisabled}
738
- >
739
- <Checkbox
740
- checked={tempSelectedRegions.includes(region.code)}
741
- disabled={isRegionDisabled}
742
- />
743
- <span class="option-label">
744
- {region.name}
745
- <span class="region-code-badge">
746
- {region.code}
747
- {#if isRegionInUse}
748
- <span class="in-use-badge">(in-use)</span>
749
- {/if}
850
+ {#if isRegionDisabled}
851
+ <button
852
+ type="button"
853
+ class="option-item"
854
+ class:indented={isMainParent}
855
+ class:disabled={isRegionDisabled}
856
+ class:in-use={isRegionInUse}
857
+ style="cursor: not-allowed;"
858
+ disabled={true}
859
+ >
860
+ <Tooltip
861
+ message={getInUseTooltipMessage(region.code)}
862
+ placement="top"
863
+ listener="hover"
864
+ offsetVal={6}
865
+ width="auto"
866
+ padding="6px 8px"
867
+ >
868
+ {#snippet target()}
869
+ <Checkbox
870
+ checked={tempSelectedRegions.includes(region.code)}
871
+ disabled={isRegionDisabled}
872
+ />
873
+ {/snippet}
874
+ </Tooltip>
875
+ <span
876
+ class="option-label"
877
+ class:disabled={isRegionDisabled}
878
+ class:in-use={isRegionInUse}
879
+ >
880
+ <span class="region-name">{region.name}</span>
881
+ <span class="in-use-badge">In use</span>
882
+ </span>
883
+ </button>
884
+ {:else}
885
+ <button
886
+ type="button"
887
+ class="option-item"
888
+ class:indented={isMainParent}
889
+ onclick={() => toggleRegion(region.code)}
890
+ >
891
+ <Checkbox checked={tempSelectedRegions.includes(region.code)} />
892
+ <span class="option-label">
893
+ <span class="region-name">{region.name}</span>
750
894
  </span>
751
- </span>
752
- </button>
895
+ </button>
896
+ {/if}
753
897
  {/each}
754
898
  {/if}
755
899
  {/if}
@@ -773,7 +917,6 @@
773
917
  align-items: flex-start;
774
918
  position: sticky;
775
919
  top: 0;
776
- background: var(--background2);
777
920
  z-index: 10;
778
921
  }
779
922
 
@@ -784,36 +927,52 @@
784
927
 
785
928
  .search-input {
786
929
  width: 100%;
787
- padding: 6px 10px;
788
- background: var(--background3);
789
- border: 1px solid var(--border1);
930
+ height: 24px;
931
+ padding: 0 4px;
932
+ background-color: var(--background1);
933
+ border: 1px solid var(--border1, #212121);
790
934
  border-radius: 4px;
791
- color: var(--text1);
792
- font-size: 11.5px;
935
+ color: var(--text3);
936
+ font-style: normal;
937
+ line-height: 16px;
938
+ letter-spacing: -0.115px;
939
+ box-shadow:
940
+ 0px 16px 16px -16px rgba(0, 0, 0, 0.13) inset,
941
+ 0px 12px 12px -12px rgba(0, 0, 0, 0.13) inset,
942
+ 0px 8px 8px -8px rgba(0, 0, 0, 0.17) inset,
943
+ 0px 4px 4px -4px rgba(0, 0, 0, 0.17) inset,
944
+ 0px 3px 3px -3px rgba(0, 0, 0, 0.17) inset,
945
+ 0px 1px 1px -1px rgba(0, 0, 0, 0.13) inset;
946
+ display: flex;
947
+ align-items: center;
948
+ }
949
+
950
+ .search-input::placeholder {
951
+ color: var(--text2) !important;
952
+ opacity: 1;
953
+ }
954
+
955
+ .search-input::-ms-input-placeholder {
956
+ color: var(--text2) !important;
793
957
  }
794
958
 
795
959
  .search-input:focus {
796
960
  outline: none;
797
- border-color: var(--actionPrimaryBackground);
961
+ border-color: var(--blueBorder);
798
962
  }
799
963
 
800
964
  .clear-btn {
801
- padding: 6px 12px;
965
+ height: 24px;
966
+ padding: 0 12px;
802
967
  background: var(--background3);
803
968
  border: 1px solid var(--border1);
804
969
  border-radius: 4px;
805
- font-size: 11px;
806
970
  cursor: pointer;
807
971
  color: var(--text1);
808
972
  white-space: nowrap;
809
973
  flex-shrink: 0;
810
974
  }
811
975
 
812
- .clear-btn:hover {
813
- background: var(--background3);
814
- border-color: var(--blueBorder);
815
- }
816
-
817
976
  .selection-display {
818
977
  padding: 8px 0;
819
978
  }
@@ -828,11 +987,10 @@
828
987
  display: inline-flex;
829
988
  align-items: center;
830
989
  gap: 4px;
831
- padding: 4px 8px;
990
+ padding: 4px 0;
832
991
  background: var(--background3);
833
992
  border: 1px solid var(--border1);
834
993
  border-radius: 4px;
835
- font-size: 11px;
836
994
  }
837
995
 
838
996
  .selected-badge:hover {
@@ -842,13 +1000,11 @@
842
1000
  .more-badge {
843
1001
  display: inline-flex;
844
1002
  align-items: center;
845
- padding: 4px 8px;
1003
+ padding: 4px 0;
846
1004
  background: var(--background3);
847
1005
  border: 1px solid var(--border1);
848
1006
  border-radius: 4px;
849
- font-size: 11px;
850
1007
  color: var(--text2);
851
- font-weight: 500;
852
1008
  }
853
1009
 
854
1010
  .remove-btn {
@@ -862,7 +1018,6 @@
862
1018
  border: none;
863
1019
  border-radius: 3px;
864
1020
  cursor: pointer;
865
- font-size: 1rem;
866
1021
  line-height: 1;
867
1022
  color: var(--text3);
868
1023
  }
@@ -885,7 +1040,6 @@
885
1040
  }
886
1041
 
887
1042
  .empty-text {
888
- font-size: 11px;
889
1043
  color: var(--text2);
890
1044
  }
891
1045
 
@@ -894,12 +1048,20 @@
894
1048
  }
895
1049
 
896
1050
  .empty-text-sub {
897
- font-size: 11px;
898
1051
  color: var(--text3);
899
1052
  }
900
1053
 
901
1054
  .option-group {
902
- margin-bottom: 8px;
1055
+ display: flex;
1056
+ padding: 0 4px;
1057
+ flex-direction: column;
1058
+ align-items: flex-start;
1059
+ gap: 2px;
1060
+ align-self: stretch;
1061
+ }
1062
+
1063
+ .option-group.hidden {
1064
+ display: none;
903
1065
  }
904
1066
 
905
1067
  .option-group:last-child {
@@ -911,7 +1073,17 @@
911
1073
  }
912
1074
 
913
1075
  .sub-group {
914
- margin-bottom: 4px;
1076
+ display: flex;
1077
+ padding: 0 4px;
1078
+ flex-direction: column;
1079
+ align-items: flex-start;
1080
+ gap: 2px;
1081
+ align-self: stretch;
1082
+ width: 100%;
1083
+ }
1084
+
1085
+ .sub-group.hidden {
1086
+ display: none;
915
1087
  }
916
1088
 
917
1089
  .group-label {
@@ -919,58 +1091,59 @@
919
1091
  display: flex;
920
1092
  align-items: center;
921
1093
  gap: 10px;
922
- padding: 8px;
1094
+ padding: 4px 0;
923
1095
  background: none;
924
1096
  border: none;
925
1097
  border-radius: 4px;
926
1098
  cursor: pointer;
927
1099
  text-align: left;
928
- margin-bottom: 4px;
929
1100
  }
930
1101
 
931
- .group-label:hover {
932
- background: var(--background3);
1102
+ .sub-group-label {
1103
+ padding: 4px 0;
1104
+ width: calc(100% - 8px);
1105
+ margin-left: 12px;
933
1106
  }
934
1107
 
935
- .group-label.partially-selected {
936
- background: var(--background3);
1108
+ .group-label-text {
1109
+ flex: 1;
1110
+ letter-spacing: 0.05em;
1111
+ color: var(--text2);
1112
+ display: flex;
1113
+ align-items: center;
1114
+ justify-content: space-between;
1115
+ gap: 6px;
937
1116
  }
938
1117
 
939
- .main-parent-label:hover {
940
- background: var(--background3);
1118
+ .group-label-text-wrapper {
1119
+ flex: 1;
1120
+ display: flex;
1121
+ align-items: center;
941
1122
  }
942
1123
 
943
- .main-parent-label.fully-selected {
944
- background: var(--background3);
1124
+ .group-name {
1125
+ flex: 1;
945
1126
  }
946
1127
 
947
- .sub-group-label {
948
- padding: 6px 12px;
949
- width: calc(100% - 28px);
950
- margin-left: 20px;
1128
+ .group-label-text.in-use .group-name {
1129
+ opacity: 0.4 !important;
951
1130
  }
952
1131
 
953
- .group-label-text {
1132
+ .group-label-text-wrapper {
954
1133
  flex: 1;
955
- font-size: 11px;
956
- font-weight: 600;
957
- text-transform: uppercase;
958
- letter-spacing: 0.05em;
959
- color: var(--text2);
960
1134
  display: flex;
961
1135
  align-items: center;
962
- gap: 6px;
963
1136
  }
964
1137
 
965
- .main-parent-label .group-label-text {
966
- font-size: 12px;
967
- font-weight: 700;
968
- color: var(--text1);
1138
+ .group-name {
1139
+ flex: 1;
1140
+ }
1141
+
1142
+ .group-label-text.in-use .group-name {
1143
+ opacity: 0.4 !important;
969
1144
  }
970
1145
 
971
1146
  .sub-group-text {
972
- font-size: 10.5px;
973
- font-weight: 500;
974
1147
  text-transform: none;
975
1148
  }
976
1149
 
@@ -1000,35 +1173,28 @@
1000
1173
  cursor: pointer;
1001
1174
  }
1002
1175
 
1003
- .group-checkbox:hover {
1004
- opacity: 0.8;
1005
- }
1006
-
1007
1176
  .option-item {
1008
1177
  width: 100%;
1009
1178
  display: flex;
1010
1179
  align-items: center;
1011
1180
  gap: 10px;
1012
- padding: 8px 12px;
1181
+ padding: 4px 0;
1013
1182
  background: none;
1183
+ height: 24px;
1014
1184
  border: none;
1015
1185
  border-radius: 4px;
1016
1186
  cursor: pointer;
1017
1187
  text-align: left;
1018
1188
  }
1019
1189
 
1020
- .option-item:hover {
1021
- background: var(--background3);
1022
- }
1023
-
1024
1190
  .option-item.indented {
1025
- width: calc(100% - 28px);
1026
- margin-left: 20px;
1191
+ width: calc(100% - 16px);
1192
+ margin-left: 16px;
1027
1193
  }
1028
1194
 
1029
1195
  .sub-group-item {
1030
- width: calc(100% - 56px);
1031
- margin-left: 56px;
1196
+ width: calc(100% - 32px);
1197
+ margin-left: 32px;
1032
1198
  }
1033
1199
 
1034
1200
  .option-item :global(.checkbox),
@@ -1036,6 +1202,11 @@
1036
1202
  flex-shrink: 0;
1037
1203
  }
1038
1204
 
1205
+ [role='radio'],
1206
+ [role='checkbox'] {
1207
+ width: 16px;
1208
+ height: 16px;
1209
+ }
1039
1210
  .partially-selected-box {
1040
1211
  width: 16px;
1041
1212
  height: 16px;
@@ -1049,27 +1220,36 @@
1049
1220
  border-color: var(--actionPrimaryBackground);
1050
1221
  }
1051
1222
 
1052
- .partially-selected-box svg {
1223
+ .partially-selected-box.disabled {
1224
+ background: var(--background3);
1225
+ border-color: var(--border1);
1226
+ opacity: 0.4;
1227
+ }
1228
+
1229
+ .partially-selected-box :global(svg) {
1053
1230
  width: 14px;
1054
1231
  height: 14px;
1055
1232
  color: white;
1056
1233
  }
1057
1234
 
1235
+ .partially-selected-box.disabled svg {
1236
+ color: var(--text3);
1237
+ }
1238
+
1058
1239
  .option-item.disabled,
1059
1240
  .group-label.disabled,
1060
1241
  .option-group.disabled {
1061
- opacity: 0.5;
1062
1242
  cursor: not-allowed;
1243
+ opacity: 1 !important;
1063
1244
  }
1064
1245
 
1065
- .option-item.disabled:hover,
1066
- .group-label.disabled:hover {
1067
- background: none;
1246
+ .group-label.in-use {
1247
+ cursor: not-allowed;
1248
+ opacity: 1 !important;
1068
1249
  }
1069
1250
 
1070
1251
  .option-label {
1071
1252
  flex: 1;
1072
- font-size: 11.5px;
1073
1253
  display: flex;
1074
1254
  justify-content: space-between;
1075
1255
  align-items: center;
@@ -1077,34 +1257,30 @@
1077
1257
  color: var(--text2);
1078
1258
  }
1079
1259
 
1080
- .region-code-badge {
1081
- font-size: 10px;
1082
- color: var(--text3);
1083
- font-family: monospace;
1084
- display: inline-flex;
1085
- align-items: center;
1086
- gap: 4px;
1260
+ .region-name {
1261
+ flex: 1;
1262
+ }
1263
+
1264
+ .option-label.disabled .region-name,
1265
+ .option-label.in-use .region-name {
1266
+ opacity: 0.4 !important;
1087
1267
  }
1088
1268
 
1089
1269
  .in-use-badge {
1270
+ display: flex;
1271
+ align-items: center;
1272
+ background:
1273
+ linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.1) 100%),
1274
+ linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.08) 100%);
1275
+ border-radius: 36px;
1276
+ padding: 4px 6px;
1090
1277
  font-size: 9px;
1091
- color: var(--yellowText, #f59e0b);
1092
- font-family:
1093
- system-ui,
1094
- -apple-system,
1095
- sans-serif;
1096
- font-weight: 500;
1097
- font-style: italic;
1278
+ color: var(--text2, #b9b9b9);
1279
+ flex-shrink: 0;
1098
1280
  }
1099
1281
 
1100
- .option-item.in-use,
1101
- .group-label.in-use {
1102
- opacity: 0.6;
1282
+ .option-item.in-use {
1103
1283
  cursor: not-allowed;
1104
- }
1105
-
1106
- .option-item.in-use:hover,
1107
- .group-label.in-use:hover {
1108
- background: none;
1284
+ opacity: 1 !important;
1109
1285
  }
1110
1286
  </style>
@@ -18,6 +18,7 @@ export interface UsedRegions {
18
18
  isGlobalUsed: boolean;
19
19
  isEUGroupUsed: boolean;
20
20
  byInstance: Map<string, Set<string>>;
21
+ instanceNames?: Map<string, string>;
21
22
  }
22
23
  export interface RegionSelectorProps {
23
24
  regionGroups: RegionGroup[];
@@ -25,8 +26,10 @@ export interface RegionSelectorProps {
25
26
  onRegionsChange?: (regions: string[]) => void;
26
27
  usedRegions?: UsedRegions;
27
28
  isLoading?: boolean;
29
+ showSelectionDisplay?: boolean;
28
30
  error?: string;
29
31
  maxVisibleBadges?: number;
30
32
  searchPlaceholder?: string;
33
+ hide?: string[];
31
34
  class?: string;
32
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.33",
3
+ "version": "1.0.37",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {
@@ -37,69 +37,70 @@
37
37
  "svelte": "^5.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@changesets/changelog-git": "^0.1.14",
41
- "@changesets/cli": "^2.27.1",
42
- "@chromatic-com/storybook": "^4",
43
- "@eslint/compat": "^1.2.5",
44
- "@eslint/js": "^9.18.0",
45
- "@playwright/test": "^1.49.1",
46
- "@storybook/addon-a11y": "^9.0.10",
47
- "@storybook/addon-docs": "^9.0.10",
48
- "@storybook/addon-svelte-csf": "^5.0.3",
49
- "@storybook/addon-vitest": "^9.0.10",
50
- "@storybook/sveltekit": "^9.0.10",
51
- "@sveltejs/adapter-auto": "^6.0.0",
52
- "@sveltejs/kit": "^2.16.0",
53
- "@sveltejs/package": "^2.0.0",
54
- "@sveltejs/vite-plugin-svelte": "^5.0.0",
55
- "@testing-library/jest-dom": "^6.6.3",
56
- "@testing-library/svelte": "^5.2.4",
57
- "@testing-library/user-event": "^14.6.1",
58
- "@types/js-cookie": "^3.0.6",
59
- "@types/lodash": "^4.17.18",
60
- "@types/lodash-es": "^4.17.12",
61
- "@types/luxon": "^3.6.2",
62
- "@types/node": "^22",
40
+ "@changesets/changelog-git": "0.2.1",
41
+ "@changesets/cli": "2.29.7",
42
+ "@chromatic-com/storybook": "4.1.1",
43
+ "@eslint/compat": "1.4.0",
44
+ "@eslint/js": "9.36.0",
45
+ "@playwright/test": "1.55.1",
46
+ "@storybook/addon-a11y": "9.1.8",
47
+ "@storybook/addon-docs": "9.1.8",
48
+ "@storybook/addon-svelte-csf": "5.0.8",
49
+ "@storybook/addon-vitest": "9.1.8",
50
+ "@storybook/sveltekit": "9.1.8",
51
+ "@sveltejs/adapter-auto": "6.1.0",
52
+ "@sveltejs/kit": "2.43.2",
53
+ "@sveltejs/package": "2.5.4",
54
+ "@sveltejs/vite-plugin-svelte": "5.1.1",
55
+ "@testing-library/jest-dom": "6.8.0",
56
+ "@testing-library/svelte": "5.2.8",
57
+ "@testing-library/user-event": "14.6.1",
58
+ "@types/js-cookie": "3.0.6",
59
+ "@types/lodash": "4.17.20",
60
+ "@types/lodash-es": "4.17.12",
61
+ "@types/luxon": "3.7.1",
62
+ "@types/node": "22.18.6",
63
63
  "@vitest/browser": "3.2.3",
64
64
  "@vitest/coverage-v8": "3.2.3",
65
- "@webflow/designer-extension-typings": "latest",
66
- "eslint": "^9.18.0",
67
- "eslint-config-prettier": "^10.0.1",
68
- "eslint-plugin-simple-import-sort": "^12.1.1",
69
- "eslint-plugin-storybook": "^9.0.10",
70
- "eslint-plugin-svelte": "^3.0.0",
71
- "globals": "^16.0.0",
72
- "jsdom": "^26.0.0",
73
- "prettier": "^3.4.2",
74
- "prettier-plugin-svelte": "^3.3.3",
75
- "publint": "^0.3.2",
76
- "storybook": "^9.0.10",
77
- "svelte": "^5.0.0",
78
- "svelte-check": "^4.0.0",
79
- "typescript": "^5.0.0",
80
- "typescript-eslint": "^8.20.0",
81
- "vite": "^6.2.6",
82
- "vitest": "^3.2.3"
65
+ "@webflow/designer-extension-typings": "2.0.23",
66
+ "eslint": "9.36.0",
67
+ "eslint-config-prettier": "10.1.8",
68
+ "eslint-plugin-simple-import-sort": "12.1.1",
69
+ "eslint-plugin-storybook": "9.1.8",
70
+ "eslint-plugin-svelte": "3.12.4",
71
+ "globals": "16.4.0",
72
+ "js-yaml": "4.1.1",
73
+ "jsdom": "26.1.0",
74
+ "prettier": "3.6.2",
75
+ "prettier-plugin-svelte": "3.4.0",
76
+ "publint": "0.3.13",
77
+ "storybook": "9.1.8",
78
+ "svelte": "5.39.5",
79
+ "svelte-check": "4.3.2",
80
+ "typescript": "5.9.2",
81
+ "typescript-eslint": "8.44.1",
82
+ "vite": "7.1.7",
83
+ "vitest": "3.2.4"
83
84
  },
84
85
  "keywords": [
85
86
  "svelte"
86
87
  ],
87
88
  "dependencies": {
88
- "@floating-ui/dom": "^1.7.1",
89
- "cheerio": "^1.1.0",
90
- "copy-text-to-clipboard": "^3.2.2",
91
- "csv-parse": "^5.6.0",
92
- "js-cookie": "^3.0.5",
93
- "just-debounce": "^1.1.0",
94
- "lodash": "^4.17.21",
95
- "lodash-es": "^4.17.21",
96
- "luxon": "^3.6.1",
97
- "motion": "^10.18.0",
98
- "svelte-routing": "^2.13.0",
99
- "swiper": "^11.2.8",
100
- "terser": "^5.43.1",
101
- "uuid": "^11.1.0",
102
- "zod": "^3.25.64"
89
+ "@floating-ui/dom": "1.7.4",
90
+ "cheerio": "1.1.2",
91
+ "copy-text-to-clipboard": "3.2.2",
92
+ "csv-parse": "5.6.0",
93
+ "js-cookie": "3.0.5",
94
+ "just-debounce": "1.1.0",
95
+ "lodash": "4.17.21",
96
+ "lodash-es": "4.17.21",
97
+ "luxon": "3.7.2",
98
+ "motion": "10.18.0",
99
+ "svelte-routing": "2.13.0",
100
+ "swiper": "11.2.10",
101
+ "terser": "5.44.0",
102
+ "uuid": "11.1.0",
103
+ "zod": "3.25.76"
103
104
  },
104
105
  "scripts": {
105
106
  "dev": "vite dev",