Make the page transition configurable, add crossfade/slide/reveal
The cube view-transition block moves out of pagerite.css into transition
designs (pagerite/themes/{name}/transition.css), handled like banner
designs: served from disk at /_themes/..., injected by the backend as
#pagerite-transition after the banner sheet, and picked site-wide in the
site settings (Data.transition, default cube; GET/PUT /_api/settings
carry transition + the designs found on disk). The site editor swaps the
sheet in place on change; the dev-mode order fixup knows the new id.
Designs: cube (unchanged), crossfade (all navigations), slide (sideways,
both pages moving together) and reveal (clip-path wipe over the
stationary old page) — the latter two mirrored on history-back and
crossfading within a section like cube.
Also compress the site settings form into a two-column grid: site name +
favicon on one row, theme + transition on the next.
This commit is contained in:
@@ -30,6 +30,8 @@ Files are content-addressed (blake3[:12] + extension) and stored **on disk** und
|
||||
|
||||
`Data.theme` is the active theme name (empty = none/base only); themes are folders in `pagerite/themes/{name}` containing `theme.css` and/or `banner.css` (+ `banner.svg` artwork and any extra assets the CSS references, like summer's `grass.svg`), served by the backend at `/_themes/{name}/...` — read from disk per request (etag by mtime), never built, so on-disk edits show on the next page load even in prod. The theme selector and banner-design selector enumerate these folders via `GET /_api/settings`.
|
||||
|
||||
`Data.transition` is the page-transition design name (default `cube`): a theme folder shipping `transition.css`, injected as `#pagerite-transition` on every page and selected in the site editor (the selector enumerates `transition.css` folders via `GET /_api/settings`). See `docs/themes-and-assets.md`.
|
||||
|
||||
`Data.custom_css` is raw trusted CSS injected inline in every page `<head>` (id `pagerite-user`) and swapped during fetch-navigation; editable in the site editor. Font picks (heading/body/brand) in the site editor are stored as plain `:root` rows in `custom_css` (`--font-body: var(--font-source-sans);` format — parsed out and rewritten on change, the `:root` block added/removed as needed), referencing the per-family variables (`--font-source-sans` etc.) from `pagerite.css`; the base stylesheet's `--font-brand` defaults to `var(--font-heading)`.
|
||||
|
||||
`Data.favicon` names a file in the content-addressed store (on disk under `<hostname>/files/`), uploaded/cleared in the site editor via `PUT`/`DELETE /_api/settings/favicon`; when set it is linked as `<link rel="icon">` on every page, otherwise browsers fall back to the build's `/favicon.ico` by convention.
|
||||
|
||||
@@ -26,7 +26,7 @@ Pagerite is a single-user CMS/blog. This document records the initial high-level
|
||||
|
||||
- All pages share one static layout, defined once as an **html5tagger Template** with capitalized placeholders (`Title`, `Banner`, `Nav`, `Sidebar`, `Main`) filled per request. The dynamic regions carry stable ids (`#page-banner`, `#nav`, `#sidebar`, `#main`).
|
||||
- The page top is a **full-width banner header** with the site name and the navigation bar overlaid on it — no separate chrome header. The banner combines two layers, stacked in `#page-banner` (a grid, so they overlay): first the **banner design** — a named design living in a theme folder (`pagerite/themes/{name}/banner.css` plus artwork as `banner.html` — arbitrary markup like canvas + style + script — or `banner.svg`), chosen per page via `Node.banner_design` (a design name, "" for none, None to inherit from the nearest ancestor, then the front page, then the active theme's own design). The artwork is inlined into a `div[data-design]` wrapper: SVG artwork can be recolored from the theme stylesheet (corporate's single SVG serves both light and dark mode via `var()`-driven stops). Second, **per-page author code**: `Node.banner` holds an arbitrary trusted HTML snippet (an image, a styled div, canvas + script — anything), resolved by walking up the node's ancestors to the front page and rendered **after** the design artwork, so author styles always win over the design's own. The base stylesheet falls back to a plain gradient. There is deliberately no scrim fading the banner into the page background — any such fade would ruin user-supplied designs; themes that want one bake it into their SVG (purple does).
|
||||
- **Fetch-navigation.** Links are plain `<a href>`; a small script (`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions, the document title, and the site-wide custom CSS (`<style id="pagerite-user">` in `<head>`), keeping the rest of `<head>` and the layout chrome. Without JS everything works as normal page loads. Scripts inside fetched banner and content regions are re-created so they execute. Swaps run inside `document.startViewTransition` for a rotating cube page transition (CSS adapted from termotohtori.fi — the `::view-transition*` block is fragile, do not tweak; skipped under `prefers-reduced-motion`). Navigation within the same top-level section crossfades instead of rotating; browser back navigation rotates in reverse.
|
||||
- **Fetch-navigation.** Links are plain `<a href>`; a small script (`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions, the document title, and the site-wide custom CSS (`<style id="pagerite-user">` in `<head>`), keeping the rest of `<head>` and the layout chrome. Without JS everything works as normal page loads. Scripts inside fetched banner and content regions are re-created so they execute. Swaps run inside `document.startViewTransition` for the page transition selected in the site settings (`Data.transition`; the `cube` design — CSS adapted from termotohtori.fi, fragile, do not tweak — rotates, mirrored on browser back; `crossfade` fades; both skipped under `prefers-reduced-motion`). With `cube`, navigation within the same top-level section crossfades instead of rotating.
|
||||
- **The site structure is a tree of labels.** `Data.menu` holds the top-level items by slug, each with `children` keyed by slug — the URL path is the slug chain. The front page is a top-level node with slug "" (an item *parallel* to the other main level pages, not their parent) and cannot have children. The header navbar holds only the top level; a top-level item is highlighted when viewing any of its subpages. A page with published children lists them as **cards** after its content (the child page's share image as the cover, like the og tags, with the title overlaid); a **left sidebar** (`#sidebar`) with the section's sub-navigation appears only from the second level down, when there is something to navigate — main-level pages, sections with fewer than two published items, leaf pages and the front page render no aside element at all. Other sections' subitems are never shown without navigating into them first.
|
||||
- **Landing pages are optional.** Every label can either have content (`Node.content`, a Markdown page) or none — a content-less label renders a 404 page listing its children as cards (with a pen to create the landing page) instead of redirecting, while nav links to it point straight at its first child, so categories need no filler content and normal navigation never sees the 404. Title and slug of every label are editable; renaming a slug moves the whole subtree. The sidebar never lists the section itself, avoiding title duplication with the navbar.
|
||||
- **Menu order is manual.** Each node has a fractional `order` key among its siblings; reordering/moving writes only the moved node (it takes a fresh value halfway between its new siblings; all other items keep theirs). New pages append at the end of their menu. Structure edits (reorder, move/rename with the whole subtree, retitle) go through `POST /_api/structure` and the editor's structure panel.
|
||||
@@ -46,7 +46,7 @@ Pagerite is a single-user CMS/blog. This document records the initial high-level
|
||||
|
||||
- Editing happens **in place**, in two modes opened by two pens:
|
||||
- **Page mode** — the 🖊️ next to a page's heading (including 404s, which is how new pages start) opens a CodeMirror Markdown editor docked to the left of the article: the panel is fixed to the viewport's left edge (its top tracks the banner's bottom until the banner scrolls away), the content shifts right and the sidebar hides while editing. Preview renders server-side per keystroke (no debouncing) and swaps the whole visible article content in one go (the edit pen and category cards survive the swap).
|
||||
- **Site mode** — the ⚙️ at the top right (after the 📊 analytics link, before login) opens a panel with the site **brand** (applied to the header live), a **theme** selector (swapping the theme stylesheet in place), **font** picks (heading/body/brand — stored as plain `:root` rows inside the custom CSS, referencing the base stylesheet's per-family font variables), a **site-wide custom CSS** field (injected into `<style id="pagerite-user">` in the live page head and swapped during fetch-navigation), the page's **banner design** selector (inherit / none / any design found on disk, inherited by children), the page's **banner HTML** field (supplementing the design, previewed into the real banner region, so you see exactly which banner you're editing) and the **structure tree**. Everything saves immediately as you edit — no save button, no edit mode.
|
||||
- **Site mode** — the ⚙️ at the top right (after the 📊 analytics link, before login) opens a panel with the site **brand** (applied to the header live), a **theme** selector (swapping the theme stylesheet in place), a **page transition** selector (`cube`/`crossfade`, swapping `#pagerite-transition` in place), **font** picks (heading/body/brand — stored as plain `:root` rows inside the custom CSS, referencing the base stylesheet's per-family font variables), a **site-wide custom CSS** field (injected into `<style id="pagerite-user">` in the live page head and swapped during fetch-navigation), the page's **banner design** selector (inherit / none / any design found on disk, inherited by children), the page's **banner HTML** field (supplementing the design, previewed into the real banner region, so you see exactly which banner you're editing) and the **structure tree**. Everything saves immediately as you edit — no save button, no edit mode.
|
||||
- Clicking a pen again closes the editor (without saving; a dirty preview reloads the page). The pens are `<button>`s wired up by `pagerite.js` — editing is an action, not a navigation. The editor's WebSocket **reconnects automatically** with local text and pending saves preserved. (All users are trusted authors for now; access control later with SSO.)
|
||||
- **CodeMirror 6** for Markdown editing (no WYSIWYG), title/published controls. Images can be pasted straight into the editor or chosen via a file input: they upload to the content store (`PUT /_api/files/...`) and insert `` at the cursor.
|
||||
- The **structure panel** (vue-draggable tree of the whole site, in site mode) covers page management: reorder any menu level, drag across sections, add, delete (two clicks: the button arms, then deletes — no dialogs). Every node is a real label — content-less category rows offer a ➕ to give them a landing page. Deleting a category removes only its landing page (the label and its subpages stay). Every non-empty list ends with a ➕ row that starts a new page as a local-only tree row at that level; the row can be dragged into place before its title and slug are filled in and is persisted only on commit. While dragging, these ➕ rows double as "end of this list" drop targets; dropping ON the lower part of a row makes the page that row's first child (even a leaf's, creating a sublist), while a row's exposed top edge inserts a sibling before it. A dragged row's indentation previews the target list's depth. Rows are always editable: titles save while typing, slug edits commit on blur/Enter since they rename the path (moving the whole subtree). The front page is the root row with an empty slug — renaming it away leaves no front page ("/" redirects to the first nav item), and giving another top-level row the empty slug makes it the front page.
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ The shell hosts four kept-alive tabs (ordered site-wide first — site, structur
|
||||
|
||||
- `PageEditor.vue` — CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor and article scrolls are linked piecewise-linearly, keyed on the section anchors' `data-line` (markdown source line the backend stamps on top-level anchored h1/h2s): the page follows the cursor (fractional, wrap-aware, anchored at a fixed window height), the editor follows page scroll with a progress-based viewport anchor, applied instantly (the window keeps scrolling normally while any editor is open — the panel is fixed to the viewport's left edge, its top tracking the banner's bottom edge until the banner scrolls away — and the panel scrolls internally); anchored h2s carry their own edit pens that open the editor scrolled to that section; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload, with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path.
|
||||
- `BannerEditor.vue` — per-page banner HTML + banner design selector, previewed into `#page-banner`.
|
||||
- `SiteEditor.vue` — site brand + optional custom brand HTML with image/video upload + theme selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into `<head id="pagerite-user">`.
|
||||
- `SiteEditor.vue` — site brand + optional custom brand HTML with image/video upload + theme selector + page-transition selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into `<head id="pagerite-user">`.
|
||||
- `StructureEditor.vue` — the vue-draggable structure tree with always-editable title/slug inputs per row.
|
||||
|
||||
Media uploads everywhere use the image icon buttons (pasting into the editor works too). The article, banner and site-settings pens are shorthands that open the shell on the matching tab; once open, clicking a pen switches tabs (and retargets the editors to the current page) instead of closing/remounting. The close button in the tab bar closes the shell (Escape too); tabs have no close buttons of their own. Closing only HIDES the shell — the Vue app stays mounted, so page-editor state (unsaved text included) survives until a real page reload; the editor always follows the URL, so fetch-navigating with the shell open (or before re-opening it) retargets it to the new page — unsaved text is stashed per path for the session and restored when returning, cleared on save. Saving there is explicit (Ctrl+S) and refreshes the page regions in place. Admin panels never reload the page.
|
||||
@@ -25,6 +25,6 @@ Dropping ON the lower part of a row moves the page under that row (the child lis
|
||||
|
||||
The shell is dynamic-imported onto the content page by pagerite.js when an edit pen is clicked (the pens are injected by pagerite.js after the session validates; they carry `data-editor-src`/`data-editor-css`/`data-editor-mode`). In dev, modules load from the Vite dev server (`PAGERITE_VITE_URL`), in prod from the hashed build assets resolved via `frontend-build/.vite/manifest.json`.
|
||||
|
||||
`vite.config.js` sets `appType: 'mpa'` (no SPA fallback) and builds with `manifest: true`, `assetsDir: '_/assets'` (so the build mirrors the URL space; `frontend/public/favicon.ico` lands at the build root and is served at `/favicon.ico`). JS inputs are `src/main.js` and `src/pagerite.js`, plus `src/assets/pagerite.css` as a separate stylesheet entry; theme and banner-design CSS are NOT built — they live in `pagerite/themes/{name}/` and are served by the backend. There is no `index.html` source (it would shadow `/` and turn missing dev paths into an empty Vue shell). All outputs are ES modules. The build sets `preserveEntrySignatures: 'exports-only'` because main.js is consumed via dynamic `import()` for its `openEditor`/`closeEditor` exports — Vite app builds otherwise strip unused entry exports, leaving dead edit pens. In dev the backend links theme/banner-design stylesheets like in prod (`/_themes/...`); only the base CSS is Vite-injected from JS, and pagerite.js then re-appends the `#pagerite-theme`/`#pagerite-banner`/`#pagerite-user` elements to restore the canonical order (base < theme < design < custom CSS). In production all page assets are inlined instead (styles as `<style id="pagerite-…">` in `<head>`, scripts at the end of the body). Theme switches in the site editor swap the `#pagerite-theme` element in place — the link href in dev, the inline style's text (fetched from `/_themes/...`) in prod.
|
||||
`vite.config.js` sets `appType: 'mpa'` (no SPA fallback) and builds with `manifest: true`, `assetsDir: '_/assets'` (so the build mirrors the URL space; `frontend/public/favicon.ico` lands at the build root and is served at `/favicon.ico`). JS inputs are `src/main.js` and `src/pagerite.js`, plus `src/assets/pagerite.css` as a separate stylesheet entry; theme, banner-design and transition CSS are NOT built — they live in `pagerite/themes/{name}/` and are served by the backend. There is no `index.html` source (it would shadow `/` and turn missing dev paths into an empty Vue shell). All outputs are ES modules. The build sets `preserveEntrySignatures: 'exports-only'` because main.js is consumed via dynamic `import()` for its `openEditor`/`closeEditor` exports — Vite app builds otherwise strip unused entry exports, leaving dead edit pens. In dev the backend links theme/banner-design stylesheets like in prod (`/_themes/...`); only the base CSS is Vite-injected from JS, and pagerite.js then re-appends the `#pagerite-theme`/`#pagerite-banner`/`#pagerite-transition`/`#pagerite-user` elements to restore the canonical order (base < theme < design < transition < custom CSS). In production all page assets are inlined instead (styles as `<style id="pagerite-…">` in `<head>`, scripts at the end of the body). Theme switches in the site editor swap the `#pagerite-theme` element in place — the link href in dev, the inline style's text (fetched from `/_themes/...`) in prod.
|
||||
|
||||
`vite-plugin-fastapi.js` has an auto-upgrade marker — edit `vite.config.js`, not the plugin.
|
||||
|
||||
@@ -12,13 +12,13 @@ Public page entry; runs fetch-navigation (backed by an in-memory page cache: eve
|
||||
|
||||
It first probes `GET /auth/api/settings` to detect whether Paskia SSO is available, then `GET /_api/settings` to learn the current session's admin status. The same reverse proxy that gates `/_api` returns 401 for anonymous users, 403 for users without the admin permission, and 200 for admins. When Paskia is detected, a login link (anonymous) or profile link (logged in) is shown in the banner corner; both are plain `<a href="/auth/">` links (Paskia does not support being iframed, so we navigate normally), and a `pageshow` handler re-probes auth when history navigation restores a cached page. Admins also get the page/banner edit pens and a site-settings pen, plus a `modulepreload` warm-up of the editor bundle (the hashed asset is immutable, so it costs nothing). If no Paskia SSO is detected (dev/no proxy), editing is left open. Pages themselves render identically for everyone; the real gate is the auth proxy in front of all of `/_api`.
|
||||
|
||||
Asset wiring differs by mode. In dev the backend links the Vite dev-server URLs (`pagerite:editor-src`/`-css`/`pagerite:analytics-src` meta tags, `<link>` stylesheets) and Vite injects the entry CSS from JS for hot reloads. In production there are no pagerite meta tags: all page assets are inlined into the document — stylesheets as `<style>` elements in `<head>` (fixed order: base, theme, banner design, entry sheets, custom CSS last), module scripts as inline `<script>`s at the end of the body (relative chunk imports are rewritten to absolute `/_assets/` paths) — and the on-demand bundles' URLs ride in a `<script type="application/json" id="pagerite-assets">` config. The editor bundle always stays external, imported on demand when a pen is opened. Every stylesheet element carries a stable id so fetch-navigation and the site editor can sync `<head>` positionally across swaps (the analytics sheet exists on `/_a` only and is added/removed as you navigate). The analytics entry is inlined into the `/_a` page itself; pagerite.js re-creates that script element after fetch-navigating there (inline scripts don't execute on a DOM swap) and calls the module's exposed unmount before swapping away.
|
||||
Asset wiring differs by mode. In dev the backend links the Vite dev-server URLs (`pagerite:editor-src`/`-css`/`pagerite:analytics-src` meta tags, `<link>` stylesheets) and Vite injects the entry CSS from JS for hot reloads. In production there are no pagerite meta tags: all page assets are inlined into the document — stylesheets as `<style>` elements in `<head>` (fixed order: base, theme, banner design, page transition, entry sheets, custom CSS last), module scripts as inline `<script>`s at the end of the body (relative chunk imports are rewritten to absolute `/_assets/` paths) — and the on-demand bundles' URLs ride in a `<script type="application/json" id="pagerite-assets">` config. The editor bundle always stays external, imported on demand when a pen is opened. Every stylesheet element carries a stable id so fetch-navigation and the site editor can sync `<head>` positionally across swaps (the analytics sheet exists on `/_a` only and is added/removed as you navigate). The analytics entry is inlined into the `/_a` page itself; pagerite.js re-creates that script element after fetch-navigating there (inline scripts don't execute on a DOM swap) and calls the module's exposed unmount before swapping away.
|
||||
|
||||
## `assets/`
|
||||
|
||||
Shared styles and data files built by Vite and served hashed under `/_assets/`: `pagerite.css` (base layout + conservative variables), `pygments.css`, and `fonts/` (self-hosted Source Sans 3/Source Serif 4/Fraunces/Literata/Cormorant/Playfair Display/Inter/Montserrat/Fira Code/Cause/Exo 2/New Rocker variable woff2).
|
||||
|
||||
The `::view-transition*` block at the end of `pagerite.css` (from termotohtori.fi) is fragile — do not tweak. Themes and banner designs are NOT built — they live in `pagerite/themes/{name}/` and are served by the backend. See `docs/themes-and-assets.md` for details.
|
||||
The `::view-transition*` rules live in the page-transition designs (`pagerite/themes/{cube,crossfade}/transition.css`), not in the base stylesheet. Themes, banner designs and transitions are NOT built — they live in `pagerite/themes/{name}/` and are served by the backend. See `docs/themes-and-assets.md` for details.
|
||||
|
||||
Vite builds ES-module `.js` outputs; in dev the backend links them as `<script type="module">` (module scripts defer by default), in production it inlines them at the end of the body.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Files under `frontend/src/assets/` are built by Vite and served hashed under `/_
|
||||
- `pygments.css` — Pygments token styles mapped onto the `--code-*` variables.
|
||||
- `fonts/` — self-hosted variable woff2 files for Source Sans 3, Source Serif 4, Fraunces, Literata, Cormorant, Playfair Display, Inter, Montserrat, Fira Code, Cause, Exo 2 and New Rocker.
|
||||
|
||||
The `::view-transition*` block at the end of `pagerite.css` (from termotohtori.fi) is fragile — do not tweak.
|
||||
The `::view-transition*` rules are not in the base stylesheet: they live in the page-transition designs (`pagerite/themes/{name}/transition.css`, see below).
|
||||
|
||||
## Themes
|
||||
|
||||
@@ -32,6 +32,17 @@ A theme folder may also ship a banner design (`banner.css` + `banner.html` arbit
|
||||
|
||||
The banner artwork has scroll parallax: pagerite.js sets the `--pry` scroll parameter on `<html>` (event-driven, so it is still when the page is idle), the banner contents drift within their window (with scale overscan so no edge shows), and designs may key their own effects off the same parameter.
|
||||
|
||||
## Page transitions
|
||||
|
||||
A theme folder may ship a page transition (`transition.css`, `::view-transition*` rules), selected site-wide by `Data.transition` in the site settings and injected as `#pagerite-transition` (after the banner design). Standalone transition designs ship as:
|
||||
|
||||
- `cube` — rotating cube (from termotohtori.fi; the block is fragile — do not tweak), mirrored on history-back (`html.nav-back`), crossfading within a section (`html.nav-fade`).
|
||||
- `slide` — plain sideways slide, old and new pages moving together; mirrored on history-back, crossfading within a section.
|
||||
- `reveal` — clip-path wipe revealing the new page over the stationary old one; mirrored on history-back, crossfading within a section.
|
||||
- `crossfade` — plain crossfade for all navigations.
|
||||
|
||||
pagerite.js toggles the `nav-back`/`nav-fade` classes on `<html>` around `document.startViewTransition` (skipped under `prefers-reduced-motion`); a transition design keys its `::view-transition*` rules off them as needed.
|
||||
|
||||
## Stylesheet order
|
||||
|
||||
The backend emits the stylesheets in a fixed order — base (Vite build), theme, banner design, entry sheets, custom CSS last — each with a stable id so fetch-navigation and the site editor can sync them in place. In dev they are `<link>`s (the base is Vite-injected from JS instead); in production they are inlined as `<style>` elements. The base stylesheet's `--font-brand` defaults to `var(--font-heading)`. Code text (Fira Code by default) is optically matched to the body font by x-height: `font-size-adjust: ex-height var(--code-x-height)` scales whatever code font is in use, so a theme that switches its body font sets `--code-x-height` to that font's x-height ratio (base: 0.478 for Source Sans 3; themes ship values for Inter, Montserrat, Literata and Cause).
|
||||
The backend emits the stylesheets in a fixed order — base (Vite build), theme, banner design, page transition, entry sheets, custom CSS last — each with a stable id so fetch-navigation and the site editor can sync them in place. In dev they are `<link>`s (the base is Vite-injected from JS instead); in production they are inlined as `<style>` elements. The base stylesheet's `--font-brand` defaults to `var(--font-heading)`. Code text (Fira Code by default) is optically matched to the body font by x-height: `font-size-adjust: ex-height var(--code-x-height)` scales whatever code font is in use, so a theme that switches its body font sets `--code-x-height` to that font's x-height ratio (base: 0.478 for Source Sans 3; themes ship values for Inter, Montserrat, Literata and Cause).
|
||||
|
||||
+92
-19
@@ -57,6 +57,10 @@ const theme = ref('')
|
||||
// Theme options come from the backend (theme folders on disk, see GET
|
||||
// /_api/settings), so added themes need no frontend changes.
|
||||
const themeOptions = ref([{ value: '', label: 'none' }])
|
||||
// Page transition (cube, crossfade, ...): a design folder with
|
||||
// transition.css under pagerite/themes/, injected as #pagerite-transition.
|
||||
const transition = ref('cube')
|
||||
const transitionOptions = ref([])
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
@@ -72,6 +76,8 @@ async function loadSettings() {
|
||||
{ value: '', label: 'none' },
|
||||
...(s.themes || []).map((t) => ({ value: t, label: t })),
|
||||
]
|
||||
transition.value = s.transition || 'cube'
|
||||
transitionOptions.value = s.transitions || []
|
||||
} catch { /* keep default */ }
|
||||
}
|
||||
|
||||
@@ -230,6 +236,7 @@ async function saveSettings(opts = {}) {
|
||||
theme: theme.value,
|
||||
custom_css: customCss.value,
|
||||
brand_html: brandHtml.value,
|
||||
transition: transition.value,
|
||||
...opts,
|
||||
}),
|
||||
})
|
||||
@@ -285,6 +292,37 @@ async function onThemeChange() {
|
||||
loadPlain(path.value)
|
||||
}
|
||||
|
||||
async function onTransitionChange() {
|
||||
await saveSettings()
|
||||
// Transition CSS is backend-served at /_themes/{name}/transition.css in
|
||||
// both dev and prod (<link> in dev, inline <style> in prod), like the
|
||||
// theme. Swap #pagerite-transition in place — it only styles view
|
||||
// transitions, so no re-render of the page regions is needed.
|
||||
let el = document.getElementById('pagerite-transition')
|
||||
const url = `/_themes/${transition.value}/transition.css`
|
||||
if (el?.tagName === 'STYLE') {
|
||||
el.textContent = await (await fetch(url)).text()
|
||||
} else if (el) {
|
||||
el.href = url
|
||||
} else {
|
||||
// Missing (created before this feature, or "none" saved directly):
|
||||
// re-create, keeping base < theme < design < transition < custom CSS.
|
||||
if (import.meta.env.DEV) {
|
||||
el = document.createElement('link')
|
||||
el.rel = 'stylesheet'
|
||||
el.href = url
|
||||
} else {
|
||||
el = document.createElement('style')
|
||||
el.textContent = await (await fetch(url)).text()
|
||||
}
|
||||
el.id = 'pagerite-transition'
|
||||
const before = document.getElementById('pagerite-banner')?.nextSibling
|
||||
?? document.getElementById('pagerite-user')
|
||||
if (before) before.before(el)
|
||||
else document.head.append(el)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Site-wide custom CSS --------------------------------------------------
|
||||
// Edits apply to the live page immediately and save while typing.
|
||||
function applyCustomCss(css) {
|
||||
@@ -483,6 +521,7 @@ onUnmounted(() => {
|
||||
<div v-if="saveError">{{ saveError }}</div>
|
||||
|
||||
<section class="block">
|
||||
<div class="field-grid">
|
||||
<label class="field">
|
||||
<span class="field-label">site name</span>
|
||||
<input
|
||||
@@ -492,6 +531,25 @@ onUnmounted(() => {
|
||||
@input="onBrandInput"
|
||||
/>
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">favicon</span>
|
||||
<button
|
||||
type="button"
|
||||
class="favicon-tile"
|
||||
title="upload favicon (ico, png, svg...)"
|
||||
@click="faviconInput.click()"
|
||||
>
|
||||
<img v-if="favicon" :src="favicon" alt="current favicon" />
|
||||
<span v-else>?</span>
|
||||
</button>
|
||||
<input
|
||||
ref="faviconInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
@change="(ev) => { uploadFavicon(ev.target.files[0]); ev.target.value = '' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">theme</span>
|
||||
<select
|
||||
@@ -514,6 +572,20 @@ onUnmounted(() => {
|
||||
A
|
||||
</button>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">transition</span>
|
||||
<select
|
||||
v-model="transition"
|
||||
class="text-input theme-select"
|
||||
title="Page transition"
|
||||
@change="onTransitionChange"
|
||||
>
|
||||
<option v-for="t in transitionOptions" :key="t" :value="t">
|
||||
{{ t }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="fontPicker" class="font-picker">
|
||||
<div class="font-tabs">
|
||||
<button
|
||||
@@ -555,25 +627,6 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">favicon</span>
|
||||
<button
|
||||
type="button"
|
||||
class="favicon-tile"
|
||||
title="upload favicon (ico, png, svg...)"
|
||||
@click="faviconInput.click()"
|
||||
>
|
||||
<img v-if="favicon" :src="favicon" alt="current favicon" />
|
||||
<span v-else>?</span>
|
||||
</button>
|
||||
<input
|
||||
ref="faviconInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
@change="(ev) => { uploadFavicon(ev.target.files[0]); ev.target.value = '' }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block grow" @paste="onBrandPaste">
|
||||
@@ -641,6 +694,26 @@ onUnmounted(() => {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Two-column grid (site name + favicon, theme + transition) so the rows
|
||||
and labels align with each other. */
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.4rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Labels share one narrow track per field, sized to the longest label
|
||||
("transition"), so they line up across rows without excess space. */
|
||||
.field-grid > .field {
|
||||
display: grid;
|
||||
grid-template-columns: 4.3rem 1fr auto;
|
||||
}
|
||||
|
||||
.field-grid .field-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Fixed-width labels keep the settings rows aligned. */
|
||||
.field > .field-label {
|
||||
min-width: 5rem;
|
||||
|
||||
@@ -1547,97 +1547,6 @@ footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Rotating-cube page transition. */
|
||||
::view-transition {
|
||||
perspective: 1000px;
|
||||
inset: 0;
|
||||
background: color-mix(var(--bg) 50%, black 50%);
|
||||
}
|
||||
|
||||
::view-transition-group(root),
|
||||
::view-transition-image-pair(root) {
|
||||
transform-style: preserve-3d;
|
||||
isolation: auto;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
backface-visibility: hidden;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes group-rotate {
|
||||
to {
|
||||
transform: rotateY(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out-a-bit {
|
||||
to {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-a-bit {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
transform-origin: 50% 50% -50vw;
|
||||
animation: 300ms ease-in-out forwards group-rotate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 300ms ease-in-out forwards fade-out-a-bit;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
transform-origin: 0 0;
|
||||
transform: rotateY(90deg);
|
||||
inset: 0 auto 0 100%;
|
||||
animation: 300ms ease-in-out forwards fade-in-a-bit;
|
||||
}
|
||||
|
||||
/* Reverse direction for browser back navigation (same geometry, mirrored). */
|
||||
@keyframes group-rotate-back {
|
||||
to {
|
||||
transform: rotateY(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-group(root) {
|
||||
animation-name: group-rotate-back;
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-new(root) {
|
||||
transform-origin: 100% 0;
|
||||
transform: rotateY(-90deg);
|
||||
inset: 0 100% 0 auto;
|
||||
}
|
||||
|
||||
/* Same-section navigation: a plain crossfade instead of the cube. These
|
||||
rules only override animation/geometry, leaving the FRAGILE block's
|
||||
perspective and layering untouched. The old snapshot stays fully opaque
|
||||
underneath while the new one fades in on top — never a dip to black. */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-group(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-old(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-new(root) {
|
||||
transform: none;
|
||||
inset: 0;
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
/* Page transition styles are NOT here: they live in the transition designs
|
||||
(pagerite/themes/{cube,crossfade}/transition.css), injected by the backend
|
||||
as #pagerite-transition per the site settings. */
|
||||
|
||||
@@ -27,7 +27,7 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
// canonical order: base < theme < banner design < custom CSS (whose
|
||||
// equal-specificity :root rules — font variables — must win by order).
|
||||
import("./assets/pagerite.css").then(() => {
|
||||
for (const id of ["pagerite-theme", "pagerite-banner", "pagerite-user"]) {
|
||||
for (const id of ["pagerite-theme", "pagerite-banner", "pagerite-transition", "pagerite-user"]) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) document.head.append(el);
|
||||
}
|
||||
@@ -687,7 +687,8 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
applyEffects();
|
||||
mountAnalytics(doc);
|
||||
};
|
||||
// Rotating cube page transition (see the FRAGILE block in pagerite.css);
|
||||
// Rotating cube page transition (styles injected as #pagerite-transition
|
||||
// from the selected design's transition.css, e.g. themes/cube/);
|
||||
// mirrored when navigating back through history. Navigation within the
|
||||
// same top-level section crossfades instead, in either direction.
|
||||
if (document.startViewTransition && !reduceMotion.matches) {
|
||||
|
||||
+8
-4
@@ -364,12 +364,12 @@ file_store = FileStore(FILES_DIR)
|
||||
def _render_html(kind: str, path: str, base_url: str) -> str:
|
||||
"""Render one of the generated pages (see _html_response)."""
|
||||
if kind == "page":
|
||||
return views.render_page(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html, base_url)
|
||||
return views.render_page(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html, base_url, transition=data.transition)
|
||||
if kind == "category":
|
||||
return views.render_category(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html)
|
||||
return views.render_category(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html, transition=data.transition)
|
||||
if kind == "not-found":
|
||||
return views.render_not_found(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html)
|
||||
return views.render_analytics(data.menu, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html)
|
||||
return views.render_not_found(data.menu, path, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html, transition=data.transition)
|
||||
return views.render_analytics(data.menu, data.brand, data.custom_css, data.theme, data.favicon, data.brand_html, transition=data.transition)
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
@@ -559,6 +559,8 @@ async def get_settings() -> dict:
|
||||
"favicon": f"/_f/{data.favicon}" if data.favicon else "",
|
||||
"themes": views._theme_names(),
|
||||
"banner_designs": views._banner_design_names(),
|
||||
"transition": data.transition,
|
||||
"transitions": views._transition_names(),
|
||||
}
|
||||
|
||||
|
||||
@@ -569,6 +571,7 @@ class SettingsIn(BaseModel):
|
||||
theme: str
|
||||
custom_css: str
|
||||
brand_html: str = ""
|
||||
transition: str = "cube"
|
||||
|
||||
|
||||
@app.put("/_api/settings", status_code=204)
|
||||
@@ -579,6 +582,7 @@ async def put_settings(settings: SettingsIn) -> None:
|
||||
data.brand_html = settings.brand_html
|
||||
data.theme = settings.theme
|
||||
data.custom_css = settings.custom_css
|
||||
data.transition = settings.transition
|
||||
data.version += 1
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ class Data(msgspec.Struct):
|
||||
#: pagerite/themes/{theme}/ (theme.css and/or banner.css/banner.svg/
|
||||
#: banner.html), served by the backend from disk.
|
||||
theme: str = "purple"
|
||||
#: Page transition design name (cube, crossfade, ...). Designs live in
|
||||
#: pagerite/themes/{name}/transition.css and are injected as
|
||||
#: #pagerite-transition on every page.
|
||||
transition: str = "cube"
|
||||
#: Raw site-wide custom CSS, injected inline in every page <head>.
|
||||
#: Trusted author content; not sanitized.
|
||||
custom_css: str = ""
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* Crossfade page transition. Injected by the backend as #pagerite-transition
|
||||
when the "crossfade" transition is selected in the site settings. The old
|
||||
snapshot stays fully opaque underneath while the new one fades in on top —
|
||||
never a dip to black. Direction-neutral, so html.nav-back needs no
|
||||
mirroring (and same-section html.nav-fade changes nothing). */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/* Rotating-cube page transition (from termotohtori.fi). FRAGILE — do not
|
||||
tweak. Injected by the backend as #pagerite-transition when the "cube"
|
||||
transition is selected in the site settings. pagerite.js toggles
|
||||
html.nav-back for history-back navigation and html.nav-fade for
|
||||
same-section navigation. */
|
||||
::view-transition {
|
||||
perspective: 1000px;
|
||||
inset: 0;
|
||||
background: color-mix(var(--bg) 50%, black 50%);
|
||||
}
|
||||
|
||||
::view-transition-group(root),
|
||||
::view-transition-image-pair(root) {
|
||||
transform-style: preserve-3d;
|
||||
isolation: auto;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
backface-visibility: hidden;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes group-rotate {
|
||||
to {
|
||||
transform: rotateY(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out-a-bit {
|
||||
to {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-a-bit {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
transform-origin: 50% 50% -50vw;
|
||||
animation: 300ms ease-in-out forwards group-rotate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 300ms ease-in-out forwards fade-out-a-bit;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
transform-origin: 0 0;
|
||||
transform: rotateY(90deg);
|
||||
inset: 0 auto 0 100%;
|
||||
animation: 300ms ease-in-out forwards fade-in-a-bit;
|
||||
}
|
||||
|
||||
/* Reverse direction for browser back navigation (same geometry, mirrored). */
|
||||
@keyframes group-rotate-back {
|
||||
to {
|
||||
transform: rotateY(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-group(root) {
|
||||
animation-name: group-rotate-back;
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-new(root) {
|
||||
transform-origin: 100% 0;
|
||||
transform: rotateY(-90deg);
|
||||
inset: 0 100% 0 auto;
|
||||
}
|
||||
|
||||
/* Same-section navigation: a plain crossfade instead of the cube. These
|
||||
rules only override animation/geometry, leaving the block above's
|
||||
perspective and layering untouched. The old snapshot stays fully opaque
|
||||
underneath while the new one fades in on top — never a dip to black. */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-group(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-old(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-new(root) {
|
||||
transform: none;
|
||||
inset: 0;
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* Wipe-reveal page transition: the old page stays put while the new one is
|
||||
revealed on top of it by a clip-path wipe sweeping left to right.
|
||||
Injected by the backend as #pagerite-transition when the "reveal"
|
||||
transition is selected in the site settings. Mirrored on history-back
|
||||
(html.nav-back: the wipe sweeps right to left); same-section navigation
|
||||
crossfades (html.nav-fade). */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes reveal-right {
|
||||
from {
|
||||
clip-path: inset(0 100% 0 0);
|
||||
}
|
||||
to {
|
||||
clip-path: inset(0);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
clip-path: inset(0);
|
||||
animation: 350ms ease-in-out reveal-right;
|
||||
}
|
||||
|
||||
/* Reverse direction for browser back navigation. */
|
||||
@keyframes reveal-left {
|
||||
from {
|
||||
clip-path: inset(0 0 0 100%);
|
||||
}
|
||||
to {
|
||||
clip-path: inset(0);
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-new(root) {
|
||||
animation-name: reveal-left;
|
||||
}
|
||||
|
||||
/* Same-section navigation: a plain crossfade instead of the wipe. The old
|
||||
snapshot stays fully opaque underneath while the new one fades in on
|
||||
top — never a dip to black. */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-new(root) {
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/* Horizontal slide page transition: the old page slides left while the new
|
||||
one follows from the right, both moving together like pages side by side.
|
||||
Injected by the backend as #pagerite-transition when the "slide"
|
||||
transition is selected in the site settings. Mirrored on history-back
|
||||
(html.nav-back); same-section navigation crossfades (html.nav-fade). Both
|
||||
snapshots cover the viewport throughout, so no background shows between
|
||||
them. */
|
||||
::view-transition {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes slide-out-left {
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: 300ms ease-in-out forwards slide-out-left;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 300ms ease-in-out slide-in-right;
|
||||
}
|
||||
|
||||
/* Reverse direction for browser back navigation. */
|
||||
@keyframes slide-out-right {
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-left {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-old(root) {
|
||||
animation-name: slide-out-right;
|
||||
}
|
||||
|
||||
html.nav-back::view-transition-new(root) {
|
||||
animation-name: slide-in-left;
|
||||
}
|
||||
|
||||
/* Same-section navigation: a plain crossfade instead of the slide. The old
|
||||
snapshot stays fully opaque underneath while the new one fades in on
|
||||
top — never a dip to black. */
|
||||
@keyframes nav-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-old(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
html.nav-fade::view-transition-new(root) {
|
||||
animation: 200ms ease-in-out nav-fade-in;
|
||||
}
|
||||
+30
-4
@@ -64,6 +64,13 @@ def _banner_design_names() -> list[str]:
|
||||
)
|
||||
|
||||
|
||||
def _transition_names() -> list[str]:
|
||||
"""Available page-transition designs: theme folders with transition.css."""
|
||||
return sorted(
|
||||
d.name for d in THEMES.iterdir() if d.is_dir() and (d / "transition.css").exists()
|
||||
)
|
||||
|
||||
|
||||
def _valid_name(name: str) -> bool:
|
||||
"""Guard against path traversal in theme/design names."""
|
||||
return bool(name) and "/" not in name and not name.startswith(".")
|
||||
@@ -94,6 +101,17 @@ def _banner_css_url(design: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _transition_css_url(transition: str) -> str | None:
|
||||
"""URL for a page-transition stylesheet, served by the backend."""
|
||||
if (
|
||||
transition
|
||||
and _valid_name(transition)
|
||||
and (THEMES / transition / "transition.css").exists()
|
||||
):
|
||||
return f"/_themes/{transition}/transition.css"
|
||||
return None
|
||||
|
||||
|
||||
def _editor_css_url(vite_url: str | None) -> str | None:
|
||||
"""URL for the editor-specific stylesheet (Vue component styles).
|
||||
|
||||
@@ -146,6 +164,7 @@ def _layout(
|
||||
custom_css: str = "",
|
||||
theme: str = "",
|
||||
banner_design: str = "",
|
||||
transition: str = "cube",
|
||||
favicon: str = "",
|
||||
social: dict[str, str] | None = None,
|
||||
) -> Template:
|
||||
@@ -160,7 +179,8 @@ def _layout(
|
||||
analytics) stay external in both modes.
|
||||
|
||||
Order matters and is fixed: base (Vite build, absent in dev where Vite
|
||||
injects it from JS), theme and banner design (from pagerite/themes/),
|
||||
injects it from JS), theme, banner design and page transition (from
|
||||
pagerite/themes/),
|
||||
entry-specific stylesheets (e.g. overlayscrollbars.css), then the user's
|
||||
custom CSS last so it always wins.
|
||||
|
||||
@@ -220,6 +240,7 @@ def _layout(
|
||||
("pagerite-base", _base_css_url(vite_url)),
|
||||
("pagerite-theme", _theme_css_url(theme)),
|
||||
("pagerite-banner", _banner_css_url(banner_design)),
|
||||
("pagerite-transition", _transition_css_url(transition)),
|
||||
]
|
||||
for id_, url in sheets:
|
||||
if not url:
|
||||
@@ -706,6 +727,7 @@ def render_page(
|
||||
favicon: str = "",
|
||||
brand_html: str = "",
|
||||
base_url: str = "",
|
||||
transition: str = "cube",
|
||||
) -> str:
|
||||
"""Render a full HTML page for the slug path."""
|
||||
node = resolve(menu, path)[-1]
|
||||
@@ -715,7 +737,7 @@ def render_page(
|
||||
return str(
|
||||
_layout(
|
||||
*_page_assets(), custom_css, theme, banner_design(menu, path, theme),
|
||||
favicon, social,
|
||||
transition, favicon, social,
|
||||
)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand, brand_html),
|
||||
@@ -735,6 +757,7 @@ def render_category(
|
||||
theme: str = "",
|
||||
favicon: str = "",
|
||||
brand_html: str = "",
|
||||
transition: str = "cube",
|
||||
) -> str:
|
||||
"""Render the listing for a content-less category label (404).
|
||||
|
||||
@@ -753,7 +776,7 @@ def render_category(
|
||||
else:
|
||||
doc.p("This section has no page of its own yet.")
|
||||
return str(
|
||||
_layout(*_page_assets(), custom_css, theme, banner_design(menu, path, theme), favicon)(
|
||||
_layout(*_page_assets(), custom_css, theme, banner_design(menu, path, theme), transition, favicon)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand, brand_html),
|
||||
Nav=nav_html(menu, path),
|
||||
@@ -772,6 +795,7 @@ def render_not_found(
|
||||
theme: str = "",
|
||||
favicon: str = "",
|
||||
brand_html: str = "",
|
||||
transition: str = "cube",
|
||||
) -> str:
|
||||
"""Render a 404 page within the normal layout."""
|
||||
doc = E.article
|
||||
@@ -779,7 +803,7 @@ def render_not_found(
|
||||
doc.h1("Not Found")
|
||||
doc.p(f"No article at /{path}. If there was before, it may have been deleted.")
|
||||
return str(
|
||||
_layout(*_page_assets(), custom_css, theme, banner_design(menu, path, theme), favicon)(
|
||||
_layout(*_page_assets(), custom_css, theme, banner_design(menu, path, theme), transition, favicon)(
|
||||
Title=f"Not Found – {brand}" if brand else "Not Found",
|
||||
Brand=_brand_link(brand, brand_html),
|
||||
Nav=nav_html(menu, path),
|
||||
@@ -847,6 +871,7 @@ def render_analytics(
|
||||
theme: str = "",
|
||||
favicon: str = "",
|
||||
brand_html: str = "",
|
||||
transition: str = "cube",
|
||||
) -> str:
|
||||
"""Render the analytics viewer as a normal page at /_a.
|
||||
|
||||
@@ -872,6 +897,7 @@ def render_analytics(
|
||||
custom_css,
|
||||
theme,
|
||||
banner_design(menu, "_a", theme),
|
||||
transition,
|
||||
favicon,
|
||||
)(
|
||||
Title=f"Analytics – {brand}" if brand else "Analytics",
|
||||
|
||||
Reference in New Issue
Block a user