<script module lang="ts">
	import { tv, type VariantProps } from "$lib/tv";

	// the field's resting height, exported so anything that swaps itself in for the field —
	// MarkdownEditor's preview pane — can match it and avoid resizing the box as you toggle
	// between them.
	export const textareaMinHeight = "min-h-64";

	export const textareaField = tv({
		slots: {
			root: "relative flex items-start gap-1 rounded border bg-background-default transition-colors",
			// the floor has to live on the textarea, not the root: the resize handle only ever
			// shrinks the textarea, so a min-height on the root would let the handle drag past the
			// box's own minimum and leave dead space under the field. keep this in sync with
			// textareaMinHeight above — it's 2px shorter because the root's border sits outside it.
			textarea:
				"min-h-[calc(16rem-2px)] flex-1 self-stretch bg-transparent py-2 text-sm leading-6 outline-none placeholder:text-foreground-placeholder read-only:resize-none disabled:cursor-not-allowed disabled:resize-none"
		},
		variants: {
			error: {
				false: {
					root: "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong"
				},
				true: {
					root: "border-border-danger bg-background-danger-subtle text-foreground-danger focus-within:border-border-focus-danger focus-within:ring-1 focus-within:ring-border-focus-danger",
					textarea: "placeholder:text-foreground-danger"
				}
			},
			disabled: {
				false: "",
				true: {
					root: "cursor-not-allowed border-border-disabled bg-background-subtle text-foreground-muted"
				}
			},
			readonly: {
				false: "",
				true: {
					root: "cursor-default border-border-disabled bg-background-subtle text-foreground-muted"
				}
			},
			resizeable: {
				false: { textarea: "resize-none" },
				true: { textarea: "resize-y" }
			}
		},
		defaultVariants: {
			error: false,
			disabled: false,
			readonly: false
		}
	});

	export type TextareaFieldVariants = VariantProps<typeof textareaField>;
</script>

<script lang="ts">
	import type { Component } from "svelte";
	import type { HTMLTextareaAttributes, SvelteHTMLElements } from "svelte/elements";
	import Spinner from "./Spinner.svelte";

	interface Props extends Omit<HTMLTextareaAttributes, "class" | "value"> {
		value?: string;
		error?: boolean;
		disabled?: boolean;
		loading?: boolean;
		readonly?: boolean;
		resizeable?: boolean;
		iconLeft?: Component<SvelteHTMLElements["svg"]>;
		iconRight?: Component<SvelteHTMLElements["svg"]>;
		class?: string;
		element?: HTMLTextAreaElement;
	}

	let {
		value = $bindable(""),
		error = false,
		disabled = false,
		loading = false,
		readonly = false,
		resizeable = false,
		iconLeft,
		iconRight,
		class: className,
		element = $bindable(),
		...rest
	}: Props = $props();

	const { root, textarea } = $derived(textareaField({ error, disabled, readonly, resizeable }));

	// The root no longer carries the text inset (so the scrollbar/resize handle can sit flush
	// against the border) — the textarea itself owns its horizontal padding, collapsing to 0 on
	// whichever side an icon (or the loading spinner) already provides the inset instead.
	const textareaPadding = $derived(
		[iconLeft ? "" : "pl-2.5", loading ? "pr-[1.875rem]" : iconRight ? "" : "pr-2.5"]
			.filter(Boolean)
			.join(" ")
	);
</script>

<div class={root({ class: className })}>
	{#if iconLeft}
		{@const IconLeft = iconLeft}
		<IconLeft
			class="mt-2.5 mr-1 ml-2.5 size-4 shrink-0 text-foreground-subtle"
			aria-hidden="true"
		/>
	{/if}
	<textarea
		bind:this={element}
		bind:value
		{disabled}
		{readonly}
		aria-invalid={error}
		aria-busy={loading}
		class={textarea({ class: textareaPadding })}
		{...rest}></textarea>
	{#if loading}
		<Spinner class="absolute top-2 right-2.5 size-4" />
	{:else if iconRight}
		{@const IconRight = iconRight}
		<IconRight
			class="mt-2.5 mr-2.5 ml-1 size-4 shrink-0 text-foreground-subtle"
			aria-hidden="true"
		/>
	{/if}
</div>
