<script lang="ts" context="module">
	export type MenuType = "Popover" | "Tooltip" | "Dropdown" | "Dialog" | "Cursor";

	/// Prevents the escape key from closing the parent floating menu of the given element.
	/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
	/// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
	/// Then after two calls of `setTimeout`, we can safely remove the attribute here.
	export function preventEscapeClosingParentFloatingMenu(element: HTMLElement) {
		const floatingMenuParent = element.closest("[data-floating-menu-content]") || undefined;
		if (floatingMenuParent instanceof HTMLElement) {
			floatingMenuParent.setAttribute("data-escape-does-not-close", "");
			setTimeout(() => {
				setTimeout(() => {
					floatingMenuParent.removeAttribute("data-escape-does-not-close");
				}, 0);
			}, 0);
		}
	}
</script>

<script lang="ts">
	import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";

	import type { MenuDirection } from "@graphite/messages";
	import { browserVersion } from "@graphite/utility-functions/platform";

	import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";

	const BUTTON_LEFT = 0;
	const POINTER_STRAY_DISTANCE = 100;

	const dispatch = createEventDispatcher<{ open: boolean; naturalWidth: number }>();

	let className = "";
	export { className as class };
	export let classes: Record<string, boolean> = {};
	let styleName = "";
	export { styleName as style };
	export let styles: Record<string, string | number | undefined> = {};
	export let open: boolean;
	export let type: MenuType;
	export let direction: MenuDirection = "Bottom";
	export let windowEdgeMargin = 6;
	export let scrollableY = false;
	export let minWidth = 0;
	export let escapeCloses = true;
	export let strayCloses = true;

	let tail: HTMLDivElement | undefined;
	let self: HTMLDivElement | undefined;
	let floatingMenuContainer: HTMLDivElement | undefined;
	let floatingMenuContent: LayoutCol | undefined;

	// The resize observer is attached to the floating menu container, which is the zero-height div of the width of the parent element's floating menu spawner.
	// Since CSS doesn't let us make the floating menu (with `position: fixed`) have a 100% width of this container, we need to use JS to observe its size and
	// tell the floating menu content to use it as a min-width so the floating menu is at least the width of the parent element's floating menu spawner.
	// This is the opposite concern of the natural width measurement system, which gets the natural width of the floating menu content in order for the
	// spawner widget to optionally set its min-size to the floating menu's natural width.
	let containerResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
		resizeObserverCallback(entries);
	});
	let wasOpen = open;
	let measuringOngoing = false;
	let measuringOngoingGuard = false;
	let minWidthParentWidth = 0;
	let pointerStillDown = false;
	let floatingMenuBounds = new DOMRect();
	let floatingMenuContentBounds = new DOMRect();

	$: watchOpenChange(open);

	$: minWidthStyleValue = measuringOngoing ? "0" : `${Math.max(minWidth, minWidthParentWidth)}px`;
	$: displayTail = open && type === "Popover";
	$: displayContainer = open || measuringOngoing;
	$: extraClasses = Object.entries(classes)
		.flatMap(([className, stateName]) => (stateName ? [className] : []))
		.join(" ");
	$: extraStyles = Object.entries(styles)
		.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
		.join(" ");

	// Called only when `open` is changed from outside this component
	async function watchOpenChange(isOpen: boolean) {
		// Mitigate a Safari rendering bug which clips the floating menu extending beyond a scrollable container.
		// The bug is possibly related to <https://bugs.webkit.org/show_bug.cgi?id=160953>, but in our case it happens when `overflow` of a parent is `auto` rather than `hidden`.
		if (browserVersion().toLowerCase().includes("safari")) {
			const scrollable = self?.closest("[data-scrollable-x], [data-scrollable-y]");
			if (scrollable instanceof HTMLElement) {
				// The issue exists when the container is set to `overflow: auto` but fine when `overflow: hidden`. So this workaround temporarily sets
				// the scrollable container to `overflow: hidden`, thus removing the scrollbars and ability to scroll until the floating menu is closed.
				scrollable.style.overflow = isOpen ? "hidden" : "";
			}
		}

		// Switching from closed to open
		if (isOpen && !wasOpen) {
			// TODO: Close any other floating menus that may already be open, which can happen using tab navigation and Enter/Space Bar to open

			// Close floating menu if pointer strays far enough away
			window.addEventListener("pointermove", pointerMoveHandler);
			// Close floating menu if esc is pressed
			window.addEventListener("keydown", keyDownHandler);
			// Close floating menu if pointer is outside (but within stray distance)
			window.addEventListener("pointerdown", pointerDownHandler);
			// Cancel the subsequent click event to prevent the floating menu from reopening if the floating menu's button is the click event target
			window.addEventListener("pointerup", pointerUpHandler);

			// Floating menu min-width resize observer

			await tick();

			// Start a new observation of the now-open floating menu
			if (floatingMenuContainer) {
				containerResizeObserver.disconnect();
				containerResizeObserver.observe(floatingMenuContainer);
			}
		}

		// Switching from open to closed
		if (!isOpen && wasOpen) {
			// Clean up observation of the now-closed floating menu
			containerResizeObserver.disconnect();

			window.removeEventListener("pointermove", pointerMoveHandler);
			window.removeEventListener("keydown", keyDownHandler);
			window.removeEventListener("pointerdown", pointerDownHandler);
			// The `pointerup` event is removed in `pointerMoveHandler()` and `pointerDownHandler()`
		}

		// Now that we're done reading the old state, update it to the current state for next time
		wasOpen = isOpen;
	}

	onMount(() => {
		// Measure the content and round up its width and height to the nearest even integer.
		// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
		const floatingMenuContentDiv = floatingMenuContent?.div?.();
		if (type === "Dialog" && floatingMenuContentDiv) {
			const resizeObserver = new ResizeObserver((entries) => {
				entries.forEach((entry) => {
					const existingWidth = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-x"));
					const existingHeight = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-y"));

					let { width, height } = entry.contentRect;
					width -= existingWidth;
					height -= existingHeight;

					let targetWidth = Math.ceil(width);
					if (targetWidth % 2 === 1) targetWidth += 1;
					let targetHeight = Math.ceil(height);
					if (targetHeight % 2 === 1) targetHeight += 1;

					// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
					floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-x", `${targetWidth - width}`);
					floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-y", `${targetHeight - height}`);
				});
			});
			resizeObserver.observe(floatingMenuContentDiv);
		}
	});

	afterUpdate(() => {
		// Gets the client bounds of the elements and apply relevant styles to them.
		// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
		// TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop).
		if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
	});

	function resizeObserverCallback(entries: ResizeObserverEntry[]) {
		minWidthParentWidth = entries[0].contentRect.width;
	}

	function positionAndStyleFloatingMenu() {
		if (type === "Cursor") return;

		const floatingMenuContentDiv = floatingMenuContent?.div?.();
		if (!self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return;

		const windowBounds = document.documentElement.getBoundingClientRect();
		floatingMenuBounds = self.getBoundingClientRect();
		const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
		floatingMenuContentBounds = floatingMenuContentDiv.getBoundingClientRect();

		const overflowingLeft = floatingMenuContentBounds.left - windowEdgeMargin <= windowBounds.left;
		const overflowingRight = floatingMenuContentBounds.right + windowEdgeMargin >= windowBounds.right;
		const overflowingTop = floatingMenuContentBounds.top - windowEdgeMargin <= windowBounds.top;
		const overflowingBottom = floatingMenuContentBounds.bottom + windowEdgeMargin >= windowBounds.bottom;

		// TODO: Make this work for all types. This is currently limited to tooltips because they're inherently small and transient.
		// TODO: But on popovers and dropdowns, it's a bit harder to do this right. First we check if it's overflowing and flip the direction to avoid the overflow.
		// TODO: But once it's flipped, if the position moves and the menu would no longer be overflowing, we're still flipped and thus unable to automatically notice the need to flip back.
		// TODO: So as a result, once flipped, it stays flipped forever even if the menu spawner element is moved back away from the edge of the window.
		if (type === "Tooltip") {
			// Flip direction if overflowing the edge of the window
			if (direction === "Top" && overflowingTop) direction = "Bottom";
			else if (direction === "Bottom" && overflowingBottom) direction = "Top";
			else if (direction === "Left" && overflowingLeft) direction = "Right";
			else if (direction === "Right" && overflowingRight) direction = "Left";
		}

		const inParentFloatingMenu = Boolean(floatingMenuContainer.closest("[data-floating-menu-content]"));
		if (!inParentFloatingMenu) {
			// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
			// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
			let tailOffset = 0;
			if (type === "Popover") tailOffset = 10;
			if (type === "Tooltip") tailOffset = direction === "Bottom" ? 20 : 10;

			if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.y}px`;
			if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + (windowBounds.height - floatingMenuBounds.y)}px`;
			if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.x}px`;
			if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + (windowBounds.width - floatingMenuBounds.x)}px`;

			// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
			// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
			if (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.y}px`;
			if (tail && direction === "Top") tail.style.bottom = `${windowBounds.height - floatingMenuBounds.y}px`;
			if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.x}px`;
			if (tail && direction === "Left") tail.style.right = `${windowBounds.width - floatingMenuBounds.x}px`;
		}

		type Edge = "Top" | "Bottom" | "Left" | "Right";
		let zeroedBorderVertical: Edge | undefined;
		let zeroedBorderHorizontal: Edge | undefined;

		if (direction === "Top" || direction === "Bottom") {
			zeroedBorderVertical = direction === "Top" ? "Bottom" : "Top";

			// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
			if (overflowingLeft) {
				floatingMenuContentDiv.style.left = `${windowEdgeMargin}px`;
				if (windowBounds.left + floatingMenuContainerBounds.left === 12) zeroedBorderHorizontal = "Left";
			}
			if (overflowingRight) {
				floatingMenuContentDiv.style.right = `${windowEdgeMargin}px`;
				if (windowBounds.right - floatingMenuContainerBounds.right === 12) zeroedBorderHorizontal = "Right";
			}
		}
		if (direction === "Left" || direction === "Right") {
			zeroedBorderHorizontal = direction === "Left" ? "Right" : "Left";

			// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
			if (overflowingTop) {
				floatingMenuContentDiv.style.top = `${windowEdgeMargin}px`;
				if (windowBounds.top + floatingMenuContainerBounds.top === 12) zeroedBorderVertical = "Top";
			}
			if (overflowingBottom) {
				floatingMenuContentDiv.style.bottom = `${windowEdgeMargin}px`;
				if (windowBounds.bottom - floatingMenuContainerBounds.bottom === 12) zeroedBorderVertical = "Bottom";
			}
		}

		// Remove the rounded corner from the content where the tail perfectly meets the corner
		if (displayTail && windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
			// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
			switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
				case "TopLeft":
					floatingMenuContentDiv.style.borderTopLeftRadius = "0";
					break;
				case "TopRight":
					floatingMenuContentDiv.style.borderTopRightRadius = "0";
					break;
				case "BottomLeft":
					floatingMenuContentDiv.style.borderBottomLeftRadius = "0";
					break;
				case "BottomRight":
					floatingMenuContentDiv.style.borderBottomRightRadius = "0";
					break;
				default:
					break;
			}
		}
	}

	export function div(): HTMLDivElement | undefined {
		return self;
	}

	// To be called by the parent component. Measures the actual width of the floating menu content element and returns it in a promise.
	export async function measureAndEmitNaturalWidth() {
		if (!measuringOngoingGuard) return;

		// Wait for the changed content which fired the `afterUpdate()` Svelte event to be put into the DOM
		await tick();

		// Wait until all fonts have been loaded and rendered so measurements of content involving text are accurate
		await document.fonts.ready;

		// Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM
		measuringOngoing = true;
		measuringOngoingGuard = true;
		await tick();

		// Measure the width of the floating menu content element, if it's currently visible
		// The result will be `undefined` if the menu is invisible, perhaps because an ancestor component is hidden with a falsy Svelte template if condition
		const naturalWidth: number | undefined = floatingMenuContent?.div?.()?.clientWidth;

		// Turn off measuring mode for the component, which triggers another call to the `afterUpdate()` Svelte event, so we can turn off the protection after that has happened
		measuringOngoing = false;
		await tick();
		measuringOngoingGuard = false;

		// Notify the parent about the measured natural width
		if (naturalWidth !== undefined && naturalWidth >= 0) {
			dispatch("naturalWidth", naturalWidth);
		}
	}

	function pointerMoveHandler(e: PointerEvent) {
		// This element and the element being hovered over
		const target = e.target as HTMLElement | undefined;

		// Get the spawner element (that which is clicked to spawn this floating menu)
		// Assumes the spawner is a sibling of this FloatingMenu component
		const ownSpawner: HTMLElement | undefined = self?.parentElement?.querySelector(":scope > [data-floating-menu-spawner]") || undefined;
		// Get the spawner element containing whatever element the user is hovering over now, if there is one
		const targetSpawner: HTMLElement | undefined = target?.closest?.("[data-floating-menu-spawner]") || undefined;

		// HOVER TRANSFER
		// Transfer from this open floating menu to a sibling floating menu if the pointer hovers to a valid neighboring floating menu spawner
		hoverTransfer(self, ownSpawner, targetSpawner);

		// POINTER STRAY
		// Close the floating menu if the pointer has strayed far enough from its bounds (and it's not hovering over its own spawner)
		const notHoveringOverOwnSpawner = ownSpawner !== targetSpawner;
		if (strayCloses && notHoveringOverOwnSpawner && isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) {
			// TODO: Extend this rectangle bounds check to all submenu bounds up the DOM tree since currently submenus disappear
			// TODO: with zero stray distance if the cursor is further than the stray distance from only the top-level menu
			dispatch("open", false);
		}

		// Clean up any messes from lost pointerup events
		const BUTTONS_LEFT = 0b0000_0001;
		const eventIncludesLmb = Boolean(e.buttons & BUTTONS_LEFT);
		if (!open && !eventIncludesLmb) {
			pointerStillDown = false;
			window.removeEventListener("pointerup", pointerUpHandler);
		}
	}

	function hoverTransfer(self: HTMLDivElement | undefined, ownSpawner: HTMLElement | undefined, targetSpawner: HTMLElement | undefined) {
		// Algorithm pseudo-code to detect and transfer to hover-transferrable floating menu spawners
		// Accompanying diagram: <https://files.keavon.com/-/SpringgreenKnownXantus/capture.png>
		//
		// Check our own parent for descendant spawners
		// Filter out ourself and our children
		// Filter out all with a different distance than our own distance from the currently-being-checked parent
		// How many left?
		//     None -> go up a level and repeat
		//     Some -> is one of them the target?
		//         Yes -> click it and terminate
		//         No -> do nothing and terminate

		// Helper function that gets used below
		const getDepthFromAncestor = (item: Element, ancestor: Element): number | undefined => {
			let depth = 1;

			let parent = item.parentElement || undefined;
			while (parent) {
				if (parent === ancestor) return depth;

				parent = parent.parentElement || undefined;
				depth += 1;
			}

			return undefined;
		};

		// A list of all the descendant spawners: the spawner for this floating menu plus any spawners belonging to widgets inside this floating menu
		const ownDescendantMenuSpawners = Array.from(self?.parentElement?.querySelectorAll("[data-floating-menu-spawner]") || []);

		// Start with the parent of the spawner for this floating menu and keep widening the search for any other valid spawners that are hover-transferrable
		let currentAncestor = (targetSpawner && ownSpawner?.parentElement) || undefined;
		while (currentAncestor) {
			const ownSpawnerDepthFromCurrentAncestor = ownSpawner && getDepthFromAncestor(ownSpawner, currentAncestor);
			const currentAncestor2 = currentAncestor; // This duplicate variable avoids an ESLint warning

			// Get the list of descendant spawners and filter out invalid possibilities for spawners that are hover-transferrable
			const listOfDescendantSpawners = Array.from(currentAncestor?.querySelectorAll("[data-floating-menu-spawner]") || []);
			const filteredListOfDescendantSpawners = listOfDescendantSpawners.filter((item: Element): boolean => {
				// Filter away ourself and our descendants
				const notOurself = !ownDescendantMenuSpawners.includes(item);
				// And filter away unequal depths from the current ancestor
				const notUnequalDepths = notOurself && getDepthFromAncestor(item, currentAncestor2) === ownSpawnerDepthFromCurrentAncestor;
				// And filter away elements that explicitly disable hover transfer
				return notUnequalDepths && !(item as HTMLElement).getAttribute?.("data-floating-menu-spawner")?.includes("no-hover-transfer");
			});

			// If none were found, widen the search by a level and keep trying (or stop looping if the root was reached)
			if (filteredListOfDescendantSpawners.length === 0) {
				currentAncestor = currentAncestor?.parentElement || undefined;
			}
			// Stop after the first non-empty set was found
			else {
				const foundTarget = filteredListOfDescendantSpawners.find((item: Element): boolean => item === targetSpawner);
				// If the currently hovered spawner is one of the found valid hover-transferrable spawners, swap to it by clicking on it
				if (foundTarget) {
					dispatch("open", false);
					(foundTarget as HTMLElement).click();
				}

				// In either case, we are done searching
				break;
			}
		}
	}

	function keyDownHandler(e: KeyboardEvent) {
		if (escapeCloses && e.key === "Escape") {
			setTimeout(() => {
				if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) {
					dispatch("open", false);
				}
			}, 0);

			// Find the parent floating menu and prevent it from also closing with the escape key when this floating menu does
			if (self) preventEscapeClosingParentFloatingMenu(self);
		}
	}

	function pointerDownHandler(e: PointerEvent) {
		// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
		if (isPointerEventOutsideFloatingMenu(e)) {
			dispatch("open", false);

			// Track if the left pointer button is now down so its later click event can be canceled
			const eventIsForLmb = e.button === BUTTON_LEFT;
			if (eventIsForLmb) pointerStillDown = true;
		}
	}

	function pointerUpHandler(e: PointerEvent) {
		const eventIsForLmb = e.button === BUTTON_LEFT;
		if (pointerStillDown && eventIsForLmb) {
			// Clean up self
			pointerStillDown = false;
			window.removeEventListener("pointerup", pointerUpHandler);
			// Prevent the click event from firing, which would normally occur right after this pointerup event
			window.addEventListener("click", clickHandlerCapture, true);
		}
	}

	function clickHandlerCapture(e: MouseEvent) {
		// Stop the click event from reopening this floating menu if the click event targets the floating menu's button
		e.stopPropagation();
		// Clean up self
		window.removeEventListener("click", clickHandlerCapture, true);
	}

	function isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean {
		// Consider all child menus as well as the top-level one
		const allContainedFloatingMenus = [...(self?.querySelectorAll("[data-floating-menu-content]") || [])];

		return !allContainedFloatingMenus.find((element) => !isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed));
	}

	function isPointerEventOutsideMenuElement(e: PointerEvent, element: Element, extraDistanceAllowed = 0): boolean {
		const floatingMenuBounds = element.getBoundingClientRect();

		if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
		if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true;
		if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true;
		if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true;

		return false;
	}
</script>

<div
	class={`floating-menu ${direction.toLowerCase()} ${type.toLowerCase()} ${className} ${extraClasses}`.trim()}
	style={`${styleName} ${extraStyles}`.trim() || undefined}
	bind:this={self}
	{...$$restProps}
>
	{#if displayTail}
		<div class="tail" bind:this={tail} />
	{/if}
	{#if displayContainer}
		<div class="floating-menu-container" bind:this={floatingMenuContainer}>
			<LayoutCol class="floating-menu-content" styles={{ "min-width": minWidthStyleValue }} {scrollableY} bind:this={floatingMenuContent} data-floating-menu-content>
				<slot />
			</LayoutCol>
		</div>
	{/if}
</div>

<style lang="scss" global>
	.floating-menu {
		position: absolute;
		width: 0;
		height: 0;
		display: flex;
		// Floating menus begin at a z-index of 1000
		z-index: 1000;
		--floating-menu-content-offset: 0;

		.tail {
			// Put the tail above the floating menu's shadow
			z-index: 10;
			// Draw over the application without being clipped by the containing panel's `overflow: hidden`
			position: fixed;

			&,
			&::before {
				width: 0;
				height: 0;
				border-style: solid;
			}

			&::before {
				content: "";
				position: absolute;
			}
		}

		.floating-menu-container {
			display: flex;

			.floating-menu-content {
				background: var(--color-2-mildblack);
				box-shadow: rgba(var(--color-0-black-rgb), 0.5) 0 2px 4px;
				border: 1px solid var(--color-4-dimgray);
				border-radius: 4px;
				color: var(--color-e-nearwhite);
				font-size: inherit;
				padding: 8px;
				z-index: 0;
				// Draw over the application without being clipped by the containing panel's `overflow: hidden`
				position: fixed;
				// Counteract the rightward shift caused by the border
				margin-left: -1px;
			}
		}

		&.dropdown {
			&.top {
				width: 100%;
				left: 0;
				top: 0;
			}

			&.bottom {
				width: 100%;
				left: 0;
				bottom: 0;
			}

			&.left {
				height: 100%;
				top: 0;
				left: 0;
			}

			&.right {
				height: 100%;
				top: 0;
				right: 0;
			}

			&.topleft {
				top: 0;
				left: 0;
				margin-top: -4px;
			}

			&.topright {
				top: 0;
				right: 0;
				margin-top: -4px;
			}

			&.topleft {
				bottom: 0;
				left: 0;
				margin-bottom: -4px;
			}

			&.topright {
				bottom: 0;
				right: 0;
				margin-bottom: -4px;
			}
		}

		&.top.dropdown .floating-menu-container,
		&.bottom.dropdown .floating-menu-container {
			justify-content: left;
		}

		&.popover {
			--floating-menu-content-offset: 10px;
		}

		&.cursor .floating-menu-container .floating-menu-content {
			background: none;
			box-shadow: none;
			border-radius: 0;
			padding: 0;
		}

		&.center {
			justify-content: center;
			align-items: center;

			> .floating-menu-container > .floating-menu-content {
				transform: translate(-50%, -50%);
			}
		}

		&.top,
		&.bottom {
			flex-direction: column;
		}

		&.top .tail,
		&.topleft .tail,
		&.topright .tail {
			border-color: var(--color-4-dimgray) transparent transparent transparent;

			&::before {
				border-color: var(--color-2-mildblack) transparent transparent transparent;
				bottom: 0;
			}

			&,
			&::before {
				border-width: 8px 6px 0 6px;
				margin-left: -6px;
				margin-bottom: 2px;
			}
		}

		&.bottom .tail,
		&.bottomleft .tail,
		&.bottomright .tail {
			border-color: transparent transparent var(--color-4-dimgray) transparent;

			&::before {
				border-color: transparent transparent var(--color-2-mildblack) transparent;
				top: 0;
			}

			&,
			&::before {
				border-width: 0 6px 8px 6px;
				margin-left: -6px;
				margin-top: 2px;
			}
		}

		&.left .tail {
			border-color: transparent transparent transparent var(--color-4-dimgray);

			&::before {
				border-color: transparent transparent transparent var(--color-2-mildblack);
				right: 0;
			}

			&,
			&::before {
				border-width: 6px 0 6px 8px;
				margin-top: -6px;
				margin-right: 2px;
			}
		}

		&.right .tail {
			border-color: transparent var(--color-4-dimgray) transparent transparent;

			&::before {
				border-color: transparent var(--color-2-mildblack) transparent transparent;
				left: 0;
			}

			&,
			&::before {
				border-width: 6px 8px 6px 0;
				margin-top: -6px;
				margin-left: 2px;
			}
		}

		&.top .floating-menu-container {
			justify-content: center;
			margin-bottom: var(--floating-menu-content-offset);
		}

		&.bottom .floating-menu-container {
			justify-content: center;
			margin-top: var(--floating-menu-content-offset);
		}

		&.left .floating-menu-container {
			align-items: center;
			margin-right: var(--floating-menu-content-offset);
		}

		&.right .floating-menu-container {
			align-items: center;
			margin-left: var(--floating-menu-content-offset);
		}
	}
</style>
