You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
170 lines
5.6 KiB
170 lines
5.6 KiB
// ============================================================================
|
|
// template.typ — technical, minimal presentation template
|
|
//
|
|
// Usage:
|
|
// #import "template.typ": *
|
|
// #show: presentation.with(title: "...", subtitle: "...", author: "...")
|
|
//
|
|
// #slide(title: "Title")[ ... ] // two-part slide: title part | content part
|
|
// #slide[ ... ] // single-part, full-width slide
|
|
// #img("assets/fig.svg") // centered image, optional caption
|
|
// ============================================================================
|
|
|
|
// Palette: warm paper, petrol accent, terracotta counterpoint.
|
|
// All text/background pairs keep >= 4.5:1 contrast; decorative
|
|
// elements (bars, markers, watermarks) never carry information.
|
|
#let paper = rgb("#faf8f3")
|
|
#let ink = rgb("#2d2a26")
|
|
#let accent = rgb("#22617f")
|
|
#let accent-soft = rgb("#4c8fad")
|
|
#let ember = rgb("#a85430")
|
|
#let wash = rgb("#edf2f5")
|
|
#let mono = "DejaVu Sans Mono"
|
|
|
|
// Short vertical accent rule (petrol -> terracotta).
|
|
#let hairline-v = 3.5pt + gradient.linear(accent, ember, angle: 90deg)
|
|
// Horizontal hairline for title/section slides.
|
|
#let hairline-h = 1pt + gradient.linear(accent, ember, angle: 0deg)
|
|
|
|
#let slide-counter = counter("slide")
|
|
#let deck-title = state("deck-title", none)
|
|
|
|
// Footer: deck title bottom-left, slide counter bottom-right.
|
|
#let slide-footer() = context {
|
|
let n = slide-counter.get().first()
|
|
let total = slide-counter.final().first()
|
|
set text(font: mono, size: 0.55em, fill: luma(150))
|
|
place(bottom + left, dy: -1em, deck-title.get())
|
|
place(bottom + right, dy: -1em, {
|
|
text(fill: accent, weight: "bold", str(n))
|
|
text(fill: luma(165), " / " + str(total))
|
|
})
|
|
}
|
|
|
|
// Tinted content panel with a colored spine — used for paired columns
|
|
// (Grok = ember, Mistral = petrol). The spine is decorative reinforcement;
|
|
// the column headers carry the information.
|
|
#let panel(body, c: accent) = block(
|
|
width: 100%,
|
|
fill: c.lighten(95%),
|
|
stroke: (left: 2.5pt + c),
|
|
inset: (x: 0.55em, y: 0.45em),
|
|
radius: (top-right: 2pt, bottom-right: 2pt),
|
|
{
|
|
set par(spacing: 0.85em)
|
|
body
|
|
},
|
|
)
|
|
|
|
// Thin gradient band across the top edge of every page.
|
|
#let top-band = place(top + left, rect(width: 100%, height: 2.5pt, fill: gradient.linear(accent, ember, angle: 0deg)))
|
|
|
|
// Import an image, centered; optional caption (never numbered).
|
|
#let img(path, width: 82%, caption: none) = {
|
|
let im = image(path, width: width)
|
|
if caption == none {
|
|
align(center, im)
|
|
} else {
|
|
align(center, figure(im, caption: caption, numbering: none))
|
|
}
|
|
}
|
|
|
|
// Two-part slide: a narrow title part on the left, content on the right.
|
|
// Without `title`, the body takes the full width.
|
|
#let slide(body, title: none, ..args) = {
|
|
slide-counter.update(n => n + 1)
|
|
page(
|
|
footer: slide-footer(),
|
|
..args,
|
|
if title == none {
|
|
body
|
|
} else {
|
|
grid(
|
|
columns: (0.3fr, 1.1fr),
|
|
column-gutter: 2.2em,
|
|
align(top, block(
|
|
stroke: (left: hairline-v),
|
|
inset: (left: 0.9em, top: 0.4em, bottom: 0.1em),
|
|
text(font: mono, weight: "bold", size: 0.85em, tracking: 0.1em, fill: accent, upper(title)),
|
|
)),
|
|
align(top, body),
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
// Section divider slide, with a large decorative ghost numeral.
|
|
#let section-counter = counter("section")
|
|
#let section(name, note: none) = {
|
|
section-counter.step()
|
|
slide[
|
|
#align(horizon)[
|
|
#block(width: 72%, align(left)[
|
|
#context {
|
|
let n = section-counter.get().first()
|
|
place(top + right, dy: -1.1em, dx: 0.2em,
|
|
text(font: mono, size: 4.2em, weight: "bold", fill: accent.lighten(86%))[
|
|
0#str(n)
|
|
])
|
|
}
|
|
#text(font: mono, size: 0.65em, weight: "bold", tracking: 0.2em, fill: accent)[SECTION]
|
|
#v(0.4em)
|
|
#text(size: 2em, weight: "bold")[#name]
|
|
#v(0.55em)
|
|
#box(width: 4.5em, align(left, line(length: 100%, stroke: hairline-h)))
|
|
#if note != none [#v(0.5em); #text(font: mono, size: 0.7em, fill: luma(110))[#note]]
|
|
])
|
|
]
|
|
]
|
|
}
|
|
|
|
// Title slide.
|
|
#let title-slide(title, subtitle: none, author: none, date: none) = {
|
|
align(center + horizon)[
|
|
#block(width: 72%, align(left)[
|
|
#text(size: 2.1em, weight: "bold")[#title]
|
|
#if subtitle != none [#v(0.4em); #text(size: 1.15em, fill: luma(90))[#subtitle]]
|
|
#v(0.9em)
|
|
#line(length: 100%, stroke: hairline-h)
|
|
#v(0.6em)
|
|
#let meta = if author == none { date } else if date == none { author } else { author + " · " + date }
|
|
#text(font: mono, size: 0.7em, fill: luma(110))[#meta]
|
|
])
|
|
]
|
|
pagebreak()
|
|
}
|
|
|
|
// Main presentation wrapper. All options are named; the document content is
|
|
// bound to `doc` by the show rule.
|
|
#let presentation(
|
|
title: "Presentation",
|
|
subtitle: none,
|
|
author: none,
|
|
date: auto,
|
|
doc,
|
|
) = {
|
|
set page(paper: "presentation-16-9", margin: (x: 3em, y: 2.6em), fill: paper, footer: none, background: top-band)
|
|
set text(size: 20pt, fill: ink)
|
|
set par(justify: false, leading: 0.68em)
|
|
show strong: set text(fill: accent)
|
|
show list: set list(marker: (
|
|
text(fill: accent-soft)[•],
|
|
text(fill: accent-soft)[–],
|
|
))
|
|
show raw: set text(font: mono, size: 0.85em)
|
|
show raw.where(block: true): it => block(width: 100%, fill: wash, inset: 0.9em, radius: 0pt, it)
|
|
|
|
deck-title.update(title)
|
|
|
|
let d = datetime.today()
|
|
let mm = if d.month() < 10 { "0" + str(d.month()) } else { str(d.month()) }
|
|
let dd = if d.day() < 10 { "0" + str(d.day()) } else { str(d.day()) }
|
|
let the-date = if date == auto {
|
|
str(d.year()) + "-" + mm + "-" + dd
|
|
} else {
|
|
date
|
|
}
|
|
|
|
title-slide(title, subtitle: subtitle, author: author, date: the-date)
|
|
doc
|
|
}
|
|
|