@marianmeres/stuic 2.54.0 → 2.56.0
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/actions/popover/index.css +2 -0
- package/dist/actions/popover/popover.svelte.js +41 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +1 -1
- package/dist/utils/body-scroll-locker.d.ts +1 -2
- package/dist/utils/body-scroll-locker.js +4 -4
- package/dist/utils/breakpoint.svelte.d.ts +1 -0
- package/dist/utils/breakpoint.svelte.js +4 -0
- package/package.json +1 -1
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
@supports (anchor-name: --anchor) {
|
|
52
52
|
.stuic-popover {
|
|
53
53
|
/* position-area is set via inline style based on position param */
|
|
54
|
+
/* prefer fallback position with most available width (helps on narrow mobile screens) */
|
|
55
|
+
position-try-order: most-width;
|
|
54
56
|
/* fallbacks ensure popover stays within viewport */
|
|
55
57
|
position-try-fallbacks:
|
|
56
58
|
--pop-top-span-right, --pop-top-span-left, flip-block, --pop-bottom-span-right,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mount, unmount } from "svelte";
|
|
2
2
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
3
3
|
import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
|
|
4
|
+
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
4
5
|
import PopoverContent from "./PopoverContent.svelte";
|
|
5
6
|
//
|
|
6
7
|
import "./index.css";
|
|
@@ -215,6 +216,7 @@ export function popover(anchorEl, fn) {
|
|
|
215
216
|
let isVisible = false;
|
|
216
217
|
let do_debug = false;
|
|
217
218
|
let prevOpen = undefined;
|
|
219
|
+
let switchingToFallback = false; // flag to prevent recursion during fallback switch
|
|
218
220
|
// Unique identifiers
|
|
219
221
|
const rnd = Math.random().toString(36).slice(2);
|
|
220
222
|
const id = `popover-${rnd}`;
|
|
@@ -382,6 +384,8 @@ export function popover(anchorEl, fn) {
|
|
|
382
384
|
popoverEl.classList.add(...twMerge("stuic-popover-fallback", _classPopover, currentOptions.class).split(/\s/));
|
|
383
385
|
wrapperEl.appendChild(popoverEl);
|
|
384
386
|
document.body.appendChild(wrapperEl);
|
|
387
|
+
// Lock body scroll in fallback (modal) mode
|
|
388
|
+
BodyScroll.lock();
|
|
385
389
|
// Click on wrapper (outside popover) closes
|
|
386
390
|
if (currentOptions.closeOnClickOutside !== false) {
|
|
387
391
|
wrapperEl.addEventListener("click", (e) => {
|
|
@@ -419,6 +423,35 @@ export function popover(anchorEl, fn) {
|
|
|
419
423
|
requestAnimationFrame(() => {
|
|
420
424
|
popoverEl?.classList.add("pop-visible");
|
|
421
425
|
backdropEl?.classList.add("pop-visible");
|
|
426
|
+
// Check for viewport overflow and switch to fallback if needed
|
|
427
|
+
if (useAnchorPositioning && popoverEl && !switchingToFallback) {
|
|
428
|
+
// Use another RAF to let browser finalize positioning
|
|
429
|
+
requestAnimationFrame(() => {
|
|
430
|
+
if (!popoverEl)
|
|
431
|
+
return;
|
|
432
|
+
const rect = popoverEl.getBoundingClientRect();
|
|
433
|
+
const viewportWidth = window.innerWidth;
|
|
434
|
+
if (rect.left < 0 || rect.right > viewportWidth) {
|
|
435
|
+
debug("overflow detected, switching to fallback mode");
|
|
436
|
+
switchingToFallback = true;
|
|
437
|
+
// Quick cleanup (skip transition)
|
|
438
|
+
if (mountedComponent) {
|
|
439
|
+
unmount(mountedComponent);
|
|
440
|
+
mountedComponent = null;
|
|
441
|
+
}
|
|
442
|
+
popoverEl.remove();
|
|
443
|
+
popoverEl = null;
|
|
444
|
+
isVisible = false;
|
|
445
|
+
// Re-show in fallback mode
|
|
446
|
+
const originalForceFallback = currentOptions.forceFallback;
|
|
447
|
+
currentOptions.forceFallback = true;
|
|
448
|
+
show();
|
|
449
|
+
currentOptions.forceFallback = originalForceFallback;
|
|
450
|
+
switchingToFallback = false;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
422
455
|
currentOptions.onShow?.();
|
|
423
456
|
});
|
|
424
457
|
// Add event listeners
|
|
@@ -454,6 +487,10 @@ export function popover(anchorEl, fn) {
|
|
|
454
487
|
// Remove event listeners
|
|
455
488
|
document.removeEventListener("keydown", onEscape);
|
|
456
489
|
document.removeEventListener("click", onClickOutside);
|
|
490
|
+
// Unlock body scroll if we were in fallback mode (wrapperEl only exists in fallback)
|
|
491
|
+
if (wrapperEl) {
|
|
492
|
+
BodyScroll.unlock();
|
|
493
|
+
}
|
|
457
494
|
// Transition out
|
|
458
495
|
popoverEl?.classList.remove("pop-visible");
|
|
459
496
|
backdropEl?.classList.remove("pop-visible");
|
|
@@ -577,6 +614,10 @@ export function popover(anchorEl, fn) {
|
|
|
577
614
|
unmount(mountedComponent);
|
|
578
615
|
mountedComponent = null;
|
|
579
616
|
}
|
|
617
|
+
// Unlock body scroll if we were in fallback mode
|
|
618
|
+
if (wrapperEl) {
|
|
619
|
+
BodyScroll.unlock();
|
|
620
|
+
}
|
|
580
621
|
popoverEl?.remove();
|
|
581
622
|
backdropEl?.remove();
|
|
582
623
|
wrapperEl?.remove();
|
|
@@ -145,7 +145,7 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
145
145
|
export declare const DROPDOWN_MENU_BASE_CLASSES = "stuic-dropdown-menu relative inline-block";
|
|
146
146
|
export declare const DROPDOWN_MENU_TRIGGER_CLASSES = "\n\t\tinline-flex items-center justify-center gap-2\n\t\tpx-3 py-2\n\t\trounded-md border\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder-neutral-200 dark:border-neutral-700\n\t\thover:brightness-95 dark:hover:brightness-110\n\t\tfocus-visible:outline-2 focus-visible:outline-offset-2\n\t\tcursor-pointer\n\t";
|
|
147
147
|
export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-sm\n\t\tp-1\n\t\toverflow-y-auto\n\t\tz-50\n\t\tmin-w-48\n\t";
|
|
148
|
-
export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-3 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left
|
|
148
|
+
export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-3 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left \n\t\trounded-md\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
|
|
149
149
|
export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "\n\t\th-px my-1\n\t\tbg-neutral-200 dark:bg-neutral-700\n\t";
|
|
150
150
|
export declare const DROPDOWN_MENU_HEADER_CLASSES = "\n\t\tpx-2 py-1.5\n\t\ttext-xs font-semibold uppercase tracking-wide\n\t\ttext-neutral-500 dark:text-neutral-400\n\t\tselect-none\n\t";
|
|
151
151
|
export declare const DROPDOWN_MENU_BACKDROP_CLASSES = "\n\t\tstuic-dropdown-menu-backdrop\n\t\tfixed inset-0 bg-black/25\n\t\tz-40\n\t";
|
|
@@ -31,7 +31,7 @@ export class BodyScroll {
|
|
|
31
31
|
if (data.originalScrollY === undefined) {
|
|
32
32
|
const scrollY = window.scrollY || window.pageYOffset;
|
|
33
33
|
// Save body styles as serialized json
|
|
34
|
-
data.originalScrollStyleBackup = BodyScroll
|
|
34
|
+
data.originalScrollStyleBackup = BodyScroll.#get_body_style();
|
|
35
35
|
// Save the original scroll position in dataset
|
|
36
36
|
data.originalScrollY = `${scrollY}`;
|
|
37
37
|
data.scrollLockCount = "1";
|
|
@@ -64,7 +64,7 @@ export class BodyScroll {
|
|
|
64
64
|
else {
|
|
65
65
|
// This is the last component, restore everything
|
|
66
66
|
const originalScrollY = parseInt(data.originalScrollY, 10);
|
|
67
|
-
BodyScroll
|
|
67
|
+
BodyScroll.#restore_body_styles(data.originalScrollStyleBackup);
|
|
68
68
|
// Remove our data attributes
|
|
69
69
|
delete data.originalScrollY;
|
|
70
70
|
delete data.originalScrollStyleBackup;
|
|
@@ -74,7 +74,7 @@ export class BodyScroll {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
static
|
|
77
|
+
static #get_body_style() {
|
|
78
78
|
// we want only explicitly defined, not computed
|
|
79
79
|
const style = document.body.style;
|
|
80
80
|
return JSON.stringify({
|
|
@@ -86,7 +86,7 @@ export class BodyScroll {
|
|
|
86
86
|
right: style.left || null,
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
static
|
|
89
|
+
static #restore_body_styles(originalJsonString) {
|
|
90
90
|
let original = {
|
|
91
91
|
position: null,
|
|
92
92
|
top: null,
|
|
@@ -29,6 +29,7 @@ const _breakpoints = [
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
export class Breakpoint {
|
|
32
|
+
static #singleton;
|
|
32
33
|
#bp = $derived.by(() => {
|
|
33
34
|
const w = innerWidth.current || 0;
|
|
34
35
|
return _breakpoints.reduce((m, [k, v]) => {
|
|
@@ -37,6 +38,9 @@ export class Breakpoint {
|
|
|
37
38
|
return m;
|
|
38
39
|
}, { current: null, sm: false, md: false, lg: false, xl: false, "2xl": false });
|
|
39
40
|
});
|
|
41
|
+
static get instance() {
|
|
42
|
+
return (Breakpoint.#singleton ??= new Breakpoint());
|
|
43
|
+
}
|
|
40
44
|
get current() {
|
|
41
45
|
return this.#bp.current;
|
|
42
46
|
}
|