Archilyzer · Source

archilyzer

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

commit 314b4ed2b5a260c58bd884a37b36467f8dffecf8
parent 3cc3f46f5218c55c6024797de9fce7d0490eace1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 03:40:33 -0400

hub: an archive the visitor added wears its published hex fitted to each base — card, scope chip, result stripe

useHubSites maps every external archive's accent through fittedHex
(perBaseColor(resolveAccent(hex)), common/lib/siteColor.ts), so the card,
the chip's dot, the search stripe and the channel group all get the fitted
value; a value that is not a #rrggbb is dropped (never into a style) and the
card falls back to the family's colour. siteColor uses the same helper.
perBaseColor now throws a RangeError on anything but a #rrggbb per base
instead of painting rgb(NaN …), and says the value is CSS only (review nit).
Unit: fittedHex, the input check. e2e-hub official-instances: a stored
pale #f4c2d7 archive's stripe and chip dot per base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/lib/siteColor.test.ts | 23++++++++++++++++++++++-
Mcommon/lib/siteColor.ts | 21+++++++++++++++++++--
Mexport/app/components/hub/ArchiveShelf.tsx | 7++++---
Mexport/app/components/hub/useHubSites.ts | 11+++++++++--
Mexport/e2e-hub/official-instances.spec.ts | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 123 insertions(+), 8 deletions(-)

diff --git a/common/lib/siteColor.test.ts b/common/lib/siteColor.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { ACCENT_CHART_SLOT, perBaseColor, siteChartColors, siteColor } from "./siteColor"; +import { ACCENT_CHART_SLOT, fittedHex, perBaseColor, siteChartColors, siteColor } from "./siteColor"; import { CHART_SLOTS, seriesColor } from "./homepageChart"; import { resolveAccent } from "./accent"; import { ACCENTS, ACCENT_IDS, BASE_GROUNDS, BASE_GROUND_IDS, contrastRatio, type BaseGround } from "./brand"; @@ -67,6 +67,27 @@ test("perBaseColor: one value, each base's colour; the light one with no token s assert.doesNotMatch(css, /["';]|url\(/); }); +test("perBaseColor: anything but a #rrggbb per base throws, never paints rgb(NaN …)", () => { + const ok = { light: "#010203", sepia: "#a0b0c0", dark: "#ffeedd" }; + for (const bad of ["#fff", "red", "", "var(--brand)", "#12345g", "#1234567"]) { + assert.throws(() => perBaseColor({ ...ok, sepia: bad }), RangeError, bad); + } + assert.throws(() => perBaseColor({ light: "#010203" } as never), /dark|sepia/); +}); + +test("fittedHex: a published hex fitted per base; anything else is undefined", () => { + assert.equal(fittedHex("#F4C2D7"), perBaseColor(resolveAccent("#f4c2d7"))); + // A named accent's published on-dark hex, with no id beside it (a visitor's + // added archive): fitted like any custom hex, readable on every ground. + const brass = fittedHex(ACCENTS.brass.onDark)!; + for (const base of BASE_GROUND_IDS) { + assert.ok(contrastRatio(paint(brass, base), BASE_GROUNDS[base]) >= 4.5, base); + } + for (const v of [undefined, "", "red", "url(https://x.example/p.png)", "var(--brand)", 7]) { + assert.equal(fittedHex(v), undefined, String(v)); + } +}); + test("siteColor: no accent is the site's chart colour", () => { assert.equal(siteColor({}, seriesColor(0)), "var(--chart-1)"); assert.equal(siteColor({}, "var(--chart-4)"), "var(--chart-4)"); diff --git a/common/lib/siteColor.ts b/common/lib/siteColor.ts @@ -94,8 +94,18 @@ export function siteChartColors(sites: readonly { accentId?: string }[]): string // rgb() for whichever base is in force, switching with it. It goes anywhere a // colour goes (a background, a border, color-mix()); the fallbacks paint the // light value on a page with no token sheet, as `:root` does. +// +// The value is CSS only: never parse it or compare it as a hex. Each value +// must be a `#rrggbb` (resolveAccent's output); anything else THROWS rather +// than paint `rgb(NaN …)`, which a browser drops without a word. A fourth +// base needs its own flag in tokens.css (themeTokens.test.ts holds exactly one +// 1 per base, over the same BASE_GROUND_IDS this walks). +const RRGGBB = /^#[0-9a-f]{6}$/i; export function perBaseColor(values: Readonly<Record<BaseGround, string>>): string { const rgb = BASE_GROUND_IDS.map((b) => { + if (!RRGGBB.test(values[b] ?? "")) { + throw new RangeError(`perBaseColor: ${b} is ${JSON.stringify(values[b])}, not a #rrggbb`); + } const n = parseInt(values[b].slice(1), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; }); @@ -106,9 +116,16 @@ export function perBaseColor(values: Readonly<Record<BaseGround, string>>): stri return `rgb(${channel(0)} ${channel(1)} ${channel(2)})`; } +// A published hex (a site's own, or a named accent's on-dark value when its +// id is not known) fitted to each base, as one CSS value; undefined for +// anything that is not a `#rrggbb`, which never reaches a style. +export function fittedHex(accent: unknown): string | undefined { + const hex = parseAccent(accent); + return hex ? perBaseColor(resolveAccent(hex)) : undefined; +} + // A site's mark colour; `chart` is its siteChartColors entry. export function siteColor(site: SiteColorSource, chart: string): string { if (isAccentId(site.accentId)) return `var(--swatch-${site.accentId})`; - const hex = parseAccent(site.accent); - return hex ? perBaseColor(resolveAccent(hex)) : chart; + return fittedHex(site.accent) ?? chart; } diff --git a/export/app/components/hub/ArchiveShelf.tsx b/export/app/components/hub/ArchiveShelf.tsx @@ -24,7 +24,8 @@ // useHubSites' (officialInstances), shared with the scope chips, the results' // stripes and /ask, so an archive is one colour on every surface. With no // summary the order is hub-sites.json's and the colour the card's place. An -// added archive with no accent wears the family's signal colour. +// added archive wears its own published hex fitted to the base in force +// (useHubSites, release 11 O2b), or, with none, the family's signal colour. import { X } from "lucide-react"; import { Badge } from "yt-dlp-transcript-common/components/ui/badge"; @@ -56,8 +57,8 @@ function ArchiveCard({ onRemove, }: { site: RegisteredSite; - // The stripe's colour: an official card's from useHubSites, an added one's - // own accent or the family's signal colour. + // The stripe's colour, from useHubSites: an official card's, or an added + // one's own hex fitted per base, or the family's signal colour. accent: string; // Official: the build-time figures (null when the summary lacks them). // Added: undefined — the card shows the descriptor's channel count only. diff --git a/export/app/components/hub/useHubSites.ts b/export/app/components/hub/useHubSites.ts @@ -12,7 +12,11 @@ // and reordered a moment later. Both files are small, same-origin and // requested together, so the wait is the gap between two requests already in // flight. The archives a visitor added follow, in the order they were added, -// each in its own accent or none. +// each in its own accent or none: the hex its /site.json published (a named +// accent's on-dark value, or the site's own), FITTED to each base like any +// custom hex (lib/siteColor.ts fittedHex, release 11 O2b) — so a pale one reads +// on light and sepia on its card, chip and result stripe. A value that is not +// a hex is dropped, never passed into a style. // // `listed` says the list is the hub's WHOLE list: `/hub-sites.json` has been // answered (siteRegistry's `builtinsLoaded`) and the summary has settled. Until @@ -27,6 +31,7 @@ import { } from "yt-dlp-transcript-common/components/siteRegistry"; import type { FederatedSite } from "yt-dlp-transcript-common/components/SearchDataContext"; import { officialInstances } from "yt-dlp-transcript-common/lib/hubSummary"; +import { fittedHex } from "yt-dlp-transcript-common/lib/siteColor"; import { useHubScope } from "./useHubScope"; import { useHubSummary } from "./useHubSummary"; @@ -41,7 +46,9 @@ export function useHubSites() { summary, ) : []; - const added = sites.filter((s) => s.kind === "external"); + const added = sites + .filter((s) => s.kind === "external") + .map((s) => ({ ...s, accent: fittedHex(s.accent) })); // Every archive, in the one order, each official one wearing its colour. const all: RegisteredSite[] = [ ...official.map((o) => ({ ...o.site, accent: o.accent })), diff --git a/export/e2e-hub/official-instances.spec.ts b/export/e2e-hub/official-instances.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page, type Route } from "@playwright/test"; +import { resolveAccent } from "../../common/lib/accent"; import { ACCENTS } from "../../common/lib/brand"; // The hub's "Official Instances": one card per built-in member @@ -187,4 +188,72 @@ test.describe("hub official instances", () => { .toBe(rgb(base === "light" ? ACCENTS.brass.onLight : ACCENTS.brass.onSepia)); } }); + + // Release 11 (O2b): an archive the VISITOR added wears the hex its + // /site.json published, fitted to the base in force — on its card and on its + // scope chip — like any custom hex. A pale one used to be painted as is: + // ~1.4:1 on the light and sepia grounds. + test("an added archive's own hex is fitted to each base, on its card and its chip", async ({ + page, + }) => { + const ADDED = "http://localhost:4597"; + const PALE = "#f4c2d7"; + await page.route("**/hub-sites.json", (r) => fulfillJson(r, [])); + await page.route("**/hub-summary.json", (r) => r.fulfill({ status: 404, body: "" })); + await page.route(`${ADDED}/**`, (r) => + r.fulfill({ status: 404, headers: { "access-control-allow-origin": "*" }, body: "" }), + ); + // As the registry stores an archive added through the form. + await page.addInitScript( + ([origin, accent]) => { + if (localStorage.getItem("ytdlp-tb:hub-sites")) return; + localStorage.setItem( + "ytdlp-tb:hub-sites", + JSON.stringify([ + { + origin, + siteId: "added", + siteTitle: "Added Archive", + accent, + pwa: false, + contract: 1, + addedAt: 1, + }, + ]), + ); + }, + [ADDED, PALE], + ); + const stripe = page + .getByTestId("shelf-spine") + .filter({ hasText: "Added Archive" }) + .locator(":scope > span[aria-hidden='true']"); + const dot = page + .getByTestId(`hub-scope-chip-${ADDED}`) + .locator("button[aria-pressed] > span.rounded-full"); + const colours = async () => ({ + stripe: await stripe.evaluate((el) => getComputedStyle(el).backgroundColor), + dot: await dot.evaluate((el) => getComputedStyle(el).backgroundColor), + }); + const rgb = (hex: string) => + `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`; + const fitted = resolveAccent(PALE); + // Fitted, not as published, where the ground needs it. + expect(fitted.light).not.toBe(PALE); + expect(fitted.sepia).not.toBe(PALE); + + await page.goto("/"); + await expect( + page.getByRole("heading", { level: 2, name: "Archives You Added", exact: true }), + ).toBeVisible(); + for (const base of ["dark", "light", "sepia"] as const) { + if (base !== "dark") { + await page.evaluate((b) => localStorage.setItem("ytdlp-tb:base", b), base); + await page.reload(); + } + await expect(page.locator("html")).toHaveAttribute("data-base", base); + const want = rgb(fitted[base]); + await expect.poll(colours, { message: base }).toEqual({ stripe: want, dot: want }); + } + }); });