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