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:
@@ -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.
|
||||
|
||||
+18
-12
@@ -82,8 +82,9 @@ evolves.
|
||||
ships a `banner.svg`; the base stylesheet falls back to a plain gradient).
|
||||
- **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
|
||||
and the document title, keeping `<head>` and the layout chrome. Without JS
|
||||
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
|
||||
@@ -132,14 +133,16 @@ evolves.
|
||||
|
||||
- The base stylesheet `frontend/src/assets/pagerite.css` provides the layout,
|
||||
typography and interaction rules with conservative CSS variables. A theme layer
|
||||
(`frontend/src/assets/themes/purple/theme.css` by default) overrides those
|
||||
variables and adds the visual styling. Vue may add per-component styles on top
|
||||
(`frontend/src/assets/themes/purple/theme.css`) overrides those variables and
|
||||
adds the visual styling; `Data.theme` selects the active theme (empty = none/base
|
||||
only) and the site editor can switch it. Vue may add per-component styles on top
|
||||
where needed.
|
||||
- Fonts, the shared stylesheet, pygments styles and the theme's banner SVG
|
||||
live under `frontend/src/assets/` (the banner SVG under
|
||||
`themes/purple/`) and are emitted as hashed assets under `/_assets/`
|
||||
(Fraunces for headings, Literata for body, Fira Code for code — variable
|
||||
woff2 files with local `@font-face`). No third-party requests.
|
||||
(Source Serif 4 for headings, Source Sans 3 for body, Fira Code for code
|
||||
by default; Inter and Montserrat kept as variable woff2 options with
|
||||
local `@font-face`). No third-party requests.
|
||||
|
||||
## Editing
|
||||
|
||||
@@ -151,10 +154,13 @@ evolves.
|
||||
sidebar hides while editing. Preview renders server-side per keystroke
|
||||
(no debouncing) straight into the visible article's heading and body.
|
||||
- **Site mode** — the 🖊️ on the banner opens a panel with the site
|
||||
**brand** (applied to the header live), the page's **banner HTML**
|
||||
field (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.
|
||||
**brand** (applied to the header live), a **theme** selector (swapping
|
||||
the theme stylesheet in place), 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 HTML** field (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
|
||||
@@ -191,7 +197,7 @@ evolves.
|
||||
avoiding REST polling and races. Rendering always stays server-side.
|
||||
- A REST API also exists for scripting, all under `/_api/`:
|
||||
`GET pages` (the full tree), `PUT/DELETE pages/{path}`,
|
||||
`GET/PUT settings` (site brand), `POST structure` (reorder/move/
|
||||
retitle), file upload/removal via `PUT/DELETE files/{name}`.
|
||||
`GET/PUT settings` (site brand, theme and custom CSS), `POST structure`
|
||||
(reorder/move/retitle), file upload/removal via `PUT/DELETE files/{name}`.
|
||||
- On startup, seed pages from `pagerite/seed.py` are added **only if
|
||||
missing** — existing user content is never overwritten.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-markdown": "^6.5.2",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
|
||||
+162
-5
@@ -15,6 +15,7 @@ import StructureTree from './StructureTree.vue'
|
||||
import { EditorView, basicSetup } from 'codemirror'
|
||||
import { EditorState, Compartment } from '@codemirror/state'
|
||||
import { placeholder } from '@codemirror/view'
|
||||
import { css } from '@codemirror/lang-css'
|
||||
import { html } from '@codemirror/lang-html'
|
||||
import { cmHighlight, cmTheme } from './cmtheme'
|
||||
import { slugify } from './slugify'
|
||||
@@ -41,6 +42,11 @@ let view = null // CodeMirror for the banner HTML
|
||||
let syncing = false // set while replacing the document programmatically
|
||||
const bannerPh = new Compartment() // placeholder shows the inherited source
|
||||
|
||||
let cssView = null // CodeMirror for the site-wide custom CSS
|
||||
let cssSyncing = false // set while replacing the CSS document programmatically
|
||||
const customCss = ref('')
|
||||
const cssEl = ref(null)
|
||||
|
||||
// path -> node, for quick lookups (current title, delete checks).
|
||||
const flatMap = computed(() => {
|
||||
const map = {}
|
||||
@@ -115,6 +121,33 @@ function swapRegions(doc) {
|
||||
} else if (freshBrand) {
|
||||
document.getElementById('nav')?.before(document.importNode(freshBrand, true))
|
||||
}
|
||||
// Site-wide custom CSS is in <head> and must be swapped too.
|
||||
const freshUserStyle = doc.getElementById('pagerite-user')
|
||||
const curUserStyle = document.getElementById('pagerite-user')
|
||||
if (freshUserStyle && curUserStyle) {
|
||||
curUserStyle.textContent = freshUserStyle.textContent
|
||||
} else if (freshUserStyle) {
|
||||
document.head.appendChild(document.importNode(freshUserStyle, true))
|
||||
} else if (curUserStyle) {
|
||||
curUserStyle.remove()
|
||||
}
|
||||
// Theme and other public stylesheets live in <head> and must be kept in
|
||||
// sync; editor-only stylesheets are preserved. Diff-based: unchanged
|
||||
// sheets keep their elements, so their @keyframes are never torn down
|
||||
// (re-creating keyframes would replay the editor's slide-in animation).
|
||||
const curLinks = [...document.head.querySelectorAll('link[rel="stylesheet"]')]
|
||||
.filter((l) => !l.dataset.pagerite)
|
||||
const freshHrefs = [...doc.head.querySelectorAll('link[rel="stylesheet"]')]
|
||||
.map((l) => l.href)
|
||||
for (const link of curLinks) {
|
||||
if (!freshHrefs.includes(link.href)) link.remove()
|
||||
}
|
||||
const have = new Set(
|
||||
[...document.head.querySelectorAll('link[rel="stylesheet"]')].map((l) => l.href),
|
||||
)
|
||||
for (const link of doc.head.querySelectorAll('link[rel="stylesheet"]')) {
|
||||
if (!have.has(link.href)) document.head.appendChild(document.importNode(link, true))
|
||||
}
|
||||
document.title = doc.title
|
||||
}
|
||||
|
||||
@@ -243,10 +276,18 @@ async function commitPending() {
|
||||
// Edits apply to the live page immediately and save while typing. An
|
||||
// empty brand removes the header link and the title suffix entirely.
|
||||
const brand = ref('')
|
||||
const theme = ref('purple')
|
||||
const THEME_OPTIONS = [
|
||||
{ value: '', label: 'none' },
|
||||
{ value: 'purple', label: 'purple' },
|
||||
]
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
brand.value = (await (await fetch('/_api/settings')).json()).brand
|
||||
const s = await (await fetch('/_api/settings')).json()
|
||||
brand.value = s.brand
|
||||
theme.value = s.theme || ''
|
||||
customCss.value = s.custom_css || ''
|
||||
} catch { /* keep default */ }
|
||||
}
|
||||
|
||||
@@ -277,11 +318,16 @@ function onBrandInput() {
|
||||
debounce('brand', saveBrand)
|
||||
}
|
||||
|
||||
async function saveBrand() {
|
||||
async function saveSettings(opts = {}) {
|
||||
const res = await fetch('/_api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ brand: brand.value }),
|
||||
body: JSON.stringify({
|
||||
brand: brand.value,
|
||||
theme: theme.value,
|
||||
custom_css: customCss.value,
|
||||
...opts,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
saveError.value = ''
|
||||
@@ -290,6 +336,58 @@ async function saveBrand() {
|
||||
}
|
||||
}
|
||||
|
||||
function saveBrand() {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
async function onThemeChange() {
|
||||
await saveSettings()
|
||||
if (import.meta.env.DEV) {
|
||||
// Dev: styles are Vite-injected <style> tags, not <link>s, so the
|
||||
// stylesheet sync in swapRegions can't switch themes. Drop the old
|
||||
// theme's injected styles and import the new theme module instead.
|
||||
for (const el of document.head.querySelectorAll('style[data-vite-dev-id]')) {
|
||||
if (el.dataset.viteDevId.includes('/themes/')) el.remove()
|
||||
}
|
||||
if (theme.value) {
|
||||
await import(/* @vite-ignore */ `/src/assets/themes/${theme.value}/theme.css`)
|
||||
}
|
||||
}
|
||||
loadPlain(path.value)
|
||||
}
|
||||
|
||||
// --- Site-wide custom CSS --------------------------------------------------
|
||||
// Edits apply to the live page immediately and save while typing.
|
||||
function applyCustomCss(css) {
|
||||
let el = document.getElementById('pagerite-user')
|
||||
if (css.trim()) {
|
||||
if (!el) {
|
||||
el = document.createElement('style')
|
||||
el.id = 'pagerite-user'
|
||||
document.head.append(el)
|
||||
}
|
||||
el.textContent = css
|
||||
} else if (el) {
|
||||
el.remove()
|
||||
}
|
||||
}
|
||||
|
||||
function onCustomCssInput() {
|
||||
applyCustomCss(customCss.value)
|
||||
debounce('custom-css', saveCustomCss, 400)
|
||||
}
|
||||
|
||||
function saveCustomCss() {
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function setCssDocument(text) {
|
||||
cssSyncing = true
|
||||
cssView.dispatch({ changes: { from: 0, to: cssView.state.doc.length, insert: text } })
|
||||
cssSyncing = false
|
||||
customCss.value = text
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -548,9 +646,8 @@ function connect() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
refreshPages()
|
||||
loadSettings()
|
||||
connect()
|
||||
view = new EditorView({
|
||||
state: EditorState.create({
|
||||
@@ -572,7 +669,30 @@ onMounted(() => {
|
||||
}),
|
||||
parent: bannerEl.value,
|
||||
})
|
||||
cssView = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: '',
|
||||
extensions: [
|
||||
basicSetup,
|
||||
css(),
|
||||
cmTheme,
|
||||
cmHighlight,
|
||||
EditorView.lineWrapping,
|
||||
placeholder('Site styling CSS'),
|
||||
EditorView.updateListener.of((u) => {
|
||||
if (u.docChanged && !cssSyncing) {
|
||||
customCss.value = cssView.state.doc.toString()
|
||||
onCustomCssInput()
|
||||
}
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: cssEl.value,
|
||||
})
|
||||
addEventListener('keydown', onKeydown)
|
||||
await loadSettings()
|
||||
setCssDocument(customCss.value)
|
||||
applyCustomCss(customCss.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -584,6 +704,7 @@ onUnmounted(() => {
|
||||
ws.close()
|
||||
}
|
||||
view?.destroy()
|
||||
cssView?.destroy()
|
||||
removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
@@ -605,7 +726,18 @@ onUnmounted(() => {
|
||||
placeholder="Site name (header link and window title)"
|
||||
@input="onBrandInput"
|
||||
/>
|
||||
<select
|
||||
v-model="theme"
|
||||
class="text-input theme-select"
|
||||
title="Theme"
|
||||
@change="onThemeChange"
|
||||
>
|
||||
<option v-for="opt in THEME_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div ref="cssEl" class="css-cm" />
|
||||
</section>
|
||||
|
||||
<section class="block" @paste="onBannerPaste">
|
||||
@@ -727,6 +859,11 @@ onUnmounted(() => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Small CodeMirror window for the banner HTML; scrolls internally. */
|
||||
.banner-cm {
|
||||
border: 1px solid var(--line);
|
||||
@@ -748,6 +885,26 @@ onUnmounted(() => {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* CodeMirror window for site-wide custom CSS. */
|
||||
.css-cm {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.css-cm :deep(.cm-editor) {
|
||||
max-height: 12rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.css-cm :deep(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.css-cm :deep(.cm-gutters) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.structure {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -274,7 +274,7 @@ body.tree-dragging .treelist {
|
||||
}
|
||||
|
||||
.slug-edit {
|
||||
font-family: "Fira Code", monospace;
|
||||
font-family: var(--font-code);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@font-face { font-family: 'Fraunces'; font-weight: 100 1000; font-style: normal; font-display: swap; src: url('fraunces.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Literata'; font-weight: 100 900; font-style: normal; font-display: swap; src: url('literata.woff2') format('woff2'); }
|
||||
/* Variable fonts: only downloaded when referenced by the page CSS. */
|
||||
@font-face { font-family: 'Inter'; font-weight: 100 900; font-style: normal; font-display: swap; src: url('inter.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Inter'; font-weight: 100 900; font-style: italic; font-display: swap; src: url('inter-italic.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Montserrat'; font-weight: 100 900; font-style: normal; font-display: swap; src: url('montserrat.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Montserrat'; font-weight: 100 900; font-style: italic; font-display: swap; src: url('montserrat-italic.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Sans 3'; font-weight: 200 900; font-style: normal; font-display: swap; src: url('sourcesans3.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Sans 3'; font-weight: 200 900; font-style: italic; font-display: swap; src: url('sourcesans3-italic.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Serif 4'; font-weight: 200 900; font-style: normal; font-display: swap; src: url('sourceserif4.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Source Serif 4'; font-weight: 200 900; font-style: italic; font-display: swap; src: url('sourceserif4-italic.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Fira Code'; font-weight: 300 700; font-style: normal; font-display: swap; src: url('firacode.woff2') format('woff2'); }
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) Montserrat-Italic[wght].ttf: Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Google Inc.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Google Inc.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,9 @@
|
||||
--accent: #0056b3;
|
||||
--accent2: #003d80;
|
||||
--line: #0000001a;
|
||||
--font-body: "Source Sans 3", system-ui, sans-serif;
|
||||
--font-heading: "Source Serif 4", Georgia, serif;
|
||||
--font-code: "Fira Code", ui-monospace, monospace;
|
||||
/* Width of the docked editor panel (used both here for shifting the page
|
||||
and in the Vue editor's own styles). */
|
||||
--editor-w: min(46rem, 50vw);
|
||||
@@ -26,9 +29,12 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Literata", Georgia, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
/* Tabular numerals wherever the active font supports them; avoids
|
||||
numbers jumping in width as counters/values change. */
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
@@ -79,7 +85,7 @@ body {
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-family: "Fraunces", serif;
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
text-decoration: none;
|
||||
@@ -257,7 +263,7 @@ main {
|
||||
article h1,
|
||||
article h2,
|
||||
article h3 {
|
||||
font-family: "Fraunces", serif;
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
@@ -463,7 +469,7 @@ pre:hover .copy,
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", ui-monospace, monospace;
|
||||
font-family: var(--font-code);
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 360">
|
||||
<rect width="1600" height="360" fill="#1b1830"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 122 B |
+11
-1
@@ -5,8 +5,9 @@
|
||||
// - SiteEditor ("site" mode): pen on the banner — banner HTML editing
|
||||
// (previewed into the real banner) and the site structure tree.
|
||||
if (import.meta.env.DEV) {
|
||||
// Base styles only; the theme CSS is imported by pagerite.js (which
|
||||
// always runs first — the editor opens from public pages).
|
||||
import("./assets/pagerite.css");
|
||||
import("./assets/themes/purple/theme.css");
|
||||
}
|
||||
|
||||
import { createApp } from 'vue'
|
||||
@@ -29,6 +30,15 @@ export function openEditor(path, { mode = 'page' } = {}) {
|
||||
pagePath: path,
|
||||
onClose: closeEditor,
|
||||
}).mount(host)
|
||||
// The slide-in (editor-slide-in in pagerite.css) is a one-shot open
|
||||
// effect; once finished, drop it so that later stylesheet swaps (theme
|
||||
// change re-creating @keyframes) cannot restart it.
|
||||
const root = host.firstElementChild
|
||||
root?.addEventListener('animationend', function done(e) {
|
||||
if (e.target !== root) return
|
||||
root.removeEventListener('animationend', done)
|
||||
root.style.animation = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
export function closeEditor() {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
import("./assets/pagerite.css");
|
||||
import("./assets/themes/purple/theme.css");
|
||||
// The theme is selectable; the backend names the active one in a meta
|
||||
// tag (dev links no stylesheets — Vite injects them from JS).
|
||||
const theme = document.querySelector('meta[name="pagerite:theme"]')?.content;
|
||||
if (theme) import(/* @vite-ignore */ `./assets/themes/${theme}/theme.css`);
|
||||
}
|
||||
|
||||
const REGIONS = ["page-banner", "nav", "sidebar", "main"];
|
||||
@@ -188,6 +191,17 @@
|
||||
const el = document.getElementById(id);
|
||||
el.replaceWith(document.importNode(doc.getElementById(id), true));
|
||||
}
|
||||
// Site-wide custom CSS lives in <head id="pagerite-user"> and must be
|
||||
// kept in sync across fetch-navigations.
|
||||
const oldUserStyle = document.getElementById("pagerite-user");
|
||||
const newUserStyle = doc.getElementById("pagerite-user");
|
||||
if (oldUserStyle && newUserStyle) {
|
||||
oldUserStyle.textContent = newUserStyle.textContent;
|
||||
} else if (newUserStyle) {
|
||||
document.head.appendChild(document.importNode(newUserStyle, true));
|
||||
} else if (oldUserStyle) {
|
||||
oldUserStyle.remove();
|
||||
}
|
||||
document.title = doc.title;
|
||||
// Banners may contain scripts (canvas etc.), content pages may too.
|
||||
runScripts(document.getElementById("page-banner"));
|
||||
@@ -235,6 +249,7 @@
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = css;
|
||||
link.dataset.pagerite = "editor-css";
|
||||
document.head.append(link);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +257,7 @@
|
||||
editorModule = import(/* @vite-ignore */ editBtn.dataset.editorSrc);
|
||||
editorModule
|
||||
.then((m) => m.openEditor(path, { mode }))
|
||||
.catch(() => {});
|
||||
.catch((e) => console.error("editor load failed:", e));
|
||||
return;
|
||||
}
|
||||
const a = ev.target.closest("a[href]");
|
||||
|
||||
@@ -32,6 +32,10 @@ export default defineConfig({
|
||||
manifest: true,
|
||||
assetsDir: '_assets',
|
||||
rollupOptions: {
|
||||
// main.js is dynamic-imported by the public page (pagerite.js) for
|
||||
// its openEditor/closeEditor exports — keep them in the bundle
|
||||
// (app builds strip unused entry exports by default).
|
||||
preserveEntrySignatures: 'exports-only',
|
||||
input: {
|
||||
main: fileURLToPath(new URL('./src/main.js', import.meta.url)),
|
||||
pagerite: fileURLToPath(new URL('./src/pagerite.js', import.meta.url)),
|
||||
|
||||
+10
-6
@@ -281,14 +281,16 @@ async def update_structure(op: StructureOp) -> None:
|
||||
|
||||
@app.get("/_api/settings")
|
||||
async def get_settings() -> dict[str, str]:
|
||||
"""Site-wide settings (the brand text)."""
|
||||
return {"brand": data.brand}
|
||||
"""Site-wide settings (brand, theme and custom CSS)."""
|
||||
return {"brand": data.brand, "theme": data.theme, "custom_css": data.custom_css}
|
||||
|
||||
|
||||
class SettingsIn(BaseModel):
|
||||
"""Payload for updating site-wide settings."""
|
||||
|
||||
brand: str
|
||||
theme: str
|
||||
custom_css: str
|
||||
|
||||
|
||||
@app.put("/_api/settings", status_code=204)
|
||||
@@ -296,6 +298,8 @@ async def put_settings(settings: SettingsIn) -> None:
|
||||
"""Update site-wide settings; bumps the version so ETags invalidate."""
|
||||
with kanta.transaction("update settings"):
|
||||
data.brand = settings.brand
|
||||
data.theme = settings.theme
|
||||
data.custom_css = settings.custom_css
|
||||
data.version += 1
|
||||
|
||||
|
||||
@@ -570,7 +574,7 @@ async def show_page(request: Request, path: str) -> HTMLResponse | Response:
|
||||
path = path.strip("/")
|
||||
if path and _is_reserved(path):
|
||||
# Reserved slug shape: never content — no tree lookup.
|
||||
return HTMLResponse(views.render_not_found(data.menu, path, data.brand), 404)
|
||||
return HTMLResponse(views.render_not_found(data.menu, path, data.brand, data.custom_css, data.theme), 404)
|
||||
chain = resolve(data.menu, path)
|
||||
node = chain[-1] if chain else None
|
||||
if node is not None and node.published and node.content is not None:
|
||||
@@ -580,17 +584,17 @@ async def show_page(request: Request, path: str) -> HTMLResponse | Response:
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304)
|
||||
return HTMLResponse(
|
||||
views.render_page(data.menu, path, data.brand),
|
||||
views.render_page(data.menu, path, data.brand, data.custom_css, data.theme),
|
||||
headers={"etag": etag},
|
||||
)
|
||||
if node is not None and node.published and node.content is None:
|
||||
# Category label without a landing page: placeholder with the pen
|
||||
# to create it (404 — no page here, but the node is real).
|
||||
return HTMLResponse(views.render_category(data.menu, path, data.brand), 404)
|
||||
return HTMLResponse(views.render_category(data.menu, path, data.brand, data.custom_css, data.theme), 404)
|
||||
if node is None and not path:
|
||||
# No front page (no top-level node with slug ""): "/" opens the
|
||||
# first item of the navigation instead.
|
||||
for slug, item in sorted_nodes(data.menu):
|
||||
if item.published:
|
||||
return RedirectResponse(f"/{slug}")
|
||||
return HTMLResponse(views.render_not_found(data.menu, path, data.brand), 404)
|
||||
return HTMLResponse(views.render_not_found(data.menu, path, data.brand, data.custom_css, data.theme), 404)
|
||||
|
||||
@@ -79,6 +79,12 @@ class Data(msgspec.Struct):
|
||||
#: Site name shown in the header and <title> suffix; editable in the
|
||||
#: site editor. Empty = no brand link in the header, no title suffix.
|
||||
brand: str = "Pagerite"
|
||||
#: Active theme name (empty = none/base only). Themes live in
|
||||
#: frontend/src/assets/themes/{theme}/theme.css.
|
||||
theme: str = "purple"
|
||||
#: Raw site-wide custom CSS, injected inline in every page <head>.
|
||||
#: Trusted author content; not sanitized.
|
||||
custom_css: str = ""
|
||||
#: Legacy flat page store (pre-tree databases); migrated into `menu`
|
||||
#: on startup, then cleared. Never written otherwise.
|
||||
pages: dict[str, Page] = {}
|
||||
|
||||
+74
-35
@@ -27,10 +27,8 @@ BUILD = Path(__file__).with_name("frontend-build")
|
||||
|
||||
# Shared CSS built as separate entries so the backend can link base and theme
|
||||
# independently. Order matters: base first, theme overrides it.
|
||||
_SHARED_CSS = {
|
||||
"src/assets/pagerite.css": "pagerite",
|
||||
"src/assets/themes/purple/theme.css": "pagerite-theme",
|
||||
}
|
||||
_BASE_CSS_KEY = "src/assets/pagerite.css"
|
||||
_THEME_CSS_KEY = "src/assets/themes/{theme}/theme.css"
|
||||
|
||||
_manifest_cache: dict | None = None
|
||||
_asset_cache: dict[str, tuple] = {}
|
||||
@@ -43,7 +41,15 @@ def _manifest() -> dict:
|
||||
return _manifest_cache
|
||||
|
||||
|
||||
def _shared_css_urls(vite_url: str | None) -> list[str]:
|
||||
def _css_keys(theme: str) -> list[str]:
|
||||
"""Manifest keys for the stylesheets to load for ``theme`` (empty = none)."""
|
||||
keys = [_BASE_CSS_KEY]
|
||||
if theme:
|
||||
keys.append(_THEME_CSS_KEY.format(theme=theme))
|
||||
return keys
|
||||
|
||||
|
||||
def _shared_css_urls(vite_url: str | None, theme: str) -> list[str]:
|
||||
"""URLs for the base and theme stylesheets.
|
||||
|
||||
In dev the JS entries import these files, so Vite injects them; the
|
||||
@@ -52,10 +58,10 @@ def _shared_css_urls(vite_url: str | None) -> list[str]:
|
||||
if vite_url:
|
||||
return []
|
||||
manifest = _manifest()
|
||||
return [f"/{manifest[key]['file']}" for key in _SHARED_CSS]
|
||||
return [f"/{manifest[key]['file']}" for key in _css_keys(theme)]
|
||||
|
||||
|
||||
def _editor_css_url(vite_url: str | None) -> str | None:
|
||||
def _editor_css_url(vite_url: str | None, theme: str) -> str | None:
|
||||
"""URL for the editor-specific stylesheet (Vue component styles).
|
||||
|
||||
This is linked by the public-page edit pen so the editor styles are
|
||||
@@ -65,24 +71,37 @@ def _editor_css_url(vite_url: str | None) -> str | None:
|
||||
return None
|
||||
manifest = _manifest()
|
||||
entry = manifest["src/main.js"]
|
||||
shared_files = {manifest[key]["file"] for key in _SHARED_CSS}
|
||||
shared_files = {manifest[key]["file"] for key in _css_keys(theme)}
|
||||
for css in entry.get("css", []):
|
||||
if css not in shared_files:
|
||||
return f"/{css}"
|
||||
return None
|
||||
|
||||
|
||||
def _layout(urls: list[str], modules: list[str] = ()) -> Template:
|
||||
def _layout(
|
||||
urls: list[str],
|
||||
modules: list[str] = (),
|
||||
custom_css: str = "",
|
||||
theme: str = "",
|
||||
) -> Template:
|
||||
"""Page layout template with standard asset URLs and ES-module scripts.
|
||||
|
||||
Stylesheets use ``blocking="render"`` so the browser waits for them before
|
||||
showing the page, avoiding a flash of unstyled content.
|
||||
|
||||
The active theme is named in a meta tag so that in dev (where the
|
||||
backend links no stylesheets and Vite injects them from JS) the
|
||||
frontend entries know which theme CSS module to import.
|
||||
"""
|
||||
doc = Document(E.Title, lang="en")
|
||||
if theme:
|
||||
doc.meta(name="pagerite:theme", content=theme)
|
||||
for url in urls:
|
||||
doc.link(rel="stylesheet", href=url, blocking="render")
|
||||
for src in modules:
|
||||
doc.script(src=src, type="module")
|
||||
if custom_css.strip():
|
||||
doc.style(custom_css, id="pagerite-user")
|
||||
return Template(
|
||||
doc
|
||||
.header(
|
||||
@@ -212,7 +231,7 @@ def banner_source(menu: dict[str, Node], path: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _edit_attrs(path: str, mode: str = "page") -> dict:
|
||||
def _edit_attrs(path: str, mode: str = "page", theme: str = "") -> dict:
|
||||
"""Attributes for a 🖊️ edit button.
|
||||
|
||||
pagerite.js wires these buttons to dynamic-import the editor app
|
||||
@@ -221,7 +240,7 @@ def _edit_attrs(path: str, mode: str = "page") -> dict:
|
||||
(the pen on the banner) edits the banner and site structure. They are
|
||||
buttons, not links: editing is an action, not a navigation.
|
||||
"""
|
||||
script, editor_css = _editor_assets()
|
||||
script, editor_css = _editor_assets(theme)
|
||||
return {
|
||||
"type": "button",
|
||||
"class": "edit-link" if mode == "page" else "edit-link banner-edit-link",
|
||||
@@ -247,25 +266,37 @@ def page_content(menu: dict[str, Node], path: str) -> HTML:
|
||||
return HTML(str(doc))
|
||||
|
||||
|
||||
def render_page(menu: dict[str, Node], path: str, brand: str = SITE_NAME) -> str:
|
||||
def render_page(
|
||||
menu: dict[str, Node],
|
||||
path: str,
|
||||
brand: str = SITE_NAME,
|
||||
custom_css: str = "",
|
||||
theme: str = "",
|
||||
) -> str:
|
||||
"""Render a full HTML page for the slug path."""
|
||||
node = resolve(menu, path)[-1]
|
||||
title = _title(path.rpartition("/")[2], node)
|
||||
scripts, styles = _page_assets()
|
||||
scripts, styles = _page_assets(theme)
|
||||
return str(
|
||||
_layout(styles, scripts)(
|
||||
_layout(styles, scripts, custom_css, theme)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand),
|
||||
Nav=nav_html(menu, path),
|
||||
Sidebar=sidebar_html(menu, path),
|
||||
Banner=banner_html(menu, path),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site")))),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site", theme)))),
|
||||
Main=page_content(menu, path),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_category(menu: dict[str, Node], path: str, brand: str = SITE_NAME) -> str:
|
||||
def render_category(
|
||||
menu: dict[str, Node],
|
||||
path: str,
|
||||
brand: str = SITE_NAME,
|
||||
custom_css: str = "",
|
||||
theme: str = "",
|
||||
) -> str:
|
||||
"""Render the placeholder for a content-less category label (404).
|
||||
|
||||
The node exists in the tree but has no page of its own. Nav links
|
||||
@@ -278,47 +309,53 @@ def render_category(menu: dict[str, Node], path: str, brand: str = SITE_NAME) ->
|
||||
with doc:
|
||||
doc.h1(title)
|
||||
# Editing works here too: the pen creates this category's page.
|
||||
doc.button("🖊️", **_edit_attrs(path))
|
||||
doc.button("🖊️", **_edit_attrs(path, "page", theme))
|
||||
doc.p(
|
||||
"Pages in this section are listed in the menu on the left."
|
||||
)
|
||||
scripts, styles = _page_assets()
|
||||
scripts, styles = _page_assets(theme)
|
||||
return str(
|
||||
_layout(styles, scripts)(
|
||||
_layout(styles, scripts, custom_css, theme)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand),
|
||||
Nav=nav_html(menu, path),
|
||||
Sidebar=sidebar_html(menu, path),
|
||||
Banner=banner_html(menu, path),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site")))),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site", theme)))),
|
||||
Main=HTML(str(doc)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_not_found(menu: dict[str, Node], path: str, brand: str = SITE_NAME) -> str:
|
||||
def render_not_found(
|
||||
menu: dict[str, Node],
|
||||
path: str,
|
||||
brand: str = SITE_NAME,
|
||||
custom_css: str = "",
|
||||
theme: str = "",
|
||||
) -> str:
|
||||
"""Render a 404 page within the normal layout."""
|
||||
doc = E.article
|
||||
with doc:
|
||||
doc.h1("Not Found")
|
||||
# Editing works here too: this is how brand new pages get created.
|
||||
doc.button("🖊️", **_edit_attrs(path))
|
||||
doc.button("🖊️", **_edit_attrs(path, "page", theme))
|
||||
doc.p(f"No page at /{path}.")
|
||||
scripts, styles = _page_assets()
|
||||
scripts, styles = _page_assets(theme)
|
||||
return str(
|
||||
_layout(styles, scripts)(
|
||||
_layout(styles, scripts, custom_css, theme)(
|
||||
Title=f"Not Found – {brand}" if brand else "Not Found",
|
||||
Brand=_brand_link(brand),
|
||||
Nav=nav_html(menu, path),
|
||||
Sidebar=sidebar_html(menu, path),
|
||||
Banner=banner_html(menu, path),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site")))),
|
||||
BannerEdit=HTML(str(E.button("🖊️", **_edit_attrs(path, "site", theme)))),
|
||||
Main=HTML(str(doc)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _page_assets() -> tuple[list[str], list[str]]:
|
||||
def _page_assets(theme: str) -> tuple[list[str], list[str]]:
|
||||
"""Script and CSS URLs for public pages (pagerite entry).
|
||||
|
||||
Dev mode loads the entry from the Vite dev server; production uses
|
||||
@@ -326,18 +363,19 @@ def _page_assets() -> tuple[list[str], list[str]]:
|
||||
"""
|
||||
vite_url = os.environ.get("PAGERITE_VITE_URL")
|
||||
if vite_url:
|
||||
return [f"{vite_url}/src/pagerite.js"], _shared_css_urls(vite_url)
|
||||
if "page" not in _asset_cache:
|
||||
return [f"{vite_url}/src/pagerite.js"], _shared_css_urls(vite_url, theme)
|
||||
key = f"page:{theme}"
|
||||
if key not in _asset_cache:
|
||||
manifest = _manifest()
|
||||
entry = manifest["src/pagerite.js"]
|
||||
_asset_cache["page"] = (
|
||||
_asset_cache[key] = (
|
||||
[f"/{entry['file']}"],
|
||||
_shared_css_urls(None),
|
||||
_shared_css_urls(None, theme),
|
||||
)
|
||||
return _asset_cache["page"]
|
||||
return _asset_cache[key]
|
||||
|
||||
|
||||
def _editor_assets() -> tuple[list[str], str | None]:
|
||||
def _editor_assets(theme: str) -> tuple[list[str], str | None]:
|
||||
"""Script URL and editor-specific CSS URL for the public-page edit pen.
|
||||
|
||||
The shared CSS is already linked on the page, so the pen only needs the
|
||||
@@ -346,8 +384,9 @@ def _editor_assets() -> tuple[list[str], str | None]:
|
||||
vite_url = os.environ.get("PAGERITE_VITE_URL")
|
||||
if vite_url:
|
||||
return [f"{vite_url}/@vite/client", f"{vite_url}/src/main.js"], None
|
||||
if "editor" not in _asset_cache:
|
||||
key = f"editor:{theme}"
|
||||
if key not in _asset_cache:
|
||||
manifest = _manifest()
|
||||
entry = manifest["src/main.js"]
|
||||
_asset_cache["editor"] = [f"/{entry['file']}"], _editor_css_url(None)
|
||||
return _asset_cache["editor"]
|
||||
_asset_cache[key] = [f"/{entry['file']}"], _editor_css_url(None, theme)
|
||||
return _asset_cache[key]
|
||||
|
||||
Reference in New Issue
Block a user