feat(t5): Farb-Token additiv, Sonnen-Farbsätze und Kontrast-Wächter (T5a A1)

Neue Token neben den bestehenden (Schrift auf Hauptfarbe und Warnung,
Zustands-, Bereichs- und Königinnen-Töne, Fokus- und Rahmenmaße,
Wabenstriche), zwei vollständige Sonnen-Sätze (hell/dunkel) mit
größerer Schrift, 3-px-Fokus und ohne Schatten, Holo-Sonne für die
Beute-Bühne und .holo-stage-Fokusring, Druck immer im hellen Satz,
forced-colors. Wächter misst alle 21 Paare der Spec-Tabelle 4.4 in
vier Sätzen (OKLCH → sRGB → WCAG, Tönungen alpha-gemischt), prüft den
sRGB-Farbraum und erzwingt das Umschalten in A4. Sichtbar ändert sich
nur ein minimal entsättigter Holo-Kantenton (vorher außerhalb sRGB).

Refs #26

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 16:23:33 +02:00
1 parent a7a7599304
commit 6b1b3cfa90
6 files changed
+846 -4

No files matched your search

@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { alphaOf, atRuleDeclarations, blend, contrastOf, inGamut, oklchOf, tokenSet } from "./css-tokens";
const CSS = `
@theme inline { --color-x: var(--x); }
:root { --x: oklch(0.5 0.1 80); --y: oklch(1 0 0 / 12%); --len: 0.75rem;
@variant sm { --nested: oklch(0 0 0); } }
.dark { --x: oklch(0.2 0.1 80); --y: oklch(0 0 0); }
:root[data-contrast="sun"] { --x: oklch(0.9 0 0); }
.dark[data-contrast="sun"] { --x: oklch(0.95 0 0); }
`;
describe("css-tokens", () => {
it("reads oklch values with and without alpha", () => {
expect(oklchOf("oklch(0.5 0.1 80)")).toEqual([0.5, 0.1, 80]);
expect(oklchOf("oklch(1 0 0 / 12%)")).toEqual([1, 0, 0]);
expect(alphaOf("oklch(1 0 0 / 12%)")).toBeCloseTo(0.12);
expect(alphaOf("oklch(1 0 0)")).toBe(1);
expect(() => oklchOf("1rem")).toThrow();
});
it("contrast of black on white is 21", () => {
expect(contrastOf([0, 0, 0], [1, 0, 0])).toBeCloseTo(21, 1);
expect(contrastOf([1, 0, 0], [1, 0, 0])).toBeCloseTo(1, 5);
});
it("blends a 15 % tint over its background before measuring", () => {
const white: [number, number, number] = [1, 0, 0];
const black: [number, number, number] = [0, 0, 0];
const mixed = blend(black, 0.15, white);
expect(mixed[0]).toBeGreaterThan(0.8);
expect(mixed[0]).toBeLessThan(1);
expect(contrastOf(mixed, white)).toBeLessThan(contrastOf(black, white));
expect(blend(black, 1, white)[0]).toBeCloseTo(0, 5);
});
it("sun-light layers over light and sun-dark over dark", () => {
expect(tokenSet(CSS, "light").x).toBe("oklch(0.5 0.1 80)");
expect(tokenSet(CSS, "dark").x).toBe("oklch(0.2 0.1 80)");
expect(tokenSet(CSS, "sun-light").x).toBe("oklch(0.9 0 0)");
expect(tokenSet(CSS, "sun-light").len).toBe("0.75rem");
expect(tokenSet(CSS, "sun-dark").x).toBe("oklch(0.95 0 0)");
expect(tokenSet(CSS, "sun-dark").y).toBe("oklch(0 0 0)");
});
it("returns only the own declarations of a sun set on request", () => {
expect(tokenSet(CSS, "sun-light", { own: true })).toEqual({ x: "oklch(0.9 0 0)" });
});
it("ignores declarations nested in at-rules", () => {
expect(tokenSet(CSS, "light").nested).toBeUndefined();
});
it("reads the declarations inside an at-rule", () => {
const css = "@media print { :root, .dark { --a: 1; } .x { --b: 2; } } :root { --c: 3; }";
expect(atRuleDeclarations(css, "@media print")).toEqual({ a: "1", b: "2" });
});
it("detects colours outside the sRGB gamut", () => {
expect(inGamut([0.6, 0.1, 50])).toBe(true);
expect(inGamut([0.6, 0.4, 150])).toBe(false);
});
});