Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -636,3 +636,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | efe472fac3489473e2465da2d92980508c20fd81 | merge main (#934 favourites) + #941 sync | Merged favourites guest-gate (#934) then Specifiers results UI (#941). Product conflicts none: kept `contentAlign="startOnPhone"` on Specifiers home while taking #941 results chrome. `ui-overlap` gotoHome wait conflict resolved with settled single-header short-circuit. No new P0–P2. Residual: human approving review required for squash auto-merge. | Local: align Vitest; favourites+app-modes+tools 42 tests; Chromium ui-overlap+ui-tools 95/95; verify:cheap unit stage green. Hosted CI green on pre-#941 tip; re-running after this merge. |
| 2026-07-20 | cursor/documents-search-header-3eab / PR #936 | bdc333a9fa7e420e2beb2f146c6f271196869cb5 (squash on main) | post-merge closeout + branch-cleanup | Squash-merged to main. Product proof on main: `DocumentResultsControls`, identity-first documents results chrome, governance notice under controls, Also-in-library strip removed from documents results path. Remote feature ref already deleted by protected-main workflow; local tip `fd559a07` retained only merge/review commits with no unique product patch vs main. | Hosted pre-merge and post-merge required checks green (Static/Unit/Build/Production UI/PR required). Squash content proof via main tree symbols; remote `ls-remote` empty after prune; local branch deleted after this ledger row. No OpenAI/Supabase provider calls. |
| 2026-07-20 | cursor/fix-ui-overlap-duplicate-header-3eab / PR #944 | 08181b8b45cf23d53ff7fb8682fb5f83f4417fb0 (squash on main) | post-merge closeout + branch-cleanup | Squash-merged to main. Product proof: `gotoHome` pins `/?mode=answer` and waits for exactly one `header#search` before overlap measurement, closing the landing-preference dual-banner flake that failed Production UI then PR required on #936. Remote feature ref already deleted; local tip `418de1b9` is merge-only after squash. | Local before merge: verify:pr-local (2961 unit) + Chromium ui-overlap 12/12. Hosted exact-head and post-merge: Production UI, PR required, Static, Unit green (Build skipped for test-only). No OpenAI/Supabase provider calls. |
| 2026-07-19 | cursor/mobile-header-new-chat-inset-66c0 (PR #940) | 84eb0b6c3782e27fbbd1ec79b87b10327802ec1e | final mobile header new-chat edge inset review + merge readiness | No high-confidence P0-P1. Root cause: unlayered `@media (max-width:639px)` zeroed `.edge-glass-header` padding and beat `@layer components`. Fixed with tokenized `--header-edge-pad: 1rem` shared by layered base + unlayered phone guard; Playwright symmetry checks at 360/390; source contract blocks a `max(0px, safe-area)` regression. Merged latest `origin/main` (including #933/#942/#943) while keeping the header-edge-pad token. Residual: headless Chromium cannot exercise asymmetric safe-area `max()`; DocumentViewer gains the same pad but is outside the symmetry test. | Local geometry probe 360/390/640 = 16px/16px symmetric; CSS contract Vitest 5/5; `ui-overlap` Chromium 14/14; prior `verify:ui` 242/242 on the functional head; `verify:cheap` unit suite hit only the known container-only `pdf-extraction-budget` python ENOENT artifact (also fails on clean main / hosted-CI-green elsewhere). PR marked ready; squash auto-merge enabled. No OpenAI/live Supabase/provider calls. |
32 changes: 19 additions & 13 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -267,6 +267,10 @@
--safe-area-right: env(safe-area-inset-right, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
/* Phone/sm header chrome inset; matches mode-home px-4. Referenced by both
the layered .edge-glass-header base and the unlayered max-width:639px
guard so the values cannot drift apart. lg overrides to 1.5rem. */
--header-edge-pad: 1rem;
/* Keep in sync with mobile-composer-reserve.ts (phone dock clearance). */
--phone-dock-hidden-pad: 0.75rem;
--phone-dock-differentials-compare-clearance: 12.5rem;
Expand DownExpand Up@@ -556,8 +560,12 @@ summary::-webkit-details-marker {
@layer components {
.edge-glass-header {
isolation: isolate;
padding-left: max(0.75rem, var(--safe-area-left));
padding-right: max(0.75rem, var(--safe-area-right));
/* Uses --header-edge-pad (1rem) so phone icons align with mode-home px-4.
Keep safe-area max() so notched devices never under-inset. The unlayered
max-width:639px block below must also use this token — unlayered rules
beat @layer components, so a literal there would reintroduce drift. */
padding-left: max(var(--header-edge-pad), var(--safe-area-left));
padding-right: max(var(--header-edge-pad), var(--safe-area-right));
/* Translucent glass bar: the header's backdrop-blur utilities frost the
content scrolling beneath it (they were inert over the old opaque
var(--surface)). The .edge-glass-header-backdrop scrim supersedes the
Expand DownExpand Up@@ -596,15 +604,9 @@ summary::-webkit-details-marker {
box-shadow: none;
}

/* Responsive header padding (moved in from the shared @media blocks below so
the whole .edge-glass-header class is layered together). */
@media (min-width: 640px) {
.edge-glass-header {
padding-left: max(1rem, var(--safe-area-left));
padding-right: max(1rem, var(--safe-area-right));
}
}

/* Desktop header padding (moved in from the shared @media blocks below so
the whole .edge-glass-header class is layered together). Base/sm share
--header-edge-pad. */
@media (min-width: 1024px) {
.edge-glass-header {
padding-left: max(1.5rem, var(--safe-area-left));
Expand DownExpand Up@@ -1771,9 +1773,13 @@ summary::-webkit-details-marker {
}

@media (max-width: 639px) {
/* Unlayered on purpose: a prior max(0px, safe-area) here beat the layered
inset and pinned new-chat to the phone bezel. Keep the tokenized phone
value so unlayered media cannot reintroduce a 0px override or drift from
the @layer components base. */
.edge-glass-header {
padding-left: max(0px, var(--safe-area-left));
padding-right: max(0px, var(--safe-area-right));
padding-left: max(var(--header-edge-pad), var(--safe-area-left));
padding-right: max(var(--header-edge-pad), var(--safe-area-right));
}

.dashboard-composer-edge.answer-footer-search-edge:not(.answer-footer-search-dock) {
Expand Down
33 changes: 33 additions & 0 deletions tests/ui-overlap.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,39 @@ test.describe("Header element overlap coverage", () => {
});
}

for (const viewport of [
{ name: "narrow-phone", width: 360, height: 780 },
{ name: "phone", width: 390, height: 820 },
] as const) {
test(`header menu and new-chat insets stay symmetric on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await mockDemoDashboard(page);
await gotoHome(page);

const menu = page.getByRole("button", { name: "Open Clinical Guide menu" });
const newChat = page.getByRole("button", { name: "Start a new chat" });
await expect(menu).toBeVisible();
await expect(newChat).toBeVisible();

// Headless Chromium reports env(safe-area-inset-*) as 0, so this asserts
// the --header-edge-pad (1rem) chrome inset — not notch asymmetry.
const menuBox = await menu.boundingBox();
const newChatBox = await newChat.boundingBox();
expect(menuBox, "menu control must have geometry").not.toBeNull();
expect(newChatBox, "new-chat control must have geometry").not.toBeNull();

const leftInset = menuBox!.x;
const rightInset = viewport.width - (newChatBox!.x + newChatBox!.width);
// 1rem header pad (~16px) with 2px subpixel tolerance.
expect(leftInset, "left menu inset should be at least ~1rem").toBeGreaterThanOrEqual(14);
expect(rightInset, "right new-chat inset should be at least ~1rem").toBeGreaterThanOrEqual(14);
expect(
Math.abs(leftInset - rightInset),
`left/right insets should match (left=${leftInset}, right=${rightInset})`,
).toBeLessThanOrEqual(2);
});
}

for (const viewport of [
{ name: "mobile", width: 390, height: 820 },
{ name: "desktop", width: 1280, height: 900 },
Expand Down
13 changes: 13 additions & 0 deletions tests/ui-overlay-css-contract.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,4 +39,17 @@ describe("overlay and global CSS contracts", () => {
expect(baseBlock).not.toContain("border-color:");
expect(globalStylesSource).toMatch(/\.answer-footer-search-pill:focus-within\s*\{[\s\S]*?border-color:/);
});

it("keeps phone header edge padding tokenized and never zeroed by unlayered media", () => {
// --header-edge-pad is the single phone/sm inset shared by the layered
// .edge-glass-header base and the unlayered max-width:639px guard. A bare
// max(0px, safe-area) override previously pinned new-chat to the bezel.
expect(occurrenceCount(globalStylesSource, "--header-edge-pad:")).toBe(1);
expect(globalStylesSource).toMatch(/--header-edge-pad:\s*1rem;/);
expect(occurrenceCount(globalStylesSource, "max(var(--header-edge-pad), var(--safe-area-left))")).toBe(2);
expect(occurrenceCount(globalStylesSource, "max(var(--header-edge-pad), var(--safe-area-right))")).toBe(2);
expect(globalStylesSource).not.toMatch(
/\.edge-glass-header\s*\{[^}]*padding-left:\s*max\(0px,\s*var\(--safe-area-left\)\)/s,
);
});
});