<script module lang="ts">
	import type { Component } from "svelte";
	import type { SvelteHTMLElements } from "svelte/elements";
	import { tv, type VariantProps } from "tailwind-variants";

	export interface TabDef {
		id: string;
		label: string;
		href: string;
		icon?: Component<SvelteHTMLElements["svg"]>;
		count?: number;
	}

	export const tabs = tv({
		slots: {
			nav: "",
			plate: "pointer-events-none absolute top-0 left-0",
			item: "relative flex items-center no-underline hover:no-underline",
			icon: "size-4",
			label: "",
			ghost: "invisible h-0 overflow-hidden font-medium select-none",
			count: "rounded-sm px-1 typography-paragraph-small"
		},
		variants: {
			vertical: {
				true: {
					nav: "relative flex w-full flex-col gap-1 bg-background-navigation-frame p-2",
					plate:
						"rounded-sm border border-border-navigation-item-active bg-background-navigation-item",
					item: "min-h-8 w-full gap-1.5 overflow-hidden rounded-sm border border-transparent px-3 py-1.5 typography-paragraph-regular text-foreground-default transition-colors duration-150 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus",
					icon: "size-3.5 shrink-0",
					// Figma clips the item rather than letting a long label wrap it taller; an
					// ellipsis keeps that height without hiding the label outright
					label: "min-w-0 truncate",
					count: "ml-auto"
				},
				false: {
					nav: "relative z-10 flex w-full overflow-x-auto overflow-y-hidden pl-4",
					plate: "rounded-t border border-b-0 border-border-default bg-background-default",
					item: "mr-1 rounded-t border border-b-0 border-transparent px-4 pt-1 pb-1.25 whitespace-nowrap text-foreground-default",
					icon: "mr-2",
					label: "flex flex-col",
					count: "ml-1"
				}
			},
			selected: {
				true: {
					count: "bg-background-muted"
				},
				false: {
					count: "bg-background-strong"
				}
			},
			ready: { true: { plate: "opacity-100" }, false: { plate: "opacity-0" } },
			overlapBottom: { true: { nav: "-mb-px" }, false: {} }
		},
		compoundVariants: [
			{
				vertical: true,
				selected: true,
				ready: false,
				class: {
					item: "border-border-navigation-item-active bg-background-navigation-item"
				}
			},
			{
				vertical: true,
				selected: false,
				class: { item: "hover:bg-background-inset" }
			},
			{
				vertical: false,
				selected: true,
				ready: false,
				class: { item: "border-border-default bg-background-default" }
			},
			{
				vertical: false,
				selected: false,
				class: { item: "hover:bg-background-muted hover:dark:bg-background-subtle/50" }
			}
		],
		defaultVariants: {
			vertical: false,
			selected: false,
			ready: false,
			overlapBottom: false
		}
	});

	export type TabsVariants = VariantProps<typeof tabs>;
</script>

<script lang="ts">
	import { resolve } from "$app/paths";
	import { navigating } from "$app/state";
	import type { ResolvedPathname } from "$app/types";
	import { Spring, prefersReducedMotion } from "svelte/motion";

	interface Props {
		tabs: TabDef[];
		active: string;
		label?: string;
		vertical?: boolean;
		overlapBottom?: boolean;
		class?: string;
	}

	let {
		tabs: defs,
		active,
		label,
		vertical = false,
		overlapBottom = false,
		class: navClass
	}: Props = $props();

	// resolved up front so the plate can match hrefs against the destination url
	const items = $derived(
		defs.map((tab) => ({
			...tab,
			url: (tab.href.startsWith("/") ? resolve(tab.href as "/") : tab.href) as ResolvedPathname
		}))
	);

	// the plate follows where we're heading rather than where we are, so it leaves
	// the moment you click instead of waiting on the route's load; aria-current
	// stays behind on the page that's still on screen
	const selected = $derived.by(() => {
		const dest = navigating.to?.url;
		if (!dest) return active;

		let best: string | undefined;
		let bestLength = -1;
		for (const item of items) {
			if (item.url === dest.pathname + dest.search) return item.id;
			// a plain path also owns everything nested under it, so an issue page
			// keeps the issues tab lit; a query-scoped tab only matches exactly
			if (item.url.includes("?")) continue;
			const owns = dest.pathname === item.url || dest.pathname.startsWith(`${item.url}/`);
			if (owns && item.url.length > bestLength) {
				best = item.id;
				bestLength = item.url.length;
			}
		}
		return best ?? active;
	});

	let nav = $state<HTMLElement>();
	let nodes = $state<(HTMLElement | undefined)[]>([]);
	let box = $state<{ top: number; left: number; width: number; height: number }>();
	let ready = $state(false);

	const plate = new Spring(
		{ pos: 0, size: 0 },
		{ stiffness: 0.145, damping: 0.65, precision: 0.1 }
	);

	const measure = () => {
		const node = nodes[items.findIndex((item) => item.id === selected)];
		if (!node?.offsetParent) return;
		box = {
			top: node.offsetTop,
			left: node.offsetLeft,
			width: node.offsetWidth,
			height: node.offsetHeight
		};
	};

	$effect(() => {
		measure();
	});

	// watch every tab, not just the row: a preceding tab changing width (a count
	// arriving, a webfont landing) shifts the plate without resizing the row
	$effect(() => {
		if (!nav) return;
		const observer = new ResizeObserver(measure);
		observer.observe(nav);
		for (const node of nodes) if (node) observer.observe(node);
		return () => observer.disconnect();
	});

	// the first placement lands instantly so the plate never slides in from nowhere;
	// until it happens the selected tab wears the chrome itself, which is also what
	// server-rendered and script-less pages get
	let placed = false;
	$effect(() => {
		if (!box) return;
		plate.set(vertical ? { pos: box.top, size: box.height } : { pos: box.left, size: box.width }, {
			instant: !placed || prefersReducedMotion.current
		});
		placed = true;
		ready = true;
	});

	const style = $derived(tabs({ vertical, ready, overlapBottom }));

	const plateStyle = $derived(
		vertical
			? `translate: ${box?.left ?? 0}px ${plate.current.pos}px; width: ${box?.width ?? 0}px; height: ${plate.current.size}px`
			: `translate: ${plate.current.pos}px; width: ${plate.current.size}px; height: ${box?.height ?? 0}px`
	);
</script>

<nav class={style.nav({ class: navClass })} aria-label={label} bind:this={nav}>
	<span aria-hidden="true" class={style.plate()} style={plateStyle}></span>
	{#each items as tab, i (tab.id)}
		{@const isSelected = selected === tab.id}
		{@const Glyph = tab.icon}
		<a
			bind:this={nodes[i]}
			href={tab.url}
			aria-current={active === tab.id ? "page" : undefined}
			class={style.item({ selected: isSelected })}
		>
			{#if Glyph}
				<Glyph class={style.icon()} aria-hidden="true" />
			{/if}
			<span class={style.label()}>
				{tab.label}
				{#if !vertical}
					<span aria-hidden="true" class={style.ghost()}>{tab.label}</span>
				{/if}
			</span>
			{#if tab.count}
				<span class={style.count({ selected: isSelected })}>{tab.count}</span>
			{/if}
		</a>
	{/each}
</nav>
