The new reader app replaces the old UI: Checklist with a rail (focus pills, Next to expire, today's dailies, pinned routines), Timeline in a new and a classic look, Routines, Archive and a Settings area, in a Structured (default) and a Relaxed interface. It follows the approved Round 6 B and Round 7 mockups, corrected screen by screen with the owner. Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data is migrated on first load, a recovery copy is written first, and the v1 keys are never removed, so a rollback finds them unchanged. Redeem codes for Genshin arrive as their own feed. Also: live's urgency ramp, editable per theme; completion and section-complete fireworks; a returning-user tour; the design mockups, specs, plans, reviews and handoff record. Co-Authored-By: Claude Opus 5.5 <[email protected]>
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import {
|
|
appearanceTokens,
|
|
readableOnSurfaces,
|
|
contrastRatio,
|
|
} from "../src/client/state/appearance.ts";
|
|
test("custom colours retain contrast on both page and surface", () => {
|
|
const t = appearanceTokens(
|
|
{
|
|
scheme: "custom",
|
|
customPalette: {
|
|
dark: {
|
|
ground: "#111111",
|
|
surface: "#222222",
|
|
text: "#111111",
|
|
accent: "#222222",
|
|
},
|
|
},
|
|
},
|
|
"dark",
|
|
);
|
|
expect(contrastRatio(t.ink, t.ground)).toBeGreaterThanOrEqual(4.5);
|
|
expect(contrastRatio(t.ink, t.surface)).toBeGreaterThanOrEqual(4.5);
|
|
const hue = readableOnSurfaces("#8bcfff", [t.ground, t.surface], 4.5);
|
|
expect(contrastRatio(hue, t.surface)).toBeGreaterThanOrEqual(4.5);
|
|
});
|
|
test("palette and theme are independent from interface and density", () => {
|
|
expect(
|
|
appearanceTokens(
|
|
{ scheme: "warm", interface: "structured", density: "compact" },
|
|
"light",
|
|
),
|
|
).toEqual(
|
|
appearanceTokens(
|
|
{ scheme: "warm", interface: "relaxed", density: "standard" },
|
|
"light",
|
|
),
|
|
);
|
|
});
|
|
|
|
test("Structured is the default while a saved Relaxed choice remains distinct", () => {
|
|
expect(appearanceTokens({}, "dark").ground).toBe("#0e1117");
|
|
expect(appearanceTokens({ interface: "relaxed" }, "dark").ground).toBe(
|
|
"#1a1c24",
|
|
);
|
|
});
|
|
|
|
import { urgencyTokens } from "../src/client/state/appearance.ts";
|
|
test("urgency follows the live ramp, grey to blue to yellow to red, whatever the accent", () => {
|
|
const dark = urgencyTokens(appearanceTokens({ accent: "#ff00ff" }, "dark"));
|
|
expect(dark.near).toBe("#5b8def");
|
|
expect(dark.soon).toBe("#e8b33c");
|
|
expect(dark.critical).toBe("#ff4d6a");
|
|
// Live's grey, nudged only as far as large-text contrast needs.
|
|
expect(dark.calm).not.toBe(dark.near);
|
|
const light = urgencyTokens(appearanceTokens({}, "light"));
|
|
expect(light.near).toBe("#2b62c6");
|
|
expect(light.soon).toBe("#8a6008");
|
|
expect(light.critical).toBe("#c81e3c");
|
|
expect(light.calm).not.toBe(light.near);
|
|
});
|
|
|
|
import { urgencyChoice } from "../src/client/state/appearance.ts";
|
|
test("the reader's urgency colours apply per theme; unset or invalid steps keep live's", () => {
|
|
const prefs = {
|
|
urgencyPalette: {
|
|
dark: { critical: "#a020f0", soon: "not a colour" },
|
|
light: { near: "#123456" },
|
|
},
|
|
};
|
|
const dark = urgencyChoice(prefs, "dark");
|
|
expect(dark.critical).toBe("#a020f0");
|
|
expect(dark.soon).toBe("#e8b33c");
|
|
expect(dark.near).toBe("#5b8def");
|
|
expect(urgencyChoice(prefs, "light").near).toBe("#123456");
|
|
expect(urgencyChoice({ urgencyPalette: "junk" }, "dark").critical).toBe(
|
|
"#ff4d6a",
|
|
);
|
|
// Applied, a chosen colour is kept when it already reads on the surfaces.
|
|
expect(
|
|
urgencyTokens(appearanceTokens(prefs, "dark"), prefs, "dark").critical,
|
|
).toBe("#a020f0");
|
|
});
|