Files
familysync/.planning/phases/20-admin-member-editor-form-declutter/20-UI-SPEC.md
T

318 lines
16 KiB
Markdown
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.
---
phase: 20
slug: admin-member-editor-form-declutter
status: draft
shadcn_initialized: false
preset: none
created: 2026-06-18
---
# Phase 20 — UI Design Contract
> Visual and interaction contract for the Admin Member Editor & Form Declutter phase.
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — custom CSS custom properties |
| Preset | not applicable |
| Component library | none — inline styles referencing CSS tokens |
| Icon library | lucide-react (existing: CheckCircle, AlertCircle, Loader2; add ChevronRight) |
| Font | system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif |
No `components.json` detected. Project uses a hand-rolled design token system declared in
`apps/pwa/src/styles/tokens.css`. All new components must reference those CSS custom properties
(never hard-code hex/px). This is an established project convention — do not introduce shadcn or
any third-party component library in this phase.
---
## Spacing Scale
All tokens sourced from `apps/pwa/src/styles/tokens.css` (pre-populated — no change needed):
| Token | Value | Usage |
|-------|-------|-------|
| `--space-1` | 4px | Icon gaps, label-to-input margin |
| `--space-2` | 8px | Badge icon gap, toast icon gap, button row gap |
| `--space-3` | 12px | Field gap inside editor sections, tab padding |
| `--space-4` | 16px | Default field padding, section heading margin-bottom |
| `--space-6` | 24px | Sheet padding, section gap between Profile / Password / App-password |
| `--space-8` | 32px | Section bottom margin in Members panel |
| `--space-12` | 48px | Page top/bottom padding |
Exceptions:
- Member row minimum height: 44px (touch target floor — not a spacing token)
- Sheet border-radius: 12px phone bottom-sheet top corners / 12px desktop modal all corners (matches existing CredentialSheet / ResetPasswordSheet)
- MemberRow add-member trigger button bottom margin: `--space-4` below member list, above the trigger
---
## Typography
All values sourced from `tokens.css` (pre-populated — no change):
| Role | Size | Weight | Line Height | Usage in this phase |
|------|------|--------|-------------|---------------------|
| Body | 15px (`--text-body-size`) | 400 (`--text-body-weight`) | 1.5 (`--text-body-line-height`) | Member display name in row; section description text; subtitle line under sheet heading |
| Label | 13px (`--text-label-size`) | 400 or 600 | 1.4 (`--text-label-line-height`) | Form field labels (600), credential status badges (400), toast copy (400), section MEMBERS heading (600 + uppercase) |
| Heading | 18px (`--text-heading-size`) | 600 (`--text-heading-weight`) | 1.25 (`--text-heading-line-height`) | Sheet h2 ("Edit member", "Add member"), editor section dividers |
| Display | 24px (`--text-display-size`) | 600 (`--text-display-weight`) | 1.2 (`--text-display-line-height`) | Not used in this phase |
---
## Color
All values sourced from `tokens.css` (pre-populated):
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `--color-surface` (#ffffff) | Page background, sheet background, input background |
| Secondary (30%) | `--color-surface-dim` (#f7f7f8) / `--color-border-subtle` (#eceef2) | MemberRow bottom border, section dividers, "Add member" trigger button border |
| Accent (10%) | `--color-member-0` (#e8915a) | Primary action buttons only (see reserved-for list below) |
| Destructive | `--color-destructive` (#dc2626) | Inline error text, error-state input border |
Accent (`--color-member-0`) reserved for:
1. Primary save/submit button background (enabled state) in the Member editor — each section's "Save" button
2. Active tab underline indicator in the Members & Accounts / Settings tab strip
3. "Currently shared" label text in the calendar picker (existing pattern, unchanged)
4. Toast success icon (`CheckCircle`)
5. "Use detected:" timezone affordance link (existing pattern, unchanged)
Accent is NOT used on:
- Cancel buttons (use `--color-text-secondary`, no background)
- Disabled buttons (use `--color-border` as background)
- The "Add member" trigger button (ghost button: `--color-surface` background, `--color-surface-dim` hover — matches Surface A; not an accent CTA)
- The row chevron/edit icon (use `--color-text-muted`)
Member avatar swatches use `--color-member-{colorIndex}` (index-derived, unchanged from existing MemberRow).
---
## Surface Inventory
This phase introduces or reworks the following surfaces:
### Surface A — Member list (Members tab, existing panel body)
The current inline "Add member" form and the per-row action-button cluster are removed.
The panel body becomes:
```
[Section label: MEMBERS]
[MemberRow] × N ← whole-row tap opens editor sheet; trailing ChevronRight icon
[Divider]
[+ Add member button] ← single trigger at bottom; opens MemberEditorSheet in create mode
```
MemberRow (reworked):
- Layout: avatar swatch (32px circle) → name + status badge → flex spacer → ChevronRight icon (16px, `--color-text-muted`)
- The entire row is the tap/click target (min-height 44px, `cursor: pointer`)
- `role="button"` on the row div, `aria-label="Edit {displayName}"`, `tabIndex={0}`, keyboard: Enter/Space opens editor
- Trailing admin badge: when `member.isAdmin === true`, show a small inline badge — label "Admin", 12px/600, `--color-member-0` text on `--color-surface-dim` background, border-radius 4px, padding 2px 6px — placed between the status badge and the chevron
- Remove: "Rotate" button, "Add credential" button, "Reset password" button — all removed from MemberRow
"Add member" trigger button:
- Full-width or left-aligned ghost button with `+` icon prefix (use lucide `Plus`, 16px)
- Label: "Add member"
- Border: `1px solid var(--color-border)`, border-radius 8px, padding `--space-3` vertical / `--space-4` horizontal
- Background: `--color-surface`; hover/focus: `--color-surface-dim`
- Min-height: 44px
- Opens MemberEditorSheet in create mode
### Surface B — Member editor sheet (MemberEditorSheet, new component)
Single sheet component with `mode` prop: `'edit'` (id present) or `'create'` (no id).
Sheet chrome (identical to CredentialSheet / ResetPasswordSheet):
- Phone: fixed bottom, `border-radius 12px 12px 0 0`, `box-shadow: 0 -4px 24px rgba(0,0,0,0.15)`, `padding: var(--space-6)`, zIndex 301
- Desktop: centered modal, `max-width 480px`, `border-radius 12px`, `box-shadow: 0 8px 32px rgba(0,0,0,0.18)`, `padding: var(--space-6)`, zIndex 301
- Backdrop: `rgba(0,0,0,0.32)` (`--color-overlay`), zIndex 300, click closes
- Escape closes; focus returns to trigger row on close
- `role="dialog"`, `aria-modal="true"`, `aria-label` = sheet heading
- `useFocusTrap` on dialog div
- On open: focus the h2 heading (`tabIndex={-1}`)
- Desktop: `max-height: calc(100dvh - var(--space-8))`, `overflow-y: auto`
Sheet heading:
- `h2`, 18px/600/1.25, `--color-text-primary`
- Edit mode: "Edit member"
- Create mode: "Add member"
Member subtitle (edit mode only):
- 15px/400, `--color-text-secondary`, `margin-bottom: var(--space-6)`
- Displays `member.displayName`
**Edit mode — three collapsible sections rendered as labeled card blocks:**
Section divider style (between sections):
- `border-top: 1px solid var(--color-border-subtle)`, `margin: var(--space-6) 0`
**Section 1 — Profile** (always shown in edit mode):
- Section heading: "Profile", 13px/600/uppercase/letterspacing 0.06em, `--color-text-muted`
- Field: "Display name" — text input, 15px, min-height 44px, border `--color-border`, border-radius 4px, padding `--space-3 --space-4`
- Field: "Admin" — toggle/checkbox row:
- Label: "Admin", 15px/400, `--color-text-primary`
- Sub-label: "Can access admin settings", 13px/400, `--color-text-secondary`
- Control: native `<input type="checkbox">` styled as a toggle (or a `<button role="switch">` with `aria-checked`) — use whichever is cleaner; visually: pill shape, 44px×24px, `--color-member-0` when checked, `--color-border` when unchecked, white thumb circle
- Initial state: `member.isAdmin` from API
- Last-admin guard: if saving would demote the last admin, show inline error below the toggle: "Cannot remove admin — at least one admin must remain." (red, 13px)
- Save button for this section: label "Save", right-aligned, same primary button style (accent background, 13px/600, min-height 44px, border-radius 4px)
- Error state: inline below Save button, `--color-destructive`, 13px
**Section 2 — Set new password** (edit mode only; only shown when `member.hasLocalCredential === true`):
- Section heading: "Set new password", same style as above
- Helper: "Leave blank to keep the current password." — 13px/400, `--color-text-secondary`, below heading
- Field: "New password" — `type="password"`, `autoComplete="new-password"`, min-height 44px
- Field: "Confirm new password" — same
- Password never pre-filled. `autoComplete="new-password"` on both fields.
- Save button: label "Set password", right-aligned, primary style
- Validation: if fields differ, inline error "Passwords do not match." If new password < 8 chars, "Password must be at least 8 characters."
- In-flight: show `Loader2` 14px inline in the button (existing pattern)
**Section 3 — App password** (edit mode only):
- Section heading: "App password", same style
- Helper text: "Fastmail app password scoped to Calendars & Contacts (CalDAV)."
- Include inline link: "Get an app password" → `https://app.fastmail.com/settings/security/devicetokens`, `target="_blank" rel="noopener noreferrer"`, `--color-member-0` color, underline
- Field: "Fastmail email" — `type="email"`, `autoComplete="email"`, min-height 44px
- In edit mode: if the stored `fastmailEmail` is available from the credential, prefill this field with the stored email (read-convenience; the user can correct it). The app password field is never prefilled.
- Field: "App password" — `type="password"`, `autoComplete="new-password"`, never prefilled
- "Validating against CalDAV…" — shown during in-flight mutation, using `Loader2` 16px + text (matches CredentialSheet pattern)
- CalDAV failure error: "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."
- Save button: label "Save app password", right-aligned, primary style
**Create mode — single unified form (no section dividers):**
- Fields: Display name, Username, Initial password, Confirm password
- Matches today's inline Add-member form fields exactly
- Save button: label "Add member", right-aligned, primary style
- Client validation: passwords match + ≥ 8 chars; username conflict 409 → "That username is already in use."
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Editor sheet heading (edit) | "Edit member" |
| Editor sheet heading (create) | "Add member" |
| Profile section heading | "Profile" |
| Password section heading | "Set new password" |
| Password section helper | "Leave blank to keep the current password." |
| App password section heading | "App password" |
| App password helper | "Fastmail app password scoped to Calendars & Contacts (CalDAV)." |
| App password link text | "Get an app password" |
| Profile Save CTA | "Save" |
| Password Save CTA | "Set password" |
| App password Save CTA | "Save app password" |
| Create mode Save CTA | "Add member" |
| Add member trigger (panel) | "Add member" |
| Cancel (all sheets) | "Cancel" |
| Admin badge label | "Admin" |
| Admin toggle label | "Admin" |
| Admin toggle sub-label | "Can access admin settings" |
| Credential status — set | "Credential set" |
| Credential status — missing | "No credential" |
| Validating state | "Validating against CalDAV…" |
| CalDAV failure error | "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." |
| Last-admin guard error | "Cannot remove admin — at least one admin must remain." |
| Password mismatch error | "Passwords do not match." |
| Password too short error | "Password must be at least 8 characters." |
| Username conflict error | "That username is already in use. Choose a different one." |
| Generic save error | "Something went wrong. Please try again." |
| Profile saved toast | "Profile saved." |
| Password reset toast | "Password updated." |
| App password saved toast | "App password saved." |
| Member added toast | "Member added." |
| Loading members state | "Loading members…" |
| Members load error | "Could not load members." |
| Empty members state heading | "No members yet" |
| Empty members state body | "Add a member to get started." |
**Retired copy (must not appear anywhere after this phase):**
- "Rotate" — remove from all button labels, headings, and aria-labels
- "Add credential" — remove from MemberRow button
- "Reset password" — remove as standalone button; replaced by "Set new password" section inside editor
---
## Interaction Contracts
### Row tap → editor open
- Tap/click anywhere on a MemberRow opens MemberEditorSheet in edit mode for that member
- Keyboard: Enter or Space on the row (when `role="button"`) opens the sheet
- Focus returns to the tapped row on sheet close
### "Add member" trigger → create sheet
- Tap/click the "Add member" button opens MemberEditorSheet in create mode
- On success: sheet closes, member list refreshes, toast "Member added." fires
- Focus returns to the "Add member" button on cancel/close
### Per-section saves (edit mode)
- Each section (Profile / Set new password / App password) has its own Save button
- Save is independent — one section save does not affect other sections
- Disabled state: button uses `--color-border` background, `cursor: default`
- In-flight: Loader2 spinner inline in button label (existing pattern), button disabled
- Success: toast fires, sheet stays open (user may save another section)
- Error: inline error below the section's save button
### Admin toggle
- Toggle flips `is_admin` optimistically on the client
- On save (Profile section Save): sends to `PATCH /api/admin/members/:id`
- If server returns 409/422 (last-admin guard): show inline error "Cannot remove admin — at least one admin must remain." Revert toggle to previous state.
### Sheet dismissal
- Backdrop click: closes sheet, discards unsaved changes, focus returns to trigger
- Escape key: same as backdrop click
- Cancel button: same as backdrop click
- No confirmation dialog for unsaved changes (forms are short; data loss is minimal)
### Toast positioning
- Phone: `bottom: calc(var(--bottom-chrome-h) + var(--space-4))` (clears BottomTabBar)
- Desktop: `bottom: var(--space-6)`
- Auto-dismiss: 3000ms (existing pattern, unchanged)
---
## Accessibility Contract
- MemberRow: `role="button"`, `aria-label="Edit {displayName}"`, `tabIndex={0}`, keyboard Enter/Space
- MemberEditorSheet: `role="dialog"`, `aria-modal="true"`, `aria-label` matches sheet h2
- Focus management: on open → h2 (`tabIndex={-1}`); on close → trigger element
- `useFocusTrap` traps Tab/Shift+Tab within dialog
- Admin toggle: `role="switch"`, `aria-checked={isAdmin}`, `aria-label="Admin"`
- Inline errors: `aria-describedby` on the relevant input(s) pointing to the error id
- Password fields: `autoComplete="new-password"`, never prefilled
- Fastmail email field (app password section): `autoComplete="email"`
- Toast: `role="status"`, `aria-live="polite"`, `aria-atomic="true"`
- 44px minimum touch target on all interactive elements
- Section headings inside sheet: use `<div>` with sectionLabelStyle (matches AdminPage pattern) — not `<h3>`, to avoid heading hierarchy issues with the existing `<h2>` sheet title
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — shadcn not initialized |
| Third-party | none | not applicable |
No third-party component registries. All components are hand-authored using project CSS tokens.
Icon additions from `lucide-react` (already a project dependency): `ChevronRight`, `Plus`.
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending