Archilyzer · Source

archilyzer

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

commit c5b5c6b354c57f05d48a8f06c333f6f32aa7ef6b
parent 54cb4f2c228c680e023c141aa4b4e972c3ffc089
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 02:47:20 -0400

common: a sixth validated chart slot (--chart-6, Vermilion's rust) and a custom hex fitted per base on the family's cards

- tokens.css: --chart-6 per base (light/sepia #823c10, dark #a54a08), the
  only hue family that clears every pair with --chart-1..5 on all three bases
  (validate_palette.js, all pairs, chart surface and page ground; its worst
  pair CVD 9.1, normal 16.3). seriesColor(5) is var(--chart-6), not the golden
  angle's hue 328 beside --chart-5's magenta; vermilion → slot 6 in
  ACCENT_CHART_SLOT, so any six sites wear the six validated slots.
- --base-light|sepia|dark (1 on their own base, 0 elsewhere) and
  perBaseColor: one CSS value that is a different colour per base. siteColor
  paints a custom hex through resolveAccent's per-base fit (4.5:1 on the
  ground and with its ink), as the site's own pages do, on the homepage cards
  and the hub's official cards alike.
- REQUIRED_TOKENS gains --chart-6 (50); tests: six-slot uniqueness, the
  vermilion slot at every rank, the fitted pale hex per base, one-hot flags.

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

Diffstat:
Mcommon/components/themeConfig.ts | 1+
Mcommon/components/themeTokens.test.ts | 22+++++++++++++++++++---
Mcommon/lib/homepageChart.ts | 11+++++++----
Mcommon/lib/hubSummary.test.ts | 11++++++++---
Mcommon/lib/hubSummary.ts | 3++-
Mcommon/lib/siteColor.test.ts | 129++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcommon/lib/siteColor.ts | 62++++++++++++++++++++++++++++++++++++++++++++------------------
Mcommon/styles/tokens.css | 39++++++++++++++++++++++++++++++++++++---
Mhomepage/app/components/ArchiveCards.tsx | 17+++++++++--------
Mhomepage/app/components/ArchiveGrowthChart.tsx | 38+++++++++++++++++++++-----------------
10 files changed, 259 insertions(+), 74 deletions(-)

diff --git a/common/components/themeConfig.ts b/common/components/themeConfig.ts @@ -147,6 +147,7 @@ export const REQUIRED_TOKENS = [ "--chart-3", "--chart-4", "--chart-5", + "--chart-6", "--chart-surface", "--chart-grid", "--chart-axis", diff --git a/common/components/themeTokens.test.ts b/common/components/themeTokens.test.ts @@ -92,8 +92,19 @@ test("every base declares every required token, color-scheme and every swatch", } }); -test("REQUIRED_TOKENS is the 49 colour tokens, each once", () => { - assert.equal(REQUIRED_TOKENS.length, 49); +test("--base-light|sepia|dark are 1 on their own base and 0 on the others", () => { + // lib/siteColor.ts perBaseColor multiplies each base's channel by these, so + // exactly one may be 1 — two would add two colours' channels together. + for (const base of BASE_GROUND_IDS) { + const r = rule(BASE_SELECTOR[base]); + for (const b of BASE_GROUND_IDS) { + assert.equal(r.decls.get(`--base-${b}`), b === base ? "1" : "0", `${base} --base-${b}`); + } + } +}); + +test("REQUIRED_TOKENS is the 50 colour tokens, each once", () => { + assert.equal(REQUIRED_TOKENS.length, 50); assert.equal(new Set(REQUIRED_TOKENS).size, REQUIRED_TOKENS.length); }); @@ -188,7 +199,12 @@ test("no retired theme family is left in the sheet", () => { test("charts stay off the accent and off the gone colour", () => { for (const base of BASE_GROUND_IDS) { const r = rule(BASE_SELECTOR[base]); - for (let n = 1; n <= 5; n++) assert.match(r.decls.get(`--chart-${n}`) ?? "", /^#[0-9a-f]{6}$/); + for (let n = 1; n <= 6; n++) assert.match(r.decls.get(`--chart-${n}`) ?? "", /^#[0-9a-f]{6}$/); + // Six different colours on every base (the sixth, release 11, is a + // validated slot of its own, not a repeat or the golden angle). + const six = [1, 2, 3, 4, 5, 6].map((n) => r.decls.get(`--chart-${n}`)); + assert.equal(new Set(six).size, 6, `${base}: ${six.join(" ")}`); + assert.notEqual(r.decls.get("--chart-6"), r.decls.get("--state-gone")); // FACTS.md ~6442: chart-3 once WAS the gone hex; the palette itself was // re-validated with the dataviz validator (plans/brand-and-themes.md, S2). assert.notEqual(r.decls.get("--chart-3"), r.decls.get("--state-gone")); diff --git a/common/lib/homepageChart.ts b/common/lib/homepageChart.ts @@ -14,11 +14,14 @@ export type Range = "2w" | "6w" | "90d" | "12mo" | "all"; export type Metric = MetricKey; // "transcribed" | "downloaded" export type Display = "share" | "counts"; -// Stable series color. The first five use the globals.css palette -// (--chart-1..5); beyond that we spread hues by the golden angle so many -// channels / >5 sites stay visually distinct (and stable across renders). +// Stable series color. The first six are the validated palette in +// common/styles/tokens.css (--chart-1..6, fixed per base); beyond that we +// spread hues by the golden angle so many channels / >6 sites stay distinct +// (and stable across renders). The sixth was the golden angle's hue 328 until +// release 11 — within a degree of --chart-5's magenta. +export const CHART_SLOTS = 6; export function seriesColor(index: number): string { - if (index < 5) return `var(--chart-${index + 1})`; + if (index < CHART_SLOTS) return `var(--chart-${index + 1})`; const hue = Math.round((index * 137.508) % 360); return `hsl(${hue} 64% 55%)`; } diff --git a/common/lib/hubSummary.test.ts b/common/lib/hubSummary.test.ts @@ -10,6 +10,11 @@ import { } from "./hubSummary"; import { seriesColor } from "./homepageChart"; import type { HomepageSummary } from "./homepageSummary"; +import { resolveAccent } from "./accent"; +import { perBaseColor } from "./siteColor"; + +// A custom hex as a card wears it: fitted to each base (lib/siteColor.ts). +const fitted = (hex: string) => perBaseColor(resolveAccent(hex)); // Run with: // pnpm --filter yt-dlp-transcript-common test @@ -204,8 +209,8 @@ test("officialInstances: an accent the site sets wins; its place still follows t out.map((o) => [o.site.siteId, o.accent]), [ ["jeralyzer", seriesColor(0)], - ["anilyzer", "#e6a1c0"], // hub-sites.json's own - ["hasanalyzer", "#b49cf2"], // the summary's, when hub-sites.json has none + ["anilyzer", fitted("#e6a1c0")], // hub-sites.json's own, fitted per base + ["hasanalyzer", fitted("#b49cf2")], // the summary's, when hub-sites.json has none ], ); }); @@ -285,7 +290,7 @@ test("officialInstances: a named accent is painted per base; a site's own differ out.map((o) => [o.site.siteId, o.accent]), [ ["jeralyzer", "var(--swatch-brass)"], - ["anilyzer", "#cc3366"], // the site's own, which disagrees: no id + ["anilyzer", fitted("#cc3366")], // the site's own, which disagrees: no id ["hasanalyzer", seriesColor(2)], ], ); diff --git a/common/lib/hubSummary.ts b/common/lib/hubSummary.ts @@ -194,7 +194,8 @@ export type OfficialInstance<T> = { // own accent, else the summary's, else its chart colour (siteChartColors over // the summary's sites: its accent family's slot, or seriesColor() at its index // when that is free). A NAMED accent is painted per base (`var(--swatch-<id>)`) -// from the summary's `accentId`; hub-sites.json carries only the on-dark hex, +// from the summary's `accentId`, and a custom hex is fitted to each base +// (perBaseColor, release 11); hub-sites.json carries only the on-dark hex, // so the id is used only when the two files name the same colour (one compose // writes both, so they agree unless a site's own is newer). A member the // summary does not name takes the chart colours after the summary's, so it diff --git a/common/lib/siteColor.test.ts b/common/lib/siteColor.test.ts @@ -1,8 +1,27 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { ACCENT_CHART_SLOT, siteChartColors, siteColor } from "./siteColor"; -import { seriesColor } from "./homepageChart"; -import { ACCENTS, ACCENT_IDS } from "./brand"; +import { ACCENT_CHART_SLOT, 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"; + +// What the browser paints for a perBaseColor value on `base`: tokens.css sets +// `--base-<base>` to 1 and the other two to 0 (`null`: no token sheet at all, +// so every var() takes its fallback). Returns "#rrggbb". +function paint(css: string, base: BaseGround | null): string { + const flags = css.replace(/var\(--base-(light|sepia|dark), ([01])\)/g, (_, b, fallback) => + base === null ? fallback : b === base ? "1" : "0", + ); + const channels = [...flags.matchAll(/calc\(([^()]*)\)/g)].map((m) => + m[1].split("+").reduce((sum, term) => { + const [a, b] = term.split("*").map(Number); + return sum + a * b; + }, 0), + ); + assert.match(flags, /^rgb\(calc\([^()]*\) calc\([^()]*\) calc\([^()]*\)\)$/, css); + assert.equal(channels.length, 3, css); + return `#${channels.map((c) => c.toString(16).padStart(2, "0")).join("")}`; +} // ── siteColor: the mark colour ─────────────────────────────────────────────── @@ -17,9 +36,35 @@ test("siteColor: a named accent is its per-base swatch, whatever hex rides along assert.equal(siteColor({ accentId: "brass" }, seriesColor(0)), "var(--swatch-brass)"); }); -test("siteColor: a custom hex is that hex, normalized", () => { - assert.equal(siteColor({ accent: "#CC3366" }, seriesColor(0)), "#cc3366"); - assert.equal(siteColor({ accent: "#cc3366", accentId: undefined }, seriesColor(4)), "#cc3366"); +test("siteColor: a custom hex is fitted to each base, as the site's own pages fit it", () => { + // A pale hex: 1.43:1 on the light ground as published, so the old card + // painted it nearly invisible on light and sepia. + const pale = "#f4c2d7"; + const fitted = resolveAccent(pale); + const css = siteColor({ accent: "#F4C2D7" }, seriesColor(0)); + assert.equal(css, perBaseColor(fitted)); + for (const base of BASE_GROUND_IDS) { + assert.equal(paint(css, base), fitted[base], base); + assert.ok(contrastRatio(paint(css, base), BASE_GROUNDS[base]) >= 4.5, base); + } + // Fitted, not as published, where the ground needs it; kept where it reads. + assert.deepEqual( + { light: paint(css, "light"), sepia: paint(css, "sepia"), dark: paint(css, "dark") }, + { light: "#846974", sepia: "#7c636e", dark: pale }, + ); + // A hex that already reads on a ground is kept there exactly (#cc3366 on + // light), and the chart colour is never used for one. + assert.equal(paint(siteColor({ accent: "#cc3366" }, seriesColor(4)), "light"), "#cc3366"); + assert.equal(paint(siteColor({ accent: "#cc3366" }, seriesColor(4)), "dark"), resolveAccent("#cc3366").dark); +}); + +test("perBaseColor: one value, each base's colour; the light one with no token sheet", () => { + const v = { light: "#010203", sepia: "#a0b0c0", dark: "#ffeedd" }; + const css = perBaseColor(v); + for (const base of BASE_GROUND_IDS) assert.equal(paint(css, base), v[base]); + assert.equal(paint(css, null), v.light); + // Plain text a style attribute takes as is: no quotes, no url(), no `;`. + assert.doesNotMatch(css, /["';]|url\(/); }); test("siteColor: no accent is the site's chart colour", () => { @@ -29,8 +74,11 @@ test("siteColor: no accent is the site's chart colour", () => { }); test("siteColor: anything unrecognised falls through, never into the style", () => { - // Not an accent id: the hex (or the chart colour) stands. - assert.equal(siteColor({ accentId: "gold", accent: "#112233" }, seriesColor(0)), "#112233"); + // Not an accent id: the hex, fitted (or the chart colour), stands. + assert.equal( + siteColor({ accentId: "gold", accent: "#112233" }, seriesColor(0)), + perBaseColor(resolveAccent("#112233")), + ); assert.equal(siteColor({ accentId: "red);x:y" }, seriesColor(1)), seriesColor(1)); // Not a #rrggbb. assert.equal(siteColor({ accent: "red" }, seriesColor(1)), seriesColor(1)); @@ -42,9 +90,16 @@ test("siteColor: anything unrecognised falls through, never into the style", () const named = (accentId: string) => ({ accentId }); const none = {}; -test("siteChartColors: each family takes its hue's slot of --chart-1..5", () => { - // --chart-1..5 is blue, green, violet, amber, magenta on every base. - assert.deepEqual(ACCENT_CHART_SLOT, { blue: 0, green: 1, violet: 2, brass: 3, sakura: 4 }); +test("siteChartColors: each family takes its hue's slot of --chart-1..6", () => { + // --chart-1..6 is blue, green, violet, amber, magenta, rust on every base. + assert.deepEqual(ACCENT_CHART_SLOT, { + blue: 0, + green: 1, + violet: 2, + brass: 3, + sakura: 4, + vermilion: 5, + }); // Today's homepage order with the operator's mapping (2026-09-27). assert.deepEqual( siteChartColors(["brass", "sakura", "violet", "blue", "green"].map(named)), @@ -56,6 +111,23 @@ test("siteChartColors: each family takes its hue's slot of --chart-1..5", () => siteChartColors(["brass", "sakura", "violet", "green", "blue"].map(named)), ["var(--chart-4)", "var(--chart-5)", "var(--chart-3)", "var(--chart-2)", "var(--chart-1)"], ); + // Jasolyzer (Vermilion) published beside them, wherever it ranks: the rust + // slot, not the golden angle's hue 328 beside Anilyzer's magenta. + const five = ["brass", "sakura", "blue", "violet", "green"]; + for (let at = 0; at <= five.length; at++) { + const ids = [...five.slice(0, at), "vermilion", ...five.slice(at)]; + const out = siteChartColors(ids.map(named)); + assert.equal(out[at], "var(--chart-6)", `vermilion at ${at}`); + } +}); + +test("seriesColor: six validated slots, then the golden angle", () => { + assert.equal(CHART_SLOTS, 6); + assert.deepEqual( + Array.from({ length: 6 }, (_, i) => seriesColor(i)), + [1, 2, 3, 4, 5, 6].map((n) => `var(--chart-${n})`), + ); + assert.match(seriesColor(6), /^hsl\(/); }); test("siteChartColors: no accents is seriesColor(0..n-1), as before", () => { @@ -64,10 +136,10 @@ test("siteChartColors: no accents is seriesColor(0..n-1), as before", () => { assert.deepEqual(siteChartColors([]), []); }); -test("siteChartColors: no family (vermilion, signal, a custom hex, an unknown id) falls back to its index", () => { +test("siteChartColors: no family (signal, a custom hex, an unknown id) falls back to its index", () => { assert.deepEqual( - siteChartColors([named("vermilion"), named("signal"), { accentId: undefined }, named("gold")]), - [seriesColor(0), seriesColor(1), seriesColor(2), seriesColor(3)], + siteChartColors([named("signal"), { accentId: undefined }, named("gold")]), + [seriesColor(0), seriesColor(1), seriesColor(2)], ); }); @@ -92,11 +164,34 @@ test("siteChartColors: a fallback whose index a family holds takes the lowest fr siteChartColors([none, named("brass"), named("blue"), none, named("sakura")]), ["var(--chart-2)", "var(--chart-4)", "var(--chart-1)", "var(--chart-3)", "var(--chart-5)"], ); - // Past the palette, the golden-angle hues, never one already worn. - const six = siteChartColors([ + // A sixth site with no free family takes the sixth slot; a seventh, past + // the palette, the golden-angle hues, never one already worn. + const seven = siteChartColors([ named("blue"), named("green"), named("violet"), named("brass"), named("sakura"), named("blue"), + named("vermilion"), ]); - assert.equal(six[5], seriesColor(5)); + assert.equal(seven[6], "var(--chart-6)"); + assert.equal(seven[5], seriesColor(6)); +}); + +test("siteChartColors: any six sites wear the six validated slots, each once", () => { + // Whatever the accents — families, duplicates, Signal, a custom hex, none — + // up to six published sites never fall past --chart-6 to the golden angle + // (whose sixth hue was within a degree of --chart-5's magenta). + const ids = [...ACCENT_IDS, undefined]; + const validated = [1, 2, 3, 4, 5, 6].map((n) => `var(--chart-${n})`); + let seed = 11; + const rand = () => (seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31; + for (let run = 0; run < 500; run++) { + const n = 1 + Math.floor(rand() * 6); + const sites = Array.from({ length: n }, () => ({ + accentId: ids[Math.floor(rand() * ids.length)], + })); + const out = siteChartColors(sites); + assert.equal(new Set(out).size, n, JSON.stringify(sites)); + for (const c of out) assert.ok(validated.includes(c), `${c} for ${JSON.stringify(sites)}`); + if (n === 6) assert.deepEqual([...out].sort(), validated); + } }); test("siteChartColors: never two sites in one colour", () => { diff --git a/common/lib/siteColor.ts b/common/lib/siteColor.ts @@ -4,36 +4,38 @@ // TWO COLOURS PER SITE, ONE HUE. // // siteChartColors(sites) — the CHART colour: a slot of the validated chart -// palette (`--chart-1..5`, tokens.css: blue, green, violet, amber, -// magenta), picked by the hue family of the site's named accent: +// palette (`--chart-1..6`, tokens.css: blue, green, violet, amber, +// magenta, rust), picked by the hue family of the site's named accent: // blue → blue (1), green → green (2), violet → violet (3), -// brass → amber (4), sakura → magenta (5). -// Vermilion, Signal, a custom hex and no accent have no family and take -// seriesColor(i). No two sites share a colour (below). +// brass → amber (4), sakura → magenta (5), vermilion → rust (6). +// Signal, a custom hex and no accent have no family and take +// seriesColor(i). No two sites share a colour (below), and any six sites +// wear the six validated slots. // // siteColor(site, chart) — the MARK colour (a card's stripe, the hub's chip // and result edge): a named accent is `var(--swatch-<id>)`, its value on -// the base in force; a custom hex is that hex, as published; no accent is -// its chart colour. So a card and its chart layer share a hue family, and a -// site with no accent matches its layer exactly. +// the base in force; a custom hex is FITTED to each base the way the site's +// own pages fit it (lib/accent.ts resolveAccent: 4.5:1 on the ground and +// with its ink), as one CSS value (perBaseColor); no accent is its chart +// colour. So a card and its chart layer share a hue family, and a site with +// no accent matches its layer exactly. // // WHY NOT THE ACCENTS THEMSELVES ON THE CHART. The accents are text colours // (≥ 4.5:1 on each ground), not a categorical palette. Validated as one // (release 10, the dataviz skill's validator, all pairs, every base): no five // that include Brass and Blue pass — Brass↔Vermilion ΔE 1.0 deutan, // Blue↔Violet ΔE 8.5 normal / 1.3 protan on dark — and on dark every accent is -// above the band. The chart slots of the same hues, in today's homepage order -// (jeralyzer brass, anilyzer sakura, bonnellyzer violet, hasanalyzer blue, -// rekietalyzer green) and with the two closest sites swapped, pass every -// ADJACENT pair on every base; over ALL pairs they are the palette's own +// above the band. The chart slots of the same hues pass every ADJACENT pair on +// every base in the homepage's order; over ALL pairs they are the palette's own // borderline (CVD in the 6–8 floor band, normal ≥ 15.3), legal with the -// chart's legend, its surface-gap edges and its table. The numbers are in -// ArchiveGrowthChart.tsx. +// chart's legend, its surface-gap edges and its table. The sixth slot (release +// 11) adds no pair below the target: its worst is CVD 9.1 / normal 16.3. The +// numbers are in ArchiveGrowthChart.tsx and tokens.css. // // Pure and client-safe: the hub resolves it in the browser. -import { parseAccent } from "./accent"; -import { isAccentId, type AccentId } from "./brand"; +import { parseAccent, resolveAccent } from "./accent"; +import { BASE_GROUND_IDS, isAccentId, type AccentId, type BaseGround } from "./brand"; import { seriesColor } from "./homepageChart"; export type SiteColorSource = { @@ -42,12 +44,14 @@ export type SiteColorSource = { }; // The chart slot (seriesColor index, 0-based) of each accent's hue family. +// Signal has none: a teal slot would sit on the family's own UI colour. export const ACCENT_CHART_SLOT: Readonly<Partial<Record<AccentId, number>>> = { blue: 0, green: 1, violet: 2, brass: 3, sakura: 4, + vermilion: 5, }; // One chart colour per site, in `sites` order, all different. @@ -56,7 +60,8 @@ export const ACCENT_CHART_SLOT: Readonly<Partial<Record<AccentId, number>>> = { // 2. Every other site (no family, or the later of two) takes its own index's // colour, seriesColor(i), when no one holds it — so a list with no accents // is exactly seriesColor(0..n-1), as before — and otherwise the lowest -// free slot (past the fifth, seriesColor's golden-angle hues). +// free slot (past the sixth, seriesColor's golden-angle hues). So up to +// six sites only ever wear the validated --chart-1..6. export function siteChartColors(sites: readonly { accentId?: string }[]): string[] { const slot: number[] = new Array(sites.length).fill(-1); const taken = new Set<number>(); @@ -81,8 +86,29 @@ export function siteChartColors(sites: readonly { accentId?: string }[]): string return slot.map((k) => seriesColor(k)); } +// ONE CSS colour that is `values.light` on the light base, `values.sepia` on +// sepia and `values.dark` on dark — for a colour a component knows and the +// token sheet cannot (a custom hex is per site). tokens.css sets +// `--base-light|sepia|dark` to 1 on their own base and 0 on the others, so each +// channel is a calc() over the three and the browser resolves it to a plain +// 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. +export function perBaseColor(values: Readonly<Record<BaseGround, string>>): string { + const rgb = BASE_GROUND_IDS.map((b) => { + const n = parseInt(values[b].slice(1), 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; + }); + const channel = (c: number) => + `calc(${BASE_GROUND_IDS.map( + (b, i) => `${rgb[i][c]} * var(--base-${b}, ${b === "light" ? 1 : 0})`, + ).join(" + ")})`; + return `rgb(${channel(0)} ${channel(1)} ${channel(2)})`; +} + // 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})`; - return parseAccent(site.accent) ?? chart; + const hex = parseAccent(site.accent); + return hex ? perBaseColor(resolveAccent(hex)) : chart; } diff --git a/common/styles/tokens.css b/common/styles/tokens.css @@ -32,6 +32,11 @@ • `--swatch-<id>` is each named accent's value ON THIS BASE (lib/brand.ts ACCENTS — themeTokens.test.ts keeps the two equal), so a picker can show the colour a choice will actually paint. + • `--base-light`, `--base-sepia`, `--base-dark` are 1 on their own base + and 0 on the others. They let a colour that differs per base but is + known only to a component be ONE CSS value: each channel a calc() over + the three (lib/siteColor.ts perBaseColor — a custom-hex site's card on + the homepage and the hub, fitted to each ground). ========================================================================== */ @import "tw-animate-css"; @@ -93,6 +98,7 @@ --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); + --color-chart-6: var(--chart-6); /* Type system */ --font-display: var(--font-display); @@ -207,6 +213,11 @@ html[data-base="light"] { customAccentVars, inline on <html>); Signal if it is missing. */ --swatch-custom: var(--accent-custom-light, var(--swatch-signal)); + /* Which base this is, as numbers (the header's note; lib/siteColor.ts). */ + --base-light: 1; + --base-sepia: 0; + --base-dark: 0; + --brand: var(--swatch-signal); --brand-strong: color-mix(in oklab, var(--brand) 78%, black); --brand-soft: color-mix(in srgb, var(--brand) 12%, transparent); @@ -229,12 +240,19 @@ html[data-base="light"] { with a secondary encoding, a legend or direct labels. chart-3 is a violet, well apart from --state-gone (ΔE 24.3 normal, 21.7 protan): it once WAS the gone hex, so the third archive in a chart read as "gone". - chart-1 is a blue, not Signal (ΔE 17.3 from it). */ + chart-1 is a blue, not Signal (ΔE 17.3 from it). + chart-6 (release 11) is a rust, Vermilion's family: the sixth site's + colour. The only hue family that clears every pair with chart-1..5 on + all three bases is the red-browns (OKLCH h ≈ 15–60); this one's worst + pair with them is CVD ΔE 12.5 protan and normal 16.6, 8.1:1 on the chart + surface and 7.4:1 on the ground. It sits near --state-gone (ΔE 8.4 + normal), which is only ever labelled text, never a chart mark. */ --chart-1: #3a7de0; --chart-2: #2f8a57; --chart-3: #5e3aa8; --chart-4: #a8741a; --chart-5: #c24a8a; + --chart-6: #823c10; --chart-surface: #ffffff; --chart-grid: rgba(22, 28, 33, 0.09); --chart-axis: #55646e; @@ -297,6 +315,11 @@ html[data-base="sepia"] { --swatch-green: #3d772b; --swatch-custom: var(--accent-custom-sepia, var(--swatch-signal)); + /* Which base this is, as numbers (the header's note; lib/siteColor.ts). */ + --base-light: 0; + --base-sepia: 1; + --base-dark: 0; + --brand: var(--swatch-signal); --brand-strong: color-mix(in oklab, var(--brand) 78%, black); --brand-soft: color-mix(in srgb, var(--brand) 12%, transparent); @@ -307,12 +330,14 @@ html[data-base="sepia"] { /* Same hue order as light, stepped for the paper surface. Adjacent pairs pass every check; all pairs pass with green ↔ amber in the CVD floor - band (6.1). chart-3 vs --state-gone: ΔE 23.7 normal. */ + band (6.1). chart-3 vs --state-gone: ΔE 23.7 normal. chart-6, the + rust, is light's: worst pair CVD 9.1 protan, normal 16.6. */ --chart-1: #3574d6; --chart-2: #2a7d4f; --chart-3: #5e3aa8; --chart-4: #a8741a; --chart-5: #bb4585; + --chart-6: #823c10; --chart-surface: #faf4e6; --chart-grid: rgba(51, 40, 26, 0.09); --chart-axis: #6b5c43; @@ -373,6 +398,11 @@ html[data-base="dark"] { --swatch-green: #7cc46a; --swatch-custom: var(--accent-custom-dark, var(--swatch-signal)); + /* Which base this is, as numbers (the header's note; lib/siteColor.ts). */ + --base-light: 0; + --base-sepia: 0; + --base-dark: 1; + --brand: var(--swatch-signal); --brand-strong: color-mix(in oklab, var(--brand) 78%, white); --brand-soft: color-mix(in srgb, var(--brand) 16%, transparent); @@ -383,12 +413,15 @@ html[data-base="dark"] { /* Same hue order, stepped into the validator's dark lightness band. Adjacent pairs pass every check; all pairs pass with green ↔ magenta in - the CVD floor band (6.9). chart-3 vs --state-gone: ΔE 22.9 normal. */ + the CVD floor band (6.9). chart-3 vs --state-gone: ΔE 22.9 normal. + chart-6, the rust stepped into the band: worst pair CVD 13.9, normal + 16.3, 3.2:1 on the surface. */ --chart-1: #3561c8; --chart-2: #3fa577; --chart-3: #9a7ee6; --chart-4: #b98a2a; --chart-5: #c24c8a; + --chart-6: #a54a08; --chart-surface: #16110a; --chart-grid: rgba(233, 220, 197, 0.08); --chart-axis: #8a8170; diff --git a/homepage/app/components/ArchiveCards.tsx b/homepage/app/components/ArchiveCards.tsx @@ -8,18 +8,19 @@ import { siteChartColors, siteColor } from "yt-dlp-transcript-common/lib/siteCol // // 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 its chart colour. The hub's official cards wear the same -// (lib/hubSummary.ts officialInstances). +// in force, from the summary's `accentId`; a custom hex fitted to each base as +// the site's own pages fit it (release 11: a pale hex published as is was +// ~1.4:1 on light and sepia); a site with no accent its chart colour. The +// hub's official cards wear the same (lib/hubSummary.ts officialInstances). // // A CARD STILL READS AS ITS LAYER'S LEGEND, BY HUE. The growth chart draws // each site in the validated chart slot of its accent's hue family // (siteChartColors: Brass → amber, Blue → blue, Violet → violet, Green → -// green, Sakura → magenta), not in the accent itself — the accents fail as a -// chart palette (Brass↔Vermilion ΔE 1.0 deutan, Blue↔Violet 8.5 normal). So a -// family accent's stripe and its layer are one hue at two values, and a site -// with no accent matches its layer exactly. Vermilion, Signal and a custom hex -// have no family, so their layer is a palette colour of its own; +// green, Sakura → magenta, Vermilion → rust), not in the accent itself — the +// accents fail as a chart palette (Brass↔Vermilion ΔE 1.0 deutan, Blue↔Violet +// 8.5 normal). So a family accent's stripe and its layer are one hue at two +// values, and a site with no accent matches its layer exactly. Signal and a +// custom hex have no family, so their layer is a palette colour of its own; // ArchiveGrowthChart's legend names every layer either way. // // Numbers are v5 fields; a pre-v5 summary lacks them and the card shows only diff --git a/homepage/app/components/ArchiveGrowthChart.tsx b/homepage/app/components/ArchiveGrowthChart.tsx @@ -25,26 +25,30 @@ import { siteChartColors } from "yt-dlp-transcript-common/lib/siteColor"; // the stack reads as strata, which is what it is. // // COLOUR follows the instance, never its rank on this chart: siteChartColors -// (common/lib/siteColor.ts) gives each site a slot of the per-base --chart-1..5 -// palette (tokens.css: blue, green, violet, amber, magenta) by the hue family -// of its accent — blue → blue, green → green, violet → violet, brass → amber, -// sakura → magenta — so a layer shares its hue with the instance card below -// (ArchiveCards, the accent's own value). A site with no family (Vermilion, -// Signal, a custom hex, no accent) takes seriesColor(i), and no two layers ever -// share a colour. The legend above the plot wears the same colours. The layers -// stack in the summary's order (STACK_ORDER, unchanged). +// (common/lib/siteColor.ts) gives each site a slot of the per-base --chart-1..6 +// palette (tokens.css: blue, green, violet, amber, magenta, rust) by the hue +// family of its accent — blue → blue, green → green, violet → violet, brass → +// amber, sakura → magenta, vermilion → rust — so a layer shares its hue with +// the instance card below (ArchiveCards, the accent's own value). A site with +// no family (Signal, a custom hex, no accent) takes seriesColor(i), and no two +// layers ever share a colour; up to six sites wear only the validated slots. +// The legend above the plot wears the same colours. The layers stack in the +// summary's order (STACK_ORDER, unchanged). // -// VALIDATED (release 10, the dataviz skill's validator, each base's values on -// its --chart-surface), with the operator's accents (Jeralyzer Brass, Anilyzer -// Sakura, Bonnellyzer Violet, Hasanalyzer Blue, Rekietalyzer Green): +// VALIDATED (the dataviz skill's validator, each base's values on its +// --chart-surface and on the page ground), with the operator's accents +// (Jeralyzer Brass, Anilyzer Sakura, Bonnellyzer Blue, Hasanalyzer Violet, +// Rekietalyzer Green; Jasolyzer Vermilion once it is published): // • adjacent — the stack — in today's order (Jeralyzer, Anilyzer, // Bonnellyzer, Hasanalyzer, Rekietalyzer): PASS on every base, worst CVD -// ΔE 12.3 / 11.1 / 11.8 and normal 17.7 / 15.3 / 16.6 (light / sepia / -// dark); with Hasanalyzer and Rekietalyzer swapped (3,048 vs 2,855 -// transcripts): PASS, CVD 12.3 / 11.3 / 13.7, normal 19.6 / 19.7 / 16.6; -// • all pairs: the palette's own borderline in any order — CVD 6.2 / 6.1 / -// 6.9 (green↔amber, green↔magenta: the 6–8 floor band, legal with the -// legend, the surface-gap edges and the table), normal ≥ 15.3. +// ΔE 13.1 / 11.1 / 11.0 and normal 17.7 / 15.3 / 16.7 (light / sepia / +// dark; release 10); with Jasolyzer at any place in it: PASS, worst CVD +// 12.5 / 9.1 / 11.0, normal 16.6 / 15.3 / 16.3 (release 11); +// • all pairs, all six: the palette's own borderline in any order — CVD 6.2 +// / 6.1 / 6.9 (green↔amber, green↔magenta: the 6–8 floor band, legal with +// the legend, the surface-gap edges and the table), normal ≥ 15.3. The +// sixth slot adds no pair under the target: its worst is CVD 9.1 (sepia, +// against green), normal 16.3 (dark, against amber). // The accents' own values fail as a chart palette (Brass↔Vermilion ΔE 1.0 // deutan, Blue↔Violet 8.5 normal), which is why the chart wears their hue // families rather than the accents.