Add selectable site theme, custom CSS, and new font set

- Data.theme/Data.custom_css site settings with /_api/settings support;
  theme stylesheet linked per page, custom CSS inline as #pagerite-user,
  swapped during fetch-navigation
- Site editor: theme selector, custom CSS CodeMirror field; in-place
  stylesheet swap (diff-based, preserves editor stylesheets); dev swaps
  Vite-injected style tags since no <link>s exist there
- Dev theme delivery via <meta name="pagerite:theme"> + dynamic import
  in pagerite.js instead of a hardcoded purple import
- Fix dead edit pens in prod: Vite app builds strip entry exports, so
  the dynamic-imported main.js had no openEditor/closeEditor; build with
  preserveEntrySignatures 'exports-only' and log editor load failures
  instead of swallowing them
- Editor slide-in is one-shot: cleared on animationend so stylesheet
  swaps cannot restart a finished animation
- Replace Fraunces/Literata with Source Sans 3/Serif 4, Inter and
  Montserrat variable fonts; font stacks as CSS variables
This commit is contained in:
2026-08-16 23:39:11 +00:00
parent 8aa5d3d4a6
commit 4a84aceb4f
28 changed files with 724 additions and 79 deletions
+26 -11
View File
@@ -59,7 +59,11 @@ not for the public pages. See `docs/design-principles.md` for the design.
and embedded in page ETags so nav-affecting changes invalidate caches.
`Data.brand` is the site name (header link + `<title>` suffix), editable
in the site editor via `/_api/settings`; empty = no header link and
no `<title>` suffix.
no `<title>` suffix. `Data.theme` is the active theme name (empty =
none/base only); themes live in `frontend/src/assets/themes/{theme}`.
`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.
- `markdown.py` — markdown-it-py renderer (html passthrough + attrs,
footnote, deflist, tasklists plugins). Custom image rule: relative srcs
resolve against the page path, titled images become figures.
@@ -83,8 +87,8 @@ not for the public pages. See `docs/design-principles.md` for the design.
- `assets/` — shared styles and data files built by Vite and served hashed
under `/_assets/`: `pagerite.css` (base layout + conservative variables),
`themes/purple/theme.css` (the purple/dark theme override, including its
own `banner.svg`), `pygments.css`, and `fonts/` (self-hosted
Fraunces/Literata/Fira Code variable woff2). The `::view-transition*` block at the end of `pagerite.css` (from
own `banner.svg`), `pygments.css`, and `fonts/` (self-hosted Source
Sans 3/Source Serif 4/Inter/Montserrat/Fira Code variable woff2). The `::view-transition*` block at the end of `pagerite.css` (from
termotohtori.fi) is fragile — do not tweak.
- Vite builds ES-module `.js` outputs; the backend renders `<script
type="module">` for them (module scripts defer by default).
@@ -97,13 +101,16 @@ not for the public pages. See `docs/design-principles.md` for the design.
(CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`,
previewing into the visible article; editor scroll drives document
scroll) opened by the article pen — it edits content and title only,
never the path — and `SiteEditor.vue` (site brand + banner HTML edited in
a small CodeMirror window and previewed into `#page-banner` + vue-draggable structure tree with
always-editable title/slug inputs per row) opened by
the banner pen — everything saves immediately as you edit (brand/title
debounced, slug on commit since it renames the path), tree rows navigate
in place without transitions when focused, and the front page is a
root-only row whose empty slug is editable like any other. Every
never the path — and `SiteEditor.vue` (site brand + theme selector +
site-wide custom CSS + banner HTML edited in small CodeMirror windows;
banner previewed into `#page-banner`, CSS injected into
`<head id="pagerite-user">`) + vue-draggable structure tree with
always-editable title/slug inputs per row, opened by the banner pen —
everything saves immediately as you edit (brand/title/CSS debounced,
slug on commit since it renames the path), theme change swaps the
stylesheet in place, tree rows navigate in place without transitions when
focused, and the front page is a root-only row whose empty slug is
editable like any other. Every
non-empty list (and the root) ends with a non-draggable footer row
(vuedraggable `#footer` slot): clicking it starts a new pending page at
that level, and while dragging it is the list's "end of list" drop
@@ -125,7 +132,15 @@ not for the public pages. See `docs/design-principles.md` for the design.
`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`; there
is no `index.html` source (it would shadow `/` and turn missing dev paths
into an empty Vue shell). All outputs are ES modules.
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 no stylesheets (Vite injects them from JS); the
active theme reaches the page as `<meta name="pagerite:theme">` and
pagerite.js imports that theme's CSS, while a theme switch in the site
editor swaps the Vite-injected `<style data-vite-dev-id>` tags (the
`<link>` sync used in prod is a no-op in dev).
vite-plugin-fastapi.js has an
auto-upgrade marker — edit `vite.config.js`, not the plugin.
- `docs/` — design documentation.