---
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
