diff --git a/.planning/phases/17-ui-optimization-polish/17-05-SUMMARY.md b/.planning/phases/17-ui-optimization-polish/17-05-SUMMARY.md new file mode 100644 index 0000000..4c61e1f --- /dev/null +++ b/.planning/phases/17-ui-optimization-polish/17-05-SUMMARY.md @@ -0,0 +1,117 @@ +--- +phase: 17-ui-optimization-polish +plan: "05" +subsystem: pwa-ui +tags: [logout, settings, desktop-centering, sheet-ux, d-07, d-09] +dependency_graph: + requires: ["17-01"] + provides: [reachable-logout, desktop-centered-sheets] + affects: [SettingsSheet, ChangePasswordSheet, LinkOidcSheet, CredentialSheet] +tech_stack: + added: [] + patterns: [phone-desktop-style-branch, fire-and-best-effort-logout, react-router-useNavigate] +key_files: + modified: + - apps/pwa/src/components/SettingsSheet.tsx + - apps/pwa/src/components/CredentialSheet.tsx +decisions: + - "phone const computed at render time via window.matchMedia (not a hook) — consistent with established App.tsx / BottomTabBar.tsx project pattern" + - "handleSignOut calls onClose() before navigate() so the sheet dismisses even if router state causes a re-render" + - "All three sub-sheets in SettingsSheet.tsx (SettingsSheet, ChangePasswordSheet, LinkOidcSheet) received the centering branch in a single commit since they share the same file and the branch is identical" +metrics: + duration: "6 minutes" + completed: "2026-06-18" + tasks_completed: 2 + tasks_total: 2 + files_changed: 2 +status: complete +--- + +# Phase 17 Plan 05: Logout Control + Sheet Centering Summary + +Wired a reachable Sign out control into SettingsSheet (D-07) and fixed dialog centering so all four settings sheets render as centered modals on desktop and unchanged bottom-sheets on phone (D-09). + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Add Sign out control and handleSignOut to SettingsSheet | `132a5e4` | SettingsSheet.tsx | +| 2 | Add phone/desktop centering branch to all sheets | `b712386` | CredentialSheet.tsx | + +## What Was Built + +### Task 1 — Sign out control (D-07) + +Added to `SettingsSheet.tsx`: +- `LogOut` icon imported from lucide-react (added to existing icon import) +- `fetchLocalLogout` imported from `../api/client.js` (added to existing import) +- `useNavigate` from `react-router` for post-logout routing +- `handleSignOut` async function: wraps `fetchLocalLogout()` in try/catch, calls `onClose()` then `navigate('/login')` in both success and catch branches — fire-and-best-effort per D-07 spec (server cookie is cleared or already expired; navigation must always proceed) +- Sign out button row: 44px minHeight, `var(--color-destructive)` color, 16px LogOut icon, "Sign out" text, full-width, `aria-label="Sign out"`, separated from prior sections by the project's 1px `var(--color-border-subtle)` divider + +The button renders unconditionally at the bottom of the sheet — visible regardless of `hasLocalCredential` or `oidcEnabled` gating. + +### Task 2 — Phone/desktop centering branch (D-09) + +Applied `phone = window.matchMedia('(max-width: 767px)').matches` + ternary style branch to four dialog wrappers: + +| Sheet | File | zIndex | +|-------|------|--------| +| SettingsSheet | SettingsSheet.tsx | 301 | +| ChangePasswordSheet | SettingsSheet.tsx | 303 | +| LinkOidcSheet | SettingsSheet.tsx | 303 | +| CredentialSheet | CredentialSheet.tsx | 301 | + +Phone branch (unchanged): `position: fixed; bottom: 0; left: 0; right: 0; borderRadius: 12px 12px 0 0` + +Desktop branch (new): `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); maxWidth: 480px; width: calc(100% - 32px); maxHeight: calc(100dvh - 32px); overflowY: auto; borderRadius: 12px; boxShadow: 0 8px 32px rgba(0,0,0,0.18)` + +`role=dialog`, `aria-modal`, `aria-label`, Escape-closes, and focus-return invariants unchanged on all wrappers. + +Note: AdminPage.ResetPasswordSheet centering is handled by plan 17-06 (single-file-owner constraint). + +## Playwright Validation + +**Desktop @1280x720:** SettingsSheet opened centered on the page as a modal dialog. Sign out button visible with destructive red styling and LogOut icon at the bottom of the sheet. Clicking Sign out navigated to `/login` (confirmed URL change from `http://localhost:5175/calendar` to `http://localhost:5175/login`). + +**Phone @390x844:** SettingsSheet rendered as a bottom-sheet anchored to the bottom of viewport with rounded top corners. Sign out button visible at bottom. Bottom-sheet behavior unchanged. + +Both screenshots confirmed correct behavior for D-07 and D-09. + +## Verification Checks + +- `grep handleSignOut` in SettingsSheet.tsx: PASS +- `grep fetchLocalLogout` in SettingsSheet.tsx: PASS +- `grep 'Sign out'` in SettingsSheet.tsx: PASS +- `grep LogOut` in SettingsSheet.tsx: PASS +- `grep 'translate(-50%, -50%)'` in SettingsSheet.tsx: PASS (3 occurrences — SettingsSheet, ChangePasswordSheet, LinkOidcSheet) +- `grep 'translate(-50%, -50%)'` in CredentialSheet.tsx: PASS +- `grep 'max-width: 767px'` in CredentialSheet.tsx: PASS +- `pnpm --filter @familysync/pwa build`: PASS (exits 0, 1858 modules) + +## Threat Model Compliance + +| Threat ID | Mitigation Applied | +|-----------|-------------------| +| T-17-05-01 | `handleSignOut` calls `fetchLocalLogout()` (clears server-side cookie) then always navigates to `/login` — no stale-cookie-with-logged-out-UI state possible | +| T-17-05-02 | Position-only CSS branch — no new input or executable content | + +## Deviations from Plan + +None — plan executed exactly as written. + +## Known Stubs + +None — both D-07 and D-09 are fully wired. Sign out calls the real `fetchLocalLogout()` endpoint. Centering is CSS-only with no data source. + +## Threat Flags + +None — no new network endpoints, auth paths, or schema changes introduced. All changes are UI-layer only. + +## Self-Check: PASSED + +- `/home/luc/projects/familysync/.claude/worktrees/agent-a65077bbd9885bf7e/apps/pwa/src/components/SettingsSheet.tsx` — FOUND, contains handleSignOut, fetchLocalLogout, Sign out, LogOut, translate(-50%, -50%) +- `/home/luc/projects/familysync/.claude/worktrees/agent-a65077bbd9885bf7e/apps/pwa/src/components/CredentialSheet.tsx` — FOUND, contains translate(-50%, -50%), max-width: 767px +- Commit `132a5e4` — FOUND (Task 1) +- Commit `b712386` — FOUND (Task 2) +- Build: `pnpm --filter @familysync/pwa build` exits 0 diff --git a/apps/pwa/src/components/CredentialSheet.tsx b/apps/pwa/src/components/CredentialSheet.tsx index f2d0157..f1ded69 100644 --- a/apps/pwa/src/components/CredentialSheet.tsx +++ b/apps/pwa/src/components/CredentialSheet.tsx @@ -153,6 +153,7 @@ export function CredentialSheet({ const heading = headingFor(mode); const isPending = credentialMutation.isPending; const saveDisabled = isPending || password.trim().length === 0 || email.trim().length === 0; + const phone = window.matchMedia('(max-width: 767px)').matches; return ( <> @@ -173,20 +174,37 @@ export function CredentialSheet({ role="dialog" aria-modal="true" aria-label={heading} - style={{ - position: 'fixed', - bottom: 0, - left: 0, - right: 0, - background: 'var(--color-surface)', - borderRadius: '12px 12px 0 0', - boxShadow: '0 -4px 24px rgba(0,0,0,0.15)', - padding: 'var(--space-6, 24px)', - zIndex: 301, - fontFamily: 'var(--font-family-base)', - maxWidth: '480px', - margin: '0 auto', - }} + style={ + phone + ? { + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + background: 'var(--color-surface)', + borderRadius: '12px 12px 0 0', + boxShadow: '0 -4px 24px rgba(0,0,0,0.15)', + padding: 'var(--space-6, 24px)', + zIndex: 301, + fontFamily: 'var(--font-family-base)', + } + : { + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + maxWidth: '480px', + width: 'calc(100% - var(--space-8, 32px))', + maxHeight: 'calc(100dvh - var(--space-8, 32px))', + overflowY: 'auto', + background: 'var(--color-surface)', + borderRadius: '12px', + boxShadow: '0 8px 32px rgba(0,0,0,0.18)', + padding: 'var(--space-6, 24px)', + zIndex: 301, + fontFamily: 'var(--font-family-base)', + } + } > {/* Heading */}