Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
at icy/tmrrpn
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
// 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-sm border bg-background-default transition-colors", textarea: "flex-1 self-stretch bg-transparent py-2 outline-none placeholder:text-foreground-placeholder read-only:resize-none disabled:cursor-not-allowed disabled:resize-none" }, variants: { // 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. `regular` keeps // textareaMinHeight above in sync — it's 2px shorter because the root's border sits // outside it. `small` is for short fields (an ssh key, a one-line note) where the // editor-sized floor would swallow the form. size: { regular: { textarea: "min-h-63.5" }, small: { textarea: "min-h-20" } }, 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, size: "regular" } });
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; size?: TextareaFieldVariants["size"]; 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, size = "regular", iconLeft, iconRight, class: className, element = $bindable(), ...rest }: Props = $props();
const { root, textarea } = $derived( textareaField({ error, disabled, readonly, resizeable, size }) );
// the root carries no text inset, so the scrollbar and the resize handle sit flush // against the border. 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>