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:
+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