This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / Textarea.svelte
3.4 kB 117 lines
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 min-h-64 items-start gap-1 rounded border bg-background-default transition-colors", 7 textarea: 8 "min-h-24 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" 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 element?: HTMLTextAreaElement; 63 } 64 65 let { 66 value = $bindable(""), 67 error = false, 68 disabled = false, 69 loading = false, 70 readonly = false, 71 resizeable = false, 72 iconLeft, 73 iconRight, 74 class: className, 75 element = $bindable(), 76 ...rest 77 }: Props = $props(); 78 79 const { root, textarea } = $derived(textareaField({ error, disabled, readonly, resizeable })); 80 81 // The root no longer carries the text inset (so the scrollbar/resize handle can sit flush 82 // against the border) — the textarea itself owns its horizontal padding, collapsing to 0 on 83 // whichever side an icon (or the loading spinner) already provides the inset instead. 84 const textareaPadding = $derived( 85 [iconLeft ? "" : "pl-2.5", loading ? "pr-[1.875rem]" : iconRight ? "" : "pr-2.5"] 86 .filter(Boolean) 87 .join(" ") 88 ); 89</script> 90 91<div class={root({ class: className })}> 92 {#if iconLeft} 93 {@const IconLeft = iconLeft} 94 <IconLeft 95 class="mt-2.5 mr-1 ml-2.5 size-4 shrink-0 text-foreground-subtle" 96 aria-hidden="true" 97 /> 98 {/if} 99 <textarea 100 bind:this={element} 101 bind:value 102 {disabled} 103 {readonly} 104 aria-invalid={error} 105 aria-busy={loading} 106 class={textarea({ class: textareaPadding })} 107 {...rest}></textarea> 108 {#if loading} 109 <Spinner class="absolute top-2 right-2.5 size-4" /> 110 {:else if iconRight} 111 {@const IconRight = iconRight} 112 <IconRight 113 class="mt-2.5 mr-2.5 ml-1 size-4 shrink-0 text-foreground-subtle" 114 aria-hidden="true" 115 /> 116 {/if} 117</div>