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 min-h-9 items-center gap-2 rounded-sm border bg-background-default px-2 py-1 transition-colors",
6 variants: {
7 error: {
8 false:
9 "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong",
10 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"
11 },
12 disabled: {
13 false: "",
14 true: "cursor-not-allowed bg-background-inset text-foreground-disabled"
15 }
16 },
17 defaultVariants: {
18 error: false,
19 disabled: false
20 }
21 });
22
23 export type InputFieldVariants = VariantProps<typeof inputField>;
24</script>
25
26<script lang="ts">
27 import type { Component } from "svelte";
28 import type { HTMLInputAttributes, SvelteHTMLElements } from "svelte/elements";
29 import Spinner from "./Spinner.svelte";
30
31 interface Props extends Omit<HTMLInputAttributes, "class" | "value"> {
32 value?: string;
33 error?: boolean;
34 disabled?: boolean;
35 loading?: boolean;
36 iconLeft?: Component<SvelteHTMLElements["svg"]>;
37 iconRight?: Component<SvelteHTMLElements["svg"]>;
38 /** static, non-editable trailing text shown inside the field, e.g. a domain suffix */
39 suffix?: string;
40 /** the input itself, for callers that have to focus or measure it */
41 element?: HTMLInputElement;
42 class?: string;
43 }
44
45 let {
46 value = $bindable(""),
47 error = false,
48 disabled = false,
49 loading = false,
50 iconLeft,
51 iconRight,
52 suffix,
53 element = $bindable(),
54 class: className,
55 ...rest
56 }: Props = $props();
57
58 const classes = $derived(inputField({ error, disabled, class: className }));
59</script>
60
61<div class={classes}>
62 {#if iconLeft}
63 {@const IconLeft = iconLeft}
64 <IconLeft class="size-4 shrink-0 text-foreground-subtle" aria-hidden="true" />
65 {/if}
66 <input
67 bind:this={element}
68 bind:value
69 {disabled}
70 aria-invalid={error}
71 aria-busy={loading}
72 class="flex-1 bg-transparent outline-none placeholder:text-foreground-placeholder disabled:cursor-not-allowed mt-px"
73 {...rest}
74 />
75 {#if suffix}
76 <span class="shrink-0 text-foreground-muted select-none">{suffix}</span>
77 {/if}
78 {#if loading}
79 <Spinner class="size-4 shrink-0" />
80 {:else if iconRight}
81 {@const IconRight = iconRight}
82 <IconRight class="size-4 shrink-0 text-foreground-subtle" aria-hidden="true" />
83 {/if}
84</div>