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
31cb24d
fix(ui): balance mobile header new-chat edge inset
cursoragent Jul 19, 2026
787bcbd
fix(ui): stop phone media from zeroing header insets
cursoragent Jul 19, 2026
84def7c
fix(ui): tokenize header edge pad and lock the phone inset contract
cursoragent Jul 19, 2026
15a0006
merge(main): keep header-edge-pad with Safari dock tokens
cursoragent Jul 19, 2026
0c00025
docs: record PR #940 header inset final review in ledger
cursoragent Jul 19, 2026
5f90d20
merge(main): sync after ingestion-worker lease hardening
cursoragent Jul 19, 2026
0c7c320
chore: replace stale PR_POLICY_BODY.md for header inset PR
cursoragent Jul 19, 2026
501f63e
chore: fix governance attestation wording in PR policy body
cursoragent Jul 19, 2026
2943185
chore: keep header inset PR non-clinical for policy sync
cursoragent Jul 19, 2026
d2bbdea
merge(main): drop PR_POLICY_BODY.md after successful policy sync
cursoragent Jul 19, 2026
923cd5f
merge(main): keep both #943 and #940 ledger closeout rows
cursoragent Jul 19, 2026
e8d3671
docs: refresh #940 ledger row for final merge-ready head
cursoragent Jul 19, 2026
83f91f1
merge(main): resolve ledger conflict for header inset PR
cursoragent Jul 20, 2026
e572399
docs: point #940 ledger row at current merge-ready HEAD
cursoragent Jul 20, 2026
471df0a
merge(main): keep #951 and #940 ledger closeout rows
cursoragent Jul 20, 2026
84eb0b6
docs: refresh #940 ledger SHA after main sync
cursoragent Jul 20, 2026
20e7469
docs: align #940 ledger SHA with tip
cursoragent Jul 20, 2026
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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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,
);
});
});