Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -1106,3 +1106,7 @@ This file is append-only. Never rewrite or delete an existing review record; app

| 2026-07-26 | `codex/phone-footer-glass` | `7e4fd1a23` | Review-follow-up and CI hydration-race review | APPROVE. Scoped the expanded collapse runway only to combined in-flow header plus reserve owners, cleared the calculator dock focus latch after sheet teardown, and made mode-home UI assertions wait for one settled owner during production hydration. Both automated review threads were addressed and resolved. No P0-P3 findings remain; physical iOS/WebKit compositing remains the only material unverified surface. | `verify:cheap` PASS; focused scroll-hide/static contracts 25/25 PASS; focused calculator teardown/geometry Chromium 3/3 PASS; affected mode-home production Chromium 5/5 PASS; no provider-backed checks. |
| 2026-07-27 | `codex/phone-footer-paint` | `e1657a4a4e3e25b4a35a1dcf544b3efddb123c3f` | Protected-main release-readiness review of phone footer safe-area paint | APPROVE. The prior footer remained visually opaque because its normal/fallback terminal tints reached 72-90%, all blur masks painted through the physical edge, and hidden WebKit paint had no explicit terminal visibility state. The shared dock now has bounded localized tint, transparent gradient/mask terminals, an 88% pill, and delayed `visibility: hidden` synchronized to the 240ms exit; reduced-motion hides immediately. Therapy and calculator ownership prove the standard shared behavior. No P0-P3 finding remains. Highest residual risk is physical iOS momentum/compositor behavior beyond local WebKit emulation. | `verify:cheap` PASS (393 files; 3518 passed / 2 skipped); focused Therapy Chromium + WebKit PASS; calculator shared-dock Chromium + WebKit PASS; `verify:ui` PASS (314/314); `verify:pr-local` PASS including production build/client-secret scan and 36-case offline RAG fixtures; no live provider-backed checks. |
| 2026-07-27 | `codex/phone-bottom-band-root-20260727` | `a8efe4a08f00a2365e2035f83ce2128ec680576f` | Protected-main release-readiness review of the shared phone viewport shell | APPROVE. The remaining bottom band clipped live result content above the hidden dock because both phone application owners used viewport-sized fixed roots, a physical-iOS paint path that can disagree with correct DOM geometry. Both owners now share a bounded in-flow dynamic-viewport shell; hidden reserve remains zero, the last viewport pixel remains content-owned, and viewport resize preserves the reading offset. No P0-P3 finding remains. Highest residual risk is physical-device iOS compositing beyond desktop WebKit emulation. | `verify:cheap` PASS (393 files; 3519 passed / 2 skipped); focused Therapy and dashboard production WebKit PASS; `verify:ui` PASS (314/314); `verify:pr-local` PASS including production build/client-secret scan and 36-case offline RAG fixtures; no live provider-backed checks. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| 2026-07-27 | `codex/phone-bottom-band-root-20260727` | `a8a72a43d` | CI follow-up review of calculator dock hide lifecycle | APPROVE. Hosted production Chromium exposed a fast-close race where effect cleanup could cancel the queued focus-latch reset, plus a paint-contract journey coupled to natural short-page geometry. The reset now survives rapid sheet teardown, actual input focus is asserted before hide, and explicit runway isolates the paint contract from the anti-clamp boundary tests. No P0-P3 finding remains. | Exact locked Next 16.2.11 / Playwright 1.61.1 production Chromium repeat PASS (20/20); `verify:cheap` PASS (393 files; 3519 passed / 2 skipped); no provider-backed checks. |
| 2026-07-27 | `codex/phone-bottom-band-root-20260727` | `cc63ee90928d402eb9c5dd241e411ed88cc01f27` | Hosted Applications search hydration follow-up review | APPROVE. The final hosted Chromium failure was a test synchronization race: server-rendered content was visible before the controlled global-search input had its React onChange handler, so hydration could discard an immediate fill. Both shared-search tests now wait for the React handler and assert the accepted value before checking filtering. No product behavior changed and no P0-P3 finding remains. | Exact locked Next 16.2.11 / Playwright 1.61.1 production Chromium repeat PASS (20/20); `verify:cheap` PASS (25 gates; 393 files; 3519 passed / 2 skipped); no non-GitHub provider-backed checks. |
| 2026-07-27 | `codex/phone-bottom-band-root-20260727` | `a4802b9373404a00549a3479d86340398e978cc8` | Automated review follow-up for phone viewport fallback layering | APPROVE. Verified the review finding and separated the baseline 100vh declarations from the supported 100svh override, while retaining the later 100dvh override as the preferred dynamic viewport size. This removes duplicate properties without changing the intended fallback order. The ledger date remains the Australia/Perth task completion date. No P0-P3 finding remains. | Focused viewport-shell static contract PASS (8/8); `git diff --check` PASS; prior full `verify:cheap` and hosted required CI were green before this CSS-only declaration-layering follow-up; no non-GitHub provider-backed checks. |
2 changes: 1 addition & 1 deletion docs/search-chrome-behaviour.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Rules that keep this working:

- **Hide the top bar, not the search field.** The collapse wrapper (`data-testid="universal-header-collapse"`) wraps `header#search` plus page navigation mounted through `PhoneHeaderCollapsePortal` into `#phone-header-collapse-addon-slot`. Keep composers outside the collapse row: tablet search stays pinned independently, and desktop search scrolls with page content rather than being translated by the header.
- **Every production phone navigation header has one collapse owner.** `PhoneHeaderCollapsePortal` moves Therapy section navigation, DocumentViewer navigation, and Differential detail navigation into `#phone-header-collapse-addon-slot` below `sm`; the same subtree stays in its existing page position at `sm+`. Do not add a second sticky/fixed phone header inside `#main-content`: the universal collapse row must own its safe area, focus pinning, timing, clipping, and measured release. Semantic content headings and modal/sheet headers are not viewport chrome and stay in their own flow/scroll context.
- **Feed the reporter from the element that actually scrolls.** `GlobalSearchShell`'s `#main-content` is the scrollport only on phones, so above that it also runs `useDocumentScrollHideReporter`. That hook self-gates: the phone shell is `fixed inset-0`, so the document cannot scroll and never fires.
- **Feed the reporter from the element that actually scrolls.** `GlobalSearchShell`'s `#main-content` is the scrollport only on phones, so above that it also runs `useDocumentScrollHideReporter`. That hook self-gates: the phone shell is a bounded, overflow-clipped `.phone-viewport-shell`, so the document cannot scroll and never fires. Keep that shell in normal flow; viewport-sized fixed roots trigger a physical-iOS bottom-gap compositor bug even when DOM geometry is correct.
- **Tablet stickiness belongs on the outer [top bar \| search] stack, not on `header#search`.** The top bar sits inside header-height boxes, which leaves a sticky rule on it zero travel. For the same reason the stack's ancestor in `GlobalSearchShell` is `display: contents` above the phone breakpoint rather than a block, and collapse returns a fragment (safe-area spacer + stack) rather than a single root box. At desktop widths the search portal leaves that same outer stack holding only the top bar.
- **Collapse only the top-bar row inside a sticky stack.** On tablets, translating the whole stack would take the search field off-screen; collapsing just the top bar lets search stay pinned at the viewport top below the wide-layout safe-area spacer. On desktop, the page-flow search is outside the stack entirely.
- **Release the phone top inset with hidden chrome.** `chrome-safe-area-top` is a full-width sibling that is `h-[var(--safe-area-top)]` while the phone header is visible and `h-0` while hidden, using the same transition timing as the top-bar row. `readChromeCollapseMetrics` must charge that released phone height as well as the controls and dock reserve, or short pages clamp and oscillate at the bottom. At `sm+` the spacer remains `h-[var(--safe-area-top)]`, and sticky chrome pins at `top: var(--safe-area-top)`. Do not leave a phone-only surface/status-bar band after the controls collapse.
Expand Down
42 changes: 32 additions & 10 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2331,6 +2331,31 @@ html[data-motion="reduced"] .pwa-notice-card {
height: 100svh;
}

/*
* Phone search surfaces keep #main-content as their only scrollport, but the
* viewport shell itself must remain in normal flow. iOS can leave a painted
* gap under viewport-sized position:fixed containers even when CSSOM reports
* the correct bounds. A bounded dynamic-viewport block avoids that compositor
* path while still giving the flex scroller an exact height.
*/
@media (max-width: 639px) {
.phone-viewport-shell {
position: relative;
width: 100%;
min-height: 100vh;
height: 100vh;
}
}

@supports (height: 100svh) {
@media (max-width: 639px) {
.phone-viewport-shell {
min-height: 100svh;
height: 100svh;
}
}
}

.mobile-popover-scroll {
max-height: min(70svh, 28rem);
}
Expand All @@ -2341,18 +2366,15 @@ html[data-motion="reduced"] .pwa-notice-card {
height: 100dvh;
}

.mobile-popover-scroll {
max-height: min(70dvh, 28rem);
@media (max-width: 639px) {
.phone-viewport-shell {
min-height: 100dvh;
height: 100dvh;
}
}
}

/* Phones: ClinicalDashboard pins with fixed inset-0 utilities. Do not keep a
dvh height clamp here — it recreates the Safari toolbar dead band under
the shell even when inset-0 is present if height still resolves to dvh. */
@media (max-width: 639px) {
.mobile-app-shell {
min-height: 0;
height: auto;
.mobile-popover-scroll {
max-height: min(70dvh, 28rem);
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3314,8 +3314,9 @@ export function ClinicalDashboard({
<div
className={cn(
appBackdrop,
// Phone: fixed inset-0 (not 100dvh) — matches GlobalSearchShell; avoids Safari toolbar dead band.
"mobile-app-shell flex flex-col overflow-hidden text-[color:var(--text)] max-sm:fixed max-sm:inset-0 max-sm:h-auto max-sm:min-h-0 max-sm:overflow-hidden md:grid md:grid-cols-[5.25rem_minmax(0,1fr)] md:overflow-hidden",
// Share the same in-flow phone viewport contract as GlobalSearchShell.
// Avoiding a viewport-sized fixed root prevents iOS compositor gaps.
"mobile-app-shell phone-viewport-shell flex flex-col overflow-hidden text-[color:var(--text)] max-sm:overflow-hidden md:grid md:grid-cols-[5.25rem_minmax(0,1fr)] md:overflow-hidden",
sidebarColumnTransitionReady &&
"motion-safe:transition-[grid-template-columns] motion-safe:duration-200 motion-safe:ease-out",
sidebarCollapsed ? "lg:grid-cols-[5.25rem_minmax(0,1fr)]" : "lg:grid-cols-[20rem_minmax(0,1fr)]",
Expand Down
12 changes: 5 additions & 7 deletions src/components/calculators/search-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,17 +520,15 @@ export function CalculatorsSearchPage() {
const dockHidden = footerHidden && !dockFocused;
const reserveTransitioning = useReserveTransitionMarker(dockHidden, activeCalc);
useEffect(() => {
if (!activeCalc) return undefined;
let cancelled = false;
if (!activeCalc) return;
// Submitting a focused dock input unmounts the dock before React is
// guaranteed to dispatch blur. Clear the latch after teardown so the dock
// can resume hide-on-scroll when the calculator sheet closes.
// can resume hide-on-scroll when the calculator sheet closes. This reset
// must survive a fast close: cancelling the microtask during effect cleanup
// can otherwise leave the remounted dock permanently focus-pinned.
queueMicrotask(() => {
if (!cancelled) setDockFocused(false);
setDockFocused(false);
});
return () => {
cancelled = true;
};
}, [activeCalc]);

const compact = density === "compact";
Expand Down
11 changes: 5 additions & 6 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,11 @@ function GlobalStandaloneSearchShellBody({
return (
<div
className={cn(
// Phone shell height comes from inset-0 alone, never 100dvh: iOS Safari
// re-resolves dvh lazily when its toolbar collapses/expands (especially
// with body scrolling disabled like here), leaving a dead band between
// the clipped shell and the toolbar. Fixed insets track the live
// viewport through the whole transition, so content stays edge to edge.
"sm:min-h-dvh max-sm:fixed max-sm:inset-0 max-sm:overflow-hidden bg-[color:var(--background)] text-[color:var(--text)]",
// Keep the phone shell viewport-bounded without making the whole app a
// viewport-sized fixed layer. Physical iOS can paint a bottom gap under
// fixed inset shells while reporting correct geometry; the shared
// in-flow contract still bounds #main-content for internal scrolling.
"phone-viewport-shell sm:min-h-dvh max-sm:overflow-hidden bg-[color:var(--background)] text-[color:var(--text)]",
shouldShowDesktopSidebar && "md:grid md:grid-cols-[5.25rem_minmax(0,1fr)]",
shouldShowDesktopSidebar &&
sidebarColumnTransitionReady &&
Expand Down
3 changes: 2 additions & 1 deletion src/components/clinical-dashboard/use-hide-on-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,8 @@ export function useScrollHideReporter(disabled = false, allowAllBreakpoints = fa
* page scrolls the document above the phone breakpoint — GlobalSearchShell,
* where `#main-content` is the scrollport only on phones and its `onScroll`
* therefore never fires on tablet/desktop. Self-gating: while the document
* cannot scroll (the phone shell is `fixed inset-0`) no scroll event arrives,
* cannot scroll (the phone shell is viewport-bounded and overflow-clipped) no
* scroll event arrives,
* so the internal scroller stays the single source at that width.
*/
export function useDocumentScrollHideReporter(reportScroll: (metrics: ScrollMetrics) => void) {
Expand Down
10 changes: 10 additions & 0 deletions tests/clinical-dashboard-merge-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ describe("ClinicalDashboard merge-artifact guards", () => {
expect(globalStylesSource).not.toMatch(/^\s*-webkit-backdrop-filter\s*:/m);
});

it("keeps shared phone shells out of the iOS fixed-viewport compositor path", () => {
expect(globalSearchShellSource).toContain('"phone-viewport-shell sm:min-h-dvh');
expect(clinicalDashboardSource).toContain('"mobile-app-shell phone-viewport-shell flex');
expect(globalSearchShellSource).not.toContain("max-sm:fixed max-sm:inset-0");
expect(clinicalDashboardSource).not.toContain("max-sm:fixed max-sm:inset-0");
expect(globalStylesSource).toContain(".phone-viewport-shell {");
expect(globalStylesSource).toContain("position: relative;");
expect(globalStylesSource).toContain("height: 100dvh;");
});

it("releases the Safari toolbar reserve only after phone composers hide", () => {
expect(mobileComposerReserveSource).toContain('export const mobileComposerHiddenReserve = "0rem"');
expect(mobileComposerReserveSource).toContain(
Expand Down
4 changes: 3 additions & 1 deletion tests/mobile-chrome-paint-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ describe("mobile chrome paint baseline", () => {

it("registers the calculator page reserve with the shared hide and transition contracts", () => {
expect(calculatorsSource).toContain("useReserveTransitionMarker(dockHidden, activeCalc)");
expect(calculatorsSource).toContain("if (!activeCalc) return undefined");
expect(calculatorsSource).toContain("if (!activeCalc) return;");
expect(calculatorsSource).toContain("queueMicrotask(() =>");
expect(calculatorsSource).toContain("setDockFocused(false)");
expect(calculatorsSource).not.toContain("if (!cancelled) setDockFocused(false)");
expect(calculatorsSource).toContain('reserveOwner="calculator"');
expect(calculatorsSource).toContain('reserveHiddenPad="0rem"');
expect(calculatorsSource).toContain("reserveTransitioning={reserveTransitioning}");
Expand Down
Loading
Loading