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