Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/__tests__/appAccessibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,14 @@ describe("the sticky nav reads on both grounds", () => {
expect(block).toContain("--primary-ink: oklch(0.55 0.19 256.4)");
});
});

describe("the announcement banner reads at full strength", () => {
it("does not dim its own link text", () => {
// Measured on production: text-white/85 on the banner resolved to #d9e9fa against
// rgb(1,109,221), which is 4.01:1, and Lighthouse failed the page's colour contrast
// for it. Full white on the same background measures 4.96:1.
const navbar = readFileSync("src/components/Navbar.tsx", "utf8");
const banner = navbar.slice(navbar.indexOf("bg-primary"), navbar.indexOf("bg-primary") + 1200);
expect(banner, "the banner dims its link text again").not.toMatch(/text-white\/\d+/);
});
});
2 changes: 1 addition & 1 deletion src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function Navbar({ position = "sticky" }: { position?: "fixed" | "
className="group mx-auto flex max-w-[1360px] flex-col items-center justify-center gap-x-10 gap-y-1 px-4 py-2.5 text-center text-sm sm:flex-row"
>
<span>Three finished sites, served as the exact bundles they export to.</span>
<span className="lc-mono inline-flex items-center gap-2 text-white/85 transition-colors group-hover:text-white">
<span className="lc-mono inline-flex items-center gap-2 text-white transition-colors">
See the examples <ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</span>
</Link>
Expand Down
42 changes: 39 additions & 3 deletions src/components/StylePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,25 @@ type FrameOptions = Parameters<typeof drawFrame2D>[4];
const REST_CACHE = new Map<string, number>();
const REST_CANDIDATES = [0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9];

function restKey(opts: FrameOptions, maxProgress: number): string {
return `${opts.style}|${opts.color1}|${opts.color2}|${opts.color3}|${maxProgress}`;
}

export function knownRestingProgress(opts: FrameOptions, maxProgress: number): number | undefined {
return REST_CACHE.get(restKey(opts, maxProgress));
}

/**
* Seven draws and seven readbacks, for one number.
*
* Cheap on its own and ruinous in a group: the landing page mounts four of these, and
* running the probe inside the first paint put 28 synchronous getImageData calls in front
* of the page settling. Measured with Lighthouse, that page blocked for 480ms while every
* other page in the site blocked for none. It runs off the critical path now, so call
* knownRestingProgress first and paint something while this works.
*/
function restingProgress(opts: FrameOptions, maxProgress: number): number {
const key = `${opts.style}|${opts.color1}|${opts.color2}|${opts.color3}|${maxProgress}`;
const key = restKey(opts, maxProgress);
const cached = REST_CACHE.get(key);
if (cached !== undefined) return cached;
let best = 0;
Expand Down Expand Up @@ -62,6 +79,13 @@ function restingProgress(opts: FrameOptions, maxProgress: number): number {
return best;
}

/** After the page has settled, or very soon, in a browser without requestIdleCallback. */
function whenIdle(run: () => void): void {
const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback;
if (typeof idle === "function") idle(run, { timeout: 1200 });
else setTimeout(run, 200);
}

/**
* Renders a live, looping animation of a 2D style directly to a canvas via
* requestAnimationFrame — no JPEG encoding, no IndexedDB. Cheap enough to mount
Expand Down Expand Up @@ -122,8 +146,20 @@ export default function StylePreview({
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
sizeRef.current = { w, h };
// Repaint immediately so a resize never leaves the canvas blank; a running
// loop overwrites this on its next tick.
drawFrame2D(ctx, w, h, restingProgress(optsRef.current, maxRef.current), optsRef.current);
// loop overwrites this on its next tick. The resting frame is whichever of the
// style's frames is brightest, and finding it costs seven readbacks, so the first
// paint uses the start of the range and the better frame arrives when the browser
// is idle.
const known = knownRestingProgress(optsRef.current, maxRef.current);
drawFrame2D(ctx, w, h, known ?? 0, optsRef.current);
if (known === undefined) {
whenIdle(() => {
const context = canvas.getContext("2d");
if (!context) return;
const { w: width, h: height } = sizeRef.current;
drawFrame2D(context, width, height, restingProgress(optsRef.current, maxRef.current), optsRef.current);
});
}
};

resize();
Expand Down
Loading