From c3f9f66619a93750a5a94cb3d22b8b0b82768607 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 22 Oct 2026 15:16:28 +0200 Subject: [PATCH 1/4] fix(docs): center the landing canvas before its first paint after a client navigation --- docs/build.ts | 4 +++- e2e/navigation.e2e.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/build.ts b/docs/build.ts index e908372b..5e27141c 100644 --- a/docs/build.ts +++ b/docs/build.ts @@ -775,7 +775,9 @@ for (const canvas of document.querySelectorAll(".canvas")) { canvas.dataset.view = name; for (const b of canvas.querySelectorAll(".canvas-bar [data-view]")) b.setAttribute("aria-pressed", b.dataset.view === name); if (keep) try { localStorage.setItem("comark-view", name) } catch {} - if (name === "canvas") { draw(); run() } else halt(); + // Measured now rather than left to the ResizeObserver, which reports after a client-side navigation has painted + // the cards uncentered. + if (name === "canvas") { vw = viewport.clientWidth; vh = viewport.clientHeight; draw(); run() } else halt(); }; new ResizeObserver(([e]) => { vw = e.contentRect.width; vh = e.contentRect.height; draw() }).observe(viewport); viewport.addEventListener("pointerdown", e => { diff --git a/e2e/navigation.e2e.ts b/e2e/navigation.e2e.ts index 97641a79..a41a5404 100644 --- a/e2e/navigation.e2e.ts +++ b/e2e/navigation.e2e.ts @@ -25,3 +25,21 @@ test("client navigation preserves the app and reinitializes page scripts", async ), ).toBe(marker); }); + +test("the landing canvas paints its cards centered after a client navigation", async ({ page }) => { + await page.goto("/zoom/"); + // Every frame's position of the first card, from the click until the canvas has settled. + const positions = await page.evaluate(async () => { + const seen: number[] = []; + document.querySelector('header a[href="/"]')!.click(); + const start = performance.now(); + while (performance.now() - start < 1500) { + await new Promise(requestAnimationFrame); + const card = document.querySelector(".canvas-stage .demo"); + if (card) seen.push(Math.round(card.getBoundingClientRect().x)); + } + return seen; + }); + expect(positions.length).toBeGreaterThan(0); + expect(new Set(positions).size).toBe(1); +}); From 73327e269e162efc006c16fd319b59d554f72588 Mon Sep 17 00:00:00 2001 From: "vitehub-bot[bot]" <320448255+vitehub-bot[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:55:53 +0200 Subject: [PATCH 2/4] test(docs): assert landing canvas centering --- docs/build.ts | 3 +-- e2e/navigation.e2e.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/build.ts b/docs/build.ts index 5e27141c..8bce6ab3 100644 --- a/docs/build.ts +++ b/docs/build.ts @@ -775,8 +775,7 @@ for (const canvas of document.querySelectorAll(".canvas")) { canvas.dataset.view = name; for (const b of canvas.querySelectorAll(".canvas-bar [data-view]")) b.setAttribute("aria-pressed", b.dataset.view === name); if (keep) try { localStorage.setItem("comark-view", name) } catch {} - // Measured now rather than left to the ResizeObserver, which reports after a client-side navigation has painted - // the cards uncentered. + // Measure during view activation so navigation can draw with the viewport size before its first paint. if (name === "canvas") { vw = viewport.clientWidth; vh = viewport.clientHeight; draw(); run() } else halt(); }; new ResizeObserver(([e]) => { vw = e.contentRect.width; vh = e.contentRect.height; draw() }).observe(viewport); diff --git a/e2e/navigation.e2e.ts b/e2e/navigation.e2e.ts index a41a5404..1b07d7bf 100644 --- a/e2e/navigation.e2e.ts +++ b/e2e/navigation.e2e.ts @@ -31,15 +31,20 @@ test("the landing canvas paints its cards centered after a client navigation", a // Every frame's position of the first card, from the click until the canvas has settled. const positions = await page.evaluate(async () => { const seen: number[] = []; + const view = document.querySelector(".canvas-viewport")!; + const expected = ((view.clientWidth - 300) % 320 + 320) % 320 / 2; document.querySelector('header a[href="/"]')!.click(); const start = performance.now(); while (performance.now() - start < 1500) { await new Promise(requestAnimationFrame); - const card = document.querySelector(".canvas-stage .demo"); - if (card) seen.push(Math.round(card.getBoundingClientRect().x)); + const card = [...document.querySelectorAll(".canvas-stage .demo")].find( + card => Number.parseFloat(card.style.left) === 0, + ); + if (card) seen.push(Math.round(card.getBoundingClientRect().x - view.getBoundingClientRect().x)); } - return seen; + return { seen, expected: Math.round(expected) }; }); - expect(positions.length).toBeGreaterThan(0); - expect(new Set(positions).size).toBe(1); + expect(positions.seen.length).toBeGreaterThan(0); + expect(new Set(positions.seen).size).toBe(1); + expect(positions.seen[0]).toBe(positions.expected); }); From 3c71d1646c641cb3afd3e5028b00d9a66dd0e62e Mon Sep 17 00:00:00 2001 From: "vitehub-bot[bot]" <320448255+vitehub-bot[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:10:47 +0200 Subject: [PATCH 3/4] test(docs): wait for landing canvas after navigation --- e2e/navigation.e2e.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/e2e/navigation.e2e.ts b/e2e/navigation.e2e.ts index 1b07d7bf..9683bf51 100644 --- a/e2e/navigation.e2e.ts +++ b/e2e/navigation.e2e.ts @@ -31,17 +31,20 @@ test("the landing canvas paints its cards centered after a client navigation", a // Every frame's position of the first card, from the click until the canvas has settled. const positions = await page.evaluate(async () => { const seen: number[] = []; - const view = document.querySelector(".canvas-viewport")!; - const expected = ((view.clientWidth - 300) % 320 + 320) % 320 / 2; document.querySelector('header a[href="/"]')!.click(); const start = performance.now(); while (performance.now() - start < 1500) { await new Promise(requestAnimationFrame); + const view = document.querySelector(".canvas-viewport"); + if (!view) continue; const card = [...document.querySelectorAll(".canvas-stage .demo")].find( - card => Number.parseFloat(card.style.left) === 0, + (card) => Number.parseFloat(card.style.left) === 0, ); - if (card) seen.push(Math.round(card.getBoundingClientRect().x - view.getBoundingClientRect().x)); + if (card) + seen.push(Math.round(card.getBoundingClientRect().x - view.getBoundingClientRect().x)); } + const view = document.querySelector(".canvas-viewport")!; + const expected = ((((view.clientWidth - 300) % 320) + 320) % 320) / 2; return { seen, expected: Math.round(expected) }; }); expect(positions.seen.length).toBeGreaterThan(0); From 04c3dba7ad313ea06421ec6e68545a8dd1fb1cbf Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 22 Oct 2026 17:36:04 +0200 Subject: [PATCH 4/4] feat(docs): fill the landing canvas with every example and turn chart cards through their charts - The canvas holds the landing's demos plus every package page's examples (45 cards instead of 15), in a fixed shuffle with no two of one package in a row. Each column's step is the one that keeps copies of a card farthest apart over the columns a screen holds, so no card shows twice on screen. - Cards without a height in their markup are measured once off screen and kept for the visit. The site's PNGs get their width and height at build time, so a card measures as it will show. - The two chart cards turn through the chart examples while hovered, crossfading SVGs drawn at build time. The per-bar and line-drawing animations, which repainted the SVG every frame, are gone. - Card loops start moving as soon as the pointer arrives instead of holding first, and they animate opacity and transforms only, with no blur. - `.demo` is `contain: strict`, and its hidden Markdown is `content-visibility: hidden` until shown. - The icon demo is gone: its logos filled the card at 2.25rem with the sentence hidden. The icon examples show icons inline at text size. - References light up in turn instead of vanishing, which made a card look like it had lost its links. - The github references demo, which repeated the page's References example, shows once. --- docs/build.ts | 209 +++++++++++++++++++++++++------- docs/pages/index.md | 10 +- docs/style.css | 281 +++++++++++++++----------------------------- e2e/docs.e2e.ts | 56 +++++++++ 4 files changed, 320 insertions(+), 236 deletions(-) diff --git a/docs/build.ts b/docs/build.ts index 8bce6ab3..a81e8c68 100644 --- a/docs/build.ts +++ b/docs/build.ts @@ -437,6 +437,18 @@ const presets = parsed.flatMap((page) => })) .filter((preset) => playable(preset.source)), ); +// The size of each PNG the pages show, read from its header, so a still reserves its room before the image loads. +const publicDir = new URL("public/", root); +const imageSizes = new Map( + await Promise.all( + (await readdir(publicDir)) + .filter((file) => file.endsWith(".png")) + .map(async (file) => { + const head = await readFile(new URL(file, publicDir)); + return [file, [head.readUInt32BE(16), head.readUInt32BE(20)]] as const; + }), + ), +); // A still of a rendered document: no scripts, its plugins' styles collected into `styles` to go in the page once, // no ids, since a page holds many, and a chart without its data table, so the still is all plot. const stillOf = (html: string, styles: Set) => @@ -445,6 +457,10 @@ const stillOf = (html: string, styles: Set) => .replace(/