Archilyzer · Source

archilyzer

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

commit 0f7fa1d1db2b072ddcf26c27141a4c1cc510f9ea
parent 5c1e38ab0ebbf8986154c398162ec3c6643cafca
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 27 Sep 2026 00:19:22 -0400

homepage: the Official Instances cards wear each site's accent; the chart keeps its palette

The operator's ask (2026-09-27): each card's left stripe is its site's accent.
ArchiveCards colours through siteColor — a named accent at its value on the
base in force, a custom hex as published, no accent the chart colour it wore.

The growth chart and its legend stay on seriesColor. The accents were
validated as its palette first (dataviz validator, all pairs, every base):
with Brass and Blue fixed, no five pass (Brass-Vermilion dE 1.0 deutan,
Blue-Violet 8.5 normal / 1.3 protan on dark, Brass-Green 1.8 protan), so a
card is no longer its layer's legend; the chart's own legend is. Both header
comments say so. /stats' SiteGrid is its chart's legend and is unchanged.

e2e: the dev server reads a copy of the summary on disk with fixture accents
on its first sites (HOMEPAGE_SUMMARY_FILE, set only by playwright.config.ts;
e2e/fixture-accents.ts): Brass, a custom hex, then none. instance-colours.spec
checks every stripe on dark, light and sepia — Brass at each base's swatch, the
hex as is, an unaccented card equal to its legend swatch — and the legend on
--chart-1..5.

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

Diffstat:
M.gitignore | 2++
Mhomepage/app/components/ArchiveCards.tsx | 25++++++++++++++++++-------
Mhomepage/app/components/ArchiveGrowthChart.tsx | 16++++++++++++++--
Mhomepage/app/lib/summary.ts | 7++++++-
Ahomepage/e2e/fixture-accents.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Ahomepage/e2e/instance-colours.spec.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhomepage/playwright.config.ts | 16++++++++++++++++
7 files changed, 196 insertions(+), 10 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -126,6 +126,8 @@ yarn-error.log* /homepage/test-results/ /homepage/playwright-report/ /homepage/out/ +# the e2e's copy of the summary with fixture accents (homepage/e2e/fixture-accents.ts) +/homepage/e2e/.e2e-summary.json # site config /settings.json diff --git a/homepage/app/components/ArchiveCards.tsx b/homepage/app/components/ArchiveCards.tsx @@ -1,15 +1,26 @@ import type { HomepageSummarySite } from "yt-dlp-transcript-common/lib/homepageSummary"; -import { seriesColor } from "yt-dlp-transcript-common/lib/homepageChart"; +import { siteColor } from "yt-dlp-transcript-common/lib/siteColor"; // The official instances: one card per public archive, its title linking out, // its own numbers underneath. No description line under the title — the // page's copy rule is no subtitles unless needed, and the name plus the -// numbers say what the archive is. The stripe is seriesColor(i), the same index the -// growth chart colours that instance's layer with, so a card is its own legend. +// numbers say what the archive is. // -// A site's `accent` is deliberately NOT used here any more: the chart cannot -// wear a per-site brand hex (it would break the validated order and dark mode), -// and a card whose stripe disagrees with its layer is worse than no stripe. +// THE STRIPE IS THE SITE'S OWN ACCENT (release 10, the operator's ask): +// siteColor() — a named accent as `var(--swatch-<id>)`, its value on the base +// in force, from the summary's `accentId`; a custom hex as published; a site +// with no accent seriesColor(i), its chart layer's colour. The hub's official +// cards wear the same (lib/hubSummary.ts officialInstances). +// +// SO A CARD IS NO LONGER THE GROWTH CHART'S LEGEND. The chart stays on +// seriesColor and carries its own legend (ArchiveGrowthChart), because the +// accents fail as a chart palette: validated with the dataviz skill's +// validator over every pair on every base, no set of five that includes the +// operator's Brass (Jeralyzer) and Blue (Hasanalyzer) passes — Brass↔Vermilion +// ΔE 1.0 (deutan), Blue↔Violet ΔE 8.5 normal / 1.3 protan on dark, Brass↔Green +// 1.8 (protan); the least-bad set still fails three pairs. Here each card names +// its site in text, so the colour only has to be the site's, not tell the +// sites apart. // // Numbers are v5 fields; a pre-v5 summary lacks them and the card shows only // the transcript count it always had. @@ -38,7 +49,7 @@ export function ArchiveCards({ sites }: { sites: HomepageSummarySite[] }) { <span aria-hidden="true" className="absolute left-0 top-0 h-full w-[3px]" - style={{ backgroundColor: seriesColor(i) }} + style={{ backgroundColor: siteColor(site, i) }} /> <h3 className="font-display text-lg font-semibold text-[var(--foreground)]"> <a diff --git a/homepage/app/components/ArchiveGrowthChart.tsx b/homepage/app/components/ArchiveGrowthChart.tsx @@ -27,13 +27,25 @@ import { // the stack reads as strata, which is what it is. // // COLOUR follows the instance, never its rank on this chart: seriesColor(i) with -// i = the site's position in `sites`, the same index the instance cards use. -// The layers stack in that same order (0..4): the per-base --chart-1..5 +// i = the site's position in `sites`, and the legend above the plot uses the +// same. The layers stack in that same order (0..4): the per-base --chart-1..5 // palette (tokens.css: blue, green, violet, amber, magenta) is validated for // ADJACENT pairs in exactly this order, so every touching pair clears the // dataviz validator on every base. It used to be interleaved (0, 2, 1, 4, 3) // for the old family palette, whose first two slots were too close; with the // new order that interleave put green against magenta, in the CVD floor band. +// +// NOT THE SITES' ACCENTS, although the instance cards below now wear them +// (ArchiveCards, release 10) — so this legend, not a card, says which layer is +// which. The accents were validated as this chart's palette first (the dataviz +// skill's validator, ALL pairs, since the order moves with transcript counts; +// each base's own swatch values on its --chart-surface): with the operator's +// Brass (Jeralyzer) and Blue (Hasanalyzer) fixed, no five pass. Brass fails +// with Vermilion (ΔE 1.0 deutan), Green (1.8 protan) and Signal; Blue with +// Violet (8.5 normal, 1.3 protan on dark) and Signal; only Sakura passes with +// both. The least-bad set still fails three pairs, and on dark every accent is +// above the band (L 0.70–0.79 > 0.67): they are text colours, not a categorical +// palette. const W = 1000; const H = 300; diff --git a/homepage/app/lib/summary.ts b/homepage/app/lib/summary.ts @@ -16,9 +16,14 @@ import type { HomepageSummary } from "yt-dlp-transcript-common/lib/homepageSumma // Read with `readFileSync` at module scope of a server component, mirroring // export/app/changelog/page.tsx. `output: "export"` means this runs at build, // never at request time. +// +// HOMEPAGE_SUMMARY_FILE names another file to read instead. Only the e2e sets +// it, to a copy of this one with fixture accents (playwright.config.ts). export function loadSummary(): HomepageSummary | null { try { - const file = path.join(process.cwd(), "public", "homepage-summary.json"); + const file = + process.env.HOMEPAGE_SUMMARY_FILE || + path.join(process.cwd(), "public", "homepage-summary.json"); const parsed = JSON.parse( fs.readFileSync(file, "utf8"), ) as HomepageSummary; diff --git a/homepage/e2e/fixture-accents.ts b/homepage/e2e/fixture-accents.ts @@ -0,0 +1,43 @@ +import fs from "node:fs"; +import { ACCENTS } from "../../common/lib/brand"; + +// The instance cards' colours (release 10) need sites with known accents, and +// the summary on disk is the operator's: its sites may set none at all. So the +// e2e dev server reads a COPY of it (HOMEPAGE_SUMMARY_FILE, app/lib/summary.ts) +// with these accents laid over its first sites — a named accent, a custom hex, +// then no accent — and every other number untouched. playwright.config.ts +// writes it before the server starts; the specs import the same constants. + +// In the summary's order: site 0, site 1; every later site has none. +export const FIXTURE_ACCENTS = [ + { accent: ACCENTS.brass.onDark, accentId: "brass" as const }, + { accent: "#cc3366" }, +] as const; + +export const FIXTURE_SUMMARY_NAME = ".e2e-summary.json"; + +// Write the fixture copy of `source` to `dest`. False (and any stale copy +// removed) when there is no readable summary with sites: a source-only +// checkout, whose data specs skip. The write is a rename, because Playwright +// evaluates its config again in every worker it starts, while the dev server +// may be reading the file. +export function writeFixtureSummary(source: string, dest: string): boolean { + let summary: { sites?: Array<Record<string, unknown>> } | null = null; + try { + summary = JSON.parse(fs.readFileSync(source, "utf8")); + } catch { + summary = null; + } + if (!Array.isArray(summary?.sites) || summary.sites.length === 0) { + fs.rmSync(dest, { force: true }); + return false; + } + summary.sites = summary.sites.map((site, i) => { + const { accent: _a, accentId: _id, ...rest } = site; + return { ...rest, ...(FIXTURE_ACCENTS[i] ?? {}) }; + }); + const tmp = `${dest}.${process.pid}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(summary)); + fs.renameSync(tmp, dest); + return true; +} diff --git a/homepage/e2e/instance-colours.spec.ts b/homepage/e2e/instance-colours.spec.ts @@ -0,0 +1,97 @@ +import fs from "node:fs"; +import path from "node:path"; +import { test, expect, type Page } from "@playwright/test"; +import { ACCENTS } from "../../common/lib/brand"; +import { FIXTURE_ACCENTS, FIXTURE_SUMMARY_NAME } from "./fixture-accents"; + +// The Official Instances cards wear their site's accent (release 10): a named +// accent at its value on the base in force (`var(--swatch-<id>)`), a custom hex +// as published, and a site with no accent its growth-chart colour. The chart +// itself keeps its validated palette (seriesColor), so only an unaccented card +// still matches its layer's legend swatch. The dev server reads the summary on +// disk with fixture accents laid over its first sites (fixture-accents.ts): +// site 0 Brass, site 1 the custom #cc3366, the rest none. + +const BASE_KEY = "ytdlp-tb:base"; + +function fixtureSites(): unknown[] | null { + try { + const file = path.resolve(test.info().project.testDir, FIXTURE_SUMMARY_NAME); + const s = JSON.parse(fs.readFileSync(file, "utf8")); + return Array.isArray(s?.sites) && s.sites.length ? s.sites : null; + } catch { + return null; + } +} + +// The colour a CSS colour resolves to on this page, as the browser reports it. +const resolve = (page: Page, css: string) => + page.evaluate((c) => { + const el = document.createElement("span"); + el.style.backgroundColor = c; + document.body.append(el); + const out = getComputedStyle(el).backgroundColor; + el.remove(); + return out; + }, css); + +async function colours(page: Page) { + const section = page.locator("section").filter({ + has: page.getByRole("heading", { level: 2, name: "Official Instances", exact: true }), + }); + const stripes = await section + .locator("li > span[aria-hidden='true']") + .evaluateAll((els) => els.map((el) => getComputedStyle(el).backgroundColor)); + const legend = await page + .locator("figure ul[aria-hidden='true'] li > span") + .evaluateAll((els) => els.map((el) => getComputedStyle(el).backgroundColor)); + return { stripes, legend }; +} + +async function useBase(page: Page, base: "dark" | "light" | "sepia") { + await page.evaluate(([k, v]) => localStorage.setItem(k, v), [BASE_KEY, base]); + await page.reload(); + await expect(page.locator("html")).toHaveAttribute("data-base", base); +} + +test("each instance card wears its site's accent on every base; the chart keeps its palette", async ({ + page, +}) => { + const sites = fixtureSites(); + test.skip(!sites, "no summary on disk — the page has no instance cards"); + const n = sites!.length; + await page.goto("/"); + + const onBase = { + dark: ACCENTS.brass.onDark, + light: ACCENTS.brass.onLight, + sepia: ACCENTS.brass.onSepia, + } as const; + for (const base of ["dark", "light", "sepia"] as const) { + await useBase(page, base); + const { stripes, legend } = await colours(page); + expect(stripes).toHaveLength(n); + + // Site 0: the NAMED accent, at this base's value — not the published + // on-dark hex on every base. + expect(FIXTURE_ACCENTS[0].accentId).toBe("brass"); + expect(stripes[0]).toBe(await resolve(page, onBase[base])); + // Site 1 (when there is one): the custom hex as published. + if (n > 1) expect(stripes[1]).toBe(await resolve(page, FIXTURE_ACCENTS[1].accent)); + // Every other site has no accent: its chart colour, the same as its + // legend swatch. + for (let i = 2; i < n; i++) { + expect(stripes[i], `site ${i}`).toBe(await resolve(page, `var(--chart-${i + 1})`)); + if (legend.length) expect(stripes[i], `site ${i} vs legend`).toBe(legend[i]); + } + + // The chart's legend stays on the validated palette, accent or not. + if (legend.length) { + expect(legend).toHaveLength(n); + for (let i = 0; i < Math.min(n, 5); i++) { + expect(legend[i], `legend ${i}`).toBe(await resolve(page, `var(--chart-${i + 1})`)); + } + expect(legend[0]).not.toBe(stripes[0]); + } + } +}); diff --git a/homepage/playwright.config.ts b/homepage/playwright.config.ts @@ -1,4 +1,9 @@ +import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; +import { + FIXTURE_SUMMARY_NAME, + writeFixtureSummary, +} from "./e2e/fixture-accents"; // Homepage e2e. Runs against `next dev` (default mode) so it reflects uncommitted // source — /stats/ reads its build-time summary from public/ on disk, which is @@ -11,6 +16,16 @@ import { defineConfig, devices } from "@playwright/test"; const PORT = Number(process.env.HOMEPAGE_E2E_PORT ?? 3040); const baseURL = `http://localhost:${PORT}`; +// The dev server reads a copy of the summary on disk with fixture accents on +// its first sites (e2e/fixture-accents.ts), so the instance cards' colours are +// exact whatever the operator's sites set. Gitignored; rewritten every run. No +// summary on disk, no copy: the server reads public/ as before and finds none. +const FIXTURE_SUMMARY = path.resolve(process.cwd(), "e2e", FIXTURE_SUMMARY_NAME); +const hasFixtureSummary = writeFixtureSummary( + path.resolve(process.cwd(), "public", "homepage-summary.json"), + FIXTURE_SUMMARY, +); + export default defineConfig({ testDir: "./e2e", timeout: 30_000, @@ -24,6 +39,7 @@ export default defineConfig({ url: baseURL, timeout: 120_000, reuseExistingServer: !process.env.CI, + env: hasFixtureSummary ? { HOMEPAGE_SUMMARY_FILE: FIXTURE_SUMMARY } : {}, }, use: { baseURL,