diff --git a/CLAUDE.md b/CLAUDE.md index 33ba735..3d90c7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,8 @@ see `layouts/partials/console.html`. ## Layout - `layouts/` — templates (`_default/`, `partials/`). `baseof.html` is the shell. -- `assets/css/main.css` — the whole stylesheet. One `--accent` var tints hovers. +- `assets/css/main.css` — the core stylesheet (base light/dark, layout, sets). + Colour themes live in `assets/css/themes/*.css`, concatenated in `baseof.html`. - `assets/js/console.js` — the floating terminal (below). - `assets/figlet/heading.flf` — figfont for the console banner. @@ -52,3 +53,63 @@ Subcommands: - Add every new command to the `help` list (aligned `name : description`). - Add an `assert` to the self-check block for each new branch, then run `node assets/js/console.js`. + +## Themes + +Themes are pure CSS: each named theme is its own file under +`assets/css/themes/*.css` holding one `:root[data-theme=""]` block. +`baseof.html` concatenates `main.css` + every theme file (themes last) into one +fingerprinted bundle. Picked from the console (`theme set `, `theme +list`) — the name list lives in `console.js`'s `theme` command `modes` array, +so add there too. `light`/`dark`/`auto` are the base modes (defined in +`main.css`); everything else is a colour-theme file. Persistence + no-flash +apply happens in `baseof.html`'s pre-paint script. + +### Colourable surfaces + +Every colour flows from CSS vars, so a theme controls all of it: + +- **Core**: `--bg`, `--fg`, `--muted`, `--border`. +- **Selection highlight**: `--selection` (the `::selection` background). +- **Inline code**: `--code`. +- **Syntax highlighting**: the pop palette below drives Chroma token classes + (`.chroma .k` keywords, `.s` strings, `.m` numbers, `.nf` functions, etc). + Requires `markup.highlight.noClasses = false` in `hugo.toml`. +- **Mermaid diagrams**: `baseof.html` reads the pop palette + core vars at load + and feeds them to `mermaid.initialize({ theme: "base", themeVariables })`. + Diagrams are tinted at page load — reload after switching themes. +- **Pop palette**: `--c-red --c-orange --c-yellow --c-green --c-cyan --c-blue + --c-purple --c-pink`. Themes remap these; they feed syntax + mermaid and are + handy source colours for the sets below. + +### Sets + +UI chrome is grouped into **sets** — named vars so a theme can tint each surface +independently (e.g. Dracula keeps the selection grey but makes pills green, rows +purple, the console pink). Every set defaults to `--accent` (fill sets also to +`--accent-fg`), so a theme that sets only `--accent` colours everything at once; +override a set to break it out. + +| set | var(s) | elements | +|-----|--------|----------| +| link | `--link` | link hover, hovered nav `.current`, prose underline, `#topic` hover | +| pill | `--pill` / `--pill-fg` | topic tag pills (`.topics__item`) hover fill | +| row | `--row` / `--row-fg` | post-list row (`.post-list__link`) hover fill | +| terminal | `--term` / `--term-fg` | console launcher + button fills, banner, prompt, caret | +| focus | `--focus` | `:focus-visible` outline | + +`*-fg` is the text/glyph colour on a filled (solid-background) set — pick one +with contrast against the fill. + +### Adding a theme + +1. Add `assets/css/themes/.css` with one `:root[data-theme=""] { … }` + block. Use `:root[...]` (not bare `[data-theme]`) so it matches the dark media + query's specificity and wins by source order (themes are concatenated after + `main.css`). Set core vars + `--selection` + `--code` + the pop palette; + override any sets you want distinct from `--accent`. It's picked up by the + `resources.Match "css/themes/*.css"` glob automatically — no wiring needed. +2. Add `` to the `modes` array in `console.js`'s `theme` command, plus an + `assert` for it in the self-check, then run `node assets/js/console.js`. +3. `pnpm build` and eyeball it (`theme set `), including a post with code + and a mermaid diagram. diff --git a/assets/css/main.css b/assets/css/main.css index 0d6fb8e..d5d4a51 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -28,6 +28,22 @@ --muted: #8a8a8a; --border: #e6e6e6; --accent: #b5451e; /* the pop of colour — change this one line to re-tint */ + --accent-fg: #ffffff; /* text/glyph colour on an accent fill */ + --selection: #f3d9cc; /* text-selection highlight */ + --code: var(--accent); /* inline `code` colour */ + + /* semantic sets — groups of UI elements coloured together. Each defaults to + --accent so a theme can re-tint one set (e.g. pills) without touching the + others. Fill sets carry a matching *-fg for text on the filled background. */ + --link: var(--accent); /* links, hovered nav, prose underline */ + --pill: var(--accent); --pill-fg: var(--accent-fg); /* topic tag pills */ + --row: var(--accent); --row-fg: var(--accent-fg); /* post-list row hover */ + --term: var(--accent); --term-fg: var(--accent-fg); /* console: fills, banner, prompt, caret */ + --focus: var(--accent); /* focus-visible outline */ + + /* pop palette — themes remap these; also feed syntax + mermaid diagrams */ + --c-red: #c0392b; --c-orange: #cc6a1a; --c-yellow: #b8860b; --c-green: #2e8b57; + --c-cyan: #1f7a8c; --c-blue: #2c5f8a; --c-purple: #7b4bc4; --c-pink: #b83280; --measure: 68ch; --pad: 1.5rem; @@ -45,14 +61,30 @@ --border: #262626; } } +/* dark base shares its pops with the media-query block above */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --selection: #33261f; + --c-red: #ff6b6b; --c-orange: #ffb86c; --c-yellow: #f1fa8c; --c-green: #50fa7b; + --c-cyan: #8be9fd; --c-blue: #7aa2f7; --c-purple: #bd93f9; --c-pink: #ff79c6; + } +} /* forced dark */ :root[data-theme="dark"] { --bg: #111111; --fg: #ededed; --muted: #7d7d7d; --border: #262626; + --selection: #33261f; + --c-red: #ff6b6b; --c-orange: #ffb86c; --c-yellow: #f1fa8c; --c-green: #50fa7b; + --c-cyan: #8be9fd; --c-blue: #7aa2f7; --c-purple: #bd93f9; --c-pink: #ff79c6; } +/* ---- colour themes (pick via the console: `theme set `) ---- + Each named theme lives in its own file under assets/css/themes/*.css and is + concatenated after this file (see baseof.html), so its :root[data-theme=...] + block matches the dark media query's specificity and wins by source order. */ + * { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } @@ -71,24 +103,29 @@ a { color: inherit; text-decoration: none; } /* ---- hover pops of colour: every clickable thing lights up ---- */ a:hover, -a:focus-visible, -.theme-toggle:hover, -.theme-toggle:focus-visible { color: var(--accent); } +a:focus-visible { color: var(--link); } .post__body a { text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--border); } -.post__body a:hover { text-decoration-color: var(--accent); } -:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } +.post__body a:hover { text-decoration-color: var(--link); } +:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } +::selection { background: var(--selection); } +::-moz-selection { background: var(--selection); } /* ---- layout ---- */ .site-header, -main, -.site-footer { +main { max-width: var(--measure); margin-inline: auto; padding-inline: var(--pad); } +/* clear the fixed footer + give the last block breathing room at page bottom */ +main { padding-bottom: 6rem; } /* ---- header / nav ---- */ .site-header { + position: sticky; + top: 0; + z-index: 40; + background: var(--bg); display: flex; align-items: baseline; justify-content: space-between; @@ -97,31 +134,7 @@ main, } .site-title { font-family: var(--pixel); font-weight: 400; font-size: 1.75rem; letter-spacing: 0.02em; } .site-nav { display: flex; align-items: baseline; gap: 1rem; font-family: var(--mono); font-size: 0.85rem; } -.site-nav .current { color: var(--accent); } - -.theme-toggle { - background: none; - border: 0; - padding: 0; - cursor: pointer; - color: inherit; - font: inherit; - font-size: 1rem; - line-height: 1; - position: relative; - top: 0px; /* nudge glyph down to sit on the nav baseline */ -} -.theme-toggle__label { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1em; - height: 1em; - line-height: 1; - transform-origin: 50% 50%; - transition: transform 0.3s ease; -} -.theme-toggle:hover .theme-toggle__label { transform: rotate(180deg); } +.site-nav .current { color: var(--link); } /* ---- back link (term pages) ---- */ .back-link { @@ -137,6 +150,64 @@ main, .page-head h1 { margin: 0 0 0.5rem; font-size: 1.6rem; letter-spacing: -0.01em; } .page-intro { color: var(--muted); max-width: 46ch; } +/* ---- landing (home) ---- */ +/* the front door: a greeting, a featured latest post, then a short recent list. + The full archive lives at /posts/, so this page reads differently on purpose. */ +.intro { + max-width: 30ch; + margin: 0.5rem 0 3.5rem; + font-size: clamp(1.5rem, 5vw, 2.1rem); + line-height: 1.25; + letter-spacing: -0.015em; + color: var(--accent); +} + +.eyebrow { + margin: 0 0 1rem; + font-family: var(--mono); + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} + +.lead { margin-bottom: 3.5rem; } +.lead__title { + margin: 0; + font-size: clamp(1.8rem, 4.5vw, 2.5rem); + line-height: 1.1; + letter-spacing: -0.025em; + font-weight: 500; +} +.lead__meta { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 0.9rem; + margin: 0.75rem 0 0; + font-family: var(--mono); + font-size: 0.8rem; + color: var(--muted); +} +.lead__topics a:not(:hover) { color: var(--muted); } +.lead__summary { max-width: var(--measure); margin: 1.1rem 0 0; color: var(--fg); } +.lead__more { + display: inline-block; + margin-top: 1.1rem; + font-family: var(--mono); + font-size: 0.85rem; + color: var(--link); +} + +.recent { margin-bottom: 2.5rem; } + +.all-link { + display: inline-block; + font-family: var(--mono); + font-size: 0.85rem; + color: var(--muted); +} +.all-link:hover { color: var(--link); } + /* ---- console (floating terminal) ---- */ .console-launch { position: fixed; @@ -152,9 +223,14 @@ main, border-radius: 999px; cursor: pointer; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); + transition: background-color 0.15s, border-color 0.15s, color 0.15s; } .console-launch:hover, -.console-launch:focus-visible { color: var(--accent); border-color: var(--accent); } +.console-launch:focus-visible { + background: var(--term); + border-color: var(--term); + color: var(--term-fg); +} .console { position: fixed; @@ -200,15 +276,17 @@ main, .console__btn { background: none; border: 0; - padding: 0 0.25em; + padding: 0.1em 0.4em; + border-radius: 4px; color: var(--muted); font: inherit; font-size: 1rem; line-height: 1; cursor: pointer; + transition: background-color 0.15s, color 0.15s; } .console__btn:hover, -.console__btn:focus-visible { color: var(--accent); } +.console__btn:focus-visible { background: var(--term); color: var(--term-fg); } .console__title { color: var(--muted); font-size: 0.75rem; } .console__screen { padding: 0.75rem; @@ -225,11 +303,11 @@ main, margin: 0 0 0.5rem; white-space: pre; line-height: 1.02; - color: var(--accent); + color: var(--term); overflow-x: auto; } .console__form { display: flex; align-items: baseline; gap: 0.5em; margin-top: 0.25rem; } -.console__prompt { color: var(--accent); } +.console__prompt { color: var(--term); } .console__input { flex: 1; min-width: 0; @@ -238,7 +316,7 @@ main, padding: 0; color: inherit; font: inherit; - caret-color: var(--accent); + caret-color: var(--term); } .console__input:focus { outline: none; } @@ -249,19 +327,24 @@ main, gap: 0.4rem 0.5rem; margin-bottom: 2.5rem; font-family: var(--mono); - font-size: 0.78rem; + font-size: 0.85rem; } .topics__item { display: inline-flex; align-items: center; gap: 0.35em; - padding: 0.25em 0.6em; + padding: 0.4em 0.85em; border: 1px solid var(--border); border-radius: 999px; + transition: background-color 0.15s, border-color 0.15s, color 0.15s; +} +.topics__item:hover { + background: var(--pill); + border-color: var(--pill); + color: var(--pill-fg); } -.topics__item:hover { border-color: var(--accent); } .topics__count { color: var(--muted); } -.topics__item:hover .topics__count { color: var(--accent); } +.topics__item:hover .topics__count { color: var(--pill-fg); } /* ---- post list ---- */ .post-list { list-style: none; margin: 0; padding: 0; } @@ -269,14 +352,33 @@ main, .post-list__item:last-child { border-bottom: 1px solid var(--border); } .post-list__link { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; gap: 1rem; padding-block: 0.9rem; + padding-inline: 0.75rem; + margin-inline: -0.75rem; + border-radius: 2px; + transition: background-color 0.1s, color 0.1s; } -.post-list__title { font-weight: 450; } +.post-list__link:hover { background: var(--row); color: var(--row-fg); } +.post-list__title { font-weight: 450; font-size: 1.25rem; } .post-list__date { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); white-space: nowrap; } -.post-list__link:hover .post-list__date { color: var(--accent); } +.post-list__link:hover .post-list__date { color: var(--row-fg); } + +/* ---- pager (paginated post list) ---- */ +.pager { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + margin-top: 2rem; + font-family: var(--mono); + font-size: 0.8rem; +} +.pager__link:hover { color: var(--link); } +.pager__link--off { color: var(--border); } +.pager__count { color: var(--muted); } /* ---- single post ---- */ .post__head { margin-bottom: 2.5rem; } @@ -324,7 +426,7 @@ main, color: var(--muted); } .post__topics a::before { content: "#"; color: var(--border); } -.post__topics a:hover::before { color: var(--accent); } +.post__topics a:hover::before { color: var(--link); } .post__body table { width: 100%; border-collapse: collapse; @@ -358,14 +460,35 @@ main, padding: 0.1em 0.35em; border: 1px solid var(--border); border-radius: 3px; + color: var(--code); } +/* syntax highlighting — Chroma token classes mapped to the pop palette so + every theme colours code (config: markup.highlight.noClasses = false) */ +.chroma { background: transparent; } +.chroma .c, .chroma .ch, .chroma .cm, .chroma .c1, .chroma .cs, +.chroma .cp, .chroma .cpf { color: var(--muted); font-style: italic; } /* comments */ +.chroma .k, .chroma .kc, .chroma .kd, .chroma .kn, .chroma .kp, +.chroma .kr, .chroma .kt { color: var(--c-pink); } /* keywords */ +.chroma .o, .chroma .ow { color: var(--c-pink); } /* operators */ +.chroma .s, .chroma .sa, .chroma .sb, .chroma .sc, .chroma .dl, .chroma .sd, +.chroma .s2, .chroma .se, .chroma .sh, .chroma .si, .chroma .sx, .chroma .sr, +.chroma .s1, .chroma .ss { color: var(--c-yellow); } /* strings */ +.chroma .m, .chroma .mb, .chroma .mf, .chroma .mh, .chroma .mi, +.chroma .il, .chroma .mo { color: var(--c-purple); } /* numbers */ +.chroma .nf, .chroma .fm { color: var(--c-green); } /* function names */ +.chroma .nb, .chroma .bp, .chroma .nc, .chroma .nn, +.chroma .ne, .chroma .nt { color: var(--c-cyan); } /* builtins, classes, tags */ +.chroma .nd, .chroma .nl { color: var(--c-orange); } /* decorators, labels */ +.chroma .na, .chroma .nv, .chroma .vc, .chroma .vg, +.chroma .vi { color: var(--c-blue); } /* attributes, variables */ + /* ---- post nav ---- */ .post-nav { display: flex; justify-content: space-between; gap: 1rem; - margin-top: 4rem; + margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-family: var(--mono); @@ -373,14 +496,15 @@ main, } .post-nav__next { margin-left: auto; text-align: right; } -/* ---- footer ---- */ +/* ---- footer: fixed to the bottom-left corner, no divider bar ---- */ .site-footer { + position: fixed; + left: 1rem; + bottom: 1rem; + z-index: 30; display: flex; - justify-content: space-between; - gap: 1rem; - margin-top: 5rem; - padding-block: 2rem; - border-top: 1px solid var(--border); + flex-direction: column; + gap: 0.2rem; font-family: var(--mono); font-size: 0.75rem; color: var(--muted); @@ -392,3 +516,9 @@ main, body { font-size: 17px; } .post__title { font-size: 1.6rem; } } +/* Console is desktop-only: a keyboard-driven terminal has no place on a touch + device. Key on pointer type, not width, so a narrow desktop window keeps it. */ +@media (hover: none) and (pointer: coarse) { + .console-launch, + .console { display: none !important; } +} diff --git a/assets/css/themes/dracula.css b/assets/css/themes/dracula.css new file mode 100644 index 0000000..2462213 --- /dev/null +++ b/assets/css/themes/dracula.css @@ -0,0 +1,14 @@ +/* Dracula — the canonical dark palette (draculatheme.com spec). + Concatenated after main.css (see baseof.html). */ +:root[data-theme="dracula"] { + --bg: #282a36; --fg: #f8f8f2; --muted: #6272a4; --border: #44475a; + --accent: #bd93f9; --accent-fg: #282a36; --selection: #44475a; --code: #ff79c6; + --c-red: #ff5555; --c-orange: #ffb86c; --c-yellow: #f1fa8c; --c-green: #50fa7b; + --c-cyan: #8be9fd; --c-blue: #8be9fd; --c-purple: #bd93f9; --c-pink: #ff79c6; + /* sets: highlight stays muted grey, each surface a distinct pop */ + --link: var(--c-cyan); + --pill: var(--c-green); --pill-fg: var(--bg); + --row: var(--c-purple); --row-fg: var(--bg); + --term: var(--c-pink); --term-fg: var(--bg); + --focus: var(--c-orange); +} diff --git a/assets/css/themes/paper.css b/assets/css/themes/paper.css new file mode 100644 index 0000000..b49deb1 --- /dev/null +++ b/assets/css/themes/paper.css @@ -0,0 +1,15 @@ +/* Paper — warm light, ink on cream with vermilion + jewel-tone pops. + Concatenated after main.css (see baseof.html); :root[...] matches the dark + media query's specificity and wins by source order. */ +:root[data-theme="paper"] { + --bg: #faf4e8; --fg: #2b2b2b; --muted: #8a7f6a; --border: #e4d8c0; + --accent: #c0392b; --accent-fg: #ffffff; --selection: #efe2c6; --code: #b83280; + --c-red: #c0392b; --c-orange: #cc6a1a; --c-yellow: #b8860b; --c-green: #2e8b57; + --c-cyan: #1f7a8c; --c-blue: #2c5f8a; --c-purple: #7b4bc4; --c-pink: #b83280; + /* sets: each surface its own hue */ + --link: var(--c-red); + --pill: var(--c-purple); --pill-fg: #ffffff; + --row: var(--c-blue); --row-fg: #ffffff; + --term: var(--c-green); --term-fg: #ffffff; + --focus: var(--c-orange); +} diff --git a/assets/css/themes/valentine.css b/assets/css/themes/valentine.css new file mode 100644 index 0000000..010dc79 --- /dev/null +++ b/assets/css/themes/valentine.css @@ -0,0 +1,14 @@ +/* Valentine — blush ground, burgundy ink, all pinks + rose/crimson pops + (no purple). Concatenated after main.css (see baseof.html). */ +:root[data-theme="valentine"] { + --bg: #fff0f5; --fg: #4a0d20; --muted: #a86b7e; --border: #f3cdd9; + --accent: #c71f5d; --accent-fg: #ffffff; --selection: #ffd6e5; --code: #b0185a; + --c-red: #a4133c; --c-orange: #d81159; --c-yellow: #b23a48; --c-green: #7b2d43; + --c-cyan: #c9184a; --c-blue: #8c1c3f; --c-purple: #e05780; --c-pink: #ff5c8a; + /* sets: pinks + crimson, each surface distinct */ + --link: var(--c-red); + --pill: var(--c-pink); --pill-fg: #ffffff; + --row: var(--c-cyan); --row-fg: #ffffff; + --term: var(--c-orange); --term-fg: #ffffff; + --focus: var(--c-pink); +} diff --git a/assets/js/console.js b/assets/js/console.js index bd89e6e..8f4f4bf 100644 --- a/assets/js/console.js +++ b/assets/js/console.js @@ -69,13 +69,14 @@ case "theme": { var sub = (cmd.args[0] || "").toLowerCase(); - var modes = ["light", "dark", "auto"]; + // base modes first, then colour palettes (see main.css) + var modes = ["light", "dark", "auto", "paper", "dracula", "valentine"]; if (sub === "list") { return { lines: modes.map(function (m) { return { text: " " + m }; }) }; } if (sub === "set") { var mode = (cmd.args[1] || "").toLowerCase(); - if (modes.indexOf(mode) === -1) return { lines: [{ text: "usage: theme set " }] }; + if (modes.indexOf(mode) === -1) return { lines: [{ text: "usage: theme set <" + modes.join(" | ") + ">" }] }; return { lines: [{ text: "theme set to " + mode + "." }], theme: mode }; } return { lines: [ @@ -142,6 +143,7 @@ var expandBtn = root.querySelector(".console__expand"); var closeBtn = root.querySelector(".console__close"); var cmdHistory = []; // local — do not shadow window.history + var histIdx = 0; // cursor for ↑/↓ recall; == length means "current line" var write = function (line, kind) { var el = document.createElement("div"); @@ -248,10 +250,22 @@ if (res.music === "off") { chiptune.off(); write({ text: "music off" }); } if (res.volume != null) chiptune.setVolume(res.volume); input.value = ""; + histIdx = cmdHistory.length; // reset recall to the (empty) current line log.scrollTop = log.scrollHeight; if (res.close) close(); }); + // ↑/↓ walk previous commands (shell-style). Down past the newest clears the line. + input.addEventListener("keydown", function (e) { + if (e.key !== "ArrowUp" && e.key !== "ArrowDown" || !cmdHistory.length) return; + e.preventDefault(); + if (e.key === "ArrowUp") { if (histIdx > 0) histIdx--; } + else if (histIdx < cmdHistory.length) histIdx++; + input.value = histIdx >= cmdHistory.length ? "" : cmdHistory[histIdx]; + var end = input.value.length; + input.setSelectionRange(end, end); + }); + root.querySelector(".console__screen").addEventListener("click", function () { input.focus(); }); // Drag by the title bar (skipped while docked). @@ -300,6 +314,8 @@ assert.strictEqual(run("", {}).lines.length, 0); assert.ok(/theme/.test(run("help", {}).lines[1].text)); assert.strictEqual(run("theme set dark", {}).theme, "dark"); + assert.strictEqual(run("theme set dracula", {}).theme, "dracula"); + assert.strictEqual(run("theme set valentine", {}).theme, "valentine"); assert.ok(/usage: theme set/.test(run("theme set purple", {}).lines[0].text)); assert.ok(/dark/.test(run("theme list", {}).lines[1].text)); assert.ok(/Subcommands/.test(run("theme", {}).lines[4].text)); diff --git a/exampleSite/content/posts/syntax-sampler.md b/exampleSite/content/posts/syntax-sampler.md new file mode 100644 index 0000000..0fe44cc --- /dev/null +++ b/exampleSite/content/posts/syntax-sampler.md @@ -0,0 +1,141 @@ ++++ +title = "A syntax highlighting sampler" +date = 2026-07-27 +draft = false +topics = ["Reference"] ++++ + +A page that does nothing but hold code, so the theme's token colours have +somewhere to show off. Switch themes from the console (`theme set dracula`, +`theme set paper`) and watch the keywords, strings, and numbers re-tint. + +## Python + +Comments, decorators, f-strings, and builtins all get their own hue. + +```python +from functools import lru_cache + + +@lru_cache(maxsize=None) +def fib(n: int) -> int: + """Classic memoised Fibonacci.""" + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + + +class Ring: + """A tiny fixed-size ring buffer.""" + + def __init__(self, size: int = 8) -> None: + self.size = size + self._buf: list[int] = [] + + def push(self, x: int) -> None: + self._buf.append(x) + if len(self._buf) > self.size: + self._buf.pop(0) + + +if __name__ == "__main__": + print(f"fib(20) = {fib(20)}") # 6765 +``` + +## JavaScript + +```js +const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi); + +async function fetchJSON(url, { retries = 3 } = {}) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return await res.json(); + } catch (err) { + if (attempt === retries) throw err; + await new Promise((r) => setTimeout(r, 2 ** attempt * 100)); + } + } +} + +export { clamp, fetchJSON }; +``` + +## Go + +```go +package main + +import ( + "fmt" + "strings" +) + +// Title-cases each word without the deprecated strings.Title. +func title(s string) string { + words := strings.Fields(s) + for i, w := range words { + words[i] = strings.ToUpper(w[:1]) + w[1:] + } + return strings.Join(words, " ") +} + +func main() { + fmt.Println(title("the quick brown fox")) // The Quick Brown Fox +} +``` + +## Rust + +```rust +/// Sum of the even Fibonacci numbers below `limit`. +fn even_fib_sum(limit: u64) -> u64 { + let (mut a, mut b) = (1u64, 2u64); + let mut total = 0; + while a < limit { + if a % 2 == 0 { + total += a; + } + (a, b) = (b, a + b); + } + total +} + +fn main() { + println!("{}", even_fib_sum(4_000_000)); +} +``` + +## Shell + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Roll back to the newest tag, or fail loudly. +latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +if [[ -z "$latest" ]]; then + echo "no tags found" >&2 + exit 1 +fi +git checkout "$latest" +``` + +## CSS + +```css +:root { + --gap: clamp(1rem, 2vw, 2rem); +} + +.grid { + display: grid; + gap: var(--gap); + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); +} +``` + +Inline code such as `git rebase -i` or `--maxsize=None` picks up the theme's +`--code` colour too. diff --git a/exampleSite/hugo.toml b/exampleSite/hugo.toml index 81da19d..fd3074e 100644 --- a/exampleSite/hugo.toml +++ b/exampleSite/hugo.toml @@ -4,8 +4,11 @@ title = "basic" theme = "basic" themesDir = "../.." +[pagination] + pagerSize = 12 + [params] - description = "A minimal black-and-white theme for text-first blogs." + description = "A minimal theme for text-first blogs." footerNote = "Built with the basic theme." [[menu.main]] @@ -27,6 +30,10 @@ themesDir = "../.." [markup.goldmark.renderer] unsafe = true +# Syntax highlighting via CSS classes so the theme (main.css) colours tokens. +[markup.highlight] + noClasses = false + # LaTeX math: $...$ / \(...\) inline, $$...$$ / \[...\] block [markup.goldmark.extensions.passthrough] enable = true diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 685b4c8..8945456 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -10,11 +10,14 @@ - {{ $css := resources.Get "css/main.css" | resources.Minify | resources.Fingerprint }} + {{/* main.css + every theme file, concatenated (themes must come after). */}} + {{ $main := resources.Get "css/main.css" }} + {{ $themes := resources.Match "css/themes/*.css" }} + {{ $css := slice $main | append $themes | resources.Concat "css/bundle.css" | resources.Minify | resources.Fingerprint }} {{ block "head" . }}{{ end }} @@ -24,14 +27,28 @@ {{ block "main" . }}{{ end }} {{ partial "footer.html" . }} + {{ partial "console.html" . }} {{ if .Store.Get "hasMermaid" }} {{/* ponytail: mermaid from CDN (Hugo's documented approach); vendor it if you need offline builds. */}} {{ end }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html index 8d5e0d7..45f183d 100644 --- a/layouts/_default/list.html +++ b/layouts/_default/list.html @@ -4,5 +4,13 @@

{{ .Title }}

{{ with .Content }}
{{ . }}
{{ end }} {{ partial "topics.html" . }} - {{ partial "post-list.html" .Pages }} + {{ $paginator := .Paginate .Pages }} + {{ partial "post-list.html" $paginator.Pages }} + {{ if gt $paginator.TotalPages 1 }} + + {{ end }} {{ end }} diff --git a/layouts/index.html b/layouts/index.html index 65160bb..acd5fd4 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -1,8 +1,34 @@ {{ define "main" }} - {{/* No h1 — the site title in the header already carries it. Keep the intro only. */}} + {{/* Landing = a front door, not the archive: a greeting, the latest post given + real weight, a few recents, topics, then a link to the full list at /posts/. */}} + {{ $posts := where site.RegularPages "Type" "posts" }} {{ $intro := or .Content site.Params.description }} - {{ with $intro }}
{{ . }}
{{ end }} - {{ partial "console.html" . }} + + {{ with $intro }}
{{ . }}
{{ end }} + + {{ with index $posts 0 }} +
+

latest

+

{{ .Title }}

+

+ + {{ with .GetTerms "topics" }}{{ range . }}#{{ .LinkTitle }} {{ end }}{{ end }} +

+ {{ with .Summary }}

{{ . | plainify | truncate 220 }}

{{ end }} + read → +
+ {{ end }} + + {{ with first 4 (after 1 $posts) }} +
+

more writing

+ {{ partial "post-list.html" . }} +
+ {{ end }} + {{ partial "topics.html" . }} - {{ partial "post-list.html" (where site.RegularPages "Type" "posts") }} + + {{ with site.GetPage "/posts" }} + {{ if gt (len $posts) 5 }}all posts →{{ end }} + {{ end }} {{ end }} diff --git a/layouts/partials/header.html b/layouts/partials/header.html index 0c96d8c..58cbe75 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -1,30 +1,6 @@ -