Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226<script module lang="ts"> import { defineMeta } from "@storybook/addon-svelte-csf"; import Search from "$icon/search"; import Mail from "$icon/mail"; import Tag from "$icon/tag"; import Button from "$lib/components/ui/Button.svelte"; import Input from "$lib/components/ui/Input.svelte"; import User from "$lib/components/ui/User.svelte"; import type { InputTag } from "$lib/components/ui/inputField";
const { Story } = defineMeta({ title: "UI/Input", component: Input, tags: ["autodocs"], argTypes: { size: { control: { type: "inline-radio" }, options: ["small", "regular", "large"] }, error: { control: "boolean" }, disabled: { control: "boolean" }, loading: { control: "boolean" }, rich: { control: "boolean" }, commitOn: { control: { type: "inline-check" }, options: ["enter", "space", "comma"] }, placeholder: { control: "text" } }, args: { size: "regular", error: false, disabled: false, loading: false, rich: false, placeholder: "Placeholder" } });</script>
<script lang="ts"> let topics = $state<InputTag[]>([{ value: "svelte" }, { value: "motion" }]); let filters = $state<InputTag[]>([ { field: "author", value: "eti", color: "info" }, { field: "label", value: "needs-review", color: "warning" } ]); let entered = $state<InputTag[]>([{ value: "svelte" }]); let spaced = $state<InputTag[]>([{ value: "motion" }]); let phrases = $state<InputTag[]>([{ value: "needs a review" }]); let reviewers = $state<InputTag[]>([{ value: "eti.tf" }]); let capped = $state<InputTag[]>([{ value: "one" }, { value: "two" }]);
const people = [ "eti.tf", "oppi.li", "icyphox.sh", "seongmin.bsky.social", "akshay.tangled.sh", "nikhil.tangled.sh" ].map((handle) => ({ value: handle }));</script>
<Story name="Default" /><Story name="Focus" args={{ autofocus: true }} /><Story name="Filled" args={{ value: "Filled value" }} /><Story name="Error" args={{ error: true, value: "Invalid value" }} /><Story name="Disabled" args={{ disabled: true, value: "Can't edit this" }} /><Story name="Loading" args={{ loading: true, value: "Checking..." }} /><Story name="With left icon" args={{ iconLeft: Search, placeholder: "Search" }} /><Story name="With right icon" args={{ iconRight: Mail, placeholder: "you@example.com" }} />
<!-- 32/36/40, the same heights Button's sm/md/lg draw, so the two align in a row. --><Story name="Sizes" asChild> <div class="flex items-center gap-sm"> <Input size="small" placeholder="Small" /> <Input size="regular" placeholder="Medium" /> <Input size="large" placeholder="Large" /> </div></Story>
<!-- the icon rides the size variant's gap, as in Button. --><Story name="Icon sizes" asChild> <div class="flex items-center gap-sm"> <Input size="small" iconLeft={Search} placeholder="Small" /> <Input size="regular" iconLeft={Search} placeholder="Medium" /> <Input size="large" iconLeft={Search} placeholder="Large" /> </div></Story>
<Story name="With Button" asChild> <div class="flex flex-col items-start gap-sm"> {#each ["small", "regular", "large"] as const as s (s)} <div class="flex items-center gap-xs"> <Input size={s} placeholder="Repository name" /> <Button size={s}>Create</Button> </div> {/each} </div></Story>
<!-- rich mode: Enter commits the draft, Backspace highlights the last chip and a second Backspace takes it. --><Story name="Rich" asChild> <div class="max-w-compact"> <Input rich bind:tags={topics} label="Topics" placeholder="Add a topic" hint="Enter adds · Backspace removes" /> </div></Story>
<!-- a `field` on a tag renders as a dim prefix, which is all it takes for the same array to read as a filter bar. --><Story name="Rich filters" asChild> <div class="max-w-narrow"> <Input rich bind:tags={filters} iconLeft={Search} label="Filters" placeholder="Filter issues" commitOn={["enter", "comma"]} /> </div></Story>
<!-- `commitOn` says what turns a draft into a chip. Space suits one-word tags, comma suits values that can hold spaces, and the same characters split a paste — so pasting "a, b" into the comma field lands two chips while the space field takes it as one. --><Story name="Rich commit keys" asChild> <div class="flex max-w-compact flex-col gap-md"> <Input rich bind:tags={entered} label="Enter" placeholder="Enter adds" hint="commitOn: enter" /> <Input rich bind:tags={spaced} commitOn={["space", "enter"]} label="Space" placeholder="Space adds" hint="commitOn: space, enter" /> <Input rich bind:tags={phrases} commitOn={["comma", "enter"]} label="Comma" placeholder="Comma adds" hint="commitOn: comma, enter — a value can hold spaces" /> </div></Story>
<!-- the point of the `tag` snippet: chips are whatever you render, here a typeahead over User.svelte. Anything already picked drops out of the suggestions. --><Story name="Rich user typeahead" asChild> <div class="max-w-compact"> <Input rich bind:tags={reviewers} suggestions={people} label="Reviewers" placeholder="Add a reviewer" hint="Pick from the list, or type a handle" emptyLabel="No one left to add" > {#snippet tag(t)} <User handle={t.value} size="regular" link={false} /> {/snippet} {#snippet suggestion(t)} <User handle={t.value} size="regular" link={false} /> {/snippet} </Input> </div></Story>
<!-- max shows a counter and turns further drafts away; validate vets the rest. Both cross-fade their reason over the hint and put it back after a moment. --><Story name="Rich limits" asChild> <div class="max-w-compact"> <Input rich bind:tags={capped} maxTags={4} hint="Up to four, 12 characters each" placeholder="Add a tag" validate={(v) => (v.length <= 12 ? true : "Keep it under 12 characters")} /> </div></Story>
<!-- the box is the same recipe either way, so the size ladder and the states carry over. --><Story name="Rich sizes" asChild> <div class="flex max-w-compact flex-col gap-sm"> {#each ["small", "regular", "large"] as const as s (s)} <Input rich size={s} iconLeft={Tag} tags={[{ value: "svelte" }, { value: "tailwind" }]} placeholder={s} /> {/each} </div></Story>
<Story name="Rich states" asChild> <div class="flex max-w-compact flex-col gap-sm"> <Input rich error tags={[{ value: "bad-input" }]} placeholder="Add a tag" /> <Input rich disabled tags={[{ value: "locked" }, { value: "read-only" }]} placeholder="Add a tag" /> <Input rich loading tags={[{ value: "checking" }]} placeholder="Add a tag" /> </div></Story>