- Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss - Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css - Replaced @tailwind directives with @import 'tailwindcss' - Added @custom-variant dark, @utility blocks for custom utilities - Updated class names across 128 files (shadow-sm → shadow-xs, etc.) - Removed autoprefixer (built into v4) - Added migration plan doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
6.9 KiB
TypeScript
156 lines
6.9 KiB
TypeScript
import { GripVertical, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import type { IntakeFormField, IntakeFieldType } from '@/types'
|
|
|
|
const FIELD_TYPE_OPTIONS: { value: IntakeFieldType; label: string }[] = [
|
|
{ value: 'text', label: 'Text' },
|
|
{ value: 'textarea', label: 'Text Area' },
|
|
{ value: 'number', label: 'Number' },
|
|
{ value: 'ip_address', label: 'IP Address' },
|
|
{ value: 'email', label: 'Email' },
|
|
{ value: 'url', label: 'URL' },
|
|
{ value: 'select', label: 'Select (Dropdown)' },
|
|
{ value: 'multi_select', label: 'Multi-Select' },
|
|
{ value: 'checkbox', label: 'Checkbox' },
|
|
{ value: 'password', label: 'Password' },
|
|
]
|
|
|
|
interface IntakeFieldEditorProps {
|
|
field: IntakeFormField
|
|
onUpdate: (updates: Partial<IntakeFormField>) => void
|
|
onRemove: () => void
|
|
}
|
|
|
|
export function IntakeFieldEditor({ field, onUpdate, onRemove }: IntakeFieldEditorProps) {
|
|
const [expanded, setExpanded] = useState(false)
|
|
const needsOptions = field.field_type === 'select' || field.field_type === 'multi_select'
|
|
|
|
return (
|
|
<div className="bg-card border border-border rounded-xl p-3">
|
|
{/* Header row */}
|
|
<div className="flex items-center gap-2">
|
|
<GripVertical className="h-4 w-4 shrink-0 cursor-grab text-muted-foreground" />
|
|
|
|
<input
|
|
type="text"
|
|
value={field.label}
|
|
onChange={(e) => onUpdate({ label: e.target.value })}
|
|
placeholder="Field label"
|
|
className="min-w-0 flex-1 rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
|
|
<select
|
|
value={field.field_type}
|
|
onChange={(e) => onUpdate({ field_type: e.target.value as IntakeFieldType })}
|
|
className="rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
>
|
|
{FIELD_TYPE_OPTIONS.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
|
|
<label className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={field.required}
|
|
onChange={(e) => onUpdate({ required: e.target.checked })}
|
|
className="rounded border-border"
|
|
/>
|
|
Req
|
|
</label>
|
|
|
|
<button
|
|
onClick={() => setExpanded(!expanded)}
|
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
{expanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
|
</button>
|
|
|
|
<button
|
|
onClick={onRemove}
|
|
className="rounded p-1 text-muted-foreground hover:bg-red-500/20 hover:text-red-400"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Expanded details */}
|
|
{expanded && (
|
|
<div className="mt-3 grid grid-cols-2 gap-3 border-t border-border pt-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">Variable Name</label>
|
|
<input
|
|
type="text"
|
|
value={field.variable_name}
|
|
onChange={(e) => onUpdate({ variable_name: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '') })}
|
|
placeholder="e.g. server_name"
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
<p className="mt-0.5 text-[10px] text-muted-foreground">Used as [VAR:{field.variable_name}]</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">Placeholder</label>
|
|
<input
|
|
type="text"
|
|
value={field.placeholder || ''}
|
|
onChange={(e) => onUpdate({ placeholder: e.target.value || undefined })}
|
|
placeholder="Hint text"
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="mb-1 block text-xs text-muted-foreground">Help Text</label>
|
|
<input
|
|
type="text"
|
|
value={field.help_text || ''}
|
|
onChange={(e) => onUpdate({ help_text: e.target.value || undefined })}
|
|
placeholder="Description or instructions"
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">Default Value</label>
|
|
<input
|
|
type="text"
|
|
value={field.default_value || ''}
|
|
onChange={(e) => onUpdate({ default_value: e.target.value || undefined })}
|
|
placeholder="Pre-filled value"
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">Group Name</label>
|
|
<input
|
|
type="text"
|
|
value={field.group_name || ''}
|
|
onChange={(e) => onUpdate({ group_name: e.target.value || undefined })}
|
|
placeholder="e.g. Network Settings"
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
|
|
{needsOptions && (
|
|
<div className="col-span-2">
|
|
<label className="mb-1 block text-xs text-muted-foreground">Options (one per line)</label>
|
|
<textarea
|
|
value={(field.options || []).join('\n')}
|
|
onChange={(e) => {
|
|
const options = e.target.value.split('\n').filter((o) => o.trim())
|
|
onUpdate({ options: options.length > 0 ? options : undefined })
|
|
}}
|
|
placeholder="Option 1 Option 2 Option 3"
|
|
rows={3}
|
|
className="w-full rounded border border-border bg-card px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|