Remove everything that can change after first paint

Firefox users saw a flash on the first navigation after an idle spell, and
Chrome occasionally flashed white. Nothing in the network path explained it
(assets are immutable, the stylesheet is render-blocking, a cold Firefox
load paints fully styled), so this removes every remaining way a page could
look different between its first paint and its final state:

- Both Newsreader faces are embedded in the served stylesheet as data: URIs
  (CSP gains `font-src 'self' data:`; the preloads go away). A font fetched
  by URL is applied after first paint whenever the browser has to bring it
  back from disk, which is exactly the "first click after a while" case.
- The cross-document view transition is gone; the operator wants snappy.
- A color-scheme meta, kept in step with the saved theme, so the canvas the
  browser paints before the stylesheet is the right shade.
- The theme toggle's icon and label are chosen by CSS from html[data-theme]
  (set pre-paint by theme.js) instead of being rewritten by app.js.
- Dashboard table filters are rendered by the templates (shown under
  `.has-js`) rather than inserted by app.js, so tables no longer jump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
This commit is contained in:
2026-09-04 18:37:44 +00:00
co-authored by Claude Fable 5.1
parent 9acd1b0800
commit 1047cf4633
13 changed files with 91 additions and 82 deletions
+17 -15
View File
@@ -9,17 +9,14 @@ if (themeToggle) {
return "system";
}
};
const renderTheme = (theme) => {
themeToggle.setAttribute("aria-label", `Theme: ${theme}`);
themeToggle.querySelector("[data-theme-label]").textContent = theme[0].toUpperCase() + theme.slice(1);
themeToggle.querySelectorAll("[data-theme-icon]").forEach((icon) => {
// SVGElement has no `hidden` IDL attribute; toggle the content attribute.
icon.toggleAttribute("hidden", icon.getAttribute("data-theme-icon") !== theme);
});
};
// Icons and label are picked by CSS from `html[data-theme]`, which theme.js sets
// before first paint, so nothing here repaints the toggle after load.
const renderTheme = (theme) => themeToggle.setAttribute("aria-label", `Theme: ${theme}`);
const setTheme = (theme) => {
if (theme === "system") document.documentElement.removeAttribute("data-theme");
else document.documentElement.dataset.theme = theme;
const scheme = document.querySelector('meta[name="color-scheme"]');
if (scheme) scheme.content = theme === "system" ? "light dark" : theme;
try {
if (theme === "system") localStorage.removeItem("theme");
else localStorage.setItem("theme", theme);
@@ -83,14 +80,19 @@ document.querySelectorAll("details[id]").forEach((details) => {
/* step 3: filter-as-you-type on tables with data-filter (this page's rows only) */
document.querySelectorAll("table[data-filter]").forEach((table) => {
const rows = table.querySelectorAll("tbody tr");
if (rows.length < 2) return;
const input = document.createElement("input");
input.type = "search";
input.className = "table-filter";
input.placeholder = "Filter rows on this page";
input.setAttribute("aria-label", "Filter rows on this page");
const host = table.closest(".scroll-x") || table;
host.parentNode.insertBefore(input, host);
// The template renders the input (shown only under `.has-js`) so the table does
// not jump when this script runs after first paint; create one if it is missing.
let input = host.previousElementSibling;
if (!(input && input.matches("input[data-table-filter]"))) {
if (rows.length < 2) return;
input = document.createElement("input");
input.type = "search";
input.className = "table-filter";
input.placeholder = "Filter rows on this page";
input.setAttribute("aria-label", "Filter rows on this page");
host.parentNode.insertBefore(input, host);
}
input.addEventListener("input", () => {
const needle = input.value.trim().toLowerCase();
rows.forEach((row) => {