@finsweet/webflow-apps-utils 1.0.28 → 1.0.29

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.
@@ -0,0 +1,1110 @@
1
+ <script lang="ts">
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+
4
+ import { Checkbox } from '../checkbox';
5
+ import type { ExtendedRegionGroup, Region, RegionGroup, RegionSelectorProps } from './types.js';
6
+
7
+ let {
8
+ regionGroups,
9
+ selectedRegions = [],
10
+ onRegionsChange,
11
+ usedRegions = {
12
+ regions: new Set(),
13
+ isGlobalUsed: false,
14
+ isEUGroupUsed: false,
15
+ byInstance: new Map()
16
+ },
17
+ isLoading = false,
18
+ error,
19
+ maxVisibleBadges = 2,
20
+ searchPlaceholder = 'Search regions...',
21
+ class: className = '',
22
+ ...restProps
23
+ }: RegionSelectorProps = $props();
24
+
25
+ let searchQuery = $state('');
26
+ let tempSelectedRegions = $state<string[]>([...selectedRegions]);
27
+ let expandedGroups = new SvelteSet<string>();
28
+ let previousSelectedRegions = $state<string[]>([...selectedRegions]);
29
+
30
+ /**
31
+ * Sync with prop changes and trigger callback
32
+ */
33
+ $effect(() => {
34
+ // Sync from prop to internal state when prop changes externally
35
+ if (JSON.stringify(selectedRegions) !== JSON.stringify(previousSelectedRegions)) {
36
+ tempSelectedRegions = [...selectedRegions];
37
+ previousSelectedRegions = [...selectedRegions];
38
+ }
39
+
40
+ // Trigger callback when internal state changes
41
+ if (JSON.stringify(tempSelectedRegions) !== JSON.stringify(selectedRegions)) {
42
+ onRegionsChange?.(tempSelectedRegions);
43
+ }
44
+ });
45
+
46
+ let restructuredGroups = $derived.by((): ExtendedRegionGroup[] => {
47
+ const globalGroup = regionGroups.find((g) => g.regions.some((r) => r.code === 'Global'));
48
+ const euGroup = regionGroups.find(
49
+ (g) => g.key.toLowerCase().includes('eu') || g.name.toLowerCase().includes('european')
50
+ );
51
+
52
+ const usGroup = regionGroups.find(
53
+ (g) => g.key === 'us' || g.name.toLowerCase() === 'united states'
54
+ );
55
+ const otherCountries = regionGroups.filter(
56
+ (g) =>
57
+ g !== globalGroup &&
58
+ g !== euGroup &&
59
+ g !== usGroup &&
60
+ !g.regions.some((r) => r.code === 'Global')
61
+ );
62
+
63
+ const mainGroups: ExtendedRegionGroup[] = [];
64
+
65
+ if (globalGroup) {
66
+ mainGroups.push({
67
+ ...globalGroup,
68
+ isMainParent: true,
69
+ parentKey: 'global-parent'
70
+ });
71
+ }
72
+
73
+ if (euGroup) {
74
+ mainGroups.push({
75
+ ...euGroup,
76
+ isMainParent: true,
77
+ parentKey: 'eu-parent'
78
+ });
79
+ }
80
+
81
+ if (otherCountries.length > 0 || usGroup) {
82
+ const selectCountriesRegions: Region[] = [];
83
+ const selectCountriesSubGroups: RegionGroup[] = [];
84
+
85
+ if (usGroup) {
86
+ selectCountriesSubGroups.push(usGroup);
87
+ }
88
+
89
+ otherCountries.forEach((group) => {
90
+ selectCountriesRegions.push(...group.regions);
91
+ });
92
+
93
+ mainGroups.push({
94
+ name: 'Select Countries',
95
+ key: 'select-countries-parent',
96
+ regions: selectCountriesRegions,
97
+ total: selectCountriesRegions.length + (usGroup ? usGroup.total : 0),
98
+ isMainParent: true,
99
+ parentKey: 'select-countries-parent',
100
+ subGroups: selectCountriesSubGroups
101
+ });
102
+ }
103
+
104
+ return mainGroups;
105
+ });
106
+
107
+ let selectedMainParent = $derived.by(() => {
108
+ if (tempSelectedRegions.includes('Global')) return 'global-parent';
109
+
110
+ if (tempSelectedRegions.includes('EU')) {
111
+ return 'eu-parent';
112
+ }
113
+
114
+ const selectCountriesGroup = restructuredGroups.find(
115
+ (g) => g.parentKey === 'select-countries-parent'
116
+ );
117
+ if (selectCountriesGroup) {
118
+ const hasAnySelected =
119
+ selectCountriesGroup.regions.some((r) => tempSelectedRegions.includes(r.code)) ||
120
+ selectCountriesGroup.subGroups?.some((sg) =>
121
+ sg.regions.some((r) => tempSelectedRegions.includes(r.code))
122
+ );
123
+ if (hasAnySelected) return 'select-countries-parent';
124
+ }
125
+
126
+ return null;
127
+ });
128
+
129
+ let isGlobalSelected = $derived(tempSelectedRegions.includes('Global'));
130
+
131
+ let filteredGroups = $derived.by(() => {
132
+ if (!searchQuery) return restructuredGroups;
133
+
134
+ const filtered = restructuredGroups
135
+ .map((group) => {
136
+ const filteredRegions = group.regions.filter(
137
+ (region) =>
138
+ region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
139
+ region.code.toLowerCase().includes(searchQuery.toLowerCase()) ||
140
+ group.name.toLowerCase().includes(searchQuery.toLowerCase())
141
+ );
142
+
143
+ const filteredSubGroups = group.subGroups
144
+ ?.map((subGroup) => ({
145
+ ...subGroup,
146
+ regions: subGroup.regions.filter(
147
+ (region) =>
148
+ region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
149
+ region.code.toLowerCase().includes(searchQuery.toLowerCase()) ||
150
+ subGroup.name.toLowerCase().includes(searchQuery.toLowerCase())
151
+ )
152
+ }))
153
+ .filter((sg) => sg.regions.length > 0);
154
+
155
+ return {
156
+ ...group,
157
+ regions: filteredRegions,
158
+ subGroups: filteredSubGroups
159
+ };
160
+ })
161
+ .filter(
162
+ (group) => group.regions.length > 0 || (group.subGroups && group.subGroups.length > 0)
163
+ );
164
+
165
+ return filtered;
166
+ });
167
+
168
+ /**
169
+ * Auto-expand groups when searching
170
+ */
171
+ $effect(() => {
172
+ if (searchQuery) {
173
+ // Clear first
174
+ expandedGroups.clear();
175
+
176
+ restructuredGroups.forEach((group) => {
177
+ // Check if this group has matching content
178
+ const hasMatchingRegions = group.regions.some(
179
+ (region) =>
180
+ region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
181
+ region.code.toLowerCase().includes(searchQuery.toLowerCase())
182
+ );
183
+
184
+ const hasMatchingSubGroups = group.subGroups?.some((sg) =>
185
+ sg.regions.some(
186
+ (region) =>
187
+ region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
188
+ region.code.toLowerCase().includes(searchQuery.toLowerCase())
189
+ )
190
+ );
191
+
192
+ if (hasMatchingRegions || hasMatchingSubGroups) {
193
+ expandedGroups.add(group.key);
194
+ group.subGroups?.forEach((sg) => {
195
+ const sgHasMatching = sg.regions.some(
196
+ (region) =>
197
+ region.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
198
+ region.code.toLowerCase().includes(searchQuery.toLowerCase())
199
+ );
200
+ if (sgHasMatching) {
201
+ expandedGroups.add(sg.key);
202
+ }
203
+ });
204
+ }
205
+ });
206
+ }
207
+ });
208
+
209
+ let tempSelectedRegionObjects = $derived.by(() => {
210
+ const selected: Region[] = [];
211
+
212
+ if (tempSelectedRegions.includes('Global')) {
213
+ const globalGroup = regionGroups.find((g) => g.regions.some((r) => r.code === 'Global'));
214
+ const globalRegion = globalGroup?.regions.find((r) => r.code === 'Global');
215
+ if (globalRegion) {
216
+ selected.push(globalRegion);
217
+ }
218
+ } else if (tempSelectedRegions.includes('EU')) {
219
+ const euGroup = regionGroups.find((g) => g.key === 'europe');
220
+ if (euGroup) {
221
+ selected.push({
222
+ code: 'EU',
223
+ name: euGroup.name
224
+ });
225
+ }
226
+ } else {
227
+ const allRegions = regionGroups.flatMap((g) => g.regions);
228
+ tempSelectedRegions.forEach((code) => {
229
+ const region = allRegions.find((r) => r.code === code);
230
+ if (region) {
231
+ selected.push(region);
232
+ }
233
+ });
234
+ }
235
+
236
+ return selected;
237
+ });
238
+
239
+ let visibleBadges = $derived(tempSelectedRegionObjects.slice(0, maxVisibleBadges));
240
+ let remainingBadgesCount = $derived(
241
+ Math.max(0, tempSelectedRegionObjects.length - maxVisibleBadges)
242
+ );
243
+
244
+ /**
245
+ * Toggle group expansion
246
+ */
247
+ const toggleGroupExpansion = (groupKey: string) => {
248
+ if (expandedGroups.has(groupKey)) {
249
+ expandedGroups.delete(groupKey);
250
+ } else {
251
+ expandedGroups.add(groupKey);
252
+ }
253
+ };
254
+
255
+ /**
256
+ * Check if group is expanded
257
+ */
258
+ const isGroupExpanded = (groupKey: string): boolean => {
259
+ return expandedGroups.has(groupKey);
260
+ };
261
+
262
+ /**
263
+ * Toggle region selection
264
+ */
265
+ const toggleRegion = (code: string) => {
266
+ if (code === 'Global') {
267
+ if (tempSelectedRegions.includes('Global')) {
268
+ tempSelectedRegions = [];
269
+ } else {
270
+ // Clear all and select only Global
271
+ tempSelectedRegions = ['Global'];
272
+ }
273
+ } else if (code === 'EU') {
274
+ if (tempSelectedRegions.includes('EU')) {
275
+ tempSelectedRegions = [];
276
+ } else {
277
+ // Clear all and select only EU
278
+ tempSelectedRegions = ['EU'];
279
+ }
280
+ } else {
281
+ // Selecting a country - clear Global and EU
282
+ const filteredRegions = tempSelectedRegions.filter((c) => c !== 'Global' && c !== 'EU');
283
+
284
+ if (filteredRegions.includes(code)) {
285
+ tempSelectedRegions = filteredRegions.filter((c) => c !== code);
286
+ } else {
287
+ tempSelectedRegions = [...filteredRegions, code];
288
+ }
289
+ }
290
+ };
291
+
292
+ /**
293
+ * Remove region from selection
294
+ */
295
+ const removeRegion = (code: string) => {
296
+ if (code === 'Global' || code === 'EU') {
297
+ tempSelectedRegions = [];
298
+ } else {
299
+ tempSelectedRegions = tempSelectedRegions.filter(
300
+ (c) => c !== code && c !== 'Global' && c !== 'EU'
301
+ );
302
+ }
303
+ onRegionsChange?.(tempSelectedRegions);
304
+ };
305
+
306
+ /**
307
+ * Clear all selections
308
+ */
309
+ const clearAll = () => {
310
+ tempSelectedRegions = [];
311
+ onRegionsChange?.([]);
312
+ };
313
+
314
+ /**
315
+ * Check if all group regions are in use
316
+ */
317
+ const areAllGroupRegionsInUse = (group: RegionGroup): boolean => {
318
+ return group.regions.every((region) => usedRegions.regions.has(region.code));
319
+ };
320
+
321
+ /**
322
+ * Check if all group and sub-group regions are in use
323
+ */
324
+ const areAllGroupAndSubGroupRegionsInUse = (group: ExtendedRegionGroup): boolean => {
325
+ const mainRegionsInUse = group.regions.every((region) => usedRegions.regions.has(region.code));
326
+
327
+ if (!group.subGroups || group.subGroups.length === 0) {
328
+ return mainRegionsInUse;
329
+ }
330
+
331
+ const allSubGroupsInUse = group.subGroups.every((subGroup) =>
332
+ areAllGroupRegionsInUse(subGroup)
333
+ );
334
+
335
+ return mainRegionsInUse && allSubGroupsInUse;
336
+ };
337
+
338
+ /**
339
+ * Check if all group regions are selected
340
+ */
341
+ const isGroupFullySelected = (group: RegionGroup): boolean => {
342
+ return group.regions.every((region) => tempSelectedRegions.includes(region.code));
343
+ };
344
+
345
+ /**
346
+ * Check if some group regions are selected
347
+ */
348
+ const isGroupPartiallySelected = (group: RegionGroup): boolean => {
349
+ const selectedCount = group.regions.filter((region) =>
350
+ tempSelectedRegions.includes(region.code)
351
+ ).length;
352
+ return selectedCount > 0 && selectedCount < group.regions.length;
353
+ };
354
+
355
+ /**
356
+ * Toggle entire group selection
357
+ */
358
+ const toggleGroup = (group: ExtendedRegionGroup) => {
359
+ const hasGlobal = group.regions.some((r) => r.code === 'Global');
360
+ const isEUGroup = group.key === 'europe' || group.parentKey === 'eu-parent';
361
+
362
+ if (hasGlobal) {
363
+ if (tempSelectedRegions.includes('Global')) {
364
+ tempSelectedRegions = [];
365
+ } else {
366
+ // Clear all and select only Global
367
+ tempSelectedRegions = ['Global'];
368
+ }
369
+ return;
370
+ }
371
+
372
+ if (isEUGroup && group.isMainParent) {
373
+ if (tempSelectedRegions.includes('EU')) {
374
+ tempSelectedRegions = [];
375
+ } else {
376
+ // Clear all and select only EU
377
+ tempSelectedRegions = ['EU'];
378
+ }
379
+ return;
380
+ }
381
+
382
+ if (group.isMainParent) {
383
+ // This is the "Select Countries" group
384
+ const allGroupAndSubGroupRegions = [
385
+ ...group.regions.map((r) => r.code),
386
+ ...(group.subGroups?.flatMap((sg) => sg.regions.map((r) => r.code)) || [])
387
+ ];
388
+
389
+ // Filter out regions that are already in use
390
+ const availableRegions = allGroupAndSubGroupRegions.filter(
391
+ (code) => !usedRegions.regions.has(code)
392
+ );
393
+
394
+ // Clear Global and EU when selecting countries
395
+ const currentCountrySelections = tempSelectedRegions.filter(
396
+ (code) => code !== 'Global' && code !== 'EU'
397
+ );
398
+
399
+ // Check if all available (non-in-use) regions are selected
400
+ const allAvailableSelected = availableRegions.every((code) =>
401
+ currentCountrySelections.includes(code)
402
+ );
403
+
404
+ if (allAvailableSelected) {
405
+ // Deselect all available countries
406
+ tempSelectedRegions = currentCountrySelections.filter(
407
+ (code) => !availableRegions.includes(code)
408
+ );
409
+ } else {
410
+ // Select all available countries (excluding Global, EU, and in-use regions)
411
+ tempSelectedRegions = [...new Set([...currentCountrySelections, ...availableRegions])];
412
+ }
413
+ return;
414
+ }
415
+
416
+ // Sub-group toggle (like United States)
417
+ const isFullySelected = isGroupFullySelected(group);
418
+ const groupCodes = group.regions.map((r) => r.code);
419
+
420
+ // Filter out regions that are already in use
421
+ const availableGroupCodes = groupCodes.filter((code) => !usedRegions.regions.has(code));
422
+
423
+ // Clear Global and EU when selecting countries
424
+ const currentCountrySelections = tempSelectedRegions.filter(
425
+ (code) => code !== 'Global' && code !== 'EU'
426
+ );
427
+
428
+ if (isFullySelected) {
429
+ tempSelectedRegions = currentCountrySelections.filter((code) => !groupCodes.includes(code));
430
+ } else {
431
+ // Only select available (non-in-use) regions
432
+ tempSelectedRegions = [...new Set([...currentCountrySelections, ...availableGroupCodes])];
433
+ }
434
+ };
435
+
436
+ /**
437
+ * Apply selections and trigger callback
438
+ */
439
+ const applySelections = () => {
440
+ onRegionsChange?.(tempSelectedRegions);
441
+ };
442
+ </script>
443
+
444
+ <div class="region-selector {className}" {...restProps}>
445
+ <div class="search-actions-row">
446
+ <div class="search-section">
447
+ <input
448
+ type="text"
449
+ class="search-input"
450
+ placeholder={searchPlaceholder}
451
+ bind:value={searchQuery}
452
+ />
453
+ </div>
454
+ {#if tempSelectedRegions.length > 0}
455
+ <button type="button" class="clear-btn" onclick={clearAll}>Clear all</button>
456
+ {/if}
457
+ </div>
458
+
459
+ {#if tempSelectedRegions.length > 0}
460
+ <div class="selection-display">
461
+ <div class="selected-items">
462
+ {#each visibleBadges as region (region.code)}
463
+ <span class="selected-badge">
464
+ {region.name}
465
+ <span
466
+ role="button"
467
+ tabindex="0"
468
+ class="remove-btn"
469
+ onclick={(e) => {
470
+ e.stopPropagation();
471
+ removeRegion(region.code);
472
+ }}
473
+ onkeydown={(e) => {
474
+ if (e.key === 'Enter' || e.key === ' ') {
475
+ e.preventDefault();
476
+ e.stopPropagation();
477
+ removeRegion(region.code);
478
+ }
479
+ }}
480
+ >
481
+ &times;
482
+ </span>
483
+ </span>
484
+ {/each}
485
+ {#if remainingBadgesCount > 0}
486
+ <span class="more-badge"> + {remainingBadgesCount} more </span>
487
+ {/if}
488
+ </div>
489
+ </div>
490
+ {/if}
491
+
492
+ <div class="options-list">
493
+ {#if isLoading}
494
+ <div class="empty-state">
495
+ <span class="empty-text">Loading regions...</span>
496
+ </div>
497
+ {:else if error}
498
+ <div class="empty-state">
499
+ <span class="empty-text error">Failed to load regions</span>
500
+ <span class="empty-text-sub">{error}</span>
501
+ </div>
502
+ {:else if filteredGroups.length === 0}
503
+ <div class="empty-state">
504
+ <span class="empty-text">No regions found</span>
505
+ {#if searchQuery}
506
+ <span class="empty-text-sub">Try searching with different terms</span>
507
+ {/if}
508
+ </div>
509
+ {:else}
510
+ {#each filteredGroups as group (group.key)}
511
+ {@const isMainParent = group.isMainParent}
512
+ {@const isThisMainParentSelected = selectedMainParent === group.parentKey}
513
+ {@const hasGlobal = group.regions.some((r) => r.code === 'Global')}
514
+ {@const isEUGroup = group.key === 'europe' || group.parentKey === 'eu-parent'}
515
+ {@const isNonExpandable = hasGlobal || isEUGroup}
516
+ {@const isGlobalGroupUsed = hasGlobal && usedRegions.isGlobalUsed}
517
+ {@const isEUGroupUsed = isEUGroup && usedRegions.isEUGroupUsed}
518
+ {@const allGroupRegionsInUse = areAllGroupAndSubGroupRegionsInUse(group)}
519
+ {@const isGroupUsedByOther = isGlobalGroupUsed || isEUGroupUsed || allGroupRegionsInUse}
520
+ {@const isGroupDisabled = isGroupUsedByOther}
521
+
522
+ <div class="option-group" class:disabled={isGroupDisabled} class:main-parent={isMainParent}>
523
+ {#if isMainParent}
524
+ <button
525
+ type="button"
526
+ class="group-label main-parent-label"
527
+ class:fully-selected={isThisMainParentSelected}
528
+ class:expanded={isGroupExpanded(group.key)}
529
+ class:disabled={isGroupDisabled}
530
+ class:in-use={isGroupUsedByOther}
531
+ onclick={() => {
532
+ if (isGroupDisabled) return;
533
+
534
+ if (isNonExpandable) {
535
+ toggleGroup(group);
536
+ } else {
537
+ toggleGroupExpansion(group.key);
538
+ }
539
+ }}
540
+ onkeydown={(e) => {
541
+ if (e.key === 'Enter' || e.key === ' ') {
542
+ e.preventDefault();
543
+ if (isGroupDisabled) return;
544
+
545
+ if (isNonExpandable) {
546
+ toggleGroup(group);
547
+ } else {
548
+ toggleGroupExpansion(group.key);
549
+ }
550
+ }
551
+ }}
552
+ disabled={isGroupDisabled}
553
+ >
554
+ <div
555
+ role="radio"
556
+ tabindex="-1"
557
+ aria-checked={isThisMainParentSelected}
558
+ onclick={(e) => {
559
+ if (!isNonExpandable) {
560
+ e.stopPropagation();
561
+ if (!isGroupDisabled) {
562
+ toggleGroup(group);
563
+ if (!isGroupExpanded(group.key)) {
564
+ toggleGroupExpansion(group.key);
565
+ }
566
+ }
567
+ }
568
+ }}
569
+ onkeydown={(e) => {
570
+ if (!isNonExpandable && (e.key === 'Enter' || e.key === ' ')) {
571
+ e.preventDefault();
572
+ e.stopPropagation();
573
+ if (!isGroupDisabled) {
574
+ toggleGroup(group);
575
+ if (!isGroupExpanded(group.key)) {
576
+ toggleGroupExpansion(group.key);
577
+ }
578
+ }
579
+ }
580
+ }}
581
+ >
582
+ <Checkbox
583
+ checked={isThisMainParentSelected}
584
+ variant="radio"
585
+ disabled={isGroupDisabled}
586
+ />
587
+ </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
+ {#if !isNonExpandable}
595
+ <span class="group-chevron">
596
+ <svg viewBox="0 0 20 20" fill="currentColor">
597
+ <path
598
+ fill-rule="evenodd"
599
+ d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
600
+ clip-rule="evenodd"
601
+ />
602
+ </svg>
603
+ </span>
604
+ {/if}
605
+ </button>
606
+ {/if}
607
+
608
+ {#if !isMainParent || isGroupExpanded(group.key)}
609
+ {#if group.subGroups && group.subGroups.length > 0}
610
+ {#each group.subGroups as subGroup (subGroup.key)}
611
+ {@const allSubGroupRegionsInUse = areAllGroupRegionsInUse(subGroup)}
612
+ {@const isSubGroupDisabled = allSubGroupRegionsInUse}
613
+ {@const isSubGroupFullySelected = isGroupFullySelected(subGroup)}
614
+ {@const isSubGroupPartiallySelected = isGroupPartiallySelected(subGroup)}
615
+ {@const isSubGroupExpanded = isGroupExpanded(subGroup.key)}
616
+
617
+ <div class="sub-group">
618
+ <button
619
+ type="button"
620
+ class="group-label sub-group-label"
621
+ class:fully-selected={isSubGroupFullySelected}
622
+ class:partially-selected={isSubGroupPartiallySelected}
623
+ class:expanded={isSubGroupExpanded}
624
+ class:disabled={isSubGroupDisabled}
625
+ class:in-use={allSubGroupRegionsInUse}
626
+ onclick={() => {
627
+ if (!isSubGroupDisabled) {
628
+ toggleGroupExpansion(subGroup.key);
629
+ }
630
+ }}
631
+ disabled={isSubGroupDisabled}
632
+ >
633
+ <div
634
+ role="checkbox"
635
+ tabindex="0"
636
+ aria-checked={isSubGroupFullySelected
637
+ ? 'true'
638
+ : isSubGroupPartiallySelected
639
+ ? 'mixed'
640
+ : 'false'}
641
+ onclick={(e) => {
642
+ e.stopPropagation();
643
+ if (!isSubGroupDisabled) {
644
+ toggleGroup(subGroup);
645
+ }
646
+ }}
647
+ onkeydown={(e) => {
648
+ if (e.key === 'Enter' || e.key === ' ') {
649
+ e.preventDefault();
650
+ e.stopPropagation();
651
+ if (!isSubGroupDisabled) {
652
+ toggleGroup(subGroup);
653
+ }
654
+ }
655
+ }}
656
+ >
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}
666
+ </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
+ <span class="group-chevron">
674
+ <svg viewBox="0 0 20 20" fill="currentColor">
675
+ <path
676
+ fill-rule="evenodd"
677
+ d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
678
+ clip-rule="evenodd"
679
+ />
680
+ </svg>
681
+ </span>
682
+ </button>
683
+
684
+ {#if isSubGroupExpanded}
685
+ {#each subGroup.regions as region (region.code)}
686
+ {@const isRegionInUse = usedRegions.regions.has(region.code)}
687
+ {@const isRegionDisabled = isRegionInUse}
688
+
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}
712
+ </span>
713
+ </span>
714
+ </button>
715
+ {/each}
716
+ {/if}
717
+ </div>
718
+ {/each}
719
+ {/if}
720
+
721
+ {#if !isMainParent || group.regions.length > 0}
722
+ {#each group.regions as region (region.code)}
723
+ {@const isRegionInUse = usedRegions.regions.has(region.code)}
724
+ {@const isRegionDisabled = isRegionInUse}
725
+
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}
750
+ </span>
751
+ </span>
752
+ </button>
753
+ {/each}
754
+ {/if}
755
+ {/if}
756
+ </div>
757
+ {/each}
758
+ {/if}
759
+ </div>
760
+ </div>
761
+
762
+ <style>
763
+ .region-selector {
764
+ display: flex;
765
+ flex-direction: column;
766
+ gap: 12px;
767
+ height: 100%;
768
+ }
769
+
770
+ .search-actions-row {
771
+ display: flex;
772
+ gap: 8px;
773
+ align-items: flex-start;
774
+ position: sticky;
775
+ top: 0;
776
+ background: var(--background2);
777
+ z-index: 10;
778
+ }
779
+
780
+ .search-section {
781
+ flex: 1;
782
+ min-width: 0;
783
+ }
784
+
785
+ .search-input {
786
+ width: 100%;
787
+ padding: 6px 10px;
788
+ background: var(--background3);
789
+ border: 1px solid var(--border1);
790
+ border-radius: 4px;
791
+ color: var(--text1);
792
+ font-size: 11.5px;
793
+ }
794
+
795
+ .search-input:focus {
796
+ outline: none;
797
+ border-color: var(--actionPrimaryBackground);
798
+ }
799
+
800
+ .clear-btn {
801
+ padding: 6px 12px;
802
+ background: var(--background3);
803
+ border: 1px solid var(--border1);
804
+ border-radius: 4px;
805
+ font-size: 11px;
806
+ cursor: pointer;
807
+ color: var(--text1);
808
+ white-space: nowrap;
809
+ flex-shrink: 0;
810
+ }
811
+
812
+ .clear-btn:hover {
813
+ background: var(--background3);
814
+ border-color: var(--blueBorder);
815
+ }
816
+
817
+ .selection-display {
818
+ padding: 8px 0;
819
+ }
820
+
821
+ .selected-items {
822
+ display: flex;
823
+ flex-wrap: wrap;
824
+ gap: 6px;
825
+ }
826
+
827
+ .selected-badge {
828
+ display: inline-flex;
829
+ align-items: center;
830
+ gap: 4px;
831
+ padding: 4px 8px;
832
+ background: var(--background3);
833
+ border: 1px solid var(--border1);
834
+ border-radius: 4px;
835
+ font-size: 11px;
836
+ }
837
+
838
+ .selected-badge:hover {
839
+ background: var(--background3);
840
+ }
841
+
842
+ .more-badge {
843
+ display: inline-flex;
844
+ align-items: center;
845
+ padding: 4px 8px;
846
+ background: var(--background3);
847
+ border: 1px solid var(--border1);
848
+ border-radius: 4px;
849
+ font-size: 11px;
850
+ color: var(--text2);
851
+ font-weight: 500;
852
+ }
853
+
854
+ .remove-btn {
855
+ display: flex;
856
+ align-items: end;
857
+ justify-content: center;
858
+ padding: 0;
859
+ width: 14px;
860
+ height: 14px;
861
+ background: none;
862
+ border: none;
863
+ border-radius: 3px;
864
+ cursor: pointer;
865
+ font-size: 1rem;
866
+ line-height: 1;
867
+ color: var(--text3);
868
+ }
869
+
870
+ .options-list {
871
+ overflow-y: auto;
872
+ padding-right: 4px;
873
+ flex: 1;
874
+ max-height: 300px;
875
+ }
876
+
877
+ .empty-state {
878
+ display: flex;
879
+ flex-direction: column;
880
+ align-items: center;
881
+ justify-content: center;
882
+ gap: 4px;
883
+ padding: 40px 20px;
884
+ text-align: center;
885
+ }
886
+
887
+ .empty-text {
888
+ font-size: 11px;
889
+ color: var(--text2);
890
+ }
891
+
892
+ .empty-text.error {
893
+ color: var(--redText);
894
+ }
895
+
896
+ .empty-text-sub {
897
+ font-size: 11px;
898
+ color: var(--text3);
899
+ }
900
+
901
+ .option-group {
902
+ margin-bottom: 8px;
903
+ }
904
+
905
+ .option-group:last-child {
906
+ margin-bottom: 0;
907
+ }
908
+
909
+ .option-group.main-parent:last-child {
910
+ border-bottom: none;
911
+ }
912
+
913
+ .sub-group {
914
+ margin-bottom: 4px;
915
+ }
916
+
917
+ .group-label {
918
+ width: 100%;
919
+ display: flex;
920
+ align-items: center;
921
+ gap: 10px;
922
+ padding: 8px;
923
+ background: none;
924
+ border: none;
925
+ border-radius: 4px;
926
+ cursor: pointer;
927
+ text-align: left;
928
+ margin-bottom: 4px;
929
+ }
930
+
931
+ .group-label:hover {
932
+ background: var(--background3);
933
+ }
934
+
935
+ .group-label.partially-selected {
936
+ background: var(--background3);
937
+ }
938
+
939
+ .main-parent-label:hover {
940
+ background: var(--background3);
941
+ }
942
+
943
+ .main-parent-label.fully-selected {
944
+ background: var(--background3);
945
+ }
946
+
947
+ .sub-group-label {
948
+ padding: 6px 12px;
949
+ width: calc(100% - 28px);
950
+ margin-left: 20px;
951
+ }
952
+
953
+ .group-label-text {
954
+ flex: 1;
955
+ font-size: 11px;
956
+ font-weight: 600;
957
+ text-transform: uppercase;
958
+ letter-spacing: 0.05em;
959
+ color: var(--text2);
960
+ display: flex;
961
+ align-items: center;
962
+ gap: 6px;
963
+ }
964
+
965
+ .main-parent-label .group-label-text {
966
+ font-size: 12px;
967
+ font-weight: 700;
968
+ color: var(--text1);
969
+ }
970
+
971
+ .sub-group-text {
972
+ font-size: 10.5px;
973
+ font-weight: 500;
974
+ text-transform: none;
975
+ }
976
+
977
+ .group-chevron {
978
+ display: flex;
979
+ align-items: center;
980
+ justify-content: center;
981
+ width: 16px;
982
+ height: 16px;
983
+ margin-left: auto;
984
+ flex-shrink: 0;
985
+ color: var(--text3);
986
+ }
987
+
988
+ .group-label.expanded .group-chevron {
989
+ transform: rotate(180deg);
990
+ }
991
+
992
+ .group-chevron svg {
993
+ width: 14px;
994
+ height: 14px;
995
+ }
996
+
997
+ .group-checkbox {
998
+ width: 18px;
999
+ height: 18px;
1000
+ cursor: pointer;
1001
+ }
1002
+
1003
+ .group-checkbox:hover {
1004
+ opacity: 0.8;
1005
+ }
1006
+
1007
+ .option-item {
1008
+ width: 100%;
1009
+ display: flex;
1010
+ align-items: center;
1011
+ gap: 10px;
1012
+ padding: 8px 12px;
1013
+ background: none;
1014
+ border: none;
1015
+ border-radius: 4px;
1016
+ cursor: pointer;
1017
+ text-align: left;
1018
+ }
1019
+
1020
+ .option-item:hover {
1021
+ background: var(--background3);
1022
+ }
1023
+
1024
+ .option-item.indented {
1025
+ width: calc(100% - 28px);
1026
+ margin-left: 20px;
1027
+ }
1028
+
1029
+ .sub-group-item {
1030
+ width: calc(100% - 56px);
1031
+ margin-left: 56px;
1032
+ }
1033
+
1034
+ .option-item :global(.checkbox),
1035
+ .group-label :global(.checkbox) {
1036
+ flex-shrink: 0;
1037
+ }
1038
+
1039
+ .partially-selected-box {
1040
+ width: 16px;
1041
+ height: 16px;
1042
+ flex-shrink: 0;
1043
+ border: 2px solid var(--border1);
1044
+ border-radius: 4px;
1045
+ display: flex;
1046
+ align-items: center;
1047
+ justify-content: center;
1048
+ background: var(--actionPrimaryBackground);
1049
+ border-color: var(--actionPrimaryBackground);
1050
+ }
1051
+
1052
+ .partially-selected-box svg {
1053
+ width: 14px;
1054
+ height: 14px;
1055
+ color: white;
1056
+ }
1057
+
1058
+ .option-item.disabled,
1059
+ .group-label.disabled,
1060
+ .option-group.disabled {
1061
+ opacity: 0.5;
1062
+ cursor: not-allowed;
1063
+ }
1064
+
1065
+ .option-item.disabled:hover,
1066
+ .group-label.disabled:hover {
1067
+ background: none;
1068
+ }
1069
+
1070
+ .option-label {
1071
+ flex: 1;
1072
+ font-size: 11.5px;
1073
+ display: flex;
1074
+ justify-content: space-between;
1075
+ align-items: center;
1076
+ gap: 8px;
1077
+ color: var(--text2);
1078
+ }
1079
+
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;
1087
+ }
1088
+
1089
+ .in-use-badge {
1090
+ 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;
1098
+ }
1099
+
1100
+ .option-item.in-use,
1101
+ .group-label.in-use {
1102
+ opacity: 0.6;
1103
+ cursor: not-allowed;
1104
+ }
1105
+
1106
+ .option-item.in-use:hover,
1107
+ .group-label.in-use:hover {
1108
+ background: none;
1109
+ }
1110
+ </style>