From e80dcafcc87e802b4b411d76350484e4acad8471 Mon Sep 17 00:00:00 2001 From: eti Date: Tue, 4 Aug 2026 17:22:17 +0200 Subject: [PATCH] web/components: give Input the same size prop as Button Input only ever drew one height. It now takes size="sm", "md" or "lg" just like Button, and both land on the same heights: 32, 36 and 40 pixels. So an input and a button sitting next to each other line up. The text and the icons grow with the box as well. The only thing that does not copy Button is the side padding, which stays a little tighter, because a button centres its label and needs the room while an input's text starts against the left edge. That also keeps "md" looking exactly the way it looked before, so none of the inputs already in the app move. Two small fixes along the way. The inner field can now shrink, so in a narrow input the suffix and the loading spinner stay inside the border instead of spilling out over whatever is beside them. And the new size prop takes the place of the native size attribute, which sets a width in characters and was not used anywhere. Figma has no size variant for Button or Input, so Button's own code was the only thing to match here. Signed-off-by: eti --- .../lib/components/ui/Input.stories.svelte | 36 +++++++++++++++++++ web/src/lib/components/ui/Input.svelte | 31 ++++++++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/ui/Input.stories.svelte b/web/src/lib/components/ui/Input.stories.svelte index 9dd19f57..7419759b 100644 --- a/web/src/lib/components/ui/Input.stories.svelte +++ b/web/src/lib/components/ui/Input.stories.svelte @@ -2,6 +2,7 @@ import { defineMeta } from "@storybook/addon-svelte-csf"; import Search from "$icon/search"; import Mail from "$icon/mail"; + import Button from "./Button.svelte"; import Input from "./Input.svelte"; const { Story } = defineMeta({ @@ -9,12 +10,17 @@ component: Input, tags: ["autodocs"], argTypes: { + size: { + control: { type: "inline-radio" }, + options: ["sm", "md", "lg"] + }, error: { control: "boolean" }, disabled: { control: "boolean" }, loading: { control: "boolean" }, placeholder: { control: "text" } }, args: { + size: "md", error: false, disabled: false, loading: false, @@ -31,3 +37,33 @@ + + + +
+ + + +
+
+ + + +
+ + + +
+
+ + + +
+ {#each ["sm", "md", "lg"] as const as s (s)} +
+ + +
+ {/each} +
+
diff --git a/web/src/lib/components/ui/Input.svelte b/web/src/lib/components/ui/Input.svelte index 83278b47..5e0681f4 100644 --- a/web/src/lib/components/ui/Input.svelte +++ b/web/src/lib/components/ui/Input.svelte @@ -2,8 +2,18 @@ import { tv, type VariantProps } from "tailwind-variants"; export const inputField = tv({ - base: "flex min-h-9 items-center gap-2 rounded-sm border bg-background-default px-2 py-1 transition-colors", + base: "flex items-center rounded-sm border bg-background-default transition-colors", variants: { + // same 32/36/40 heights as Button's sm/md/lg, so an input and a button of the same + // size line up in a row. line-height + py + border stays under each min-h, so the + // field sits exactly on that height whatever it holds. horizontal padding runs one + // step tighter than Button's — a button centres its label and needs the room, a + // field's text is left-aligned against the border. + size: { + sm: "min-h-8 gap-1.5 px-1.5 py-1 typography-paragraph-small", + md: "min-h-9 gap-2 px-2 py-1 typography-paragraph-regular", + lg: "min-h-10 gap-2.5 px-3 py-1 typography-paragraph-large" + }, error: { false: "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong", @@ -15,6 +25,7 @@ } }, defaultVariants: { + size: "md", error: false, disabled: false } @@ -28,8 +39,11 @@ import type { HTMLInputAttributes, SvelteHTMLElements } from "svelte/elements"; import Spinner from "./Spinner.svelte"; - interface Props extends Omit { + // `size` shadows the native character-width attribute, which nothing here uses and which + // no design would reach for over a width class + interface Props extends Omit { value?: string; + size?: InputFieldVariants["size"]; error?: boolean; disabled?: boolean; loading?: boolean; @@ -44,6 +58,7 @@ let { value = $bindable(""), + size = "md", error = false, disabled = false, loading = false, @@ -55,13 +70,15 @@ ...rest }: Props = $props(); - const classes = $derived(inputField({ error, disabled, class: className })); + const classes = $derived(inputField({ size, error, disabled, class: className })); + // matches Button: the icon only steps up on lg + const iconSize = $derived(size === "lg" ? "size-5" : "size-4");
{#if iconLeft} {@const IconLeft = iconLeft} -
-- 2.51.2