fix(ui): surface-conditional Topology mobile z-index carve-out (slice 26 OPT-C3 repair)

velocity-qa caught at /topology + 375px (slice 26 OPT-B recheck):
hamburger tap pegged the headless browser renderer. Reproduced
twice; stuck renderers killed to avoid host CPU burn.

Forensic finding (qitem-20260512112436-347b6c98):
pure CSS z-index swap does NOT trigger React reconciliation work;
TopologyTableView (443L) + TopologyTreeView (264L) use standard
hook patterns; mounted pre-OPT-B too (just covered by rail-tray).
Strong evidence: pre-existing renderer-spin in Topology mobile
render path; OPT-B z-index change just made the click path
reachable to QA.

Repair (OPT-C3 per orch routing at qitem-20260512112530-0de8d3c1):
surface-conditional z-index. Topology surface pinned to z-20
(pre-OPT-B baseline; covered by rail-tray on mobile; unreachable;
click path doesn't reach the peg trigger). Other 4 destinations
(settings / project / specs / for-you) keep z-40 (OPT-B fix
preserved; cross-destination chrome consistency for 4/5).

Smallest-correct path: 1-block className conditional, no React
state changes, no new components, no daemon work. Topology mobile
drawer remains broken-by-overlap (legacy state) but does not peg
the renderer.

0.3.2 plans a Topology mobile render-path fix (per banked
feedback_usestate_not_usemutation_for_fire_and_forget — Table-hang
renderer-spin class; useRef + imperative DOM escape-hatch
territory). This carve-out reverts at that time.

Tests added (slice-26.C OPT-C3 carve-out regressions):
- POSITIVE: opaque-mode Topology Explorer className contains z-20
- NEGATIVE: z-40 absent for Topology opaque mode (discriminates
  the carve-out from the other-destinations OPT-B path)
- NUMERIC LAYERING INVARIANT: extract z-N as integer + assert
  N < MOBILE_RAIL_TRAY_Z (30); Topology Explorer must be BELOW
  rail-tray to remain covered + prevent peg trigger

Preserved (OPT-B regression coverage; all from earlier commit):
- 4 opaque surfaces (project/specs/for-you/settings) still assert
  z-40 + not-z-20 + N > 30
- Topology overlay-mode still asserts z-30 (graph canvas float)

Test counts:
- explorer-mobile-zindex-layering: 15 tests (13 prior + 2 NEW
  Topology carve-out regressions)
- settings-explorer: 8/8 unchanged
- settings-pages: 4/4 unchanged
- Full UI suite: 784 pass + 1 skipped + 52 pre-existing fails
  (baseline identical; +2 net pass / 0 new fail)
- Touched files typecheck clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-12 04:26:59 -07:00
1 parent 125c747c5d
commit 1d935dae5e
2 files changed
+103 -8

No files matched your search

+24 -8
View File
@@ -312,18 +312,34 @@ export function Explorer({
//
// Slice 26.B HG-8 mobile-drawer-layering repair (OPT-B per
// orch routing): opaque-mode Explorer mobile drawer must
// layer ABOVE the mobile-rail-tray (AppShell.tsx z-30)
// so click hits register on Explorer items. Pre-repair value
// layer ABOVE the mobile-rail-tray (AppShell.tsx z-30) so
// click hits register on Explorer items. Pre-repair value
// was z-20 (below rail-tray) — pre-existing bug exposed by
// slice 26 because Settings was the 5th Explorer-bearing
// destination on mobile. Bumped to z-40. Overlay-mode stays
// z-30 (Topology graph behavior preserved; rail-tray and
// overlay-mode Explorer paint at same z, DOM order resolves
// Explorer above since it renders later in AppShell). Rail-tray
// remains reachable via backdrop dismissal on mobile.
// destination on mobile. Bumped to z-40 for opaque-mode
// destinations. Overlay-mode stays z-30 (Topology graph
// behavior preserved; rail-tray and overlay-mode Explorer
// paint at same z, DOM order resolves Explorer above since
// it renders later in AppShell). Rail-tray remains reachable
// via backdrop dismissal on mobile.
//
// Slice 26.C OPT-C3 Topology mobile carve-out (per orch
// routing on qa renderer-peg report at qitem-2026-05-12...):
// Topology surface is intentionally pinned to z-20 on opaque
// mode — restoring the pre-OPT-B "covered by rail-tray"
// baseline. Pre-existing renderer-spin in TopologyTableView
// pegs the headless browser when Explorer mounts/becomes
// visible at 375px; keeping Topology Explorer hidden behind
// rail-tray on mobile prevents the click path from reaching
// the peg trigger. Other 4 Explorer destinations (settings /
// project / specs / for-you) keep the OPT-B z-40 fix. 0.3.2
// will fix the Topology mobile render path; this carve-out
// reverts at that time.
isOverlay
? "vellum z-30 shadow-[6px_0_14px_rgba(46,52,46,0.06)]"
: "z-40 bg-[rgba(250,249,245,0.035)] supports-[backdrop-filter]:bg-[rgba(250,249,245,0.018)] backdrop-blur-[14px] backdrop-saturate-75 shadow-[6px_0_14px_rgba(46,52,46,0.04)]",
: surface === "topology"
? "z-20 bg-[rgba(250,249,245,0.035)] supports-[backdrop-filter]:bg-[rgba(250,249,245,0.018)] backdrop-blur-[14px] backdrop-saturate-75 shadow-[6px_0_14px_rgba(46,52,46,0.04)]"
: "z-40 bg-[rgba(250,249,245,0.035)] supports-[backdrop-filter]:bg-[rgba(250,249,245,0.018)] backdrop-blur-[14px] backdrop-saturate-75 shadow-[6px_0_14px_rgba(46,52,46,0.04)]",
// Mobile: slide-over from left below the top-bar header (h-14).
"fixed top-14 bottom-0 left-0 transition-transform duration-200 ease-tactical w-72 max-w-[80vw]",
open ? "translate-x-0" : "-translate-x-full",
@@ -137,4 +137,83 @@ describe("Explorer mobile drawer z-index layering (slice 26.B HG-8 OPT-B repair)
// changed by this repair.
});
});
// Slice 26.C OPT-C3 Topology mobile carve-out: opaque-mode Topology
// Explorer is intentionally pinned to z-20 (pre-OPT-B baseline) so
// mobile-rail-tray covers it — sidesteps the pre-existing
// TopologyTableView renderer-spin that pegs the browser when the
// mobile drawer becomes visible at 375px. The 0.3.2 Topology mobile
// refactor will fix the renderer-spin; this carve-out reverts then.
describe("surface=topology (opaque-mode OPT-C3 carve-out)", () => {
it("opaque-mode Topology Explorer pins to z-20 (BELOW mobile-rail-tray z-30)", async () => {
render(
createAppTestRouter({
initialPath: "/topology",
routes: [
{
path: "/topology",
component: () => (
<Explorer
open={true}
onClose={() => {}}
selection={null}
onSelect={() => {}}
desktopMode="full"
surface="topology"
overlayMode="opaque"
/>
),
},
],
}),
);
await waitFor(() => {
expect(screen.getByTestId("explorer")).toBeTruthy();
});
const explorer = screen.getByTestId("explorer");
// POSITIVE: z-20 present (the carve-out value; restores
// pre-OPT-B baseline for Topology mobile)
expect(explorer.className).toMatch(/\bz-20\b/);
// NEGATIVE: z-40 absent (the OPT-B fix that applies to other
// destinations is intentionally NOT applied to Topology — the
// carve-out discriminator)
expect(explorer.className).not.toMatch(/\bz-40\b/);
});
it("NUMERIC LAYERING INVARIANT: Topology opaque-mode z-index is LESS THAN mobile-rail-tray z-30", async () => {
render(
createAppTestRouter({
initialPath: "/topology",
routes: [
{
path: "/topology",
component: () => (
<Explorer
open={true}
onClose={() => {}}
selection={null}
onSelect={() => {}}
desktopMode="full"
surface="topology"
overlayMode="opaque"
/>
),
},
],
}),
);
await waitFor(() => {
expect(screen.getByTestId("explorer")).toBeTruthy();
});
const explorer = screen.getByTestId("explorer");
const zMatch = explorer.className.match(/\bz-(\d+)\b/);
expect(zMatch).toBeTruthy();
const explorerZ = Number(zMatch![1]);
// Topology mobile drawer must be BELOW rail-tray (z-30) to
// remain covered + prevent the renderer-peg path. Inverts the
// other-destinations invariant where Explorer must be above.
expect(explorerZ).toBeLessThan(MOBILE_RAIL_TRAY_Z);
expect(explorerZ).toBe(20);
});
});
});