PR 10969 · pickers float above the table header and the side-nav resize strip

The building picker and the company picker now open above sticky table headers and the sidebar's drag strip. Every dropdown portals to the page body on one layer.

23 Sept 2026 · branch fix/popover-layers · captured against propflow-stage · at 51e82386c3

Data note. Every name and balance below comes from propflow-stage, which is anonymized by construction (ADR-0097/0110 — phones in the impossible +1000 NPA, emails @example.test). The identical capture against prod would carry real tenants' names and balances and is not safe to attach anywhere, which is why the generator refuses to run against one.

1 · Before: the Collections header row paints over the building picker

This is Gera's report. After a scroll, the table's sticky column-header row (TENANT, PROPERTY, UNIT…) sticks just under the top bar and paints over the open building list. Collections pins its first column, which puts its header at Z.rail (50). The list was a position: absolute child of the top bar, a fixed header at z-30, so the list's own z-index was only ever compared inside the top bar. 30 loses to 50, whatever the list asks for.

Provenance note: the before server runs 41ec47759f, which is this branch's merge base (git merge-base origin/main fix/popover-layers). The capture tool marks any section on the tip of main as 'not a before', but that check assumes main has moved on since the branch was cut. It hasn't: the tip of main doesn't contain this change.

Asserted in the captured DOM — All Properties · TENANT · PROPERTY · UNIT

Before: the Collections header row paints over the building picker
⚠ Provenance. Captured from commit 41ec47759f, read from the worktree the server was started from (/Users/miniclaw/code/PropFlow/propflowai-popover-before) — the server could not report its own commit, so this is the weaker of the two sources. ⚠ That is the current tip of origin/main. If this section is labelled a BEFORE, it is not one — it is showing today's main, which already carries the change.

2 · After: the building list sits above the header row

Same page, same scroll. The list now renders through AnchoredPanel: it portals to <body>, positions fixed against the trigger, and sits at Z.modalPopover (350), the one layer every portalled popover uses. The header row is hidden behind the panel instead of cutting across it. AnchoredPanel.layer.test.tsx pins the DOM half: the list's parent is <body>, it carries that layer, and a click inside it doesn't close it.

Asserted in the captured DOM — All Properties · 1–30

After: the building list sits above the header row

3 · Before: the sidebar's resize strip cuts through the company picker

The company picker is open and the pointer is on the sidebar's drag strip. The strip, a sibling of the side nav at the same Z.rail and later in the DOM, wins the tie. Its blue hover band runs down through the open panel. The panel was stuck inside the nav's stacking context and was also clipped to the nav's width.

Provenance note: the before server runs 41ec47759f, which is this branch's merge base (git merge-base origin/main fix/popover-layers). The capture tool marks any section on the tip of main as 'not a before', but that check assumes main has moved on since the branch was cut. It hasn't: the tip of main doesn't contain this change.

Asserted in the captured DOM — Every company · Done

Before: the sidebar's resize strip cuts through the company picker
⚠ Provenance. Captured from commit 41ec47759f, read from the worktree the server was started from (/Users/miniclaw/code/PropFlow/propflowai-popover-before) — the server could not report its own commit, so this is the weaker of the two sources. ⚠ That is the current tip of origin/main. If this section is labelled a BEFORE, it is not one — it is showing today's main, which already carries the change.

4 · After: the company picker is above the strip

The panel portals out of the nav, so nothing cuts through it. It also no longer shrinks to the nav's width. The capture can't hover the strip here, because the panel now covers it and the browser refuses the hover, and that refusal is the fix. Ticking a company still doesn't navigate until Done or a click away: the outside-click check now includes the portalled panel. org-scope-switcher.test.tsx covers this ('two ticks then a click AWAY is the same one commit').

Asserted in the captured DOM — Every company · Done

After: the company picker is above the strip

5 · After (phone, 390px): the company picker opened from the nav drawer

On a phone the nav is a drawer at Z.modal (300). The portalled panel (350) opens above it, over the drawer and its scrim. The drawer's focus trap now cycles Tab and Shift+Tab inside the portalled panel while focus is there, so focus can't slip behind the scrim. SideNav.phone-drawer.test.tsx covers this ('while focus is in a portalled popover…'), and both new cases fail against a trap that ignores the panel.

Asserted in the captured DOM — Every company · Done

After (phone, 390px): the company picker opened from the nav drawer

What else had the same bug

Fixed here: the shared Popover and DropdownMenu (every table row ⋯ menu) defaulted to Z.rail, the same layer as the chrome they must cover. IdentitySelect and FilterBar sat at Z.overlay, below modals. All four now use Z.modalPopover. Still absolute inside their trigger and listed for a follow-up: ComboBox, DateRangePicker, CompoundFilterPill, ConfirmPopover, TableSearch suggestions, and the side-nav account menu.

PropFlow Docs