krz/brand-bench

A dynamic brand documentation generator with Ollama integration.

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

main: src/components/InputPanel.tsx · raw

  1import type { BrandInputs } from '../types';
  2import { TokenInput } from './TokenInput';
  3
  4const TONE_SUGGESTIONS = ['minimal', 'technical', 'calm', 'bold', 'warm', 'dry', 'focused', 'sharp'];
  5const AVOID_SUGGESTIONS = ['buzzwords', 'hype', 'startup language', 'passive voice', 'corporate tone', 'exclamation points', 'superlatives'];
  6
  7interface Props {
  8  inputs: BrandInputs;
  9  onChange: (inputs: BrandInputs) => void;
 10  onGenerate: () => void;
 11  isGenerating: boolean;
 12  generateLabel?: string;
 13}
 14
 15export function InputPanel({ inputs, onChange, onGenerate, isGenerating, generateLabel }: Props) {
 16  const set = <K extends keyof BrandInputs>(key: K) =>
 17    (value: BrandInputs[K]) => onChange({ ...inputs, [key]: value });
 18
 19  const handleKey = (e: React.KeyboardEvent) => {
 20    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
 21      e.preventDefault();
 22      onGenerate();
 23    }
 24  };
 25
 26  const canGenerate = inputs.name.trim().length > 0 && inputs.purpose.trim().length > 0;
 27
 28  return (
 29    <div className="input-panel" onKeyDown={handleKey}>
 30      <div className="input-section">
 31        <div className="input-section-label">Project</div>
 32        <div className="field">
 33          <label className="field-label" htmlFor="f-name">Name</label>
 34          <input
 35            id="f-name"
 36            className="field-input"
 37            placeholder="e.g. Foundry"
 38            value={inputs.name}
 39            onChange={e => set('name')(e.target.value)}
 40          />
 41        </div>
 42        <div className="field">
 43          <label className="field-label" htmlFor="f-category">Category</label>
 44          <input
 45            id="f-category"
 46            className="field-input"
 47            placeholder="e.g. developer tool, design studio"
 48            value={inputs.category}
 49            onChange={e => set('category')(e.target.value)}
 50          />
 51        </div>
 52        <div className="field">
 53          <label className="field-label" htmlFor="f-purpose">One-line purpose</label>
 54          <input
 55            id="f-purpose"
 56            className="field-input"
 57            placeholder="e.g. deploy microservices without boilerplate"
 58            value={inputs.purpose}
 59            onChange={e => set('purpose')(e.target.value)}
 60          />
 61        </div>
 62        <div className="field">
 63          <label className="field-label" htmlFor="f-audience">Audience</label>
 64          <input
 65            id="f-audience"
 66            className="field-input"
 67            placeholder="e.g. backend engineers"
 68            value={inputs.audience}
 69            onChange={e => set('audience')(e.target.value)}
 70          />
 71        </div>
 72      </div>
 73
 74      <div className="input-section">
 75        <div className="input-section-label">Voice</div>
 76        <div className="field">
 77          <TokenInput
 78            label="Tone attributes"
 79            values={inputs.tone}
 80            onChange={set('tone')}
 81            suggestions={TONE_SUGGESTIONS}
 82            placeholder="Add tone..."
 83          />
 84        </div>
 85        <div className="field" style={{ marginTop: 8 }}>
 86          <TokenInput
 87            label="Avoid"
 88            values={inputs.avoid}
 89            onChange={set('avoid')}
 90            suggestions={AVOID_SUGGESTIONS}
 91            placeholder="Add avoid..."
 92          />
 93        </div>
 94      </div>
 95
 96      <div className="input-section">
 97        <div className="input-section-label">Notes</div>
 98        <div className="field">
 99          <label className="field-label sr-only" htmlFor="f-notes">Notes & constraints</label>
100          <textarea
101            id="f-notes"
102            className="field-textarea"
103            placeholder="Optional: constraints, context, inspirations, or anything the brand package should reflect."
104            value={inputs.notes}
105            onChange={e => set('notes')(e.target.value)}
106            rows={4}
107          />
108        </div>
109      </div>
110
111      <div className="generate-area">
112        <button
113          className={`btn-generate${isGenerating ? ' generating' : ''}`}
114          onClick={onGenerate}
115          disabled={!isGenerating && !canGenerate}
116          title={!canGenerate ? 'Enter a name and purpose to generate' : 'Generate brand package (⌘Enter)'}
117        >
118          {generateLabel ?? (isGenerating ? 'Generating…' : 'Generate brand package')}
119        </button>
120        {!canGenerate && (
121          <div style={{ marginTop: 7, fontSize: 11, color: 'var(--text-4)', textAlign: 'center' }}>
122            Name and purpose required
123          </div>
124        )}
125        {canGenerate && !isGenerating && (
126          <div style={{ marginTop: 7, fontSize: 11, color: 'var(--text-4)', textAlign: 'center' }}>
127             Enter to generate
128          </div>
129        )}
130      </div>
131    </div>
132  );
133}