fix(e10): stop the hidden chip measurer scrolling the whole app sideways

The four chip lanes render an intentionally unbounded hidden "mirror" so the shared
fit core can read each chip's natural width. An absolutely positioned box still
contributes to its ancestors' SCROLLABLE overflow, and `visibility: hidden` does not
exempt it — and nothing between the toolbar band and <html> clipped it.

At the 1024x768 floor that meant the channel manager's document scroll width was
1258px: the whole app scrolled 249px sideways into dead space and dragged the fixed
header to x=-177. The same mirror was also 228px wide inside the Tags column's 144px
cell, which pushed the table to 990px inside its 906px container — i.e. it broke
DataTable's "the table never exceeds its container" contract and truncated channel
names to "The...". One cause, two symptoms.

The lane now carries a shared `chipLaneClass` = `relative overflow-clip`, so the
contract lives in one constant instead of four hand-rolled class strings. `clip`
rather than `hidden` because it creates no scrollport, so nothing can ever scroll the
mirror into view. Measurement is untouched: offsetLeft/offsetWidth are layout values
and a clip does not change layout.

Measured after: document 1258 -> 1024, sideways scroll 249 -> 0, table 990 -> 921
with no cell overflow, and the "+N" collapse still fits exactly as before.

A source-level test asserts every component rendering a mirror uses the lane class —
the drift a fifth hand-rolled lane would introduce, which no DOM test can catch since
jsdom has no layout engine to overflow.
This commit is contained in:
2026-09-05 22:22:14 +02:00
parent cc8cf96c19
commit f99240d8ca
5 changed files with 74 additions and 7 deletions
@@ -0,0 +1,48 @@
import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { chipLaneClass } from "./useChipOverflowFit";
// E10 S4. The chip lanes' hidden measurer is laid out UNBOUNDED on purpose (it exists to read natural
// widths), and an absolutely-positioned box still feeds its ancestors' SCROLLABLE overflow even while
// `visibility: hidden`. Un-clipped, that leaked to <html>: at a 1024px viewport the channel manager's
// document scrolled 249px sideways and the fixed header slid off. `chipLaneClass` carries the clip that
// contains it — so the invariant worth locking is not "this lane looks right" but "every mirror lives
// inside a lane that clips". A fifth lane hand-rolling `relative` is exactly the drift this catches,
// and no DOM test can: jsdom has no layout, so the overflow it guards against is unobservable there.
const MIRROR = "invisible absolute left-0 top-0";
function componentSources(): Array<[string, string]> {
const dir = new URL("../components/", import.meta.url);
const walk = (u: URL, prefix: string): Array<[string, string]> =>
readdirSync(u, { withFileTypes: true }).flatMap((e) => {
if (e.isDirectory()) return walk(new URL(`${e.name}/`, u), `${prefix}${e.name}/`);
if (!e.name.endsWith(".tsx") || e.name.includes(".test.")) return [];
return [
[`${prefix}${e.name}`, readFileSync(new URL(e.name, u), "utf-8")] as [string, string],
];
});
return walk(dir, "");
}
describe("chip lane containment contract", () => {
it("keeps both halves of the lane class — the offsetParent AND the clip", () => {
expect(chipLaneClass.split(/\s+/)).toEqual(
expect.arrayContaining(["relative", "overflow-clip"]),
);
});
it("every component rendering a hidden chip measurer uses chipLaneClass for its lane", () => {
const offenders = componentSources()
.filter(([, src]) => src.includes(MIRROR))
.filter(([, src]) => !src.includes("chipLaneClass"))
.map(([name]) => name);
expect(offenders).toEqual([]);
});
it("finds the measurers it claims to guard (the marker has not been renamed away)", () => {
const withMirror = componentSources().filter(([, src]) => src.includes(MIRROR));
expect(withMirror.length).toBeGreaterThanOrEqual(3);
});
});