<!--
  Per-row "⋮" kebab menu. Hides secondary actions (edit, deactivate,
  delete, etc.) behind a single trigger so list rows stay tidy.

  Usage:
    <ActionMenu
      items={[
        { label: 'Edit', onClick: () => openEdit(row) },
        { label: row.is_active ? 'Deactivate' : 'Reactivate',
          onClick: () => toggleActive(row) },
        { label: 'Delete', danger: true, onClick: () => openDelete(row),
          disabled: !canDelete(row) },
      ]}
    />

  Closes on: item click, click outside, ESC, scroll/resize. Keyboard:
  Enter/Space opens; Up/Down navigate; Enter activates; ESC closes and
  re-focuses the trigger. The popover is absolutely positioned relative
  to the trigger and right-anchored — the parent must allow overflow
  (`overflow: visible`) for it to extend past the row.
-->
<script lang="ts">
	export interface MenuItem {
		label: string;
		onClick: () => void;
		danger?: boolean;
		disabled?: boolean;
	}

	interface Props {
		items: MenuItem[];
		/** Accessible label for the trigger button. */
		label?: string;
	}

	let { items, label = 'More actions' }: Props = $props();

	let open = $state(false);
	let triggerEl = $state<HTMLButtonElement | null>(null);
	let menuEl = $state<HTMLDivElement | null>(null);
	let activeIndex = $state(-1);

	let enabledIndices = $derived(
		items
			.map((it, i) => (it.disabled ? -1 : i))
			.filter((i) => i >= 0)
	);

	function toggle() {
		open ? close() : openMenu();
	}

	function openMenu() {
		open = true;
		activeIndex = enabledIndices[0] ?? -1;
	}

	function close(refocus = false) {
		open = false;
		activeIndex = -1;
		if (refocus) triggerEl?.focus();
	}

	function activate(index: number) {
		const item = items[index];
		if (!item || item.disabled) return;
		close();
		item.onClick();
	}

	function moveActive(step: 1 | -1) {
		if (enabledIndices.length === 0) return;
		const cur = enabledIndices.indexOf(activeIndex);
		const next =
			cur === -1
				? enabledIndices[0]
				: enabledIndices[(cur + step + enabledIndices.length) % enabledIndices.length];
		activeIndex = next;
	}

	function onTriggerKeydown(e: KeyboardEvent) {
		if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
			e.preventDefault();
			if (!open) openMenu();
		}
	}

	function onMenuKeydown(e: KeyboardEvent) {
		switch (e.key) {
			case 'ArrowDown':
				e.preventDefault();
				moveActive(1);
				break;
			case 'ArrowUp':
				e.preventDefault();
				moveActive(-1);
				break;
			case 'Enter':
			case ' ':
				e.preventDefault();
				if (activeIndex >= 0) activate(activeIndex);
				break;
			case 'Escape':
				e.preventDefault();
				close(true);
				break;
			case 'Tab':
				close();
				break;
		}
	}

	function onWindowMouseDown(e: MouseEvent) {
		if (!open) return;
		const target = e.target as Node;
		if (menuEl?.contains(target) || triggerEl?.contains(target)) return;
		close();
	}

	// Close on viewport changes — naive but enough; without a portal a
	// scrolling list would otherwise leave the popover drifting away from
	// its row.
	function onViewportChange() {
		if (open) close();
	}
</script>

<svelte:window
	onmousedown={onWindowMouseDown}
	onscroll={onViewportChange}
	onresize={onViewportChange}
/>

<div class="wrap">
	<button
		bind:this={triggerEl}
		type="button"
		class="trigger"
		class:open
		aria-label={label}
		aria-haspopup="menu"
		aria-expanded={open}
		onclick={toggle}
		onkeydown={onTriggerKeydown}
	>
		<!-- vertical ellipsis ⋮ — kept inline as text so it inherits color -->
		<span aria-hidden="true">⋮</span>
	</button>

	{#if open}
		<div
			bind:this={menuEl}
			class="menu"
			role="menu"
			tabindex="-1"
			onkeydown={onMenuKeydown}
		>
			{#each items as item, i (i)}
				<button
					type="button"
					role="menuitem"
					class="item"
					class:danger={item.danger}
					class:active={i === activeIndex}
					disabled={item.disabled}
					onclick={() => activate(i)}
					onmouseenter={() => {
						if (!item.disabled) activeIndex = i;
					}}
				>
					{item.label}
				</button>
			{/each}
		</div>
	{/if}
</div>

<style>
	.wrap {
		position: relative;
		display: inline-flex;
		justify-content: flex-end;
	}

	.trigger {
		background: transparent;
		color: #94a3b8;
		border: 1px solid transparent;
		width: 1.75rem;
		height: 1.75rem;
		border-radius: 0.25rem;
		font: inherit;
		font-size: 1.1rem;
		line-height: 1;
		cursor: pointer;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		padding: 0;
	}

	.trigger:hover,
	.trigger:focus-visible,
	.trigger.open {
		background: #1e293b;
		color: #e2e8f0;
		border-color: #334155;
		outline: none;
	}

	.menu {
		position: absolute;
		top: calc(100% + 4px);
		right: 0;
		min-width: 9rem;
		background: #0f172a;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		box-shadow: 0 10px 25px -10px rgba(0, 0, 0, 0.6);
		padding: 0.25rem;
		display: flex;
		flex-direction: column;
		z-index: 50;
	}

	.item {
		background: transparent;
		color: #cbd5e1;
		border: none;
		text-align: left;
		padding: 0.4rem 0.6rem;
		font: inherit;
		font-size: 0.8rem;
		border-radius: 0.25rem;
		cursor: pointer;
	}

	.item.active:not(:disabled) {
		background: #1e293b;
		color: #e2e8f0;
	}

	.item.danger {
		color: #fca5a5;
	}

	.item.danger.active:not(:disabled) {
		background: #450a0a;
		color: #fecaca;
	}

	.item:disabled {
		opacity: 0.45;
		cursor: not-allowed;
	}
</style>
