Monorepo for Tangled
tangled.org
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 export const textareaField = tv({
5 slots: {
6 root: "relative flex max-h-64 items-start gap-1 rounded border bg-background-default px-2.5 py-2 transition-colors",
7 textarea:
8 "min-h-24 flex-1 bg-transparent text-sm leading-6 outline-none placeholder:text-foreground-placeholder read-only:resize-none disabled:cursor-not-allowed disabled:resize-none"
9 },
10 variants: {
11 error: {
12 false: {
13 root: "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong"
14 },
15 true: {
16 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",
17 textarea: "placeholder:text-foreground-danger"
18 }
19 },
20 disabled: {
21 false: "",
22 true: {
23 root: "cursor-not-allowed border-border-disabled bg-background-subtle text-foreground-muted"
24 }
25 },
26 readonly: {
27 false: "",
28 true: {
29 root: "cursor-default border-border-disabled bg-background-subtle text-foreground-muted"
30 }
31 },
32 resizeable: {
33 false: { textarea: "resize-none" },
34 true: { textarea: "resize-y" }
35 }
36 },
37 defaultVariants: {
38 error: false,
39 disabled: false,
40 readonly: false
41 }
42 });
43
44 export type TextareaFieldVariants = VariantProps<typeof textareaField>;
45</script>
46
47<script lang="ts">
48 import type { Component } from "svelte";
49 import type { HTMLTextareaAttributes, SvelteHTMLElements } from "svelte/elements";
50 import Spinner from "./Spinner.svelte";
51
52 interface Props extends Omit<HTMLTextareaAttributes, "class" | "value"> {
53 value?: string;
54 error?: boolean;
55 disabled?: boolean;
56 loading?: boolean;
57 readonly?: boolean;
58 resizeable?: boolean;
59 iconLeft?: Component<SvelteHTMLElements["svg"]>;
60 iconRight?: Component<SvelteHTMLElements["svg"]>;
61 class?: string;
62 }
63
64 let {
65 value = $bindable(""),
66 error = false,
67 disabled = false,
68 loading = false,
69 readonly = false,
70 resizeable = false,
71 iconLeft,
72 iconRight,
73 class: className,
74 ...rest
75 }: Props = $props();
76
77 const { root, textarea } = $derived(textareaField({ error, disabled, readonly, resizeable }));
78</script>
79
80<div class={root({ class: className })}>
81 {#if iconLeft}
82 {@const IconLeft = iconLeft}
83 <IconLeft class="size-4 mt-0.5 mr-1 shrink-0 text-foreground-subtle" aria-hidden="true" />
84 {/if}
85 <textarea
86 bind:value
87 {disabled}
88 {readonly}
89 aria-invalid={error}
90 aria-busy={loading}
91 class={textarea({ class: loading ? "pr-5" : undefined })}
92 {...rest}></textarea>
93 {#if loading}
94 <Spinner class="absolute right-2.5 top-2 size-4" />
95 {:else if iconRight}
96 {@const IconRight = iconRight}
97 <IconRight class="size-4 mt-0.5 ml-1 shrink-0 text-foreground-subtle" aria-hidden="true" />
98 {/if}
99</div>