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:
File diff suppressed because one or more lines are too long
+17
-15
@@ -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) => {
|
||||
|
||||
+10
-2
@@ -2,10 +2,18 @@
|
||||
// Marks the document as scripted before first paint so progressively enhanced
|
||||
// widgets (the contents panel) can start collapsed without a flash.
|
||||
document.documentElement.classList.add("has-js");
|
||||
// The color-scheme meta decides what the browser paints before the stylesheet
|
||||
// arrives; keep it in step with an explicit theme so that first paint is not
|
||||
// the wrong shade.
|
||||
const scheme = document.querySelector('meta[name="color-scheme"]');
|
||||
try {
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
|
||||
else document.documentElement.removeAttribute("data-theme");
|
||||
if (theme === "light" || theme === "dark") {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
if (scheme) scheme.content = theme;
|
||||
} else {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
}
|
||||
} catch (_) {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user