diff --git a/apps/website/app/globals.css b/apps/website/app/globals.css index 20282f4e..2061f120 100644 --- a/apps/website/app/globals.css +++ b/apps/website/app/globals.css @@ -350,3 +350,40 @@ margin-inline-start: calc(var(--sidebar-width) * -1); } } + +/* Same story for `?q=` and `?library=`, one step earlier: a static export + ships the list rendered whole, so all ~160 cards paint while the JS that + will narrow them down is still loading, and the filter lands as a visible + jump. `bootNav` marks a filtered arrival, and these hold the list back + behind a handful of skeletons until React has applied it -- only for those + arrivals; everyone else gets the prerendered list painted as it always was + and never renders the skeletons at all. + + `visibility`, not `display`: the cards have to keep their boxes, both + because the list windowing measures them on that very commit -- collapsed + to nothing, every card reads as on-screen and all ~160 thumbnails mount at + once -- and because it is what lets the skeletons sit over the list rather + than push it down. */ +#example-skeletons, +#nav-filters-skeleton { + display: none; +} + +html[data-nav-filtering] #example-skeletons { + display: flex; +} + +html[data-nav-filtering] #nav-filters-skeleton { + display: block; +} + +html[data-nav-filtering] #example-list { + visibility: hidden; +} + +/* The filter row is the same story in miniature: whichever of its two forms + the HTML was built with is the wrong one here -- the dropdown where the + search field belongs, or the dropdown with nothing selected. */ +html[data-nav-filtering] #nav-filters { + display: none; +} diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 23cec686..d5b4f0aa 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -12,6 +12,50 @@ import { builder } from "material-theme-builder"; const inter = Inter({ subsets: ["latin"] }); const examples = getExamples(); +/** + * Two things only the client knows, both needed before the first paint, so the + * blocking script below runs this and leaves the verdict on : whether the + * rail starts collapsed, and whether this is a filtered arrival — `?q=` + * /`?library=`, whose list must not paint whole while the JS that will narrow + * it is still loading. `globals.css` acts on both marks and `Nav` takes them + * over, then drops them. A filter beats a stored collapse: a shared link has to + * be able to show what it filtered down to. + * + * A function rather than a template string, so it is typed, formatted and + * linted like everything else — `String(bootNav)` is what ends up in the page. + * The catch that comes with that: it has to stay hermetic. No imports, no + * module-level constants, nothing but its arguments, or the bundler leaves a + * dangling reference in the string. Hence the storage key coming in as one — + * `Nav` owns the other half of that contract. + */ +function bootNav(storageKey: string) { + const params = new URLSearchParams(window.location.search); + const filtering = !!(params.get("q") || params.get("library")); + + const isCollapsed = () => { + const parts = window.location.pathname.split("/").filter(Boolean); + const examplesIndex = parts.indexOf("examples"); + const onAnExample = examplesIndex !== -1 && !!parts[examplesIndex + 1]; + + /* Two cases where the rail stays out whatever this visitor last left it + at: the index, where there is nothing to look at beside it, and a + shared filter link, which has to show what it filtered down to. */ + if (!onAnExample || filtering) return false; + + /* Then `?nav=`, a link saying how it wants to arrive… */ + const nav = params.get("nav"); + if (nav === "closed") return true; + if (nav === "open") return false; + + /* …and failing that, wherever this visitor left it. */ + return localStorage.getItem(storageKey) === "1"; + }; + + const root = document.documentElement; + root.toggleAttribute("data-nav-collapsed", isCollapsed()); + root.toggleAttribute("data-nav-filtering", filtering); +} + /** * The one hex the whole palette hangs off -- poimandres' signature mint. * Material Color Utilities derives every `--md-sys-color-*` role from it, and @@ -81,21 +125,10 @@ export default function RootLayout({ precedence="high" dangerouslySetInnerHTML={{ __html: mcuCss }} /> + {/* Blocking on purpose: `bootNav` settles what the rail looks like + before anything paints. */}