Archilyzer · Source

archilyzer

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

commit 484f066b2bb9947d61a122f19570fa11689327bb
parent a4afb4d3178a41de274b9a29c1d8ae3a18302972
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu, 17 Sep 2026 20:37:29 -0400

Merge storage/locations-s0: the Advanced menu can leave a dimmed row

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

Diffstat:
Meditor/app/channels/components/ChannelsTable.tsx | 54+++++++++++++++++++++++++++++++-----------------------
Meditor/e2e/channel-priority.spec.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+), 23 deletions(-)

diff --git a/editor/app/channels/components/ChannelsTable.tsx b/editor/app/channels/components/ChannelsTable.tsx @@ -595,22 +595,27 @@ function ChannelTableRow({ // through the pinned identity column while the rest scrolls. const stickyBg = selected ? "bg-accent" : "bg-background"; const bridge = "bg-surface w-24 min-w-20"; + // Dimmed for the two things that take the row out of a pipeline: it is + // excluded from the export build, or its base tier is Paused. (The sync + // exclusion flag that used to dim it is now a `sync` pin, which is a + // per-operation fact and not a property of the whole row.) + // + // THE DIM IS PER CELL, NEVER ON THE `<tr>`. `opacity` below 1 creates a + // STACKING CONTEXT, and a stacking context confines every positioned + // descendant to it: put `opacity-60` on the row and the Tier cell's + // `absolute z-30` Advanced popover (ChannelTierSelect) can no longer paint + // above the rows that follow, however high its z-index — every later row + // draws over it and swallows the clicks. So the Tier cell — the one that + // hosts the popover — is the one cell that is NOT dimmed. Moving this back + // onto the row re-breaks the menu (operator report 2026-09-17). + const dim = + c.config.excludeFromBuild || c.priority.tier === "paused" + ? " opacity-60" + : ""; return ( - <tr - className={ - "border-t border-border " + - (selected ? "bg-accent " : "") + - // Dimmed for the two things that take the row out of a pipeline: it is - // excluded from the export build, or its base tier is Paused. (The sync - // exclusion flag that used to dim it is now a `sync` pin, which is a - // per-operation fact and not a property of the whole row.) - (c.config.excludeFromBuild || c.priority.tier === "paused" - ? "opacity-60" - : "") - } - > + <tr className={"border-t border-border " + (selected ? "bg-accent " : "")}> <td - className={`sticky left-0 z-10 w-9 px-2 py-1.5 align-middle ${stickyBg}`} + className={`sticky left-0 z-10 w-9 px-2 py-1.5 align-middle ${stickyBg}${dim}`} > <input type="checkbox" @@ -621,7 +626,7 @@ function ChannelTableRow({ /> </td> <Td - className={`sticky left-8 z-20 whitespace-nowrap font-mono shadow-[1px_0_0_var(--color-border)] ${stickyBg}`} + className={`sticky left-8 z-20 whitespace-nowrap font-mono shadow-[1px_0_0_var(--color-border)] ${stickyBg}${dim}`} > <span className="inline-flex items-center gap-1.5"> <Link @@ -633,11 +638,11 @@ function ChannelTableRow({ <MediaLocationBadge media={c.media} compact /> </span> </Td> - <Td className="max-w-48 truncate">{c.config.name ?? ""}</Td> - <Td className="font-mono text-[11px] text-muted-foreground"> + <Td className={`max-w-48 truncate${dim}`}>{c.config.name ?? ""}</Td> + <Td className={`font-mono text-[11px] text-muted-foreground${dim}`}> {c.config.handling} </Td> - <Td> + <Td className={dim}> <ChannelBuildToggle slug={c.slug} excluded={c.config.excludeFromBuild === true} @@ -653,19 +658,21 @@ function ChannelTableRow({ /> </Td> <Td - className="text-right tabular-nums" + className={`text-right tabular-nums${dim}`} ariaLabel={`playlist count for ${c.slug}`} > {c.playlistCount ?? "—"} </Td> - <Td className="whitespace-nowrap text-xs tabular-nums text-muted-foreground"> + <Td + className={`whitespace-nowrap text-xs tabular-nums text-muted-foreground${dim}`} + > {formatStamp(c.config.lastSyncedAt)} </Td> <Td ariaLabel={`report age for ${c.slug}`} className={`whitespace-nowrap text-xs tabular-nums ${ c.report.state === "current" ? "text-muted-foreground" : "text-warning" - }`} + }${dim}`} > {c.report.state === "current" ? formatStamp(c.report.generatedAt) @@ -680,7 +687,8 @@ function ChannelTableRow({ className={ bridge + (i === 0 ? " border-l border-border" : "") + - (i === columns.length - 1 ? " border-r border-border" : "") + (i === columns.length - 1 ? " border-r border-border" : "") + + dim } count={ col.id === "download" @@ -691,7 +699,7 @@ function ChannelTableRow({ } /> ))} - <Td className="whitespace-nowrap"> + <Td className={`whitespace-nowrap${dim}`}> <div className="flex items-center gap-2"> <ChannelSyncButton slug={c.slug} disabled={!c.config.url} /> <ChannelAvailabilityButton slug={c.slug} disabled={!c.config.url} /> diff --git a/editor/e2e/channel-priority.spec.ts b/editor/e2e/channel-priority.spec.ts @@ -145,6 +145,69 @@ test("the Sync only preset is paused everywhere with sync pinned back", async ({ } }); +// THE ADVANCED MENU HAS TO ESCAPE ITS OWN ROW. +// +// A row is dimmed when it is excluded from the build or its base tier is +// Paused, and the dim used to sit on the `<tr>` as `opacity-60`. Opacity below +// 1 makes the row a stacking context painted as one group, so the panel's +// `absolute z-30` no longer lifts it out of the row: the NEXT dimmed row is a +// later group at the same level and paints straight over the menu (operator +// report 2026-09-17, on a corpus where paused rows sit next to each other). +// Two paused rows in a row is the smallest shape that reproduces it; one alone +// does not, because an opacity group still paints above the plain rows below. +test("a paused row's Advanced menu draws above the paused row below it", async ({ + page, +}) => { + await resetData("two-slow-channels"); + await page.goto(ALL); + + // slow-a is the FIRST row and slow-b the one its panel hangs over. The + // assertion is meaningless on the last row of the table, which has nothing + // after it to be covered by. + const slugs = await page + .locator('[aria-label^="select slow-"]') + .evaluateAll((els) => + els.map((el) => el.getAttribute("aria-label")!.replace("select ", "")), + ); + expect(slugs).toEqual(["slow-a", "slow-b"]); + + await page.getByLabel("select all channels").check(); + await page.getByLabel("bulk tier").selectOption("paused"); + await page + .getByLabel("channel priority bulk") + .getByRole("button", { name: "Apply tier" }) + .click(); + await expect + .poll(async () => (await settings()).channelPriority?.channels) + .toEqual({ "slow-a": { tier: "paused" }, "slow-b": { tier: "paused" } }); + // Reloaded, so both rows render dimmed from the server's own tiers rather + // than from the controls' optimistic state. + await page.reload(); + + await page.getByLabel("advanced priority for slow-a").click(); + // `sync` is the first control in the panel, the part that hangs over slow-b. + const override = page.getByLabel("sync override for slow-a"); + await expect(override).toBeInViewport(); + + // THE ASSERTION. `selectOption` never hit-tests, so it passes straight + // through a row painted on top of the panel; this asks the page directly who + // owns the pixel. With `opacity-60` back on the `<tr>` the answer is a cell + // of the paused slow-b row below. + const owner = await override.evaluate((el) => { + const r = el.getBoundingClientRect(); + const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); + return top === el || el.contains(top) + ? "the override" + : `${top?.tagName ?? "nothing"} ${top?.getAttribute("aria-label") ?? top?.textContent?.trim().slice(0, 40) ?? ""}`; + }); + expect(owner).toBe("the override"); + + await override.selectOption("normal"); + await expect + .poll(async () => (await settings()).channelPriority?.channels["slow-a"]) + .toEqual({ tier: "paused", overrides: { sync: "normal" } }); +}); + // MOVED HERE FROM channel-sync-toggle.spec.ts, which this slice deletes with the // control it drove. The assertion is the same one — a pool sweep skips the // excluded channel and names it in the tooltip — restated against the tier the