From 2d66774f83c670b2a725187461ad8dbceac20f9b Mon Sep 17 00:00:00 2001 From: xwysyy <3185205565@qq.com> Date: Tue, 29 Sep 2026 22:42:28 +0800 Subject: [PATCH] xwysyy:0.5.0 --- packages/preview/xwysyy/0.5.0/LICENSE | 22 + packages/preview/xwysyy/0.5.0/README.md | 210 +++ .../preview/xwysyy/0.5.0/src/elements.typ | 139 ++ packages/preview/xwysyy/0.5.0/src/layout.typ | 1366 +++++++++++++++++ packages/preview/xwysyy/0.5.0/src/slides.typ | 359 +++++ packages/preview/xwysyy/0.5.0/src/themes.typ | 95 ++ .../preview/xwysyy/0.5.0/template/main.typ | 37 + packages/preview/xwysyy/0.5.0/thumbnail.png | Bin 0 -> 188834 bytes packages/preview/xwysyy/0.5.0/typst.toml | 18 + packages/preview/xwysyy/0.5.0/xwysyy.typ | 9 + 10 files changed, 2255 insertions(+) create mode 100644 packages/preview/xwysyy/0.5.0/LICENSE create mode 100644 packages/preview/xwysyy/0.5.0/README.md create mode 100644 packages/preview/xwysyy/0.5.0/src/elements.typ create mode 100644 packages/preview/xwysyy/0.5.0/src/layout.typ create mode 100644 packages/preview/xwysyy/0.5.0/src/slides.typ create mode 100644 packages/preview/xwysyy/0.5.0/src/themes.typ create mode 100644 packages/preview/xwysyy/0.5.0/template/main.typ create mode 100644 packages/preview/xwysyy/0.5.0/thumbnail.png create mode 100644 packages/preview/xwysyy/0.5.0/typst.toml create mode 100644 packages/preview/xwysyy/0.5.0/xwysyy.typ diff --git a/packages/preview/xwysyy/0.5.0/LICENSE b/packages/preview/xwysyy/0.5.0/LICENSE new file mode 100644 index 0000000000..45bb67b23e --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Carlos-Mero +Copyright (c) 2026 xwysyy + +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/xwysyy/0.5.0/README.md b/packages/preview/xwysyy/0.5.0/README.md new file mode 100644 index 0000000000..d61369a834 --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/README.md @@ -0,0 +1,210 @@ +

xwysyy

+ +

+ Typst Universe + License: MIT + Typst version: >= 0.15.0 + touying version: 0.8.0 + Built-in themes +

+ +

+ 中文 | English +

+ +Academic presentation templates built on [touying](https://github.com/touying-typ/touying). The package covers slide decks, handouts, speaker notes, and pdfpc metadata. The visual theme is derived from [Carlos-Mero/may](https://github.com/Carlos-Mero/may) under MIT. + +## Features + +- Universe template support: `typst init @preview/xwysyy:0.5.0` creates a ready-to-compile deck. +- Six built-in themes: `sky`, `sunset`, `forest`, `midnight`, `violet`, and `graphite`. +- Custom theme dictionaries can be passed directly to `theme`, so users can customize colors without forking the package. +- `xwysyy-pre` takes `font`, `code-font`, and `lang`, plus `heading-font` for the header title. +- Touying handout mode, `#speaker-note`, and pdfpc export are documented with tagged source examples. +- Eight semantic layout components (`duo-slide`, `grid-slide`, `figure-slide`, `stat-slide`, ...) measure every block at compile time, distribute space fill-first, and export layout telemetry without hand-written `#v()` spacing. + +## Preview + +Rendered previews are generated from the [tagged source examples](https://github.com/xwysyy/xwysyy-typst/tree/v0.5.0/examples). + +### Slide Themes + +| sky | sunset | forest | +|:---:|:---:|:---:| +| ![Sky theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-sky-p1-01.png) | ![Sunset theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-sunset-p1-01.png) | ![Forest theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-forest-p1-01.png) | + +| midnight | violet | graphite | +|:---:|:---:|:---:| +| ![Midnight theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-midnight-p1-01.png) | ![Violet theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-violet-p1-01.png) | ![Graphite theme cover](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-theme-graphite-p1-01.png) | + +### Component Pages + +| Sky cover | Sky components | +|:---:|:---:| +| ![Sky theme cover slide](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-sky-p1-01.png) | ![Sky theme textbox components](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-sky-p5-05.png) | + +| Sunset cover | Sunset components | +|:---:|:---:| +| ![Sunset theme cover slide](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-sunset-p1-01.png) | ![Sunset theme textbox components](https://raw.githubusercontent.com/xwysyy/xwysyy-typst/v0.5.0/assets/preview-sunset-p5-05.png) | + +## Quick Start + +Create a new project from the Universe template: + +```bash +typst init @preview/xwysyy:0.5.0 my-talk +cd my-talk +typst compile main.typ +``` + +Use the package in an existing Typst project: + +```typst +#import "@preview/xwysyy:0.5.0": * + +#show: xwysyy-pre.with( + theme: "sunset", + config-info( + title: [My Presentation Title], + subtitle: [Subtitle], + author: " ", + date: datetime.today(), + institution: " ", + ), +) + +#title-slide() +#outline-slide() + += Section Title + +== Slide Title + +Body text with *bold* and #red[red highlight]. + +#textbox( + [*Module A* + + First column], + + [*Module B* + + Second column], +) + +#end-slide(title: [Thank You!], body: [Questions?]) +``` + +## Themes + +Select a built-in theme by name: + +```typst +#show: xwysyy-pre.with( + theme: "forest", + config-info(title: [My Presentation]), +) +``` + +Pass a custom dictionary directly: + +```typst +#let my-theme = ( + sea: rgb("#1f5d45"), + sky: rgb("#a8d5ba"), + skyll: rgb("#f5fbf7"), + paper: rgb("#f7faf8"), + page-fill: white, +) + +#show: xwysyy-pre.with( + theme: my-theme, + config-info(title: [My Presentation]), +) +``` + +All five fields are required: + +| Field | Purpose | +|-------|---------| +| `sea` | Primary dark color for the header title, links, table heads, and badges | +| `sky` | Accent color, also the fade-out end of the header rule | +| `skyll` | Code block, table row, and textbox fill | +| `paper` | Text on dark backgrounds | +| `page-fill` | Slide page background | + +## Component Reference + +| Category | API | Usage | +|----------|-----|-------| +| Slide entry | `xwysyy-pre` | `#show: xwysyy-pre.with(theme: "sky", ...)` | +| Title slide | `title-slide` | `#title-slide()` | +| Outline | `outline-slide` | `#outline-slide()` auto-collects section headings | +| Content slide | `xwysyy-slide` | `== Title` auto-triggers | +| Section transition | `new-section-slide` | `= Title` auto-triggers | +| Full-screen image | `image-slide` | `#image-slide(img: image("bg.png"))` | +| End slide | `end-slide` | `#end-slide(title: [...])` | +| Layout · pair | `duo-slide` | figure over text, measured spacing + telemetry | +| Layout · single | `focus-slide` | one centered block for sparse pages | +| Layout · columns | `grid-slide` | N equal-height peer columns | +| Layout · stack | `stack-slide` | N vertical blocks, the visual grows dominant or cards grow tall | +| Layout · compare | `compare-slide` | two top-aligned blocks read as a contrast | +| Layout · stats | `stat-slide` | a row of big-number metric tiles | +| Layout · figure | `figure-slide` | figure, tight caption, optional takeaway | +| Layout · sidebar | `sidebar-slide` | a label tab beside a content card | +| Text box | `textbox` | `#textbox[Content]` or `#textbox([Col 1], [Col 2])` | +| Highlight | `red` / `bred` | `#red[text]` / `#bred[bold red]` | +| Highlight | `yellow` / `byellow` | `#yellow[text]` / `#byellow[bold yellow]` | + +The layout components (`duo-slide`, `focus-slide`, `grid-slide`, `stack-slide`, `compare-slide`, `stat-slide`, `figure-slide`, `sidebar-slide`) take typed content items (`visual` / `card` / `takeaway` / `plain`, with `metric` entries for `stat-slide`; `sidebar-slide` takes plain content) with declared sizing, measure every block, distribute space fill-first, and export `` v4 telemetry (allocated frame, natural preferred size, 2-D payload bbox with a measured/declared source, and paint box + fill per object). For stepwise reveal use the `reveal: true` parameter of the multi-block components (all but `focus-slide` and `sidebar-slide`) instead of `#pause`, which cannot appear inside the components (touying panics). See the [layout guide](https://github.com/xwysyy/xwysyy-typst/blob/v0.5.0/docs/LAYOUT.md). + +## Handouts And Speaker Notes + +Pass touying's handout setting through `xwysyy-pre`. A command-line switch can be wired as follows: + +```typst +#let handout = sys.inputs.at("handout", default: "false") == "true" + +#show: xwysyy-pre.with( + config-common(handout: handout), + config-info(title: [My Presentation]), +) +``` + +```bash +typst compile main.typ slides.pdf +typst compile --input handout=true main.typ slides-handout.pdf +``` + +Speaker notes are available because `xwysyy.typ` re-exports touying: + +```typst +#speaker-note[ + Mention the ablation table before moving to the next section. +] +``` + +Export pdfpc metadata: + +```bash +typst eval --in main.typ --format json 'query().first().value' > slides.pdfpc +``` + +## Requirements + +- Typst >= 0.15.0 +- touying 0.8.0, downloaded on first compile +- physica 0.9.8, downloaded on first compile +- Default local fonts: Times New Roman, Noto Serif CJK SC, Libertinus Sans, Noto Sans CJK SC, Maple Mono, and Noto Sans Mono CJK SC +- Typst web app users can pass web-available fonts with `font:`, `heading-font:`, and `code-font:` + +Full API reference: [docs/USAGE.md](https://github.com/xwysyy/xwysyy-typst/blob/v0.5.0/docs/USAGE.md). Customization guide: [docs/CUSTOMIZATION.md](https://github.com/xwysyy/xwysyy-typst/blob/v0.5.0/docs/CUSTOMIZATION.md). Theme generator: [docs/THEME-GENERATOR.md](https://github.com/xwysyy/xwysyy-typst/blob/v0.5.0/docs/THEME-GENERATOR.md). + +## Acknowledgements + +- Theme derived from [Carlos-Mero/may](https://github.com/Carlos-Mero/may) under MIT +- Built on [touying](https://github.com/touying-typ/touying) + +## License + +[MIT](./LICENSE) diff --git a/packages/preview/xwysyy/0.5.0/src/elements.typ b/packages/preview/xwysyy/0.5.0/src/elements.typ new file mode 100644 index 0000000000..a7e8d5ea7f --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/src/elements.typ @@ -0,0 +1,139 @@ +// Shared show-chain (xwysyy-elements) and reusable components (info, textbox). + +#import "@preview/touying:0.8.0": components +#import "themes.typ": * + +#let xwysyy-elements( + doc, + code-font: ("Maple Mono", "Noto Sans Mono CJK SC"), + t-sea: sea, + t-sky: sky, + t-skyll: skyll, + t-paper: paper, +) = [ + // Bold enhancement (recipe in themes.typ `_bold-run`) + #show strong: it => _bold-run(it.body) + + // List style + #set list(marker: (text(fill: t-sea, [❖]), text(fill: t-sky, [⬦]), text(fill: t-sky, [–])), spacing: 1.2em, indent: 0.5em, body-indent: 0.8em) + #set enum(spacing: 1.2em, indent: 0.5em) + + // Italic — per-character synthetic skew for CJK + #show emph: it => { + if type(it.body) == content and it.body.has("text") { + for c in it.body.text { + box(skew(ax: -8deg, c)) + } + } else { + box(skew(ax: -8deg, it.body)) + } + } + + // No image shadow: wrapping images in a box sized by measure() collapses + // percentage-width images to zero size. + + // Figure captions — smaller + #show figure.caption: it => { + set text(size: 0.78em, fill: luma(100)) + v(0.3em) + it + } + + // Table captions on top + #show figure.where(kind: table): set figure.caption(position: top) + + // Codeblocks + #show raw.where(block: true): it => { + set text(font: code-font, size: 0.9em) + block( + width: 100%, + height: auto, + fill: t-skyll, + inset: 0.6em, + radius: 0.5em, + it + ) + } + // Inline code — the chip fill is painted with `outset` so it does not join + // baseline/line-height layout (the old inset+baseline recipe sank the code + // text ~0.1em below the surrounding baseline). + #show raw.where(block: false): it => { + set text(font: code-font) + box( + fill: t-skyll, + inset: (x: 0.3em), + outset: (y: 0.2em), + radius: 0.3em, + it + ) + } + + // Links + #show link: underline + #show link: it => { + set text(fill: t-sea) + it + } + + // Detail Decoration — longer patterns first to avoid partial matches. + // Each rule is guarded so it never rewrites code content: string show rules + // also match text inside raw, which used to turn `<=` in code into ⇐. + // NB: `text.font` reports lowercased family names — compare case-insensitively. + #let code-head = lower(if type(code-font) == array { code-font.first() } else { code-font }) + #let non-code(arrow) = it => context { + let f = text.font + let head = if type(f) == array and f.len() > 0 { f.first() } else { f } + if lower(head) == code-head { it } else { arrow } + } + #show "<==>": non-code([$arrow.l.r.double.long$]) + #show "<=>": non-code([$<=>$]) + #show "-->": non-code([$-->$]) + #show "<--": non-code([$<--$]) + #show "==>": non-code([$==>$]) + #show "<==": non-code([$arrow.l.double.long$]) + #show "->": non-code([$->$]) + #show "<-": non-code([$<-$]) + #show "=>": non-code([$=>$]) + #show "<=": non-code([$arrow.l.double$]) + #show "|->": non-code([$|->$]) + + // Tables — seamless filled header + zebra body (no gutter: cell gaps used to + // slice the fills into fragments), theme-toned thin hlines + #set table( + stroke: none, + inset: (x: 0.6em, y: 0.42em), + align: center, + fill: (x, y) => if y == 0 { t-sea } else if calc.even(y) { t-skyll } else { none }, + ) + #set table.hline(stroke: 0.5pt + t-sea.lighten(30%)) + #show table.cell: it => { + if it.y == 0 { + set text(t-paper, weight: "bold") + it + } else {it} + } + + #doc +] + +// information item +#let info(something, description) = [ + *#something* #h(1fr) *#description*\ +] + +#let textbox(inset: 0.8em, radius: 0.4em, width: 100%, gutter: 0.6em, ..bodies) = context { + let t = _theme-state.get() + let bodies = bodies.pos() + if bodies.len() == 1 { + block(width: width, fill: t.skyll, inset: inset, radius: radius, bodies.first()) + } else { + components.lazy-layout(grid( + columns: (1fr,) * bodies.len(), + gutter: gutter, + ..bodies.map(b => block(width: 100%, fill: t.skyll, inset: inset, radius: radius, { + b + components.lazy-v(1fr) + })), + )) + } +} diff --git a/packages/preview/xwysyy/0.5.0/src/layout.typ b/packages/preview/xwysyy/0.5.0/src/layout.typ new file mode 100644 index 0000000000..25c323351c --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/src/layout.typ @@ -0,0 +1,1366 @@ +// Semantic layout layer for xwysyy-typst — telemetry schema v4. +// +// Authors (human or AI) pick a semantic component and provide typed content +// items; each component measures the real rendered size of every block, runs +// a shared allocator, and exports the *measured* normalized geometry as +// `` v4 metadata for scripts/slide-check.py. +// +// Honesty rules baked into the schema: +// * sizing is DECLARED, never inferred: percent-sized content must be +// wrapped in `visual(...)` (fit: "stretch"); required slots panic on +// `none`; text slots panic on content with no measurable width or height +// (spacers, empty strings, bare rules) instead of masquerading as a +// payload; +// * every object carries `frame` (allocated), `preferred` (what the +// allocator saw), `payload` (a 2-D flow bbox of the inner content — +// approximate for wrapped text, exact for fixed-size content) and +// `paint` (the visible card box + its fill colour, or none); +// * `payload_source` separates "measured" payloads from "declared" ones +// (stretch slots and percent-width media): a declared payload is a +// claim, not evidence — the checker verifies it against rendered pixels +// instead of trusting it; +// * unbreakable content wider than its slot is exported as `overflow_x` +// (wrapped text is clamped honestly; a long URL that cannot wrap is not); +// * the allocator has four states — normal / compressed (gaps squeezed +// below their preferred size) / tight (outer margins consumed) / +// overflow — with the invariants overflow => body_overflow_ratio > 0 and +// compressed => gap_ratio < 1; +// * one `` mapping is emitted per physically rendered +// subslide, carrying the physical body geometry in pt, and the slide id +// is stable across subslides (touying's logical slide counter), so every +// frame joins its layout record and coverage counts real pages. +// +// Authors never write raw `v()` spacing or absolute coordinates; they adjust +// `mode` and read the numeric feedback the checker returns after each +// compile. Numeric layout knobs live in the `tuning` dictionary (validated +// for key, type, and range); the AI generation contract treats `tuning` as +// off-limits and the telemetry records whether it was used. +// +// Stepwise reveal: touying's `#pause` cannot be used inside these components +// (marks do not survive `context` / `layout` closures; touying panics). +// Components take `reveal: true` (or per-item `reveal-from`) instead, which +// shows blocks one subslide at a time via the callback-style `utils.uncover` +// — hidden steps keep their measured space, so the layout is identical on +// every subslide. One resolver (`_steps`) serves every component and an +// explicit `reveal-from` always wins over the `reveal: true` sugar; +// components without reveal steps have a single frame and reject any +// `reveal-from` beyond step 1 instead of silently ignoring it. + +#import "@preview/touying:0.8.0": utils +#import "slides.typ": xwysyy-slide +#import "themes.typ": _theme-state + +// Capture `top` / `left` before the parameters of the same name (duo's `top`, +// compare's `left`) shadow them inside a component body. +#let _atop = top +#let _aleft = left + +#let _clamp(x, lo, hi) = calc.min(calc.max(x, lo), hi) + +// Optical center sits slightly above the geometric center so a centered group +// does not read as sinking. Only focus-slide centers a group; every other +// component fills the body instead. +#let _OPTICAL-CENTER = 0.46 + +// Fill-first distribution. Outer margins are pinned small; leftover space +// grows the content (figures become dominant, cards become tall) instead of +// becoming blank margin. +#let _MARGIN-TOP = 0.07 +#let _MARGIN-BOT = 0.09 +#let _FILL = 1.0 - _MARGIN-TOP - _MARGIN-BOT +// A card row occupies at least this fraction of the body height, so a row of +// short cards reads as substantial cards rather than tiles floating in space. +#let _CARD-FILL-MIN = 0.60 +// Hard minimum for a stretch visual: below this the visual is starved and the +// slide must degrade to tight/overflow instead of reporting normal. +#let _VISUAL-MIN = 0.28 +// Semantic gaps compress down to this fraction of their preferred size before +// the slide starts consuming its outer margins. +#let _GAP-MIN = 0.4 +// Card inner padding. +#let _CARD-PAD = 0.9em +// stat-slide shrinks a long value to fit its tile, but never below this scale +// (below it the value wraps and the tile grows, reported honestly by fit). +#let _STAT-SCALE-MIN = 0.6 +// Measurement epsilon: below this a dimension counts as zero. +#let _EPS-L = 0.01pt + +#let _MODES = ("compact", "balanced", "separated") +#let _check-mode(mode) = { + if mode not in _MODES { + panic("unknown mode " + repr(mode) + "; use \"compact\" | \"balanced\" | \"separated\"") + } +} + +// Gap between semantically-related blocks, as a fraction of body height. The +// three values land inside the checker's proximity ranges so a correctly +// generated slide passes without diagnostics. +#let _mode-gap(mode) = { + if mode == "compact" { 0.06 } else if mode == "separated" { 0.22 } else { 0.13 } +} +#let _mode-proximity(mode) = { + if mode == "compact" { "compact" } else if mode == "separated" { "loose" } else { "medium" } +} + +// Numeric layout knobs, validated for key, type, and range so a typo or a +// nonsense value fails loudly instead of being silently ignored. +// `spec` maps key -> (default:, lo:, hi:). +#let _tuning(comp, given, spec) = { + for (k, v) in given { + if k not in spec { + panic(comp + ": unknown tuning key " + repr(k) + "; allowed: " + repr(spec.keys())) + } + if type(v) != float and type(v) != int { + panic(comp + ": tuning " + repr(k) + " must be a number, got " + repr(v)) + } + let s = spec.at(k) + if v < s.lo or v > s.hi { + panic(comp + ": tuning " + repr(k) + " = " + repr(v) + " outside [" + repr(s.lo) + ", " + repr(s.hi) + "]") + } + } + let out = (:) + for (k, s) in spec { out.insert(k, float(given.at(k, default: s.default))) } + out +} + +#let _require(comp, slot, v) = { + if v == none { panic(comp + ": " + slot + " is required") } +} + +// Reveal gating. The block becomes visible from subslide `step` (1-based); +// hidden steps keep their measured space. Uses the callback-style +// `utils.uncover` because mark-based `#pause` / global `#uncover` panic inside +// `context` / `layout`. +#let _from(self, step, body) = { + if step <= 1 { body } else { utils.uncover(self: self, str(step) + "-", body) } +} + +// --------------------------------------------------------------------------- +// typed items (declared sizing; sidebar slots take plain content instead) +// --------------------------------------------------------------------------- +// +// `visual(...)` marks the slide's visual payload: never carded, and with +// fit: "stretch" (the default) it absorbs free space and its content is told +// to fill the allocated frame (use `rect(height: 100%)`, or +// `image(width: 100%, height: 100%, fit: "contain")` for real images). +// fit: "natural" keeps the visual at its measured size (`image(width: 100%)`). +// `card(...)` / `takeaway(...)` draw the theme card behind their content. +// `plain(...)` is an uncarded text block. Plain (untyped) content passed to +// a slot is coerced per component (documented at each component). +// +// Roles form a closed set: they weight the checker's optical-centre estimate +// and must not be able to change its control flow, so there is no +// "decorative" escape hatch. + +#let _ROLES = ( + "main_visual", "figure", "image", "chart", "visual", "table", "formula", + "callout", "takeaway", "option", "column", "explanation", "label", + "metric", "text", "content", "caption", +) +#let _check-role(ctor, role) = { + if role not in _ROLES { + panic(ctor + ": role " + repr(role) + " is not in the closed role set " + repr(_ROLES)) + } +} + +#let visual(body, fit: "stretch", role: "main_visual", reveal-from: auto) = { + if fit not in ("stretch", "natural") { + panic("visual: fit must be \"stretch\" or \"natural\", got " + repr(fit)) + } + _check-role("visual", role) + (xwysyy-item: true, kind: "visual", fit: fit, role: role, body: body, reveal-from: reveal-from) +} +#let card(body, role: "explanation", reveal-from: auto) = { + _check-role("card", role) + (xwysyy-item: true, kind: "card", fit: "natural", role: role, body: body, reveal-from: reveal-from) +} +#let takeaway(body, reveal-from: auto) = ( + xwysyy-item: true, kind: "takeaway", fit: "natural", role: "takeaway", body: body, reveal-from: reveal-from, +) +#let plain(body, role: "text", reveal-from: auto) = { + _check-role("plain", role) + (xwysyy-item: true, kind: "plain", fit: "natural", role: role, body: body, reveal-from: reveal-from) +} + +#let _as-item(x, default-kind) = { + if type(x) == dictionary and x.at("xwysyy-item", default: false) { x } + else if default-kind == "visual" { + (xwysyy-item: true, kind: "visual", fit: "natural", role: "main_visual", body: x, reveal-from: auto) + } else if default-kind == "takeaway" { + (xwysyy-item: true, kind: "takeaway", fit: "natural", role: "takeaway", body: x, reveal-from: auto) + } else { + (xwysyy-item: true, kind: "card", fit: "natural", role: "explanation", body: x, reveal-from: auto) + } +} + +#let _painted(it) = it.kind in ("card", "takeaway") +#let _stretchy(it) = it.kind == "visual" and it.fit == "stretch" + +#let _KINDS = ("visual", "card", "takeaway", "plain") + +// Central item validation, run up front in every component. The constructor +// marker is not trusted: a hand-built dictionary with an unknown kind, fit, or +// role fails here instead of silently taking some other code path. +// `stretch: false` marks slots sized by their natural height; `reveal: false` +// marks components without reveal steps, which accept `reveal-from: 1` (their +// single frame) and reject any later step instead of ignoring it. +#let _validate-item(comp, slot, it, stretch: true, reveal: true) = { + if it.at("kind", default: none) not in _KINDS { + panic(comp + ": " + slot + " item kind must be one of " + repr(_KINDS) + + ", got " + repr(it.at("kind", default: none))) + } + if it.at("fit", default: none) not in ("stretch", "natural") { + panic(comp + ": " + slot + " fit must be \"stretch\" or \"natural\", got " + + repr(it.at("fit", default: none))) + } + if it.at("role", default: none) not in _ROLES { + panic(comp + ": " + slot + " role " + repr(it.at("role", default: none)) + + " is not in the closed role set " + repr(_ROLES)) + } + if it.at("body", default: none) == none { + panic(comp + ": " + slot + " body is required") + } + if not stretch and _stretchy(it) { + panic(comp + ": " + slot + " cannot be visual(fit: \"stretch\") — this slot is " + + "sized by its natural height; use fit: \"natural\"") + } + let rf = it.at("reveal-from", default: auto) + if not reveal and rf != auto and rf != 1 { + panic(comp + ": " + slot + " has no reveal steps; remove reveal-from") + } + it +} + +// Relation kinds an author may declare between blocks. Structural kinds +// (peer / caption / labels) are emitted by the components themselves. +#let _AUTHOR-RELS = ("supports", "contrast") +#let _check-relation(comp, rel) = { + if rel not in _AUTHOR-RELS { + panic(comp + ": relation must be one of " + repr(_AUTHOR-RELS) + ", got " + repr(rel)) + } +} + +// Resolve per-item reveal steps — the single resolver for every component. +// `reveal: true` reveals auto items in order (item i on subslide +// min(i+1, max-step)); an explicit `reveal-from` must be an integer in +// [1, max-step] and ALWAYS wins over the sugar. +#let _steps(comp, items, reveal, max-step) = { + items.enumerate().map(((i, it)) => { + let rf = it.at("reveal-from", default: auto) + if rf == auto { + if reveal { calc.min(i + 1, max-step) } else { 1 } + } else { + if type(rf) != int { + panic(comp + ": reveal-from must be an integer >= 1, got " + repr(rf)) + } + if rf < 1 or rf > max-step { + panic(comp + ": reveal-from = " + str(rf) + " outside [1, " + str(max-step) + "]") + } + rf + } + }) +} + +// --------------------------------------------------------------------------- +// allocator +// --------------------------------------------------------------------------- +// +// Column: every item is a spec (min:, pref:, max:, grow:) in absolute +// lengths (max: none = unbounded); every gap is (min:, pref:). Resolution +// order and states: +// normal — everything at preferred fits the safe area; free space goes +// to grow-weighted items (water-filling against max); residual +// free space centers the group inside the safe area +// compressed — fits the safe area only with gaps squeezed below preferred +// (but not below their min); gap_ratio in [_GAP-MIN, 1) +// tight — fits the page only by consuming the outer margins; gaps at +// min, group centered on the page; margin_deficit_ratio > 0 +// overflow — taller than the page even at minimums; +// body_overflow_ratio > 0 (hard invariant) +// `gap_ratio` is the REAL ratio of the summed actual gaps to the summed +// preferred gaps (1.0 when the layout has no gaps), not an interpolation +// parameter. +#let _alloc-column(specs, gaps, H) = { + let budget = H * _FILL + let prefs = specs.map(s => calc.max(s.pref, s.min)) + let body = prefs.fold(0pt, (a, h) => a + h) + let gap-pref = gaps.fold(0pt, (a, g) => a + g.pref) + let gap-min = gaps.fold(0pt, (a, g) => a + g.min) + let base = body + gap-pref + let min-ratio = if gap-pref > 0pt { gap-min / gap-pref } else { 1.0 } + if base <= budget { + let heights = prefs + let free = budget - base + let frozen = specs.map(s => s.grow <= 0) + while free > 0.05pt { + let gsum = 0.0 + for (i, s) in specs.enumerate() { + if not frozen.at(i) { gsum += s.grow } + } + if gsum == 0 { break } + let spent = 0pt + for (i, s) in specs.enumerate() { + if frozen.at(i) { continue } + let add = free * (s.grow / gsum) + if s.max != none and heights.at(i) + add >= s.max { + add = calc.max(s.max - heights.at(i), 0pt) + frozen.at(i) = true + } + heights.at(i) += add + spent += add + } + free -= spent + if spent <= 0.05pt { break } + } + let used = heights.fold(0pt, (a, h) => a + h) + gap-pref + (heights: heights, gaps: gaps.map(g => g.pref), + y0: H * _MARGIN-TOP + calc.max(budget - used, 0pt) / 2, + fit: (state: "normal", required_height_ratio: base / H, gap_ratio: 1.0, + margin_deficit_ratio: 0.0, body_overflow_ratio: 0.0)) + } else if body + gap-min <= budget { + let span = gap-pref - gap-min + let t = if span > 0pt { (budget - body - gap-min) / span } else { 1.0 } + let actual = gaps.map(g => g.min + (g.pref - g.min) * t) + let actual-sum = actual.fold(0pt, (a, g) => a + g) + (heights: prefs, gaps: actual, + y0: H * _MARGIN-TOP, + fit: (state: "compressed", required_height_ratio: base / H, + gap_ratio: if gap-pref > 0pt { actual-sum / gap-pref } else { 1.0 }, + margin_deficit_ratio: 0.0, body_overflow_ratio: 0.0)) + } else if body + gap-min <= H { + let total = body + gap-min + (heights: prefs, gaps: gaps.map(g => g.min), y0: (H - total) / 2, + fit: (state: "tight", required_height_ratio: base / H, gap_ratio: min-ratio, + margin_deficit_ratio: (total - budget) / H, body_overflow_ratio: 0.0)) + } else { + let total = body + gap-min + (heights: prefs, gaps: gaps.map(g => g.min), y0: 0pt, + fit: (state: "overflow", required_height_ratio: base / H, gap_ratio: min-ratio, + margin_deficit_ratio: (H - budget) / H, body_overflow_ratio: (total - H) / H)) + } +} + +// Row variant: one shared height for side-by-side cards. No gaps to +// compress, so the states are normal / tight / overflow and gap_ratio is 1. +#let _fit-row(naturals, H) = { + let natural = naturals.fold(0pt, (a, h) => calc.max(a, h)) + let budget = H * _FILL + if natural <= budget { + let row = _clamp(natural, _CARD-FILL-MIN * H, budget) + (row: row, y: H * _MARGIN-TOP + (budget - row) / 2, + fit: (state: "normal", required_height_ratio: natural / H, gap_ratio: 1.0, + margin_deficit_ratio: 0.0, body_overflow_ratio: 0.0)) + } else if natural <= H { + (row: natural, y: (H - natural) / 2, + fit: (state: "tight", required_height_ratio: natural / H, gap_ratio: 1.0, + margin_deficit_ratio: (natural - budget) / H, body_overflow_ratio: 0.0)) + } else { + (row: natural, y: 0pt, + fit: (state: "overflow", required_height_ratio: natural / H, gap_ratio: 1.0, + margin_deficit_ratio: (H - budget) / H, body_overflow_ratio: (natural - H) / H)) + } +} + +// --------------------------------------------------------------------------- +// measurement +// --------------------------------------------------------------------------- + +// Content signatures (verified against typst 0.15 `measure`): +// * `v(...)` and an empty string have zero unconstrained WIDTH; +// * a bare horizontal rule has zero constrained HEIGHT; +// * percent-width media (`image(width: 100%)`) measures exactly like a +// spacer — zero unconstrained width — so only visual() slots may fall +// back to the slot width, and that payload is marked "declared"; +// * `hide(...)` keeps its full layout size and can only be caught by the +// pixel cross-check. +// Text slots (card / takeaway / plain / metric fields) therefore require a +// positive unconstrained width AND a positive constrained height; visuals +// only panic when every dimension is zero (genuinely empty content). +#let _assert-measurable(comp, slot, kind, body, w) = { + let mu = measure(body) + let mc = measure(block(width: w, body)) + if kind == "visual" { + if mu.width < _EPS-L and mu.height < _EPS-L and mc.height < _EPS-L { + panic(comp + ": " + slot + " renders empty — pass real content; percent-sized " + + "content must be declared visual() (fit: \"stretch\")") + } + } else { + if mu.width < _EPS-L { + panic(comp + ": " + slot + " has no measurable width — spacers and empty " + + "strings are not content, and percent-width media belongs in visual()") + } + if mc.height < _EPS-L { + panic(comp + ": " + slot + " has no measurable height — a bare rule is " + + "decoration, not content") + } + } + (mu: mu, mc: mc) +} + +// Horizontal-overflow evidence. Unbreakable content wider than its slot +// cannot wrap: the constrained height (`mc`) stays at the unconstrained +// height (`mu`) while the natural width exceeds the slot width `w`. Wrapped +// text is clamped honestly instead. +#let _overflows-x(mu, mc, w) = mu.width > w + _EPS-L and mc.height <= mu.height + _EPS-L + +// Measure a natural item for a slot of width `w`. Returns the outer size the +// allocator uses (including card padding), the 2-D payload flow bbox, the +// payload source, and horizontal-overflow evidence. +#let _measure-item(comp, slot, it, w) = { + let pad = if _painted(it) { _CARD-PAD.to-absolute() } else { 0pt } + let iw = w - 2 * pad + let m = _assert-measurable(comp, slot, it.kind, it.body, iw) + // Only reachable for visual items: percent-width media measures at zero + // width, claims the slot width, and is marked "declared" so the checker + // demands pixel evidence instead of counting it as measured payload. + let fallback = m.mu.width < _EPS-L + ( + outer: m.mc.height + 2 * pad, + pay-w: if fallback { iw } else { calc.min(m.mu.width, iw) }, + pay-h: m.mc.height, + pad: pad, + src: if fallback { "declared" } else { "measured" }, + over-x: _overflows-x(m.mu, m.mc, iw), + ) +} + +// Build the allocator spec and telemetry seed for one column/slot item. +// Stretch visuals get the hard minimum and a grow weight; natural items are +// pinned at their measured size (max = pref) unless the component grants +// cards a grow weight. +#let _item-slot(comp, slot, it, w, H) = { + if _stretchy(it) { + (stretch: true, painted: false, pad: 0pt, + spec: (min: H * _VISUAL-MIN, pref: H * _VISUAL-MIN, max: none, grow: 1.0), + pay-w: none, pay-h: none, src: "declared", over-x: false) + } else { + let m = _measure-item(comp, slot, it, w) + (stretch: false, painted: _painted(it), pad: m.pad, + spec: (min: m.outer, pref: m.outer, max: m.outer, grow: 0.0), + pay-w: m.pay-w, pay-h: m.pay-h, src: m.src, over-x: m.over-x) + } +} + +// --------------------------------------------------------------------------- +// telemetry primitives (schema v4) +// --------------------------------------------------------------------------- + +// One telemetry object. +// frame — the box the component allocated (normalized to the body) +// preferred — the natural outer size the allocator saw +// payload — 2-D flow bbox of the inner content (inside card +// padding); approximate for wrapped text (flow bbox, not +// glyph ink); equals the padded frame for declared-stretch +// content +// paint — the visible card box, or none for unpainted objects +// paint_fill — hex colour of the card fill (pixel-check evidence) +// payload_source — "measured" | "declared"; declared payloads are claims +// the checker verifies against pixels, never evidence +// overflow_x — unbreakable content wider than the slot (see +// _measure-item) +// Payload height is NOT clamped to the frame: overflowing content reports a +// payload that runs past its frame, and the checker sees it. +#let _obj( + id, kind, role, + fx, fy, fw, fh, + pref-h: none, + pay-w: none, pay-h: none, + pay-src: "measured", over-x: false, + pad-x: 0.0, pad-y: 0.0, + calign: "center", halign: "center", + painted: false, paint-fill: none, visible-from: 1, +) = { + let ix = fx + pad-x + let iy = fy + pad-y + let iw = calc.max(fw - 2 * pad-x, 0.0) + let ih = calc.max(fh - 2 * pad-y, 0.0) + let sx = if pay-w == none { "stretch" } else { "natural" } + let sy = if pay-h == none { "stretch" } else { "natural" } + let pw = if pay-w == none { iw } else { calc.min(pay-w, iw) } + let ph = if pay-h == none { ih } else { pay-h } + let px = if sx == "stretch" or halign == "left" { ix } else { ix + (iw - pw) / 2 } + let py = if sy == "stretch" or calign == "top" { iy } else { iy + (ih - ph) / 2 } + ( + id: id, + object_kind: kind, + semantic_role: role, + frame: (x: fx, y: fy, w: fw, h: fh), + preferred: (w: fw, h: if pref-h == none { fh } else { pref-h }), + payload: (x: px, y: py, w: pw, h: ph), + paint: if painted { (x: fx, y: fy, w: fw, h: fh) } else { none }, + paint_fill: if painted { paint-fill } else { none }, + payload_source: if pay-w == none or pay-h == none { "declared" } else { pay-src }, + overflow_x: over-x, + sizing: (x: sx, y: sy), + visible_from: visible-from, + ) +} + +#let _rel(from, to, kind, axis, proximity) = ( + from: from, + to: to, + kind: kind, + axis: axis, + desired_proximity: proximity, +) + +// One frame mapping per physically rendered subslide, carrying the physical +// body geometry (pt) so the pixel checker maps normalized telemetry onto the +// render without hard-coded page constants. In handout mode only the +// surviving subslide emits, so coverage counts real pages. +#let _frame-mark(sid, self, size) = { + let pos = here().position() + [#metadata(( + schema: "xwysyy-frame/v2", + id: sid, + step: self.subslide, + steps: self.repeat, + page: pos.page, + handout: self.handout, + body: (x: pos.x.pt(), y: pos.y.pt(), w: size.width.pt(), h: size.height.pt()), + page_size: (w: page.width.pt(), h: page.height.pt()), + )) ] +} + +// Emit one slide's telemetry. Must be produced in markup so the label binds +// (a label on `metadata(...)` in code mode is a Typst syntax error). +#let _emit(id, archetype, engine, page, frame-count, objects, relations, fit, extra) = [ + #metadata(( + schema: "xwysyy-slide-layout/v4", + id: id, + archetype: archetype, + layout_engine: engine, + page: page, + frame_count: frame-count, + coordinate_system: "normalized-slide-body", + objects: objects, + relations: relations, + fit: fit, + extra: extra, + )) +] + +// Debug overlay: solid rect = frame, dashed rect = payload bbox. +#let _debug-palette = ( + rgb("#d7263d"), rgb("#2e86ab"), rgb("#4f772d"), + rgb("#7b2cbf"), rgb("#f77f00"), rgb("#006d77"), +) +#let _debug-layer(objects, W, H) = { + for (i, o) in objects.enumerate() { + let c = _debug-palette.at(calc.rem(i, _debug-palette.len())) + let f = o.frame + place(_atop + _aleft, dx: W * f.x, dy: H * f.y, rect( + width: W * f.w, height: H * f.h, + stroke: (paint: c, thickness: 0.7pt), fill: c.transparentize(93%), + )) + if o.sizing.y == "natural" or o.sizing.x == "natural" { + let p = o.payload + place(_atop + _aleft, dx: W * p.x, dy: H * p.y, rect( + width: W * p.w, height: H * p.h, + stroke: (paint: c, thickness: 0.5pt, dash: "dashed"), + )) + } + place(_atop + _aleft, dx: W * f.x + 0.2em, dy: H * f.y + 0.12em, + text(fill: c, size: 0.5em, weight: "bold", o.id)) + } +} + +// Card container for slot rendering: the theme's rounded skyll box; `calign` +// controls where content sits inside a fixed-height box. +#let _card-box(cw, h, fill, body, calign: horizon) = { + block(width: cw, height: h, fill: fill, inset: _CARD-PAD, radius: 0.4em, align(calign, body)) +} + +// Render one typed item into its allocated slot. +#let _render-item(it, w, h, fill, calign: horizon) = { + if _painted(it) { + _card-box(w, h, fill, it.body, calign: calign) + } else { + block(width: w, height: h, align(center + calign, it.body)) + } +} + +// Resolve the slide id: `auto` becomes "@s" from touying's logical +// slide counter, which is STABLE across reveal subslides and handout mode +// (the physical page number is not: it changes per subslide and would detach +// early frames from their layout record). Must run inside a context. +#let _sid(id, base) = { + if id == auto { base + "@s" + str(utils.slide-counter.get().first()) } else { id } +} + +// --------------------------------------------------------------------------- +// duo-slide — one vertical semantic pair (e.g. figure over takeaway) +// --------------------------------------------------------------------------- +// +// `top` coerces plain content to visual(fit: "natural"); wrap percent-height +// content in visual() to make it the growing dominant block. `bottom` +// coerces plain content to card() (painted). + +#let duo-slide( + title: auto, + id: auto, + top: none, + bottom: none, + mode: "balanced", + relation: "supports", + reveal: false, + debug: false, + tuning: (:), +) = { + _check-mode(mode) + _check-relation("duo-slide", relation) + _require("duo-slide", "top", top) + _require("duo-slide", "bottom", bottom) + let tn = _tuning("duo-slide", tuning, ( + "top-width": (default: 0.82, lo: 0.3, hi: 0.95), + "bottom-width": (default: 0.74, lo: 0.3, hi: 0.95), + )) + let ti = _validate-item("duo-slide", "top", _as-item(top, "visual")) + let bi = _validate-item("duo-slide", "bottom", _as-item(bottom, "card")) + let steps = _steps("duo-slide", (ti, bi), reveal, 2) + let rep = calc.max(..steps, 1) + xwysyy-slide(title: title, repeat: rep, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "duo") + _frame-mark(sid, self, size) + let twn = tn.at("top-width") + let bwn = tn.at("bottom-width") + let tw = W * twn + let bw = W * bwn + let gap = H * _mode-gap(mode) + let ts = _item-slot("duo-slide", "top", ti, tw, H) + let bs = _item-slot("duo-slide", "bottom", bi, bw, H) + let alloc = _alloc-column((ts.spec, bs.spec), ((min: gap * _GAP-MIN, pref: gap),), H) + let th = alloc.heights.at(0) + let bh = alloc.heights.at(1) + let g = alloc.gaps.at(0) + let y0 = alloc.y0 + + place(_atop + _aleft, dx: (W - tw) / 2, dy: y0, + _from(self, steps.at(0), _render-item(ti, tw, th, cfill))) + place(_atop + _aleft, dx: (W - bw) / 2, dy: y0 + th + g, + _from(self, steps.at(1), _render-item(bi, bw, bh, cfill))) + + let objects = ( + _obj(sid + ":top", ti.kind, ti.role, + (1.0 - twn) / 2, y0 / H, twn, th / H, + pref-h: ts.spec.pref / H, + pay-w: if ts.stretch { none } else { ts.pay-w / W }, + pay-h: if ts.stretch { none } else { ts.pay-h / H }, + pay-src: ts.src, over-x: ts.over-x, + pad-x: ts.pad / W, pad-y: ts.pad / H, + halign: if ts.painted { "left" } else { "center" }, + painted: ts.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(0)), + _obj(sid + ":bottom", bi.kind, bi.role, + (1.0 - bwn) / 2, (y0 + th + g) / H, bwn, bh / H, + pref-h: bs.spec.pref / H, + pay-w: if bs.stretch { none } else { bs.pay-w / W }, + pay-h: if bs.stretch { none } else { bs.pay-h / H }, + pay-src: bs.src, over-x: bs.over-x, + pad-x: bs.pad / W, pad-y: bs.pad / H, + halign: if bs.painted { "left" } else { "center" }, + painted: bs.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(1)), + ) + let relations = ( + _rel(sid + ":top", sid + ":bottom", relation, "vertical", _mode-proximity(mode)), + ) + if self.subslide == rep { + _emit(sid, "duo", "column", here().position().page, rep, objects, relations, + alloc.fit, (mode: mode, tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// focus-slide — a single centered block for low-content pages +// --------------------------------------------------------------------------- +// +// The one deliberate exception to fill-first: a focus page states a single +// point and is allowed its symmetric whitespace. Plain content is coerced to +// card(). A focus page has exactly one frame: it rejects reveal-from beyond +// step 1 and stretch visuals (there is no free space to absorb). Fit still honours the +// safe area: content taller than it reports tight, taller than the page +// reports overflow. + +#let focus-slide( + title: auto, + id: auto, + body: none, + debug: false, + tuning: (:), +) = { + _require("focus-slide", "body", body) + let tn = _tuning("focus-slide", tuning, ( + "width": (default: 0.76, lo: 0.3, hi: 0.95), + "center-y": (default: _OPTICAL-CENTER, lo: 0.30, hi: 0.70), + )) + let it = _validate-item("focus-slide", "body", _as-item(body, "card"), + stretch: false, reveal: false) + xwysyy-slide(title: title, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "focus") + _frame-mark(sid, self, size) + let wn = tn.at("width") + let cw = W * wn + let m = _measure-item("focus-slide", "body", it, cw) + let hr = m.outer / H + let fit = if hr <= _FILL { + (state: "normal", required_height_ratio: hr, gap_ratio: 1.0, + margin_deficit_ratio: 0.0, body_overflow_ratio: 0.0) + } else if hr <= 1.0 { + (state: "tight", required_height_ratio: hr, gap_ratio: 1.0, + margin_deficit_ratio: hr - _FILL, body_overflow_ratio: 0.0) + } else { + (state: "overflow", required_height_ratio: hr, gap_ratio: 1.0, + margin_deficit_ratio: 1.0 - _FILL, body_overflow_ratio: hr - 1.0) + } + let y = if fit.state == "normal" { + _clamp(tn.at("center-y") - hr / 2, _MARGIN-TOP, + calc.max(1.0 - _MARGIN-BOT - hr, _MARGIN-TOP)) + } else if fit.state == "tight" { (1.0 - hr) / 2 } else { 0.0 } + + place(_atop + _aleft, dx: (W - cw) / 2, dy: H * y, + _render-item(it, cw, m.outer, cfill)) + + let objects = ( + _obj(sid + ":focus", it.kind, it.role, + (1.0 - wn) / 2, y, wn, hr, + pref-h: hr, + pay-w: m.pay-w / W, pay-h: m.pay-h / H, + pay-src: m.src, over-x: m.over-x, + pad-x: m.pad / W, pad-y: m.pad / H, + halign: if _painted(it) { "left" } else { "center" }, + painted: _painted(it), paint-fill: cfill.to-hex()), + ) + _emit(sid, "focus", "single", here().position().page, 1, objects, (), fit, + (tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + }) + }) +} + +// --------------------------------------------------------------------------- +// stack-slide — N vertical blocks, fill-first (duo generalized) +// --------------------------------------------------------------------------- +// +// Takes typed items; plain content is coerced to card(). Stretch visuals +// absorb the free space; a stack with no stretch visual grows its cards so +// every card reads tall. With `reveal: true` item i appears on subslide i +// (override per item with `reveal-from`). + +#let stack-slide( + title: auto, + id: auto, + items: (), + relation: "supports", + mode: "balanced", + reveal: false, + debug: false, + tuning: (:), +) = { + _check-mode(mode) + _check-relation("stack-slide", relation) + let tn = _tuning("stack-slide", tuning, ( + "width": (default: 0.82, lo: 0.3, hi: 0.95), + )) + let its = items.enumerate().map(((i, x)) => + _validate-item("stack-slide", "items[" + str(i) + "]", _as-item(x, "card"))) + let n = its.len() + if n == 0 { + panic("stack-slide: items is empty") + } + let steps = _steps("stack-slide", its, reveal, n) + let rep = calc.max(..steps, 1) + xwysyy-slide(title: title, repeat: rep, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "stack") + _frame-mark(sid, self, size) + let wn = tn.at("width") + let bw = W * wn + let slots = its.enumerate().map(((i, it)) => + _item-slot("stack-slide", "items[" + str(i) + "]", it, bw, H)) + // Grow policy: stretch visuals absorb free space; without one, the + // cards share it so a pure-text stack reads as tall cards. + let has-stretch = slots.any(s => s.stretch) + let specs = slots.enumerate().map(((i, s)) => { + if s.stretch { s.spec } + else if not has-stretch and its.at(i).kind == "card" { + (min: s.spec.min, pref: s.spec.pref, max: none, grow: 1.0) + } else { s.spec } + }) + let gap = H * _mode-gap(mode) + let alloc = _alloc-column(specs, ((min: gap * _GAP-MIN, pref: gap),) * (n - 1), H) + + let objects = () + let relations = () + let cy = alloc.y0 + for (i, it) in its.enumerate() { + let h = alloc.heights.at(i) + let s = slots.at(i) + place(_atop + _aleft, dx: (W - bw) / 2, dy: cy, + _from(self, steps.at(i), _render-item(it, bw, h, cfill))) + let oid = sid + ":" + str(i) + objects.push(_obj(oid, it.kind, it.role, + (1.0 - wn) / 2, cy / H, wn, h / H, + pref-h: specs.at(i).pref / H, + pay-w: if s.stretch { none } else { s.pay-w / W }, + pay-h: if s.stretch { none } else { s.pay-h / H }, + pay-src: s.src, over-x: s.over-x, + pad-x: s.pad / W, pad-y: s.pad / H, + halign: if s.painted { "left" } else { "center" }, + painted: s.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(i))) + if i > 0 { + relations.push(_rel(sid + ":" + str(i - 1), oid, relation, "vertical", _mode-proximity(mode))) + } + cy = cy + h + if i < n - 1 { alloc.gaps.at(i) } else { 0pt } + } + if self.subslide == rep { + _emit(sid, "stack", "column", here().position().page, rep, objects, relations, + alloc.fit, (mode: mode, tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// grid-slide — N equal-height peer columns (N >= 2) +// --------------------------------------------------------------------------- +// +// Columns take typed items; plain content is coerced to card(). Row layouts +// are sized by their natural height, so stretch visuals are rejected up +// front. Consecutive columns carry a `peer` relation so the checker +// validates the gutter. + +#let grid-slide( + title: auto, + id: auto, + columns: (), + reveal: false, + debug: false, + tuning: (:), +) = { + let tn = _tuning("grid-slide", tuning, ( + "gutter": (default: 0.04, lo: 0.0, hi: 0.2), + )) + let its = columns.enumerate().map(((i, x)) => + _validate-item("grid-slide", "columns[" + str(i) + "]", _as-item(x, "card"), + stretch: false)) + let n = its.len() + if n < 2 { + panic("grid-slide: needs at least 2 columns (use stack-slide or focus-slide for one block)") + } + let steps = _steps("grid-slide", its, reveal, n) + let rep = calc.max(..steps, 1) + xwysyy-slide(title: title, repeat: rep, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "grid") + _frame-mark(sid, self, size) + let gutter = tn.at("gutter") + let cwn = (1.0 - gutter * (n - 1)) / n + if cwn <= 0.0 { + panic("grid-slide: gutter " + repr(gutter) + " leaves no width for " + repr(n) + " columns") + } + let cw = W * cwn + let slots = its.enumerate().map(((i, it)) => + _item-slot("grid-slide", "columns[" + str(i) + "]", it, cw, H)) + let naturals = slots.map(s => s.spec.pref) + let row = _fit-row(naturals, H) + + let objects = () + let relations = () + for (i, it) in its.enumerate() { + let s = slots.at(i) + let xn = i * (cwn + gutter) + place(_atop + _aleft, dx: W * xn, dy: row.y, + _from(self, steps.at(i), _render-item(it, cw, row.row, cfill))) + let oid = sid + ":" + str(i) + objects.push(_obj(oid, it.kind, it.role, + xn, row.y / H, cwn, row.row / H, + pref-h: s.spec.pref / H, + pay-w: s.pay-w / W, pay-h: s.pay-h / H, + pay-src: s.src, over-x: s.over-x, + pad-x: s.pad / W, pad-y: s.pad / H, + halign: if s.painted { "left" } else { "center" }, + painted: s.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(i))) + if i > 0 { + relations.push(_rel(sid + ":" + str(i - 1), oid, "peer", "horizontal", "gutter")) + } + } + if self.subslide == rep { + _emit(sid, "grid", "row", here().position().page, rep, objects, relations, row.fit, + (tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// compare-slide — two equal-height cards read as a contrast +// --------------------------------------------------------------------------- +// +// Cards share one row height; content is TOP-aligned so the two openings sit +// on the same line, which is how a contrast is read. Both sides are +// required; plain content is coerced to card(); stretch visuals are rejected +// up front (row layouts are sized by their natural height). + +#let compare-slide( + title: auto, + id: auto, + left: none, + right: none, + reveal: false, + debug: false, + tuning: (:), +) = { + _require("compare-slide", "left", left) + _require("compare-slide", "right", right) + let tn = _tuning("compare-slide", tuning, ( + "gutter": (default: 0.06, lo: 0.0, hi: 0.2), + )) + let li = _validate-item("compare-slide", "left", _as-item(left, "card"), stretch: false) + let ri = _validate-item("compare-slide", "right", _as-item(right, "card"), stretch: false) + let steps = _steps("compare-slide", (li, ri), reveal, 2) + let rep = calc.max(..steps, 1) + xwysyy-slide(title: title, repeat: rep, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "compare") + _frame-mark(sid, self, size) + let gutter = tn.at("gutter") + let cwn = (1.0 - gutter) / 2 + let cw = W * cwn + let ls = _item-slot("compare-slide", "left", li, cw, H) + let rs = _item-slot("compare-slide", "right", ri, cw, H) + let row = _fit-row((ls.spec.pref, rs.spec.pref), H) + + place(_atop + _aleft, dx: 0pt, dy: row.y, + _from(self, steps.at(0), _render-item(li, cw, row.row, cfill, calign: _atop))) + place(_atop + _aleft, dx: W * (cwn + gutter), dy: row.y, + _from(self, steps.at(1), _render-item(ri, cw, row.row, cfill, calign: _atop))) + + let objects = ( + _obj(sid + ":left", li.kind, li.role, + 0.0, row.y / H, cwn, row.row / H, + pref-h: ls.spec.pref / H, + pay-w: ls.pay-w / W, pay-h: ls.pay-h / H, + pay-src: ls.src, over-x: ls.over-x, + pad-x: ls.pad / W, pad-y: ls.pad / H, + calign: "top", halign: if ls.painted { "left" } else { "center" }, + painted: ls.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(0)), + _obj(sid + ":right", ri.kind, ri.role, + cwn + gutter, row.y / H, cwn, row.row / H, + pref-h: rs.spec.pref / H, + pay-w: rs.pay-w / W, pay-h: rs.pay-h / H, + pay-src: rs.src, over-x: rs.over-x, + pad-x: rs.pad / W, pad-y: rs.pad / H, + calign: "top", halign: if rs.painted { "left" } else { "center" }, + painted: rs.painted, paint-fill: cfill.to-hex(), + visible-from: steps.at(1)), + ) + let relations = ( + _rel(sid + ":left", sid + ":right", "contrast", "horizontal", "gutter"), + ) + if self.subslide == rep { + _emit(sid, "compare", "row", here().position().page, rep, objects, relations, row.fit, + (tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// stat-slide — a row of metric tiles (big value + label) +// --------------------------------------------------------------------------- +// +// Standalone row engine (shares `_fit-row`), so the value auto-shrink scale +// is measured here and exported in the telemetry. `stats` takes typed +// `metric(value, label, reveal-from: ...)` entries; the value and the label +// are measured separately (never through the tile's forced width) and both +// must render non-empty. + +#let metric(value, label, reveal-from: auto) = { + if value == none { panic("metric: value is required") } + if label == none { panic("metric: label is required") } + (xwysyy-metric: true, value: value, label: label, reveal-from: reveal-from) +} + +#let stat-slide( + title: auto, + id: auto, + stats: (), + reveal: false, + debug: false, + tuning: (:), +) = { + let n = stats.len() + if n == 0 { + panic("stat-slide: stats is empty") + } + for (i, s) in stats.enumerate() { + if type(s) != dictionary or not s.at("xwysyy-metric", default: false) { + panic("stat-slide: stats[" + str(i) + "] must be built with metric(value, label)") + } + if s.at("value", default: none) == none or s.at("label", default: none) == none { + panic("stat-slide: stats[" + str(i) + "] must carry both value and label") + } + } + let tn = _tuning("stat-slide", tuning, ( + "gutter": (default: 0.04, lo: 0.0, hi: 0.2), + )) + let steps = _steps("stat-slide", stats, reveal, n) + let rep = calc.max(..steps, 1) + // Both metric fields must render non-empty (an empty string still has line + // height, but zero width). + let assert-metric(i, s) = { + if measure(text(weight: 700, s.value)).width < _EPS-L { + panic("stat-slide: stats[" + str(i) + "] value renders empty") + } + if measure(s.label).width < _EPS-L { + panic("stat-slide: stats[" + str(i) + "] label renders empty") + } + } + xwysyy-slide(title: title, repeat: rep, self => context { + let t = _theme-state.get() + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "stat") + _frame-mark(sid, self, size) + for (i, s) in stats.enumerate() { assert-metric(i, s) } + let gutter = tn.at("gutter") + let cwn = (1.0 - gutter * (n - 1)) / n + if cwn <= 0.0 { + panic("stat-slide: gutter " + repr(gutter) + " leaves no width for " + repr(n) + " tiles") + } + let cw = W * cwn + let pad = _CARD-PAD.to-absolute() + let iw = cw - 2 * pad + // Fit each value to its tile: shrink down to the floor scale, below + // which the value wraps and the tile grows (reported by fit). + let scales = stats.map(s => { + let vw = measure(text(size: 2.6em, weight: 700, s.value)).width + if vw > iw and vw > 0pt { calc.max(iw / vw, _STAT-SCALE-MIN) } else { 1.0 } + }) + let tile(i) = { + let s = stats.at(i) + align(center + horizon, stack( + spacing: 0.2em, + align(center, text(size: 2.6em * scales.at(i), weight: 700, fill: t.sea, s.value)), + align(center, text(size: 0.95em, fill: t.sea.lighten(12%), s.label)), + )) + } + // Payload width comes from the value and the label measured + // separately, never from the tile's forced full width. + let pays = range(n).map(i => { + let s = stats.at(i) + let vstyled = text(size: 2.6em * scales.at(i), weight: 700, s.value) + let vm = measure(vstyled) + let lm = measure(text(size: 0.95em, s.label)) + let vc = measure(block(width: iw, vstyled)) + ( + w: calc.max(vm.width, lm.width), + h: measure(block(width: iw, tile(i))).height, + over-x: _overflows-x(vm, vc, iw), + ) + }) + let naturals = pays.map(p => p.h + 2 * pad) + let row = _fit-row(naturals, H) + + let objects = () + let relations = () + for i in range(n) { + let xn = i * (cwn + gutter) + place(_atop + _aleft, dx: W * xn, dy: row.y, + _from(self, steps.at(i), + block(width: cw, height: row.row, fill: t.skyll, inset: _CARD-PAD, + radius: 0.4em, align(horizon, tile(i))))) + let oid = sid + ":" + str(i) + objects.push(_obj(oid, "card", "metric", + xn, row.y / H, cwn, row.row / H, + pref-h: naturals.at(i) / H, + pay-w: calc.min(pays.at(i).w, iw) / W, pay-h: pays.at(i).h / H, + pay-src: "measured", over-x: pays.at(i).over-x, + pad-x: pad / W, pad-y: pad / H, + painted: true, paint-fill: t.skyll.to-hex(), + visible-from: steps.at(i))) + if i > 0 { + relations.push(_rel(sid + ":" + str(i - 1), oid, "peer", "horizontal", "gutter")) + } + } + if self.subslide == rep { + _emit(sid, "stat", "row", here().position().page, rep, objects, relations, row.fit, ( + value_scales: scales, + tuned: tuning.len() > 0, + )) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// figure-slide — figure, tight caption, optional takeaway +// --------------------------------------------------------------------------- +// +// The caption is measured first and subtracted, then the figure slot receives +// an explicit height, so a stretch figure fills its slot instead of escaping +// the wrapper (Typst measures percentage heights as 0 in an unbounded +// context). `fig` coerces plain content to visual(fit: "natural"); wrap it +// in visual() to make it fill. `takeaway` coerces to a painted takeaway card +// and rejects stretch. Reveal goes through the shared resolver: with +// `reveal: true` the figure shows first and the takeaway follows; per-item +// `reveal-from` overrides that order, and the caption always follows the +// figure. + +#let figure-slide( + title: auto, + id: auto, + fig: none, + caption: none, + takeaway: none, + mode: "balanced", + reveal: false, + debug: false, + tuning: (:), +) = { + _check-mode(mode) + _require("figure-slide", "fig", fig) + let tn = _tuning("figure-slide", tuning, ( + "figure-width": (default: 0.80, lo: 0.3, hi: 0.95), + "takeaway-width": (default: 0.74, lo: 0.3, hi: 0.95), + )) + let fi = _validate-item("figure-slide", "fig", _as-item(fig, "visual")) + let ki = if takeaway == none { none } else { + _validate-item("figure-slide", "takeaway", _as-item(takeaway, "takeaway"), stretch: false) + } + let items = if ki == none { (fi,) } else { (fi, ki) } + let steps = _steps("figure-slide", items, reveal, items.len()) + let fig-step = steps.at(0) + let tk-step = if ki != none { steps.at(1) } else { 1 } + let rep = calc.max(..steps, 1) + let cap = if caption == none { none } else { + context text(size: 0.85em, fill: _theme-state.get().sea.lighten(12%), style: "italic", caption) + } + xwysyy-slide(title: title, repeat: rep, self => context { + let cfill = _theme-state.get().skyll + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "figure") + _frame-mark(sid, self, size) + let fwn = tn.at("figure-width") + let twn = tn.at("takeaway-width") + let fw = W * fwn + let tw = W * twn + let cap-gap = 0.5em.to-absolute() + let mode-gap = H * _mode-gap(mode) + + let fs = _item-slot("figure-slide", "fig", fi, fw, H) + let specs = (fs.spec,) + let gaps = () + let cap-m = if cap != none { + let m = measure(block(width: fw, cap)) + specs.push((min: m.height, pref: m.height, max: m.height, grow: 0.0)) + // The caption gap is tight by design and not compressible. + gaps.push((min: cap-gap, pref: cap-gap)) + m + } else { none } + let ks = if ki != none { + let s = _item-slot("figure-slide", "takeaway", ki, tw, H) + specs.push(s.spec) + gaps.push((min: mode-gap * _GAP-MIN, pref: mode-gap)) + s + } else { none } + let alloc = _alloc-column(specs, gaps, H) + + let objects = () + let relations = () + let cy = alloc.y0 + let idx = 0 + let fh = alloc.heights.at(idx) + place(_atop + _aleft, dx: (W - fw) / 2, dy: cy, + _from(self, fig-step, _render-item(fi, fw, fh, cfill))) + objects.push(_obj(sid + ":figure", fi.kind, fi.role, + (1.0 - fwn) / 2, cy / H, fwn, fh / H, + pref-h: fs.spec.pref / H, + pay-w: if fs.stretch { none } else { fs.pay-w / W }, + pay-h: if fs.stretch { none } else { fs.pay-h / H }, + pay-src: fs.src, over-x: fs.over-x, + pad-x: fs.pad / W, pad-y: fs.pad / H, + painted: fs.painted, paint-fill: cfill.to-hex(), + visible-from: fig-step)) + cy = cy + fh + idx += 1 + if cap != none { + cy = cy + alloc.gaps.at(idx - 1) + let ch = alloc.heights.at(idx) + place(_atop + _aleft, dx: (W - fw) / 2, dy: cy, + _from(self, fig-step, block(width: fw, align(center, cap)))) + let cm = measure(cap) + objects.push(_obj(sid + ":caption", "plain", "caption", + (1.0 - fwn) / 2, cy / H, fwn, ch / H, + pref-h: ch / H, + pay-w: calc.min(cm.width, fw) / W, pay-h: cap-m.height / H, + pay-src: "measured", + over-x: _overflows-x(cm, cap-m, fw), + visible-from: fig-step)) + relations.push(_rel(sid + ":figure", sid + ":caption", "caption", "vertical", "tight")) + cy = cy + ch + idx += 1 + } + if ki != none { + cy = cy + alloc.gaps.at(idx - 1) + let th = alloc.heights.at(idx) + place(_atop + _aleft, dx: (W - tw) / 2, dy: cy, + _from(self, tk-step, _render-item(ki, tw, th, cfill))) + let anchor = if cap != none { sid + ":caption" } else { sid + ":figure" } + objects.push(_obj(sid + ":takeaway", ki.kind, ki.role, + (1.0 - twn) / 2, cy / H, twn, th / H, + pref-h: ks.spec.pref / H, + pay-w: ks.pay-w / W, pay-h: ks.pay-h / H, + pay-src: ks.src, over-x: ks.over-x, + pad-x: ks.pad / W, pad-y: ks.pad / H, + halign: if ks.painted { "left" } else { "center" }, + painted: ks.painted, paint-fill: cfill.to-hex(), + visible-from: tk-step)) + relations.push(_rel(anchor, sid + ":takeaway", "supports", "vertical", _mode-proximity(mode))) + } + if self.subslide == rep { + _emit(sid, "figure", "column", here().position().page, rep, objects, relations, + alloc.fit, (mode: mode, tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + } + }) + }) +} + +// --------------------------------------------------------------------------- +// sidebar-slide — a narrow label tab beside a wide content card +// --------------------------------------------------------------------------- +// +// No `reveal`: the label and its content have no presentation order. Both +// slots take plain content only (the component paints its own boxes; do not +// wrap the body in `textbox` or pass typed items). + +#let sidebar-slide( + title: auto, + id: auto, + label: none, + body: none, + debug: false, + tuning: (:), +) = { + _require("sidebar-slide", "label", label) + _require("sidebar-slide", "body", body) + if type(label) == dictionary or type(body) == dictionary { + panic("sidebar-slide: label and body take plain content, not typed items") + } + let tn = _tuning("sidebar-slide", tuning, ( + "label-width": (default: 0.26, lo: 0.1, hi: 0.5), + "gutter": (default: 0.04, lo: 0.0, hi: 0.2), + )) + // Both slots are text cards: spacers, empty strings, and bare rules panic. + let assert-slot(slot, body, w) = { + let mu = measure(body) + let mc = measure(block(width: w, body)) + if mu.width < _EPS-L { + panic("sidebar-slide: " + slot + " has no measurable width — spacers and " + + "empty strings are not content") + } + if mc.height < _EPS-L { + panic("sidebar-slide: " + slot + " has no measurable height — a bare rule " + + "is decoration, not content") + } + (mu: mu, mc: mc) + } + xwysyy-slide(title: title, self => context { + let t = _theme-state.get() + layout(size => { + let W = size.width + let H = size.height + let sid = _sid(id, "sidebar") + _frame-mark(sid, self, size) + let lwn = tn.at("label-width") + let gutter = tn.at("gutter") + let bwn = 1.0 - lwn - gutter + let lw = W * lwn + let bw = W * bwn + let pad = _CARD-PAD.to-absolute() + // The label is measured with the same styled content it is rendered + // with (bold via `set text`, not `strong`, whose show rule enlarges + // the run and would make the measurement disagree with the render). + let label-inner = { + set text(weight: "bold") + show raw: set text(fill: t.sea) + label + } + let lmm = assert-slot("label", label-inner, lw - 2 * pad) + let bmm = assert-slot("body", body, bw - 2 * pad) + let lh = lmm.mc.height + 2 * pad + let bh = bmm.mc.height + 2 * pad + let row = _fit-row((lh, bh), H) + + place(_atop + _aleft, dx: 0pt, dy: row.y, + block(width: lw, height: row.row, fill: t.sea, inset: pad, radius: 0.4em, + align(horizon + _aleft, { + // Label text is light (paper) on the dark sea tab; inline code + // keeps its light chip but takes dark (sea) text so it stays + // readable instead of light-on-light. + set text(fill: t.paper) + label-inner + }))) + place(_atop + _aleft, dx: W * (lwn + gutter), dy: row.y, + block(width: bw, height: row.row, fill: t.skyll, inset: pad, radius: 0.4em, + align(horizon, body))) + + let objects = ( + _obj(sid + ":label", "card", "label", + 0.0, row.y / H, lwn, row.row / H, + pref-h: lh / H, + pay-w: calc.min(lmm.mu.width, lw - 2 * pad) / W, + pay-h: lmm.mc.height / H, + pay-src: "measured", + over-x: _overflows-x(lmm.mu, lmm.mc, lw - 2 * pad), + pad-x: pad / W, pad-y: pad / H, + halign: "left", painted: true, paint-fill: t.sea.to-hex()), + _obj(sid + ":body", "card", "content", + lwn + gutter, row.y / H, bwn, row.row / H, + pref-h: bh / H, + pay-w: calc.min(bmm.mu.width, bw - 2 * pad) / W, + pay-h: bmm.mc.height / H, + pay-src: "measured", + over-x: _overflows-x(bmm.mu, bmm.mc, bw - 2 * pad), + pad-x: pad / W, pad-y: pad / H, + halign: "left", painted: true, paint-fill: t.skyll.to-hex()), + ) + let relations = (_rel(sid + ":label", sid + ":body", "labels", "horizontal", "gutter"),) + _emit(sid, "sidebar", "row", here().position().page, 1, objects, relations, row.fit, + (tuned: tuning.len() > 0)) + if debug { _debug-layer(objects, W, H) } + }) + }) +} diff --git a/packages/preview/xwysyy/0.5.0/src/slides.typ b/packages/preview/xwysyy/0.5.0/src/slides.typ new file mode 100644 index 0000000000..88234f0869 --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/src/slides.typ @@ -0,0 +1,359 @@ +// Slides: touying-based slide variants and the xwysyy-pre entry. + +#import "@preview/touying:0.8.0": * +#import "themes.typ": * +#import "elements.typ": * + +#let _auto-outline-title(title) = { + if title == auto { + context { + if text.lang == "zh" { [目录] } else { [Contents] } + } + } else { + title + } +} + +// Per-page manifest: every slide layout declares its kind and physical page, +// so the layout checker can compute telemetry coverage (which content pages +// carry no layout telemetry) and exempt title/section/end/image/outline pages. +#let _page-manifest(kind) = context [ + #metadata((schema: "xwysyy-page/v1", kind: kind, page: here().position().page)) +] + +// Checker-exempt page kinds (title / section / end / image / outline) are +// emitted by their own slide functions only: the public `xwysyy-slide` has no +// kind parameter, so a content page cannot spoof an exemption. +#let _kinded-slide(kind, title: auto, ..args) = { + touying-slide-wrapper(self => { + if title != auto { + self.store.title = title + } + // Open header (ported from the poster's section header): sans title in + // the theme color over a thin gradient rule fading to the right. The + // title shrinks to fit one line (floor 0.65×): the page's top margin is + // fixed, so a wrapped title would collide with the rule and the body. + // The applied scale is exported as `` telemetry; a title + // still wider than the header at the floor scale reports fits: false. + let header(self) = { + set align(top) + block( + width: 100% + 2em, + inset: (x: 1em, top: 1.1em), + { + context { + let avail = page.width - 2em.to-absolute() + let title-body = if self.store.title != none { + utils.call-or-display(self, self.store.title) + } else { + utils.display-current-heading(level: 2) + } + let styled(s) = text( + font: self.store.heading-font, + fill: self.colors.neutral-dark, + weight: "bold", + size: 1.45em * s, + title-body, + ) + let natural = measure(box(styled(1.0))).width + let scale = if natural > avail and natural > 0pt { + calc.max(avail / natural, 0.65) + } else { 1.0 } + let hsize = measure(box(styled(scale))) + // Vertical fit: top inset + title + gap + rule must stay inside + // the fixed top margin, or an explicitly wrapped / oversized + // title collides with the rule and the body. + let used = (1.1em + 0.65em + 0.12em).to-absolute() + hsize.height + [#metadata(( + schema: "xwysyy-header/v2", + page: here().position().page, + scale: scale, + fits: natural * scale <= avail + 0.01pt, + height: hsize.height.pt(), + fits_v: used <= 4.35em.to-absolute() + 0.01pt, + )) ] + block(styled(scale)) + } + v(0.65em, weak: true) + // Full-width track whose ink fades out completely by 92% — the + // tail dies before the page edge instead of running into it. + rect( + width: 100%, + height: 0.12em, + radius: (left: 0.06em), + fill: gradient.linear( + (self.colors.neutral-dark, 0%), + (self.colors.primary, 42%), + (self.colors.primary.transparentize(100%), 92%), + (self.colors.primary.transparentize(100%), 100%), + ), + ) + }, + ) + } + // Footer: page number in the bottom-right corner only. + let footer(self) = { + set align(bottom + right) + set text(fill: self.colors.neutral-dark, size: .9em) + block( + inset: (x: 0.5em, bottom: 0.4em), + context utils.slide-counter.display(), + ) + } + self = utils.merge-dicts( + self, + config-page( + header: header, + footer: footer, + ), + ) + // Prepend the page manifest to the slide body (works for both plain + // content and callback-style `self => ...` bodies). + let bodies = args.pos() + let raw-body = if bodies.len() > 0 { bodies.first() } else { [] } + let tagged = if type(raw-body) == function { + s => { _page-manifest(kind); raw-body(s) } + } else { + { _page-manifest(kind); raw-body } + } + touying-slide(self: self, ..args.named(), tagged, ..bodies.slice(1)) + }) +} + +#let xwysyy-slide(title: auto, ..args) = { + if "kind" in args.named() { + panic("xwysyy-slide: kind is not a parameter; outline pages use outline-slide, " + + "full-bleed pages use title-slide / new-section-slide / end-slide / image-slide") + } + _kinded-slide("content", title: title, ..args) +} + +#let title-slide(..args) = { + touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-page(margin: 0em), + ) + self.info = self.info + args.named() + let info = self.info + let body = { + _page-manifest("title") + set align(center + horizon) + line(length: 100%, stroke: self.colors.neutral-dark) + v(-0.9em) + block( + fill: self.colors.neutral-dark, + width: 100%, + inset: (y: 1.2em), + text(size: 2.2em, fill: self.colors.neutral-lightest, weight: "bold", info.title), + ) + if info.subtitle != none { + v(-0.9em) + block( + fill: self.colors.neutral-lighter, + width: 100%, + { + line(length: 100%, stroke: self.colors.neutral-dark) + v(-0.65em) + text(size: 1.6em, fill: self.colors.neutral-dark, weight: "bold", info.subtitle) + v(-0.65em) + line(length: 100%, stroke: self.colors.neutral-dark) + } + ) + } + set text(fill: self.colors.neutral-darkest) + v(1em) + if info.author != none { + block(text(size: 1.1em, info.author)) + } + if info.institution != none { + block(text(size: 0.9em, style: "italic", info.institution)) + } + if info.date != none { + block(utils.display-info-date(self)) + } + } + touying-slide(self: self, body) + }) +} + +#let new-section-slide(body) = { + touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-page( + margin: 0em, + ), + ) + let main-body = { + _page-manifest("section") + set align(center + horizon) + set text(size: 2em, fill: self.colors.neutral-dark, weight: "bold", style: "italic") + line(start: (17%, 0em), length: 83%, stroke: self.colors.neutral-dark) + v(-0.97em) + block( + fill: self.colors.neutral-lighter, + inset: (y: 0.42em), + width: 100%, + utils.display-current-heading(level: 1) + ) + v(-0.97em) + line(start: (-17%, 0em), length: 83%, stroke: self.colors.neutral-dark) + } + touying-slide(self: self, main-body) + }) +} + +#let end-slide(title: [Thank You!], body: none) = { + touying-slide-wrapper(self => { + self = utils.merge-dicts(self, config-page(margin: 2em)) + let main-body = { + _page-manifest("end") + set align(center + horizon) + text(fill: self.colors.neutral-dark, size: 3em, weight: "bold", title) + if body != none { + v(0.5em) + line(length: 20%, stroke: (thickness: 1.5pt, paint: self.colors.primary)) + v(0.5em) + text(size: 1.3em, fill: self.colors.neutral-dark.lighten(30%), body) + } + } + touying-slide(self: self, main-body) + }) +} + +#let outline-slide(chapters: auto, title: auto) = { + _kinded-slide("outline", title: _auto-outline-title(title))[ + #context { + let t = _theme-state.get() + let chs = if chapters == auto { + query(heading.where(level: 1)) + .filter(h => not h.has("label") or str(h.label) != "touying:hidden") + .map(h => h.body) + } else { + chapters + } + let two-col = chs.len() > 5 + let bsz = if two-col { 2em } else { 2.6em } + let tsz = if two-col { 1.4em } else { 2em } + let badge(n) = box( + width: bsz, height: bsz, fill: t.sea, radius: 50%, + align(center + horizon, text(fill: t.paper, weight: "bold", size: bsz * 0.58, n)) + ) + let items = chs.enumerate().map(((i, ch)) => grid( + columns: (auto, 1fr), + column-gutter: if two-col { 1em } else { 1.4em }, + align: (center + horizon, left + horizon), + badge(numbering("01", i + 1)), + text(size: tsz, weight: "semibold", ch), + )) + if two-col { + let mid = calc.ceil(items.len() / 2) + v(0.5em) + grid( + columns: (1fr, 1fr), + gutter: 1.5em, + stack(spacing: 1.6em, ..items.slice(0, mid)), + stack(spacing: 1.6em, ..items.slice(mid)), + ) + } else { + v(1em) + stack(spacing: 2.4em, ..items) + } + } + ] +} + +#let image-slide(body: none, img: none) = { + // An image slide without an image is a spoofed full-bleed exemption: the + // page would carry arbitrary body content while the checker skips it. + if img == none { + panic("image-slide: img is required; use xwysyy-slide for text-only pages") + } + touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-page( + background: img, + margin: (x: 0em, y: 1.4em), + ), + ) + set text(fill: self.colors.neutral-dark, size: 1.4em) + set image(width: 100%, height: auto) + touying-slide(self: self, { + _page-manifest("image") + align(left + bottom, + align( + center, + block( + fill: self.colors.neutral-lighter, + if body != none { + line(length: 100%, stroke: self.colors.neutral-dark) + v(-0.85em) + body + v(-0.85em) + line(length: 100%, stroke: self.colors.neutral-dark) + } + ) + ) + ) + }) + }) +} + +#let xwysyy-pre( + aspect-ratio: "16-9", + theme: "sky", + font: ("Times New Roman", "Noto Serif CJK SC"), + heading-font: ("Libertinus Sans", "Noto Sans CJK SC"), + code-font: ("Maple Mono", "Noto Sans Mono CJK SC"), + lang: "en", + ..args, + body, +) = { + let t = _resolve-theme(theme) + _theme-state.update(t) + set text( + font: font, + lang: lang, + size: 5.5mm, + weight: "semibold", + style: "normal", + ) + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + margin: (top: 4.35em, x: 1em, bottom: 1.4em) + ), + config-common( + slide-fn: xwysyy-slide, + new-section-slide-fn: new-section-slide, + frozen-counters: (counter(figure), counter(math.equation)), + ), + config-colors( + primary: t.sky, + neutral-lighter: t.skyll, + neutral-lightest: t.paper, + neutral-dark: t.sea, + neutral-darkest: black, + ), + config-store( + title: none, + heading-font: heading-font, + ), + config-page( + fill: t.page-fill, + ), + ..args, + ) + + show: xwysyy-elements.with( + code-font: code-font, + t-sea: t.sea, + t-sky: t.sky, + t-skyll: t.skyll, + t-paper: t.paper, + ) + + body +} diff --git a/packages/preview/xwysyy/0.5.0/src/themes.typ b/packages/preview/xwysyy/0.5.0/src/themes.typ new file mode 100644 index 0000000000..dd1c300639 --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/src/themes.typ @@ -0,0 +1,95 @@ +// Theme color palettes, shared theme state, and inline highlight macros. + +#let themes = ( + sky: ( + sea: rgb("#3b60a0"), + sky: rgb("#bdd0f1"), + skyll: rgb("#f4f9ff"), + paper: rgb("#f5f6f8"), + page-fill: white, + ), + sunset: ( + sea: rgb("#970014"), + sky: rgb("#D8A6A2"), + skyll: rgb("#FFF8F6"), + paper: rgb("#f5f6f8"), + page-fill: rgb("#fffefd"), + ), + forest: ( + sea: rgb("#1f5d45"), + sky: rgb("#a8d5ba"), + skyll: rgb("#f5fbf7"), + paper: rgb("#f7faf8"), + page-fill: white, + ), + midnight: ( + sea: rgb("#1f2a44"), + sky: rgb("#8fa8d8"), + skyll: rgb("#f7faff"), + paper: rgb("#f7f8fb"), + page-fill: white, + ), + violet: ( + sea: rgb("#5a3e85"), + sky: rgb("#c7b7e8"), + skyll: rgb("#faf7ff"), + paper: rgb("#f8f6fb"), + page-fill: white, + ), + graphite: ( + sea: rgb("#31343a"), + sky: rgb("#b9c0c9"), + skyll: rgb("#f8f9fa"), + paper: rgb("#f7f7f5"), + page-fill: white, + ), +) + +#let _theme-required-fields = ( + "sea", + "sky", + "skyll", + "paper", + "page-fill", +) + +#let _resolve-theme(theme) = { + if type(theme) == str { + themes.at(theme) + } else if type(theme) == dictionary { + for field in _theme-required-fields { + if theme.at(field, default: auto) == auto { + panic("xwysyy-pre theme dictionary is missing field `" + field + "`") + } + } + theme + } else { + panic("xwysyy-pre theme must be a string name or a dictionary") + } +} + +// Default theme colors (sky) +#let sea = themes.sky.sea +#let sky = themes.sky.sky +#let skyll = themes.sky.skyll +#let paper = themes.sky.paper + +// Theme state for dynamic components +#let _theme-state = state("xwysyy-theme", themes.sky) + +// Bold recipe shared by `strong` and the colored bold macros: 1.1em + true +// bold weight, no stroke (stroking fills the CJK glyph counters and looks +// muddy). 0.03em tracking opens the run internally; 0.05em hair spacing on +// both sides keeps it off the neighbors (non-weak: weak spacing would swallow +// adjacent Latin word spaces); the 0.035em baseline drop re-centers the +// enlarged CJK glyphs optically. +#let _bold-run(body, fill: none) = { + set text(fill: fill) if fill != none + h(0.05em) + text(size: 1.1em, weight: 700, tracking: 0.03em, baseline: 0.035em, body) + h(0.05em) +} +#let red(body) = text(fill: rgb("#9c1d11"), body) +#let bred(body) = _bold-run(body, fill: rgb("#9c1d11")) +#let yellow(body) = text(fill: rgb("#d9ad20"), body) +#let byellow(body) = _bold-run(body, fill: rgb("#d9ad20")) diff --git a/packages/preview/xwysyy/0.5.0/template/main.typ b/packages/preview/xwysyy/0.5.0/template/main.typ new file mode 100644 index 0000000000..dcee4c28cc --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/template/main.typ @@ -0,0 +1,37 @@ +#import "@preview/xwysyy:0.5.0": * + +#show: xwysyy-pre.with( + theme: "sky", + config-info( + title: [xwysyy Starter Deck], + subtitle: [Academic slides in Typst], + author: " ", + date: datetime.today(), + institution: " ", + ), +) + +#title-slide() + +#outline-slide() + += Motivation + +== One Minute Setup + +Use `typst init @preview/xwysyy:0.5.0` to create this deck, then edit `main.typ`. + +#textbox( + [*Reusable components* + + `textbox`, #red[red highlights], #yellow[yellow highlights], tables, code blocks, and touying animations share one theme.], + + [*Theme control* + + Switch built-in themes with `theme: "sunset"` or pass a custom color dictionary directly.], +) + +#end-slide( + title: [Thank You!], + body: [Questions?], +) diff --git a/packages/preview/xwysyy/0.5.0/thumbnail.png b/packages/preview/xwysyy/0.5.0/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..3675e91a2aa84f97aa1506a902cc5f1a9a9347e2 GIT binary patch literal 188834 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 literal 0 HcmV?d00001 diff --git a/packages/preview/xwysyy/0.5.0/typst.toml b/packages/preview/xwysyy/0.5.0/typst.toml new file mode 100644 index 0000000000..301f74dcfe --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/typst.toml @@ -0,0 +1,18 @@ +[package] +name = "xwysyy" +version = "0.5.0" +entrypoint = "xwysyy.typ" +authors = ["xwysyy"] +license = "MIT" +description = "Create academic slides with touying." +repository = "https://github.com/xwysyy/xwysyy-typst" +keywords = ["slides", "presentation", "academic", "touying", "theme"] +categories = ["presentation", "components"] +disciplines = ["computer-science"] +compiler = "0.15.0" +exclude = [] + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png" diff --git a/packages/preview/xwysyy/0.5.0/xwysyy.typ b/packages/preview/xwysyy/0.5.0/xwysyy.typ new file mode 100644 index 0000000000..876d101f2e --- /dev/null +++ b/packages/preview/xwysyy/0.5.0/xwysyy.typ @@ -0,0 +1,9 @@ +// xwysyy.typ — facade re-exporting every submodule. +// Users do `#import "xwysyy.typ": *` to pull in themes, slides, and layout components. + +#import "@preview/physica:0.9.8": * + +#import "src/themes.typ": * +#import "src/elements.typ": * +#import "src/slides.typ": * +#import "src/layout.typ": *