Monorepo for Tangled
0

Configure Feed

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

web/components/ui: improve Textarea

Signed-off-by: eti <eti@eti.tf>

author
eti
committer
dawn
date (Jul 23, 2026, 6:48 PM +0300) commit 0062df69 parent 5d7fa380 change-id mopxtpkr
+48 -12
+7 -1
web/src/lib/components/ui/Textarea.stories.svelte
··· 1 1 <script module lang="ts"> 2 2 import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import Pencil from "$icon/pencil"; 4 + import Sun from "$icon/sun"; 3 5 import Textarea from "./Textarea.svelte"; 4 6 5 7 const { Story } = defineMeta({ ··· 11 13 disabled: { control: "boolean" }, 12 14 loading: { control: "boolean" }, 13 15 readonly: { control: "boolean" }, 16 + resizeable: { control: "boolean" }, 14 17 placeholder: { control: "text" }, 15 18 rows: { control: "number" } 16 19 }, ··· 19 22 disabled: false, 20 23 loading: false, 21 24 readonly: false, 25 + resizeable: false, 22 26 placeholder: "Placeholder" 23 27 } 24 28 }); 25 29 </script> 26 30 27 31 <Story name="Default" /> 28 - <Story name="Focus" args={{ autofocus: true }} /> 32 + <Story name="Focus" args={{ autofocus: true, resizeable: false }} /> 29 33 <Story name="Filled" args={{ value: "Some notes go here" }} /> 30 34 <Story name="Error" args={{ error: true, value: "Invalid value" }} /> 31 35 <Story name="Disabled" args={{ disabled: true, value: "Can't edit this" }} /> 32 36 <Story name="Read-only" args={{ readonly: true, value: "Read-only content" }} /> 33 37 <Story name="Loading" args={{ loading: true, value: "Checking..." }} /> 38 + <Story name="With left icon" args={{ iconLeft: Pencil, placeholder: "Write something" }} /> 39 + <Story name="With right icon" args={{ iconRight: Sun, placeholder: "Write something" }} /> 34 40 35 41 <Story name="States"> 36 42 <div class="flex w-64 flex-col gap-3">
+41 -11
web/src/lib/components/ui/Textarea.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 4 export const textareaField = tv({ 5 - base: "min-h-20 w-full resize-y rounded border bg-background-default px-3 py-2 text-sm transition-colors placeholder:text-foreground-placeholder focus:outline-none", 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 + }, 6 10 variants: { 7 11 error: { 8 - false: 9 - "border-border-default focus:border-border-strong focus:ring-1 focus:ring-border-strong", 10 - true: "border-border-danger bg-background-danger-subtle text-foreground-danger focus:border-border-focus-danger focus:ring-1 focus:ring-border-focus-danger" 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 + } 11 19 }, 12 20 disabled: { 13 21 false: "", 14 - true: "cursor-not-allowed resize-none bg-background-inset text-foreground-disabled" 22 + true: { 23 + root: "cursor-not-allowed border-border-disabled bg-background-subtle text-foreground-muted" 24 + } 15 25 }, 16 26 readonly: { 17 27 false: "", 18 - true: "cursor-default resize-none bg-background-subtle" 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" } 19 35 } 20 36 }, 21 37 defaultVariants: { ··· 29 45 </script> 30 46 31 47 <script lang="ts"> 32 - import type { HTMLTextareaAttributes } from "svelte/elements"; 48 + import type { Component } from "svelte"; 49 + import type { HTMLTextareaAttributes, SvelteHTMLElements } from "svelte/elements"; 33 50 import Spinner from "./Spinner.svelte"; 34 51 35 52 interface Props extends Omit<HTMLTextareaAttributes, "class" | "value"> { ··· 38 55 disabled?: boolean; 39 56 loading?: boolean; 40 57 readonly?: boolean; 58 + resizeable?: boolean; 59 + iconLeft?: Component<SvelteHTMLElements["svg"]>; 60 + iconRight?: Component<SvelteHTMLElements["svg"]>; 41 61 class?: string; 42 62 } 43 63 ··· 47 67 disabled = false, 48 68 loading = false, 49 69 readonly = false, 70 + resizeable = false, 71 + iconLeft, 72 + iconRight, 50 73 class: className, 51 74 ...rest 52 75 }: Props = $props(); 53 76 54 - const classes = $derived(textareaField({ error, disabled, readonly, class: className })); 77 + const { root, textarea } = $derived(textareaField({ error, disabled, readonly, resizeable })); 55 78 </script> 56 79 57 - <div class="relative"> 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} 58 85 <textarea 59 86 bind:value 60 87 {disabled} 61 88 {readonly} 62 89 aria-invalid={error} 63 90 aria-busy={loading} 64 - class={classes} 91 + class={textarea({ class: loading ? "pr-5" : undefined })} 65 92 {...rest}></textarea> 66 93 {#if loading} 67 - <Spinner class="absolute right-2 top-2 size-4" /> 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" /> 68 98 {/if} 69 99 </div>