···11+/**
22+ * Updates the active state of nav links based on the current URL.
33+ * Required because the nav now lives in <header> and is not replaced by PPR.
44+ */
55+export function updateActiveLinks() {
66+ const nav = document.getElementById("diffuse-nav");
77+ const menu = document.getElementById("nav-overflow-menu");
88+99+ for (const container of [nav, menu]) {
1010+ if (!container) continue;
1111+ for (const link of container.querySelectorAll("a[href]")) {
1212+ const a = /** @type {HTMLAnchorElement} */ (link);
1313+ const isActive = new URL(a.href).pathname === location.pathname;
1414+ a.classList.toggle("button--transparent", !isActive);
1515+ }
1616+ }
1717+}
1818+119export function update() {
220 const nav = document.getElementById("diffuse-nav");
321 const btn = document.getElementById("nav-overflow-btn");
···1129 for (const item of items) item.style.display = "";
1230 btn.style.display = "none";
13313232+ // Available width is the nav-container's width (nav may be content-sized in column layouts)
3333+ const availableWidth = (nav.parentElement ?? nav).clientWidth;
3434+3535+ // Measure total content width directly — scrollWidth is unreliable with
3636+ // justify-content: flex-end because overflow spills left (negative direction)
3737+ // and isn't reflected in scrollWidth.
3838+ const gap = parseFloat(getComputedStyle(nav).columnGap) || 0;
3939+ const contentWidth = () => {
4040+ const visible = items.filter((el) => el.style.display !== "none");
4141+ return visible.reduce((acc, el) => acc + el.offsetWidth, 0)
4242+ + gap * Math.max(0, visible.length - 1);
4343+ };
4444+1445 // No overflow — nothing to do
1515- if (nav.scrollWidth <= nav.clientWidth) return;
4646+ if (contentWidth() <= availableWidth) return;
16471717- // Show button (nav shrinks to accommodate it via flex)
4848+ // Show button (takes up space; subtract its width from available)
1849 btn.style.display = "";
19502020- // Hide items from right until nav content fits
5151+ // Hide items from right until content fits
2152 const hidden = [];
2253 for (let i = items.length - 1; i >= 0; i--) {
2323- if (nav.scrollWidth <= nav.clientWidth) break;
5454+ if (contentWidth() <= availableWidth - btn.offsetWidth) break;
2455 items[i].style.display = "none";
2556 hidden.unshift(items[i]);
2657 }
···4374 }
4475}
45767777+let _observer = /** @type {ResizeObserver | undefined} */ (undefined);
7878+4679export function watchResize() {
4780 const nav = document.getElementById("diffuse-nav");
4848- if (nav) new ResizeObserver(update).observe(nav);
8181+ if (!nav) return;
8282+ _observer?.disconnect();
8383+ _observer = new ResizeObserver(update);
8484+ // Observe the container — in column layouts the nav is content-sized and
8585+ // won't resize, but the container does as the header width changes.
8686+ _observer.observe(nav.parentElement ?? nav);
4987}
+2
src/common/pages/ppr.js
···2828 const path = relativePathname(url.pathname);
29293030 Nav.update();
3131+ Nav.updateActiveLinks();
3132 Nav.watchResize();
32333334 Grid.setupFilter();
···72737374 const url = new URL(event.destination.url);
7475 if (url.origin !== location.origin) return;
7676+ if (url.pathname === location.pathname) return;
75777678 // Only intercept paths one level deep
7779 const relative = relativePathname(url.pathname);