aboutsummaryrefslogtreecommitdiff
path: root/global
diff options
context:
space:
mode:
Diffstat (limited to 'global')
-rw-r--r--global/common.typ228
-rw-r--r--global/extlink.svg25
-rw-r--r--global/favicon.icobin0 -> 15086 bytes
-rw-r--r--global/favicon.svg8
-rw-r--r--global/main.css371
5 files changed, 632 insertions, 0 deletions
diff --git a/global/common.typ b/global/common.typ
new file mode 100644
index 0000000..14738a6
--- /dev/null
+++ b/global/common.typ
@@ -0,0 +1,228 @@
+// Site template for kumardamani.net, adapted from the example Typst blog.
+// Structured for Typst's bundle export: pages are emitted as #document
+// elements from the main.typ entrypoint and cross-page links use labels.
+
+#let _site-info = state("site-info", none)
+#let _page-name = state("page-name", none)
+#let _page-dir = state("page-dir", "")
+
+// Image paths collected from pages, emitted as bundle assets in main.typ.
+#let _asset-paths = state("asset-paths", ())
+
+#let srcurl = "https://gitlab.com/kdam0/kumardamani.net"
+
+// Deployment info, overwritten by CI per build:
+// printf '{ "date": "%s", "link": "%s" }\n' \
+// "$(date '+%B %d, %Y' | sed 's/ 0/ /')" "$CI_JOB_URL" > ci.json
+// date is pre-formatted for display, e.g. "July 24, 2026".
+#let ci = json("/ci.json")
+
+// Page ids ("post/self-host") map to labels on the corresponding #document
+// elements ("post-self-host"); typst resolves relative links between
+// documents automatically.
+#let label-for(id) = label(id.replace("/", "-"))
+
+#let extlink(
+ dest,
+ attrs: (:),
+ body
+) = html.elem(
+ "a",
+ attrs: (
+ class: "extlink",
+ rel: "external",
+ target: "_blank",
+ href: dest,
+ ) + attrs,
+ body
+)
+
+#let navbar(pages, current) = {
+ html.elem("nav", html.elem("ul", {
+ for (pagename, prefs) in pages {
+ let (title,) = prefs
+ let hidden = prefs.at("hidden", default: false)
+
+ if hidden and pagename != current {
+ continue
+ }
+
+ let attrs = (:)
+ let class = ()
+
+ if pagename == current {
+ class += ("current",)
+ }
+
+ if hidden {
+ class += ("hidden",)
+ }
+
+ if class != () {
+ attrs += ("class": class.join(" "))
+ }
+
+ if pagename == current {
+ // the current page's nav item is not a link
+ html.elem("li", attrs: attrs, title)
+ } else if "url" in prefs {
+ html.elem("li", attrs: attrs, extlink(prefs.url, title))
+ } else {
+ html.elem("li", attrs: attrs, link(label-for(pagename), title))
+ }
+ }
+ }))
+}
+
+// Image helpers. These emit plain <img> tags with literal src paths so that
+// images stay as separate files instead of being embedded into the HTML, and
+// register local sources as bundle assets (emitted in main.typ).
+#let img(src, alt: "", width: none) = {
+ if not src.starts-with("http") {
+ context {
+ let dir = _page-dir.get()
+ _asset-paths.update(s => s + (dir + src,))
+ }
+ }
+
+ let attrs = (src: src, alt: alt)
+ if width != none {
+ attrs.insert("style", "width: " + width)
+ }
+ html.elem("img", attrs: attrs)
+}
+
+#let fig(src, alt: "", caption: none, width: none) = html.elem("figure", {
+ img(src, alt: alt, width: width)
+ if caption != none {
+ html.elem("figcaption", caption)
+ }
+})
+
+#let _preferred-title(id) = {
+ let page = _site-info.get().at("pages").at(id)
+
+ if "long-title" in page {
+ return page.at("long-title")
+ } else {
+ return page.at("title")
+ }
+}
+
+// The preferred (long, if available) title of the current page.
+#let preferred-title = context {
+ _preferred-title(_page-name.get())
+}
+
+// The publication date of the current page, if any.
+#let page-date = context {
+ let info = _site-info.get().at("pages").at(_page-name.get())
+ if "date" in info {
+ html.elem("p", attrs: (class: "date"),
+ html.elem("time", attrs: (datetime: info.date), info.date))
+ }
+}
+
+// Link to another page on the site; relative paths are resolved by typst.
+#let pagelink(id, body) = link(label-for(id), body)
+
+// Link to another page, using its preferred title as the link text.
+#let pageref(id) = context {
+ link(label-for(id), _preferred-title(id))
+}
+
+#let page(
+ pagename,
+ site-info,
+ banner: none,
+ stylesheets: (),
+ body
+) = {
+ let site = site-info.at("title", default: "Untitled")
+ let pages = site-info.at("pages", default: (:))
+
+ if banner == none {
+ banner = html.elem("h1", attrs: (class: "banner"), site)
+ }
+
+ // Prefix for links back to the site root from nested pages (posts).
+ let parts = pagename.split("/")
+ let depth = parts.len() - 1
+ let base = "../" * depth
+
+ html.html(
+ lang: "en",
+ {
+ html.head({
+ html.meta(charset: "utf-8")
+ html.meta(name: "viewport", content: "width=device-width, initial-scale=1")
+ html.title(pages.at(pagename).title + " – " + site)
+
+ html.elem("link", attrs: ("rel": "vcs-git", "title": "site source", "href": srcurl))
+ html.link(rel: "icon", href: base + "favicon.svg", type: "image/svg+xml")
+ html.link(rel: "icon", href: base + "favicon.ico")
+ html.elem("link", attrs: (rel: "preload", href: base + "extlink.svg", "as": "image"))
+ html.link(rel: "stylesheet", href: base + "main.css")
+
+ for ss in stylesheets {
+ html.link(rel: "stylesheet", href: base + ss)
+ }
+ })
+ html.body({
+ _site-info.update(site-info)
+ _page-name.update(pagename)
+ _page-dir.update(if depth > 0 { parts.slice(0, -1).join("/") + "/" } else { "" })
+
+ show html.elem: it => {
+ if "title" in it.attrs {
+ let attrs = it.attrs
+ let has-class = "class" in it.attrs
+
+ if not has-class or regex("\btooltip\b") not in it.attrs.class {
+ if has-class {
+ attrs.class += " tooltip"
+ } else {
+ attrs.class = "tooltip"
+ }
+
+ it = html.elem(it.tag, attrs: attrs, it.body)
+ }
+ }
+
+ it
+ }
+
+ show outline.entry: it => {
+ if it.prefix() != none {
+ html.span(class: "prefix", it.prefix())
+ }
+ link(it.element.location(), it.body())
+ }
+
+ html.header({
+ banner
+ navbar(pages, pagename)
+ })
+
+ html.main(body)
+ html.hr()
+ html.footer({
+ html.span(class: "updated")[Updated: #ci.date.]
+ html.span(class: "logs")[#extlink(ci.link)[Logs] for nerds!]
+ })
+ })
+ }
+ )
+}
+
+// Scaffold for post pages: numbered headings (the heading counter is global
+// to the bundle, so it is reset per document) with title and date on top.
+#let post-page(pagename, site-info, body) = page(pagename, site-info, {
+ set heading(numbering: "1.")
+ counter(heading).update(0)
+
+ title(preferred-title)
+ page-date
+
+ body
+})
diff --git a/global/extlink.svg b/global/extlink.svg
new file mode 100644
index 0000000..3673c92
--- /dev/null
+++ b/global/extlink.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ version="1.1"
+ id="svg1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs1" />
+ <g
+ id="layer1">
+ <path
+ style="fill:#000000;stroke:none;stroke-width:10"
+ d="M 12,0 H 24 V 12 H 21 L 21.020819,5.1005019 11.121321,15 9,15 V 12.878679 L 18.899498,2.9791809 12,3 Z"
+ id="path1" />
+ <path
+ style="fill:#000000;stroke:none;stroke-width:10"
+ d="M 0,6 V 24 H 18 V 12 h -3 v 9 H 3 V 9 h 9 V 6 Z"
+ id="path2" />
+ </g>
+</svg>
diff --git a/global/favicon.ico b/global/favicon.ico
new file mode 100644
index 0000000..49cf463
--- /dev/null
+++ b/global/favicon.ico
Binary files differ
diff --git a/global/favicon.svg b/global/favicon.svg
new file mode 100644
index 0000000..b8d5205
--- /dev/null
+++ b/global/favicon.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
+ <!-- crown -->
+ <path d="M 22,10 L 42,10 L 47,38 L 17,38 Z" fill="#3a3f44"/>
+ <!-- band -->
+ <path d="M 17.6,31 L 46.4,31 L 47,38 L 17,38 Z" fill="#22262a"/>
+ <!-- brim: downward sloping skirt -->
+ <path d="M 17,36 L 47,36 L 60,48 Q 61.5,51.5 57,52 L 7,52 Q 2.5,51.5 4,48 Z" fill="#5c646b"/>
+</svg>
diff --git a/global/main.css b/global/main.css
new file mode 100644
index 0000000..42951f4
--- /dev/null
+++ b/global/main.css
@@ -0,0 +1,371 @@
+/* SPDX-License-Identifier: MIT */
+/* SPDX-FileCopyrightText: 2024-2026 Katalin Rebhan <me@dblsaiko.net> */
+/* Adapted for kumardamani.net */
+
+:root {
+ --button-border: color-mix(in oklab, ButtonFace, CanvasText 10%);
+ --extlink-icon: url('extlink.svg');
+
+ /* base font size is 16px, which is 12pt */
+ --font-size-px-mul: calc(1rem / 16);
+ --font-size-pt-mul: calc(1rem / 12);
+
+ /* fonts we use */
+ --shell-ff: system-ui, ui-sans-serif, sans-serif;
+ --shell-fs: 1rem;
+ --shell-font: var(--shell-fs) var(--shell-ff);
+
+ --body-ff: var(--shell-ff);
+ --body-fs: 1rem;
+ --body-font: var(--body-fs) var(--body-ff);
+
+ --mono-ff: ui-monospace, monospace;
+ --mono-fs: calc(13 * var(--font-size-px-mul)); /* according to Safari */
+ --mono-font: var(--mono-fs) var(--mono-ff);
+
+ /* some extra lengths */
+ --elem-spacing: 0.5rem;
+ --navbar-extra: 0.15rem;
+ --border-size: 0.15rem;
+ --text-skip: 0.2em;
+}
+
+@media not (pointer: coarse) {
+ :root {
+ /* on displays with a non-coarse pointer (i.e. desktop computers),
+ shrink the shell font a bit because we can get away with it */
+ --shell-fs: calc(10 * var(--font-size-pt-mul));
+ }
+}
+
+html {
+ max-width: 800px;
+ height: min(100%, 600px);
+ /* importantly, we never set font-size on the html element, so we can use
+ rem later to refer to the browser's default which might be modified by
+ the user. */
+ font-family: var(--body-ff);
+ display: flex;
+ flex-direction: column;
+ color-scheme: light dark;
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+ background: Canvas;
+}
+
+body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+pre, xmp, plaintext, listing, tt, code, kbd, samp {
+ font: var(--mono-font);
+}
+
+header {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: baseline;
+ margin: 0 0 var(--elem-spacing) 0;
+ font: var(--shell-font);
+}
+
+header > nav {
+ display: flex;
+ flex-wrap: nowrap;
+ text-wrap: nowrap;
+ flex-grow: 1;
+ overflow-x: auto;
+ padding: 0 calc(var(--elem-spacing) + .1rem);
+ border-bottom: var(--border-size) solid var(--button-border);
+}
+
+header > nav > ul {
+ display: contents;
+}
+
+header > nav a:link {
+ text-decoration: unset;
+ color: unset;
+}
+
+header > nav a:visited {
+ color: unset;
+}
+
+header > nav li {
+ display: inline;
+ padding: 0 calc(var(--elem-spacing) + var(--navbar-extra));
+ margin: 0 calc(-1 * var(--navbar-extra));
+}
+
+header > nav li.current {
+ background-color: ButtonFace;
+ color: ButtonText;
+}
+
+header > nav li.hidden {
+ font-style: italic;
+}
+
+.banner {
+ font-size: 1.5em;
+ font-weight: unset;
+ margin: 0 var(--elem-spacing) 0 0;
+}
+
+main {
+ flex-grow: 1;
+ text-wrap-style: pretty;
+}
+
+main > :first-child {
+ margin-top: 0;
+}
+
+main > :last-child {
+ margin-bottom: 0;
+}
+
+hr {
+ /* Fixes <hr> collapsing to width 0 inside of a flex container */
+ margin-left: 0;
+ margin-right: 0;
+ align-self: stretch;
+
+ /* Same style as navbar */
+ border: 0;
+ border-bottom: var(--border-size) solid var(--button-border);
+}
+
+pre {
+ overflow: auto;
+}
+
+blockquote {
+ display: block;
+ margin-inline-start: var(--elem-spacing);
+ margin-inline-end: 0;
+ border-inline-start: var(--border-size) solid;
+ padding-inline-start: var(--text-skip);
+}
+
+/* Safari does not like font-size: 1rem + clip-path mixed units (FB22092644).
+ Hope we never have to combine both! */
+.biglist > li .description > a.extlink::after {
+ font-size: unset;
+}
+
+a.extlink::after {
+ padding-right: 0.5em;
+ color: CanvasText;
+ background-color: currentcolor;
+ mask: var(--extlink-icon);
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ clip-path: polygon(
+ calc(50% - 0.25em) calc(50% - 0.25em),
+ calc(50% + 0.25em) calc(50% - 0.25em),
+ calc(50% + 0.25em) calc(50% + 0.25em),
+ calc(50% - 0.25em) calc(50% + 0.25em)
+ );
+ content: "";
+ vertical-align: super;
+ line-height: 0;
+ font-size: 1rem;
+}
+
+.biglist {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1em;
+ padding: 0;
+}
+
+.biglist > li {
+ display: block;
+}
+
+.biglist > li > h2 {
+ float: left;
+ margin: 0;
+ font-size: unset;
+}
+
+.biglist > li > h2::after {
+ color: GrayText;
+ content: "—";
+ font-weight: initial;
+ margin: 0 var(--text-skip);
+}
+
+.biglist > li .description {
+ display: inline;
+ margin: 0;
+}
+
+.biglist > li .source {
+ float: right;
+ margin: 0;
+}
+
+.biglist > li .source::before {
+ color: GrayText;
+ content: "—";
+ margin: 0 var(--text-skip);
+}
+
+article {
+ hyphens: auto;
+}
+
+article > p, article > blockquote {
+ text-align: justify;
+}
+
+article > :first-child {
+ margin-top: 0;
+}
+
+article > :last-child {
+ margin-bottom: 0;
+}
+
+article > nav li {
+ display: contents;
+}
+
+article > nav ol {
+ display: grid;
+ column-gap: var(--text-skip);
+ grid-template-columns: 0 1fr;
+}
+
+article > nav > ol {
+ overflow: auto;
+}
+
+article > nav ol {
+ grid-column: 2;
+}
+
+article > nav .prefix {
+ user-select: none;
+ grid-column: 1;
+ justify-self: end;
+}
+
+article > nav a {
+ grid-column: 2;
+ justify-self: start;
+}
+
+footer {
+ display: grid;
+ /* equal flexible side tracks keep the middle track truly centered */
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+ font: var(--shell-font);
+}
+
+footer > .updated {
+ grid-column: 2;
+ text-align: center;
+}
+
+footer > .logs {
+ grid-column: 3;
+ justify-self: end;
+}
+
+footer > ul {
+ display: contents;
+}
+
+footer > ul > li {
+ display: inline;
+ font: var(--shell-font);
+}
+
+footer > ul > li:not(:last-child)::after {
+ color: GrayText;
+ content: "—";
+ font-weight: initial;
+ margin: 0 var(--text-skip);
+ padding: 0;
+}
+
+.motb {
+ font-style: italic;
+ color: GrayText;
+ white-space: pre-wrap;
+}
+
+.tooltip {
+ text-decoration: underline;
+ text-decoration-style: dotted;
+ cursor: help;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 0;
+ border: 0 solid transparent;
+ background-clip: padding-box;
+}
+
+tr > td + td {
+ border-left-width: 1.5em;
+}
+
+tr + tr > td {
+ border-top-width: 0.2em;
+}
+
+/* --- additions for kumardamani.net --- */
+
+/* publication date under post titles */
+.date {
+ color: GrayText;
+ font: var(--shell-font);
+ margin-top: 0;
+}
+
+figure {
+ margin: 1em 0;
+ text-align: center;
+}
+
+figcaption {
+ color: GrayText;
+ margin-top: 0.3em;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+p > img:only-child {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+pre {
+ padding: 0.5em;
+ border: 1px solid var(--button-border);
+}
+
+details {
+ margin: 1em 0;
+}
+
+summary {
+ cursor: pointer;
+}