summaryrefslogtreecommitdiff
path: root/docs/typ/style.typ
blob: ed71353740ecb0f45852cb1141c989e000fe86a6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// 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-<id>`.
#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, for contributors", file: "building.typ"),
)

/// The one-line pitch: the A4 sheet's header and the site's landing page.
#let tagline = "acme's tags and three buttons, helix's keys, terminals as panes, the editor as a 9P filesystem"

/// 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() {
  let sep(c, cls) = html.elem("span", attrs: (class: cls, "aria-hidden": "true"), c)
  let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(sep("+", "plus"))
  html.elem("kbd", attrs: (class: "key"), k.split(" ").map(press).join(sep("›", "then")))
} 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")
#let click-names = ("B1": "left", "B2": "middle", "B3": "right")
/// Text that is read and copied but not shown, beside a drawn glyph.
#let hidden(t) = html.elem("span", attrs: (class: "vh"), t)

/// 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 },
    ))
  })
}

/// The same mouse as inline SVG for HTML. Each part is classed `on`
/// (pressed) or `off`, and the stylesheet colours them: the pressed part
/// filled in the accent, the rest outlined and muted.
#let mouse-svg(pressed) = {
  let cls(b) = if b in pressed { "on" } else { "off" }
  let part(d, b) = html.elem("path", attrs: (class: cls(b), d: d))
  html.elem("svg", attrs: (class: "mouse", viewBox: "-6 -6 96 132", "aria-hidden": "true"), {
    // taller buttons and a slimmer wheel than on paper, so a pressed
    // button shows as a solid block at body size
    part("M0 64H84V78A42 42 0 0 1 0 78Z", none)
    part("M0 64V42A42 42 0 0 1 42 0V64Z", "B1")
    part("M42 0A42 42 0 0 1 84 42V64H42Z", "B3")
    // a pressed wheel is drawn wider, so it reads as pressed, not as a seam
    let (hx, hw, wx, ww) = if "B2" in pressed { ("27", "30", "31", "22") } else { ("31", "22", "35", "14") }
    html.elem("rect", attrs: (class: "halo", x: hx, y: "4", width: hw, height: "48", rx: "13"))
    html.elem("rect", attrs: (class: "wheel " + cls("B2"), x: wx, y: "8", width: ww, height: "40", rx: "9"))
  })
}

/// 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", title: label), {
    if shift { html.elem("span", attrs: (class: "shift", "aria-hidden": "true"), "⇧") }
    mouse-svg((b,))
    hidden((if shift { "Shift-" } else { "" }) + click-names.at(b) + "-click")
  })
} 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 ")
  let (held, ..clicks) = bs.pos()
  let arrow = html.elem("span", attrs: (class: "then", "aria-hidden": "true"), "›")
  let said = "hold " + click-names.at(held) + ", click " + clicks.map(c => click-names.at(c)).join(", then ")
  html.elem("span", attrs: (class: "chord", title: label), {
    ((mouse-svg((held,)),) + clicks.map(c => mouse-svg((held, c)))).join(arrow)
    hidden(said)
  })
} 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"), {
    html.elem("span", attrs: (class: "grip", "aria-hidden": "true"))[]
    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() { html.elem("div", attrs: (class: "cmd"), 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))
    }
  }
}

/// Where pardes lives and under what licence, for the site's footer (and
/// any output that wants them). none until decided.
#let project = (source: "https://git.0x4200.cafe/pardes", license: "ISC")

/// A glossary: like #pairs, under its own heading. On paper the heading
/// and the table; in HTML a <details> that starts closed, its summary the
/// heading and the count, so a reader who knows the words skips them.
#let glossary(title, lbl, ..cells) = context if html-out() {
  html.elem("details", attrs: (class: "glossary"), {
    html.elem("summary", [#heading(title) #lbl #html.elem("span", attrs: (class: "count"), "(" + str(calc.quo(cells.pos().len(), 2)) + ")")])
    pairs(..cells)
  })
} else {
  [#heading(title) #lbl]
  pairs(..cells)
}