Add font picker to site editor, neutral base banner styling
- Base stylesheet: per-family font variables (--font-source-sans etc.) with functional slots referencing them; --font-brand defaults to var(--font-heading). Restore Fraunces/Literata faces dropped in the font-set swap - Site editor: stylized A button (always Fira Code) opens a font panel with body/heading/brand tabs; options render their own name in the candidate font at the target's size/weight; clicking the current pick clears it. Picks are stored as plain :root rows in the custom CSS, parsed and rewritten on change - Base banner: drop black text-shadow on light background; legibility via a bottom scrim fading artwork into --bg. Purple keeps its own nav shadow - Headings no longer accent-colored in base (moved to purple theme) - Keep #pagerite-user custom CSS last in <head> at every mutation point so its :root rules win equal specificity (dev injects base/theme styles after the server-rendered tag)
This commit is contained in:
@@ -63,7 +63,13 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
|||||||
none/base only); themes live in `frontend/src/assets/themes/{theme}`.
|
none/base only); themes live in `frontend/src/assets/themes/{theme}`.
|
||||||
`Data.custom_css` is raw trusted CSS injected inline in every page
|
`Data.custom_css` is raw trusted CSS injected inline in every page
|
||||||
`<head>` (id `pagerite-user`) and swapped during fetch-navigation;
|
`<head>` (id `pagerite-user`) and swapped during fetch-navigation;
|
||||||
editable in the site editor.
|
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)`.
|
||||||
- `markdown.py` — markdown-it-py renderer (html passthrough + attrs,
|
- `markdown.py` — markdown-it-py renderer (html passthrough + attrs,
|
||||||
footnote, deflist, tasklists plugins). Custom image rule: relative srcs
|
footnote, deflist, tasklists plugins). Custom image rule: relative srcs
|
||||||
resolve against the page path, titled images become figures.
|
resolve against the page path, titled images become figures.
|
||||||
@@ -88,7 +94,8 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
|||||||
under `/_assets/`: `pagerite.css` (base layout + conservative variables),
|
under `/_assets/`: `pagerite.css` (base layout + conservative variables),
|
||||||
`themes/purple/theme.css` (the purple/dark theme override, including its
|
`themes/purple/theme.css` (the purple/dark theme override, including its
|
||||||
own `banner.svg`), `pygments.css`, and `fonts/` (self-hosted Source
|
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
|
Sans 3/Source Serif 4/Fraunces/Literata/Inter/Montserrat/Fira Code
|
||||||
|
variable woff2). The `::view-transition*` block at the end of `pagerite.css` (from
|
||||||
termotohtori.fi) is fragile — do not tweak.
|
termotohtori.fi) is fragile — do not tweak.
|
||||||
- Vite builds ES-module `.js` outputs; the backend renders `<script
|
- Vite builds ES-module `.js` outputs; the backend renders `<script
|
||||||
type="module">` for them (module scripts defer by default).
|
type="module">` for them (module scripts defer by default).
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ evolves.
|
|||||||
live under `frontend/src/assets/` (the banner SVG under
|
live under `frontend/src/assets/` (the banner SVG under
|
||||||
`themes/purple/`) and are emitted as hashed assets under `/_assets/`
|
`themes/purple/`) and are emitted as hashed assets under `/_assets/`
|
||||||
(Source Serif 4 for headings, Source Sans 3 for body, Fira Code for code
|
(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
|
by default; Fraunces, Literata, Inter and Montserrat kept as variable
|
||||||
|
woff2 options with
|
||||||
local `@font-face`). No third-party requests.
|
local `@font-face`). No third-party requests.
|
||||||
|
|
||||||
## Editing
|
## Editing
|
||||||
@@ -155,7 +156,9 @@ evolves.
|
|||||||
(no debouncing) straight into the visible article's heading and body.
|
(no debouncing) straight into the visible article's heading and body.
|
||||||
- **Site mode** — the 🖊️ on the banner opens a panel with the site
|
- **Site mode** — the 🖊️ on the banner opens a panel with the site
|
||||||
**brand** (applied to the header live), a **theme** selector (swapping
|
**brand** (applied to the header live), a **theme** selector (swapping
|
||||||
the theme stylesheet in place), a **site-wide custom CSS** field (injected
|
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
|
into `<style id="pagerite-user">` in the live page head and swapped during
|
||||||
fetch-navigation), the page's **banner HTML** field (previewed into the
|
fetch-navigation), the page's **banner HTML** field (previewed into the
|
||||||
real banner region, so you see exactly which banner you're editing) and
|
real banner region, so you see exactly which banner you're editing) and
|
||||||
|
|||||||
+237
-1
@@ -352,6 +352,9 @@ async function onThemeChange() {
|
|||||||
if (theme.value) {
|
if (theme.value) {
|
||||||
await import(/* @vite-ignore */ `/src/assets/themes/${theme.value}/theme.css`)
|
await import(/* @vite-ignore */ `/src/assets/themes/${theme.value}/theme.css`)
|
||||||
}
|
}
|
||||||
|
// The freshly injected theme style now sits after the custom CSS;
|
||||||
|
// move the custom CSS back to the end so it keeps winning.
|
||||||
|
applyCustomCss(customCss.value)
|
||||||
}
|
}
|
||||||
loadPlain(path.value)
|
loadPlain(path.value)
|
||||||
}
|
}
|
||||||
@@ -364,9 +367,12 @@ function applyCustomCss(css) {
|
|||||||
if (!el) {
|
if (!el) {
|
||||||
el = document.createElement('style')
|
el = document.createElement('style')
|
||||||
el.id = 'pagerite-user'
|
el.id = 'pagerite-user'
|
||||||
document.head.append(el)
|
|
||||||
}
|
}
|
||||||
el.textContent = css
|
el.textContent = css
|
||||||
|
// Keep it last in <head>: in dev Vite injects the base stylesheet
|
||||||
|
// after the server-rendered tag, and equal-specificity :root rules
|
||||||
|
// (font variables) are decided by order.
|
||||||
|
document.head.append(el)
|
||||||
} else if (el) {
|
} else if (el) {
|
||||||
el.remove()
|
el.remove()
|
||||||
}
|
}
|
||||||
@@ -388,6 +394,98 @@ function setCssDocument(text) {
|
|||||||
customCss.value = text
|
customCss.value = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Font overrides --------------------------------------------------------
|
||||||
|
// Font picks live in the custom CSS as plain :root rows in one exact format
|
||||||
|
// (` --font-body: var(--font-source-sans);`), so no marker comments are
|
||||||
|
// needed: the rows are parsed out on load, and on change they are stripped
|
||||||
|
// and rewritten — adding a :root block if none exists, dropping it when the
|
||||||
|
// last font row goes away. Values reference the per-family variables from
|
||||||
|
// pagerite.css, so no font stacks are spelled out here.
|
||||||
|
const FONT_ROW = /^\s*--font-(body|heading|brand):\s*var\(--font-[a-z-]+\);\s*$/
|
||||||
|
const FONT_OPTIONS = [
|
||||||
|
{ value: 'var(--font-source-serif)', label: 'Source Serif 4', serif: true },
|
||||||
|
{ value: 'var(--font-fraunces)', label: 'Fraunces', serif: true },
|
||||||
|
{ value: 'var(--font-literata)', label: 'Literata', serif: true },
|
||||||
|
{ value: 'var(--font-source-sans)', label: 'Source Sans 3', serif: false },
|
||||||
|
{ value: 'var(--font-inter)', label: 'Inter', serif: false },
|
||||||
|
{ value: 'var(--font-montserrat)', label: 'Montserrat', serif: false },
|
||||||
|
{ value: 'var(--font-fira-code)', label: 'Fira Code', serif: false },
|
||||||
|
]
|
||||||
|
const fontHeading = ref('')
|
||||||
|
const fontBody = ref('')
|
||||||
|
const fontBrand = ref('')
|
||||||
|
|
||||||
|
// Font picker popup: a stylized "A" opens a panel with a tab per target
|
||||||
|
// (heading/body/brand); each option's name is its own preview, rendered in
|
||||||
|
// the candidate font at the size and weight of the element being styled.
|
||||||
|
const fontPicker = ref(null) // open tab: 'heading' | 'body' | 'brand' | null
|
||||||
|
let fontTabLast = 'body'
|
||||||
|
const serifFonts = computed(() => FONT_OPTIONS.filter((o) => o.serif))
|
||||||
|
const sansFonts = computed(() => FONT_OPTIONS.filter((o) => !o.serif))
|
||||||
|
|
||||||
|
function toggleFontPanel() {
|
||||||
|
if (fontPicker.value) {
|
||||||
|
fontTabLast = fontPicker.value
|
||||||
|
fontPicker.value = null
|
||||||
|
} else {
|
||||||
|
fontPicker.value = fontTabLast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fontRefFor(name) {
|
||||||
|
return { heading: fontHeading, body: fontBody, brand: fontBrand }[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
function fontStyleFor(name) {
|
||||||
|
if (name === 'brand') return { fontWeight: 700, fontSize: '1.5rem' }
|
||||||
|
if (name === 'heading') return { fontWeight: 600, fontSize: '1.3rem' }
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFont(value) {
|
||||||
|
const r = fontRefFor(fontPicker.value)
|
||||||
|
// Clicking the current pick clears it back to the base-style default.
|
||||||
|
r.value = r.value === value ? '' : value
|
||||||
|
onFontChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fontRows() {
|
||||||
|
const rows = []
|
||||||
|
if (fontBody.value) rows.push(` --font-body: ${fontBody.value};`)
|
||||||
|
if (fontHeading.value) rows.push(` --font-heading: ${fontHeading.value};`)
|
||||||
|
if (fontBrand.value) rows.push(` --font-brand: ${fontBrand.value};`)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFontChange() {
|
||||||
|
const rows = fontRows()
|
||||||
|
// Strip our rows wherever they are, then drop :root blocks left empty.
|
||||||
|
let css = customCss.value
|
||||||
|
.split('\n')
|
||||||
|
.filter((l) => !FONT_ROW.test(l))
|
||||||
|
.join('\n')
|
||||||
|
.replace(/:root\s*\{\s*\}\n?/g, '')
|
||||||
|
if (rows.length) {
|
||||||
|
if (/:root\s*\{/.test(css)) {
|
||||||
|
// Merge into the existing :root block.
|
||||||
|
css = css.replace(/:root\s*\{/, (m) => `${m}\n${rows.join('\n')}`)
|
||||||
|
} else {
|
||||||
|
const rest = css.trimStart()
|
||||||
|
css = `:root {\n${rows.join('\n')}\n}\n${rest ? `\n${rest}` : ''}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCssDocument(css)
|
||||||
|
onCustomCssInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFonts(css) {
|
||||||
|
const get = (name) =>
|
||||||
|
css.match(new RegExp(`^\\s*--font-${name}:\\s*(var\\(--font-[a-z-]+\\));`, 'm'))?.[1] || ''
|
||||||
|
fontBody.value = get('body')
|
||||||
|
fontHeading.value = get('heading')
|
||||||
|
fontBrand.value = get('brand')
|
||||||
|
}
|
||||||
|
|
||||||
// Two-step delete (no dialogs): the first click arms the row's button for
|
// Two-step delete (no dialogs): the first click arms the row's button for
|
||||||
// a few seconds, the second actually deletes.
|
// a few seconds, the second actually deletes.
|
||||||
const arming = ref(null)
|
const arming = ref(null)
|
||||||
@@ -691,6 +789,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
addEventListener('keydown', onKeydown)
|
addEventListener('keydown', onKeydown)
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
|
parseFonts(customCss.value)
|
||||||
setCssDocument(customCss.value)
|
setCssDocument(customCss.value)
|
||||||
applyCustomCss(customCss.value)
|
applyCustomCss(customCss.value)
|
||||||
})
|
})
|
||||||
@@ -736,7 +835,57 @@ onUnmounted(() => {
|
|||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="font-btn"
|
||||||
|
:class="{ active: !!fontPicker }"
|
||||||
|
title="Fonts"
|
||||||
|
@click="toggleFontPanel"
|
||||||
|
>
|
||||||
|
A
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="fontPicker" class="font-picker">
|
||||||
|
<div class="font-tabs">
|
||||||
|
<button
|
||||||
|
v-for="name in ['body', 'heading', 'brand']"
|
||||||
|
:key="name"
|
||||||
|
type="button"
|
||||||
|
:class="{ active: fontPicker === name }"
|
||||||
|
@click="fontPicker = name"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="font-cols">
|
||||||
|
<div class="font-col">
|
||||||
|
<button
|
||||||
|
v-for="opt in serifFonts"
|
||||||
|
:key="opt.label"
|
||||||
|
type="button"
|
||||||
|
class="font-opt"
|
||||||
|
:class="{ current: fontRefFor(fontPicker).value === opt.value }"
|
||||||
|
:style="{ fontFamily: opt.value, ...fontStyleFor(fontPicker) }"
|
||||||
|
@click="pickFont(opt.value)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="font-col">
|
||||||
|
<button
|
||||||
|
v-for="opt in sansFonts"
|
||||||
|
:key="opt.label"
|
||||||
|
type="button"
|
||||||
|
class="font-opt"
|
||||||
|
:class="{ current: fontRefFor(fontPicker).value === opt.value }"
|
||||||
|
:style="{ fontFamily: opt.value, ...fontStyleFor(fontPicker) }"
|
||||||
|
@click="pickFont(opt.value)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div ref="cssEl" class="css-cm" />
|
<div ref="cssEl" class="css-cm" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -864,6 +1013,93 @@ onUnmounted(() => {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stylized "A" icon button opening the font panel — always Fira Code, so
|
||||||
|
it stays distinctive no matter which fonts are configured. */
|
||||||
|
.font-btn {
|
||||||
|
font-family: var(--font-fira-code);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
color: var(--muted);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-btn:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font picker panel. Colors come from the theme variables, so contrast
|
||||||
|
against the page background is automatic in both light and dark themes. */
|
||||||
|
.font-picker {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 16px #0004;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-tabs button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--muted);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-tabs button.active {
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each option's name is its own preview (font family + size/weight of the
|
||||||
|
element being styled are set inline). */
|
||||||
|
.font-opt {
|
||||||
|
padding: 0.3rem 0.45rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-opt:hover {
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-opt.current {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Small CodeMirror window for the banner HTML; scrolls internally. */
|
/* Small CodeMirror window for the banner HTML; scrolls internally. */
|
||||||
.banner-cm {
|
.banner-cm {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* Variable fonts: only downloaded when referenced by the page CSS. */
|
/* Variable fonts: only downloaded when referenced by the page CSS. */
|
||||||
|
@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'); }
|
||||||
@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: 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: '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: normal; font-display: swap; src: url('montserrat.woff2') format('woff2'); }
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -12,9 +12,20 @@
|
|||||||
--accent: #0056b3;
|
--accent: #0056b3;
|
||||||
--accent2: #003d80;
|
--accent2: #003d80;
|
||||||
--line: #0000001a;
|
--line: #0000001a;
|
||||||
--font-body: "Source Sans 3", system-ui, sans-serif;
|
/* One variable per available font family (faces in fonts/fonts.css);
|
||||||
--font-heading: "Source Serif 4", Georgia, serif;
|
everything else references these, so font stacks live here only. */
|
||||||
--font-code: "Fira Code", ui-monospace, monospace;
|
--font-source-sans: "Source Sans 3", system-ui, sans-serif;
|
||||||
|
--font-source-serif: "Source Serif 4", Georgia, serif;
|
||||||
|
--font-fraunces: "Fraunces", Georgia, serif;
|
||||||
|
--font-literata: "Literata", Georgia, serif;
|
||||||
|
--font-inter: "Inter", system-ui, sans-serif;
|
||||||
|
--font-montserrat: "Montserrat", system-ui, sans-serif;
|
||||||
|
--font-fira-code: "Fira Code", ui-monospace, monospace;
|
||||||
|
--font-body: var(--font-source-sans);
|
||||||
|
--font-heading: var(--font-source-serif);
|
||||||
|
/* The brand follows the heading font unless overridden separately. */
|
||||||
|
--font-brand: var(--font-heading);
|
||||||
|
--font-code: var(--font-fira-code);
|
||||||
/* Width of the docked editor panel (used both here for shifting the page
|
/* Width of the docked editor panel (used both here for shifting the page
|
||||||
and in the Vue editor's own styles). */
|
and in the Vue editor's own styles). */
|
||||||
--editor-w: min(46rem, 50vw);
|
--editor-w: min(46rem, 50vw);
|
||||||
@@ -79,13 +90,23 @@ body {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Legibility scrim: fade banner artwork into the page background behind
|
||||||
|
the overlaid brand/nav. Text shadows are not used for this — on light
|
||||||
|
backgrounds they just look like blurry text. */
|
||||||
|
#page-banner::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 30% 0 0;
|
||||||
|
background: linear-gradient(transparent, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
#brand,
|
#brand,
|
||||||
#nav {
|
#nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#brand {
|
#brand {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-brand);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -97,7 +118,6 @@ body {
|
|||||||
/* Nav overlaid at the bottom of the banner */
|
/* Nav overlaid at the bottom of the banner */
|
||||||
#nav {
|
#nav {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
text-shadow: 0 0 0.1em black;
|
|
||||||
padding: 0.35rem 1.25rem;
|
padding: 0.35rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +291,6 @@ article h3 {
|
|||||||
article h1 {
|
article h1 {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
margin: 2rem 0 1.2rem;
|
margin: 2rem 0 1.2rem;
|
||||||
color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Margin strategy: bottom-only inside articles. Top margins misalign
|
/* Margin strategy: bottom-only inside articles. Top margins misalign
|
||||||
@@ -289,7 +308,6 @@ article figure {
|
|||||||
|
|
||||||
article h3 {
|
article h3 {
|
||||||
margin: 1.4rem 0 0.4rem;
|
margin: 1.4rem 0 0.4rem;
|
||||||
color: color-mix(in oklab, var(--accent2) 60%, var(--muted));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lists: small diamond emoji markers — blue 🔹 on odd nesting levels,
|
/* Lists: small diamond emoji markers — blue 🔹 on odd nesting levels,
|
||||||
@@ -398,7 +416,6 @@ article dd {
|
|||||||
article h2 {
|
article h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 2.2rem 0 0.6rem;
|
margin: 2.2rem 0 0.6rem;
|
||||||
color: var(--accent2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
article a {
|
article a {
|
||||||
|
|||||||
@@ -29,6 +29,26 @@
|
|||||||
background: url("./banner.svg") center 40% / cover;
|
background: url("./banner.svg") center 40% / cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Colored headings are part of the theme's more elaborate styling; the
|
||||||
|
base keeps headings in plain text color. */
|
||||||
|
article h1 {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
article h2 {
|
||||||
|
color: var(--accent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
article h3 {
|
||||||
|
color: color-mix(in oklab, var(--accent2) 60%, var(--muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark artwork: keep the nav readable with a shadow (the base style uses
|
||||||
|
a scrim instead — shadows only work on dark backgrounds). */
|
||||||
|
#nav {
|
||||||
|
text-shadow: 0 0 0.15em black;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background: #ffffff09;
|
background: #ffffff09;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,18 @@
|
|||||||
// support from the article itself and are re-applied after each swap.
|
// support from the article itself and are re-applied after each swap.
|
||||||
(() => {
|
(() => {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
import("./assets/pagerite.css");
|
|
||||||
// The theme is selectable; the backend names the active one in a meta
|
// The theme is selectable; the backend names the active one in a meta
|
||||||
// tag (dev links no stylesheets — Vite injects them from JS).
|
// tag (dev links no stylesheets — Vite injects them from JS).
|
||||||
const theme = document.querySelector('meta[name="pagerite:theme"]')?.content;
|
const theme = document.querySelector('meta[name="pagerite:theme"]')?.content;
|
||||||
if (theme) import(/* @vite-ignore */ `./assets/themes/${theme}/theme.css`);
|
const sheets = [import("./assets/pagerite.css")];
|
||||||
|
if (theme) sheets.push(import(/* @vite-ignore */ `./assets/themes/${theme}/theme.css`));
|
||||||
|
// The injected styles land after the server-rendered custom CSS in
|
||||||
|
// <head>; move the custom CSS back to the end so its equal-specificity
|
||||||
|
// :root rules (font variables) win.
|
||||||
|
Promise.all(sheets).then(() => {
|
||||||
|
const el = document.getElementById("pagerite-user");
|
||||||
|
if (el) document.head.append(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGIONS = ["page-banner", "nav", "sidebar", "main"];
|
const REGIONS = ["page-banner", "nav", "sidebar", "main"];
|
||||||
@@ -192,11 +199,14 @@
|
|||||||
el.replaceWith(document.importNode(doc.getElementById(id), true));
|
el.replaceWith(document.importNode(doc.getElementById(id), true));
|
||||||
}
|
}
|
||||||
// Site-wide custom CSS lives in <head id="pagerite-user"> and must be
|
// Site-wide custom CSS lives in <head id="pagerite-user"> and must be
|
||||||
// kept in sync across fetch-navigations.
|
// kept in sync across fetch-navigations. It is kept last in <head>:
|
||||||
|
// in dev Vite injects the base stylesheet after the server-rendered
|
||||||
|
// tag, and equal-specificity :root rules are decided by order.
|
||||||
const oldUserStyle = document.getElementById("pagerite-user");
|
const oldUserStyle = document.getElementById("pagerite-user");
|
||||||
const newUserStyle = doc.getElementById("pagerite-user");
|
const newUserStyle = doc.getElementById("pagerite-user");
|
||||||
if (oldUserStyle && newUserStyle) {
|
if (oldUserStyle && newUserStyle) {
|
||||||
oldUserStyle.textContent = newUserStyle.textContent;
|
oldUserStyle.textContent = newUserStyle.textContent;
|
||||||
|
document.head.appendChild(oldUserStyle);
|
||||||
} else if (newUserStyle) {
|
} else if (newUserStyle) {
|
||||||
document.head.appendChild(document.importNode(newUserStyle, true));
|
document.head.appendChild(document.importNode(newUserStyle, true));
|
||||||
} else if (oldUserStyle) {
|
} else if (oldUserStyle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user