From 580e798e16346eb894eb899edac82d2efdf40adc Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 18 Apr 2026 22:32:53 -0500 Subject: initial commit --- src/components/TokenInput.tsx | 85 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/components/TokenInput.tsx (limited to 'src/components/TokenInput.tsx') diff --git a/src/components/TokenInput.tsx b/src/components/TokenInput.tsx new file mode 100644 index 0000000..c0e23de --- /dev/null +++ b/src/components/TokenInput.tsx @@ -0,0 +1,85 @@ +import { useState, useRef, KeyboardEvent } from 'react'; + +interface Props { + label: string; + values: string[]; + onChange: (values: string[]) => void; + suggestions?: string[]; + placeholder?: string; +} + +export function TokenInput({ label, values, onChange, suggestions = [], placeholder = 'Type and press Enter' }: Props) { + const [draft, setDraft] = useState(''); + const inputRef = useRef(null); + + const add = (val: string) => { + const trimmed = val.trim().toLowerCase(); + if (trimmed && !values.includes(trimmed)) { + onChange([...values, trimmed]); + } + setDraft(''); + }; + + const remove = (val: string) => { + onChange(values.filter(v => v !== val)); + }; + + const handleKey = (e: KeyboardEvent) => { + if ((e.key === 'Enter' || e.key === ',') && draft.trim()) { + e.preventDefault(); + add(draft); + } else if (e.key === 'Backspace' && !draft && values.length > 0) { + remove(values[values.length - 1]); + } + }; + + const availableSuggestions = suggestions.filter(s => !values.includes(s)); + + return ( +
+ +
inputRef.current?.focus()} + > + {values.map(v => ( + + {v} + + + ))} + setDraft(e.target.value)} + onKeyDown={handleKey} + onBlur={() => { if (draft.trim()) add(draft); }} + placeholder={values.length === 0 ? placeholder : ''} + aria-label={label} + /> +
+ {availableSuggestions.length > 0 && ( +
+ {availableSuggestions.map(s => ( + + ))} +
+ )} +
+ ); +} -- cgit v1.2.3