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:
co-authored by
Claude Opus 4.8
parent
1642fbce39
commit
1b31f6db1b
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user