diff --git a/packages/preview/obelisk/0.2.0/LICENSE.md b/packages/preview/obelisk/0.2.0/LICENSE.md new file mode 100644 index 0000000000..c990b027e9 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/LICENSE.md @@ -0,0 +1,7 @@ +Copyright © 2026 atzlt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/obelisk/0.2.0/README.md b/packages/preview/obelisk/0.2.0/README.md new file mode 100644 index 0000000000..696b68668c --- /dev/null +++ b/packages/preview/obelisk/0.2.0/README.md @@ -0,0 +1,133 @@ +# Obelisk + +A high-precision Typst document template engineered for mathematical monographs, course notes, and technical publications. Inspired by the strict geometric discipline of Bauhaus and Russian Constructivism, **Obelisk** treats typography as an architectural problem — locking every element, heading, and block to a cohesive structural canvas. + +--- + +## Key Philosophies + +* **Bauhaus-Inspired Aesthetic:** Rejects decorative fluff in favor of heavy typography, active negative space, and dynamic layout vectors. + +* **The Absolute Baseline Grid:** Every single line of text, heading, and block element automatically snaps to a rigid vertical rhythm. + +* **Print & Publication Ready:** Engineered natively for physical printing. Features asymmetrical, alternating margins with a dedicated ledger column for sidenotes and technical markers. + +* **Zero-Configuration Onboarding:** Despite the complex geometric arithmetic running under the hood, Obelisk requires no elaborate layout scaffolding or boilerplate setup. It compiles instantly out of the box with highly refined, production-ready defaults—allowing you to focus entirely on writing your mathematics without wrestling with document configuration. + +--- + +## Core Features & Layout Architecture + +1. **Built-in Theorem Environments:** pre-configured environments for mathematical exposition (Theorems, Lemmas, Corollaries, and Proofs). Define your own using the exposed `#make-environment`. + +2. **Grid-Aligned Blocks (`#bblock`):** Standard block elements can easily throw off a baseline grid due to fractional padding or stroke widths. Obelisk exposes the _baseline block_ `bblock` component, which automatically calculates and pads structural boxes so that the following text snaps perfectly back onto the running baseline. By default, all block equations are wrapped in `#bblock`. + +3. **Intentional Page Breaks (`#blank-page`):** When preparing documents for double-sided publication, forcing a section to start on an odd page can leave an empty facing page. The `blank-page` function inserts a clean break and prints an authoritative, technical layout marker. By default, all top level headings break to an odd page using `#black-page`. + +4. **Customizable:** Customize page sizes and margins, baseline step sizes, fonts, and more. Obelisk comes out of the box with a carefully designed default configuration, allowing you to tweak the underlying structural metrics without breaking the core grid alignment arithmetic. _Customization is still in early stage; more options are coming in the future. See below for a guide to customization._ + +--- + +## Example + +```typst +#import "@preview/obelisk:0.2.0": * + +#show: init + += Obelisk Template + +#definition[ + *Obelisk* is a note template inspired by Bauhaus aesthetics. +] + +== Second Level Header + +All texts are by default aligned to a baseline grid. + +$ integral_0^(+oo) "e"^(-x^2) dif x=sqrt(pi)/2 $ + +Math blocks are also automatically aligned. + +=== Third Level Header + +The template includes several theorem environments by default. #sidenote[inline sidenotes can be added] Theorems can have titles and referenced. + +#theorem[mean inequality][ + One of the mean inequalities is $n/(sum_(i=1)^n 1/x_i)<=root(n, product_(i=1)^n x_i).$ +] + +Theorems can be referenced by numbering (@thm:mean-ineq) or by title (@thm:mean-ineq[!]). +``` + +--- + +## Customization Guide + +Obelisk exposes its architectural engine through a centralized configuration dictionary. To apply your customizations, replace `show: init` with `show: init.with()`. + +### 1. Layout Configuration + +```typst +paper: (width, height, two-sided), +margin: (s, e, t, f), +side: (half-gutter, margin), +texts: (size, ascender, step), +``` + +* **`paper`**: Maps the absolute dimensions of the physical canvas. + * `width` / `height`: Explicit length dimensions. Defaults to A4 size. + * `two-sided`: whether to use a two-sided layout. Defaults to `true`. + + +* **`margin`**: Uses logical binding scopes to handle alternating page layouts dynamically. + * `s` / `e`: Spine (Inside) and Face (Outside) horizontal margins. The layout engine flips these on odd and even pages to maintain binding gutters. In single-sided mode, they are right and left margins respectively. + * `t` / `f`: Top and Foot (Bottom) vertical margins, defining the boundaries where the baseline grid activates and terminates. + + +* **`side`**: Manages the geometry of the margin ledger. + * `half-gutter`: Half of the gutter between the body text and the side margin body. Equals the gutter between the body text and the vertical separating line. + * `margin`: The outer margin for the sidenotes. + + +* **`texts`**: The core parameters of the vertical baseline grid. + * `size`: The default font size for the document body text (configured to `12pt` by default). + * `ascender`: The explicit vertical (ascender) height of the font. + * `step`: The absolute grid increment (configured to `16pt` by default), or the default line height. All block vertical paddings, headings, and margins must be integer multiples of this value. + +### 2. Font Configuration + +```typst +fonts: (body, math, sans, mono) +``` +By default, Obelisk uses **TeX Gyre Pagella (Math)** for body and math fonts, **Inter** for sans and **IBM Plex Mono** for mono. These fonts are already included in the web app. + +For font replacements, _we recommend_: + +* For the sans font: use heavy, geometric, or unextended modern grotesques. We recommend: Switzer, Inter, IBM Plex Sans. +* For the mono font: use high-legibility coding typefaces. We recommend: IBM Plex Mono, Jetbrains Mono. + +### 3. Decoration Configuration + +```typst +deco: (line-number) +headers: (h2: (sym, dy, size), h3: (sym, dy, size)) +``` + +* **`deco`**: configures decorations. + * `line-number`: toggle show line number or not. Default `true`. + +* **`headers`**: configures H2 and H3 headers. + * `sym`: the symbol to place on the vertical line. + * `dy`: for some large symbols the symbol will be a bit off the horizontal line. Nudge this parameter to bring it back. Default: `0pt`. + * `size`: the size factor of the headings. Default: `2` for H2, `1.5` for H3. + +--- + +## Known Issues + +1. Inline math equations are aligned using a box to adjust its height. However, once it is wrapped inside a box, it becomes unbreakable, so inline long equations may fail to break lines. Also, it may break some packages that rely on the layout of the equation, like `fletcher`. Currently the workaround is that if there is no need to adjust its height, then don't wrap it in a box. This should work most of the time; for extra tall equations, manual adjusting might be needed. + +2. The Typst engine will try to automatically increase line height when a inline math equation is particularly tall or deep. This behaviour could sometimes cause the line to shift a little. To fix this, you can adjust the `texts.descender` field to a value such that tall equations are automatically handled by the template, rather than the Typst engine. For most scenario, the default value `0pt` should work decently except for some extreme cases. + +3. Theorem blocks rely on measuring the height of the sidenote. However the current measuring mechanism is somewhat broken and sometimes returns the wrong height for very long paragraphs, a little less than the actual height. Manual adjusting might be needed; you can use `v-step(size)` (size being an integer) to insert spaces. This should not be needed for short texts. diff --git a/packages/preview/obelisk/0.2.0/default.typ b/packages/preview/obelisk/0.2.0/default.typ new file mode 100644 index 0000000000..31cb13ce6f --- /dev/null +++ b/packages/preview/obelisk/0.2.0/default.typ @@ -0,0 +1,160 @@ +// Paper measures +#let width = 21cm +#let height = width * calc.sqrt(2) +#let def-paper = (width: width, height: height, two-sided: true) + +// Paper margins +#let t-margin = width / 9 +#let e-margin = width / 4 +#let f-margin = width / 6 +#let s-margin = width / 9 +#let def-margin = ( + t: t-margin, + f: f-margin, + e: e-margin, + s: s-margin, +) + +// Text body measures +#let t-width = width - s-margin - e-margin +#let t-height = height - t-margin - f-margin + +// Gutter and margin sidenotes +#let half-gutter = 0.3cm +#let e-margin-margin = s-margin / 3 + +// Text measures +#let text-size = 12pt +#let text-height = 10pt // Approximately the ascender height + +// Baseline grid +#let step = 16pt +#let step-num = calc.floor(t-height / step) +#let f-margin = height - t-margin - step-num * step // recalculate bottom margin to align with baseline grid + +#let def-texts = ( + size: text-size, + ascender: text-height, + step: step, +) +#let def-side = ( + half-gutter: half-gutter, + margin: e-margin-margin, +) + +// Fonts +#let body-font = "TeX Gyre Pagella" +#let math-font = "TeX Gyre Pagella Math" +#let sans-font = "Inter" +#let mono-font = "IBM Plex Mono" +#let def-fonts = ( + body: body-font, + math: math-font, + sans: sans-font, + mono: mono-font, +) + +#let def-headers = ( + h2: ( + sym: sym.circle.filled, + dy: 0pt, + size: 2, + ), + h3: ( + sym: sym.triangle.filled.small.b, + dy: 0pt, + size: 1.5, + ), +) + +#let def-deco = (line-number: true) + +#let rec-or-default(user, defaults) = { + if user == none { return defaults } + + if ( + type(user) != dictionary or type(defaults) != dictionary + ) { return defaults } + + for (key, value) in defaults { + if type(defaults.at(key)) == dictionary { + if not user.keys().contains(key) { + user.insert(key, value) + } else if type(user.at(key)) == dictionary { + user.at(key) = rec-or-default(user.at(key), value) + } + } else if ( + user.keys().contains(key) and user.at(key) == none + ) { + user.at(key) = value + } else if not user.keys().contains(key) { + user.insert(key, value) + } + } + return user +} + +#let or-default-settings( + paper, + margin, + side, + texts, + fonts, + deco, + headers, +) = { + let ( + paper, + margin, + side, + texts, + fonts, + deco, + headers, + ) = rec-or-default( + ( + paper: paper, + margin: margin, + side: side, + texts: texts, + fonts: fonts, + deco: deco, + headers: headers, + ), + ( + paper: def-paper, + margin: def-margin, + side: def-side, + texts: def-texts, + fonts: def-fonts, + deco: def-deco, + headers: def-headers, + ), + ) + let body = ( + width: paper.width - margin.s - margin.e, + height: paper.height - margin.t - margin.f, + ) + texts.step-num = calc.floor(body.height / texts.step) + texts.descender = 0pt + margin.f = ( + paper.height - margin.t - texts.step-num * texts.step + ) + body.height = paper.height - margin.t - margin.f + side.width = margin.e - side.margin - side.half-gutter + return ( + paper: paper, + body: body, + margin: margin, + side: side, + texts: texts, + fonts: fonts, + deco: deco, + headers: headers, + ) +} + +#let default-settings = state( + "obelisk:default-settings", + none, +) diff --git a/packages/preview/obelisk/0.2.0/headers.typ b/packages/preview/obelisk/0.2.0/headers.typ new file mode 100644 index 0000000000..cf513932ea --- /dev/null +++ b/packages/preview/obelisk/0.2.0/headers.typ @@ -0,0 +1,111 @@ +#import "default.typ": default-settings +#import "layout.typ": blank-page, place-node + +#let show-headers(it) = context { + let ( + paper, + margin, + fonts, + texts, + headers, + ) = default-settings.get() + + show heading: it => { + let step = texts.step + set block( + above: step * 3, + below: step * 2, + breakable: false, + ) + set par(leading: step * 2, justify: false) + set text(font: fonts.sans) + block(it) + } + + show heading.where(level: 1): it => context { + let step = texts.step + let two-sided = paper.two-sided + if two-sided { + blank-page(to: "odd", weak: true) + } else { + blank-page(weak: true) + } + let h1 = counter(heading).at(here()).at(0) + let boxed = box( + stroke: 0.75pt + luma(180), + fill: white, + width: 7cm, + height: 7cm, + align( + center + horizon, + text( + font: fonts.sans, + size: 120pt, + fill: white, + top-edge: "bounds", + bottom-edge: "bounds", + stroke: 1.5pt + luma(180), + [#h1], + ), + ), + ) + if two-sided { + place( + top + right, + dx: margin.e + 10pt, + dy: -margin.t - step - 10pt, + boxed, + ) + } else { + place( + top + left, + dx: -margin.e - 10pt, + dy: -margin.t - step - 10pt, + boxed + ) + } + + v(step * 12) + text( + size: texts.size * 3.5, + font: fonts.sans, + align(right)[ + #text(weight: "light")[Section #h1]\ #text( + weight: "extrabold", + it, + ) + ], + ) + v(step * 3) + } + + show heading.where(level: 2): it => { + let step = texts.step + set text(size: texts.size * headers.h2.size) + set block( + above: step * 3, + below: step * 2, + breakable: false, + ) + block(breakable: false)[#place-node( + $#headers.h2.sym$, + dy: headers.h2.dy, + ) #h(2pt)#box(it)] + } + + show heading.where(level: 3): it => { + let step = texts.step + set text(texts.size * headers.h3.size) + set block( + above: step * 3, + below: step * 2, + breakable: false, + ) + block(breakable: false)[#place-node( + $#headers.h3.sym$, + dy: headers.h3.dy, + ) #it] + } + + it +} diff --git a/packages/preview/obelisk/0.2.0/layout.typ b/packages/preview/obelisk/0.2.0/layout.typ new file mode 100644 index 0000000000..c41450bf4b --- /dev/null +++ b/packages/preview/obelisk/0.2.0/layout.typ @@ -0,0 +1,326 @@ +#import "default.typ": default-settings + +#let v-step(size) = context { + let def = default-settings.get() + v(size * def.texts.step) +} + +// A baseline-aligned block. +#let bblock(it, ..args) = context { + let def = default-settings.get() + let step = def.texts.step + let ascender = def.texts.ascender + + set text(top-edge: "bounds", bottom-edge: "bounds") + let height = measure({ + set text(top-edge: "bounds", bottom-edge: "bounds") + it + }).height + let desired-height = calc.round(height / step) + 1 + block( + height: desired-height * step, + inset: (top: (step - ascender) / 2), + align(horizon, it), + above: 0pt, + below: step, + ..args, + ) +} + + +#let true-metrics(eq) = { + let total = measure(text( + top-edge: "bounds", + bottom-edge: "bounds", + eq, + )) + let strut-height = 1000pt + let strut = box( + width: 0pt, + height: strut-height, + baseline: 0pt, + ) + + let combined = measure(text( + top-edge: "bounds", + bottom-edge: "bounds", + eq + strut, + )) + + let descender = combined.height - strut-height + let ascender = total.height - descender + + return ( + height: total.height, + ascender: ascender, + descender: descender, + ) +} + +#let place-side(it, dx: 0pt, dy: 0pt, ..args) = context { + let def = default-settings.get() + let t-width = def.body.width + let side-margin = def.side.margin + let side-width = def.side.width + let half-gutter = def.side.half-gutter + let e-margin = def.margin.e + let text-height = def.texts.ascender + let two-sided = def.paper.two-sided + + let page-num = here().page() + let move = if calc.even(page-num) or not two-sided { + -side-width - half-gutter + dx + } else { + t-width + half-gutter + dx + } + let flush = if calc.even(page-num) or not two-sided { + right + } else { + left + } + set par(justify: false) + let boxed = box( + width: side-width, + inset: ( + left: half-gutter, + right: half-gutter, + ), + outset: ( + top: half-gutter + text-height, + bottom: half-gutter, + ), + ..args, + align(flush, it), + ) + place(dx: move, dy: dy, boxed) +} + +#let place-node(sym, dy: 0pt) = context { + let def = default-settings.get() + let half-gutter = def.side.half-gutter + let two-sided = def.paper.two-sided + let page-num = here().page() + let width = measure(sym).width + let dx = if calc.even(page-num) or not two-sided { + half-gutter + width / 2 + } else { + -half-gutter - width / 2 + } + place-side(sym, dx: dx, dy: dy) +} + +#let small(it, scale: 2 / 3, size: 0.75em) = context { + let def = default-settings.get() + let step = def.texts.step * scale + set par(leading: step) + text(size, it) +} + +// automatic stroke side block. +#let init-sblock() = context { + let def = default-settings.get() + let width = def.paper.width + let height = def.paper.height + let e-margin = def.margin.e + let t-margin = def.margin.t + let f-margin = def.margin.f + let step = def.texts.step + let ascender = def.texts.ascender + let two-sided = def.paper.two-sided + + let page-num = counter(page).get().first() + let is-odd = calc.odd(page-num) and two-sided + + let all-starts = query() + let all-ends = query() + + // Get the total number of sblocks in the document + let total-blocks = calc.min(all-starts.len(), all-ends.len()) + + for id in range(total-blocks) { + let start-el = all-starts.at(id) + let start-loc = start-el.location() + let end-loc = all-ends.at(id).location() + let start-pos = start-loc.position() + let end-pos = end-loc.position() + + // If the block exists on the current page... + if ( + start-pos.page <= page-num and end-pos.page >= page-num + ) { + // Extract the unique custom style dictionary from the metadata + let config = start-el.value + let stroke-style = config.at( + "stroke", + default: 1pt + black, + ) + let offset = config.at("offset", default: 0.2cm) + let outset = config.at("outset", default: ( + left: 0pt, + right: 0pt, + top: 0pt, + bottom: 0pt, + )) + + let x-pos = if is-odd { + width - e-margin + offset + } else { + e-margin - offset + } + let y-start = ( + if start-pos.page == page-num { + start-pos.y + } else { + t-margin + step + } + - outset.top + ) + let y-end = ( + if end-pos.page == page-num { + end-pos.y + } else { + height - f-margin + } + + outset.bottom + ) + + place( + top + left, + dx: x-pos, + dy: y-start, + line( + start: (0pt, 0pt), + end: (0pt, y-end - y-start), + stroke: stroke-style, + ), + ) + } + } +} + +#let sblock( + stroke: 1.5pt + black, + offset: 0.2cm, + body, + outset: ( + left: 0pt, + right: 0pt, + top: 0pt, + bottom: 0pt, + ), + height: none, + ..args, +) = { + context { + block( + width: 100%, + breakable: true, + [ + #metadata(( + stroke: stroke, + offset: offset, + outset: outset, + )) + + #if height == none { + block( + body, + breakable: true, + ) + } else { + block( + body, + height: height, + breakable: true, + ) + } + #metadata(none) + + ], + outset: outset, + ) + } +} + +#let theorem-render( + env, + name, + numstr, + it, + color: rgb("#A63A2B"), +) = context { + let def = default-settings.get() + let margin-w = def.side.width + let sans-font = def.fonts.sans + let step = def.texts.step + let ascender = def.texts.ascender + let t-width = def.body.width + let half-gutter = def.side.half-gutter + + let page-num = here().page() + let side = block( + width: def.side.width, + text( + fill: color, + font: sans-font, + [*#env #numstr*\ #if name != none { name }], + ), + ) + let s-height = measure(side).height + let bottom-out = step - ascender + let body-stroke = color + 3pt + let body = block( + [#place-side(side) #it], + width: t-width, + breakable: true, + ) + let b-height = measure(body).height + sblock( + body, + height: if s-height > b-height { + s-height + } else { + none + }, + stroke: body-stroke, + offset: half-gutter, + outset: ( + left: half-gutter, + right: half-gutter, + top: ascender + bottom-out, + bottom: bottom-out, + ), + ) +} + +#let sidenote(it, dy: 0pt) = context { + let def = default-settings.get() + let two-sided = def.paper.two-sided + let page-num = here().page() + let posx = here().position().x + sym.wj + let dx = if calc.even(page-num) or not two-sided { + -posx + def.margin.e + } else { + -posx + def.margin.s + } + box(place-side(small(it), dx: dx, dy: dy)) + h(0pt, weak: true) +} + +#let blank-page(..args) = context { + let mono-font = default-settings.get().fonts.mono + set page( + background: { + v(2fr) + align(center, text( + font: mono-font, + fill: luma(150), + )[\/\/ This page is intentionally left blank.]) + v(3fr) + }, + footer: none, + header: none, + ) + pagebreak(..args) +} diff --git a/packages/preview/obelisk/0.2.0/lib-inner.typ b/packages/preview/obelisk/0.2.0/lib-inner.typ new file mode 100644 index 0000000000..44ab053173 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/lib-inner.typ @@ -0,0 +1,255 @@ +#import "layout.typ": * +#import "headers.typ": * +#import "default.typ": ( + def-deco, def-fonts, def-headers, def-margin, def-paper, + def-side, def-texts, default-settings, + or-default-settings, +) +#import "theorem.typ": * + +#import "@preview/hydra:0.6.3": anchor, hydra + +#let hydra = hydra.with(use-last: true) + +#let init( + it, + paper: def-paper, + margin: def-margin, + side: def-side, + texts: def-texts, + fonts: def-fonts, + deco: def-deco, + headers: def-headers, +) = { + let default = or-default-settings( + paper, + margin, + side, + texts, + fonts, + deco, + headers, + ) + default-settings.update(_ => default) + let ( + paper, + margin, + side, + body, + fonts, + texts, + deco, + headers, + ) = default + let paper-margin = if paper.two-sided { + ( + top: margin.t + texts.step, + outside: margin.e, + inside: margin.s, + bottom: margin.f, + ) + } else { + ( + top: margin.t + texts.step, + left: margin.e, + right: margin.s, + bottom: margin.f, + ) + } + set page( + margin: paper-margin, + background: context { + let page-num = here().page() + let dx = if ( + calc.even(page-num) or not paper.two-sided + ) { + margin.e - side.half-gutter + } else { + paper.width - margin.e + side.half-gutter + } + place( + top + left, + dx: dx, + dy: 0pt, + line( + start: (0%, 0%), + end: (0%, 100%), + stroke: 0.75pt + luma(180), + ), + ) + // place( + // top + left, + // dx: margin.e, + // dy: margin.t, + // grid( + // columns: (body.width,), + // stroke: (bottom: 0.5pt + gray), + // ..(block(width: 100%, height:texts.step),) * 42 + // ), + // ) + if deco.line-number { + let dx = if ( + calc.even(page-num) or not paper.two-sided + ) { + paper.width - margin.s + side.half-gutter + } else { + margin.s - side.half-gutter - 3pt + } + let rot = if ( + calc.even(page-num) or not paper.two-sided + ) { + 90deg + } else { + -90deg + } + set text( + font: fonts.mono, + features: ("tnum",), + size: 0.66em, + fill: luma(210), + ) + for i in range(texts.step-num) { + place( + dx: dx, + dy: margin.t + + texts.step * (i + 1) + - texts.ascender / 4, + rotate(rot, [#( + "0123456789ABCDEF".at(calc.rem-euclid(i, 16)) + )]), + ) + } + } + init-sblock() + }, + footer: context { + let page-num = here().page() + let pnum-text = text( + font: fonts.mono, + fill: luma(180), + )[[ #text(fill: luma(60))[#page-num] ]] + let pnum-width = measure(pnum-text).width + + let header-text = text( + font: fonts.mono, + fill: luma(210), + if calc.odd(page-num) and paper.two-sided { + context hydra(2) + " " + } else { + "" + context hydra(1) + }, + ) + + let dx = -body.width / 2 - side.half-gutter + place( + top + center, + dx: if calc.even(page-num) or not paper.two-sided { + dx + } else { + -dx + }, + dy: side.half-gutter, + box( + fill: white, + outset: ( + top: side.half-gutter + texts.ascender, + bottom: side.half-gutter, + ), + pnum-text, + ), + ) + + let dx = -side.half-gutter + pnum-width / 2 + let flush = if ( + calc.odd(page-num) and paper.two-sided + ) { + right + } else { + left + } + place( + top + flush, + dx: if calc.even(page-num) or not paper.two-sided { + dx + } else { + -dx + }, + dy: side.half-gutter, + header-text, + ) + }, + header: anchor(), + ) + + set text( + font: fonts.body, + size: texts.size, + top-edge: "baseline", + bottom-edge: "baseline", + ) + + show math.equation: set text(font: fonts.math) + + set par( + leading: texts.step, + spacing: texts.step * 2, + justify: true, + ) + + show math.equation.where(block: true): it => { + set block(breakable: true) + set par(leading: texts.step / 2) + bblock(it, breakable: true) + } + + set heading(numbering: (..levels) => { + let parts = levels.pos() + if parts.len() > 1 { + // For Level 2+ headings (e.g., == Subsection) + parts.map(str).join(".") + } else { + none + } + }) + + show math.equation.where(block: false): it => context { + let ( + height: h, + ascender: ht, + descender: hb, + ) = true-metrics(it) + let gt = ( + calc.max(calc.ceil((ht - texts.step + texts.descender) / texts.step), 0) + * texts.step + ) + let gb = ( + calc.max( + calc.ceil( + (hb - texts.step + texts.ascender) / texts.step, + ), + 0, + ) + * texts.step + ) + + // FIXME: if we wrap the equation in a box, it becomes unbreakable. The box may also break some diagram packages like fletcher. + // This is a temporary fix that should work most of the time: when there is no adjustment needed, don't wrap the equation. + if gt + gb == 0pt { + it + } else { + box( + // height: gt + gb, + inset: (top: gt - ht, bottom: gb - hb), + // stroke: blue, + text(top-edge: "bounds", bottom-edge: "bounds", it), + // outset: (top: texts.step), + ) + } + } + + show: show-headers + + show: init-theorem + + it +} diff --git a/packages/preview/obelisk/0.2.0/lib.typ b/packages/preview/obelisk/0.2.0/lib.typ new file mode 100644 index 0000000000..964e580e00 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/lib.typ @@ -0,0 +1,8 @@ +#import "lib-inner.typ": ( + bblock, blank-page, default-settings, init, sidenote, + v-step, +) +#import "theorem.typ": ( + corollary, definition, example, lemma, make-environment, + proof, proposition, remark, theorem, +) diff --git a/packages/preview/obelisk/0.2.0/template/main.typ b/packages/preview/obelisk/0.2.0/template/main.typ new file mode 100644 index 0000000000..973d8097b6 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/template/main.typ @@ -0,0 +1,25 @@ +#import "@preview/obelisk:0.2.0": * + +#show: init + += Obelisk Template + +#definition[ + *Obelisk* is a note template inspired by Bauhaus aesthetics. +] + +== Second Level Header + +All texts are by default aligned to a baseline grid. + +$ integral_0^(+oo) "e"^(-x^2) dif x=sqrt(pi)/2 $ + +=== Third Level Header + +The template includes several theorem environments by default. #sidenote[inline sidenotes can be added] Theorems can have titles and referenced. + +#theorem[mean inequality][ + One of the mean inequalities is $n/(sum_(i=1)^n 1/x_i)<=root(n, product_(i=1)^n x_i).$ +] + +Theorems can be referenced by numbering (@thm:mean-ineq) or by title (@thm:mean-ineq[!]). diff --git a/packages/preview/obelisk/0.2.0/theorem.typ b/packages/preview/obelisk/0.2.0/theorem.typ new file mode 100644 index 0000000000..736e100026 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/theorem.typ @@ -0,0 +1,159 @@ +#import "layout.typ": * + +#let init-theorem(it) = { + // 1. Suppress default figure layout for theorems so we only see your custom styling + show figure.where(kind: "theorem"): it => align( + left, + it.body, + ) + + // 2. Reset the shared theorem counter every time a top-level section (Heading 1) begins + show heading.where(level: 1): it => { + counter(figure.where(kind: "theorem")).update(0) + it + } + + // 3. Custom reference logic for @tag and @tag[!] + show ref: it => { + let el = it.element + if ( + el != none + and el.func() == figure + and el.kind == "theorem" + ) { + let loc = el.location() + + // Calculate the "x.x" numbering string + let h1 = counter(heading).at(loc).at(0, default: 0) + let thm-num = counter(figure.where(kind: "theorem")) + .at(loc) + .at(0) + let num-str = if h1 == 0 { str(thm-num) } else { + str(h1) + "." + str(thm-num) + } + + // Handle @tag[!] + if it.supplement == [!] { + if el.caption != none { + // Return the title as the clickable link + return link(loc, el.caption.body) + } else { + // Fallback if no title was provided + return link(loc, [#el.supplement #num-str]) + } + } else { + // Standard @tag will now use the full-name (supplement) + return link(loc, [#el.supplement #num-str]) + } + } + return it + } + it +} + +// Make a theorem environment. +#let make-environment( + full-name, + short-name, + accent-color, +) = { + return (..args) => { + let pos = args.pos() + let title = none + let body = none + + if pos.len() == 1 { + body = pos.at(0) + } else if pos.len() == 2 { + title = pos.at(0) + body = pos.at(1) + } else { + panic( + "Theorem environments expect exactly 1 or 2 positional arguments.", + ) + } + + figure( + kind: "theorem", + supplement: full-name, // Injects the full name (e.g., "Theorem") into the reference + caption: title, + outlined: false, + )[ + #context { + let loc = here() + let h1 = counter(heading).at(loc).at(0) + let thm-num = counter(figure.where(kind: "theorem")) + .at(loc) + .at(0) + let num-str = str(h1) + "." + str(thm-num) + + theorem-render( + short-name, + title, + num-str, + body, + color: accent-color, + ) + } + ] + } +} + +#let theorem = make-environment( + "Theorem", + "THM", + rgb("#1e3a8a"), +) +#let lemma = make-environment( + "Lemma", + "LEM", + rgb("#065f46"), +) +#let proposition = make-environment( + "Proposition", + "PROP", + rgb("#5b21b6"), +) +#let corollary = make-environment( + "Corollary", + "COR", + rgb("#0e7490"), +) +#let definition = make-environment( + "Definition", + "DEF", + rgb("#991b1b"), +) +#let example = make-environment( + "Example", + "EX", + rgb("#475569"), +) +#let remark = make-environment( + "Remark", + "REM", + rgb("#854d0e"), +) + +#let proof(..args) = { + let pos = args.pos() + let title = none + let body = none + + if pos.len() == 1 { + body = pos.at(0) + } else if pos.len() == 2 { + title = pos.at(0) + body = pos.at(1) + } else { + panic( + "Proof environment expect exactly 1 or 2 positional arguments.", + ) + } + block( + width: 100%, + breakable: true, + )[ + _Proof#if title != none { [ #title] }._ #body #h(1fr) #sym.square + ] +} diff --git a/packages/preview/obelisk/0.2.0/thumbnail.png b/packages/preview/obelisk/0.2.0/thumbnail.png new file mode 100644 index 0000000000..26cc582c1a Binary files /dev/null and b/packages/preview/obelisk/0.2.0/thumbnail.png differ diff --git a/packages/preview/obelisk/0.2.0/typst.toml b/packages/preview/obelisk/0.2.0/typst.toml new file mode 100644 index 0000000000..f359137447 --- /dev/null +++ b/packages/preview/obelisk/0.2.0/typst.toml @@ -0,0 +1,15 @@ +[package] +name = "obelisk" +version = "0.2.0" +entrypoint = "lib.typ" +authors = ["atzlt"] +license = "MIT" +description = "A high-precision, print-ready, Bauhaus-inspired note-taking template." +categories = ["book", "layout"] +disciplines = ["mathematics", "physics", "computer-science"] +repository = "https://github.com/atzlt/obelisk-template" + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png"