+
+
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";