diff --git a/assets/layout.html b/assets/layout.html index 9e8088d..f8bbe1f 100644 --- a/assets/layout.html +++ b/assets/layout.html @@ -45,6 +45,21 @@
{{if not .EditMode}}{{end}}
diff --git a/assets/style.css b/assets/style.css index 1465054..a0a639f 100644 --- a/assets/style.css +++ b/assets/style.css @@ -48,6 +48,28 @@ --reading-width: 60rem; } +/* Cross-document view transitions: page navigations stay full reloads, but the + browser holds the old frame until the new page is ready and swaps them + without a blank flash. The header and tree rail are named so they swap in + place (no crossfade) — only the rest of the page fades, and only briefly. */ +@view-transition { navigation: auto; } +body > header { view-transition-name: app-header; } +.tree-sidebar { view-transition-name: tree-sidebar; } +::view-transition-group(*), +::view-transition-old(*), +::view-transition-new(*) { animation-duration: 0.12s; } +::view-transition-group(app-header), +::view-transition-group(tree-sidebar), +::view-transition-old(app-header), +::view-transition-new(app-header), +::view-transition-old(tree-sidebar), +::view-transition-new(tree-sidebar) { animation: none; } +@media (prefers-reduced-motion: reduce) { + ::view-transition-group(*), + ::view-transition-old(*), + ::view-transition-new(*) { animation: none; } +} + /* Anchor / TOC jumps are handled by the .center scroll container's own scroll-padding-top (the header no longer overlaps content in the app-shell), so no html-level scroll padding is needed. */ diff --git a/assets/tree-sidebar.js b/assets/tree-sidebar.js index 079daf7..61fb3a2 100644 --- a/assets/tree-sidebar.js +++ b/assets/tree-sidebar.js @@ -40,6 +40,7 @@ } })() var activeRow = null + var live = false // set once the fetched tree is on screen function disabledRow(text) { var row = document.createElement("div") @@ -169,15 +170,26 @@ } function render() { - container.textContent = "" + // If the layout pre-painted a snapshot of the previous page's tree, keep it + // on screen and build the live tree off-DOM, swapping it in once ready — + // no "…" placeholder, no scroll jump. var listing = document.createElement("div") - container.appendChild(listing) - listing.appendChild(disabledRow("…")) + var hasSnapshot = container.firstChild !== null + if (!hasSnapshot) { + container.appendChild(listing) + listing.appendChild(disabledRow("…")) + } fetchFolder("/", activePath === "/" ? "" : activePath) .then(function (resp) { listing.textContent = "" renderInto(listing, "/", resp.entries) + if (hasSnapshot) { + var top = container.scrollTop + container.replaceChildren(listing) + container.scrollTop = top + } + live = true if (activeRow) { try { activeRow.scrollIntoView({ block: "nearest" }) @@ -185,17 +197,34 @@ } }) .catch(function () { + live = false + container.replaceChildren(listing) listing.textContent = "" var err = document.createElement("div") err.className = "tree-row" err.textContent = "(failed — tap to retry)" err.addEventListener("click", function () { + container.textContent = "" render() }) listing.appendChild(err) }) } + // Snapshot the tree (markup + scroll) on the way out so the next page's + // layout can paint it immediately. Skipped until the live tree has rendered, + // so a placeholder or error row never becomes the snapshot. The active + // highlight is left out; the next page marks its own row. + window.addEventListener("pagehide", function () { + if (!live) return + if (activeRow) activeRow.classList.remove("is-active") + var html = container.innerHTML + if (activeRow) activeRow.classList.add("is-active") // bfcache may restore this page + try { + sessionStorage.setItem("tree-snapshot", JSON.stringify({ html: html, top: container.scrollTop })) + } catch (e) {} + }) + function setupMenuItem() { var specials = document.querySelector(".actions-special") if (!specials) return