From 53e54aa267dbfb427e8818cb0988d4c43b8d90a1 Mon Sep 17 00:00:00 2001 From: eti Date: Thu, 6 Aug 2026 13:37:38 +0300 Subject: [PATCH] web/components: add rich mode to Input Signed-off-by: eti --- .../lib/components/ui/Input.stories.svelte | 119 +++ web/src/lib/components/ui/Input.svelte | 163 +++-- web/src/lib/components/ui/TagInput.svelte | 677 ++++++++++++++++++ web/src/lib/components/ui/inputField.ts | 55 ++ .../settings/profile/handle/+page.svelte | 3 +- 5 files changed, 953 insertions(+), 64 deletions(-) create mode 100644 web/src/lib/components/ui/TagInput.svelte create mode 100644 web/src/lib/components/ui/inputField.ts diff --git a/web/src/lib/components/ui/Input.stories.svelte b/web/src/lib/components/ui/Input.stories.svelte index 9d8cd0078..f5890741f 100644 --- a/web/src/lib/components/ui/Input.stories.svelte +++ b/web/src/lib/components/ui/Input.stories.svelte @@ -2,8 +2,11 @@ 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 "./Button.svelte"; import Input from "./Input.svelte"; + import User from "./User.svelte"; + import type { InputTag } from "./inputField"; const { Story } = defineMeta({ title: "UI/Input", @@ -17,6 +20,7 @@ error: { control: "boolean" }, disabled: { control: "boolean" }, loading: { control: "boolean" }, + rich: { control: "boolean" }, placeholder: { control: "text" } }, args: { @@ -24,11 +28,31 @@ error: false, disabled: false, loading: false, + rich: false, placeholder: "Placeholder" } }); + + @@ -67,3 +91,98 @@ {/each} + + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ + {#snippet tag(t)} + + {/snippet} + {#snippet suggestion(t)} + + {/snippet} + +
+
+ + + +
+ (v.length <= 12 ? true : "Keep it under 12 characters")} + /> +
+
+ + + +
+ {#each ["small", "regular", "large"] 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 95eadd898..710be6191 100644 --- a/web/src/lib/components/ui/Input.svelte +++ b/web/src/lib/components/ui/Input.svelte @@ -1,47 +1,13 @@ - - -
- {#if iconLeft} - {@const IconLeft = iconLeft} -
+{:else} +
+ {#if iconLeft} + {@const IconLeft = iconLeft} +
+{/if} diff --git a/web/src/lib/components/ui/TagInput.svelte b/web/src/lib/components/ui/TagInput.svelte new file mode 100644 index 000000000..d5abd35c5 --- /dev/null +++ b/web/src/lib/components/ui/TagInput.svelte @@ -0,0 +1,677 @@ + + + + +
+ + + + + {#if suggestions.length > 0} +
(open = (event as ToggleEvent).newState === "open")} + style="position-anchor: {anchorName}" + class={slots.panel({ class: panelClass })} + > +
+ {#each visible as option, index (identity(option))} + + {:else} +

{emptyLabel}

+ {/each} +
+
+ {/if} + + {#if hint || maxTags !== undefined || rejection} +
+
+ {hint ?? ""} + + {rejectionText} + +
+ {#if maxTags !== undefined} + {tags.length}/{maxTags} + {/if} +
+ {/if} + + {announcement} +
+ + diff --git a/web/src/lib/components/ui/inputField.ts b/web/src/lib/components/ui/inputField.ts new file mode 100644 index 000000000..905215a70 --- /dev/null +++ b/web/src/lib/components/ui/inputField.ts @@ -0,0 +1,55 @@ +import type { Component } from "svelte"; +import type { SvelteHTMLElements } from "svelte/elements"; +import { tv, type VariantProps } from "tailwind-variants"; +import type { TagVariants } from "./Tag.svelte"; + +/** One token in a rich Input. On its own a tag is just a word; give it a `field` and it reads as + * a filter — `author: eti` — so tags and filters sit in the same array and remove the same way. */ +export interface InputTag { + /** identity: what a duplicate check compares, and what a committed draft becomes */ + value: string; + /** shown instead of `value`; `value` is still what gets matched */ + label?: string; + /** dim prefix inside the chip, which is what turns a tag into a filter */ + field?: string; + color?: TagVariants["color"]; + icon?: Component; +} + +/** why a draft was turned away: the footer says which, and a duplicate flashes the chip it hit */ +export type TagRejection = "duplicate" | "limit" | "invalid"; + +export const inputField = tv({ + base: "flex items-center rounded-sm border bg-background-default transition-colors", + variants: { + size: { + small: "min-h-8 gap-1.5 px-1.5 py-1 typography-paragraph-small", + regular: "min-h-9 gap-2 px-2 py-1 typography-paragraph-regular", + large: "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", + 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" + }, + disabled: { + false: "", + true: "cursor-not-allowed bg-background-inset text-foreground-disabled" + }, + rich: { + // tags wrap onto more rows, so the box has to be free to grow past the min-h its size + // sets. items-stretch hands the full height to the tag list, which then centres its + // own rows — that way one row of tags sits exactly where a plain input's text sits. + true: "items-center", + false: "" + } + }, + defaultVariants: { + size: "regular", + error: false, + disabled: false, + rich: false + } +}); + +export type InputFieldVariants = VariantProps; diff --git a/web/src/routes/settings/profile/handle/+page.svelte b/web/src/routes/settings/profile/handle/+page.svelte index 7d5c2066b..9020203e7 100644 --- a/web/src/routes/settings/profile/handle/+page.svelte +++ b/web/src/routes/settings/profile/handle/+page.svelte @@ -2,7 +2,8 @@ import { goto } from "$app/navigation"; import { resolve } from "$app/paths"; import Button from "$lib/components/ui/Button.svelte"; - import Input, { inputField } from "$lib/components/ui/Input.svelte"; + import Input from "$lib/components/ui/Input.svelte"; + import { inputField } from "$lib/components/ui/inputField"; import Toggle from "$lib/components/ui/Toggle.svelte"; import DrillDown from "$lib/components/settings/DrillDown.svelte"; import SettingsList from "$lib/components/settings/SettingsList.svelte"; -- 2.51.2