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 / Input.svelte
3.4 kB 101 lines
1<script module lang="ts"> 2 import { tv, type VariantProps } from "tailwind-variants"; 3 4 export const inputField = tv({ 5 base: "flex items-center rounded-sm border bg-background-default transition-colors", 6 variants: { 7 // same 32/36/40 heights as Button's sm/md/lg, so an input and a button of the same 8 // size line up in a row. line-height + py + border stays under each min-h, so the 9 // field sits exactly on that height whatever it holds. horizontal padding runs one 10 // step tighter than Button's — a button centres its label and needs the room, a 11 // field's text is left-aligned against the border. 12 size: { 13 sm: "min-h-8 gap-1.5 px-1.5 py-1 typography-paragraph-small", 14 md: "min-h-9 gap-2 px-2 py-1 typography-paragraph-regular", 15 lg: "min-h-10 gap-2.5 px-3 py-1 typography-paragraph-large" 16 }, 17 error: { 18 false: 19 "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong", 20 true: "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 }, 22 disabled: { 23 false: "", 24 true: "cursor-not-allowed bg-background-inset text-foreground-disabled" 25 } 26 }, 27 defaultVariants: { 28 size: "md", 29 error: false, 30 disabled: false 31 } 32 }); 33 34 export type InputFieldVariants = VariantProps<typeof inputField>; 35</script> 36 37<script lang="ts"> 38 import type { Component } from "svelte"; 39 import type { HTMLInputAttributes, SvelteHTMLElements } from "svelte/elements"; 40 import Spinner from "./Spinner.svelte"; 41 42 // `size` shadows the native character-width attribute, which nothing here uses and which 43 // no design would reach for over a width class 44 interface Props extends Omit<HTMLInputAttributes, "class" | "value" | "size"> { 45 value?: string; 46 size?: InputFieldVariants["size"]; 47 error?: boolean; 48 disabled?: boolean; 49 loading?: boolean; 50 iconLeft?: Component<SvelteHTMLElements["svg"]>; 51 iconRight?: Component<SvelteHTMLElements["svg"]>; 52 /** static, non-editable trailing text shown inside the field, e.g. a domain suffix */ 53 suffix?: string; 54 /** the input itself, for callers that have to focus or measure it */ 55 element?: HTMLInputElement; 56 class?: string; 57 } 58 59 let { 60 value = $bindable(""), 61 size = "md", 62 error = false, 63 disabled = false, 64 loading = false, 65 iconLeft, 66 iconRight, 67 suffix, 68 element = $bindable(), 69 class: className, 70 ...rest 71 }: Props = $props(); 72 73 const classes = $derived(inputField({ size, error, disabled, class: className })); 74 // matches Button: the icon only steps up on lg 75 const iconSize = $derived(size === "lg" ? "size-5" : "size-4"); 76</script> 77 78<div class={classes}> 79 {#if iconLeft} 80 {@const IconLeft = iconLeft} 81 <IconLeft class="{iconSize} shrink-0 text-foreground-subtle" aria-hidden="true" /> 82 {/if} 83 <input 84 bind:this={element} 85 bind:value 86 {disabled} 87 aria-invalid={error} 88 aria-busy={loading} 89 class="mt-px min-w-0 flex-1 bg-transparent outline-none placeholder:text-foreground-placeholder disabled:cursor-not-allowed" 90 {...rest} 91 /> 92 {#if suffix} 93 <span class="shrink-0 text-foreground-muted select-none">{suffix}</span> 94 {/if} 95 {#if loading} 96 <Spinner class="{iconSize} shrink-0" /> 97 {:else if iconRight} 98 {@const IconRight = iconRight} 99 <IconRight class="{iconSize} shrink-0 text-foreground-subtle" aria-hidden="true" /> 100 {/if} 101</div>