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
4.0 kB 126 lines
1<script module lang="ts"> 2 import { tv, type VariantProps } from "tailwind-variants"; 3 4 // the field's resting height, exported so anything that swaps itself in for the field — 5 // MarkdownEditor's preview pane — can match it and avoid resizing the box as you toggle 6 // between them. 7 export const textareaMinHeight = "min-h-64"; 8 9 export const textareaField = tv({ 10 slots: { 11 root: "relative flex items-start gap-1 rounded-sm border bg-background-default transition-colors", 12 // the floor has to live on the textarea, not the root: the resize handle only ever 13 // shrinks the textarea, so a min-height on the root would let the handle drag past the 14 // box's own minimum and leave dead space under the field. keep this in sync with 15 // textareaMinHeight above — it's 2px shorter because the root's border sits outside it. 16 textarea: 17 "min-h-63.5 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" 18 }, 19 variants: { 20 error: { 21 false: { 22 root: "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong" 23 }, 24 true: { 25 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", 26 textarea: "placeholder:text-foreground-danger" 27 } 28 }, 29 disabled: { 30 false: "", 31 true: { 32 root: "cursor-not-allowed border-border-disabled bg-background-subtle text-foreground-muted" 33 } 34 }, 35 readonly: { 36 false: "", 37 true: { 38 root: "cursor-default border-border-disabled bg-background-subtle text-foreground-muted" 39 } 40 }, 41 resizeable: { 42 false: { textarea: "resize-none" }, 43 true: { textarea: "resize-y" } 44 } 45 }, 46 defaultVariants: { 47 error: false, 48 disabled: false, 49 readonly: false 50 } 51 }); 52 53 export type TextareaFieldVariants = VariantProps<typeof textareaField>; 54</script> 55 56<script lang="ts"> 57 import type { Component } from "svelte"; 58 import type { HTMLTextareaAttributes, SvelteHTMLElements } from "svelte/elements"; 59 import Spinner from "./Spinner.svelte"; 60 61 interface Props extends Omit<HTMLTextareaAttributes, "class" | "value"> { 62 value?: string; 63 error?: boolean; 64 disabled?: boolean; 65 loading?: boolean; 66 readonly?: boolean; 67 resizeable?: boolean; 68 iconLeft?: Component<SvelteHTMLElements["svg"]>; 69 iconRight?: Component<SvelteHTMLElements["svg"]>; 70 class?: string; 71 element?: HTMLTextAreaElement; 72 } 73 74 let { 75 value = $bindable(""), 76 error = false, 77 disabled = false, 78 loading = false, 79 readonly = false, 80 resizeable = false, 81 iconLeft, 82 iconRight, 83 class: className, 84 element = $bindable(), 85 ...rest 86 }: Props = $props(); 87 88 const { root, textarea } = $derived(textareaField({ error, disabled, readonly, resizeable })); 89 90 // The root no longer carries the text inset (so the scrollbar/resize handle can sit flush 91 // against the border) — the textarea itself owns its horizontal padding, collapsing to 0 on 92 // whichever side an icon (or the loading spinner) already provides the inset instead. 93 const textareaPadding = $derived( 94 [iconLeft ? "" : "pl-2.5", loading ? "pr-[1.875rem]" : iconRight ? "" : "pr-2.5"] 95 .filter(Boolean) 96 .join(" ") 97 ); 98</script> 99 100<div class={root({ class: className })}> 101 {#if iconLeft} 102 {@const IconLeft = iconLeft} 103 <IconLeft 104 class="mt-2.5 mr-1 ml-2.5 size-4 shrink-0 text-foreground-subtle" 105 aria-hidden="true" 106 /> 107 {/if} 108 <textarea 109 bind:this={element} 110 bind:value 111 {disabled} 112 {readonly} 113 aria-invalid={error} 114 aria-busy={loading} 115 class={textarea({ class: textareaPadding })} 116 {...rest}></textarea> 117 {#if loading} 118 <Spinner class="absolute top-2 right-2.5 size-4" /> 119 {:else if iconRight} 120 {@const IconRight = iconRight} 121 <IconRight 122 class="mt-2.5 mr-2.5 ml-1 size-4 shrink-0 text-foreground-subtle" 123 aria-hidden="true" 124 /> 125 {/if} 126</div>