summaryrefslogtreecommitdiff
path: root/src/components/TokenInput.tsx
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-18 22:32:53 -0500
committerChristian Cleberg <[email protected]>2026-04-18 22:32:53 -0500
commit580e798e16346eb894eb899edac82d2efdf40adc (patch)
tree6dec3b7db678f781138b40d34da37d21ac8fbe7d /src/components/TokenInput.tsx
downloadbrand-bench-580e798e16346eb894eb899edac82d2efdf40adc.tar.gz
brand-bench-580e798e16346eb894eb899edac82d2efdf40adc.tar.bz2
brand-bench-580e798e16346eb894eb899edac82d2efdf40adc.zip
initial commit
Diffstat (limited to 'src/components/TokenInput.tsx')
-rw-r--r--src/components/TokenInput.tsx85
1 files changed, 85 insertions, 0 deletions
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<HTMLInputElement>(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<HTMLInputElement>) => {
+ 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 (
+ <div className="token-field">
+ <label className="field-label">{label}</label>
+ <div
+ className="token-container"
+ onClick={() => inputRef.current?.focus()}
+ >
+ {values.map(v => (
+ <span key={v} className="token">
+ {v}
+ <button
+ type="button"
+ className="token-remove"
+ onClick={e => { e.stopPropagation(); remove(v); }}
+ aria-label={`Remove ${v}`}
+ >
+ ×
+ </button>
+ </span>
+ ))}
+ <input
+ ref={inputRef}
+ className="token-input"
+ value={draft}
+ onChange={e => setDraft(e.target.value)}
+ onKeyDown={handleKey}
+ onBlur={() => { if (draft.trim()) add(draft); }}
+ placeholder={values.length === 0 ? placeholder : ''}
+ aria-label={label}
+ />
+ </div>
+ {availableSuggestions.length > 0 && (
+ <div className="token-suggestions">
+ {availableSuggestions.map(s => (
+ <button
+ key={s}
+ type="button"
+ className={`token-suggestion${values.includes(s) ? ' active' : ''}`}
+ onClick={() => add(s)}
+ >
+ {s}
+ </button>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+}