From f080b95fa69eef2ef80e3fb26c23111e3c01385d Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 25 Sep 2026 08:29:50 +0700 Subject: [PATCH 1/5] fix(overlay): lift an open overlay host above later siblings ui-overlay-host isolates every Select and Dropdown so a pointer can reach an overlay that hangs outside its root. The isolation also confines the overlay's z-index to its host, which paints at z-index auto in tree order, so a later positioned sibling of an ancestor covered the open list. A Select in a table row is where it shipped: the next .table__row paints over the options and they cannot be pressed. While anything inside the host is open, the host takes the overlay's layer. --- src/components/_shared/overlayHost.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/_shared/overlayHost.css b/src/components/_shared/overlayHost.css index 0a35dd93..4063c916 100644 --- a/src/components/_shared/overlayHost.css +++ b/src/components/_shared/overlayHost.css @@ -35,10 +35,24 @@ * * Import this file and put `ui-overlay-host` on the component's root, alongside * whatever positioning that root already sets. + * + * ## Why an open host lifts itself + * + * The stacking context has a cost: the overlay's own `z-index` now only ranks + * it inside its host. The host itself paints at `z-index: auto`, in tree order, + * so any later positioned sibling of an ancestor paints over the open overlay. + * A Select in a table row is the case that shipped: `.table__row` is + * `position: relative`, so the next row covered the open list and its options + * could not be pressed. While anything inside the host is open, the host takes + * the overlay's layer so the whole subtree paints above what follows it. */ @layer components { .ui-overlay-host { position: relative; isolation: isolate; } + + .ui-overlay-host:has([data-open="true"]) { + z-index: 80; + } } From 2bae4f26dfad15653f45d2e6b39b750c439f5113 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 25 Sep 2026 08:29:50 +0700 Subject: [PATCH 2/5] fix(overlay): size an overlay before measuring its first position The width limits reached the element through the style signal only after the first update had measured it, so the overlay was measured at its unconstrained width. For Select, whose CSS floor is max(100%, 14rem) on a fixed box, that is the whole viewport, and the left clamp pinned the list to the screen edge. A desktop browser moved it into place a frame later through the ResizeObserver, which read as a blink on the left; chuzz, whose observer does not fire, left it there. The same limits are now written onto the element before it is measured, so the first position is the final one. Measured on a Select in a table row: chuzz-headless placed the list at left 88 under a trigger at 696 and now at 696; the desktop browser's first write was left 8 under a trigger at 765 and is now 765.38. --- src/components/_shared/overlayPosition.ts | 35 +++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/_shared/overlayPosition.ts b/src/components/_shared/overlayPosition.ts index 8fc472a4..0c88e98f 100644 --- a/src/components/_shared/overlayPosition.ts +++ b/src/components/_shared/overlayPosition.ts @@ -135,9 +135,32 @@ export const createOverlayPosition = ( options.anchorRect?.(), ); if (!triggerRect) return; - const overlayRect = overlay.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; + + // Size before measuring. The width limits below arrive through the + // style signal, which lands only after this pass, so the overlay was + // measured at its unconstrained width: for Select, whose CSS floor is + // `max(100%, 14rem)` on a fixed box, that is the whole viewport. The + // left clamp then pinned it to the screen edge. Chromium corrected it a + // frame later through the ResizeObserver, which read as a blink; an + // engine whose observer does not fire kept it there. Writing the same + // limits onto the element first makes the one measurement the real one. + const maxWidth = `${round(Math.max(0, viewportWidth - viewportPadding * 2))}px`; + const minWidth = options.matchTriggerWidth?.() + ? `${round(Math.max(triggerRect.width, options.minWidth?.() ?? 0))}px` + : undefined; + // A throw here would halt every reactive owner in the app, and an + // overlay stand-in without a style declaration (tests measure with + // one) is still positionable, so the write is skipped rather than + // assumed. + const overlayStyle = overlay.style as CSSStyleDeclaration | undefined; + if (overlayStyle) { + overlayStyle.maxWidth = maxWidth; + if (minWidth) overlayStyle.minWidth = minWidth; + } + + const overlayRect = overlay.getBoundingClientRect(); const offset = options.offset(); const placement = resolvePlacement( @@ -201,14 +224,8 @@ export const createOverlayPosition = ( position: "fixed", top: `${round(clamp(top, viewportPadding, maxTop))}px`, left: `${round(clamp(left, viewportPadding, maxLeft))}px`, - "max-width": `${round(Math.max(0, viewportWidth - viewportPadding * 2))}px`, - ...(options.matchTriggerWidth?.() - ? { - "min-width": `${round( - Math.max(triggerRect.width, options.minWidth?.() ?? 0), - )}px`, - } - : {}), + "max-width": maxWidth, + ...(minWidth ? { "min-width": minWidth } : {}), }); }; From 13123ab0fc7f601d1e2c169a2d16de017e94d530 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 25 Sep 2026 13:37:48 +0700 Subject: [PATCH 3/5] ci: take the released headless host instead of compiling an old chuzz The host action was pinned to a chuzz revision from before headless releases existed, so every run compiled that source against the current registry. Its unlocked dependencies moved on (tokio features, blitz-control-protocol 0.5.3) and it stopped compiling, turning every PR red in the host step. The pin now names chuzz master, whose action downloads the verified chuzz-headless 0.1.39 release. --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db85ca82..a1514d5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,10 @@ jobs: cache-on-failure: true # The host is the browser, and the browser is not a crate a site can - # install: `chuzz-headless` is built from source by the composite action - # below, which also installs the driver. That replaced `qa-inspect-host`, + # install: the composite action below downloads the verified + # `chuzz-headless` release matching its own revision (building that + # revision from source only if no release exists), and installs the + # driver. That replaced `qa-inspect-host`, # which was a second headless browser with the web platform in only the # other one -- no `URLSearchParams`, no `matchMedia`, no storage, no # observers -- so every gap closed for the browser had to be closed a @@ -54,12 +56,18 @@ jobs: # # 0.1.12 is still on crates.io, so nothing broke when the crate was # deleted from source; this is the last consumer to move off it. + # + # Pinned to a chuzz revision that has a published host (0.1.39). The + # previous pin predated the releases, so every run compiled that old + # source against today's registry, and it stopped compiling once its + # unlocked dependencies moved on (tokio features, blitz-control-protocol + # 0.5.3): every PR here went red on a browser build, not on this code. - name: Headless browser host id: qa - uses: pathscale/chuzz/.github/actions/headless-host@b0e4ce0d35fe96230c90d2a3ed55a64686abe370 + uses: pathscale/chuzz/.github/actions/headless-host@117cb4001934ca6706869a76b8ff5a80540206c7 with: token: ${{ secrets.SIBLING_REPOS_TOKEN }} - chuzz-ref: b0e4ce0d35fe96230c90d2a3ed55a64686abe370 + chuzz-ref: 117cb4001934ca6706869a76b8ff5a80540206c7 ps-qa-version: =0.7.3 - name: Install Dependencies From 64085bea301a0d0c6909c63d6d24b861b129cc78 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 25 Sep 2026 14:04:01 +0700 Subject: [PATCH 4/5] docs(reviews): take local paths out of two reviews One named a session scratchpad file under /private/tmp by its full path, the other ran git -C against /Users/revenge/code/UI. Neither path exists anywhere but the machine the review ran on. The scratch file is described instead, and the git command runs in the checkout it is read from. --- docs/reviews/2026-07-27-ai-smell-deadcode-tests-docs.md | 6 +++--- docs/reviews/2026-07-27-theming-css-build-perf.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reviews/2026-07-27-ai-smell-deadcode-tests-docs.md b/docs/reviews/2026-07-27-ai-smell-deadcode-tests-docs.md index 18e5468e..c42c03bb 100644 --- a/docs/reviews/2026-07-27-ai-smell-deadcode-tests-docs.md +++ b/docs/reviews/2026-07-27-ai-smell-deadcode-tests-docs.md @@ -901,9 +901,9 @@ bun run playground:dev # fastest visual loop; aliases @pathscale/ui -> ``` **Reproduce the duplication census:** the 6-line normalized-window scanner used for SEV-4 is -at -`/private/tmp/claude-501/-Users-revenge-code/4526fe76-867f-4d6a-a325-84ff907ebbb8/scratchpad/dup.ts` -(scratchpad, may be cleaned up; it is ~40 lines and trivial to rewrite). Spot checks: +was a +`dup.ts` in the reviewer's scratch directory, not kept in the repository (it is ~40 +lines and trivial to rewrite). Spot checks: ```bash rg -n "const invokeEventHandler" src | wc -l # 25 diff --git a/docs/reviews/2026-07-27-theming-css-build-perf.md b/docs/reviews/2026-07-27-theming-css-build-perf.md index 81050b29..c867d4ac 100644 --- a/docs/reviews/2026-07-27-theming-css-build-perf.md +++ b/docs/reviews/2026-07-27-theming-css-build-perf.md @@ -733,7 +733,7 @@ Read in this order: Commands that work in this checkout (no `node_modules` present): ```bash -git -C /Users/revenge/code/UI rev-parse --short HEAD +git rev-parse --short HEAD diff <(sed -n '88,135p' src/styles/themes/light.css) <(sed -n '87,134p' src/styles/themes/dark.css) # SEV-6: silent rg --no-filename -o "z-index: [^;]+;" src/components --glob '*.css' | sort | uniq -c | sort -rn # SEV-7 rg -n -- "--z-overlay\s*:" src # SEV-7: no hits From 6c49eb2d7ebfec0ad87c566af7f3fd7821142f76 Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 25 Sep 2026 14:46:42 +0700 Subject: [PATCH 5/5] ci: take chuzz-headless 0.1.40 0.1.40 reports the live viewport size and fires ResizeObserver, and pulls ps-blitz 0.4.12 with MutationObserver, so the QA host behaves like the browser it stands in for on overlays and observers. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1514d5c..2cfd95de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,17 +57,17 @@ jobs: # 0.1.12 is still on crates.io, so nothing broke when the crate was # deleted from source; this is the last consumer to move off it. # - # Pinned to a chuzz revision that has a published host (0.1.39). The + # Pinned to a chuzz revision that has a published host (0.1.40). The # previous pin predated the releases, so every run compiled that old # source against today's registry, and it stopped compiling once its # unlocked dependencies moved on (tokio features, blitz-control-protocol # 0.5.3): every PR here went red on a browser build, not on this code. - name: Headless browser host id: qa - uses: pathscale/chuzz/.github/actions/headless-host@117cb4001934ca6706869a76b8ff5a80540206c7 + uses: pathscale/chuzz/.github/actions/headless-host@094d77b6f28f78ffcdc20b8b19e160ee6c4992ee with: token: ${{ secrets.SIBLING_REPOS_TOKEN }} - chuzz-ref: 117cb4001934ca6706869a76b8ff5a80540206c7 + chuzz-ref: 094d77b6f28f78ffcdc20b8b19e160ee6c4992ee ps-qa-version: =0.7.3 - name: Install Dependencies