--- phase: 17-ui-optimization-polish fixed_at: 2026-06-18T00:00:00Z review_path: .planning/phases/17-ui-optimization-polish/17-REVIEW.md iteration: 1 findings_in_scope: 15 fixed: 15 skipped: 0 status: all_fixed --- # Phase 17: Code Review Fix Report **Fixed at:** 2026-06-18T00:00:00Z **Source review:** .planning/phases/17-ui-optimization-polish/17-REVIEW.md **Iteration:** 1 **Summary:** - Findings in scope: 15 (fix_scope: all — includes Info) - Fixed: 15 - Skipped: 0 All fixes were verified with `tsc --noEmit` (clean) and `eslint --max-warnings 0` (clean) on every touched file; the PWA also builds (`vite build` succeeds). A new shared hook `apps/pwa/src/hooks/useIsPhone.ts` was created to back WR-05/IN-03. ## Fixed Issues ### WR-01: Modal dialogs declare `aria-modal="true"` but do not trap focus **Files modified:** `apps/pwa/src/components/CredentialSheet.tsx`, `apps/pwa/src/components/SettingsSheet.tsx`, `apps/pwa/src/routes/AdminPage.tsx` **Commit:** fb30800 **Applied fix:** Reused the existing `useFocusTrap(dialogRef)` hook (already used by EventForm/SeriesEditPrompt). Added a `dialogRef` + `onKeyDown={handleDialogKeyDown}` to every modal sheet that asserts `aria-modal="true"`: CredentialSheet, SettingsSheet, ChangePasswordSheet, LinkOidcSheet, and ResetPasswordSheet. Tab/Shift-Tab now cycle within the dialog instead of escaping to occluded background controls. ### WR-02: Admin tab strip keyboard nav is incomplete (no Home/End, no explicit wrap) **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** f601c0c **Applied fix:** Rewrote `handleTabKeyDown` to the full WAI-ARIA tabs pattern: ArrowLeft/Right now wrap around the ends using modular arithmetic over the `['members','settings']` order, and Home/End jump to the first/last tab. ### WR-03: Success toast does not re-announce repeated identical messages **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** f601c0c **Applied fix:** Changed toast state from `string | null` to `{ id: number; msg: string } | null` with a `showToast(msg)` helper that mints a fresh `id` (Date.now()) per call. The rendered toast `
` is now keyed on `toast.id` so an identical repeated message remounts and `aria-live` re-announces it; the auto-dismiss effect depends on the fresh object reference so the 3s timer restarts. ### WR-04: Toast `whiteSpace: nowrap` is a latent horizontal-overflow regression **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** f601c0c **Applied fix:** Removed `whiteSpace: 'nowrap'` from the toast style so a longer/localized message wraps within `maxWidth: 90vw` instead of overflowing `documentElement.scrollWidth` (which would trip the layout suite's no-horizontal-overflow rule). ### WR-05: `matchMedia(...)` read at render time does not react to resize/orientation **Files modified:** `apps/pwa/src/hooks/useIsPhone.ts` (new), `apps/pwa/src/App.tsx`, `apps/pwa/src/components/CalendarShell.tsx`, `apps/pwa/src/components/SettingsSheet.tsx`, `apps/pwa/src/components/CredentialSheet.tsx`, `apps/pwa/src/routes/AdminPage.tsx` **Commit:** a4a7438 **Applied fix:** Added a resize-aware `useMediaQuery`/`useIsPhone` hook backed by `matchMedia.addEventListener('change', …)`. Replaced all six synchronous `matchMedia('(max-width: 767px)')` render-time reads with `useIsPhone()`. Hook calls were placed before any early `return null` to respect the Rules of Hooks. Components now re-render when the 767px breakpoint is crossed (iPad rotation, desktop resize). ### WR-06: Timezone combobox `aria-activedescendant`/highlight can desync after filtering **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** 1c0f357 **Applied fix:** Derived a clamped `tzActiveIndexClamped = Math.min(tzActiveIndex, max(0, filteredZones.length - 1))` in render and used it for `aria-activedescendant`, the Enter-to-commit lookup, and the visual highlight (`i === tzActiveIndexClamped`). ArrowUp/ArrowDown clamp the current index before moving so they never start from a stale position past the end of a freshly-shrunk list. **Note:** Combobox interaction logic — recommend a quick manual/keyboard pass (type to filter, arrow, Enter) to confirm behavior. ### WR-07: Timezone combobox drops Tab-to-commit and relies on a fragile blur timeout **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** 1c0f357 **Applied fix:** Added a `Tab` branch to the combobox `onKeyDown` that commits the highlighted option WITHOUT `preventDefault` (focus still advances to Save). Added an unmount cleanup effect that clears `tzBlurTimer`. Reduced the blur-close `setTimeout` from 120ms to 0ms now that options `preventDefault()` on `onMouseDown` (so a click never blurs the input first). **Note:** Interaction logic — recommend a manual check that tabbing out of the open listbox commits the highlighted zone and that clicking an option still selects it. ### WR-08: `pwa:icons` script is non-portable and silently coupled to generated filenames **Files modified:** `apps/pwa/scripts/copy-pwa-icons.mjs` (new), `apps/pwa/package.json`, `apps/pwa/vite.config.ts` **Commit:** dd0b761 **Applied fix:** Replaced the five-`cp` Unix-only chain with a cross-platform Node script (`fs.copyFileSync`) that maps each generated filename to its stable manifest name and fails loudly with a named error if a generated file is missing (generator rename guard). Added a discoverability comment beside the manifest `icons` array in `vite.config.ts` pointing at the script's COPIES table. ### IN-01: `OidcRedirect` navigates as a render-phase side effect **Files modified:** `apps/pwa/src/App.tsx` **Commit:** a4a7438 **Applied fix:** Moved `window.location.replace('/api/login')` into a `useEffect(() => {...}, [])` so the navigation is no longer a render-phase side effect. ### IN-02: Inconsistent `exhaustive-deps` disables across sibling dialogs **Files modified:** `apps/pwa/src/components/CredentialSheet.tsx`, `apps/pwa/src/components/SettingsSheet.tsx` **Commit:** 3f4b7ea **Applied fix:** Wrapped `handleClose` in `useCallback` in CredentialSheet and ChangePasswordSheet, added it to the Escape effect's dependency array, and removed the `// eslint-disable-line react-hooks/exhaustive-deps` comments — matching the LinkOidc/Reset sheet pattern. ### IN-03: `isPhone`/`phone` 767px check duplicated across ~6 sites **Files modified:** (same as WR-05) **Commit:** a4a7438 **Applied fix:** Resolved together with WR-05 — the single `useIsPhone()` hook now backs all call sites, and the `(max-width: 767px)` query lives in one place (`PHONE_MAX_QUERY` in the hook). The old standalone `isPhone()` helpers in App.tsx and CalendarShell.tsx were deleted. ### IN-04: Dead placeholder brand tokens retained **Files modified:** `apps/pwa/src/styles/tokens.css` **Commit:** 2317833 **Applied fix:** Removed the unused `--brand-logo-bg`, `--brand-logo-text`, and `--brand-app-name` declarations (verified via grep that nothing references them); left a short comment explaining the removal and that BrandSlot only reads `--brand-logo-size`/`--brand-logo-border-radius`. ### IN-05: Admin members-panel JSX has inconsistent indentation / stacked bottom margins **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** 4bc1e2a **Applied fix:** Ran Prettier (project `.prettierrc`) over AdminPage.tsx, normalizing the members-panel indentation and the rest of the file's drift; `prettier --check` now passes on the file. The stacked `marginBottom: var(--space-8)` on the last panel section was left intentionally — the review flagged it only as a minor cosmetic note, and changing section spacing risks a visual regression outside the finding's scope. ### IN-06: Toast and dialog `zIndex` overlap (300/301) **Files modified:** `apps/pwa/src/routes/AdminPage.tsx` **Commit:** f601c0c **Applied fix:** Raised the toast `zIndex` from 300 to 400 so it always paints above sheet backdrops (300) and sheets (301), removing the DOM-order-dependent paint ambiguity. ### IN-07: `Intl.DateTimeFormat()` recomputed every render in the calendar-config path **Files modified:** `apps/pwa/src/components/CalendarShell.tsx`, `apps/pwa/src/routes/AdminPage.tsx` **Commit:** 11b6b36 **Applied fix:** Wrapped both `Intl.DateTimeFormat().resolvedOptions().timeZone` reads in `useMemo(…, [])` — `displayTimeZone` in CalendarShell (feeds the stable `useCalendarApp` config) and `detectedTz` in AdminPage. --- _Fixed: 2026-06-18T00:00:00Z_ _Fixer: Claude (gsd-code-fixer)_ _Iteration: 1_