---
phase: 17-ui-optimization-polish
plan: 05
type: execute
wave: 2
depends_on: ["17-01"]
files_modified:
  - apps/pwa/src/components/SettingsSheet.tsx
  - apps/pwa/src/components/CredentialSheet.tsx
autonomous: true
requirements: [D-07, D-09]
must_haves:
  truths:
    - "A reachable Sign out control exists in SettingsSheet and clears the session then routes to /login"
    - "Logout navigates to /login even if the logout API call fails (fire-and-best-effort)"
    - "SettingsSheet, ChangePasswordSheet, LinkOidcSheet, and CredentialSheet render centered on desktop and as bottom-sheets on phone"
  artifacts:
    - path: "apps/pwa/src/components/SettingsSheet.tsx"
      provides: "Sign out control + handleSignOut + desktop-centering branch for SettingsSheet/ChangePasswordSheet/LinkOidcSheet"
      contains: "handleSignOut"
    - path: "apps/pwa/src/components/CredentialSheet.tsx"
      provides: "Desktop-centering phone/desktop style branch on the dialog wrapper"
  key_links:
    - from: "apps/pwa/src/components/SettingsSheet.tsx"
      to: "apps/pwa/src/api/client.ts"
      via: "handleSignOut calls fetchLocalLogout()"
      pattern: "fetchLocalLogout"
---

<objective>
Wire a reachable logout control into SettingsSheet (D-07) and fix dialog/sheet centering so all settings sheets render as a centered modal on desktop and an unchanged bottom-sheet on phone (D-09, the SettingsSheet-owned sheets + CredentialSheet).

D-07: logout is fully plumbed server-side (`POST /api/auth/local/logout` works, `fetchLocalLogout()` exists at client.ts:124) but no component calls it. Add a "Sign out" row to SettingsSheet that calls the existing client function then navigates to /login. UI wiring only — no backend work.

D-09: sheets currently use bottom-only positioning (`bottom: 0; left: 0; right: 0; maxWidth: 480px; margin: 0 auto`) which renders bottom-center on desktop instead of truly centered. Add a phone/desktop style branch to each sheet's outer dialog wrapper. (The admin reset-password sheet's centering lives in AdminPage.tsx and is handled in plan 17-06 to keep that file single-owner.)

Purpose: make logout reachable and make all settings sheets feel properly centered on desktop.
Output: a Sign out control + handleSignOut in SettingsSheet, and phone/desktop centering branches on SettingsSheet, ChangePasswordSheet, LinkOidcSheet, and CredentialSheet dialog wrappers.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/17-ui-optimization-polish/17-UI-SPEC.md
@.planning/phases/17-ui-optimization-polish/17-PATTERNS.md
@.planning/phases/17-ui-optimization-polish/17-RESEARCH.md
@.planning/phases/17-ui-optimization-polish/17-01-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add the Sign out control and handleSignOut to SettingsSheet</name>
  <files>apps/pwa/src/components/SettingsSheet.tsx</files>
  <read_first>
    - apps/pwa/src/components/SettingsSheet.tsx (the existing imports line for lucide icons + the api/client imports; the Account section + its section-divider pattern at lines ~372-376; the end of the sheet content where the logout row appends)
    - apps/pwa/src/api/client.ts lines 124-134 (fetchLocalLogout — POST /api/auth/local/logout, credentials include, redirect manual; throws on non-ok)
    - 17-UI-SPEC.md section "Workstream D" -> "D-07 — Logout control" + "Interaction Contract" -> "Logout flow (D-07)" (placement, style, fire-and-best-effort semantics, copy "Sign out")
    - 17-PATTERNS.md section "apps/pwa/src/components/SettingsSheet.tsx" -> the logout row pattern, handleSignOut try/catch excerpt, section divider analog, accessible button row pattern
    - 17-RESEARCH.md section "Common Pitfalls" -> Pitfall 4 (fetchLocalLogout error must not prevent navigation)
  </read_first>
  <action>
    Add the LogOut lucide icon to SettingsSheet's existing lucide-react import, add fetchLocalLogout to the existing api/client import, and add a navigation primitive (react-router useNavigate, matching the project's router usage; if SettingsSheet has no router context use window.location.replace('/login') as the documented fallback per UI-SPEC).

    Add a "Sign out" row at the BOTTOM of the sheet, after all existing sections, separated by the existing 1px var(--color-border-subtle) divider pattern (margin var(--space-4) top/bottom). The button: type button, onClick handleSignOut, aria-label "Sign out", full width, display flex, alignItems center, gap var(--space-2), minHeight 44px, background none, border none, cursor pointer, padding "var(--space-2) 0", fontSize var(--text-body-size), fontWeight 400, color var(--color-destructive), textAlign left, fontFamily var(--font-family-base). Content: a 16px LogOut icon (aria-hidden) + the text "Sign out". No confirmation dialog.

    Implement handleSignOut as an async function that wraps fetchLocalLogout() in try/catch and calls navigate('/login') (or window.location.replace('/login')) in BOTH the success and catch branches — fire-and-best-effort per RESEARCH Pitfall 4 (the server-side cookie is cleared or already expired; navigation must always proceed so the sheet does not stay open on an API failure). Close the sheet as part of the flow if the existing close handler is in scope.
  </action>
  <verify>
    <automated>grep -q 'handleSignOut' apps/pwa/src/components/SettingsSheet.tsx && grep -q 'fetchLocalLogout' apps/pwa/src/components/SettingsSheet.tsx && grep -q 'Sign out' apps/pwa/src/components/SettingsSheet.tsx && grep -q 'LogOut' apps/pwa/src/components/SettingsSheet.tsx && pnpm --filter @familysync/pwa build</automated>
  </verify>
  <acceptance_criteria>
    - SettingsSheet imports LogOut (lucide) and fetchLocalLogout (api/client).
    - A "Sign out" button (aria-label "Sign out", minHeight 44px, var(--color-destructive)) renders at the bottom of the sheet after a divider.
    - handleSignOut calls fetchLocalLogout() inside try/catch and navigates to /login in BOTH branches.
    - playwright-cli: open the settings sheet, confirm the "Sign out" control is visible with its aria-label; click it and confirm redirect to /login. Observation noted in SUMMARY.
    - pnpm --filter @familysync/pwa build exits 0.
  </acceptance_criteria>
  <done>A reachable Sign out control in SettingsSheet calls the existing logout endpoint and always routes to /login (even on API failure), with a 44px tap target and destructive styling.</done>
</task>

<task type="auto">
  <name>Task 2: Add desktop-centering / phone-bottom-sheet branch to all SettingsSheet-owned sheets and CredentialSheet</name>
  <files>apps/pwa/src/components/SettingsSheet.tsx, apps/pwa/src/components/CredentialSheet.tsx</files>
  <read_first>
    - apps/pwa/src/components/SettingsSheet.tsx outer dialog wrapper (lines ~184-202: SettingsSheet), the ChangePasswordSheet wrapper (~598-616), the LinkOidcSheet wrapper (~894-909) — all currently bottom-only positioned
    - apps/pwa/src/components/CredentialSheet.tsx (its outer role=dialog wrapper — apply the same branch)
    - 17-UI-SPEC.md section "Workstream D" -> "D-09 — Dialog/sheet centering fix" (the exact phone vs desktop style contract) + section "Responsive Behavior" (Sheets/dialogs row)
    - 17-PATTERNS.md section "apps/pwa/src/components/SettingsSheet.tsx" -> the phone/desktop style-branch excerpt + the "applies to ALL sheets" list + the Phone/Desktop Breakpoint shared pattern
  </read_first>
  <action>
    Add an isPhone-style breakpoint check (window.matchMedia('(max-width: 767px)').matches, the established project pattern) and branch each sheet's outer role=dialog wrapper style:

    Phone branch (unchanged bottom-sheet): position fixed, bottom 0, left 0, right 0, borderRadius "12px 12px 0 0", plus the existing background/boxShadow/padding/zIndex/fontFamily.

    Desktop branch (centered modal): 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, borderRadius 12px, boxShadow "0 8px 32px rgba(0,0,0,0.18)", plus the existing background/padding/zIndex/fontFamily. Remove bottom/left/right/margin auto from the desktop branch.

    Apply this exact branch to the SettingsSheet wrapper, the ChangePasswordSheet wrapper, and the LinkOidcSheet wrapper (all inside SettingsSheet.tsx), and to the CredentialSheet.tsx dialog wrapper. The backdrop (position fixed, inset 0, var(--color-overlay)) is unchanged. The role=dialog, aria-modal, aria-label, Escape-closes, and focus-return invariants are unchanged — only the position CSS branches. The admin reset-password sheet is NOT in this plan (it lives in AdminPage.tsx; plan 17-06 owns it).
  </action>
  <verify>
    <automated>grep -q 'translate(-50%, -50%)' apps/pwa/src/components/SettingsSheet.tsx && grep -q 'translate(-50%, -50%)' apps/pwa/src/components/CredentialSheet.tsx && grep -q 'max-width: 767px' apps/pwa/src/components/CredentialSheet.tsx && pnpm --filter @familysync/pwa build</automated>
  </verify>
  <acceptance_criteria>
    - SettingsSheet.tsx outer wrappers for SettingsSheet, ChangePasswordSheet, and LinkOidcSheet each have a phone/desktop style branch; desktop uses top/left 50% + translate(-50%, -50%); phone keeps bottom 0 / left 0 / right 0.
    - CredentialSheet.tsx dialog wrapper has the same phone/desktop branch.
    - role=dialog / aria-modal / aria-label unchanged on every wrapper (only position CSS changed).
    - playwright-cli @1280x720: SettingsSheet renders centered (top/left 50% transform); @390x844: renders as bottom-sheet. Observations noted in SUMMARY.
    - pnpm --filter @familysync/pwa build exits 0.
  </acceptance_criteria>
  <done>SettingsSheet, ChangePasswordSheet, LinkOidcSheet, and CredentialSheet all render centered on desktop and as bottom-sheets on phone, with their dialog accessibility invariants unchanged.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Client UI -> existing logout endpoint | The Sign out control calls the already-implemented, Phase-19-verified POST /api/auth/local/logout via fetchLocalLogout(). No new endpoint, no new auth logic. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-17-05-01 | Elevation of Privilege | logout control (D-07) | mitigate | fetchLocalLogout() clears the local-session cookie server-side via the existing endpoint (BL-02 verified live in Phase 19); the client navigates to /login regardless of success/failure so a stale-cookie-with-logged-out-UI state cannot persist. This is UI wiring to an existing, already-verified endpoint — no new trust boundary. |
| T-17-05-02 | Tampering | sheet centering CSS | accept | Position-only CSS branch; no input, no executable content. No new threat above LOW. |

No new high-severity threats. D-07 reuses an existing verified endpoint; D-09 is position-only CSS.
</threat_model>

<verification>
- grep handleSignOut / fetchLocalLogout / "Sign out" / LogOut in SettingsSheet.tsx
- grep translate(-50%, -50%) in SettingsSheet.tsx and CredentialSheet.tsx
- pnpm --filter @familysync/pwa build — exits 0
- playwright-cli: Sign out reachable + redirects to /login; sheets centered @desktop, bottom-sheet @phone
</verification>

<success_criteria>
A reachable Sign out control clears the session and routes to /login (even on API failure), and all four settings sheets (SettingsSheet, ChangePasswordSheet, LinkOidcSheet, CredentialSheet) render centered on desktop and as bottom-sheets on phone with accessibility invariants intact.
</success_criteria>

<output>
Create `.planning/phases/17-ui-optimization-polish/17-05-SUMMARY.md` when done.
</output>
