Split the 1371-line VVT page into _components/ extractions (FormPrimitives, api, TabVerzeichnis, TabEditor, TabExport) to bring page.tsx under the 300 LOC soft target. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
export function FormSection({ title, children }: { title: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="p-4 space-y-3">
|
|
<h4 className="font-medium text-gray-800 text-sm">{title}</h4>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function FormField({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-1">{label}</label>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function MultiTextInput({ values, onChange, placeholder }: { values: string[]; onChange: (v: string[]) => void; placeholder?: string }) {
|
|
const [input, setInput] = useState('')
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && input.trim()) {
|
|
e.preventDefault()
|
|
onChange([...values, input.trim()])
|
|
setInput('')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
{values.map((v, i) => (
|
|
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 rounded text-sm">
|
|
{v}
|
|
<button onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-purple-400 hover:text-purple-600">
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
placeholder={placeholder}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function CheckboxGrid({ options, selected, onChange }: {
|
|
options: { value: string; label: string; highlight?: boolean }[]
|
|
selected: string[]
|
|
onChange: (v: string[]) => void
|
|
}) {
|
|
const toggle = (value: string) => {
|
|
if (selected.includes(value)) {
|
|
onChange(selected.filter(v => v !== value))
|
|
} else {
|
|
onChange([...selected, value])
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-1.5">
|
|
{options.map(opt => (
|
|
<label
|
|
key={opt.value}
|
|
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer text-sm transition-colors ${
|
|
selected.includes(opt.value)
|
|
? opt.highlight ? 'bg-red-50 border border-red-300' : 'bg-purple-50 border border-purple-300'
|
|
: opt.highlight ? 'bg-red-50/30 border border-gray-200' : 'border border-gray-200 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.includes(opt.value)}
|
|
onChange={() => toggle(opt.value)}
|
|
className="w-3.5 h-3.5 text-purple-600 rounded"
|
|
/>
|
|
<span className={opt.highlight ? 'text-red-700' : 'text-gray-700'}>{opt.label}</span>
|
|
{opt.highlight && <span className="text-xs text-red-400">Art.9</span>}
|
|
</label>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|