feat(reg): inline MemberFields (+ add-row) replacing the chip input

Phase 1 members now entered as inline text rows with a '+ Add member' affordance and
per-row remove, per the pre-deployed-devices onboarding rework. Parent store still
receives only trimmed, non-empty names. Caps at 5. MemberChips retained (unused) for
now. Tests updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-21 06:08:28 -07:00
co-authored by Claude Opus 4.8
parent 1642fbce39
commit 1b31f6db1b
4 changed files with 141 additions and 5 deletions
+86
View File
@@ -0,0 +1,86 @@
import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
const MAX_MEMBERS = 5
export interface MemberFieldsProps {
members: string[]
onChange: (next: string[]) => void
}
/**
* Inline member entry: one text field per member with a "+" to append another
* row below, and a "×" to remove a row. The parent store only ever sees the
* trimmed, non-empty names; empty rows are a local editing affordance.
*/
export function MemberFields({ members, onChange }: MemberFieldsProps) {
// Seed local rows from the parent (always at least one row to type into).
const [rows, setRows] = useState<string[]>(members.length ? members : [''])
const commit = (next: string[]) => {
setRows(next)
onChange(next.map((r) => r.trim()).filter(Boolean))
}
const setRow = (i: number, value: string) => {
const next = rows.slice()
next[i] = value
commit(next)
}
const addRow = () => {
if (rows.length >= MAX_MEMBERS) return
setRows([...rows, '']) // don't commit — empty row adds nothing to the store
}
const removeRow = (i: number) => {
const next = rows.length > 1 ? rows.filter((_, idx) => idx !== i) : ['']
commit(next)
}
const full = rows.length >= MAX_MEMBERS
const filled = rows.filter((r) => r.trim()).length
return (
<div className="space-y-2" data-testid="member-fields">
{rows.map((row, i) => (
<div key={i} className="flex gap-2 items-center">
<span className="font-mono text-[10px] text-muted-foreground w-4 shrink-0 text-right">
{i + 1}
</span>
<Input
aria-label={`Member ${i + 1}`}
placeholder="e.g. A. Rossi"
value={row}
onChange={(e) => setRow(i, e.target.value)}
className="font-mono text-sm"
/>
<button
type="button"
aria-label={`Remove member ${i + 1}`}
onClick={() => removeRow(i)}
className="text-muted-foreground hover:text-destructive transition leading-none px-1.5 text-lg shrink-0"
>
×
</button>
</div>
))}
<div className="flex items-center justify-between pl-6">
<Button
type="button"
variant="ghost"
size="sm"
onClick={addRow}
disabled={full}
className="font-mono text-[11px] tracking-wider uppercase h-7 px-2"
>
+ Add member
</Button>
<span className="font-mono text-[10px] text-muted-foreground tracking-wider uppercase">
{filled} / {MAX_MEMBERS}
</span>
</div>
</div>
)
}