101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
|
|
interface Pair { id: string; key: string; value: string; }
|
|
|
|
function parse(text: string): Pair[] {
|
|
let n = 0;
|
|
return text.split('\n')
|
|
.filter(line => { const t = line.trim(); return t && !t.startsWith('#'); })
|
|
.map(line => {
|
|
const eq = line.indexOf('=');
|
|
return eq < 0
|
|
? { id: String(n++), key: line.trim(), value: '' }
|
|
: { id: String(n++), key: line.slice(0, eq).trim(), value: line.slice(eq + 1).trim() };
|
|
});
|
|
}
|
|
|
|
function serialize(pairs: Pair[]): string {
|
|
return pairs.map(p => `${p.key}=${p.value}`).join('\n') + (pairs.length ? '\n' : '');
|
|
}
|
|
|
|
interface ConfigEditorProps {
|
|
text: string;
|
|
onSave?: (text: string) => Promise<void>;
|
|
}
|
|
|
|
export default function ConfigEditor({ text, onSave }: ConfigEditorProps) {
|
|
const [pairs, setPairs] = useState<Pair[]>(() => parse(text));
|
|
const [saving, setSaving] = useState(false);
|
|
const counter = useRef(parse(text).length);
|
|
|
|
const newId = () => String(counter.current++);
|
|
|
|
const updateKey = (id: string, key: string) => setPairs(ps => ps.map(p => p.id === id ? { ...p, key } : p));
|
|
const updateValue = (id: string, value: string) => setPairs(ps => ps.map(p => p.id === id ? { ...p, value } : p));
|
|
const deleteRow = (id: string) => setPairs(ps => ps.filter(p => p.id !== id));
|
|
const addRow = () => setPairs(ps => [...ps, { id: newId(), key: '', value: '' }]);
|
|
|
|
const handleSave = async () => {
|
|
if (!onSave) return;
|
|
setSaving(true);
|
|
try { await onSave(serialize(pairs)); }
|
|
finally { setSaving(false); }
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-neutral-950">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
|
|
<button
|
|
onClick={addRow}
|
|
className="px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 inline-flex items-center gap-1"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" /> Add
|
|
</button>
|
|
{onSave && (
|
|
<button
|
|
onClick={() => void handleSave()}
|
|
disabled={saving}
|
|
className="px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 inline-flex items-center gap-1"
|
|
>
|
|
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{pairs.length === 0 && (
|
|
<div className="text-neutral-500 text-sm text-center py-12">
|
|
No entries — click Add to create one
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
{pairs.map(pair => (
|
|
<div key={pair.id} className="flex items-center gap-2">
|
|
<input
|
|
value={pair.key}
|
|
onChange={e => updateKey(pair.id, e.target.value)}
|
|
placeholder="name"
|
|
className="w-36 flex-shrink-0 px-2 py-1.5 bg-neutral-800 border border-neutral-600 rounded text-sm text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-blue-500"
|
|
/>
|
|
<span className="text-neutral-500 flex-shrink-0">=</span>
|
|
<input
|
|
value={pair.value}
|
|
onChange={e => updateValue(pair.id, e.target.value)}
|
|
placeholder="value"
|
|
className="flex-1 min-w-0 px-2 py-1.5 bg-neutral-800 border border-neutral-600 rounded text-sm text-neutral-200 placeholder:text-neutral-600 focus:outline-none focus:border-blue-500"
|
|
/>
|
|
<button
|
|
onClick={() => deleteRow(pair.id)}
|
|
className="flex-shrink-0 p-1.5 rounded hover:bg-neutral-700 text-neutral-500 hover:text-red-400"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|