diff --git a/docs/branch-review-records/2fed40a238cbd6e5803e25f6c4778648494337db0a6927d4b2f55c077e60c173.record.md b/docs/branch-review-records/2fed40a238cbd6e5803e25f6c4778648494337db0a6927d4b2f55c077e60c173.record.md new file mode 100644 index 0000000000..306888f4ff --- /dev/null +++ b/docs/branch-review-records/2fed40a238cbd6e5803e25f6c4778648494337db0a6927d4b2f55c077e60c173.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 6a1ef9ef020ff41165a9d02245367052a68bd7b3 | PWA mobile root CLS budget remediation | Narrowed install-sheet root-content repositioning from all phone widths to the documented <=359px compact layout, preserving the 320px overflow safeguard while avoiding the confirmed mobile-root shift. | git diff --check; Lighthouse CI log diagnosis: mobile-root CLS +0.105 confirmed in 3/3 samples; local Lighthouse intentionally not run | diff --git a/docs/branch-review-records/497a7b26ad6568692b3dbc9e939c1e7d35a1a50108e2c0268cbf41ee612eaa76.record.md b/docs/branch-review-records/497a7b26ad6568692b3dbc9e939c1e7d35a1a50108e2c0268cbf41ee612eaa76.record.md new file mode 100644 index 0000000000..015bde1bc4 --- /dev/null +++ b/docs/branch-review-records/497a7b26ad6568692b3dbc9e939c1e7d35a1a50108e2c0268cbf41ee612eaa76.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | c6d8c2829ddd2fc52441a3696aed56fbd0262917 | required base sync through main 17402395 | Approved — required main update merged; prior PWA lifecycle review and style-inventory repair remain applicable with no PR-path conflict | git diff --check; ci-change-scope self-test; ledger/inbox/outstanding/discipline guards passed | diff --git a/docs/branch-review-records/4a51a80a457f85c939048278f01e3a3059d5509bfd975096af94b63ef16e825a.record.md b/docs/branch-review-records/4a51a80a457f85c939048278f01e3a3059d5509bfd975096af94b63ef16e825a.record.md new file mode 100644 index 0000000000..1812f5505d --- /dev/null +++ b/docs/branch-review-records/4a51a80a457f85c939048278f01e3a3059d5509bfd975096af94b63ef16e825a.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 80fd6c04afdfa30e92c1cdf2ea5ba9d3d32a02c6 | PWA style-contract formatter follow-up | Formatted the PWA style-contract exemption entries reported by changed-file formatting. Targeted registry test unavailable because this isolated worktree has no node_modules/vitest; Lighthouse was not run locally by instruction. | node --check tests/helpers/style-contracts.ts; git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; ci-change-scope --self-test | diff --git a/docs/branch-review-records/534a1703b8963c66fe43de4889cf3f33e6bc83a7ebc8734fba8c6c71148e0d6f.record.md b/docs/branch-review-records/534a1703b8963c66fe43de4889cf3f33e6bc83a7ebc8734fba8c6c71148e0d6f.record.md new file mode 100644 index 0000000000..1d9d2361b4 --- /dev/null +++ b/docs/branch-review-records/534a1703b8963c66fe43de4889cf3f33e6bc83a7ebc8734fba8c6c71148e0d6f.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | aa041fd15f902dce172ea0d2707f3f81cb8f160c | PWA install lifecycle: final current-base merge | Merged latest required base after validated PWA registry fix; no merge conflicts | git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; static PWA registry assertion | diff --git a/docs/branch-review-records/615deb9148ec106e31cf72902d4a1b2710987b3597aeef62b9a2fb964975dd5b.record.md b/docs/branch-review-records/615deb9148ec106e31cf72902d4a1b2710987b3597aeef62b9a2fb964975dd5b.record.md new file mode 100644 index 0000000000..6a5b00ae82 --- /dev/null +++ b/docs/branch-review-records/615deb9148ec106e31cf72902d4a1b2710987b3597aeef62b9a2fb964975dd5b.record.md @@ -0,0 +1 @@ +| 2026-08-15 | 1976 | 54327591bb89f6f9f25f6834fbefc277a9bff80b | review-and-fix | P2 fixed: native install prompt now avoids hero reflow, composer overlap, and prompt-induced CLS | pwa DOM 10/10; PWA Chromium 5/5; format; exact-head merge-tree | diff --git a/docs/branch-review-records/63bb22a3ea42c41966caaa63a61b73a2bba0026f0898a8f6e3afae6123075fce.record.md b/docs/branch-review-records/63bb22a3ea42c41966caaa63a61b73a2bba0026f0898a8f6e3afae6123075fce.record.md new file mode 100644 index 0000000000..2162c854bd --- /dev/null +++ b/docs/branch-review-records/63bb22a3ea42c41966caaa63a61b73a2bba0026f0898a8f6e3afae6123075fce.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 80fd6c04afdfa30e92c1cdf2ea5ba9d3d32a02c6 | PR #1976 PWA exemption formatter follow-up | Corrected two exemption descriptions that were under the 120-column formatter width and must remain single-line properties. | git diff --check; ledger/outstanding/branch-ledger/ledger-discipline guards; ci-change-scope self-test passed; npm test -- tests/style-contract-registry.test.ts unavailable: node_modules/vitest/vitest.mjs absent. | diff --git a/docs/branch-review-records/7620543101feadcebdc49427ad0395f1f4c8a0d304ec3a5251f1920d023b06c0.record.md b/docs/branch-review-records/7620543101feadcebdc49427ad0395f1f4c8a0d304ec3a5251f1920d023b06c0.record.md new file mode 100644 index 0000000000..2e9450f621 --- /dev/null +++ b/docs/branch-review-records/7620543101feadcebdc49427ad0395f1f4c8a0d304ec3a5251f1920d023b06c0.record.md @@ -0,0 +1 @@ +| 2026-08-15 | 1976 | 8bb76ce085f212d47a0918b9242cb412e8a9a3f7 | review-and-fix | fixed phone install-sheet overlap on current-base head | pwa regression 1/1; lifecycle DOM 10/10; verify:phone-chrome 437/437 UI | diff --git a/docs/branch-review-records/a9f750bbfb5bbb2857e2cdbb97a2dc446b1851cec993f8a91dd526fcbc750d25.record.md b/docs/branch-review-records/a9f750bbfb5bbb2857e2cdbb97a2dc446b1851cec993f8a91dd526fcbc750d25.record.md new file mode 100644 index 0000000000..2bb54b8f69 --- /dev/null +++ b/docs/branch-review-records/a9f750bbfb5bbb2857e2cdbb97a2dc446b1851cec993f8a91dd526fcbc750d25.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | dd687f20c2143466b40d51ce2831542130964af4 | PWA install lifecycle: style-contract registry and current-base merge | Fixed PR-introduced unregistered PWA visual classes; stale install-sheet exemption removed; merged current base | git diff --check; ledger-inbox; outstanding-issues; branch-review-ledger; ledger-write-discipline; static PWA registry assertion; targeted Vitest unavailable (node_modules absent) | diff --git a/docs/branch-review-records/b3523668f8839bde68f1fae4de2e7e8275b5cabc1802d1b6511b30323adad6ad.record.md b/docs/branch-review-records/b3523668f8839bde68f1fae4de2e7e8275b5cabc1802d1b6511b30323adad6ad.record.md new file mode 100644 index 0000000000..83b79fb206 --- /dev/null +++ b/docs/branch-review-records/b3523668f8839bde68f1fae4de2e7e8275b5cabc1802d1b6511b30323adad6ad.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 80fd6c04afdfa30e92c1cdf2ea5ba9d3d32a02c6 | PR #1976 base sync | Merged main 6f7b7deefaf7e0cd062b748f18fc6ca8988093f6 into the reviewed PR head; merge tree was clean. | git merge-tree --write-tree exact-head main: clean; git diff --check; ledger guards. | diff --git a/docs/branch-review-records/bde56718d14d5138e7a29a72d16402b4445bd109c6f2bfcffd686c6e436d8c74.record.md b/docs/branch-review-records/bde56718d14d5138e7a29a72d16402b4445bd109c6f2bfcffd686c6e436d8c74.record.md new file mode 100644 index 0000000000..21f4011c7a --- /dev/null +++ b/docs/branch-review-records/bde56718d14d5138e7a29a72d16402b4445bd109c6f2bfcffd686c6e436d8c74.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 80eb8f6d1c332ee5c32a27da93eb026fb6d9b0d1 | PWA stylesheet design-token contract remediation | Replaced PR-introduced raw PWA motion, padding, radius, gap, and line-height declarations with scoped semantic roles and existing design tokens; preserved rendered values. | git diff --check; manual CSS-contract audit (no new raw guarded declarations); static gate unavailable locally: @typescript/typescript6 not installed | diff --git a/docs/branch-review-records/c56fc20eb8b758bec749726901ef79ce50012974b924d01bcc5b55b6dc00d1ad.record.md b/docs/branch-review-records/c56fc20eb8b758bec749726901ef79ce50012974b924d01bcc5b55b6dc00d1ad.record.md new file mode 100644 index 0000000000..5487a026f4 --- /dev/null +++ b/docs/branch-review-records/c56fc20eb8b758bec749726901ef79ce50012974b924d01bcc5b55b6dc00d1ad.record.md @@ -0,0 +1 @@ +| 2026-08-15 | 1976 | 81c44aee0b788a5aa93522a09092cddcd12b0bf0 | review-and-fix | CI blocker fixed: register compact native-install CSS selectors in the unlayered style inventory | style-contract registry 15/15; PWA DOM 10/10; PWA Chromium 5/5; Lighthouse exact-head green | diff --git a/docs/branch-review-records/cebd03715e584420f2fb63b757718fd8f04e9dfb00089261c37800f7d8edf694.record.md b/docs/branch-review-records/cebd03715e584420f2fb63b757718fd8f04e9dfb00089261c37800f7d8edf694.record.md new file mode 100644 index 0000000000..e86692ee4a --- /dev/null +++ b/docs/branch-review-records/cebd03715e584420f2fb63b757718fd8f04e9dfb00089261c37800f7d8edf694.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 80fd6c04afdfa30e92c1cdf2ea5ba9d3d32a02c6 | PWA phone install/composer overlap regression | Restored the shared phone composer reserve for mode-home install sheets after the exact-head production UI geometry test found a 390px overlap. | git diff --check 0dd4b7e9be293815ec3d06f3b57682a1578bafe4; node scripts/ledger-inbox.mjs check; node scripts/check-outstanding-issues.mjs; node scripts/ci-change-scope.mjs --self-test; exact-head production UI geometry failure reviewed | diff --git a/docs/branch-review-records/fe8542d735a4d0a26e76906a700a861925da55131c0caacbaab18ac81507cd0d.record.md b/docs/branch-review-records/fe8542d735a4d0a26e76906a700a861925da55131c0caacbaab18ac81507cd0d.record.md new file mode 100644 index 0000000000..2dfcd8bcfc --- /dev/null +++ b/docs/branch-review-records/fe8542d735a4d0a26e76906a700a861925da55131c0caacbaab18ac81507cd0d.record.md @@ -0,0 +1 @@ +| 2026-08-15 | codex/pwa-install-polish-20260815 | 233047f36f210f0e9ea4846183b2368ca67fa338 | Required base sync through main d301d8f4 | approved | git diff --check; CI scope self-test; ledger and issue guards | diff --git a/docs/pwa.md b/docs/pwa.md index 2516055477..b5188936c6 100644 --- a/docs/pwa.md +++ b/docs/pwa.md @@ -52,9 +52,13 @@ registered service worker. Localhost is the browser's secure-context development is shown only when the browser emits `beforeinstallprompt`; browsers that do not expose that event retain their own install/Add to Home Screen flow. -The install card is not shown in standalone mode. Choosing **Not now**, or dismissing the browser prompt, suppresses -the custom prompt for 30 days using `clinical-kb-pwa-install-dismissed-at` in localStorage. `appinstalled` clears that -value. Storage failures are treated as non-fatal progressive-enhancement failures. +The install card is not shown in standalone mode. It stays non-blocking and lower-right on wide screens; on phones it +becomes a compact sheet positioned clear of the current composer owner (above a bottom dock or below an in-flow home +composer) and the safe area. The benefit list is deliberately limited to quick access, app-like launch, and the +familiar Clinical KB workspace. It does not imply offline clinical access. +Choosing **Not now**, using **Dismiss**, or dismissing the browser prompt suppresses the custom prompt for 30 days +using `clinical-kb-pwa-install-dismissed-at` in localStorage. `appinstalled` clears that value. Storage failures are +treated as non-fatal progressive-enhancement failures. iOS and iPadOS never emit `beforeinstallprompt`, so outside standalone mode those platforms get a one-time manual hint instead (Safari: Share, then Add to Home Screen). **Not now** suppresses it for 30 days via @@ -155,11 +159,11 @@ clinical actions remain network-dependent. ### Update UX -- An already-waiting worker, or an installing worker that reaches `installed` while a controller exists, shows **An - update is ready**. +- An already-waiting worker, or an installing worker that reaches `installed` while a controller exists, shows + **Update available**. - When the page becomes visible or connectivity returns, the registration may check for an update. App-triggered checks are throttled to at most once per hour; there is no background polling timer. -- **Refresh now** sends `SKIP_WAITING`. The page reloads once after `controllerchange`, so it cannot loop. +- **Reload** sends `SKIP_WAITING`. The page reloads once after `controllerchange`, so it cannot loop. - **Later** hides the update for the current lifecycle instance. A later page load can surface the waiting update again. - If another tab activates the update, an already-controlled tab receives `controllerchange` and offers its own @@ -232,7 +236,7 @@ resource problem by weakening the page CSP or adding a provider origin to the wo ## Accessibility, theme, and device fit -- Lifecycle notices use a polite live region, labelled status/region containers, real buttons, 44 px minimum target +- Lifecycle notices use a polite live region, labelled status/region containers, real buttons, 48 px minimum target height, and visible focus treatment. - The notice stack does not take page scroll ownership. Its cards restore pointer events while the container remains transparent to unrelated interaction. @@ -299,7 +303,7 @@ explicitly authorized. document must appear and must not reveal prior content. Restore connectivity and verify the restored notice. - [ ] Confirm an offline API/document/media request is not answered from a PWA cache. - [ ] Deploy a worker with a new cache version in a staging environment. Verify the update waits, **Later** does not - force a refresh, **Refresh now** activates it, every older tab is offered a refresh, and only the two newest + force a refresh, **Reload** activates it, every older tab is offered a refresh, and only the two newest prior static caches remain for lazy-chunk compatibility. - [ ] Check keyboard focus, screen-reader announcements, forced colours, light/dark theme, portrait/landscape, display cutouts, and the standalone home-indicator area at phone and desktop breakpoints. diff --git a/src/app/globals.css b/src/app/globals.css index 1139e89937..2f704cc2f3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2618,46 +2618,287 @@ summary::-webkit-details-marker { padding-bottom: calc(0.5rem + env(safe-area-inset-bottom)); } -/* Global PWA notices sit above the bottom composer and home indicator without - changing page scroll ownership. The lifecycle component renders only when an - offline, restored, installable, or waiting-update state needs attention. */ -/* PWA notices: bottom sheet-style on phones (thumb zone, above the fixed - composer), floating bottom-right card on medium screens, and top-right - toast under the header on desktop — beside the browser's own install - affordance and clear of the composer. */ +/* Global PWA notices sit above the phone composer and home indicator without + taking page scroll ownership. Install is a compact surface, not a modal: + central search stays usable and the wide-screen card remains lower-right. */ .pwa-notice-stack { - --pwa-notice-bottom-gap: calc(max(0.75rem, var(--safe-area-bottom)) + 5.5rem); + --pwa-notice-bottom-gap: calc(max(0.75rem, var(--safe-area-bottom)) + 5rem); + /* PWA-only geometry and typography roles. Keep the component's exact + measured proportions while consuming the shared radius, spacing, and motion + ladders wherever a matching step exists. */ + --pwa-notice-gap: var(--radius-lg); + --pwa-card-radius: var(--radius-xl); + --pwa-control-radius: var(--radius-md); + --pwa-inset-xs: var(--radius-xs); + --pwa-inset-sm: var(--radius-sm); + --pwa-inset-md: var(--radius-md); + --pwa-inset-lg: var(--radius-lg); + --pwa-inset-xl: var(--spacing-icon-md); + --pwa-grip-height: calc(var(--spacing-icon-sm) + var(--radius-xs)); + --pwa-grip-width: calc(var(--spacing-icon-xl) + var(--radius-lg)); + --pwa-grip-thickness: 0.1875rem; + --pwa-header-min-height: calc(4 * var(--radius-xl)); + --pwa-row-min-height: calc(2 * var(--radius-xl) + 2 * var(--radius-xs)); + --pwa-row-padding-block: 0.4375rem; + --pwa-line-height-control: 1; + --pwa-line-height-tagline: 1.35rem; + --pwa-line-height-copy: var(--spacing-icon-lg); + --pwa-line-height-support: var(--pwa-grip-height); + --pwa-motion-enter: 280ms; position: fixed; z-index: 95; - left: max(0.75rem, var(--safe-area-left)); - right: max(0.75rem, var(--safe-area-right)); + left: max(0.5rem, var(--safe-area-left)); + right: max(0.5rem, var(--safe-area-right)); bottom: var(--pwa-notice-bottom-gap); display: grid; - gap: 0.75rem; + gap: var(--pwa-notice-gap); pointer-events: none; } -/* Install notices present as a native-feeling bottom sheet on phones: full - bleed to the viewport edges (escaping the stack gutter), flush to the - bottom over the composer zone while visible, top corners only, and content - padded clear of the home indicator. Matches the drag-grip language of the - phone Mode-menu sheet (src/components/ui/sheet.tsx). */ +.pwa-notice-card { + overflow: hidden; + border: 1px solid var(--border-lux); + border-radius: var(--pwa-card-radius); + background: var(--surface-lux); + box-shadow: + 0 18px 48px color-mix(in srgb, var(--text-heading) 11%, transparent), + 0 3px 10px color-mix(in srgb, var(--text-heading) 6%, transparent); + animation: pwa-notice-in var(--pwa-motion-enter) cubic-bezier(0.2, 0.9, 0.3, 1) both; +} + +.pwa-lifecycle-card { + padding: var(--pwa-inset-xl); +} + +.pwa-connection-restored { + justify-self: end; + border-color: color-mix(in srgb, var(--success) 24%, var(--border-lux)); + border-radius: var(--radius-pill, 9999px); + background: color-mix(in srgb, var(--success-soft) 88%, var(--surface-lux)); + padding: var(--pwa-inset-sm) var(--pwa-inset-lg) var(--pwa-inset-sm) calc(2 * var(--pwa-inset-xs)); + box-shadow: var(--e1); +} + +.pwa-connection-restored > div > span { + width: calc(2 * var(--pwa-inset-xl)); + height: calc(2 * var(--pwa-inset-xl)); + border-radius: var(--radius-pill, 9999px); +} + +.pwa-install-sheet { + max-height: calc(100dvh - var(--pwa-notice-bottom-gap) - 4.25rem); + overflow-y: auto; + overscroll-behavior: contain; +} + +.pwa-install-grip { + display: flex; + height: var(--pwa-grip-height); + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--clinical-accent-soft) 72%, var(--surface-lux)); +} + +.pwa-install-grip span { + width: var(--pwa-grip-width); + height: var(--pwa-grip-thickness); + border-radius: var(--radius-pill, 9999px); + background: color-mix(in srgb, var(--text-muted) 48%, transparent); +} + +.pwa-install-header { + display: flex; + min-height: var(--pwa-header-min-height); + align-items: center; + justify-content: space-between; + gap: var(--pwa-notice-gap); + border-bottom: 1px solid color-mix(in srgb, var(--border-lux) 82%, transparent); + background: color-mix(in srgb, var(--clinical-accent-soft) 72%, var(--surface-lux)); + padding: calc(2 * var(--pwa-inset-xs)) var(--pwa-inset-md) calc(2 * var(--pwa-inset-xs)) var(--pwa-inset-xl); +} + +.pwa-install-mark { + border-radius: var(--pwa-control-radius); + filter: drop-shadow(0 2px 3px color-mix(in srgb, var(--clinical-accent) 18%, transparent)); +} + +.pwa-install-dismiss { + display: inline-flex; + min-height: var(--spacing-tap); + flex: none; + align-items: center; + justify-content: center; + gap: var(--pwa-inset-xs); + border-radius: var(--pwa-control-radius); + padding: 0 calc(2 * var(--pwa-inset-xs)); + color: var(--text-muted); + font-size: 0.6875rem; + font-weight: 600; + line-height: var(--pwa-line-height-control); + transition: + color var(--duration-quick) ease, + background-color var(--duration-quick) ease; +} + +.pwa-install-dismiss:hover { + background: color-mix(in srgb, var(--surface-lux) 72%, transparent); + color: var(--text-heading); +} + +.pwa-install-dismiss:focus-visible { + outline: 2px solid var(--focus); + outline-offset: -2px; +} + +.pwa-install-body { + background: var(--surface-lux); + padding: var(--spacing-icon-sm) var(--pwa-inset-xl) var(--pwa-inset-xl); +} + +.pwa-install-compact-copy { + display: none; +} + +.pwa-install-tagline { + color: var(--text-heading); + font-size: 0.875rem; + font-weight: 700; + line-height: var(--pwa-line-height-tagline); +} + +.pwa-install-copy { + margin-top: var(--pwa-inset-sm); + color: var(--text-muted); + font-size: 0.8125rem; + line-height: var(--pwa-line-height-copy); +} + +.pwa-install-support { + margin-top: var(--pwa-inset-sm); + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 600; + line-height: var(--pwa-line-height-support); +} + +.pwa-install-benefits, +.pwa-install-steps { + margin-top: var(--pwa-inset-lg); + overflow: hidden; + border: 1px solid var(--border-lux); + border-radius: var(--pwa-control-radius); + background: color-mix(in srgb, var(--clinical-accent-soft) 20%, var(--surface-lux)); +} + +.pwa-install-benefits li, +.pwa-install-steps li { + display: flex; + min-height: var(--pwa-row-min-height); + align-items: center; + gap: var(--pwa-inset-md); + padding: var(--pwa-row-padding-block) var(--pwa-inset-lg); + color: var(--text); + font-size: 0.75rem; + font-weight: 600; + line-height: var(--pwa-line-height-support); +} + +.pwa-install-benefits li + li, +.pwa-install-steps li + li { + border-top: 1px solid color-mix(in srgb, var(--border-lux) 80%, transparent); +} + +.pwa-install-benefits svg, +.pwa-install-steps svg { + flex: none; + color: var(--clinical-accent); +} + +.pwa-install-steps li { + min-height: var(--spacing-tap); + justify-content: space-between; + font-size: 0.8125rem; +} + +.pwa-install-actions { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); + gap: calc(2 * var(--pwa-inset-xs)); + margin-top: var(--pwa-inset-lg); +} + +.pwa-install-actions-single { + grid-template-columns: 1fr; +} + +/* The in-flow phone home composer sits near the viewport centre. Keep the + browser-native install prompt below it without moving page content: the full + value proposition remains on larger and dock-owned surfaces, while the phone + hero gets a concise, action-first card. */ @media (max-width: 639.98px) { - .pwa-install-sheet { - margin-left: calc(-1 * max(0.75rem, var(--safe-area-left))); - margin-right: calc(-1 * max(0.75rem, var(--safe-area-right))); - margin-bottom: calc(-1 * var(--pwa-notice-bottom-gap)); - border-radius: 1.25rem 1.25rem 0 0; - border-bottom: 0; - padding-left: max(1.25rem, var(--safe-area-left)); - padding-right: max(1.25rem, var(--safe-area-right)); - padding-bottom: calc(1.25rem + var(--safe-area-bottom)); + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-notice-stack { + --pwa-notice-bottom-gap: max(0.5rem, var(--safe-area-bottom)); + } + + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-grip, + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-tagline, + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-copy, + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-support, + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-benefits { + display: none; + } + + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-header { + min-height: 3.25rem; + padding-block: calc(2 * var(--pwa-inset-xs)); + } + + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-body { + padding: var(--pwa-inset-sm) var(--pwa-inset-xl) var(--pwa-inset-xl); + } + + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-compact-copy { + display: block; + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 600; + line-height: var(--pwa-line-height-support); + } + + body:has(#main-content[data-phone-footer-owner="hero"]):has(.pwa-install-native-sheet) .pwa-install-actions { + margin-top: var(--pwa-inset-sm); } } -.pwa-notice-card { - animation: pwa-notice-in 280ms cubic-bezier(0.2, 0.9, 0.3, 1) both; +.pwa-action { + transition: + border-color var(--duration-quick) ease, + background-color var(--duration-quick) ease, + color var(--duration-quick) ease, + box-shadow var(--duration-quick) ease; +} + +.pwa-action-primary { + border: 1px solid var(--clinical-accent); + background: var(--clinical-accent); + color: var(--clinical-accent-contrast); + box-shadow: var(--e1); +} + +.pwa-action-primary:hover { + border-color: var(--clinical-accent-hover); + background: var(--clinical-accent-hover); +} + +.pwa-action-secondary { + border: 1px solid color-mix(in srgb, var(--clinical-accent) 58%, var(--border-lux)); + background: var(--surface-lux); + color: var(--clinical-accent); +} + +.pwa-action-secondary:hover { + background: var(--clinical-accent-soft); } @keyframes pwa-notice-in { @@ -2674,32 +2915,33 @@ summary::-webkit-details-marker { @media (min-width: 640px) { .pwa-notice-stack { left: auto; - width: min(25rem, calc(100vw - 2rem - var(--safe-area-left) - var(--safe-area-right))); + right: max(1rem, var(--safe-area-right)); + bottom: max(1rem, var(--safe-area-bottom)); + width: min(27rem, calc(100vw - 2rem - var(--safe-area-left) - var(--safe-area-right))); + } + + /* Submitted answer views own a fixed bottom composer. Keep the install + surface in the opposite corner so neither control can obscure the other. */ + body:has(form.answer-footer-search-edge) .pwa-notice-stack { + top: calc(4.25rem + max(0.75rem, var(--safe-area-top))); + bottom: auto; + } + + .pwa-install-grip { + display: none; } } @media (display-mode: standalone) { .pwa-notice-stack { - --pwa-notice-bottom-gap: calc(max(0.75rem, var(--safe-area-bottom)) + 6rem); + --pwa-notice-bottom-gap: calc(max(0.75rem, var(--safe-area-bottom)) + 5.5rem); } } -/* Last so it also wins over the standalone bottom offset at desktop width. */ @media (min-width: 1280px) { .pwa-notice-stack { - top: calc(4.25rem + max(0.5rem, var(--safe-area-top))); - bottom: auto; - } - - @keyframes pwa-notice-in { - from { - opacity: 0; - transform: translateY(-0.625rem) scale(0.98); - } - to { - opacity: 1; - transform: none; - } + right: max(1.5rem, var(--safe-area-right)); + bottom: max(1.5rem, var(--safe-area-bottom)); } } @@ -2713,6 +2955,24 @@ html[data-motion="reduced"] .pwa-notice-card { animation: none; } +@media (forced-colors: active) { + .pwa-notice-card, + .pwa-install-benefits, + .pwa-install-steps, + .pwa-action, + .pwa-install-dismiss { + border-color: CanvasText; + } + + .pwa-install-header, + .pwa-install-body, + .pwa-install-benefits, + .pwa-install-steps, + .pwa-action-secondary { + background: Canvas; + } +} + /* Tabular figures for data, counts, page numbers, byte sizes */ @utility nums { font-variant-numeric: tabular-nums; diff --git a/src/components/pwa-lifecycle.tsx b/src/components/pwa-lifecycle.tsx index 75eada15c3..0b6db9bc95 100644 --- a/src/components/pwa-lifecycle.tsx +++ b/src/components/pwa-lifecycle.tsx @@ -1,7 +1,20 @@ "use client"; -import { RefreshCw, Share, SquarePlus, Wifi, WifiOff, X, type LucideIcon } from "lucide-react"; +import { + Download, + LayoutGrid, + RefreshCw, + Share, + Smartphone, + SquarePlus, + Wifi, + WifiOff, + X, + Zap, + type LucideIcon, +} from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { BrandMark } from "@/components/clinical-dashboard/brand"; import { createBrowserStore } from "@/lib/client-store-factory"; const SERVICE_WORKER_URL = "/sw.js"; @@ -100,12 +113,11 @@ async function teardownLocalPwa() { } } -const cardClassName = - "pwa-notice-card pointer-events-auto relative rounded-2xl border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-4 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-[color:var(--ring-glass)] backdrop-blur-md"; +const cardClassName = "pwa-notice-card pointer-events-auto relative text-[color:var(--text)]"; const primaryButtonClassName = - "inline-flex min-h-tap items-center justify-center rounded-lg bg-[color:var(--clinical-accent)] px-3.5 py-2 text-sm font-semibold text-[color:var(--clinical-accent-contrast)] shadow-[var(--e1)] transition-colors hover:bg-[color:var(--clinical-accent-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + "pwa-action pwa-action-primary inline-flex min-h-tap items-center justify-center rounded-md px-4 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; const secondaryButtonClassName = - "inline-flex min-h-tap items-center justify-center rounded-lg border border-[color:var(--border-lux)] px-3.5 py-2 text-sm font-semibold text-[color:var(--text)] transition-colors hover:bg-[color:var(--surface-subtle)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + "pwa-action pwa-action-secondary inline-flex min-h-tap items-center justify-center rounded-md px-4 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; const dismissIconButtonClassName = "absolute right-1 top-1 inline-flex h-tap w-tap items-center justify-center rounded-full text-[color:var(--text-muted)] transition-colors hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; @@ -130,47 +142,75 @@ function NoticeIcon({ icon: Icon, tone }: { icon: LucideIcon; tone: "accent" | " ); } -// Phone install notices read as a native install sheet: grip bar (visual -// echo of src/components/ui/sheet.tsx; static — notices are transient cards, -// not draggable dialogs) and the real app icon as the identity mark. +// Phone install notices echo the application's sheet language without acting +// like a draggable modal. The grip is deliberately decorative and the notice +// remains a non-blocking region. function InstallSheetGrip() { return ( -