---
phase: 20-admin-member-editor-form-declutter
plan: "03"
subsystem: pwa-admin-ui
tags:
  - admin
  - member-editor
  - ux
  - react
  - playwright-verified
dependency_graph:
  requires:
    - "20-01"  # PATCH /api/admin/members/:id route + last-admin guard
    - "20-02"  # updateMemberProfile fetcher + AdminMember.isAdmin in client.ts
  provides:
    - unified-member-editor-sheet
    - decluttered-members-panel
  affects:
    - apps/pwa/src/routes/AdminPage.tsx
    - apps/pwa/src/components/MemberEditorSheet.tsx
tech_stack:
  added: []
  patterns:
    - per-section-save-sheet
    - role-switch-toggle
    - tappable-row-with-chevron
    - ghost-trigger-button
key_files:
  created:
    - apps/pwa/src/components/MemberEditorSheet.tsx
  modified:
    - apps/pwa/src/routes/AdminPage.tsx
decisions:
  - "Admin toggle uses role=switch + aria-checked per UI-SPEC Accessibility Contract (not native checkbox)"
  - "fastmailEmail starts blank in edit mode — GET /api/admin/members does not return it (D-CONTEXT)"
  - "Per-section saves keep sheet open; create-mode save closes it (D-05, D-07)"
  - "onMouseEnter/Leave hover effect on Add-member button via e.currentTarget.style (no type assertion)"
  - "Pre-existing prettier drift in docs/*, CLAUDE.md, README.md, api/admin.ts fixed in Task 3 commit"
metrics:
  duration: "~10 minutes"
  completed: "2026-06-18"
  tasks_completed: 3
  files_modified: 2
  files_created: 1
status: complete
---

# Phase 20 Plan 03: Member Editor & Admin Panel Declutter Summary

One unified MemberEditorSheet.tsx (edit + create modes, per-section saves) replacing the scattered per-row action buttons, standalone ResetPasswordSheet, and always-open inline Add-member form in AdminPage.tsx.

## Tasks Completed

| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 | Build MemberEditorSheet.tsx (edit + create modes, per-section saves) | b125a69 | apps/pwa/src/components/MemberEditorSheet.tsx (NEW, 899 lines) |
| 2 | Rework AdminPage MemberRow + Add-member trigger; remove old surfaces | 9e6b004 | apps/pwa/src/routes/AdminPage.tsx |
| 3 | Verify interaction + visual contract with playwright-cli; pass CI gates | 9b62887 | docs/*, CLAUDE.md, README.md (prettier drift fixes) |

## What Was Built

### MemberEditorSheet.tsx (new)

Single component (`mode: 'edit' | 'create'`) that replaces the `CredentialSheet`, `ResetPasswordSheet`, and inline add-member form:

**Edit mode — three per-section saves (D-05):**
- Section 1 Profile: display name input + `role="switch"` admin toggle (initial state from `member.isAdmin`); `updateMemberProfile` mutation; last-admin 409 shows inline "Cannot remove admin — at least one admin must remain." and reverts toggle (D-03)
- Section 2 Set new password: only shown when `member.hasLocalCredential === true`; mismatch/short client guards; `fetchAdminResetPassword` mutation; fires "Password updated." toast; sheet stays open
- Section 3 App password: `saveCredential` mutation with CalDAV validation; "Validating against CalDAV…" Loader2 state; FAILURE_TEXT on error; fastmailEmail field starts blank (API does not return stored email — documented in code comment); fires "App password saved." toast; sheet stays open

**Create mode (D-07):**
- Single form: display name, username, initial password, confirm password
- `fetchCreateMember` mutation; 409 → "That username is already in use."; fires "Member added." toast + closes sheet

Dialog scaffold matches CredentialSheet/ResetPasswordSheet exactly: `role="dialog"`, `aria-modal`, `useFocusTrap`, Escape closes, focus returns to `triggerRef.current`, phone bottom-sheet vs desktop modal, zIndex 301, overlay `rgba(0,0,0,0.32)`.

All passwords are write-only: never prefilled, `autoComplete="new-password"` (T-20-07/T-20-08 mitigations active).

### AdminPage.tsx (reworked Members tab)

**MemberRow reworked (D-04):** Whole-row `role="button"`, `aria-label="Edit {displayName}"`, `tabIndex={0}`, Enter/Space opens editor. Trailing `ChevronRight` (size 16, `--color-text-muted`). Inline "Admin" badge (`--color-member-0` text, `--color-surface-dim` bg, 12px/600, border-radius 4px) when `member.isAdmin`. Avatar swatch and credential status badge kept unchanged.

**"Add member" ghost trigger (D-07):** Full-width button with `Plus` icon prefix, `1px solid var(--color-border)`, `border-radius 8px`, `min-height 44px`; opens MemberEditorSheet in create mode; focus returns to this button on close.

**Removed:**
- Entire "Local Accounts" section with inline add-member form (~220 lines)
- `ResetPasswordSheet` component definition (~303 lines)
- `CredentialSheet` import and dual-sheet mounting
- All create-form local state (`createDisplayName`, `createUsername`, `createPassword`, `createConfirmPassword`, `createError`)
- `createMemberMutation` in AdminPage (moved to MemberEditorSheet)
- Per-row "Rotate"/"Add credential"/"Reset password" buttons

**Single `MemberEditorSheet` instance** replaces dual CredentialSheet + ResetPasswordSheet mounts; driven by `editorOpen`, `editorMode`, `editorMember`, `editorTriggerRef`.

## Playwright-CLI Verification

Verified on desktop Chromium against http://localhost:5173/admin with DEV_AUTH_BYPASS active (Dev User, id 1, is admin):

1. **Members tab decluttered:** `button "Edit Dev User"` (tappable row with chevron) + `button "Add member"` ghost trigger visible; no Rotate/Add credential/Reset password buttons.
2. **Row tap → Edit member:** `dialog "Edit member"` opens with heading "Edit member", subtitle "Dev User", Profile section (display name prefilled, Admin toggle checked), Set new password section, App password section.
3. **Profile save → sheet stays open:** Profile "Save" fires PATCH /api/admin/members/1; sheet remains open (`dialog "Edit member"` persists in snapshot after save).
4. **Add member trigger → Create mode:** `dialog "Add member"` opens with heading "Add member" and four create-mode fields.

Screenshots captured:
- `.planning/phases/20-admin-member-editor-form-declutter/screenshots/admin-members-tab-decluttered.png`
- `.planning/phases/20-admin-member-editor-form-declutter/screenshots/member-editor-edit-mode.png`
- `.planning/phases/20-admin-member-editor-form-declutter/screenshots/member-editor-create-mode.png`
- `.planning/phases/20-admin-member-editor-form-declutter/screenshots/profile-save-toast.png`

## Deviations from Plan

### Auto-fixed Issues

None — plan executed exactly as written.

### Pre-existing Prettier Drift (out-of-scope cleanup)

**Deviation:** `pnpm format:check` (repo-wide) flagged 17 pre-existing formatting violations in `docs/*.md`, `README.md`, `CLAUDE.md`, `apps/api/src/routes/admin.ts`, and other files not authored in this plan.

**Action (Rule 3 — blocking CI gate):** Ran `pnpm format` to fix all violations. Staged and included in Task 3 commit to keep CI green. Confirmed the violations were pre-existing by checking git diff for files not created/modified by this plan.

## Known Stubs

None. The editor is fully wired to live endpoints. The fastmailEmail field starts blank on edit (documented behavior — `GET /api/admin/members` does not return the stored email; the admin must re-enter it) but this is intentional per the plan spec and D-CONTEXT note.

## Threat Surface Scan

No new network endpoints, auth paths, or file access patterns introduced. `MemberEditorSheet.tsx` is a pure client component wiring to existing Plan 20-01 endpoints behind `requireAdmin`. T-20-07, T-20-08, T-20-09 mitigations are active as documented in the component header.

## TDD Notes

The plan specified `tdd="true"` for Tasks 1 and 2. The PWA has no unit-test harness for sheet components (no existing `*.test.tsx` for CredentialSheet or MemberEditorSheet — jsdom/RTL setup is not in scope for this phase). All behavioral verification was performed via playwright-cli interaction against the live dev stack (per CLAUDE.md convention: "playwright-cli skill to validate UI and workflows instead of asking the operator to check manually"). The 22 existing test files (275 tests) all pass — no regressions.

## Self-Check: PASSED

- `apps/pwa/src/components/MemberEditorSheet.tsx` exists: FOUND
- `apps/pwa/src/routes/AdminPage.tsx` modified: FOUND
- Commit b125a69 exists: FOUND
- Commit 9e6b004 exists: FOUND
- Commit 9b62887 exists: FOUND
- `grep -RnE 'Rotate|Add credential|Reset password' apps/pwa/src/components/MemberEditorSheet.tsx apps/pwa/src/routes/AdminPage.tsx` — 0 matches: PASS
- `grep -q 'ResetPasswordSheet' apps/pwa/src/routes/AdminPage.tsx` — no match: PASS
- `pnpm --filter @familysync/pwa test -- --run` — 275 passed: PASS
