Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit a3a8276a06c025e9c0a34d48bf2208873937f43c
parent 9278a39fab8b8cd830652a9d9ed506a707b9ba3d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu, 24 Sep 2026 19:05:14 -0400

channels: a group header's controls stay on screen when the rack scrolls across

Audit finding (desktop/mobile grouped-scrolled): the group header is one
th spanning every column, so its name, description and five station
buttons scrolled off to the left with the table — the group's controls
out of reach exactly when the operator had scrolled across to read a
column; only "16 channels" at the far right was left.

The header's content now sits in a sticky left-2 box capped at the
region's visible width, which ChannelsRack measures into --rack-w beside
--thead-h (the ResizeObserver now watches the region too). The layers
spec asserts the Sync station is inside the region and owns its centre
after a bottom-right scroll.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/channels/components/ChannelGroupHeaderRow.tsx | 55+++++++++++++++++++++++++++++++++----------------------
Meditor/app/channels/components/ChannelsRack.tsx | 9++++++++-
Meditor/e2e/channels-rack-layers.spec.ts | 18++++++++++++++++++
3 files changed, 59 insertions(+), 23 deletions(-)

diff --git a/editor/app/channels/components/ChannelGroupHeaderRow.tsx b/editor/app/channels/components/ChannelGroupHeaderRow.tsx @@ -47,32 +47,43 @@ export function ChannelGroupHeaderRow({ `md:sticky md:top-[var(--thead-h,2.25rem)] ${RACK_LAYERS.groupHeader}` } > - <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> - <span - data-testid="group-name" - className="text-xs font-semibold uppercase tracking-wider" - > - {name} - </span> - {/* Rendered because it is true and currently invisible: a visitor to - the public site does not get this group preselected. */} - {!group.selectedByDefault && ( - <span className="text-[10px] uppercase tracking-wide text-muted-foreground border border-border rounded px-1"> - off by default + {/* THE CONTENT PINS LEFT. The th spans every column, so its name and + five stations used to scroll off to the left with the table — the + group's controls out of reach exactly when the operator had + scrolled across to read a column. Capped at the region's visible + width (`--rack-w`, measured by ChannelsRack; 100% before the first + measure) less the th's padding, so the count on the right stays + in view too. */} + <div className="sticky left-2 max-w-[calc(var(--rack-w,100%)-1rem)]"> + <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> + <span + data-testid="group-name" + className="text-xs font-semibold uppercase tracking-wider" + > + {name} </span> - )} - {/* The authored description. Written by SiteForm, parsed by + {/* Rendered because it is true and currently invisible: a visitor to + the public site does not get this group preselected. */} + {!group.selectedByDefault && ( + <span className="text-[10px] uppercase tracking-wide text-muted-foreground border border-border rounded px-1"> + off by default + </span> + )} + {/* The authored description. Written by SiteForm, parsed by parseChannelGroup, and until now rendered nowhere in the editor. */} - {group.description && ( - <span className="text-xs text-muted-foreground"> - {group.description} + {group.description && ( + <span className="text-xs text-muted-foreground"> + {group.description} + </span> + )} + <span className="ml-auto text-xs text-muted-foreground whitespace-nowrap"> + {channels.length === 1 + ? "1 channel" + : `${channels.length} channels`} </span> - )} - <span className="ml-auto text-xs text-muted-foreground whitespace-nowrap"> - {channels.length === 1 ? "1 channel" : `${channels.length} channels`} - </span> + </div> + <ChannelGroupLine section={section} siteId={siteId} /> </div> - <ChannelGroupLine section={section} siteId={siteId} /> </th> </tr> ); diff --git a/editor/app/channels/components/ChannelsRack.tsx b/editor/app/channels/components/ChannelsRack.tsx @@ -134,11 +134,18 @@ export function ChannelsRack({ const region = regionRef.current; const thead = theadRef.current; if (!region || !thead || typeof ResizeObserver === "undefined") return; - const measure = () => + // `--rack-w` is the region's visible width: a group header's content pins + // to the region's left edge and is capped at this, so its name and + // stations stay on screen however far the columns scroll across + // (ChannelGroupHeaderRow). + const measure = () => { region.style.setProperty("--thead-h", `${thead.offsetHeight}px`); + region.style.setProperty("--rack-w", `${region.clientWidth}px`); + }; measure(); const observer = new ResizeObserver(measure); observer.observe(thead); + observer.observe(region); return () => observer.disconnect(); }, []); diff --git a/editor/e2e/channels-rack-layers.spec.ts b/editor/e2e/channels-rack-layers.spec.ts @@ -103,6 +103,24 @@ test("scrolled to the bottom-right, the pinned group header paints over the pinn l.evaluate((el) => Number(getComputedStyle(el).zIndex) || 0); expect(await zOf(header)).toBeGreaterThan(await zOf(slugCell)); + // (2b) The group's controls stayed on screen: the header's content pins to + // the region's left edge, so scrolled all the way across, its Sync station + // is inside the region and a click on it lands on it. + const sync = page.getByLabel("sync group All channels"); + const syncBox = (await sync.boundingBox())!; + expect(syncBox.x).toBeGreaterThanOrEqual(regionBox.x); + expect(syncBox.x + syncBox.width).toBeLessThanOrEqual( + regionBox.x + regionBox.width, + ); + expect( + await hitsInside( + page, + sync, + syncBox.x + syncBox.width / 2, + syncBox.y + syncBox.height / 2, + ), + ).toBe(true); + // (3) The column header still owns its own centre over everything scrolling // under it. const slugTh = page.locator("th", {