# CLAUDE.md — `web/src/components/groups/`

Operator surfaces for a group's people. Mounted at `/in/[groupId]/members.astro`.

## The presentational-panel contract (load-bearing)

`MembersPanel.tsx` is **presentational and complete** — it renders every control
(add-form, inline role picker, remove, act-as delegate) and emits intent through
**optional handler props**:

| Prop | Wired by | Control when absent |
|------|----------|---------------------|
| `onRoleChange(uid, role)` | C2 changeRole | role picker read-only (disabled) |
| `onRemove(uid)` | C3 removeMember | remove button disabled |
| `onAdd(email, role)` | C5 add-to-team | add form disabled |
| `onDelegateActAs(uid)` | C7 act-as | delegate toggle disabled |

**A control is disabled when its handler is absent — never removed.** This is
what lets later cycles ship a backend + ONE wire line without ever editing
`MembersPanel.tsx`.

## Files

| File | What |
|------|------|
| `MembersPanel.tsx` | presentational panel — props only, no fetch/no backend |
| `MembersPanelClient.tsx` | **the wire site** — fetches `GET /api/groups/members`, renders the panel; later cycles add handler props here |
| `../../pages/in/[groupId]/members.astro` | mount (client:only island) |

Roles are shown via `@/lib/role-labels` (`member`→"Editor"); the substrate value
(`owner/admin/member/viewer/agent/auditor`) is LOCKED and never surfaced as a
label. Add defaults to `viewer` (least privilege).

## Don't

- Don't edit `MembersPanel.tsx` to add a backend — wire a handler in `MembersPanelClient.tsx`.
- Don't pass function handlers from the `.astro` page — handlers don't cross the island boundary; they live in the client container.
- Design rules apply (6 tokens, `emitClick` on every onClick) — auto-loaded from `../CLAUDE.md`.
