@makolabs/ripple 3.0.1 → 3.0.2
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.
- package/dist/elements/accordion/Accordion.svelte +1 -1
- package/dist/elements/combobox/ComboBox.svelte +50 -17
- package/dist/elements/dropdown/Select.svelte +98 -62
- package/dist/elements/popover/Popover.svelte +48 -45
- package/dist/filters/CompactFilters.svelte +1 -1
- package/dist/forms/DateRange.svelte +217 -200
- package/dist/forms/NumberInput.svelte +148 -39
- package/dist/forms/SegmentedControl.svelte +1 -1
- package/dist/forms/Toggle.svelte +18 -18
- package/dist/forms/calendar/Calendar.svelte +3 -1
- package/dist/forms/form-types.d.ts +28 -0
- package/dist/forms/month-picker/MonthPicker.svelte +299 -0
- package/dist/forms/month-picker/MonthPicker.svelte.d.ts +4 -0
- package/dist/forms/month-picker/month-picker-types.d.ts +22 -0
- package/dist/forms/month-picker/month-picker-types.js +1 -0
- package/dist/forms/segmented-control.d.ts +2 -2
- package/dist/forms/segmented-control.js +7 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/layout/activity-list/ActivityList.svelte +1 -1
- package/package.json +1 -1
|
@@ -47,9 +47,7 @@
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const filteredItems = $derived(
|
|
50
|
-
onsearch
|
|
51
|
-
? items // when server-side, parent controls the filtered list
|
|
52
|
-
: items.filter((item) => (filter ?? defaultFilter)(item, query))
|
|
50
|
+
onsearch ? items : items.filter((item) => (filter ?? defaultFilter)(item, debouncedQuery))
|
|
53
51
|
);
|
|
54
52
|
|
|
55
53
|
// Reset highlight when list shrinks
|
|
@@ -62,6 +60,10 @@
|
|
|
62
60
|
|
|
63
61
|
function openMenu() {
|
|
64
62
|
if (disabled) return;
|
|
63
|
+
if (skipNextFocusOpen) {
|
|
64
|
+
skipNextFocusOpen = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
65
67
|
open = true;
|
|
66
68
|
query = '';
|
|
67
69
|
}
|
|
@@ -78,21 +80,35 @@
|
|
|
78
80
|
closeMenu();
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
let skipNextFocusOpen = false;
|
|
84
|
+
|
|
81
85
|
function clear(e: MouseEvent) {
|
|
82
86
|
e.stopPropagation();
|
|
83
87
|
value = null;
|
|
84
88
|
onchange?.(null);
|
|
89
|
+
skipNextFocusOpen = true;
|
|
85
90
|
inputEl?.focus();
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
94
|
+
let debouncedQuery = $state('');
|
|
95
|
+
|
|
88
96
|
function handleInput(e: Event) {
|
|
89
97
|
const v = (e.currentTarget as HTMLInputElement).value;
|
|
90
98
|
query = v;
|
|
91
99
|
open = true;
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
clearTimeout(debounceTimer);
|
|
101
|
+
debounceTimer = setTimeout(() => {
|
|
102
|
+
debouncedQuery = v;
|
|
103
|
+
highlightedIndex = 0;
|
|
104
|
+
onsearch?.(v);
|
|
105
|
+
}, 1000);
|
|
94
106
|
}
|
|
95
107
|
|
|
108
|
+
$effect(() => {
|
|
109
|
+
return () => clearTimeout(debounceTimer);
|
|
110
|
+
});
|
|
111
|
+
|
|
96
112
|
function handleKey(e: KeyboardEvent) {
|
|
97
113
|
if (disabled) return;
|
|
98
114
|
if (e.key === 'ArrowDown') {
|
|
@@ -169,7 +185,7 @@
|
|
|
169
185
|
type="button"
|
|
170
186
|
onclick={clear}
|
|
171
187
|
aria-label="Clear selection"
|
|
172
|
-
class="text-default-400 hover:text-default-700 flex size-5 items-center justify-center rounded"
|
|
188
|
+
class="text-default-400 hover:text-default-700 flex size-5 cursor-pointer items-center justify-center rounded"
|
|
173
189
|
>
|
|
174
190
|
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
175
191
|
<path
|
|
@@ -181,18 +197,35 @@
|
|
|
181
197
|
</svg>
|
|
182
198
|
</button>
|
|
183
199
|
{/if}
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onclick={(e) => {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
if (!disabled) {
|
|
205
|
+
if (open) closeMenu();
|
|
206
|
+
else openMenu();
|
|
207
|
+
}
|
|
208
|
+
}}
|
|
209
|
+
class={cn(
|
|
210
|
+
'text-default-400 hover:text-default-700 flex cursor-pointer items-center justify-center rounded',
|
|
211
|
+
!disabled && 'hover:text-default-600'
|
|
212
|
+
)}
|
|
213
|
+
aria-label={open ? 'Close suggestions' : 'Open suggestions'}
|
|
214
|
+
{disabled}
|
|
189
215
|
>
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
<svg
|
|
217
|
+
class={cn('size-4 transition-transform', open && 'rotate-180')}
|
|
218
|
+
viewBox="0 0 20 20"
|
|
219
|
+
fill="currentColor"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
>
|
|
222
|
+
<path
|
|
223
|
+
fill-rule="evenodd"
|
|
224
|
+
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06z"
|
|
225
|
+
clip-rule="evenodd"
|
|
226
|
+
/>
|
|
227
|
+
</svg>
|
|
228
|
+
</button>
|
|
196
229
|
</div>
|
|
197
230
|
|
|
198
231
|
{#snippet content()}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick } from 'svelte';
|
|
2
|
+
import { tick, onMount } from 'svelte';
|
|
3
3
|
import { cn } from '../../helper/cls.js';
|
|
4
4
|
import { buildTestId } from '../../helper/testid.js';
|
|
5
5
|
import { selectTV } from './select.js';
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
import { Size } from '../../variants.js';
|
|
11
11
|
import Portal from '../../utils/Portal.svelte';
|
|
12
12
|
|
|
13
|
+
let isMobile = $state(
|
|
14
|
+
typeof window !== 'undefined' && window.matchMedia('(max-width: 639.98px)').matches
|
|
15
|
+
);
|
|
16
|
+
onMount(() => {
|
|
17
|
+
const mql = window.matchMedia('(max-width: 639.98px)');
|
|
18
|
+
const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
|
|
19
|
+
mql.addEventListener('change', handler);
|
|
20
|
+
return () => mql.removeEventListener('change', handler);
|
|
21
|
+
});
|
|
22
|
+
|
|
13
23
|
let {
|
|
14
24
|
items = [],
|
|
15
25
|
value = $bindable(''),
|
|
@@ -402,7 +412,7 @@
|
|
|
402
412
|
{/each}
|
|
403
413
|
{/if}
|
|
404
414
|
|
|
405
|
-
{#if open}
|
|
415
|
+
{#if open && !isMobile}
|
|
406
416
|
<Portal target={labelRef}>
|
|
407
417
|
<div
|
|
408
418
|
class={containerClass_}
|
|
@@ -411,66 +421,92 @@
|
|
|
411
421
|
data-testid={buildTestId('select', 'list', testId)}
|
|
412
422
|
transition:fly={{ y: -8, duration: 300, easing: quintOut }}
|
|
413
423
|
>
|
|
414
|
-
{
|
|
415
|
-
<div class={searchInputClass_}>
|
|
416
|
-
<svg
|
|
417
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
418
|
-
width="12"
|
|
419
|
-
height="12"
|
|
420
|
-
viewBox="0 0 12 12"
|
|
421
|
-
class="size-4"
|
|
422
|
-
>
|
|
423
|
-
<path
|
|
424
|
-
fill="currentColor"
|
|
425
|
-
d="M5 1a4 4 0 1 0 2.452 7.16l2.694 2.693a.5.5 0 1 0 .707-.707L8.16 7.453A4 4 0 0 0 5 1M2 5a3 3 0 1 1 6 0a3 3 0 0 1-6 0"
|
|
426
|
-
/>
|
|
427
|
-
</svg>
|
|
428
|
-
<input
|
|
429
|
-
bind:this={searchInputRef}
|
|
430
|
-
bind:value={searchQuery}
|
|
431
|
-
type="text"
|
|
432
|
-
class="w-full ring-0 outline-0"
|
|
433
|
-
placeholder="Search..."
|
|
434
|
-
aria-label="Search select options"
|
|
435
|
-
oninput={() => (hasSearched = true)}
|
|
436
|
-
data-testid={buildTestId('select', 'search', testId)}
|
|
437
|
-
/>
|
|
438
|
-
</div>
|
|
439
|
-
{/if}
|
|
440
|
-
|
|
441
|
-
{#if asyncLoading}
|
|
442
|
-
<div class={emptyMessageClass} data-select-loading="">{loadingText}</div>
|
|
443
|
-
{:else if filteredItems.length === 0}
|
|
444
|
-
<div class={emptyMessageClass}>{emptyText}</div>
|
|
445
|
-
{:else if groupedItems}
|
|
446
|
-
<ul class={listClass_}>
|
|
447
|
-
{#each groupedItems as group, gIdx (group.label ?? `__null__${gIdx}`)}
|
|
448
|
-
{#if group.label !== null}
|
|
449
|
-
<li
|
|
450
|
-
class="text-default-500 bg-default-50 px-3 py-1.5 text-xs font-semibold tracking-wide uppercase"
|
|
451
|
-
role="presentation"
|
|
452
|
-
data-select-group={group.label}
|
|
453
|
-
>
|
|
454
|
-
{group.label}
|
|
455
|
-
</li>
|
|
456
|
-
{/if}
|
|
457
|
-
{#each group.items as groupItem (groupItem.value)}
|
|
458
|
-
{@const flatIdx = filteredItems.indexOf(groupItem)}
|
|
459
|
-
{@const selected = valueArray.includes(groupItem.value)}
|
|
460
|
-
{@const highlighted = flatIdx === highlightedIndex}
|
|
461
|
-
{@render itemButton(groupItem, flatIdx, selected, highlighted)}
|
|
462
|
-
{/each}
|
|
463
|
-
{/each}
|
|
464
|
-
</ul>
|
|
465
|
-
{:else}
|
|
466
|
-
<ul class={listClass_}>
|
|
467
|
-
{#each filteredItems as flatItem, index (flatItem.value)}
|
|
468
|
-
{@const selected = valueArray.includes(flatItem.value)}
|
|
469
|
-
{@const highlighted = index === highlightedIndex}
|
|
470
|
-
{@render itemButton(flatItem, index, selected, highlighted)}
|
|
471
|
-
{/each}
|
|
472
|
-
</ul>
|
|
473
|
-
{/if}
|
|
424
|
+
{@render selectListContent()}
|
|
474
425
|
</div>
|
|
475
426
|
</Portal>
|
|
476
427
|
{/if}
|
|
428
|
+
|
|
429
|
+
{#if open && isMobile}
|
|
430
|
+
<button
|
|
431
|
+
type="button"
|
|
432
|
+
class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
|
|
433
|
+
aria-label="Close"
|
|
434
|
+
onclick={() => (open = false)}
|
|
435
|
+
></button>
|
|
436
|
+
<div
|
|
437
|
+
class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
|
|
438
|
+
role="listbox"
|
|
439
|
+
aria-labelledby="{selectId}-label"
|
|
440
|
+
transition:fly={{ y: 300, duration: 200, easing: quintOut }}
|
|
441
|
+
>
|
|
442
|
+
<div class="flex justify-center py-2">
|
|
443
|
+
<div class="bg-default-300 h-1 w-8 rounded-full"></div>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="flex-1 cursor-pointer overflow-y-auto">
|
|
446
|
+
{@render selectListContent()}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
{/if}
|
|
450
|
+
|
|
451
|
+
{#snippet selectListContent()}
|
|
452
|
+
{#if isSearchable}
|
|
453
|
+
<div class={searchInputClass_}>
|
|
454
|
+
<svg
|
|
455
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
456
|
+
width="12"
|
|
457
|
+
height="12"
|
|
458
|
+
viewBox="0 0 12 12"
|
|
459
|
+
class="size-4"
|
|
460
|
+
>
|
|
461
|
+
<path
|
|
462
|
+
fill="currentColor"
|
|
463
|
+
d="M5 1a4 4 0 1 0 2.452 7.16l2.694 2.693a.5.5 0 1 0 .707-.707L8.16 7.453A4 4 0 0 0 5 1M2 5a3 3 0 1 1 6 0a3 3 0 0 1-6 0"
|
|
464
|
+
/>
|
|
465
|
+
</svg>
|
|
466
|
+
<input
|
|
467
|
+
bind:this={searchInputRef}
|
|
468
|
+
bind:value={searchQuery}
|
|
469
|
+
type="text"
|
|
470
|
+
class="w-full ring-0 outline-0"
|
|
471
|
+
placeholder="Search..."
|
|
472
|
+
aria-label="Search select options"
|
|
473
|
+
oninput={() => (hasSearched = true)}
|
|
474
|
+
data-testid={buildTestId('select', 'search', testId)}
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
{/if}
|
|
478
|
+
|
|
479
|
+
{#if asyncLoading}
|
|
480
|
+
<div class={emptyMessageClass} data-select-loading="">{loadingText}</div>
|
|
481
|
+
{:else if filteredItems.length === 0}
|
|
482
|
+
<div class={emptyMessageClass}>{emptyText}</div>
|
|
483
|
+
{:else if groupedItems}
|
|
484
|
+
<ul class={listClass_}>
|
|
485
|
+
{#each groupedItems as group, gIdx (group.label ?? `__null__${gIdx}`)}
|
|
486
|
+
{#if group.label !== null}
|
|
487
|
+
<li
|
|
488
|
+
class="text-default-500 bg-default-50 px-3 py-1.5 text-xs font-semibold tracking-wide uppercase"
|
|
489
|
+
role="presentation"
|
|
490
|
+
data-select-group={group.label}
|
|
491
|
+
>
|
|
492
|
+
{group.label}
|
|
493
|
+
</li>
|
|
494
|
+
{/if}
|
|
495
|
+
{#each group.items as groupItem (groupItem.value)}
|
|
496
|
+
{@const flatIdx = filteredItems.indexOf(groupItem)}
|
|
497
|
+
{@const selected = valueArray.includes(groupItem.value)}
|
|
498
|
+
{@const highlighted = flatIdx === highlightedIndex}
|
|
499
|
+
{@render itemButton(groupItem, flatIdx, selected, highlighted)}
|
|
500
|
+
{/each}
|
|
501
|
+
{/each}
|
|
502
|
+
</ul>
|
|
503
|
+
{:else}
|
|
504
|
+
<ul class={listClass_}>
|
|
505
|
+
{#each filteredItems as flatItem, index (flatItem.value)}
|
|
506
|
+
{@const selected = valueArray.includes(flatItem.value)}
|
|
507
|
+
{@const highlighted = index === highlightedIndex}
|
|
508
|
+
{@render itemButton(flatItem, index, selected, highlighted)}
|
|
509
|
+
{/each}
|
|
510
|
+
</ul>
|
|
511
|
+
{/if}
|
|
512
|
+
{/snippet}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import { fly } from 'svelte/transition';
|
|
5
|
+
import { quintOut } from 'svelte/easing';
|
|
6
|
+
import { onMount } from 'svelte';
|
|
4
7
|
import type { PopoverProps, PopoverPlacement } from './popover-types.js';
|
|
5
8
|
|
|
6
9
|
let {
|
|
@@ -25,13 +28,25 @@
|
|
|
25
28
|
let showTimer: ReturnType<typeof setTimeout> | undefined;
|
|
26
29
|
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
27
30
|
|
|
28
|
-
// Panel position in viewport coordinates — updated on open and on
|
|
29
|
-
// scroll/resize so the panel tracks the trigger. Kept reactive so the
|
|
30
|
-
// style attribute re-renders when these change.
|
|
31
31
|
let panelTop = $state(0);
|
|
32
32
|
let panelLeft = $state(0);
|
|
33
33
|
let panelTransform = $state('');
|
|
34
34
|
|
|
35
|
+
// Mobile detection — below `sm` (640px), click/manual popovers
|
|
36
|
+
// render as a bottom sheet instead of a positioned dropdown.
|
|
37
|
+
// Hover popovers are skipped on mobile (no hover).
|
|
38
|
+
let isMobile = $state(
|
|
39
|
+
typeof window !== 'undefined' && window.matchMedia('(max-width: 639.98px)').matches
|
|
40
|
+
);
|
|
41
|
+
onMount(() => {
|
|
42
|
+
const mql = window.matchMedia('(max-width: 639.98px)');
|
|
43
|
+
const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
|
|
44
|
+
mql.addEventListener('change', handler);
|
|
45
|
+
return () => mql.removeEventListener('change', handler);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const useSheet = $derived(isMobile && trigger !== 'hover');
|
|
49
|
+
|
|
35
50
|
function show() {
|
|
36
51
|
if (disabled) return;
|
|
37
52
|
clearTimeout(showTimer);
|
|
@@ -65,7 +80,6 @@
|
|
|
65
80
|
open = false;
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
// Clean up any in-flight timers on unmount.
|
|
69
83
|
$effect(() => {
|
|
70
84
|
return () => {
|
|
71
85
|
clearTimeout(showTimer);
|
|
@@ -79,23 +93,12 @@
|
|
|
79
93
|
|
|
80
94
|
function handleWindowClick(e: MouseEvent) {
|
|
81
95
|
if (!closeOnOutsideClick || !open) return;
|
|
96
|
+
if (useSheet) return;
|
|
82
97
|
const target = e.target as Node;
|
|
83
|
-
// The panel lives outside the trigger wrapper in the DOM, so check both.
|
|
84
98
|
if (wrapper?.contains(target) || panelEl?.contains(target)) return;
|
|
85
99
|
close();
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
/**
|
|
89
|
-
* Compute panel position from the trigger's bounding rect. Uses
|
|
90
|
-
* `position: fixed` + a very high z-index so the panel sits above
|
|
91
|
-
* sibling content (Storybook sidebars, sticky headers, etc.) instead
|
|
92
|
-
* of being clipped / layered under it.
|
|
93
|
-
*
|
|
94
|
-
* The computed position is then clamped to the viewport with an 8px
|
|
95
|
-
* gutter — this keeps panels on-screen near viewport edges on narrow
|
|
96
|
-
* devices. We measure the panel's own rect so the clamp accounts for
|
|
97
|
-
* the `translate(-50%, 0)` etc. transforms that re-anchor the panel.
|
|
98
|
-
*/
|
|
99
102
|
function updatePosition() {
|
|
100
103
|
if (!wrapper || !panelEl) return;
|
|
101
104
|
const r = wrapper.getBoundingClientRect();
|
|
@@ -124,10 +127,6 @@
|
|
|
124
127
|
break;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
// Clamp to viewport. We re-measure the panel after applying the
|
|
128
|
-
// transform (next frame) because its true on-screen rect depends
|
|
129
|
-
// on `panelTransform`. Adjust `panelLeft`/`panelTop` so the rect
|
|
130
|
-
// ends up inside [VIEWPORT_GUTTER, viewport - VIEWPORT_GUTTER].
|
|
131
130
|
requestAnimationFrame(() => {
|
|
132
131
|
if (!panelEl) return;
|
|
133
132
|
const pr = panelEl.getBoundingClientRect();
|
|
@@ -144,13 +143,8 @@
|
|
|
144
143
|
});
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
/**
|
|
148
|
-
* Re-measure on every open, and while open, on scroll/resize so the
|
|
149
|
-
* panel follows its trigger when the page scrolls.
|
|
150
|
-
*/
|
|
151
146
|
$effect(() => {
|
|
152
|
-
if (!open) return;
|
|
153
|
-
// Initial placement (defer one frame so panelEl has mounted).
|
|
147
|
+
if (!open || useSheet) return;
|
|
154
148
|
requestAnimationFrame(updatePosition);
|
|
155
149
|
|
|
156
150
|
const handler = () => updatePosition();
|
|
@@ -162,15 +156,6 @@
|
|
|
162
156
|
};
|
|
163
157
|
});
|
|
164
158
|
|
|
165
|
-
/**
|
|
166
|
-
* Portal the panel element to `document.body` once it mounts, so any
|
|
167
|
-
* ancestor with `transform` / `filter` / `will-change` (Storybook's
|
|
168
|
-
* docs container, a scaled preview, a card with transform-based
|
|
169
|
-
* hover, etc.) doesn't re-parent our `position: fixed` panel and
|
|
170
|
-
* clip / mis-position it. On unmount, we simply remove the node —
|
|
171
|
-
* Svelte's own `{#if}` teardown will still work because it tracks
|
|
172
|
-
* the element by reference, not by DOM path.
|
|
173
|
-
*/
|
|
174
159
|
$effect(() => {
|
|
175
160
|
if (!panelEl) return;
|
|
176
161
|
const el = panelEl;
|
|
@@ -202,14 +187,6 @@
|
|
|
202
187
|
data-testid={buildTestId('popover', undefined, testId)}
|
|
203
188
|
>
|
|
204
189
|
{#if trigger === 'click'}
|
|
205
|
-
<!--
|
|
206
|
-
Click-mode: forward clicks to toggle. Do NOT add role/tabindex here
|
|
207
|
-
— consumers pass an interactive child (Button, <a>, etc.) that
|
|
208
|
-
handles its own focus/keyboard; adding them here would nest
|
|
209
|
-
interactive elements (invalid HTML + a11y issue). Keyboard support
|
|
210
|
-
comes via Enter/Space on the inner button dispatching a click,
|
|
211
|
-
which bubbles to this handler.
|
|
212
|
-
-->
|
|
213
190
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
214
191
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
215
192
|
<span class="inline-flex" aria-haspopup="dialog" aria-expanded={open} onclick={toggle}>
|
|
@@ -227,12 +204,12 @@
|
|
|
227
204
|
{@render children()}
|
|
228
205
|
</span>
|
|
229
206
|
{:else}
|
|
230
|
-
<!-- manual — consumer drives `open` -->
|
|
231
207
|
<span class="inline-flex">{@render children()}</span>
|
|
232
208
|
{/if}
|
|
233
209
|
</span>
|
|
234
210
|
|
|
235
|
-
|
|
211
|
+
<!-- Desktop: positioned panel -->
|
|
212
|
+
{#if open && !useSheet}
|
|
236
213
|
<div
|
|
237
214
|
bind:this={panelEl}
|
|
238
215
|
role="dialog"
|
|
@@ -252,3 +229,29 @@
|
|
|
252
229
|
{@render content({ close })}
|
|
253
230
|
</div>
|
|
254
231
|
{/if}
|
|
232
|
+
|
|
233
|
+
<!-- Mobile: bottom sheet -->
|
|
234
|
+
{#if open && useSheet}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
|
|
238
|
+
aria-label="Close"
|
|
239
|
+
onclick={close}
|
|
240
|
+
></button>
|
|
241
|
+
<div
|
|
242
|
+
role="dialog"
|
|
243
|
+
tabindex="-1"
|
|
244
|
+
class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
|
|
245
|
+
transition:fly={{ y: 300, duration: 200, easing: quintOut }}
|
|
246
|
+
data-testid={buildTestId('popover', 'sheet', testId)}
|
|
247
|
+
>
|
|
248
|
+
<!-- Drag handle -->
|
|
249
|
+
<div class="flex cursor-pointer justify-center py-2">
|
|
250
|
+
<div class="bg-default-300 h-1 w-8 rounded-full"></div>
|
|
251
|
+
</div>
|
|
252
|
+
<!-- Content -->
|
|
253
|
+
<div class="flex-1 cursor-pointer overflow-y-auto p-2">
|
|
254
|
+
{@render content({ close })}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
type="button"
|
|
315
315
|
onclick={() => handleSelect(group, tab.value)}
|
|
316
316
|
class={cn(
|
|
317
|
-
'rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap',
|
|
317
|
+
'cursor-pointer rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap',
|
|
318
318
|
active
|
|
319
319
|
? 'bg-primary-50 text-primary-700 border-primary-200'
|
|
320
320
|
: 'border-default-200 text-default-700 hover:bg-default-50'
|