// The semantic elements every docs/typ topic uses, and the one place they // are defined. Content files call them ("this is a key", "this is a // builtin"); how each looks is decided here, per output: paged outputs // (the A4 cheatsheet, later the book) draw them, HTML export gives them // elements and classes for a stylesheet. Page geometry and fonts belong to // the output wrappers beside this file. /// The `atelier` theme (src/themes/atelier.zig): pardes's acme homage, /// butter paper and blue-green tags. #let palette = ( paper: rgb("#fffdeb"), ink: rgb("#293f35"), tag-bg: rgb("#e4f0ed"), tag-fg: rgb("#3d6055"), tag-name: rgb("#6e4774"), grip: rgb("#36826b"), col-grip: rgb("#955815"), str: rgb("#34743d"), num: rgb("#9a5810"), rule: rgb("#95baa2"), mouse: rgb("#8a3a00"), ) #let mono = "DejaVu Sans Mono" /// Topic ids for #doc: each names a chapter (an id in `chapters`) and, /// optionally, the label of a section in it. Old ids stay so older calls /// keep working. #let topics = ( guide: ("guide", none), setup: ("setup", none), cheatsheet: ("cheatsheet", none), scripting: ("scripting", none), reference: ("reference", none), themes: ("themes", none), building: ("building", none), tags: ("guide", none), fs: ("reference", none), config: ("guide", "config"), keys: ("guide", "keys"), detached: ("guide", "sessions"), ) /// Set by a wrapper that holds every chapter (the book, the HTML): #doc /// then links to the chapter or section. Alone (the A4 sheet) it names it. #let doc-links = state("doc-links", false) /// The book's and the HTML's reading order. `file` is the content file /// in docs/typ, or none while the topic is not written yet (the outputs /// then show a placeholder). Wrappers include each with heading offset 1, /// so a file's `=` headings sit under its chapter title, labelled /// `ch-`. #let chapters = ( (id: "guide", title: "Guide", file: "guide.typ"), (id: "setup", title: "Setting up your environment", file: "setup.typ"), (id: "cheatsheet", title: "Cheatsheet", file: "cheatsheet.typ"), (id: "scripting", title: "Scripting", file: "scripting.typ"), (id: "reference", title: "Reference", file: "reference.typ"), (id: "themes", title: "Themes", file: "themes.typ"), (id: "building", title: "Building", file: "building.typ"), ) /// The tutor, shown as it is in the editor (`Tutor`), from the repo root. #let tutor-path = "/src/tutor.txt" /// How much room tables and shell lines get: "roomy" (the book, the HTML) /// or "tight" (the A4 sheet sets it). #let density = state("density", "roomy") #let html-out() = target() == "html" #let span(class, body) = html.elem("span", attrs: (class: class), body) #let m(s, fill: palette.ink) = text(font: mono, size: 0.92em, fill: fill, s) /// A key: one press (`x`, `Ctrl-w`, `Shift-Esc`), or a sequence pressed one /// after another, written space-separated (`g g`, `SPC f c`). Either is ONE /// pill: a combination's parts joined by a muted +, a sequence's steps by a /// muted › (`Ctrl-w h` reads Ctrl + w › h). #let key(k) = context if html-out() { html.elem("kbd", k.split(" ").map(t => html.elem("kbd", t)).join(" ")) } else { let muted(c) = text(fill: palette.grip.transparentize(25%), size: 0.85em, c) let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(muted[+]) box( fill: palette.grip.transparentize(84%), radius: 2pt, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.5pt), text(font: mono, size: 0.92em, fill: palette.ink, k.split(" ").map(press).join(muted[#h(1pt)›#h(1pt)])), ) } /// Alternatives: separate keys listed together, each its own command. #let keys(..ks) = context ks.pos().map(key).join(if html-out() [ ] else { h(0.25em) }) #let button-names = ("B1": "left button", "B2": "wheel", "B3": "right button") /// A mouse, about cap height: left button, wheel, right button. The parts /// in `pressed` ("B1", "B2", "B3") are filled. #let mouse(pressed) = { let (w, h, top) = (0.84em, 1.2em, 0.56em) let ink = palette.mouse let s = 0.7pt + ink let on(b) = if b in pressed { ink } else { none } box(width: w, height: h, baseline: 0.24em, { place(rect(width: w / 2, height: top, radius: (top-left: w / 2), fill: on("B1"), stroke: s)) place(dx: w / 2, rect(width: w / 2, height: top, radius: (top-right: w / 2), fill: on("B3"), stroke: s)) place(dy: top, rect(width: w, height: h - top, radius: (bottom: w / 2), stroke: s)) // the wheel: a narrow tall slot over the buttons' seam, on a paper // halo so a filled wheel stands apart from the button outlines let (ww, wh, halo) = (0.26em, 0.44em, 0.07em) let (wx, wy) = ((w - ww) / 2, 0.06em) place(dx: wx - halo, dy: wy - halo / 2, rect( width: ww + 2 * halo, height: wh + halo, radius: ww, fill: palette.paper)) place(dx: wx, dy: wy, rect( width: ww, height: wh, radius: ww / 2, stroke: 0.5pt + ink, fill: if "B2" in pressed { ink } else { palette.paper }, )) }) } /// A mouse button; `shift: true` for the button with Shift held. #let btn(b, shift: false) = context if html-out() { let label = (if shift { "shift " } else { "" }) + button-names.at(b) html.elem("span", attrs: (class: "btn", aria-label: label, title: label), (if shift { "⇧" } else { "" }) + b) } else { if shift { text(size: 0.8em, fill: palette.col-grip)[⇧] } mouse((b,)) } /// A chord: the first button held, each next one clicked while it is. /// Drawn as the held button, then for each click the held one and that one /// down, with arrows between. #let chord(..bs) = context if html-out() { let label = bs.pos().map(b => button-names.at(b)).join(" then ") html.elem("span", attrs: (class: "chord", aria-label: label, title: label), bs.pos().join("-")) } else { let (held, ..clicks) = bs.pos() let arrow = text(size: 0.75em, fill: palette.col-grip)[#h(0.5pt)›#h(0.5pt)] ((mouse((held,)),) + clicks.map(c => mouse((held, c)))).join(arrow) } /// A tag word or builtin, as it sits in a tag. #let word(w) = context if html-out() { span("word", raw(w)) } else { box(fill: palette.tag-bg, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.4pt), m(w, fill: palette.tag-fg)) } /// A whole tagline: its grip, the path, then its words. #let tag(words, path: none) = context if html-out() { html.elem("div", attrs: (class: "tag"), { if path != none { span("path", path); [ ] } raw(words) }) } else { block(width: 100%, fill: palette.tag-bg, inset: (x: 2pt, y: 2.4pt), spacing: 0.5em, { box(fill: palette.grip, width: 0.55em, height: 0.75em, baseline: 0.1em) h(3pt) if path != none { m(path, fill: palette.tag-name); h(0.6em) } m(words, fill: palette.tag-fg) }) } /// A look address. #let addr(a) = context if html-out() { span("addr", raw(a)) } else { m(a, fill: palette.tag-name) } /// A 9P path. #let file(p) = context if html-out() { span("file", raw(p)) } else { m(p, fill: palette.str) } /// A shell line. #let cmd(c) = context if html-out() { raw(c, block: true, lang: "sh") } else { let tight = density.get() == "tight" block( width: 100%, spacing: if tight { 0.4em } else { 0.9em }, inset: if tight { (left: 3pt, y: 1.2pt) } else { (left: 6pt, y: 3pt) }, stroke: (left: (if tight { 1pt } else { 1.5pt }) + palette.rule), fill: if tight { none } else { palette.tag-bg.transparentize(50%) }, text(font: mono, size: 0.85em, fill: palette.ink, c), ) } /// Where the full story is: a topic id from `topics`, and a section label /// in it. A link where the wrapper holds the chapters, else its name. #let doc(id, section: none) = { let (chapter, default) = topics.at(id) let section = if section != none { section } else { default } let title = chapters.find(c => c.id == chapter).title let to = label(if section != none { section } else { "ch-" + chapter }) let named = title + if section != none { ": " + section.replace("-", " ") } else { "" } context { if doc-links.get() { let found = query(to) let shown = if found.len() > 0 and found.first().func() == heading { if section != none [#title: #found.first().body] else [#title] } else [#named] link(to, text(fill: palette.num)[→ #shown]) } else { text(fill: palette.num)[→ #named] } } } /// Pairs: a thing and what it means, as a two-column table, key column /// first, rows on alternating tints. `split: 2` sets the rows as that many /// tables side by side (on paper; HTML gets one table). #let pairs(split: 1, ..cells) = { let rows = cells.pos().chunks(2) context { let tight = density.get() == "tight" let one(rows) = table( columns: (auto, 1fr), stroke: none, inset: if tight { (x: 2pt, y: 1.6pt) } else { (x: 7pt, y: 4pt) }, fill: (_, y) => if calc.even(y) { palette.tag-bg.transparentize(35%) }, ..rows.flatten(), ) if html-out() or split == 1 { one(rows) } else { let n = calc.ceil(rows.len() / split) grid(columns: (1fr,) * split, column-gutter: if tight { 2pt } else { 10pt }, ..rows.chunks(n).map(one)) } } }