Add corporate and nitro themes, inline theme banners, code palette sets
- Two new themes (corporate light/dark, nitro racing) and a purple retune; theme banner SVGs moved to pagerite/themes/, inlined by the backend, recolored per scheme via CSS classes, with scroll parallax. - pygments.css maps tokens onto --code-* variables; light and dark palette sets in the base stylesheet resolve via light-dark() from the theme's color-scheme; themes just pick a set or retint --code-bg. - Sidebar omitted unless the section has at least two published items; fetch-navigation and the site editor handle it appearing/disappearing. - Floated figures keep captions below at 30% of the text column; h1/h2 clear floats so images never overflow into the next section. - Links use --link (color-mix of text and accent), weight 500, no underlines; hover goes full accent.
This commit is contained in:
@@ -54,7 +54,11 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
moves never break them. `Node.banner` is a raw trusted HTML snippet
|
||||
for the header banner (img, styled div, canvas+script...); empty
|
||||
inherits from the node's ancestors (front page last), then the active
|
||||
theme's banner artwork (or a plain gradient in the base stylesheet).
|
||||
theme's banner artwork: an inline SVG from
|
||||
`pagerite/themes/{theme}/banner.svg`, inlined into `#page-banner` by
|
||||
the backend only when no user banner applies (so it is recolorable
|
||||
from the theme CSS via `var(...)` and never fights user designs; the
|
||||
base stylesheet falls back to a plain gradient).
|
||||
`Data.version` is bumped on every write
|
||||
and embedded in page ETags so nav-affecting changes invalidate caches.
|
||||
`Data.brand` is the site name (header link + `<title>` suffix), editable
|
||||
@@ -81,8 +85,11 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
is NOT rendered as an additional h1 (it still supplies <title> and nav
|
||||
labels). The navbar holds
|
||||
top-level items only; the current section's subitems go to a left
|
||||
`#sidebar` (empty and hidden elsewhere). Dynamic regions have stable ids
|
||||
(`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps.
|
||||
`#sidebar`, which is rendered only when the section offers at least two
|
||||
published items (no aside element at all on the front page, leaf pages
|
||||
and one-page sections). Dynamic regions have stable ids
|
||||
(`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps
|
||||
(`#sidebar` may be absent on either side of a swap).
|
||||
- `seed.py` — demo content written on startup for paths missing from the
|
||||
database (never overwrites existing pages).
|
||||
- `frontend/src/` — the Vue editor and public-page entries.
|
||||
@@ -98,8 +105,15 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
stylesheets (base and theme) so they can be swapped or augmented.
|
||||
- `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 Source
|
||||
`themes/{purple,corporate,nitro}/theme.css` (theme overrides and font
|
||||
picks: `purple` = dark dusk palette with Fraunces/Literata and a tilted
|
||||
oversized gradient brand; `corporate` = light-first with automatic
|
||||
`prefers-color-scheme` dark mode, Montserrat/Inter and a huge solid
|
||||
brand; `nitro` = racing/HUD style following `prefers-color-scheme`
|
||||
(warm light-grey page, deep violet in dark), Montserrat/Literata,
|
||||
black as an accent only, a straight orange blade under the banner, and
|
||||
an orange racing-tab nav clipped with a bezier `shape()`), `pygments.css`,
|
||||
and `fonts/` (self-hosted Source
|
||||
Sans 3/Source Serif 4/Fraunces/Literata/Cormorant/Playfair
|
||||
Display/Inter/Montserrat/Fira Code
|
||||
variable woff2). The `::view-transition*` block at the end of `pagerite.css` (from
|
||||
@@ -151,7 +165,10 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
`appType: 'mpa'` (no SPA fallback) and builds with `manifest: true`,
|
||||
`assetsDir: '_/assets'` (so the build mirrors the URL space;
|
||||
`frontend/public/favicon.ico` lands at the build root and is served at
|
||||
`/favicon.ico`). JS inputs are `src/main.js` and `src/pagerite.js`; there
|
||||
`/favicon.ico`). JS inputs are `src/main.js` and `src/pagerite.js`, plus
|
||||
`src/assets/pagerite.css` and every `src/assets/themes/*/theme.css` as
|
||||
separate stylesheet entries (enumerated from the themes directory, so new
|
||||
themes need no config change); there
|
||||
is no `index.html` source (it would shadow `/` and turn missing dev paths
|
||||
into an empty Vue shell). All outputs are ES modules. The build sets
|
||||
`preserveEntrySignatures: 'exports-only'` because main.js is consumed
|
||||
@@ -202,8 +219,11 @@ not for the public pages. See `docs/design-principles.md` for the design.
|
||||
keep its integration points (`Frontend`, build hook) intact.
|
||||
- **markdown-it-py** — Markdown rendering with `html=True` raw
|
||||
passthrough; mdit-py-plugins for footnote/deflist/tasklists/attrs;
|
||||
**Pygments** for server-side code highlighting (`nowrap` spans, styles
|
||||
in `frontend/src/assets/pygments.css` scoped to "pre code").
|
||||
**Pygments** for server-side code highlighting (`nowrap` spans, styled
|
||||
by `frontend/src/assets/pygments.css` which maps token classes 1:1 onto
|
||||
the `--code-*` variables; light/dark palette sets live in
|
||||
`pagerite.css` and resolve via `light-dark()` from the theme's
|
||||
`color-scheme` — themes pick a set, not individual colors).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
+40
-13
@@ -52,8 +52,12 @@ evolves.
|
||||
- Renderer: **markdown-it-py** with mdit-py-plugins (footnotes, definition
|
||||
lists, task lists, brace-attributes; tables and strikethrough from the
|
||||
default preset), with `html=True` for raw passthrough. Fenced code blocks
|
||||
are highlighted server-side with **Pygments** (github-dark palette in
|
||||
`/_assets/pygments-*.css`); a JS copy button appears on hover. Should this
|
||||
are highlighted server-side with **Pygments** (`nowrap` spans styled by
|
||||
`/_assets/pygments-*.css`, which maps every token class onto the `--code-*`
|
||||
variables; the base stylesheet defines light and dark palette sets resolved
|
||||
via `light-dark()`, so each theme gets the set matching its `color-scheme`
|
||||
and may only retint `--code-bg` to keep the well in the page's color
|
||||
family); a JS copy button appears on hover. Should this
|
||||
prove limiting, we implement our own renderer on top of html5tagger,
|
||||
which we already use for all HTML generation.
|
||||
- **Files are content-addressed.** Uploads (`PUT /_api/files/{filename}`)
|
||||
@@ -62,10 +66,13 @@ evolves.
|
||||
that survive page renames and dedupe identical content; pages no longer
|
||||
own files. An image with a title becomes a `<figure>` with
|
||||
`<figcaption>`. Positioning is by attribute classes:
|
||||
`{.right}` — `{.right}`, `{.left}` float,
|
||||
`{.right}` — `{.right}`, `{.left}` float at
|
||||
30% of the text column (the caption wraps within it; an explicit
|
||||
`width=300` overrides on uncaptioned images),
|
||||
`{.wide}` goes full bleed (viewport edge to edge, or up to the docked
|
||||
editor; the sidebar stacks on top of it); plain attributes like `width=300`
|
||||
work too.
|
||||
work too. Headings (h1/h2) clear floats, so images never overflow into the
|
||||
next section.
|
||||
|
||||
## Page structure and navigation
|
||||
|
||||
@@ -78,8 +85,15 @@ evolves.
|
||||
**per-page configurable**: `Node.banner` holds an arbitrary trusted HTML
|
||||
snippet (an image, a styled div, canvas + script — anything), resolved by
|
||||
walking up the node's ancestors to the front page; when nothing in the
|
||||
chain sets one, the active theme's banner artwork shows (the purple theme
|
||||
ships a `banner.svg`; the base stylesheet falls back to a plain gradient).
|
||||
chain sets one, the active theme's banner artwork shows. That artwork is
|
||||
an **inline SVG** (`pagerite/themes/{name}/banner.svg`) the backend
|
||||
inlines into `#page-banner`: as markup it can be recolored from the theme
|
||||
stylesheet (corporate's single SVG serves both light and dark mode via
|
||||
`var()`-driven stops) and it is never rendered underneath a user banner.
|
||||
The base stylesheet falls back to a plain gradient. There is deliberately
|
||||
no scrim fading the banner into the page background — any such fade would
|
||||
ruin user-supplied designs; themes that want one bake it into their SVG
|
||||
(purple does).
|
||||
- **Fetch-navigation.** Links are plain `<a href>`; a small script
|
||||
(`frontend/src/pagerite.js`) intercepts same-origin clicks, fetches the
|
||||
page, and swaps the `#page-banner`, `#nav`, `#sidebar` and `#main` regions,
|
||||
@@ -99,8 +113,10 @@ evolves.
|
||||
cannot have children. The header navbar holds only the top level; a
|
||||
top-level item is highlighted when viewing any of its subpages. When the
|
||||
current page is inside a main level section with children, those direct
|
||||
children are listed in a **left sidebar** (`#sidebar`), one level deep;
|
||||
the sidebar is empty (and hidden) elsewhere. Other sections' subitems
|
||||
children are listed in a **left sidebar** (`#sidebar`), one level deep.
|
||||
The sidebar exists only when there is something to navigate — sections
|
||||
with fewer than two published items, leaf pages and the front page render
|
||||
no aside element at all. Other sections' subitems
|
||||
are never shown without navigating into them first.
|
||||
- **Landing pages are optional.** Every label can either have content
|
||||
(`Node.content`, a Markdown page) or none — a content-less label renders
|
||||
@@ -133,13 +149,24 @@ 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`) overrides those variables and
|
||||
(`frontend/src/assets/themes/{name}/theme.css` — currently `purple`, `corporate`
|
||||
and `nitro`) 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/`
|
||||
where needed. The corporate and nitro themes switch palettes automatically via
|
||||
`prefers-color-scheme` (corporate is light-first with a matching dark palette;
|
||||
nitro a warm light-grey page or, in dark mode, a deep violet one — its dark
|
||||
banner and orange accents carry over unchanged); purple (dusk) uses one
|
||||
fixed palette for everyone. Themes may restyle structural details the base
|
||||
leaves plain — heading colors and underlines, list markers, nav treatment,
|
||||
brand sizing. The banner artwork has scroll parallax: pagerite.js sets the
|
||||
`--pry` scroll parameter on `<html>` (event-driven, so it is still when the
|
||||
page is idle), the banner contents drift within their window (with scale
|
||||
overscan so no edge shows), and themes may key their own effects off the
|
||||
same parameter — purple's sun rises as you scroll.
|
||||
- Fonts, the shared stylesheet and pygments styles
|
||||
live under `frontend/src/assets/` and are emitted as hashed assets under
|
||||
`/_assets/`
|
||||
(Source Serif 4 for headings, Source Sans 3 for body, Fira Code for code
|
||||
by default; Fraunces, Literata, Cormorant, Playfair Display, Inter and
|
||||
Montserrat kept as woff2 options with local `@font-face`, variable-weight
|
||||
|
||||
@@ -105,11 +105,22 @@ function openPath(p) {
|
||||
|
||||
// --- In-place navigation (no transitions, replaceState) ------------------
|
||||
function swapRegions(doc) {
|
||||
for (const id of ['page-banner', 'nav', 'sidebar', 'main']) {
|
||||
for (const id of ['page-banner', 'nav', 'main']) {
|
||||
const fresh = doc.getElementById(id)
|
||||
const el = document.getElementById(id)
|
||||
if (fresh && el) el.replaceWith(document.importNode(fresh, true))
|
||||
}
|
||||
// #sidebar is omitted entirely when the section has no sub-navigation,
|
||||
// so it may be absent on either side: replace, insert, or remove.
|
||||
const freshSidebar = doc.getElementById('sidebar')
|
||||
const curSidebar = document.getElementById('sidebar')
|
||||
if (freshSidebar && curSidebar) {
|
||||
curSidebar.replaceWith(document.importNode(freshSidebar, true))
|
||||
} else if (freshSidebar) {
|
||||
document.getElementById('main')?.before(document.importNode(freshSidebar, true))
|
||||
} else if (curSidebar) {
|
||||
curSidebar.remove()
|
||||
}
|
||||
// The brand link lives in the header, outside the swappable regions,
|
||||
// and is absent entirely when no brand is configured.
|
||||
const freshBrand = doc.getElementById('brand')
|
||||
@@ -283,6 +294,8 @@ const theme = ref('purple')
|
||||
const THEME_OPTIONS = [
|
||||
{ value: '', label: 'none' },
|
||||
{ value: 'purple', label: 'purple' },
|
||||
{ value: 'corporate', label: 'corporate' },
|
||||
{ value: 'nitro', label: 'nitro' },
|
||||
]
|
||||
|
||||
async function loadSettings() {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
@import url("./pygments.css");
|
||||
|
||||
:root {
|
||||
/* The base theme is light, period — this also resolves the light-dark()
|
||||
code palette below (themes declare their own color-scheme). */
|
||||
color-scheme: light;
|
||||
--bg: #ffffff;
|
||||
--surface: #f5f5f5;
|
||||
--text: #1a1a1a;
|
||||
@@ -12,6 +15,28 @@
|
||||
--accent: #0056b3;
|
||||
--accent2: #003d80;
|
||||
--line: #0000001a;
|
||||
/* Links stay quiet — mostly text color with a hint of the accent —
|
||||
and light up to the full accent on hover. */
|
||||
--link: color-mix(in oklab, var(--text) 50%, var(--accent));
|
||||
/* Code highlighting palette, consumed by pygments.css: complete light and
|
||||
dark sets (background included), resolved by light-dark() from the
|
||||
used color-scheme. A theme picks a set simply by declaring
|
||||
color-scheme; a fixed dark code block on a light page pins the dark
|
||||
set with `pre { color-scheme: dark }`. Only override individual
|
||||
--code-* variables for specific theming needs. */
|
||||
--code-bg: light-dark(var(--surface), #0d1117);
|
||||
--code-text: light-dark(#24292f, #e6edf3);
|
||||
--code-muted: light-dark(#6e7781, #6e7681);
|
||||
--code-comment: light-dark(#59636e, #8b949e);
|
||||
--code-keyword: light-dark(#cf222e, #ff7b72);
|
||||
--code-builtin: light-dark(#0550ae, #79c0ff);
|
||||
--code-string: light-dark(#0a3069, #a5d6ff);
|
||||
--code-function: light-dark(#8250df, #d2a8ff);
|
||||
--code-type: light-dark(#953800, #f0883e);
|
||||
--code-tag: light-dark(#116329, #7ee787);
|
||||
--code-error: light-dark(#d1242f, #f85149);
|
||||
--code-added: light-dark(#1a7f37, #56d364);
|
||||
--code-hl: light-dark(#fff8c5, #6e7681);
|
||||
/* One variable per available font family (faces in fonts/fonts.css);
|
||||
everything else references these, so font stacks live here only. */
|
||||
--font-source-sans: "Source Sans 3", system-ui, sans-serif;
|
||||
@@ -77,8 +102,10 @@ body {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Per-page banner content (img, styled div, canvas...) overlays the
|
||||
default artwork; swapped along with #nav/#main on fetch-navigation. */
|
||||
/* Per-page banner content (img, styled div, inline SVG...) fills the
|
||||
banner; swapped along with #nav/#main on fetch-navigation. When the page
|
||||
sets no banner of its own, the backend inlines the active theme's SVG
|
||||
artwork here instead (pagerite/themes/{theme}/banner.svg). */
|
||||
#page-banner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -90,16 +117,11 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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));
|
||||
/* Scroll parallax: pagerite.js sets --pry on <html>; the banner stays
|
||||
windowed in place while the artwork drifts inside it. The scale
|
||||
provides overscan so the drift never reveals an edge. */
|
||||
transform: scale(1.25) translateY(var(--pry, 0px));
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#brand,
|
||||
@@ -247,10 +269,6 @@ button.banner-edit-link {
|
||||
backdrop-filter: blur(0.5rem);
|
||||
}
|
||||
|
||||
#sidebar:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -436,12 +454,13 @@ article h2 {
|
||||
}
|
||||
|
||||
article a {
|
||||
color: var(--accent);
|
||||
color: var(--link);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
article a:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Blockquotes: inner paragraphs carry no margins (spacing comes from the
|
||||
@@ -463,9 +482,11 @@ pre {
|
||||
overflow-x: auto;
|
||||
padding: 0.5rem 0.8rem;
|
||||
/* Code text aligns with the surrounding paragraphs: the box extends
|
||||
past them by its own padding. */
|
||||
past them by its own padding. Themes that add a left border must
|
||||
extend margin-left by the border width to keep this alignment. */
|
||||
margin-left: -0.8rem;
|
||||
margin-right: -0.8rem;
|
||||
background: var(--code-bg);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
@@ -531,7 +552,6 @@ figure {
|
||||
figcaption {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
/* Captions of full-bleed images: centered and kept to a readable width. */
|
||||
@@ -546,14 +566,12 @@ figure:has(.right),
|
||||
img.right {
|
||||
float: right;
|
||||
margin: 0.3rem 0 1rem 1.5rem;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
figure:has(.left),
|
||||
img.left {
|
||||
float: left;
|
||||
margin: 0.3rem 1.5rem 1rem 0;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
/* .wide is full bleed: edge to edge of the viewport (or of the space left
|
||||
@@ -577,6 +595,44 @@ p:has(> img.wide:only-child) {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* A captioned image is a <figure>, and the positioning class lives on the
|
||||
img inside it: only the figure may float/shift, never the img itself,
|
||||
or the caption would wrap beside the image instead of sitting below.
|
||||
The image fills the figure's width. */
|
||||
figure:has(.right) img,
|
||||
figure:has(.left) img {
|
||||
float: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
figure:has(.wide) img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Floats take a fixed share of the text column rather than sizing by the
|
||||
image's intrinsic width, which varies wildly (SVGs have none, and
|
||||
min-content collapses them). A percentage also scales correctly inside
|
||||
two-column segments, where the column is the containing block. The
|
||||
caption wraps within that width. A width attribute ({.right width=300})
|
||||
overrides the default on uncaptioned images. */
|
||||
figure:has(.right),
|
||||
figure:has(.left),
|
||||
img.right:not([width]),
|
||||
img.left:not([width]) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
/* Section headings start below any floated figures of the previous
|
||||
section, so images never overflow into the next one. */
|
||||
article h1,
|
||||
article h2 {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
body.editing figure:has(.wide),
|
||||
body.editing img.wide {
|
||||
width: calc(100vw - var(--editor-w) - 1rem);
|
||||
|
||||
@@ -1,86 +1,90 @@
|
||||
/* Pygments token styling, mapped 1:1 onto the --code-* palette variables
|
||||
defined in pagerite.css (light and dark sets resolved there via
|
||||
light-dark() from the theme's color-scheme). No literal colors in this
|
||||
file: recoloring code happens through the variables only. */
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: #6e7681; background-color: #0d1117; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #6e7681; background-color: #0d1117; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #e6edf3; background-color: #6e7681; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #e6edf3; background-color: #6e7681; padding-left: 5px; padding-right: 5px; }
|
||||
pre code .hll { background-color: #6e7681 }
|
||||
pre code { color: #E6EDF3 }
|
||||
pre code .c { color: #8B949E; font-style: italic } /* Comment */
|
||||
pre code .err { color: #F85149 } /* Error */
|
||||
pre code .esc { color: #E6EDF3 } /* Escape */
|
||||
pre code .g { color: #E6EDF3 } /* Generic */
|
||||
pre code .k { color: #FF7B72 } /* Keyword */
|
||||
pre code .l { color: #A5D6FF } /* Literal */
|
||||
pre code .n { color: #E6EDF3 } /* Name */
|
||||
pre code .o { color: #FF7B72; font-weight: bold } /* Operator */
|
||||
pre code .x { color: #E6EDF3 } /* Other */
|
||||
pre code .p { color: #E6EDF3 } /* Punctuation */
|
||||
pre code .ch { color: #8B949E; font-style: italic } /* Comment.Hashbang */
|
||||
pre code .cm { color: #8B949E; font-style: italic } /* Comment.Multiline */
|
||||
pre code .cp { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Preproc */
|
||||
pre code .cpf { color: #8B949E; font-style: italic } /* Comment.PreprocFile */
|
||||
pre code .c1 { color: #8B949E; font-style: italic } /* Comment.Single */
|
||||
pre code .cs { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
pre code .gd { color: #FFA198; background-color: #490202 } /* Generic.Deleted */
|
||||
pre code .ge { color: #E6EDF3; font-style: italic } /* Generic.Emph */
|
||||
pre code .ges { color: #E6EDF3; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
pre code .gr { color: #FFA198 } /* Generic.Error */
|
||||
pre code .gh { color: #79C0FF; font-weight: bold } /* Generic.Heading */
|
||||
pre code .gi { color: #56D364; background-color: #0F5323 } /* Generic.Inserted */
|
||||
pre code .go { color: #8B949E } /* Generic.Output */
|
||||
pre code .gp { color: #8B949E } /* Generic.Prompt */
|
||||
pre code .gs { color: #E6EDF3; font-weight: bold } /* Generic.Strong */
|
||||
pre code .gu { color: #79C0FF } /* Generic.Subheading */
|
||||
pre code .gt { color: #FF7B72 } /* Generic.Traceback */
|
||||
pre code .g-Underline { color: #E6EDF3; text-decoration: underline } /* Generic.Underline */
|
||||
pre code .kc { color: #79C0FF } /* Keyword.Constant */
|
||||
pre code .kd { color: #FF7B72 } /* Keyword.Declaration */
|
||||
pre code .kn { color: #FF7B72 } /* Keyword.Namespace */
|
||||
pre code .kp { color: #79C0FF } /* Keyword.Pseudo */
|
||||
pre code .kr { color: #FF7B72 } /* Keyword.Reserved */
|
||||
pre code .kt { color: #FF7B72 } /* Keyword.Type */
|
||||
pre code .ld { color: #79C0FF } /* Literal.Date */
|
||||
pre code .m { color: #A5D6FF } /* Literal.Number */
|
||||
pre code .s { color: #A5D6FF } /* Literal.String */
|
||||
pre code .na { color: #E6EDF3 } /* Name.Attribute */
|
||||
pre code .nb { color: #E6EDF3 } /* Name.Builtin */
|
||||
pre code .nc { color: #F0883E; font-weight: bold } /* Name.Class */
|
||||
pre code .no { color: #79C0FF; font-weight: bold } /* Name.Constant */
|
||||
pre code .nd { color: #D2A8FF; font-weight: bold } /* Name.Decorator */
|
||||
pre code .ni { color: #FFA657 } /* Name.Entity */
|
||||
pre code .ne { color: #F0883E; font-weight: bold } /* Name.Exception */
|
||||
pre code .nf { color: #D2A8FF; font-weight: bold } /* Name.Function */
|
||||
pre code .nl { color: #79C0FF; font-weight: bold } /* Name.Label */
|
||||
pre code .nn { color: #FF7B72 } /* Name.Namespace */
|
||||
pre code .nx { color: #E6EDF3 } /* Name.Other */
|
||||
pre code .py { color: #79C0FF } /* Name.Property */
|
||||
pre code .nt { color: #7EE787 } /* Name.Tag */
|
||||
pre code .nv { color: #79C0FF } /* Name.Variable */
|
||||
pre code .ow { color: #FF7B72; font-weight: bold } /* Operator.Word */
|
||||
pre code .pm { color: #E6EDF3 } /* Punctuation.Marker */
|
||||
pre code .w { color: #6E7681 } /* Text.Whitespace */
|
||||
pre code .mb { color: #A5D6FF } /* Literal.Number.Bin */
|
||||
pre code .mf { color: #A5D6FF } /* Literal.Number.Float */
|
||||
pre code .mh { color: #A5D6FF } /* Literal.Number.Hex */
|
||||
pre code .mi { color: #A5D6FF } /* Literal.Number.Integer */
|
||||
pre code .mo { color: #A5D6FF } /* Literal.Number.Oct */
|
||||
pre code .sa { color: #79C0FF } /* Literal.String.Affix */
|
||||
pre code .sb { color: #A5D6FF } /* Literal.String.Backtick */
|
||||
pre code .sc { color: #A5D6FF } /* Literal.String.Char */
|
||||
pre code .dl { color: #79C0FF } /* Literal.String.Delimiter */
|
||||
pre code .sd { color: #A5D6FF } /* Literal.String.Doc */
|
||||
pre code .s2 { color: #A5D6FF } /* Literal.String.Double */
|
||||
pre code .se { color: #79C0FF } /* Literal.String.Escape */
|
||||
pre code .sh { color: #79C0FF } /* Literal.String.Heredoc */
|
||||
pre code .si { color: #A5D6FF } /* Literal.String.Interpol */
|
||||
pre code .sx { color: #A5D6FF } /* Literal.String.Other */
|
||||
pre code .sr { color: #79C0FF } /* Literal.String.Regex */
|
||||
pre code .s1 { color: #A5D6FF } /* Literal.String.Single */
|
||||
pre code .ss { color: #A5D6FF } /* Literal.String.Symbol */
|
||||
pre code .bp { color: #E6EDF3 } /* Name.Builtin.Pseudo */
|
||||
pre code .fm { color: #D2A8FF; font-weight: bold } /* Name.Function.Magic */
|
||||
pre code .vc { color: #79C0FF } /* Name.Variable.Class */
|
||||
pre code .vg { color: #79C0FF } /* Name.Variable.Global */
|
||||
pre code .vi { color: #79C0FF } /* Name.Variable.Instance */
|
||||
pre code .vm { color: #79C0FF } /* Name.Variable.Magic */
|
||||
pre code .il { color: #A5D6FF } /* Literal.Number.Integer.Long */
|
||||
td.linenos .normal { color: var(--code-muted); background-color: var(--code-bg); padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: var(--code-muted); background-color: var(--code-bg); padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: var(--code-text); background-color: var(--code-muted); padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: var(--code-text); background-color: var(--code-muted); padding-left: 5px; padding-right: 5px; }
|
||||
pre code .hll { background-color: var(--code-hl) }
|
||||
pre code { color: var(--code-text) }
|
||||
pre code .c { color: var(--code-comment); font-style: italic } /* Comment */
|
||||
pre code .err { color: var(--code-error) } /* Error */
|
||||
pre code .esc { color: var(--code-text) } /* Escape */
|
||||
pre code .g { color: var(--code-text) } /* Generic */
|
||||
pre code .k { color: var(--code-keyword) } /* Keyword */
|
||||
pre code .l { color: var(--code-string) } /* Literal */
|
||||
pre code .n { color: var(--code-text) } /* Name */
|
||||
pre code .o { color: var(--code-keyword); font-weight: bold } /* Operator */
|
||||
pre code .x { color: var(--code-text) } /* Other */
|
||||
pre code .p { color: var(--code-text) } /* Punctuation */
|
||||
pre code .ch { color: var(--code-comment); font-style: italic } /* Comment.Hashbang */
|
||||
pre code .cm { color: var(--code-comment); font-style: italic } /* Comment.Multiline */
|
||||
pre code .cp { color: var(--code-comment); font-weight: bold; font-style: italic } /* Comment.Preproc */
|
||||
pre code .cpf { color: var(--code-comment); font-style: italic } /* Comment.PreprocFile */
|
||||
pre code .c1 { color: var(--code-comment); font-style: italic } /* Comment.Single */
|
||||
pre code .cs { color: var(--code-comment); font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
pre code .gd { color: var(--code-error); background-color: color-mix(in oklab, var(--code-error) 25%, var(--code-bg)) } /* Generic.Deleted */
|
||||
pre code .ge { color: var(--code-text); font-style: italic } /* Generic.Emph */
|
||||
pre code .ges { color: var(--code-text); font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
pre code .gr { color: var(--code-error) } /* Generic.Error */
|
||||
pre code .gh { color: var(--code-builtin); font-weight: bold } /* Generic.Heading */
|
||||
pre code .gi { color: var(--code-added); background-color: color-mix(in oklab, var(--code-added) 25%, var(--code-bg)) } /* Generic.Inserted */
|
||||
pre code .go { color: var(--code-muted) } /* Generic.Output */
|
||||
pre code .gp { color: var(--code-muted) } /* Generic.Prompt */
|
||||
pre code .gs { color: var(--code-text); font-weight: bold } /* Generic.Strong */
|
||||
pre code .gu { color: var(--code-builtin) } /* Generic.Subheading */
|
||||
pre code .gt { color: var(--code-error) } /* Generic.Traceback */
|
||||
pre code .g-Underline { color: var(--code-text); text-decoration: underline } /* Generic.Underline */
|
||||
pre code .kc { color: var(--code-builtin) } /* Keyword.Constant */
|
||||
pre code .kd { color: var(--code-keyword) } /* Keyword.Declaration */
|
||||
pre code .kn { color: var(--code-keyword) } /* Keyword.Namespace */
|
||||
pre code .kp { color: var(--code-builtin) } /* Keyword.Pseudo */
|
||||
pre code .kr { color: var(--code-keyword) } /* Keyword.Reserved */
|
||||
pre code .kt { color: var(--code-builtin) } /* Keyword.Type */
|
||||
pre code .ld { color: var(--code-builtin) } /* Literal.Date */
|
||||
pre code .m { color: var(--code-string) } /* Literal.Number */
|
||||
pre code .s { color: var(--code-string) } /* Literal.String */
|
||||
pre code .na { color: var(--code-text) } /* Name.Attribute */
|
||||
pre code .nb { color: var(--code-text) } /* Name.Builtin */
|
||||
pre code .nc { color: var(--code-type); font-weight: bold } /* Name.Class */
|
||||
pre code .no { color: var(--code-builtin); font-weight: bold } /* Name.Constant */
|
||||
pre code .nd { color: var(--code-function); font-weight: bold } /* Name.Decorator */
|
||||
pre code .ni { color: var(--code-type) } /* Name.Entity */
|
||||
pre code .ne { color: var(--code-type); font-weight: bold } /* Name.Exception */
|
||||
pre code .nf { color: var(--code-function); font-weight: bold } /* Name.Function */
|
||||
pre code .nl { color: var(--code-builtin); font-weight: bold } /* Name.Label */
|
||||
pre code .nn { color: var(--code-keyword) } /* Name.Namespace */
|
||||
pre code .nx { color: var(--code-text) } /* Name.Other */
|
||||
pre code .py { color: var(--code-builtin) } /* Name.Property */
|
||||
pre code .nt { color: var(--code-tag) } /* Name.Tag */
|
||||
pre code .nv { color: var(--code-builtin) } /* Name.Variable */
|
||||
pre code .ow { color: var(--code-keyword); font-weight: bold } /* Operator.Word */
|
||||
pre code .pm { color: var(--code-text) } /* Punctuation.Marker */
|
||||
pre code .w { color: var(--code-muted) } /* Text.Whitespace */
|
||||
pre code .mb { color: var(--code-string) } /* Literal.Number.Bin */
|
||||
pre code .mf { color: var(--code-string) } /* Literal.Number.Float */
|
||||
pre code .mh { color: var(--code-string) } /* Literal.Number.Hex */
|
||||
pre code .mi { color: var(--code-string) } /* Literal.Number.Integer */
|
||||
pre code .mo { color: var(--code-string) } /* Literal.Number.Oct */
|
||||
pre code .sa { color: var(--code-builtin) } /* Literal.String.Affix */
|
||||
pre code .sb { color: var(--code-string) } /* Literal.String.Backtick */
|
||||
pre code .sc { color: var(--code-string) } /* Literal.String.Char */
|
||||
pre code .dl { color: var(--code-builtin) } /* Literal.String.Delimiter */
|
||||
pre code .sd { color: var(--code-string) } /* Literal.String.Doc */
|
||||
pre code .s2 { color: var(--code-string) } /* Literal.String.Double */
|
||||
pre code .se { color: var(--code-builtin) } /* Literal.String.Escape */
|
||||
pre code .sh { color: var(--code-builtin) } /* Literal.String.Heredoc */
|
||||
pre code .si { color: var(--code-builtin) } /* Literal.String.Interpol */
|
||||
pre code .sx { color: var(--code-string) } /* Literal.String.Other */
|
||||
pre code .sr { color: var(--code-builtin) } /* Literal.String.Regex */
|
||||
pre code .s1 { color: var(--code-string) } /* Literal.String.Single */
|
||||
pre code .ss { color: var(--code-string) } /* Literal.String.Symbol */
|
||||
pre code .bp { color: var(--code-text) } /* Name.Builtin.Pseudo */
|
||||
pre code .fm { color: var(--code-function); font-weight: bold } /* Name.Function.Magic */
|
||||
pre code .vc { color: var(--code-builtin) } /* Name.Variable.Class */
|
||||
pre code .vg { color: var(--code-builtin) } /* Name.Variable.Global */
|
||||
pre code .vi { color: var(--code-builtin) } /* Name.Variable.Instance */
|
||||
pre code .vm { color: var(--code-builtin) } /* Name.Variable.Magic */
|
||||
pre code .il { color: var(--code-string) } /* Literal.Number.Integer.Long */
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/* Corporate theme: bright and bold professional. Saturated royal-blue
|
||||
gradients on white, geometric Montserrat display type over Inter body,
|
||||
and a genuinely large brand with a soft blue overlap shadow. Automatic
|
||||
dark mode keeps the same saturated blue identity on deep navy; the
|
||||
banner artwork (inlined by the backend) is recolored from here via the
|
||||
cb-* classes, so one SVG serves both modes. */
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #ffffff;
|
||||
--surface: #eef3fd;
|
||||
--text: #12203f;
|
||||
--muted: #4d5f83;
|
||||
--accent: #0a5cff;
|
||||
--accent2: #0933a0;
|
||||
--line: #12203f14;
|
||||
--font-body: var(--font-inter);
|
||||
--font-heading: var(--font-montserrat);
|
||||
}
|
||||
|
||||
/* Banner artwork colors, light mode */
|
||||
.cb-bg0 {
|
||||
stop-color: #ffffff;
|
||||
}
|
||||
|
||||
.cb-bg1 {
|
||||
stop-color: #e6eefe;
|
||||
}
|
||||
|
||||
.cb-r0 {
|
||||
stop-color: var(--accent);
|
||||
}
|
||||
|
||||
.cb-r1 {
|
||||
stop-color: #00b3ff;
|
||||
}
|
||||
|
||||
.cb-g0,
|
||||
.cb-g1 {
|
||||
stop-color: var(--accent);
|
||||
}
|
||||
|
||||
.cb-dot {
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.cb-orbit {
|
||||
stroke: var(--accent);
|
||||
}
|
||||
|
||||
.cb-spark {
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0b1428;
|
||||
--surface: #142446;
|
||||
--text: #e9eefe;
|
||||
--muted: #93a7d0;
|
||||
--accent: #4d8dff;
|
||||
--accent2: #8ab6ff;
|
||||
--line: #ffffff17;
|
||||
/* Code wells stay navy in dark mode (light mode uses --surface). */
|
||||
--code-bg: #0d1b3e;
|
||||
}
|
||||
|
||||
/* Banner artwork colors, dark mode */
|
||||
.cb-bg0 {
|
||||
stop-color: #0d1830;
|
||||
}
|
||||
|
||||
.cb-bg1 {
|
||||
stop-color: #0a1122;
|
||||
}
|
||||
|
||||
.cb-r0 {
|
||||
stop-color: #2f7bff;
|
||||
}
|
||||
|
||||
.cb-r1 {
|
||||
stop-color: #00d0ff;
|
||||
}
|
||||
|
||||
.cb-g0,
|
||||
.cb-g1 {
|
||||
stop-color: #2f7bff;
|
||||
}
|
||||
|
||||
.cb-dot {
|
||||
fill: #4d8dff;
|
||||
}
|
||||
|
||||
.cb-orbit {
|
||||
stroke: #4d8dff;
|
||||
}
|
||||
|
||||
.cb-spark {
|
||||
fill: #6ea8ff;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Genuinely large solid brand with a soft blue shadow overlapping the
|
||||
artwork — conservative, but unmissable. */
|
||||
#brand {
|
||||
font-size: clamp(4rem, 11vw, 8.5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
color: var(--accent2);
|
||||
filter: drop-shadow(0 0.4rem 1.4rem rgb(10 92 255 / 0.3));
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#brand {
|
||||
color: #eaf1ff;
|
||||
filter: drop-shadow(0 0.4rem 1.4rem rgb(0 0 0 / 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
#banner {
|
||||
min-height: 15rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#nav {
|
||||
font-size: 1.05em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
#nav .current {
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, var(--accent), var(--accent2));
|
||||
border-radius: 999px;
|
||||
padding: 0.15rem 0.85rem;
|
||||
margin: -0.15rem -0.85rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#nav {
|
||||
text-shadow: 0 0 0.15em #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading hierarchy: h1 navy with a short gradient bar, h2 in accent
|
||||
blue, h3 as an uppercase kicker. */
|
||||
article h1 {
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
article h1 {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
article h1::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3.6rem;
|
||||
height: 0.32rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--accent), #00b3ff);
|
||||
}
|
||||
|
||||
article h2 {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
article h3 {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: var(--accent);
|
||||
background: color-mix(in oklab, var(--accent) 6%, transparent);
|
||||
padding: 0.4rem 0.9rem;
|
||||
/* Keep the quoted text on the paragraph edge: the tinted box extends
|
||||
past it by its own border/padding, like code blocks. */
|
||||
margin: 0 -0.9rem 1rem calc(-0.25rem - 0.9rem);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Code follows the color scheme: the light set on a faint-blue surface in
|
||||
light mode, a navy well in dark mode (--code-bg above). The accent side
|
||||
bar stays in both. */
|
||||
pre {
|
||||
border: 1px solid transparent;
|
||||
border-left: 0.25rem solid var(--accent);
|
||||
/* Text on the paragraph edge: the box extends by padding + border. */
|
||||
margin-left: calc(-0.8rem - 0.25rem);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::view-transition {
|
||||
background: var(--bg);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/* Nitro theme: racing/HUD style — saturated orange + cyan, console/HUD-style
|
||||
monospace display type, an orange racing-tab nav with a bezier clip-path,
|
||||
and a dark bezier banner. Follows prefers-color-scheme: the light scheme
|
||||
is a warm light-grey page, the dark scheme a deep violet page; the banner
|
||||
and other dark/bright elements carry over unchanged.
|
||||
|
||||
Color model: the base stylesheet's variables carry the page; nitro adds
|
||||
a small set of theme-specific colors below, and everything else in the
|
||||
file references variables only, so recoloring happens here. */
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
/* Page palette (base variables) */
|
||||
--bg: #eee;
|
||||
/* warm light grey page background */
|
||||
--surface: #fff;
|
||||
--text: #1a1a1a;
|
||||
--muted: #655f52;
|
||||
/* warm grey secondary text */
|
||||
--line: #1a1a1a1f;
|
||||
/* hairlines: tables, column rules */
|
||||
--accent: #ff6a00;
|
||||
/* racing orange: decorations, nav tab, markers */
|
||||
--accent2: #008ba3;
|
||||
/* deep cyan: h3, blockquote bar, nested markers */
|
||||
|
||||
/* Nitro-specific colors */
|
||||
--orange-deep: #d95a00;
|
||||
/* orange dark enough for body text (h2, links) */
|
||||
--ink: #000;
|
||||
/* pure black details: tab text, brand shadow */
|
||||
--neon: #59ecff;
|
||||
/* glowing cyan: current page on the orange tab */
|
||||
--tab-current: #eee;
|
||||
--tab-hover: #fff;
|
||||
/* glowing white: nav hover (kept bright: the
|
||||
nav glow is text-shadow, which fades dimmer) */
|
||||
--tab-muted: #0000008c;
|
||||
/* secondary labels on the orange tab */
|
||||
|
||||
--font-body: var(--font-montserrat);
|
||||
--font-heading: var(--font-literata);
|
||||
}
|
||||
|
||||
/* Dark scheme: same identity, but the page goes deep violet (never muddy
|
||||
near-black); the cyan and orange accents brighten to keep their punch. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #17141f;
|
||||
--surface: #231f2e;
|
||||
--text: #f0ede8;
|
||||
--muted: #a89f8f;
|
||||
--line: #ffffff22;
|
||||
--accent2: #2fc3dd;
|
||||
/* brighter cyan, readable on dark */
|
||||
--orange-deep: #ff8a3d;
|
||||
/* brighter orange, readable on dark */
|
||||
--code-bg: #12101b;
|
||||
/* code wells join the violet family */
|
||||
}
|
||||
|
||||
/* Banner dark tones tinted to the same violet family as the page. */
|
||||
.nb-base {
|
||||
fill: #100d18;
|
||||
}
|
||||
|
||||
.nb-s1a {
|
||||
stop-color: #292536;
|
||||
}
|
||||
|
||||
.nb-s1b {
|
||||
stop-color: #100d18;
|
||||
}
|
||||
|
||||
.nb-s2a {
|
||||
stop-color: #1e1a2b;
|
||||
}
|
||||
|
||||
.nb-s2b {
|
||||
stop-color: #090811;
|
||||
}
|
||||
|
||||
.nb-c0 {
|
||||
stop-color: #322d44;
|
||||
}
|
||||
|
||||
.nb-c1 {
|
||||
stop-color: #171422;
|
||||
}
|
||||
|
||||
.nb-c2 {
|
||||
stop-color: #100d18;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* Oversized outlined brand, spilling off the banner edge: orange stroke,
|
||||
solid black fill. */
|
||||
#brand {
|
||||
font-size: 10rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
color: var(--orange-deep);
|
||||
text-shadow: 0 0 0.1em black;
|
||||
}
|
||||
|
||||
/* Bezier-swept banner with wide orange stripes (inlined SVG), separated
|
||||
from the page by a straight orange blade. */
|
||||
#banner {
|
||||
height: 13rem;
|
||||
border-bottom: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
/* Banner artwork dark tones: neutral greys in light mode (retinted to the
|
||||
page's violet family by the dark-scheme block above). */
|
||||
.nb-base {
|
||||
fill: #0b0b0d;
|
||||
}
|
||||
|
||||
.nb-s1a {
|
||||
stop-color: #242428;
|
||||
}
|
||||
|
||||
.nb-s1b {
|
||||
stop-color: #0b0b0d;
|
||||
}
|
||||
|
||||
.nb-s2a {
|
||||
stop-color: #19191d;
|
||||
}
|
||||
|
||||
.nb-s2b {
|
||||
stop-color: #060607;
|
||||
}
|
||||
|
||||
.nb-c0 {
|
||||
stop-color: #2a2a2f;
|
||||
}
|
||||
|
||||
.nb-c1 {
|
||||
stop-color: #131315;
|
||||
}
|
||||
|
||||
.nb-c2 {
|
||||
stop-color: #0b0b0d;
|
||||
}
|
||||
|
||||
#nav {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.95em;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
/* Racing tab: sitting at the bottom of the banner, with a bezier clip —
|
||||
slanted sides that join top and bottom horizontally, and a shallow
|
||||
wide notch at the top middle. Generous side padding so the em-based
|
||||
slants fit. The border-radius is the fallback for browsers without
|
||||
shape(). */
|
||||
background: var(--accent);
|
||||
width: fit-content;
|
||||
padding: 0.3em 2.5em 0;
|
||||
border-radius: 1.2em 1.2em 0 0;
|
||||
clip-path: shape(
|
||||
/* left side */
|
||||
from 0% 100%,
|
||||
curve to 2.5em 0 with 2em 100% / 1em 0,
|
||||
/* notch: ≈ golden-ratio width of the available top edge */
|
||||
hline to calc(19.1% + 1.7em),
|
||||
line to calc(19.1% + 2.1em) 0.2em,
|
||||
hline to calc(80.9% - 2.1em),
|
||||
line to calc(80.9% - 1.7em) 0,
|
||||
/* right side, bottom */
|
||||
hline to calc(100% - 2.5em),
|
||||
curve to 100% 100% with calc(100% - 1em) 0 / calc(100% - 2em) 100%,
|
||||
close);
|
||||
}
|
||||
|
||||
#nav a {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* Current page and hover glow on the orange tab; both stay bright colors
|
||||
because the glow is text-shadow. */
|
||||
#nav .current {
|
||||
color: var(--tab-current);
|
||||
text-shadow: 0 0 0.1em;
|
||||
}
|
||||
|
||||
#nav a:hover {
|
||||
color: var(--tab-hover);
|
||||
text-shadow: 0 0 0.1em;
|
||||
}
|
||||
|
||||
#nav span {
|
||||
color: var(--tab-muted);
|
||||
}
|
||||
|
||||
/* Console-style headings: uppercase monospace. h1 in the page text color
|
||||
with a hazard-stripe underline, h2 deep orange, h3 cyan. */
|
||||
article h1,
|
||||
article h2,
|
||||
article h3 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
padding-bottom: 0.5rem;
|
||||
/* The hazard-stripe underline breaks out of the page box: the negative
|
||||
right margin extends the h1's box (and thus its background) all the
|
||||
way to the viewport's right edge. */
|
||||
margin-right: calc((100% - 100vw) / 2);
|
||||
background:
|
||||
linear-gradient(-55deg,
|
||||
transparent 0 0.2rem,
|
||||
var(--accent) 0.2rem 0.9rem,
|
||||
transparent 0.9rem 1.4rem) -0.2rem bottom / 1.4rem 4px repeat-x;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
color: var(--orange-deep);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
color: var(--accent2);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article a:hover {
|
||||
color: var(--accent2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Chevron markers instead of the base emoji diamonds. */
|
||||
article ul li::before {
|
||||
content: "»";
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article ul ul li::before {
|
||||
content: "›";
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
article ul ul ul li::before {
|
||||
content: "»";
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: var(--accent2);
|
||||
background: color-mix(in oklab, var(--accent2) 6%, transparent);
|
||||
padding: 0.25rem 0.75rem;
|
||||
/* Keep the quoted text on the paragraph edge: the tinted box extends
|
||||
past it by its own border/padding, like code blocks. */
|
||||
margin: 0 -0.75rem 1rem -1rem;
|
||||
}
|
||||
|
||||
/* Code follows the color scheme; the dark-scheme well joins the violet
|
||||
family (--code-bg above). The orange side bar stays in both. */
|
||||
pre {
|
||||
border-left: 0.25rem solid var(--accent);
|
||||
/* Text on the paragraph edge: the box extends by padding + border. */
|
||||
margin-left: calc(-0.8rem - 0.25rem);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::view-transition {
|
||||
background: var(--bg);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 360">
|
||||
<rect width="1600" height="360" fill="#1b1830"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 122 B |
@@ -1,38 +1,74 @@
|
||||
/* Purple theme: overrides the conservative base variables and adds the
|
||||
more elaborate visual styling (dark palette, gradient brand, etc.). */
|
||||
/* Purple theme: bold dusk palette (sky/violet/pink on deep indigo),
|
||||
editorial typography (Fraunces display over Literata body), the sunrise
|
||||
banner artwork (inlined by the backend into #page-banner) and a
|
||||
playfully oversized tilted brand. */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #12101c;
|
||||
--surface: #1b1830;
|
||||
--text: #e8e6f2;
|
||||
--muted: #9a94b8;
|
||||
--accent: #00d4c8;
|
||||
--accent2: #7c5cff;
|
||||
--line: #ffffff1a;
|
||||
--bg: #131022;
|
||||
--surface: #1e1a36;
|
||||
--text: #ece9f7;
|
||||
--muted: #a79ecb;
|
||||
/* Icy sky-cyan: the cool counterweight to violet/pink (green-leaning
|
||||
teal clashed with them), and complementary to the sunrise's sun. */
|
||||
--accent: #5ad1f5;
|
||||
--accent2: #9b6bff;
|
||||
--accent3: #ff6b9d;
|
||||
--line: #ffffff1c;
|
||||
--font-body: var(--font-literata);
|
||||
--font-heading: var(--font-fraunces);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Oversized tilted brand in the sky→violet gradient. */
|
||||
#brand {
|
||||
font-size: clamp(3.2rem, 9vw, 7.5rem);
|
||||
line-height: 1;
|
||||
margin-bottom: -0.28em;
|
||||
transform: rotate(-2deg);
|
||||
transform-origin: left bottom;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
filter: drop-shadow(0 0 0.1rem #000);
|
||||
filter: drop-shadow(0 0.15rem 0.6rem #9b6bff55);
|
||||
}
|
||||
|
||||
/* Sunrise parallax: the sun and its glow rise faster than the artwork
|
||||
drift (pagerite.js sets --pry on <html>), so scrolling the page makes
|
||||
the sun come up. */
|
||||
#page-banner .sun,
|
||||
#page-banner .sun-glow {
|
||||
transform-box: fill-box;
|
||||
transform: translateY(calc(var(--pry, 0px) * -2));
|
||||
}
|
||||
|
||||
/* The banner artwork fades into the page background at its bottom edge
|
||||
(baked into the SVG, so a user banner replaces it cleanly). */
|
||||
.banner-fade {
|
||||
stop-color: var(--bg);
|
||||
}
|
||||
|
||||
#banner {
|
||||
background: url("./banner.svg") center 40% / cover;
|
||||
min-height: 13rem;
|
||||
}
|
||||
|
||||
/* Colored headings are part of the theme's more elaborate styling; the
|
||||
base keeps headings in plain text color. */
|
||||
/* Dark artwork: keep the nav readable with a shadow. */
|
||||
#nav {
|
||||
text-shadow: 0 0 0.15em black;
|
||||
}
|
||||
|
||||
/* Colored headings, one accent per level: teal h1 with a gradient
|
||||
underline, violet h2, pink h3 in a quieter weight. */
|
||||
article h1 {
|
||||
color: var(--accent);
|
||||
padding-bottom: 0.35rem;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2) 45%, transparent) bottom left / 100% 2px no-repeat;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
@@ -40,17 +76,37 @@ article h2 {
|
||||
}
|
||||
|
||||
article h3 {
|
||||
color: color-mix(in oklab, var(--accent2) 60%, var(--muted));
|
||||
color: var(--accent3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* Theme-colored diamond markers instead of the base emoji (blue/orange
|
||||
clashes with this palette). */
|
||||
article ul li::before {
|
||||
content: "◆";
|
||||
color: var(--accent);
|
||||
font-size: 0.7em;
|
||||
vertical-align: 0.15em;
|
||||
}
|
||||
|
||||
article ul ul li::before {
|
||||
content: "◆";
|
||||
color: var(--accent2);
|
||||
}
|
||||
|
||||
article ul ul ul li::before {
|
||||
content: "◆";
|
||||
color: var(--accent3);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left-color: var(--accent2);
|
||||
}
|
||||
|
||||
/* Code panels sit slightly lighter than the page; the token colors come
|
||||
from the base dark set (this theme declares color-scheme: dark). */
|
||||
pre {
|
||||
background: #ffffff09;
|
||||
--code-bg: var(--surface);
|
||||
}
|
||||
|
||||
::view-transition {
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
const REGIONS = ["page-banner", "nav", "sidebar", "main"];
|
||||
// Regions every page has. #sidebar is NOT among them: it is omitted
|
||||
// entirely when the section has no sub-navigation, and handled below.
|
||||
const REGIONS = ["page-banner", "nav", "main"];
|
||||
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
let editorModule = null;
|
||||
|
||||
@@ -235,6 +237,27 @@
|
||||
currentPath = location.pathname;
|
||||
});
|
||||
|
||||
// --- Banner parallax ----------------------------------------------------
|
||||
// The banner artwork stays windowed in place while its contents drift
|
||||
// against the scroll. The --pry scroll parameter is also available to
|
||||
// themes for their own effects (e.g. the purple sun rising faster than
|
||||
// the drift). Event-driven only: perfectly still when the page is idle.
|
||||
if (!reduceMotion.matches) {
|
||||
let ticking = false;
|
||||
const drift = () => {
|
||||
ticking = false;
|
||||
document.documentElement.style.setProperty(
|
||||
"--pry", `${Math.min(scrollY * 0.1, 30)}px`,
|
||||
);
|
||||
};
|
||||
addEventListener("scroll", () => {
|
||||
if (!ticking) {
|
||||
ticking = true;
|
||||
requestAnimationFrame(drift);
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
// --- Fetch navigation ------------------------------------------------
|
||||
async function load(url, push = true, back = false) {
|
||||
// Navigating with the editor open closes it; unsaved edits are lost
|
||||
@@ -264,6 +287,18 @@
|
||||
const el = document.getElementById(id);
|
||||
el.replaceWith(document.importNode(doc.getElementById(id), true));
|
||||
}
|
||||
// #sidebar is omitted entirely when the section has no
|
||||
// sub-navigation, so it may be absent on either side of the swap:
|
||||
// replace, insert (as #main's preceding sibling), or remove.
|
||||
const oldSidebar = document.getElementById("sidebar");
|
||||
const newSidebar = doc.getElementById("sidebar");
|
||||
if (oldSidebar && newSidebar) {
|
||||
oldSidebar.replaceWith(document.importNode(newSidebar, true));
|
||||
} else if (newSidebar) {
|
||||
document.getElementById("main").before(document.importNode(newSidebar, true));
|
||||
} else if (oldSidebar) {
|
||||
oldSidebar.remove();
|
||||
}
|
||||
// Site-wide custom CSS lives in <head id="pagerite-user"> and must be
|
||||
// kept in sync across fetch-navigations. It is kept last in <head>:
|
||||
// in dev Vite injects the base stylesheet after the server-rendered
|
||||
|
||||
+11
-1
@@ -1,4 +1,5 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import fastapiVue from './vite-plugin-fastapi.js'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -7,6 +8,15 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
const backendUrl = process.env.PAGERITE_BACKEND_URL || 'http://localhost:3200'
|
||||
|
||||
// Every theme directory ships its theme.css as a separate build entry, so
|
||||
// the backend can link base and theme stylesheets independently.
|
||||
const themesDir = fileURLToPath(new URL('./src/assets/themes', import.meta.url))
|
||||
const themeInputs = Object.fromEntries(
|
||||
readdirSync(themesDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => [`theme_${d.name}`, `${themesDir}/${d.name}/theme.css`]),
|
||||
)
|
||||
|
||||
// Proxy content pages (/slug, /path/to/slug) to the FastAPI backend in dev.
|
||||
// Excludes Vite internals (/@..., /src, /node_modules, /__...) and the
|
||||
// backend's /_ prefix. /_api and /_f are handled by the fastapi-vue plugin.
|
||||
@@ -40,7 +50,7 @@ export default defineConfig({
|
||||
main: fileURLToPath(new URL('./src/main.js', import.meta.url)),
|
||||
pagerite: fileURLToPath(new URL('./src/pagerite.js', import.meta.url)),
|
||||
pagerite_base: fileURLToPath(new URL('./src/assets/pagerite.css', import.meta.url)),
|
||||
pagerite_theme: fileURLToPath(new URL('./src/assets/themes/purple/theme.css', import.meta.url)),
|
||||
...themeInputs,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+2
-1
@@ -80,7 +80,8 @@ class Data(msgspec.Struct):
|
||||
#: 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.
|
||||
#: frontend/src/assets/themes/{theme}/theme.css, with their banner
|
||||
#: artwork at pagerite/themes/{theme}/banner.svg (inlined server-side).
|
||||
theme: str = "purple"
|
||||
#: Raw site-wide custom CSS, injected inline in every page <head>.
|
||||
#: Trusted author content; not sanitized.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 360" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<linearGradient id="cbg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" class="cb-bg0"/>
|
||||
<stop offset="1" class="cb-bg1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cribbon" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" class="cb-r0"/>
|
||||
<stop offset="1" class="cb-r1"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="cglow" cx="0.78" cy="0.25" r="0.55">
|
||||
<stop offset="0" class="cb-g0" stop-opacity="0.16"/>
|
||||
<stop offset="1" class="cb-g1" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<pattern id="cdots" width="26" height="26" patternUnits="userSpaceOnUse">
|
||||
<circle cx="4" cy="4" r="3" class="cb-dot"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="1600" height="360" fill="url(#cbg)"/>
|
||||
<rect width="1600" height="360" fill="url(#cglow)"/>
|
||||
<!-- Dot-matrix patch, fading towards the middle -->
|
||||
<rect x="40" y="30" width="520" height="150" fill="url(#cdots)" opacity="0.14"/>
|
||||
<rect x="40" y="200" width="340" height="104" fill="url(#cdots)" opacity="0.08"/>
|
||||
<!-- Diagonal ribbons slashing across the right side -->
|
||||
<g transform="rotate(-18 1250 180)">
|
||||
<rect x="1060" y="-140" width="130" height="640" rx="8" fill="url(#cribbon)"/>
|
||||
<rect x="1225" y="-140" width="36" height="640" rx="8" fill="url(#cribbon)" opacity="0.55"/>
|
||||
<rect x="1295" y="-140" width="230" height="640" rx="8" fill="url(#cribbon)" opacity="0.14"/>
|
||||
</g>
|
||||
<!-- Large orbit circles, bottom right -->
|
||||
<g fill="none" class="cb-orbit">
|
||||
<circle cx="1410" cy="310" r="150" stroke-width="2" opacity="0.22"/>
|
||||
<circle cx="1310" cy="335" r="85" stroke-width="2" opacity="0.14"/>
|
||||
</g>
|
||||
<!-- A few accent sparks -->
|
||||
<g class="cb-spark">
|
||||
<circle cx="620" cy="70" r="5" opacity="0.5"/>
|
||||
<circle cx="700" cy="120" r="3.5" opacity="0.35"/>
|
||||
<circle cx="840" cy="55" r="4" opacity="0.4"/>
|
||||
<circle cx="950" cy="290" r="5" opacity="0.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 360" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<linearGradient id="flare" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#ff6a00"/>
|
||||
<stop offset="0.55" stop-color="#ff9e00"/>
|
||||
<stop offset="1" stop-color="#ffd23f"/>
|
||||
</linearGradient>
|
||||
<!-- Layered bezier sweeps shade the panel: no flat fills, no plain
|
||||
one-shot gradients. The dark tones are the nb-* classes so the
|
||||
theme can tint them per color scheme (neutral in light mode,
|
||||
violet to match the dark page in dark mode). -->
|
||||
<linearGradient id="sweep1" x1="0" y1="0" x2="0.7" y2="1">
|
||||
<stop offset="0" stop-color="#242428" class="nb-s1a"/>
|
||||
<stop offset="1" stop-color="#0b0b0d" class="nb-s1b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sweep2" x1="1" y1="0" x2="0.3" y2="1">
|
||||
<stop offset="0" stop-color="#19191d" class="nb-s2a"/>
|
||||
<stop offset="1" stop-color="#060607" class="nb-s2b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cowl" x1="0" y1="0" x2="0.8" y2="1">
|
||||
<stop offset="0" stop-color="#2a2a2f" class="nb-c0"/>
|
||||
<stop offset="0.6" stop-color="#131315" class="nb-c1"/>
|
||||
<stop offset="1" stop-color="#0b0b0d" stop-opacity="0" class="nb-c2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="1600" height="360" fill="#0b0b0d" class="nb-base"/>
|
||||
|
||||
<!-- Sweeping fairing curves shading the panel -->
|
||||
<path d="M 0 360 C 350 240, 700 290, 1050 200 C 1250 150, 1450 170, 1600 110 L 1600 360 L 0 360 Z" fill="url(#sweep1)"/>
|
||||
<path d="M 0 330 C 350 225, 700 272, 1050 188 C 1250 145, 1450 162, 1600 104" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="3"/>
|
||||
<path d="M 0 360 C 480 295, 950 345, 1600 250 L 1600 360 Z" fill="url(#sweep2)"/>
|
||||
<path d="M 0 352 C 480 288, 950 336, 1600 242" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="2"/>
|
||||
|
||||
<!-- The wide orange stripes -->
|
||||
<g transform="skewX(-24)">
|
||||
<rect x="880" y="-40" width="96" height="440" fill="url(#flare)"/>
|
||||
<rect x="1002" y="-40" width="26" height="440" fill="#ff6a00"/>
|
||||
<rect x="1050" y="-40" width="10" height="440" fill="#ffd23f"/>
|
||||
</g>
|
||||
|
||||
<!-- Top-right cowl: a glossy bezier brow echoing the console language -->
|
||||
<path d="M 950 -5 C 1150 80, 1380 130, 1605 190 L 1605 -5 Z" fill="url(#cowl)"/>
|
||||
<path d="M 950 -5 C 1150 80, 1380 130, 1605 190" fill="none" stroke="#ffffff" stroke-opacity="0.08" stroke-width="3"/>
|
||||
<path d="M 950 -5 C 1150 80, 1380 130, 1605 190" fill="none" stroke="#ff6a00" stroke-opacity="0.3" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,38 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 300" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#2b1b4d"/>
|
||||
<stop offset="1" stop-color="#ff8a5c"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="sunglow" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0" stop-color="#ffd9a0" stop-opacity="0.9"/>
|
||||
<stop offset="0.35" stop-color="#ffb37a" stop-opacity="0.55"/>
|
||||
<stop offset="1" stop-color="#ff8a5c" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#131022" stop-opacity="0"/>
|
||||
<stop offset="1" class="banner-fade"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="300" fill="url(#sky)"/>
|
||||
<!-- Stars in the upper dusk sky -->
|
||||
<g fill="#ffffff">
|
||||
<circle cx="120" cy="38" r="1.6" opacity="0.8"/>
|
||||
<circle cx="240" cy="70" r="1.1" opacity="0.5"/>
|
||||
<circle cx="365" cy="30" r="1.4" opacity="0.7"/>
|
||||
<circle cx="520" cy="58" r="1" opacity="0.45"/>
|
||||
<circle cx="660" cy="26" r="1.5" opacity="0.75"/>
|
||||
<circle cx="790" cy="66" r="1.1" opacity="0.5"/>
|
||||
<circle cx="930" cy="34" r="1.6" opacity="0.8"/>
|
||||
<circle cx="1060" cy="72" r="1" opacity="0.45"/>
|
||||
<circle cx="1140" cy="28" r="1.3" opacity="0.6"/>
|
||||
</g>
|
||||
<!-- Sun low on the horizon with a wide glow (rises on scroll) -->
|
||||
<circle class="sun-glow" cx="600" cy="212" r="120" fill="url(#sunglow)"/>
|
||||
<circle class="sun" cx="600" cy="212" r="42" fill="#ffd9a0"/>
|
||||
<path d="M0 210 Q 300 150 600 210 T 1200 210 V300 H0 Z" fill="#3d2b6b"/>
|
||||
<path d="M0 250 Q 300 200 600 250 T 1200 250 V300 H0 Z" fill="#241842"/>
|
||||
<!-- Gentle fade into the page background behind the nav; part of the
|
||||
theme artwork, so a user banner replaces it entirely. -->
|
||||
<rect y="190" width="1200" height="110" fill="url(#fade)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
+51
-23
@@ -7,8 +7,9 @@ can swap them without reloading the page chrome.
|
||||
|
||||
Navigation walks the Node tree directly (see data.py): nav_html lists the
|
||||
top level — the front page (slug "") is an ordinary top-level item, not
|
||||
the parent of the others — and sidebar_html the children of the current
|
||||
top-level section. Nodes without content are category labels; nav links
|
||||
the parent of the others — and sidebar_html the sub-navigation of the
|
||||
current top-level section, rendered only when the section offers at
|
||||
least two published items. Nodes without content are category labels; nav links
|
||||
to them point straight at their first child page (first_leaf), and their
|
||||
own URL renders a placeholder page (render_category).
|
||||
"""
|
||||
@@ -54,11 +55,13 @@ def _shared_css_urls(vite_url: str | None, theme: str) -> list[str]:
|
||||
|
||||
In dev the JS entries import these files, so Vite injects them; the
|
||||
backend does not link them, avoiding the HMR-wrapped module output.
|
||||
Themes added after the last frontend build are missing from the
|
||||
manifest — fall back to the base stylesheet rather than failing.
|
||||
"""
|
||||
if vite_url:
|
||||
return []
|
||||
manifest = _manifest()
|
||||
return [f"/{manifest[key]['file']}" for key in _css_keys(theme)]
|
||||
return [f"/{manifest[key]['file']}" for key in _css_keys(theme) if key in manifest]
|
||||
|
||||
|
||||
def _editor_css_url(vite_url: str | None, theme: str) -> str | None:
|
||||
@@ -118,7 +121,7 @@ def _layout(
|
||||
id="banner",
|
||||
)
|
||||
.div(
|
||||
E.aside(E.Sidebar, id="sidebar"),
|
||||
E.Sidebar,
|
||||
E.main(E.Main, id="main"),
|
||||
id="content",
|
||||
)
|
||||
@@ -168,10 +171,13 @@ def nav_html(menu: dict[str, Node], current: str) -> HTML:
|
||||
|
||||
|
||||
def sidebar_html(menu: dict[str, Node], current: str) -> HTML:
|
||||
"""Render the contents of the #sidebar element for the current path.
|
||||
"""Render the #sidebar element for the current path (empty when none).
|
||||
|
||||
Lists the direct children of the current main level section; empty when
|
||||
the path is not inside a section or the section has no children.
|
||||
The sidebar is the current main level section's sub-navigation, so it
|
||||
exists only when there is something to navigate: the section must
|
||||
offer at least two published items. The front page, leaf pages and
|
||||
one-page sections get no aside element at all (rather than an empty
|
||||
or one-item box).
|
||||
"""
|
||||
if not current:
|
||||
return HTML("")
|
||||
@@ -179,12 +185,14 @@ def sidebar_html(menu: dict[str, Node], current: str) -> HTML:
|
||||
node = menu.get(section)
|
||||
if node is None:
|
||||
return HTML("")
|
||||
items = [(s, c) for s, c in sorted_nodes(node.children) if c.published]
|
||||
if len(items) < 2:
|
||||
return HTML("")
|
||||
nav = E.ul
|
||||
with nav:
|
||||
for slug, child in sorted_nodes(node.children):
|
||||
if child.published:
|
||||
_nav_link(nav, menu, child, f"{section}/{slug}", current)
|
||||
return HTML(str(nav))
|
||||
for slug, child in items:
|
||||
_nav_link(nav, menu, child, f"{section}/{slug}", current)
|
||||
return HTML(str(E.aside(nav, id="sidebar")))
|
||||
|
||||
|
||||
def first_leaf(menu: dict[str, Node], path: str) -> str | None:
|
||||
@@ -208,18 +216,36 @@ def _first_leaf(node: Node, path: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def banner_html(menu: dict[str, Node], path: str) -> HTML:
|
||||
def banner_html(menu: dict[str, Node], path: str, theme: str = "") -> HTML:
|
||||
"""Resolve the banner for a path: the nearest node on the ancestor
|
||||
chain (the node itself first), then the front page, then the default
|
||||
CSS artwork. The front page is a top-level *sibling* of the other
|
||||
chain (the node itself first), then the front page, then the theme
|
||||
artwork. The front page is a top-level *sibling* of the other
|
||||
main-level nodes, not their parent, so it never appears in the chain
|
||||
and is consulted explicitly, last. The snippet is raw trusted HTML,
|
||||
so a banner can be anything — an img, a styled div, canvas + script.
|
||||
|
||||
With no user banner anywhere in the chain, the active theme's inline
|
||||
SVG artwork is inlined instead: as markup it can be recolored from the
|
||||
theme stylesheet (``var(--accent)`` etc.) and animated, and it is not
|
||||
rendered at all when the user supplies their own banner.
|
||||
"""
|
||||
source = banner_source(menu, path)
|
||||
if source is None:
|
||||
if source is not None:
|
||||
return HTML(resolve(menu, source)[-1].banner)
|
||||
return _theme_banner(theme)
|
||||
|
||||
|
||||
_banner_cache: dict[str, HTML] = {}
|
||||
|
||||
|
||||
def _theme_banner(theme: str) -> HTML:
|
||||
"""The theme's inline banner SVG (empty for none/unknown themes)."""
|
||||
if not theme or "/" in theme:
|
||||
return HTML("")
|
||||
return HTML(resolve(menu, source)[-1].banner)
|
||||
if theme not in _banner_cache:
|
||||
path = Path(__file__).parent / "themes" / theme / "banner.svg"
|
||||
_banner_cache[theme] = HTML(path.read_text()) if path.exists() else HTML("")
|
||||
return _banner_cache[theme]
|
||||
|
||||
|
||||
def banner_source(menu: dict[str, Node], path: str) -> str | None:
|
||||
@@ -267,7 +293,7 @@ def render_page(
|
||||
Brand=_brand_link(brand),
|
||||
Nav=nav_html(menu, path),
|
||||
Sidebar=sidebar_html(menu, path),
|
||||
Banner=banner_html(menu, path),
|
||||
Banner=banner_html(menu, path, theme),
|
||||
Main=page_content(menu, path),
|
||||
),
|
||||
)
|
||||
@@ -288,20 +314,22 @@ def render_category(
|
||||
"""
|
||||
node = resolve(menu, path)[-1]
|
||||
title = _title(path.rpartition("/")[2], node)
|
||||
sidebar = sidebar_html(menu, path)
|
||||
doc = E.article
|
||||
with doc:
|
||||
doc.h1(title)
|
||||
doc.p(
|
||||
"Pages in this section are listed in the menu on the left."
|
||||
)
|
||||
if sidebar:
|
||||
doc.p("Pages in this section are listed in the menu on the left.")
|
||||
else:
|
||||
doc.p("This section has no page of its own yet.")
|
||||
scripts, styles = _page_assets(theme)
|
||||
return str(
|
||||
_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),
|
||||
Sidebar=sidebar,
|
||||
Banner=banner_html(menu, path, theme),
|
||||
Main=HTML(str(doc)),
|
||||
),
|
||||
)
|
||||
@@ -326,7 +354,7 @@ def render_not_found(
|
||||
Brand=_brand_link(brand),
|
||||
Nav=nav_html(menu, path),
|
||||
Sidebar=sidebar_html(menu, path),
|
||||
Banner=banner_html(menu, path),
|
||||
Banner=banner_html(menu, path, theme),
|
||||
Main=HTML(str(doc)),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user