<!--
Turn-report section menu.

Replaces the F8-09 era 14 rem sticky sidebar (plus its mobile
`<select>` twin) with a single sticky icon-popup trigger that sits
in the top-right corner of the report column. The button shows
`≡` + the title of the section currently in view, derived from the
`activeSlug` prop the orchestrator computes through its
`IntersectionObserver`. Clicking the button opens an anchored menu
(or a bottom-sheet on `<768.98 px`) that lists every section; a
click on an item closes the menu and scrolls the matching section
into view via `scrollIntoView`. The active section gets
`aria-current="location"` plus the `.active` style.

The trigger label is clamped with `text-overflow: ellipsis` so a
long RU title cannot stretch the button. The popover uses the same
open/close state machine as `lib/active-view/map-toggles.svelte`:
outside-click + Escape close, focus is restored to the trigger
through the `restoreFocus` action. No focus trap — the menu is
non-modal.

The component owns no IntersectionObserver and never sets
`activeSlug` itself. The orchestrator picks the new active slug up
naturally as the target section enters the viewport during the
smooth scroll. The previous "Back to map" affordance was dropped
in F8-09: that switch is available in the app-shell view menu.
-->
<script lang="ts">
	import { onMount } from "svelte";

	import { restoreFocus } from "$lib/a11y/restore-focus";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";

	export interface TocEntry {
		slug: string;
		titleKey: TranslationKey;
	}

	type Props = {
		entries: readonly TocEntry[];
		activeSlug: string;
	};

	let { entries, activeSlug }: Props = $props();

	let open = $state(false);
	let rootEl: HTMLDivElement | null = $state(null);

	const activeEntry = $derived(
		entries.find((e) => e.slug === activeSlug) ?? entries[0],
	);
	const activeTitle = $derived(
		activeEntry ? i18n.t(activeEntry.titleKey) : "",
	);

	function scrollToSlug(slug: string): void {
		const target = document.getElementById(`report-${slug}`);
		if (target === null) return;
		const reduced = window.matchMedia(
			"(prefers-reduced-motion: reduce)",
		).matches;
		target.scrollIntoView({
			behavior: reduced ? "auto" : "smooth",
			block: "start",
		});
	}

	function pickSection(slug: string): void {
		open = false;
		// Defer the scroll one frame so Svelte commits the close and
		// `restoreFocus` returns focus to the trigger first. Without
		// this, the focus restoration (the trigger is sticky, but its
		// natural in-flow position is at the top of the report) can
		// reset the smooth/instant scroll the browser had just begun.
		requestAnimationFrame(() => scrollToSlug(slug));
	}

	function onKeyDown(event: KeyboardEvent): void {
		if (event.key === "Escape" && open) {
			open = false;
		}
	}

	onMount(() => {
		const handleClick = (event: MouseEvent): void => {
			if (!open || rootEl === null) return;
			const target = event.target;
			if (target instanceof Node && rootEl.contains(target)) return;
			open = false;
		};
		document.addEventListener("click", handleClick, true);
		document.addEventListener("keydown", onKeyDown);
		return () => {
			document.removeEventListener("click", handleClick, true);
			document.removeEventListener("keydown", onKeyDown);
		};
	});
</script>

<div
	class="report-toc"
	bind:this={rootEl}
	data-testid="report-toc"
	aria-label={i18n.t("game.report.toc.title")}
>
	<button
		type="button"
		class="trigger"
		data-testid="report-toc-trigger"
		aria-haspopup="menu"
		aria-expanded={open}
		aria-label={open
			? i18n.t("game.report.toc.close")
			: i18n.t("game.report.toc.open")}
		onclick={() => (open = !open)}
	>
		<span class="icon" aria-hidden="true">≡</span>
		<span class="label">{activeTitle}</span>
	</button>
	{#if open}
		<div
			class="surface"
			role="menu"
			data-testid="report-toc-surface"
			use:restoreFocus
		>
			{#each entries as entry (entry.slug)}
				<button
					type="button"
					role="menuitem"
					class:active={activeSlug === entry.slug}
					aria-current={activeSlug === entry.slug
						? "location"
						: undefined}
					data-testid="report-toc-item-{entry.slug}"
					onclick={() => pickSection(entry.slug)}
				>
					{i18n.t(entry.titleKey)}
				</button>
			{/each}
		</div>
	{/if}
</div>

<style>
	/*
	The trigger uses `position: fixed` rather than `position: sticky`
	because on desktop the in-game shell's `.active-view-host`
	declares `overflow-y: auto` while staying tall enough that no
	overflow actually engages — the document body scrolls instead.
	Per the CSS sticky spec the host is the "scrollport" for any
	sticky descendant, so a sticky trigger inside the report column
	never receives a scroll event and rides up with the page. Fixed
	positioning anchors the trigger to the viewport directly; the
	component is only mounted while the report active view is on
	screen, so the fixed element is naturally tied to the view's
	lifetime.

	`top: 4rem` clears the sticky header (~3 rem). On ≥ 1024 px the
	sidebar (`lib/sidebar/sidebar.svelte`) is always visible and
	occupies the right 18 rem of the viewport, so the trigger has to
	be pushed left by that much to stay inside the report column.
	Below 1024 px the sidebar collapses into an overlay drawer,
	so a viewport-right anchor of 1.25 rem matches the report's
	right padding.

	When the history banner is showing (historical turn view,
	~2 rem extra) the trigger sits at the banner's lower edge —
	acceptable for the rare historical-turn read path.
	*/
	.report-toc {
		position: fixed;
		top: 4rem;
		right: 1.25rem;
		z-index: 30;
		font-family: system-ui, sans-serif;
	}
	@media (min-width: 1024px) {
		.report-toc {
			right: calc(18rem + 1.25rem);
		}
	}
	.trigger {
		display: inline-flex;
		align-items: center;
		gap: 0.4rem;
		min-height: 44px;
		max-width: min(18rem, calc(100vw - 2rem));
		background: var(--color-surface-overlay);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 6px;
		padding: 0.25rem 0.6rem;
		cursor: pointer;
		font: inherit;
		font-size: 0.9rem;
	}
	.trigger:hover {
		background: var(--color-surface-hover);
	}
	.trigger .icon {
		flex: 0 0 auto;
		font-size: 1.1rem;
		line-height: 1;
	}
	.trigger .label {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		min-width: 0;
	}
	.surface {
		position: absolute;
		top: calc(100% + 0.25rem);
		right: 0;
		min-width: 16rem;
		max-height: calc(100vh - 4rem);
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
		background: var(--color-surface-overlay);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 6px;
		box-shadow: var(--shadow-lg);
		padding: 0.35rem;
		overflow-y: auto;
		z-index: 50;
	}
	.surface [role="menuitem"] {
		text-align: left;
		background: transparent;
		color: var(--color-text-muted);
		border: 0;
		border-left: 2px solid transparent;
		border-radius: 0 3px 3px 0;
		padding: 0.3rem 0.6rem;
		font: inherit;
		font-size: 0.85rem;
		line-height: 1.3;
		cursor: pointer;
	}
	.surface [role="menuitem"]:hover {
		background: var(--color-surface-hover);
		color: var(--color-text);
	}
	.surface [role="menuitem"].active {
		color: var(--color-text);
		background: var(--color-surface);
		border-left-color: var(--color-accent);
	}
	@media (max-width: 767.98px) {
		.report-toc {
			right: 0.75rem;
		}
		.surface {
			position: fixed;
			top: auto;
			left: 0;
			right: 0;
			bottom: 3.25rem;
			max-height: calc(100vh - 6rem);
			border-radius: 0;
			border-left: 0;
			border-right: 0;
			box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
		}
	}
</style>
