From 0471c2d6c4594c98928aaf59db16639120a2fd70 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 19:13:23 +0000 Subject: [PATCH 1/5] feat(settings): wire up and rebuild the account & app settings surface Turn the settings dialog from a mostly-static display into a fully functional, persisted preferences surface, and polish the design. Behaviour - Add a per-browser app-preferences store (use-app-preferences.ts) mirroring the existing external-store pattern: density, motion, jurisdiction, population, answer style, default landing, personalisation, and notification toggles, with defensive normalisation of stored values. - Extend the theme hook to a light/dark/system preference (setPreference) while keeping theme/toggleTheme; "system" follows the OS and stores no pin. - Density and reduce-motion now take real effect via html[data-density] font scaling and an html[data-motion="reduced"] suppression block, applied before first paint by the layout pre-hydration script so nothing flashes in. - Privacy actions clear recent searches / saved items and reset preferences, with live counters that disable the action when there is nothing to clear. UI - Functional desktop scroll-spy navigation across eight sections. - Interactive controls: wrapping segmented controls (readable long labels on narrow screens), accessible native selects, and role="switch" toggles, all design-token based and legible in light/dark and forced-colors. - Complete Account, Clinical defaults, App preferences, Personalisation, Notifications, Privacy, Keyboard shortcuts, and Help & About sections. Drops the now-unused theme/onToggleTheme props from the two call sites; the required headings, row test ids, and close affordance are preserved. Tests: add theme preference and app-preference normalisation coverage. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RiRMGogS7jxgngFRYEHjw3 --- src/app/globals.css | 36 + src/app/layout.tsx | 6 +- src/components/ClinicalDashboard.tsx | 2 - .../global-search-shell.tsx | 2 - .../recent-query-storage.ts | 21 + .../clinical-dashboard/settings-dialog.tsx | 1230 ++++++++++++----- .../clinical-dashboard/use-app-preferences.ts | 230 +++ .../clinical-dashboard/use-theme.ts | 56 +- src/lib/theme.ts | 17 + tests/app-preferences.test.ts | 68 + tests/theme.test.ts | 13 +- 11 files changed, 1327 insertions(+), 354 deletions(-) create mode 100644 src/components/clinical-dashboard/use-app-preferences.ts create mode 100644 tests/app-preferences.test.ts diff --git a/src/app/globals.css b/src/app/globals.css index a9fb9afe6..d2bbac44f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -436,6 +436,22 @@ html { scroll-padding-top: calc(4rem + env(safe-area-inset-top)); } +/* + * Interface density scales the rem baseline so every rem-based size shifts + * together. "Comfortable" is the browser default (16px) and sets no attribute, + * so the untouched default experience is unchanged. The attribute is applied on + * before first paint (layout.tsx inline script) and kept in sync by + * useAppPreferences. The unlayered 16px mobile input floor above still wins for + * form controls so iOS never zooms. + */ +html[data-density="compact"] { + font-size: 15px; +} + +html[data-density="spacious"] { + font-size: 17px; +} + body { min-height: 100%; min-height: 100svh; @@ -2183,6 +2199,26 @@ summary::-webkit-details-marker { } } +/* + * Explicit "Reduce motion" preference from the settings surface. Mirrors the + * prefers-reduced-motion suppression above so users can opt in regardless of + * their OS setting. The attribute is set on before first paint by the + * inline script in layout.tsx and kept in sync by useAppPreferences. + */ +html[data-motion="reduced"] *, +html[data-motion="reduced"] *::before, +html[data-motion="reduced"] *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; +} + +html[data-motion="reduced"] .source-capsule-hit:hover .source-capsule-face, +html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .source-capsule-face { + transform: none !important; +} + @media (forced-colors: active) { :root, .dark { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 61fa12ce0..115c1b2ae 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -80,14 +80,16 @@ export default async function RootLayout({ {/* Applies the resolved theme before first paint on every route (standalone pages don't mount useTheme, and hydration-time toggling flashes light). Mirrors resolveThemePreference in src/lib/theme.ts: stored choice wins, - otherwise the OS preference. Key must match use-theme.ts. */} + otherwise the OS preference. Key must match use-theme.ts. The second + block applies the density/motion preferences (keys must match + use-app-preferences.ts) so an opted-in choice never flashes in. */}