Archilyzer · Source

archilyzer

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

commit 43a950af8402102fa2f1dab681a4f38d98cb7cd0
parent e5b783d46c528ffcbd4f22366fd78fc0c01f23c4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 20:49:43 -0400

homepage: the social row's scroll box and the compact nav are not tab stops of their own; the toggle's no-flash test records every accent change (final review F1, F5)

Firefox 146 made the overflowing scroll box (and the compact nav's rule,
which overflows below 320 px) an extra, unnamed stop; tabIndex={-1} on both,
the links inside stay the stops. The stored-accent test now watches
data-accent with a MutationObserver installed before the page's first
script.

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

Diffstat:
Mcommon/components/SocialScroll.tsx | 5+++++
Mhomepage/app/components/Header.tsx | 7+++++--
Mhomepage/e2e/social.spec.ts | 4++++
Mhomepage/e2e/toggle.spec.ts | 26++++++++++++++++++++++----
Rscripts/umtool-build-trace.test.mjs -> scripts/next-build-trace.test.mjs | 0
5 files changed, 36 insertions(+), 6 deletions(-)

diff --git a/common/components/SocialScroll.tsx b/common/components/SocialScroll.tsx @@ -16,6 +16,10 @@ import { cn } from "../lib/utils"; // (`nearest`, inside the 4 px `scroll-padding`, so its ring shows). Chromium // does not bring a partly hidden link into view on focus inside an rtl box. // +// `tabIndex={-1}`: Firefox makes a scroll container that overflows a tab stop +// of its own, with no name. The links inside are the stops, and focus scrolls +// them into view, so the box itself is taken out of the order. +// // Put the row's own 4 px inline padding on the row (SocialLinks' className, // `px-1`) — the padding must be inside the scrolled content to be reachable — // and this box cancels it with `-mx-1`, so the key after the box (the header's @@ -30,6 +34,7 @@ export function SocialScroll({ return ( <div data-social-scroll="" + tabIndex={-1} className={cn( "min-w-0 overflow-x-auto [direction:rtl] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden scroll-px-1 -mx-1 -my-1 py-1", className, diff --git a/homepage/app/components/Header.tsx b/homepage/app/components/Header.tsx @@ -125,10 +125,13 @@ export default function Header() { fit a 320 px rule. Only one of the two navs is ever in the accessibility tree (the other is display:none), but they carry distinct labels so a test or a screen reader can never conflate them. - `overflow-x-auto` only guards a larger text setting; nothing scrolls - at the default size. */} + `overflow-x-auto` only guards a larger text setting or a screen + under 320 px; nothing scrolls at the default size from 320 px. + `tabIndex={-1}`: Firefox makes a scroll container that overflows a + tab stop of its own; the links are the stops (as SocialScroll). */} <nav aria-label="Main, compact" + tabIndex={-1} className="md:hidden border-t border-[var(--border)] overflow-x-auto" > <NavList className="gap-5 px-5 sm:px-6 h-10" /> diff --git a/homepage/e2e/social.spec.ts b/homepage/e2e/social.spec.ts @@ -245,6 +245,10 @@ test.describe("under a coarse pointer", () => { await page.goto("/"); const box = scrollBox(page); expect(await box.evaluate((el) => el.scrollWidth > el.clientWidth + 1)).toBe(true); + // Not tab stops of their own (Firefox makes an overflowing scroll box one): + // the row's box, and the compact nav's rule, which overflows here too. + await expect(box).toHaveAttribute("tabindex", "-1"); + await expect(page.getByRole("navigation", { name: "Main, compact" })).toHaveAttribute("tabindex", "-1"); await noHorizontalOverflow(page); await expect(themeToggle(page)).toBeInViewport({ ratio: 1 }); const inBox = (key: import("@playwright/test").Locator) => diff --git a/homepage/e2e/toggle.spec.ts b/homepage/e2e/toggle.spec.ts @@ -103,7 +103,9 @@ test("the toggle's focus: the ring, and in forced colours the browser's own outl // With no accent control on the homepage, a stored accent (set on this origin // by an earlier build that had one) must not tint it: the pre-paint script -// and the provider both ignore it, and it stays in storage untouched. +// and the provider both ignore it, and it stays in storage untouched. Every +// value `data-accent` ever holds is recorded by a MutationObserver installed +// before the page's first script, so a flash between two samples cannot pass. test("a stored accent is ignored: the homepage keeps its own, with no flash, and the stored value stays", async ({ page, }) => { @@ -111,14 +113,30 @@ test("a stored accent is ignored: the homepage keeps its own, with no flash, and try { localStorage.setItem("ytdlp-tb:accent", "violet"); } catch {} + const held: (string | null)[] = []; + (window as unknown as { __accents: (string | null)[] }).__accents = held; + new MutationObserver((records) => { + for (const r of records) { + if (r.target === document.documentElement) held.push(r.oldValue); + } + }).observe(document, { + subtree: true, + attributes: true, + attributeFilter: ["data-accent"], + attributeOldValue: true, + }); }); await page.goto("/", { waitUntil: "commit" }); await page.waitForFunction(() => document.documentElement?.dataset.themeReady === "1"); - // Before hydration: the pre-paint script's work only. - expect((await htmlState(page)).accent).toBe("signal"); await page.waitForLoadState("load"); await expect.poll(async () => (await htmlState(page)).accent).toBe("signal"); await page.waitForTimeout(300); - expect((await htmlState(page)).accent).toBe("signal"); + const held = await page.evaluate(() => [ + ...(window as unknown as { __accents: (string | null)[] }).__accents, + document.documentElement.getAttribute("data-accent"), + ]); + // Every value it held before each change, and the one it holds now. + expect(held.filter((v) => v !== null && v !== "signal"), JSON.stringify(held)).toEqual([]); + expect(held.at(-1)).toBe("signal"); expect(await page.evaluate(() => localStorage.getItem("ytdlp-tb:accent"))).toBe("violet"); }); diff --git a/scripts/umtool-build-trace.test.mjs b/scripts/next-build-trace.test.mjs