Bottom-anchor banner artwork, instant navigation via in-memory page cache, seed/stars/structure tweaks
- Fix banner artwork sizing: explicit 100% grid track so children stretch instead of resolving height:100% against a content-sized row (SVG intrinsic ratio bloated the row, cropping the artwork's bottom). Bottom-anchor via object-position, transform-origin and YMax slice. - pagerite.js: in-memory page cache — preload every visible internal link once, serve navigation from memory without fetching; editors' loadPlain keeps the cache in sync (pagerite:page-fetched). - Structure editor: delete pages directly, no two-step confirmation. - New 'stars' banner design (drifting starfield) alongside 'eyes'. - Rewrite seed content: welcome page, three-level docs section covering all Markdown features (source + rendered), showcase hierarchy with image positioning and a simple leaf-page banner example.
This commit is contained in:
@@ -228,23 +228,7 @@ async function commitSlug(node, ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// Two-step delete (no dialogs): the first click arms the row's button for
|
||||
// a few seconds, the second actually deletes.
|
||||
const arming = ref(null)
|
||||
let armTimer = null
|
||||
|
||||
function armRemove(node) {
|
||||
if (arming.value === node.path) {
|
||||
clearTimeout(armTimer)
|
||||
arming.value = null
|
||||
removePage(node)
|
||||
} else {
|
||||
arming.value = node.path
|
||||
clearTimeout(armTimer)
|
||||
armTimer = setTimeout(() => { arming.value = null }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion is immediate, no confirmation.
|
||||
async function removePage(node) {
|
||||
const res = await fetch(`/_api/pages/${node.path}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
@@ -267,8 +251,7 @@ async function removePage(node) {
|
||||
provide('structureHandlers', {
|
||||
current: () => path.value,
|
||||
open: navigate,
|
||||
arming: () => arming.value,
|
||||
armRemove,
|
||||
removePage,
|
||||
reorder: onReorder,
|
||||
titleInput: onTitleInput,
|
||||
commitSlug,
|
||||
@@ -284,7 +267,6 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(armTimer)
|
||||
for (const t of Object.values(timers)) clearTimeout(t)
|
||||
removeEventListener('pagerite:editor-shown', onEditorShown)
|
||||
})
|
||||
|
||||
@@ -144,12 +144,11 @@ function onEnd() {
|
||||
v-if="element.has_content || !element.children.length"
|
||||
type="button"
|
||||
class="act del"
|
||||
:class="{ armed: handlers.arming() === element.path }"
|
||||
:title="element.children.length
|
||||
? 'delete the landing page (the category keeps its subpages)'
|
||||
: 'delete page'"
|
||||
@click="handlers.armRemove(element)"
|
||||
>{{ handlers.arming() === element.path ? 'delete?' : '✕' }}</button>
|
||||
@click="handlers.removePage(element)"
|
||||
>✕</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -300,12 +299,6 @@ body.tree-dragging .treelist {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Two-step delete: the first click arms the button, the second deletes. */
|
||||
.act.armed {
|
||||
color: #e06c75;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.del:hover {
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
@@ -132,22 +132,35 @@ body {
|
||||
overflow: hidden;
|
||||
/* Stack the design artwork and the page's own banner code on top of
|
||||
each other (artwork first): the banner is a background layer, author
|
||||
code overlays it. A single child behaves exactly as before. */
|
||||
code overlays it. A single child behaves exactly as before. The track
|
||||
is explicitly banner-sized: an auto row would size to the content
|
||||
(an SVG's intrinsic aspect ratio makes it far taller than the banner),
|
||||
and children's height:100% would resolve against that bloated row. */
|
||||
display: grid;
|
||||
grid-template: 100% / 100%;
|
||||
}
|
||||
|
||||
/* :not(style, script): author-level display:block would override the UA's
|
||||
display:none on those and render their source as banner text. */
|
||||
display:none on those and render their source as banner text. No
|
||||
width/height: the default stretch alignment fills the track without a
|
||||
percentage-resolution cycle. */
|
||||
#page-banner>*:not(style, script) {
|
||||
grid-area: 1 / 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* Banner artwork is bottom-anchored: the meaningful content (horizon,
|
||||
ground, characters) sits at the bottom, so on wide viewports — where
|
||||
the fixed-height banner crops the artwork vertically — the sky/top is
|
||||
what scrolls out of view. object-position covers replaced elements
|
||||
(img, canvas); inline SVG is anchored by the transform-origin below
|
||||
together with preserveAspectRatio="...YMax slice" in the artwork. */
|
||||
object-position: bottom;
|
||||
/* Scroll parallax: pagerite.js sets --pry on <html>; the banner stays
|
||||
windowed in place while the artwork drifts inside it. The scale
|
||||
provides overscan so the drift never reveals an edge. */
|
||||
provides overscan so the drift never reveals an edge; scaling from the
|
||||
bottom keeps the artwork's bottom edge pinned to the banner's. */
|
||||
transform: scale(1.25) translateY(var(--pry, 0px));
|
||||
transform-origin: bottom;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
|
||||
+35
-28
@@ -268,29 +268,31 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Preloading ------------------------------------------------------
|
||||
// Warm the HTTP cache with all linked pages and their resources, so
|
||||
// navigation (and the cube transition) is instant. Pages carry ETags,
|
||||
// so re-running this after each navigation revalidates cheaply (304)
|
||||
// and picks up changed content and images.
|
||||
// --- Page cache / preloading ------------------------------------------
|
||||
// Articles are deliberately NOT HTTP-cacheable, so speed comes from an
|
||||
// in-memory cache instead: at load (and after each swap) every visible
|
||||
// internal link is fetched exactly once, and navigation is served from
|
||||
// memory with no fetch at all. Editor re-renders (swapdoc.loadPlain)
|
||||
// announce their fresh copies via pagerite:page-fetched, keeping the
|
||||
// cache in sync after edits.
|
||||
const pageCache = new Map(); // pathname -> HTML text
|
||||
addEventListener("pagerite:page-fetched", (ev) => {
|
||||
pageCache.set(new URL(ev.detail.url, location.href).pathname, ev.detail.html);
|
||||
});
|
||||
|
||||
function preload() {
|
||||
const urls = new Set();
|
||||
for (const a of document.querySelectorAll('#nav a[href^="/"], #main a[href^="/"]')) {
|
||||
const urls = new Set([location.pathname]);
|
||||
for (const a of document.querySelectorAll(
|
||||
'#nav a[href^="/"], #sidebar a[href^="/"], #main a[href^="/"]',
|
||||
)) {
|
||||
urls.add(a.pathname);
|
||||
}
|
||||
for (const url of urls) {
|
||||
if (url === location.pathname) continue;
|
||||
if (pageCache.has(url)) continue;
|
||||
fetch(url)
|
||||
.then((r) => (r.ok ? r.text() : ""))
|
||||
.then((html) => {
|
||||
if (!html) return;
|
||||
// Off-screen parse: load the page's images and other resources
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
for (const img of doc.querySelectorAll("img")) {
|
||||
const i = new Image();
|
||||
i.src = img.src;
|
||||
}
|
||||
})
|
||||
.then((r) => (r.ok && (r.headers.get("content-type") || "").includes("text/html")
|
||||
? r.text() : ""))
|
||||
.then((html) => { if (html) pageCache.set(url, html); })
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -333,16 +335,21 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
}
|
||||
let doc;
|
||||
let finalUrl = url;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const type = res.headers.get("content-type") || "";
|
||||
if (!res.ok || !type.includes("text/html")) throw new Error("not a page");
|
||||
// Reflect any redirect the server issued.
|
||||
if (res.redirected) finalUrl = res.url;
|
||||
doc = new DOMParser().parseFromString(await res.text(), "text/html");
|
||||
} catch {
|
||||
location.href = url; // fall back to a normal navigation
|
||||
return;
|
||||
const cached = pageCache.get(new URL(url, location.href).pathname);
|
||||
if (cached) {
|
||||
doc = new DOMParser().parseFromString(cached, "text/html");
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const type = res.headers.get("content-type") || "";
|
||||
if (!res.ok || !type.includes("text/html")) throw new Error("not a page");
|
||||
// Reflect any redirect the server issued.
|
||||
if (res.redirected) finalUrl = res.url;
|
||||
doc = new DOMParser().parseFromString(await res.text(), "text/html");
|
||||
} catch {
|
||||
location.href = url; // fall back to a normal navigation
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (REGIONS.some((id) => !doc.getElementById(id))) {
|
||||
location.href = url;
|
||||
|
||||
@@ -100,18 +100,22 @@ function swapRegions(doc) {
|
||||
export async function loadPlain(p) {
|
||||
let doc
|
||||
let finalUrl = `/${p}`
|
||||
let html
|
||||
try {
|
||||
const res = await fetch(finalUrl)
|
||||
const type = res.headers.get('content-type') || ''
|
||||
if (!type.includes('text/html')) return null
|
||||
if (res.redirected) finalUrl = res.url
|
||||
doc = new DOMParser().parseFromString(await res.text(), 'text/html')
|
||||
html = await res.text()
|
||||
doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
} catch { return null }
|
||||
if (!doc.getElementById('main')) return null
|
||||
swapRegions(doc)
|
||||
history.replaceState(null, '', finalUrl)
|
||||
runScripts(document.getElementById('page-banner'))
|
||||
runScripts(document.getElementById('main'))
|
||||
// Keep pagerite.js's in-memory page cache in sync with the fresh copy.
|
||||
dispatchEvent(new CustomEvent('pagerite:page-fetched', { detail: { url: finalUrl, html } }))
|
||||
dispatchEvent(new CustomEvent('pagerite:preview')) // re-inject + re-tuck the edit pens
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user