This repository has no description
0

Configure Feed

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

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 <eti@eti.tf>

+60 -7
+36
web/src/lib/components/ui/Input.stories.svelte
··· 2 2 import { defineMeta } from "@storybook/addon-svelte-csf"; 3 3 import Search from "$icon/search"; 4 4 import Mail from "$icon/mail"; 5 + import Button from "./Button.svelte"; 5 6 import Input from "./Input.svelte"; 6 7 7 8 const { Story } = defineMeta({ ··· 9 10 component: Input, 10 11 tags: ["autodocs"], 11 12 argTypes: { 13 + size: { 14 + control: { type: "inline-radio" }, 15 + options: ["sm", "md", "lg"] 16 + }, 12 17 error: { control: "boolean" }, 13 18 disabled: { control: "boolean" }, 14 19 loading: { control: "boolean" }, 15 20 placeholder: { control: "text" } 16 21 }, 17 22 args: { 23 + size: "md", 18 24 error: false, 19 25 disabled: false, 20 26 loading: false, ··· 31 37 <Story name="Loading" args={{ loading: true, value: "Checking..." }} /> 32 38 <Story name="With left icon" args={{ iconLeft: Search, placeholder: "Search" }} /> 33 39 <Story name="With right icon" args={{ iconRight: Mail, placeholder: "you@example.com" }} /> 40 + 41 + <!-- 32/36/40, the same heights Button's sm/md/lg draw, so the two align in a row. --> 42 + <Story name="Sizes" asChild> 43 + <div class="flex items-center gap-3"> 44 + <Input size="sm" placeholder="Small" /> 45 + <Input size="md" placeholder="Medium" /> 46 + <Input size="lg" placeholder="Large" /> 47 + </div> 48 + </Story> 49 + 50 + <!-- the icon rides the size variant's gap and steps up to size-5 at lg, as in Button. --> 51 + <Story name="Icon sizes" asChild> 52 + <div class="flex items-center gap-3"> 53 + <Input size="sm" iconLeft={Search} placeholder="Small" /> 54 + <Input size="md" iconLeft={Search} placeholder="Medium" /> 55 + <Input size="lg" iconLeft={Search} placeholder="Large" /> 56 + </div> 57 + </Story> 58 + 59 + <!-- proof of the alignment claim: matched sizes, identical box heights. --> 60 + <Story name="With Button" asChild> 61 + <div class="flex flex-col items-start gap-3"> 62 + {#each ["sm", "md", "lg"] as const as s (s)} 63 + <div class="flex items-center gap-2"> 64 + <Input size={s} placeholder="Repository name" /> 65 + <Button size={s}>Create</Button> 66 + </div> 67 + {/each} 68 + </div> 69 + </Story>
+24 -7
web/src/lib/components/ui/Input.svelte
··· 2 2 import { tv, type VariantProps } from "tailwind-variants"; 3 3 4 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", 5 + base: "flex items-center rounded-sm border bg-background-default transition-colors", 6 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 + }, 7 17 error: { 8 18 false: 9 19 "border-border-default focus-within:border-border-strong focus-within:ring-1 focus-within:ring-border-strong", ··· 15 25 } 16 26 }, 17 27 defaultVariants: { 28 + size: "md", 18 29 error: false, 19 30 disabled: false 20 31 } ··· 28 39 import type { HTMLInputAttributes, SvelteHTMLElements } from "svelte/elements"; 29 40 import Spinner from "./Spinner.svelte"; 30 41 31 - interface Props extends Omit<HTMLInputAttributes, "class" | "value"> { 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"> { 32 45 value?: string; 46 + size?: InputFieldVariants["size"]; 33 47 error?: boolean; 34 48 disabled?: boolean; 35 49 loading?: boolean; ··· 44 58 45 59 let { 46 60 value = $bindable(""), 61 + size = "md", 47 62 error = false, 48 63 disabled = false, 49 64 loading = false, ··· 55 70 ...rest 56 71 }: Props = $props(); 57 72 58 - const classes = $derived(inputField({ error, disabled, class: className })); 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"); 59 76 </script> 60 77 61 78 <div class={classes}> 62 79 {#if iconLeft} 63 80 {@const IconLeft = iconLeft} 64 - <IconLeft class="size-4 shrink-0 text-foreground-subtle" aria-hidden="true" /> 81 + <IconLeft class="{iconSize} shrink-0 text-foreground-subtle" aria-hidden="true" /> 65 82 {/if} 66 83 <input 67 84 bind:this={element} ··· 69 86 {disabled} 70 87 aria-invalid={error} 71 88 aria-busy={loading} 72 - class="flex-1 bg-transparent outline-none placeholder:text-foreground-placeholder disabled:cursor-not-allowed mt-px" 89 + class="mt-px min-w-0 flex-1 bg-transparent outline-none placeholder:text-foreground-placeholder disabled:cursor-not-allowed" 73 90 {...rest} 74 91 /> 75 92 {#if suffix} 76 93 <span class="shrink-0 text-foreground-muted select-none">{suffix}</span> 77 94 {/if} 78 95 {#if loading} 79 - <Spinner class="size-4 shrink-0" /> 96 + <Spinner class="{iconSize} shrink-0" /> 80 97 {:else if iconRight} 81 98 {@const IconRight = iconRight} 82 - <IconRight class="size-4 shrink-0 text-foreground-subtle" aria-hidden="true" /> 99 + <IconRight class="{iconSize} shrink-0 text-foreground-subtle" aria-hidden="true" /> 83 100 {/if} 84 101 </div>