This repository has no description
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>