diff --git a/packages/preview/scholia/0.1.0/LICENSE b/packages/preview/scholia/0.1.0/LICENSE new file mode 100644 index 0000000000..bdda43cbe1 --- /dev/null +++ b/packages/preview/scholia/0.1.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eric Yang (杨星宇) + +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/scholia/0.1.0/README.md b/packages/preview/scholia/0.1.0/README.md new file mode 100644 index 0000000000..e539405cc4 --- /dev/null +++ b/packages/preview/scholia/0.1.0/README.md @@ -0,0 +1,113 @@ +# Scholia + +Fill-in study notes for STEM: read one layer, *fill* the other. Theorem **knots**, +intuition **notes**, margin **threads**, and `fillin` blanks — in light or dark. + +![Scholia, in light and dark](gallery/preview.png) + +Most notes are read-only and forgotten by the next page. Scholia typesets the +*active* layer too: statements and intuition to take in, blanks and proof-skeletons +to work out. The source is your answer key. + +> Start a new notebook with `typst init @preview/scholia`, or import the library: + +## Use + +```typ +#import "@preview/scholia:0.1.0": * + +// options: theme: "light" | "dark" · prose: "notes" | "book" · fonts: (…) +#show: scholia + +#cover("Title", subtitle: "subtitle", author: "Your Name", date: "2026") + += First course + +#note[The big idea, in your own words. The passive layer.] + +#definition[a name][State the object, leave a clause blank: #fillin(width: 3cm).] + +#theorem[attribution][State the result, then refer back to @def:thing.] +#proof[Skeleton: (i) … (ii) … #TODO[the step that makes it work]] + +#yourturn[Restage it as your own computation. #workspace(n: 3)] +``` + +Knots are built on [`@preview/frame-it`](https://typst.app/universe/package/frame-it): +each is a `figure`, so they get **native `@label` cross-references** and shared, +section-tied numbering for free. The title is the first argument, the body the +last: `#theorem[title][body]` (omit the title with `#theorem[body]`). An optional +middle slot is a small **tag** — a source or claim-ID, styled by `label-it`: +`#theorem[title][GSM 211, Thm 2.16][body]`. + +Compile [`examples/example.typ`](examples/example.typ) to see every device in one +mini-chapter. It defaults to the light `notes` theme; uncomment the `theme: "dark"` +or `prose: "book"` line at the top to see the other modes. + +## The toolkit + +| you want… | you write… | +|---|---| +| the cover | `cover(title, subtitle: …, author: …, date: …, kicker: …)` | +| a result (blue) | `theorem[t][b]` · `lemma` · `proposition` · `corollary` · `conjecture` · `claim` | +| a definition (teal) | `definition[t][b]` · `axiom` · `assumption` · `notation` | +| an example / remark (amber / plain) | `example[t][b]` · `remark[b]` | +| cross-reference a knot | label with ``, refer with `@thm:x` (native) | +| a source / claim-ID tag | a middle slot `theorem[t][tag][b]`, or `label-it[…]` inline | +| the intuition voice | `note[…]` · `whisper[…]` · `keyword[…]` | +| a blank to fill | `fillin(width: 2.2cm)` | +| a proof gap | `TODO[the missing step]` | +| a do-it-yourself box | `yourturn[…]` + `workspace(n: 3)` | +| a margin note | `sidenote[…]` · `recall[…]` (the `?` preset) | + +## Options (`#show: scholia.with(…)`) + +| option | values | effect | +|---|---|---| +| `theme` | `"light"` (default) · `"dark"` | swaps the whole colour card — page, text, knots, accents (dark = slate) | +| `prose` | `"notes"` (default) · `"book"` | notes = no indent + paragraph spacing; book = first-line indent + tight | +| `paper` | `"a4"` (default) · `"us-letter"` · … | page size | +| `fonts` | dict overriding `default-fonts` | e.g. `(heading: ("Gill Sans",))` | + +| Light | Dark | +|:---:|:---:| +| ![Cover, light theme](gallery/cover-light.png) | ![Cover, dark theme](gallery/cover-dark.png) | + +## Fonts + +Every role is a **fallback list — the nice face first, an embedded face last** — so +output never breaks and degrades gracefully when a font is missing: + +| role | default list | +|---|---| +| body | `("STIX Two Text", "Libertinus Serif")` | +| math | `("STIX Two Math", "New Computer Modern Math")` | +| heading | `("Optima", "Libertinus Serif")` | +| sans (intuition voice) | `("Avenir Next", "Libertinus Serif")` | +| mono | `("Menlo", "DejaVu Sans Mono")` | + +A Typst package cannot ship fonts, so the universal baseline is Typst's four +embedded faces (Libertinus Serif, New Computer Modern + Math, DejaVu Sans Mono). +For the intended look install **STIX Two** (Text + Math, free/OFL); on macOS, Optima +and Avenir Next are already present. Override any role via `fonts:`, or retune the +defaults in [`src/fonts.typ`](src/fonts.typ). + +## Known limitations + +- Subsection / subsubsection are styled blocks, not true run-in headings. +- A `fonts:` override reaches body / math / heading; the `note` voice and knot labels + still read `default-fonts` directly (full theming is a later pass). + +## Internals + +`src/` is modular: [`colors.typ`](src/colors.typ) (palettes + theme state) · +[`fonts.typ`](src/fonts.typ) · [`visual.typ`](src/visual.typ) · +[`theorems.typ`](src/theorems.typ) (frame-it knots) · +[`lib.typ`](src/lib.typ) (pedagogy, margins, cover, wrapper). + +## License & credits + +MIT © Eric Yang. An independent Typst reimplementation, inspired by the design of the +[loom-notes](https://github.com/Polaris-Aeterna/loom-notes) XeLaTeX project (no source +code reused). Example notes restate cited results for educational use; the mathematics +belongs to its authors. diff --git a/packages/preview/scholia/0.1.0/examples/example.typ b/packages/preview/scholia/0.1.0/examples/example.typ new file mode 100644 index 0000000000..40115f2295 --- /dev/null +++ b/packages/preview/scholia/0.1.0/examples/example.typ @@ -0,0 +1,181 @@ +// example.typ — the Scholia showcase. Compile THIS file to see every device. +// For dark or book mode, uncomment an option below (mirrors template/main.typ). +#import "@preview/scholia:0.1.0": * + +#show: scholia.with( + // theme: "dark", // light (default) | dark (slate) + // prose: "book", // notes (default) | book (first-line indent, tight) +) + +#cover( + "Descent", + subtitle: "A fill-in primer on convexity and first-order methods", + author: "Eric Yang", + date: datetime.today().display("[month repr:long] [day], [year]"), + kicker: "Optimization · Notebook 4", +) + +// =========================================================================== +// NOTATION AND SETUP +// Shows: notation, axiom, assumption, keyword, sidenote, recall +// =========================================================================== + += Notation and Setup + +Before the main arguments, we fix symbols and state the #keyword[standing hypotheses] +that hold throughout without being repeated. + +#notation[Throughout this chapter][ + $x, y, z in bb(R)^n$ are vectors; $f, g: bb(R)^n -> bb(R)$ are real-valued. + The Euclidean inner product is $x^top y$ and the induced norm $||x|| = sqrt(x^top x)$. + We write $f^* = inf_x f(x)$ for the global minimum value and $x^*$ for any minimiser. +] + +#axiom[Finite-dimensional Hilbert space][ + $bb(R)^n$ is equipped with the standard inner product $x^top y$, which is bilinear, + symmetric, and positive-definite. Every bounded sequence has a convergent subsequence + (Bolzano–Weierstrass). +] + +#assumption[Regularity][ + Unless stated otherwise, $f$ is #keyword[convex], continuously differentiable, and + bounded below: a minimiser $x^*$ exists so that $f^* > -infinity$. +] + +#sidenote[Non-differentiable convex $f$ needs *subgradients* — the subject of a later notebook.] + +#recall[Why must we assume $f^* > -infinity$? Give a convex $f$ for which it fails.] + +// =========================================================================== +// CONVEX FUNCTIONS +// Shows: note + whisper, definition, lemma, cross-ref @, block-head, trigger +// =========================================================================== + += Convex Functions + +A function is #keyword[convex] when its graph never climbs above any of its chords. +That single picture — a bowl with no false bottoms — is why some optimisation is easy +and the rest is a fight to make problems look like this one. + +#note[ + Read this layer for the shape of the idea; fill the blanks and close the proof gaps + to make it yours. #whisper[The margin is where you argue back with the text.] +] + +#definition[convex function][ + $f: bb(R)^n -> bb(R)$ is #keyword[convex] if, for every $x, y$ and $lambda in [0, 1]$, + $ f(lambda x + (1 - lambda) y) <= lambda f(x) + (1 - lambda) f(y). $ + It is #keyword[strictly convex] when the inequality is strict for $x != y, lambda in (0,1)$. +] + +#recall[Which way does the inequality face — and what becomes of it for concave $f$?] + +== The first-order picture + +Once $f$ is differentiable, convexity wears a cleaner face: every tangent plane is a +global _underestimate_ of the function. + +#lemma[first-order condition][ + $f$ is convex if and only if, for all $x, y$, + $ f(y) >= f(x) + nabla f(x)^top (y - x). $ +] + +#block-head[Reading the gradient] +The gradient $nabla f(x)$ is the slope of the best linear underestimate at $x$. +A point where $nabla f(x) = 0$ is therefore not merely flat — by @lem:fo it is a +*global* minimum. Convexity turns a local check into a global guarantee. + +#trigger[Reach for @lem:fo whenever you must turn "it's convex" into an inequality.] + +// =========================================================================== +// GRADIENT DESCENT +// Shows: definition (L-smooth), theorem + tag badge, proof + claim + TODO, +// corollary, table + label-it, proposition, conjecture, +// example, yourturn + fillin + workspace, sidenote, remark +// =========================================================================== + += Gradient Descent + +#definition[$L$-smoothness][ + $f$ is #keyword[$L$-smooth] if its gradient is $L$-Lipschitz: + $ ||nabla f(x) - nabla f(y)|| <= L ||x - y|| quad "for all" x, y. $ +] + +Two assumptions now sit on the table — convex (a global shape) and smooth (a local +speed limit on gradients). Together they are exactly what a first-order method needs +to make promises. + +// The middle slot [Nesterov §2.1] is a visible source badge, independent of . +#theorem[descent lemma][Nesterov §2.1][ + If $f$ is $L$-smooth then, for all $x, y$, + $ f(y) <= f(x) + nabla f(x)^top (y - x) + L / 2 ||y - x||^2. $ +] + +#proof[ + Write the remainder $r(y) = f(y) - f(x) - nabla f(x)^top(y-x)$ as an integral. + + #claim[ + $r(y) = integral_0^1 [nabla f(x + t(y-x)) - nabla f(x)]^top (y-x) d t.$ + ] + + Apply the triangle inequality, then bound the bracketed gradient difference + using @def:smooth. + #TODO[carry out the Lipschitz bound on $||nabla f(x + t(y-x)) - nabla f(x)||$] +] + +#corollary[the $O(1 slash t)$ rate][ + On a convex, $L$-smooth $f$, gradient descent $x_(t+1) = x_t - eta nabla f(x_t)$ + with step $eta = 1 slash L$ satisfies + $ f(x_t) - f^* <= (||x_0 - x^*||^2) / (2 eta t). $ + Bounded suboptimality, no luck required — it leans on @thm:descent and @lem:fo. +] + +The cost of each assumption is legible in the rate it buys: + +#table( + columns: (1fr, auto), + stroke: none, + table.header[#label-it[assumptions]][#label-it[rate of $f(x_t) - f^*$]], + table.hline(stroke: 0.4pt), + [convex + $L$-smooth], [$O(1 slash t)$], + [$mu$-strongly convex + $L$-smooth], [$O((1 - mu slash L)^t)$ — linear], +) + +#proposition[strong convexity buys speed][ + If $f$ is additionally $mu$-strongly convex, the rate of @cor:rate sharpens to linear: + $f(x_t) - f^* <= (1 - mu slash L)^t (f(x_0) - f^*)$. +] + +#conjecture[lower bound on first-order methods][ + No deterministic first-order method on convex, $L$-smooth objectives can achieve + convergence faster than $O(1 slash t)$ per gradient query without momentum. + #whisper[Nesterov's accelerated gradient achieves $O(1 slash t^2)$ under the same + assumptions — suggesting the tight lower bound is below $O(1 slash t)$. The exact + constant is still an active research question.] +] + +#example[one step on a quadratic][ + For $f(x) = 1/2 x^top A x$ with $0 prec.eq A prec.eq L I$, the gradient step + $x_1 = x_0 - eta A x_0$ gives the error contraction + $ ||x_1 - x^*|| = ||I - eta A|| dot ||x_0 - x^*||. $ + With $x^* = 0$: the worst-case shrink factor is $max(|1 - eta lambda_"min"|, |1 - eta lambda_"max"|)$. +] + +#yourturn[ + Derive the contraction yourself. Expand $||x_(t+1) - x^*||^2$ for the quadratic + $f(x) = 1/2 x^top A x$, then find the step $eta$ that minimises the contraction factor. + What is the optimal $eta$? + $ eta^* = #fillin(width: 2cm) $ + #workspace(n: 3) +] + +#sidenote[What fails when $f$ is convex but *not* smooth? Subgradients survive, +but the rate slows to $O(1 slash sqrt(t))$.] + +=== Stochastic steps, in one line + +#remark[ + Replace $nabla f$ with any unbiased gradient estimate and the same convergence + machinery survives in expectation — the quiet engine inside essentially every modern + learning system. +] diff --git a/packages/preview/scholia/0.1.0/gallery/cover-dark.png b/packages/preview/scholia/0.1.0/gallery/cover-dark.png new file mode 100644 index 0000000000..d5df3b44bf Binary files /dev/null and b/packages/preview/scholia/0.1.0/gallery/cover-dark.png differ diff --git a/packages/preview/scholia/0.1.0/gallery/cover-light.png b/packages/preview/scholia/0.1.0/gallery/cover-light.png new file mode 100644 index 0000000000..843bf2729b Binary files /dev/null and b/packages/preview/scholia/0.1.0/gallery/cover-light.png differ diff --git a/packages/preview/scholia/0.1.0/gallery/preview.png b/packages/preview/scholia/0.1.0/gallery/preview.png new file mode 100644 index 0000000000..0a240473dc Binary files /dev/null and b/packages/preview/scholia/0.1.0/gallery/preview.png differ diff --git a/packages/preview/scholia/0.1.0/src/colors.typ b/packages/preview/scholia/0.1.0/src/colors.typ new file mode 100644 index 0000000000..ab7ddbaad3 --- /dev/null +++ b/packages/preview/scholia/0.1.0/src/colors.typ @@ -0,0 +1,44 @@ +// Color palettes + active-theme state. +// Two cards switch seamlessly via `scholia.with(theme: "light" | "dark")`. +// Scheme: "cool modern STEM" — blue (theorem) / teal (definition) / amber (example). +// +// Roles: bg (page) · ink (body) · muted (secondary) · hairline (faint rules) · +// thm/def/eg (knot accents) · rule (section rule) · tag · keyword. + +#let palettes = ( + light: ( + dark: false, + bg: white, + ink: rgb("#16202E"), + muted: rgb("#6B7686"), + hairline: rgb("#D2D8E0"), + thm: rgb("#2F5EA8"), + def: rgb("#0F766E"), + eg: rgb("#B45309"), + rule: rgb("#2F5EA8"), + tag: rgb("#0F766E"), + keyword: rgb("#0F766E"), + ), + dark: ( + dark: true, + bg: rgb("#141B27"), // deep blue-grey slate + ink: rgb("#E3E8F0"), + muted: rgb("#8893A4"), + hairline: rgb("#2A3340"), + thm: rgb("#74A6FF"), + def: rgb("#54C8B6"), + eg: rgb("#E0A458"), + rule: rgb("#74A6FF"), + tag: rgb("#54C8B6"), + keyword: rgb("#54C8B6"), + ), +) + +// the active palette; the document wrapper updates it from the `theme` option. +#let active = state("scholia-palette", palettes.light) + +// a soft box fill: blend an accent toward the page background. +#let tint(p, c, strength: auto) = { + let s = if strength != auto { strength } else if p.dark { 16% } else { 9% } + color.mix((c, s), (p.bg, 100% - s)) +} diff --git a/packages/preview/scholia/0.1.0/src/fonts.typ b/packages/preview/scholia/0.1.0/src/fonts.typ new file mode 100644 index 0000000000..34eac50344 --- /dev/null +++ b/packages/preview/scholia/0.1.0/src/fonts.typ @@ -0,0 +1,23 @@ +// Font roles as fallback lists: the "nice" face first, an embedded face last. +// Typst embeds Libertinus Serif, New Computer Modern (+ Math), DejaVu Sans Mono — +// so every list degrades to something that exists everywhere, zero install. +// (A Typst package cannot ship fonts; downstream users only have embedded + +// their own system fonts. Missing first choices just warn and fall back.) + +#let default-fonts = ( + body: ("STIX Two Text", "Libertinus Serif"), + math: ("STIX Two Math", "New Computer Modern Math"), + heading: ("Optima", "Libertinus Serif"), + sans: ("Avenir Next", "Libertinus Serif"), // the informal "intuition voice" + mono: ("Menlo", "DejaVu Sans Mono"), + cjk: ("Songti SC",), // appended after body/sans for 中文 glyph fallback +) + +// shallow-merge user overrides over the defaults +#let resolve-fonts(overrides) = { + let f = default-fonts + if overrides != none { + for (k, v) in overrides { f.insert(k, v) } + } + f +} diff --git a/packages/preview/scholia/0.1.0/src/lib.typ b/packages/preview/scholia/0.1.0/src/lib.typ new file mode 100644 index 0000000000..e6a9f6305a --- /dev/null +++ b/packages/preview/scholia/0.1.0/src/lib.typ @@ -0,0 +1,181 @@ +// Scholia — fill-in study notes for STEM (AI & math). +// A Typst reimplementation inspired by the loom-notes design (MIT). No source reused. +// +// knot = a result (theorem box, via frame-it) +// note = the intuition voice +// sidenote = a Tufte-style margin note (recall = the "?" active-recall preset) + +#import "@preview/marginalia:0.3.1" as marginalia: note as _note +#import "colors.typ": * +#import "fonts.typ": * +#import "visual.typ": * +#import "theorems.typ": * + +// =========================================================================== +// THE INTUITION VOICE +// =========================================================================== +#let note(body) = context { + let p = active.get() + block( + width: 100%, breakable: true, fill: tint(p, p.thm), + stroke: (left: 1.6pt + p.thm), above: 8pt, below: 8pt, + inset: (left: 11pt, right: 9pt, top: 6pt, bottom: 6pt), + )[ + #set text(font: (..default-fonts.sans, ..default-fonts.cjk), size: 0.92em, fill: p.ink) + #body + ] +} + +#let whisper(body) = context text( + font: (..default-fonts.sans, ..default-fonts.cjk), size: 0.92em, fill: active.get().thm, +)[#body] +#let keyword(body) = context text(weight: "bold", fill: active.get().keyword)[#body] + +// =========================================================================== +// PEDAGOGY — fill-in study notes +// =========================================================================== +#let block-head(body) = context block(above: 1em, below: 0.5em)[ + #text(font: default-fonts.heading, weight: "bold", fill: active.get().thm)[#body] +] + +#let trigger(body) = context block(above: 0.5em, below: 0.5em)[ + #text(font: default-fonts.heading, weight: "bold", fill: active.get().def)[Trigger.] #h(0.3em)#body +] + +#let TODO(hint) = context text(font: default-fonts.sans, size: 0.92em, fill: active.get().eg)[[ fill in: #hint ]] + +#let fillin(width: 2.2cm) = context box(width: width, height: 0.9em, stroke: (bottom: 0.6pt + active.get().ink)) + +#let yourturn(body) = context { + let p = active.get() + block( + width: 100%, breakable: true, fill: tint(p, p.eg), + stroke: (left: 2pt + p.eg), above: 9pt, below: 9pt, + inset: (left: 11pt, right: 10pt, top: 7pt, bottom: 8pt), + )[#text(font: default-fonts.heading, weight: "bold", fill: p.eg)[Your turn.] #h(0.3em)#body] +} + +#let workspace(n: 3) = context { + let p = active.get() + v(1pt) + for _ in range(n) { + line(length: 100%, stroke: 0.3pt + p.hairline) + v(9pt) + } +} + +// =========================================================================== +// MARGIN NOTES (right selvage, via marginalia, no number marker) +// =========================================================================== +// one Tufte-style margin note (unnumbered). An optional leading `symbol` marks +// its intent; colour follows the theme accent. +#let sidenote(body, symbol: none) = _note(counter: none)[ + #context { + let p = active.get() + set text(font: (..default-fonts.sans, ..default-fonts.cjk), size: 0.82em, fill: p.muted) + set par(leading: 0.5em) + if symbol != none { text(fill: p.thm, weight: "bold")[#symbol]; h(0.3em) } + body + } +] + +// active-recall prompt: a margin note marked with "?" +#let recall(body) = sidenote(body, symbol: [?]) + +// (cross-references use Typst's native labels: put `` after a knot, `@lbl` to refer.) + +// =========================================================================== +// THE COVER +// =========================================================================== +// a pure-typographic cover. symmetric margins so it centres on the physical +// page (not the note-offset body box marginalia sets up). `kicker` is an +// optional small tracked line above the title (course code, series, ∇·, …). +#let cover(title, subtitle: none, author: none, date: none, kicker: none) = { + context { + let p = active.get() + page(fill: p.bg, background: none, footer: none, header: none, margin: (x: 2.5cm, top: 2cm, bottom: 2.3cm))[ + #set align(center) + #v(1fr) + #if kicker != none { + text(font: default-fonts.heading, size: 10pt, fill: p.muted, tracking: 3pt)[#upper(kicker)] + v(0.9cm) + } + #text(font: default-fonts.heading, weight: "bold", size: 30pt, fill: p.thm, tracking: 1.5pt)[#title] + #v(0.5cm) + #line(length: 32%, stroke: 1pt + p.rule) + #if subtitle != none { + v(0.5cm) + text(font: (..default-fonts.sans, ..default-fonts.cjk), size: 13pt, fill: p.muted)[#subtitle] + } + #v(1fr) + #if author != none { + text(font: (..default-fonts.sans, ..default-fonts.cjk), size: 11pt, fill: p.ink, tracking: 0.5pt)[#author] + } + #if date != none { + v(0.3em) + text(font: default-fonts.sans, size: 9pt, fill: p.muted, tracking: 0.5pt)[#date] + } + #v(1.6cm) + ] + } + counter(page).update(1) +} + +// =========================================================================== +// THE DOCUMENT WRAPPER +// =========================================================================== +// theme: "light" (default) | "dark" (slate) +// prose: "notes" (no indent, para spacing) | "book" (first-line indent, tight) +// fonts: overrides merged over `default-fonts` +#let scholia(theme: "light", prose: "notes", paper: "a4", fonts: none, body) = { + let p = palettes.at(theme) + let f = resolve-fonts(fonts) + let hfont = f.heading + active.update(p) + + set page( + paper: paper, + fill: p.bg, + footer: context { + set text(font: f.sans, size: 0.8em, fill: p.muted) + align(right)[#counter(page).display()] + }, + ) + show: marginalia.setup.with( + inner: (far: 6mm, width: 12mm, sep: 6mm), + outer: (far: 6mm, width: 26mm, sep: 7mm), + top: 2cm, bottom: 2.3cm, + ) + + set text(font: (..f.body, ..f.cjk), size: 11pt, fill: p.ink, lang: "en") + show math.equation: set text(font: f.math) + show raw: set text(font: f.mono) + set par( + justify: true, + leading: 0.72em, + first-line-indent: if prose == "book" { 1.2em } else { 0pt }, + spacing: if prose == "book" { 0.7em } else { 0.95em }, + ) + set heading(numbering: "1.1") + + // section: number + title over a rule; reset the per-section knot counter + show heading.where(level: 1): it => { + counter(figure.where(kind: "frame")).update(0) + block(above: 20pt, below: 8pt, width: 100%)[ + #text(font: hfont, weight: "bold", size: 15pt, fill: p.muted)[#counter(heading).display()] + #h(0.7em) + #text(font: hfont, weight: "bold", size: 15pt, fill: p.thm)[#it.body] + #v(1pt) + #line(length: 100%, stroke: 1.1pt + p.rule) + ] + } + show heading.where(level: 2): it => block(above: 11pt, below: 0.5em)[ + #text(font: hfont, weight: "bold", fill: p.def)[#counter(heading).display() #it.body] + ] + show heading.where(level: 3): it => block(above: 8pt, below: 0.5em)[ + #text(font: f.sans, weight: "bold", fill: p.thm)[#it.body] + ] + + show: theorem-rules + body +} diff --git a/packages/preview/scholia/0.1.0/src/theorems.typ b/packages/preview/scholia/0.1.0/src/theorems.typ new file mode 100644 index 0000000000..fc6a513a83 --- /dev/null +++ b/packages/preview/scholia/0.1.0/src/theorems.typ @@ -0,0 +1,92 @@ +// Knots — theorem-likes built on frame-it, custom-skinned for Scholia. +// frame-it makes each a `figure(kind:"frame")`, so we inherit native @label +// cross-references and Typst numbering for free. Colours come from the active +// theme palette (by supplement), not from the frame definitions below. +#import "@preview/frame-it:2.0.0" as frame-it +#import "colors.typ": * +#import "visual.typ": mark, label-it +#import "fonts.typ": default-fonts + +// All knots share one kind ("frame") → one counter (amsthm `[theorem]` style). +// The supplement string is how the style tells them apart; the colours passed +// here are placeholders (our style ignores them and reads the active palette). +#let ( + theorem, lemma, proposition, corollary, + definition, axiom, assumption, notation, + example, remark, + conjecture, claim, +) = frame-it.frames( + base-color: palettes.light.thm, + theorem: ("Theorem", palettes.light.thm), + lemma: ("Lemma", palettes.light.thm), + proposition: ("Proposition", palettes.light.thm), + corollary: ("Corollary", palettes.light.thm), + conjecture: ("Conjecture", palettes.light.thm), + claim: ("Claim", palettes.light.thm), + definition: ("Definition", palettes.light.def), + axiom: ("Axiom", palettes.light.def), + assumption: ("Assumption", palettes.light.def), + notation: ("Notation", palettes.light.def), + example: ("Example", palettes.light.eg), + remark: ("Remark", palettes.light.def), +) + +// Colour dispatch: def-family = teal, eg-family = amber, everything else = blue. +#let _def-family = ("Definition", "Axiom", "Assumption", "Notation") +#let _eg-family = ("Example",) + +// the frame-it style factory: (title, tags, body, supplement, number, accent) +#let knot-style(title, tags, body, supplement, number, _accent) = context { + let p = active.get() + let a = if supplement in _def-family { p.def } + else if supplement in _eg-family { p.eg } + else { p.thm } + set align(left) // frame-it frames live in a figure caption, which centres by default + + if supplement == "Remark" { + // a remark is unboxed, italic in the definition colour + block(above: 8pt, below: 8pt, { + text(style: "italic", fill: p.def)[#supplement #number] + if title != none { text(style: "italic", fill: p.ink)[ (#title)] } + text(fill: p.def)[.] + h(0.4em) + body + }) + } else { + block( + width: 100%, breakable: true, fill: tint(p, a), + stroke: (left: 2.2pt + a), + inset: (left: 12pt, right: 11pt, top: 8pt, bottom: 8pt), + above: 10pt, below: 10pt, + { + text(font: default-fonts.heading, weight: "bold", fill: a)[#supplement #number] + if title != none { text(font: default-fonts.heading, style: "italic", fill: p.ink)[ (#title)] } + for tag in tags { h(0.5em); label-it(tag) } + text(font: default-fonts.heading, weight: "bold", fill: a)[.] + h(0.4em) + body + }, + ) + } +} + +// proofs end on a small solid square (qed) in the theorem colour +#let proof(body) = context { + let p = active.get() + block(above: 6pt, below: 8pt, { + text(font: default-fonts.heading, style: "italic", fill: p.muted)[Proof.] + h(0.4em) + body + h(1fr) + mark(p.thm, size: 6pt) + }) +} + +// activate the knot style + section-tied "N.n" numbering; call as `#show: ...` +#let theorem-rules(body) = { + show figure.where(kind: "frame"): set figure( + numbering: n => numbering("1.1", counter(heading).get().first(), n), + ) + show: frame-it.frame-style(knot-style) + body +} diff --git a/packages/preview/scholia/0.1.0/src/visual.typ b/packages/preview/scholia/0.1.0/src/visual.typ new file mode 100644 index 0000000000..e44c9de5b5 --- /dev/null +++ b/packages/preview/scholia/0.1.0/src/visual.typ @@ -0,0 +1,17 @@ +// Minimal inline marks. (The woven tile/emblem of the original were dropped in +// favour of a cleaner STEM look.) +#import "colors.typ": * +#import "fonts.typ": default-fonts + +// a small solid square — used as the proof end-mark (qed), and anywhere a tiny +// coloured accent is wanted. Takes an explicit colour. +#let mark(c, size: 6pt) = box( + baseline: 1pt, + rect(width: size, height: size, fill: c, stroke: none, radius: 0.5pt), +) + +// a small inline label / badge — bold sans in the theme's tag colour. The default +// style for theorem tags (source, claim-ID), and usable anywhere: `label-it[GSM 211]`. +#let label-it(t) = context text( + font: default-fonts.sans, size: 8pt, weight: "bold", fill: active.get().tag, +)[#t] diff --git a/packages/preview/scholia/0.1.0/template/main.typ b/packages/preview/scholia/0.1.0/template/main.typ new file mode 100644 index 0000000000..9bbb4006d7 --- /dev/null +++ b/packages/preview/scholia/0.1.0/template/main.typ @@ -0,0 +1,47 @@ +#import "@preview/scholia:0.1.0": * + +#show: scholia.with( + // theme: "dark", // light (default) | dark (slate) + // prose: "book", // notes (default, no indent) | book (first-line indent) +) + +#cover( + "Your Notebook", + subtitle: "A one-line subtitle", + author: "Your Name", + date: "2026", +) + += First Course + +#keyword[The big idea], in a sentence, before the machinery. + +#note[ +Intuition first: say what this is really about, in your own words. This layer is +written to be *read* — the formal layer below is written to be *filled*. +] + +#definition[a term][ +State the object, but leave the key clause blank: closed under #fillin(width: 2.5cm). +] + +#theorem[attribution][source][ +State the result in full; later you can refer back to @def:thing. +] + +#proof[ +Sketch the moves: (i) the first; (ii) the second. Leave the crux as +#TODO[the step that makes it work]. +] + +#example[a worked instance][ +Show the computation once, so the reader has a model to imitate. +] + +#yourturn[ +Now restage the example as your own computation. +#workspace(n: 3) +] + +#recall[A question to park in the margin.] +#remark[An aside that doesn't need a box.] diff --git a/packages/preview/scholia/0.1.0/thumbnail.png b/packages/preview/scholia/0.1.0/thumbnail.png new file mode 100644 index 0000000000..ad18996724 Binary files /dev/null and b/packages/preview/scholia/0.1.0/thumbnail.png differ diff --git a/packages/preview/scholia/0.1.0/typst.toml b/packages/preview/scholia/0.1.0/typst.toml new file mode 100644 index 0000000000..b9cbc97173 --- /dev/null +++ b/packages/preview/scholia/0.1.0/typst.toml @@ -0,0 +1,18 @@ +[package] +name = "scholia" +version = "0.1.0" +entrypoint = "src/lib.typ" +authors = ["Eric Yang <@Eryc123Y>"] +license = "MIT" +description = "Fill-in study notes for STEM to aid active recall." +repository = "https://github.com/Eryc123Y/scholia" +keywords = ["notes", "study", "theorem", "active-recall", "mathematics"] +categories = ["book", "components"] +disciplines = ["computer-science", "mathematics", "education"] +compiler = "0.13.0" +exclude = ["examples", "gallery"] + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png"