krz/brand-bench

A dynamic brand documentation generator with Ollama integration.

clone: git clone https://gitbay.org/krz/brand-bench.git

main: src/components/TokenInput.tsx · raw

 1import { useState, useRef, KeyboardEvent } from 'react';
 2
 3interface Props {
 4  label: string;
 5  values: string[];
 6  onChange: (values: string[]) => void;
 7  suggestions?: string[];
 8  placeholder?: string;
 9}
10
11export function TokenInput({ label, values, onChange, suggestions = [], placeholder = 'Type and press Enter' }: Props) {
12  const [draft, setDraft] = useState('');
13  const inputRef = useRef<HTMLInputElement>(null);
14
15  const add = (val: string) => {
16    const trimmed = val.trim().toLowerCase();
17    if (trimmed && !values.includes(trimmed)) {
18      onChange([...values, trimmed]);
19    }
20    setDraft('');
21  };
22
23  const remove = (val: string) => {
24    onChange(values.filter(v => v !== val));
25  };
26
27  const handleKey = (e: KeyboardEvent<HTMLInputElement>) => {
28    if ((e.key === 'Enter' || e.key === ',') && draft.trim()) {
29      e.preventDefault();
30      add(draft);
31    } else if (e.key === 'Backspace' && !draft && values.length > 0) {
32      remove(values[values.length - 1]);
33    }
34  };
35
36  const availableSuggestions = suggestions.filter(s => !values.includes(s));
37
38  return (
39    <div className="token-field">
40      <label className="field-label">{label}</label>
41      <div
42        className="token-container"
43        onClick={() => inputRef.current?.focus()}
44      >
45        {values.map(v => (
46          <span key={v} className="token">
47            {v}
48            <button
49              type="button"
50              className="token-remove"
51              onClick={e => { e.stopPropagation(); remove(v); }}
52              aria-label={`Remove ${v}`}
53            >
54              ×
55            </button>
56          </span>
57        ))}
58        <input
59          ref={inputRef}
60          className="token-input"
61          value={draft}
62          onChange={e => setDraft(e.target.value)}
63          onKeyDown={handleKey}
64          onBlur={() => { if (draft.trim()) add(draft); }}
65          placeholder={values.length === 0 ? placeholder : ''}
66          aria-label={label}
67        />
68      </div>
69      {availableSuggestions.length > 0 && (
70        <div className="token-suggestions">
71          {availableSuggestions.map(s => (
72            <button
73              key={s}
74              type="button"
75              className={`token-suggestion${values.includes(s) ? ' active' : ''}`}
76              onClick={() => add(s)}
77            >
78              {s}
79            </button>
80          ))}
81        </div>
82      )}
83    </div>
84  );
85}