Merge pull request #601 from PR0M3TH3AN/codex/add-floating-ui/dom-and-refactor-popoverengine

Refactor popover engine to use Floating UI
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-16 16:18:49 -04:00
commit bc3e60d57f
10 files changed
+206 -233

No files matched your search

+13 -1
View File
@@ -29,7 +29,7 @@ dev surface without worrying about duplicates.
`createPopover(trigger, render, options)` wires focus management, aria
attributes, collision detection, and auto updates. The render callback receives
a portal `container` inside the overlay root. Return the panel element so the
engine can toggle `data-popover-state` values while it opens and closes.
engine toggles `data-popover-state` / `data-state` values while it opens and closes.
```js
import createPopover from "../js/ui/overlay/popoverEngine.js";
@@ -76,6 +76,18 @@ Pass `document`, `placement`, and other floating-ui options through the third
argument. The helper exposes `open()`, `close()`, `toggle()`, `isOpen()`, and
`destroy()` for lifecycle control.
### Floating UI dependency
The shared popover engine standardizes on
[`@floating-ui/dom`](https://floating-ui.com/) for positioning. The module is
vendored to `vendor/floating-ui.dom.bundle.min.js` by
`npm run build:beacon`, which bundles dependencies and copies the upstream
license alongside `nostr-tools`. Run that build whenever you bump Floating UI so
the checked-in vendor artifact stays in sync. New overlays should lean on the
engine rather than re-implementing manual gap, flip, or viewport clamp math—the
middleware layer already applies the configured gap and viewport padding via
`offset()`, `flip()`, and `shift()` helpers.
## Tailwind overlay classes
Tailwind ships token-backed overlay helpers that keep surfaces aligned with the
+96 -231
View File
@@ -5,6 +5,14 @@ import {
readDesignToken,
} from "../../designSystem/metrics.js";
import { ensureOverlayRoot } from "./overlayRoot.js";
import {
arrow as arrowMiddleware,
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "../../../vendor/floating-ui.dom.bundle.min.js";
const DEFAULT_PLACEMENT = "bottom-start";
const DEFAULT_STRATEGY = "fixed";
@@ -107,61 +115,56 @@ function resolveArrow(option, panel) {
return null;
}
function applyArrowStyles({
arrowElement,
side,
panelLeft,
panelTop,
panelWidth,
panelHeight,
anchorRect,
}) {
if (!arrowElement || !side || !anchorRect) {
function applyArrowStyles({ arrowElement, placement, middlewareData }) {
if (!arrowElement || typeof placement !== "string") {
return;
}
const arrowRect =
typeof arrowElement.getBoundingClientRect === "function"
? arrowElement.getBoundingClientRect()
: null;
const arrowWidth = arrowRect?.width ?? arrowElement.offsetWidth ?? 0;
const arrowHeight = arrowRect?.height ?? arrowElement.offsetHeight ?? 0;
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
const basePlacement = placement.split("-")[0];
const arrowData = middlewareData?.arrow || null;
arrowElement.style.left = "";
arrowElement.style.right = "";
arrowElement.style.top = "";
arrowElement.style.bottom = "";
if (side === "top" || side === "bottom") {
let arrowLeft = anchorCenterX - panelLeft - arrowWidth / 2;
arrowLeft = Math.min(
Math.max(arrowLeft, 0),
Math.max(panelWidth - arrowWidth, 0),
);
arrowElement.style.left = `${Math.round(arrowLeft)}px`;
if (side === "bottom") {
arrowElement.style.top = `${-Math.round(arrowHeight)}px`;
} else {
arrowElement.style.bottom = `${-Math.round(arrowHeight)}px`;
}
} else if (side === "left" || side === "right") {
let arrowTop = anchorCenterY - panelTop - arrowHeight / 2;
arrowTop = Math.min(
Math.max(arrowTop, 0),
Math.max(panelHeight - arrowHeight, 0),
);
arrowElement.style.top = `${Math.round(arrowTop)}px`;
if (side === "right") {
arrowElement.style.left = `${-Math.round(arrowWidth)}px`;
} else {
arrowElement.style.right = `${-Math.round(arrowWidth)}px`;
}
if (!arrowData || !basePlacement) {
delete arrowElement.dataset.popoverArrowSide;
return;
}
arrowElement.dataset.popoverArrowSide = side;
const staticSideMap = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
};
if (Number.isFinite(arrowData.x)) {
arrowElement.style.left = `${Math.round(arrowData.x)}px`;
}
if (Number.isFinite(arrowData.y)) {
arrowElement.style.top = `${Math.round(arrowData.y)}px`;
}
const staticSide = staticSideMap[basePlacement] || null;
if (staticSide) {
const arrowRect =
typeof arrowElement.getBoundingClientRect === "function"
? arrowElement.getBoundingClientRect()
: null;
const arrowWidth = arrowRect?.width ?? arrowElement.offsetWidth ?? 0;
const arrowHeight = arrowRect?.height ?? arrowElement.offsetHeight ?? 0;
const offsetValue =
basePlacement === "top" || basePlacement === "bottom"
? arrowHeight
: arrowWidth;
arrowElement.style[staticSide] = `${-Math.round(offsetValue)}px`;
}
arrowElement.dataset.popoverArrowSide = basePlacement;
}
function containsTarget(root, target) {
@@ -181,85 +184,6 @@ function setExpandedAttribute(trigger, value) {
trigger.setAttribute("aria-expanded", value ? "true" : "false");
}
function createAutoUpdate({ documentRef, anchor, panel, update }) {
if (!documentRef || !anchor || !panel || typeof update !== "function") {
return null;
}
const cleanupFns = [];
const safeUpdate = () => {
try {
const result = update();
if (result && typeof result.then === "function") {
result.catch((error) => {
logger.dev.warn("[popover] auto-update failed", error);
});
}
} catch (error) {
logger.dev.warn("[popover] auto-update failed", error);
}
};
const view = documentRef.defaultView || globalThis;
if (view && typeof view.addEventListener === "function") {
const resizeHandler = () => safeUpdate();
view.addEventListener("resize", resizeHandler);
cleanupFns.push(() => view.removeEventListener("resize", resizeHandler));
const scrollHandler = () => safeUpdate();
view.addEventListener("scroll", scrollHandler, true);
cleanupFns.push(() => view.removeEventListener("scroll", scrollHandler, true));
}
const scrollTarget = documentRef.scrollingElement || documentRef;
if (scrollTarget && typeof scrollTarget.addEventListener === "function") {
const docScrollHandler = () => safeUpdate();
scrollTarget.addEventListener("scroll", docScrollHandler, true);
cleanupFns.push(() =>
scrollTarget.removeEventListener("scroll", docScrollHandler, true),
);
}
const observers = [];
if (typeof ResizeObserver === "function") {
try {
const anchorObserver = new ResizeObserver(safeUpdate);
anchorObserver.observe(anchor);
observers.push(anchorObserver);
} catch (error) {
logger.dev.warn("[popover] anchor resize observer failed", error);
}
if (panel !== anchor) {
try {
const panelObserver = new ResizeObserver(safeUpdate);
panelObserver.observe(panel);
observers.push(panelObserver);
} catch (error) {
logger.dev.warn("[popover] panel resize observer failed", error);
}
}
}
return () => {
cleanupFns.forEach((cleanup) => {
try {
cleanup();
} catch (error) {
logger.dev.warn("[popover] auto-update cleanup failed", error);
}
});
observers.forEach((observer) => {
try {
observer.disconnect();
} catch (error) {
logger.dev.warn("[popover] observer disconnect failed", error);
}
});
};
}
export function createPopover(trigger, render, options = {}) {
const anchor = isElement(trigger) ? trigger : null;
const documentRef = resolveDocument(anchor, options.document);
@@ -714,124 +638,56 @@ export function createPopover(trigger, render, options = {}) {
}
async function updatePosition() {
if (!anchor || !panel || !documentRef) {
if (!anchor || !panel) {
return;
}
try {
const anchorRect =
typeof anchor.getBoundingClientRect === "function"
? anchor.getBoundingClientRect()
: null;
if (!anchorRect) {
return;
}
const view = documentRef.defaultView || globalThis;
const docEl = documentRef.documentElement || null;
const viewportWidth =
docEl?.clientWidth ?? view?.innerWidth ?? panel.offsetWidth ?? 0;
const viewportHeight =
docEl?.clientHeight ?? view?.innerHeight ?? panel.offsetHeight ?? 0;
const panelRect =
typeof panel.getBoundingClientRect === "function"
? panel.getBoundingClientRect()
: null;
const panelWidth = panelRect?.width ?? panel.offsetWidth ?? 0;
const panelHeight = panelRect?.height ?? panel.offsetHeight ?? 0;
let [side, alignment = "start"] = placement.split("-");
if (!side || !["top", "bottom", "left", "right"].includes(side)) {
side = "bottom";
}
if (!["start", "end", "center"].includes(alignment)) {
alignment = "start";
}
const effectiveGap = Number.isFinite(gap) ? gap : 0;
const safePadding = Number.isFinite(viewportPadding) ? viewportPadding : 0;
const middleware = [
offset(effectiveGap),
flip({ padding: safePadding }),
shift({ padding: safePadding }),
];
function clamp(value, min, max, fallback) {
if (!Number.isFinite(value)) {
return fallback ?? min;
}
if (min > max) {
return fallback ?? (min + max) / 2;
}
return Math.min(Math.max(value, min), max);
if (arrowElement) {
middleware.push(
arrowMiddleware({
element: arrowElement,
}),
);
}
if (side === "bottom") {
const fitsBelow =
anchorRect.bottom + effectiveGap + panelHeight <=
viewportHeight - safePadding;
const fitsAbove =
anchorRect.top - effectiveGap - panelHeight >= safePadding;
if (!fitsBelow && fitsAbove) {
side = "top";
}
} else if (side === "top") {
const fitsAbove =
anchorRect.top - effectiveGap - panelHeight >= safePadding;
const fitsBelow =
anchorRect.bottom + effectiveGap + panelHeight <=
viewportHeight - safePadding;
if (!fitsAbove && fitsBelow) {
side = "bottom";
}
}
const {
x,
y,
placement: resolvedPlacement,
middlewareData,
strategy: resolvedStrategy,
} = await computePosition(anchor, panel, {
placement,
strategy,
middleware,
});
let top = 0;
let left = 0;
const nextX = Number.isFinite(x) ? x : 0;
const nextY = Number.isFinite(y) ? y : 0;
const finalPlacement =
typeof resolvedPlacement === "string" && resolvedPlacement
? resolvedPlacement
: placement;
if (side === "top") {
top = anchorRect.top - panelHeight - effectiveGap;
} else if (side === "bottom") {
top = anchorRect.bottom + effectiveGap;
} else if (side === "left") {
top = anchorRect.top + anchorRect.height / 2 - panelHeight / 2;
} else if (side === "right") {
top = anchorRect.top + anchorRect.height / 2 - panelHeight / 2;
}
if (side === "left") {
left = anchorRect.left - panelWidth - effectiveGap;
} else if (side === "right") {
left = anchorRect.right + effectiveGap;
} else if (alignment === "end") {
left = anchorRect.right - panelWidth;
} else if (alignment === "center") {
left = anchorRect.left + anchorRect.width / 2 - panelWidth / 2;
} else {
left = anchorRect.left;
}
const minTop = safePadding;
const maxTop = viewportHeight - safePadding - panelHeight;
const minLeft = safePadding;
const maxLeft = viewportWidth - safePadding - panelWidth;
const fallbackTop = viewportHeight / 2 - panelHeight / 2;
const fallbackLeft = viewportWidth / 2 - panelWidth / 2;
top = clamp(top, minTop, maxTop, fallbackTop);
left = clamp(left, minLeft, maxLeft, fallbackLeft);
panel.style.position = strategy;
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.dataset.popoverPlacement = `${side}-${alignment}`;
panel.style.position = resolvedStrategy || strategy;
panel.style.left = `${Math.round(nextX)}px`;
panel.style.top = `${Math.round(nextY)}px`;
panel.dataset.popoverPlacement = finalPlacement;
if (arrowElement) {
applyArrowStyles({
arrowElement,
side,
panelLeft: left,
panelTop: top,
panelWidth,
panelHeight,
anchorRect,
placement: finalPlacement,
middlewareData,
});
}
} catch (error) {
@@ -900,6 +756,7 @@ export function createPopover(trigger, render, options = {}) {
panelElement.hidden = false;
panelElement.setAttribute("aria-hidden", "false");
panelElement.dataset.popoverState = "open";
panelElement.dataset.state = "open";
setExpandedAttribute(anchor, true);
bindGlobalHandlers();
@@ -909,12 +766,19 @@ export function createPopover(trigger, render, options = {}) {
autoUpdateCleanup = null;
}
autoUpdateCleanup = createAutoUpdate({
documentRef,
autoUpdateCleanup = autoUpdate(
anchor,
panel: panelElement,
update: () => updatePosition(),
});
panelElement,
() => {
void updatePosition();
},
{
ancestorScroll: true,
ancestorResize: true,
elementResize: true,
layoutShift: true,
},
);
await updatePosition();
@@ -946,6 +810,7 @@ export function createPopover(trigger, render, options = {}) {
panel.setAttribute("aria-hidden", "true");
panel.hidden = true;
panel.dataset.popoverState = "closed";
panel.dataset.state = "closed";
updateAriaActiveDescendant(panel, null);
if (menuState.items.length) {
menuState.items.forEach((item) => {
+26
View File
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"path": "^0.12.7",
"webtorrent": "^1.9.7"
},
@@ -708,6 +709,31 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+1
View File
@@ -40,6 +40,7 @@
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"path": "^0.12.7",
"webtorrent": "^1.9.7"
}
+32 -1
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env node
import { build } from "esbuild";
import { mkdir } from "node:fs/promises";
import { copyFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -11,8 +12,23 @@ const repoRoot = path.resolve(__dirname, "..");
const entryPoint = path.join(repoRoot, "torrent/src/beacon-vendor.js");
const outDir = path.join(repoRoot, "torrent/dist");
const outFile = path.join(outDir, "beacon.js");
const vendorDir = path.join(repoRoot, "vendor");
const vendorOutFile = path.join(vendorDir, "floating-ui.dom.bundle.min.js");
const vendorLicenseFile = path.join(vendorDir, "floating-ui.LICENSE");
const floatingUiEntry = path.join(
repoRoot,
"scripts/vendor/floating-ui-dom-entry.mjs",
);
const require = createRequire(import.meta.url);
const floatingUiPackageJson = require.resolve("@floating-ui/dom/package.json");
const floatingUiLicense = path.join(
path.dirname(floatingUiPackageJson),
"LICENSE",
);
await mkdir(outDir, { recursive: true });
await mkdir(vendorDir, { recursive: true });
await build({
bundle: true,
@@ -34,3 +50,18 @@ await build({
},
logLevel: "info",
});
await build({
bundle: true,
entryPoints: [floatingUiEntry],
outfile: vendorOutFile,
format: "esm",
splitting: false,
platform: "browser",
target: ["es2020"],
minify: true,
sourcemap: false,
logLevel: "info",
});
await copyFile(floatingUiLicense, vendorLicenseFile);
+8
View File
@@ -0,0 +1,8 @@
export {
arrow,
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
+4
View File
@@ -151,6 +151,7 @@ test("flips placement when bottom placement would collide with viewport", async
const panel = documentRef.querySelector('[data-test-panel="collision"]');
assert.ok(panel, "panel should render");
assert.equal(panel.dataset.popoverState, "open");
assert.equal(panel.dataset.state, "open");
assert.equal(panel.dataset.popoverPlacement, "top-start");
assert.equal(panel.style.position, "fixed");
assert.equal(panel.style.maxWidth, "calc(100vw - var(--space-xl))");
@@ -254,15 +255,18 @@ test("closes previously open popovers without restoring focus", async () => {
assert.equal(popoverA.isOpen(), true);
assert.equal(triggerA.getAttribute("aria-expanded"), "true");
assert.equal(panelA?.dataset.popoverState, "open");
assert.equal(panelA?.dataset.state, "open");
await popoverB.open();
assert.equal(popoverB.isOpen(), true);
assert.equal(triggerB.getAttribute("aria-expanded"), "true");
assert.equal(panelB?.dataset.popoverState, "open");
assert.equal(panelB?.dataset.state, "open");
assert.equal(popoverA.isOpen(), false, "first popover should close");
assert.equal(triggerA.getAttribute("aria-expanded"), "false");
assert.equal(panelA?.dataset.popoverState, "closed");
assert.equal(panelA?.dataset.state, "closed");
assert.notEqual(active.active, triggerA, "focus should not jump back to first trigger");
popoverA.destroy();
+5
View File
@@ -124,6 +124,7 @@ test("opens a popover in the overlay root and positions the panel", async () =>
assert.ok(panel, "panel should be rendered");
assert.equal(trigger.getAttribute("aria-controls"), panel.id);
assert.equal(panel.dataset.popoverState, "open");
assert.equal(panel.dataset.state, "open");
assert.equal(panel.dataset.popoverPlacement, "bottom-start");
assert.equal(panel.style.position, "fixed");
assert.equal(panel.style.left, "100px");
@@ -134,6 +135,7 @@ test("opens a popover in the overlay root and positions the panel", async () =>
popover.close();
assert.equal(panel.dataset.popoverState, "closed");
assert.equal(panel.dataset.state, "closed");
assert.equal(trigger.getAttribute("aria-expanded"), "false");
popover.destroy();
@@ -369,6 +371,7 @@ test("escape closes the popover and restores trigger focus", async () => {
assert.equal(popover.isOpen(), false);
assert.equal(panel.dataset.popoverState, "closed");
assert.equal(panel.dataset.state, "closed");
assert.equal(panel.getAttribute("aria-activedescendant"), null);
assert.equal(documentRef.activeElement, trigger);
@@ -496,6 +499,8 @@ test("ensures only one popover is open at a time", async () => {
const panelB = documentRef.getElementById("panel-b");
assert.equal(panelA.dataset.popoverState, "closed");
assert.equal(panelB.dataset.popoverState, "open");
assert.equal(panelA.dataset.state, "closed");
assert.equal(panelB.dataset.state, "open");
popoverA.destroy();
popoverB.destroy();
+20
View File
@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2021-present Floating UI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
var H=Math.min,B=Math.max,ot=Math.round,it=Math.floor,L=t=>({x:t,y:t}),qt={left:"right",right:"left",bottom:"top",top:"bottom"},Ut={start:"end",end:"start"};function lt(t,e,n){return B(t,H(e,n))}function U(t,e){return typeof t=="function"?t(e):t}function V(t){return t.split("-")[0]}function K(t){return t.split("-")[1]}function pt(t){return t==="x"?"y":"x"}function at(t){return t==="y"?"height":"width"}var Kt=new Set(["top","bottom"]);function W(t){return Kt.has(V(t))?"y":"x"}function ft(t){return pt(W(t))}function Ot(t,e,n){n===void 0&&(n=!1);let o=K(t),i=ft(t),r=at(i),s=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[r]>e.floating[r]&&(s=nt(s)),[s,nt(s)]}function Rt(t){let e=nt(t);return[ct(t),e,ct(e)]}function ct(t){return t.replace(/start|end/g,e=>Ut[e])}var bt=["left","right"],At=["right","left"],Gt=["top","bottom"],Jt=["bottom","top"];function Qt(t,e,n){switch(t){case"top":case"bottom":return n?e?At:bt:e?bt:At;case"left":case"right":return e?Gt:Jt;default:return[]}}function St(t,e,n,o){let i=K(t),r=Qt(V(t),n==="start",o);return i&&(r=r.map(s=>s+"-"+i),e&&(r=r.concat(r.map(ct)))),r}function nt(t){return t.replace(/left|right|bottom|top/g,e=>qt[e])}function Zt(t){return{top:0,right:0,bottom:0,left:0,...t}}function wt(t){return typeof t!="number"?Zt(t):{top:t,right:t,bottom:t,left:t}}function X(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function Ct(t,e,n){let{reference:o,floating:i}=t,r=W(e),s=ft(e),c=at(s),a=V(e),l=r==="y",d=o.x+o.width/2-i.width/2,u=o.y+o.height/2-i.height/2,g=o[c]/2-i[c]/2,f;switch(a){case"top":f={x:d,y:o.y-i.height};break;case"bottom":f={x:d,y:o.y+o.height};break;case"right":f={x:o.x+o.width,y:u};break;case"left":f={x:o.x-i.width,y:u};break;default:f={x:o.x,y:o.y}}switch(K(e)){case"start":f[s]-=g*(n&&l?-1:1);break;case"end":f[s]+=g*(n&&l?-1:1);break}return f}var Pt=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:r=[],platform:s}=n,c=r.filter(Boolean),a=await(s.isRTL==null?void 0:s.isRTL(e)),l=await s.getElementRects({reference:t,floating:e,strategy:i}),{x:d,y:u}=Ct(l,o,a),g=o,f={},m=0;for(let h=0;h<c.length;h++){let{name:w,fn:p}=c[h],{x,y,data:b,reset:v}=await p({x:d,y:u,initialPlacement:o,placement:g,strategy:i,middlewareData:f,rects:l,platform:s,elements:{reference:t,floating:e}});d=x??d,u=y??u,f={...f,[w]:{...f[w],...b}},v&&m<=50&&(m++,typeof v=="object"&&(v.placement&&(g=v.placement),v.rects&&(l=v.rects===!0?await s.getElementRects({reference:t,floating:e,strategy:i}):v.rects),{x:d,y:u}=Ct(l,g,a)),h=-1)}return{x:d,y:u,placement:g,strategy:i,middlewareData:f}};async function xt(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:r,rects:s,elements:c,strategy:a}=t,{boundary:l="clippingAncestors",rootBoundary:d="viewport",elementContext:u="floating",altBoundary:g=!1,padding:f=0}=U(e,t),m=wt(f),w=c[g?u==="floating"?"reference":"floating":u],p=X(await r.getClippingRect({element:(n=await(r.isElement==null?void 0:r.isElement(w)))==null||n?w:w.contextElement||await(r.getDocumentElement==null?void 0:r.getDocumentElement(c.floating)),boundary:l,rootBoundary:d,strategy:a})),x=u==="floating"?{x:o,y:i,width:s.floating.width,height:s.floating.height}:s.reference,y=await(r.getOffsetParent==null?void 0:r.getOffsetParent(c.floating)),b=await(r.isElement==null?void 0:r.isElement(y))?await(r.getScale==null?void 0:r.getScale(y))||{x:1,y:1}:{x:1,y:1},v=X(r.convertOffsetParentRelativeRectToViewportRelativeRect?await r.convertOffsetParentRelativeRectToViewportRelativeRect({elements:c,rect:x,offsetParent:y,strategy:a}):x);return{top:(p.top-v.top+m.top)/b.y,bottom:(v.bottom-p.bottom+m.bottom)/b.y,left:(p.left-v.left+m.left)/b.x,right:(v.right-p.right+m.right)/b.x}}var Tt=t=>({name:"arrow",options:t,async fn(e){let{x:n,y:o,placement:i,rects:r,platform:s,elements:c,middlewareData:a}=e,{element:l,padding:d=0}=U(t,e)||{};if(l==null)return{};let u=wt(d),g={x:n,y:o},f=ft(i),m=at(f),h=await s.getDimensions(l),w=f==="y",p=w?"top":"left",x=w?"bottom":"right",y=w?"clientHeight":"clientWidth",b=r.reference[m]+r.reference[f]-g[f]-r.floating[m],v=g[f]-r.reference[f],M=await(s.getOffsetParent==null?void 0:s.getOffsetParent(l)),R=M?M[y]:0;(!R||!await(s.isElement==null?void 0:s.isElement(M)))&&(R=c.floating[y]||r.floating[m]);let Z=b/2-v/2,_=R/2-h[m]/2-1,F=H(u[p],_),tt=H(u[x],_),z=F,et=R-h[m]-tt,A=R/2-h[m]/2+Z,I=lt(z,A,et),k=!a.arrow&&K(i)!=null&&A!==I&&r.reference[m]/2-(A<z?F:tt)-h[m]/2<0,P=k?A<z?A-z:A-et:0;return{[f]:g[f]+P,data:{[f]:I,centerOffset:A-I-P,...k&&{alignmentOffset:P}},reset:k}}});var Lt=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,o;let{placement:i,middlewareData:r,rects:s,initialPlacement:c,platform:a,elements:l}=e,{mainAxis:d=!0,crossAxis:u=!0,fallbackPlacements:g,fallbackStrategy:f="bestFit",fallbackAxisSideDirection:m="none",flipAlignment:h=!0,...w}=U(t,e);if((n=r.arrow)!=null&&n.alignmentOffset)return{};let p=V(i),x=W(c),y=V(c)===c,b=await(a.iLine truncated