feat(client): page-level edit toggle on Profile (per design package) #266

Merged
james merged 2 commits from profile-page-level-edit into main 2026-06-23 23:41:43 +00:00
Owner

Scope

Refactor Carol's Profile screen to match the design package's pattern: a single page-level Edit / Cancel / Done control in the header that flips every card on the page (Picture, Basics, Contact details) between view and edit mode together.

Mirrors /var/home/wynnj/projects/carol-design/design_files/ProfileScreen.jsx: PageHead renders title + lede on the left with an Edit (secondary, pencil-leading) button on the right that becomes Cancel (ghost) + Done (primary) when editing; cards are stateless w.r.t. that flag and render view/edit UI from props.

Per-surface narrative

  • PageHeader (new). Replaces the standalone header <View>. Renders title + lede + the edit control. Edit shows the Lucide Pencil icon next to the label; Done shows "Saving…" while the batched basics PUT is in flight. Cancel is disabled while committing so a half-committed state can't get cancelled.
  • PictureCard. View mode: avatar + name + "Used on your profile and when you chat with me." hint. Edit mode: avatar + Upload + Remove buttons + the upload hint. The card-level inline remove-confirm is gone — picture ops are atomic, the page-level toggle is the gate. The error banner stays.
  • BasicsCard. No card-level Edit button. forwardRef exposes commit() / revert() so the page's Done flushes a single PUT for name + title statement + brief and Cancel re-seeds inputs from server state. commit() is a no-op when nothing changed.
  • ContactsCard. Each row in view mode is just badge + value + label, no action buttons. In edit mode each row picks up Lucide Pencil + Trash2 icon buttons; pencil expands the existing inline edit form (per-row Save / Cancel + Enter chain preserved), trash deletes immediately. The Add inline form moves to the bottom of the list and renders only when the page is in edit mode.
  • i18n. New profile.edit.{edit,cancel,done,saving,editAria,cancelAria,doneAria} keys plus contacts.{editRowAria,removeRowAria} for the per-row icon buttons.

Decisions worth flagging

  • Done semantics. Only Basics has a batched diff to flush, so only Basics implements the EditableHandle imperative API. Picture upload/remove and Contact CRUD remain immediate mutations behind the page-level toggle — batching them would have meant inverting their existing per-op state machines for no user-visible win.
  • Cancel revert. Basics re-seeds its inputs from the latest server props; Contacts row-edit forms re-seed via their existing local effect when the page leaves edit mode; the Add form clears.
  • Contact icon buttons. RN equivalents of the design's <IconButton> are <Pressable> wrappers around the Lucide icon component (32×32 tap target, 16px icon at 1.75 stroke). hitSlop={6} cushions touch on Android.
  • Trash = one-tap delete. Matches the design's bare-icon affordance. The pre-existing confirm-on-delete dialog inside the row's "Remove" button is gone — the icon's semantic intent (Trash2) plus the accessibilityLabel are now the only affordance.

Plain-object styles

Every <View>, <Text>, <Pressable>, <Image>, and <TextInput> on the touched code paths hands off a { ...style, ...overrides } plain object — no nested style={[a, b]} arrays — per #239 and the runtime crash documented in #232's PR body.

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck / lint / test — unchanged surface
  • pnpm -F @carol/api-client typecheck / lint / test / check
  • pnpm -F @carol/client typecheck / lint / test
  • pnpm -F @carol/client export:web
  • Manual: PWA — load /profile, click Edit, edit a name, click Done; observe single PUT lands and screen returns to view mode.
  • Manual: PWA — click Edit, edit a name, click Cancel; observe no PUT, original value restored.
  • Manual: PWA — in edit mode, click pencil on a contact row, change the value, Save; observe row-level PATCH and row collapses.
  • Manual: PWA — in edit mode, click trash on a contact row; observe immediate DELETE.
  • Manual: PWA — in edit mode, fill out the Add form at the bottom of the list and click Add; observe POST and new row appears.
  • Manual: PWA — in edit mode, click Upload, pick an image; observe immediate POST and avatar refresh.
  • Manual: Android APK once published — same end-to-end paths.

🤖 Generated with Claude Code

## Scope Refactor Carol's Profile screen to match the design package's pattern: a single page-level Edit / Cancel / Done control in the header that flips every card on the page (Picture, Basics, Contact details) between view and edit mode together. Mirrors `/var/home/wynnj/projects/carol-design/design_files/ProfileScreen.jsx`: `PageHead` renders title + lede on the left with an Edit (secondary, pencil-leading) button on the right that becomes Cancel (ghost) + Done (primary) when editing; cards are stateless w.r.t. that flag and render view/edit UI from props. ## Per-surface narrative - **`PageHeader` (new).** Replaces the standalone header `<View>`. Renders title + lede + the edit control. Edit shows the Lucide `Pencil` icon next to the label; Done shows "Saving…" while the batched basics PUT is in flight. Cancel is disabled while committing so a half-committed state can't get cancelled. - **`PictureCard`.** View mode: avatar + name + "Used on your profile and when you chat with me." hint. Edit mode: avatar + Upload + Remove buttons + the upload hint. The card-level inline remove-confirm is gone — picture ops are atomic, the page-level toggle is the gate. The error banner stays. - **`BasicsCard`.** No card-level Edit button. `forwardRef` exposes `commit()` / `revert()` so the page's Done flushes a single PUT for name + title statement + brief and Cancel re-seeds inputs from server state. `commit()` is a no-op when nothing changed. - **`ContactsCard`.** Each row in view mode is just badge + value + label, no action buttons. In edit mode each row picks up Lucide `Pencil` + `Trash2` icon buttons; pencil expands the existing inline edit form (per-row Save / Cancel + Enter chain preserved), trash deletes immediately. The Add inline form moves to the bottom of the list and renders only when the page is in edit mode. - **i18n.** New `profile.edit.{edit,cancel,done,saving,editAria,cancelAria,doneAria}` keys plus `contacts.{editRowAria,removeRowAria}` for the per-row icon buttons. ## Decisions worth flagging - **Done semantics.** Only Basics has a batched diff to flush, so only Basics implements the `EditableHandle` imperative API. Picture upload/remove and Contact CRUD remain immediate mutations behind the page-level toggle — batching them would have meant inverting their existing per-op state machines for no user-visible win. - **Cancel revert.** Basics re-seeds its inputs from the latest server props; Contacts row-edit forms re-seed via their existing local effect when the page leaves edit mode; the Add form clears. - **Contact icon buttons.** RN equivalents of the design's `<IconButton>` are `<Pressable>` wrappers around the Lucide icon component (32×32 tap target, 16px icon at 1.75 stroke). `hitSlop={6}` cushions touch on Android. - **Trash = one-tap delete.** Matches the design's bare-icon affordance. The pre-existing confirm-on-delete dialog inside the row's "Remove" button is gone — the icon's semantic intent (`Trash2`) plus the `accessibilityLabel` are now the only affordance. ## Plain-object styles Every `<View>`, `<Text>`, `<Pressable>`, `<Image>`, and `<TextInput>` on the touched code paths hands off a `{ ...style, ...overrides }` plain object — no nested `style={[a, b]}` arrays — per #239 and the runtime crash documented in #232's PR body. ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` / `lint` / `test` — unchanged surface - [x] `pnpm -F @carol/api-client typecheck` / `lint` / `test` / `check` - [x] `pnpm -F @carol/client typecheck` / `lint` / `test` - [x] `pnpm -F @carol/client export:web` - [ ] Manual: PWA — load `/profile`, click Edit, edit a name, click Done; observe single PUT lands and screen returns to view mode. - [ ] Manual: PWA — click Edit, edit a name, click Cancel; observe no PUT, original value restored. - [ ] Manual: PWA — in edit mode, click pencil on a contact row, change the value, Save; observe row-level PATCH and row collapses. - [ ] Manual: PWA — in edit mode, click trash on a contact row; observe immediate DELETE. - [ ] Manual: PWA — in edit mode, fill out the Add form at the bottom of the list and click Add; observe POST and new row appears. - [ ] Manual: PWA — in edit mode, click Upload, pick an image; observe immediate POST and avatar refresh. - [ ] Manual: Android APK once published — same end-to-end paths. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
New strings power the Profile page's header Edit/Cancel/Done control
and the per-row pencil/trash aria labels, both introduced when the
page lifts editing state up from individual cards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat(client): page-level edit toggle on Profile
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 9s
PR / OSV-Scanner (pull_request) Successful in 1m0s
PR / Static analysis (pull_request) Successful in 1m23s
PR / Client (web export smoke) (pull_request) Successful in 2m39s
PR / pnpm audit (pull_request) Successful in 2m54s
PR / Test (sqlite) (pull_request) Successful in 3m26s
PR / Lint (pull_request) Successful in 3m26s
PR / OpenAPI (pull_request) Successful in 3m26s
PR / Typecheck (pull_request) Successful in 3m33s
PR / Package age policy (soft) (pull_request) Successful in 51s
PR / Build (pull_request) Successful in 3m40s
PR / Trivy (image) (pull_request) Successful in 2m26s
Secrets / gitleaks (pull_request) Successful in 44s
PR / Test (postgres) (pull_request) Successful in 3m45s
PR / Coverage (soft) (pull_request) Successful in 2m22s
754046d834
Mirrors the design package's ProfileScreen.jsx: a single Edit /
Cancel / Done control in the page header swaps every card on the
page between view and edit mode together.

- PageHeader (new) renders title + lede on the left and the edit
  control on the right. Edit (secondary, with Lucide Pencil)
  becomes Cancel (ghost) + Done (primary) when editing.
- PictureCard: avatar + name + view-hint in view mode; avatar +
  Upload + Remove + upload hint in edit mode. The card-level
  inline confirm and standalone Change/Remove affordances are
  gone — picture ops stay atomic (no diff to batch), just hidden
  behind the page-level toggle.
- BasicsCard: forwardRef exposes commit() / revert() so the page
  Done flushes the batched PUT and Cancel reverts pending inputs.
  No card-level Edit button.
- ContactsCard: each row in view mode is just badge + value +
  label. In edit mode, rows pick up Lucide Pencil + Trash2 icon
  buttons; pencil expands the existing inline edit form (with
  its own per-row Save / Cancel + Enter chain), trash deletes
  immediately. The Add inline form moves to the bottom of the
  list and only renders when the page is in edit mode.

Styles on DOM-leaf primitives are pre-flattened to plain objects
per #239 — every touched View / Text / Pressable / Image / TextInput
hands off `{ ...style, ...overrides }` instead of an array.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.7% ≥ 50%
Branches 75.1% ≥ 75%
Functions 91.7% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.7% ✅ | ≥ 50% | | Branches | 75.1% ✅ | ≥ 75% | | Functions | 91.7% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 4f197afa0f into main 2026-06-23 23:41:43 +00:00
james deleted branch profile-page-level-edit 2026-06-23 23:41:43 +00:00
Sign in to join this conversation.
No description provided.