Files
apress/src/components/MemberFields.tsx
T
Omar SobhandClaude Opus 4.8 1b31f6db1b 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]>
2026-07-21 06:08:28 -07:00

87 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}