mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
fix(ui): suppress Topology Explorer mount at narrow viewports (slice 26 HG-8 OPT-D3 repair)
OPT-C3 z-index carve-out (1d935da) was invalidated by velocity-qa
recheck: pinning Topology mobile Explorer to z-20 hides it visually
but does NOT prevent mount; the pre-existing TopologyTableView +
TopologyTreeView renderer-spin pegs the browser when the Explorer
component mounts on hamburger click, independent of whether it is
visible.
OPT-D3 reverts the surface-conditional z-index back to uniform
opaque-mode z-40 (matching all 4 other Explorer-bearing destinations)
and instead gates the Explorer MOUNT on viewport for the Topology
surface. The rule lives in a new pure helper
`shouldSuppressExplorerMount(surface, isWideLayout)` exported from
Explorer.tsx; AppShell composes it with the existing explorerVisible
gate. At narrow viewports (< lg breakpoint) the Topology Explorer is
not rendered at all — the peg trigger is never armed.
Cross-destination preservation: settings / project / specs / for-you
mount normally regardless of viewport; the carve-out is Topology-
specific. The 0.3.2 Topology mobile render-path fix will remove this
suppression.
Tests: pure-predicate tests for shouldSuppressExplorerMount cover 5
surfaces × 2 viewports + the "none" surface; replaces the 2 OPT-C3
class-contract tests that asserted z-20 for Topology opaque-mode.
Other 13 tests (cross-destination z-40 contract for 4 surfaces +
overlay-mode z-30 preservation) unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
1d935dae5e
commit
94e7155f92
3 files changed
+85
-101
No files matched your search
@@ -41,7 +41,7 @@ import {
|
||||
Sparkles,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { Explorer, type ExplorerSurface } from "./Explorer.js";
|
||||
import { Explorer, shouldSuppressExplorerMount, type ExplorerSurface } from "./Explorer.js";
|
||||
import { SharedDetailDrawer, type DrawerSelection } from "./SharedDetailDrawer.js";
|
||||
import { PreviewStack } from "./preview/PreviewStack.js";
|
||||
import type { DiscoveryPlacementTarget } from "./DiscoveryPanel.js";
|
||||
@@ -436,6 +436,14 @@ function AppShellInner({ children }: AppShellProps) {
|
||||
useGlobalEvents();
|
||||
|
||||
const explorerVisible = surface !== "none";
|
||||
// Slice 26.D OPT-D3 Topology mobile mount-suppression: rule lives
|
||||
// in shouldSuppressExplorerMount() (Explorer.tsx). Pre-existing
|
||||
// renderer-spin in Topology mobile render path pegs the browser
|
||||
// when Explorer mounts at 375px; only mount-suppression sidesteps
|
||||
// the peg trigger. Other 4 destinations mount normally regardless
|
||||
// of viewport. 0.3.2 fixes Topology mobile render path; this
|
||||
// suppression reverts at that time.
|
||||
const explorerMounted = explorerVisible && !shouldSuppressExplorerMount(surface, isWideLayout);
|
||||
const drawerOpen = Boolean(selectionState);
|
||||
|
||||
// V1 attempt-3 Phase 3 bounce-fix — Class B fixed-anchor + selective overlay.
|
||||
@@ -572,9 +580,11 @@ function AppShellInner({ children }: AppShellProps) {
|
||||
|
||||
{/* Explorer — desktop column or mobile slide-over.
|
||||
In overlay mode (topology graph): vellum-translucent + z-30
|
||||
so it floats over the canvas. In opaque mode: default
|
||||
Phase 2 behavior (z-20, opaque background). */}
|
||||
{explorerVisible && (
|
||||
so it floats over the canvas. In opaque mode: z-40
|
||||
per slice 26.B OPT-B (above rail-tray on mobile).
|
||||
Slice 26.D OPT-D3: Topology Explorer doesn't mount
|
||||
on mobile (suppressTopologyMobileExplorer above). */}
|
||||
{explorerMounted && (
|
||||
<Explorer
|
||||
open={explorerOpen}
|
||||
onClose={() => setExplorerOpen(false)}
|
||||
|
||||
@@ -37,6 +37,26 @@ export type ExplorerSurface =
|
||||
| "settings"
|
||||
| "none";
|
||||
|
||||
/**
|
||||
* Slice 26.D OPT-D3 Topology mobile Explorer mount-suppression rule.
|
||||
*
|
||||
* Returns true when the Explorer for a given surface should NOT MOUNT
|
||||
* at the current viewport. The single carve-out: Topology surface at
|
||||
* narrow viewports (isWideLayout=false). Pre-existing renderer-spin
|
||||
* in the Topology mobile render path (TopologyTableView +
|
||||
* TopologyTreeView combined) pegs the browser when Explorer mounts
|
||||
* at 375px; mount-suppression sidesteps the peg trigger.
|
||||
*
|
||||
* 0.3.2 fixes the Topology mobile render path; this carve-out
|
||||
* returns false for all surfaces at that time.
|
||||
*/
|
||||
export function shouldSuppressExplorerMount(
|
||||
surface: ExplorerSurface,
|
||||
isWideLayout: boolean,
|
||||
): boolean {
|
||||
return surface === "topology" && !isWideLayout;
|
||||
}
|
||||
|
||||
interface ExplorerProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -316,30 +336,25 @@ export function Explorer({
|
||||
// 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 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.
|
||||
// destination on mobile. Opaque-mode bumped to z-40 for all
|
||||
// surfaces; 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.
|
||||
// Slice 26.D OPT-D3 carve-out (separate gate in AppShell.tsx,
|
||||
// NOT here): Topology mobile Explorer doesn't MOUNT on
|
||||
// viewport < lg (avoids pre-existing TopologyTableView
|
||||
// renderer-spin that pegs the browser when Explorer mounts
|
||||
// at 375px). The mount-gate is in AppShell.tsx ~line 577;
|
||||
// this z-index block is reached only when Explorer is
|
||||
// actually mounted, so no conditional is needed here. 0.3.2
|
||||
// will fix the Topology mobile render path; the AppShell
|
||||
// mount-gate reverts at that time.
|
||||
isOverlay
|
||||
? "vellum z-30 shadow-[6px_0_14px_rgba(46,52,46,0.06)]"
|
||||
: 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)]",
|
||||
: "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",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { Explorer } from "../src/components/Explorer.js";
|
||||
import { Explorer, shouldSuppressExplorerMount } from "../src/components/Explorer.js";
|
||||
import type { ExplorerSurface } from "../src/components/Explorer.js";
|
||||
import { createAppTestRouter } from "./helpers/test-router.js";
|
||||
|
||||
@@ -138,82 +138,41 @@ describe("Explorer mobile drawer z-index layering (slice 26.B HG-8 OPT-B 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);
|
||||
// Slice 26.D OPT-D3 Topology mobile Explorer mount-suppression.
|
||||
//
|
||||
// OPT-C3 z-index carve-out was invalidated by velocity-qa recheck:
|
||||
// z-index suppresses visibility but NOT React MOUNT. The peg trigger
|
||||
// is the Explorer drawer MOUNTING on hamburger click, which causes
|
||||
// adjacent Topology re-render cascade — independent of whether the
|
||||
// drawer is visible. OPT-D3 suppresses the mount itself for Topology
|
||||
// at narrow viewports. Pure predicate; trivially testable.
|
||||
|
||||
describe("shouldSuppressExplorerMount (slice 26.D OPT-D3 mount-suppression predicate)", () => {
|
||||
it("Topology + narrow viewport (isWideLayout=false) → SUPPRESS mount (true)", () => {
|
||||
expect(shouldSuppressExplorerMount("topology", false)).toBe(true);
|
||||
});
|
||||
|
||||
it("Topology + wide viewport (isWideLayout=true) → mount normally (false)", () => {
|
||||
expect(shouldSuppressExplorerMount("topology", true)).toBe(false);
|
||||
});
|
||||
|
||||
// Cross-destination preservation: all 4 other Explorer-bearing
|
||||
// surfaces mount regardless of viewport. The carve-out is
|
||||
// Topology-specific.
|
||||
const otherSurfaces: ExplorerSurface[] = ["settings", "project", "specs", "for-you"];
|
||||
for (const surface of otherSurfaces) {
|
||||
it(`${surface} + narrow viewport → mount normally (false)`, () => {
|
||||
expect(shouldSuppressExplorerMount(surface, false)).toBe(false);
|
||||
});
|
||||
it(`${surface} + wide viewport → mount normally (false)`, () => {
|
||||
expect(shouldSuppressExplorerMount(surface, true)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
it("'none' surface → never suppressed (consistent with Explorer not mounting anyway via explorerVisible gate)", () => {
|
||||
expect(shouldSuppressExplorerMount("none", false)).toBe(false);
|
||||
expect(shouldSuppressExplorerMount("none", true)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user