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

	export const button = tv({
		base: [
			"inline-flex cursor-pointer items-center justify-center overflow-hidden rounded-l-(--btn-radius-l) rounded-r-(--btn-radius-r) no-underline select-none [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] [--btn-radius:var(--radius-sm)]",
			"transition-colors duration-150 ease-in-out",
			"hover:no-underline",
			"focus-visible:outline-2 focus-visible:outline-offset-2",
			// each variant paints its own disabled surface (see the variant blocks) —
			// Figma models disabled as a distinct fill/text pair, not a dimmed enabled state
			"disabled:pointer-events-none aria-disabled:pointer-events-none"
		],
		variants: {
			variant: {
				default: [
					"border border-border-default bg-background-default text-foreground-default",
					"hover:bg-background-subtle",
					"focus-visible:outline-border-focus",
					"disabled:border-transparent disabled:bg-background-subtle disabled:text-foreground-subtle",
					"aria-disabled:border-transparent aria-disabled:bg-background-subtle aria-disabled:text-foreground-subtle"
				],
				primary: [
					"border border-border-success bg-background-success-emphasis text-foreground-on-emphasis",
					"hover:bg-background-success-emphasis-hover",
					"focus-visible:outline-background-success-emphasis",
					"disabled:border-transparent disabled:bg-background-success-subtle disabled:text-foreground-success-disabled",
					"aria-disabled:border-transparent aria-disabled:bg-background-success-subtle aria-disabled:text-foreground-success-disabled"
				],
				ghost: [
					"border border-transparent bg-transparent",
					"hover:bg-background-inset",
					"focus-visible:outline-border-focus",
					"disabled:text-foreground-disabled aria-disabled:text-foreground-disabled"
				],
				// danger / success / warning are outline buttons in Figma: default surface +
				// border, colored text only (bound to the fg-* tokens), no accent fill. They
				// keep that surface when disabled and only fade the label — the outline is
				// what carries the variant, so dropping it would read as a different button.
				danger: [
					"border border-border-default bg-background-default text-foreground-danger",
					"hover:bg-background-subtle",
					"focus-visible:outline-foreground-danger",
					"disabled:text-foreground-danger-disabled aria-disabled:text-foreground-danger-disabled"
				],
				success: [
					"border border-border-default bg-background-default text-foreground-success",
					"hover:bg-background-subtle",
					"focus-visible:outline-foreground-success",
					"disabled:text-foreground-success-disabled aria-disabled:text-foreground-success-disabled"
				],
				warning: [
					"border border-border-default bg-background-default text-foreground-warning",
					"hover:bg-background-subtle",
					"focus-visible:outline-foreground-warning",
					"disabled:text-foreground-warning-disabled aria-disabled:text-foreground-warning-disabled"
				]
			},
			// the min-h is the button's real height (32/36/40 in Figma), so py has to stay
			// small enough that line-height + padding + border never exceeds it — otherwise a
			// button with a label grows past the min-h while an icon-only one stays on it, and
			// the two render at different heights side by side. py only takes over as the
			// floor when a label wraps to a second line.
			size: {
				sm: "min-h-8 gap-1.5 px-2 py-1 typography-paragraph-small",
				md: "min-h-9 gap-2 px-3 py-1 typography-paragraph-regular",
				lg: "min-h-10 gap-2.5 px-4 py-1 typography-paragraph-large"
			},
			insetShadow: {
				true: "",
				false: ""
			}
		},
		compoundVariants: [
			// raised variants draw inset depth via a ::before layer. pressing shifts the
			// content wrapper rather than the button itself, so the box stays put.
			{
				variant: "default",
				insetShadow: true,
				class: [
					"relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)]",
					"hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]",
					"active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]",
					"disabled:before:shadow-none disabled:active:*:translate-y-0"
				]
			},
			{
				variant: "primary",
				insetShadow: true,
				class: [
					"relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-alpha-light-25)]",
					"active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-dark-50)] dark:active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-light-50)]",
					"disabled:before:shadow-none disabled:active:*:translate-y-0"
				]
			}
		],
		defaultVariants: {
			variant: "default",
			size: "md",
			insetShadow: false
		}
	});

	export type ButtonVariants = VariantProps<typeof button>;
</script>

<script lang="ts">
	import type { ResolvedPathname } from "$app/types";
	import type { Component, Snippet } from "svelte";
	import type {
		HTMLAnchorAttributes,
		HTMLButtonAttributes,
		SvelteHTMLElements
	} from "svelte/elements";
	import Spinner from "./Spinner.svelte";

	interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> {
		variant?: ButtonVariants["variant"];
		size?: ButtonVariants["size"];
		insetShadow?: boolean;
		href?: ResolvedPathname;
		type?: "button" | "submit" | "reset";
		disabled?: boolean;
		icon?: Component<SvelteHTMLElements["svg"]>;
		iconSide?: string;
		class?: string;
		loading?: boolean;
		spinnerClass?: string;
		ref?: HTMLAnchorElement | HTMLButtonElement;
		children?: Snippet;
	}

	let {
		variant = "default",
		size = "md",
		insetShadow = false,
		href,
		type = "button",
		disabled = false,
		icon,
		iconSide = "left",
		class: className,
		loading = false,
		spinnerClass,
		ref = $bindable(),
		children,
		...rest
	}: Props = $props();

	const classes = $derived(button({ variant, size, insetShadow, class: className }));
	const spinnerSize = $derived(spinnerClass ?? (size === "lg" ? "size-5" : "size-4"));
	const iconSize = $derived(size === "lg" ? "size-5" : "size-4");
	const Icon = $derived(icon);
</script>

{#snippet content()}
	<!-- single wrapper so the press animation moves the label/icon, not the button box.
	     gap-[inherit] keeps the size variant's gap working across the extra level, and the
	     transition matches the ::before shadow's so both halves of the press ease together. -->
	<span
		class="inline-flex min-w-0 items-center justify-center gap-[inherit] transition-transform duration-150 ease-in-out"
	>
		{#if loading}
			<Spinner class={spinnerSize} />
		{:else}
			{#if Icon && iconSide == "left"}
				<Icon class="{iconSize} shrink-0" aria-hidden="true" />
			{/if}
			{#if children}
				{@render children()}
			{/if}
			{#if Icon && iconSide == "right"}
				<Icon class="{iconSize} shrink-0" aria-hidden="true" />
			{/if}
		{/if}
	</span>
{/snippet}

{#if href}
	<a
		bind:this={ref}
		{href}
		class={classes}
		data-variant={variant}
		aria-disabled={disabled || loading ? "true" : undefined}
		tabindex={disabled ? -1 : undefined}
		{...rest}
	>
		{@render content()}
	</a>
{:else}
	<button
		bind:this={ref}
		{type}
		disabled={disabled || loading}
		class={classes}
		data-variant={variant}
		aria-busy={loading}
		{...rest}
	>
		{@render content()}
	</button>
{/if}
