Refine TOC and cover styling, add numbered headings and gray body text, unify #f6f7f8 surfaces, and replace sample content with a Markdown usage manual. Co-authored-by: Cursor <cursoragent@cursor.com>
466 lines
11 KiB
Plaintext
466 lines
11 KiB
Plaintext
#let transit-red = rgb("#c53a2f")
|
||
#import "theme.typ": color-quote, code-block-radius, code-inline-radius, quote-bar-stroke, quote-bar-color, color-body-text, color-block-bg, tw
|
||
#let metro-blue = rgb("#005f9e")
|
||
#let deep-teal = rgb("#0f766e")
|
||
#let burnt-orange = rgb("#c96b2c")
|
||
#let emerald = rgb("#1f7a4f")
|
||
#let deep-violet = rgb("#5a4fcf")
|
||
|
||
#let page-numbering-started = state("page-numbering-started", false)
|
||
|
||
#let start-body-page-numbering() = {
|
||
page-numbering-started.update(true)
|
||
counter(page).update(1)
|
||
}
|
||
|
||
#let metropole(
|
||
// -- Metadata --
|
||
title: none,
|
||
subtitle: none,
|
||
author: none,
|
||
date: none,
|
||
// -- Document --
|
||
paper-size: "a4",
|
||
language: "en",
|
||
date-format: "[day] [month repr:long] [year]",
|
||
cover-page: false,
|
||
inline-title: true,
|
||
show-header: false,
|
||
// -- Page margins (override metropole defaults) --
|
||
margin-x: none,
|
||
margin-y: none,
|
||
header-skip: none,
|
||
// -- Fonts --
|
||
font-size: 11pt,
|
||
leading-ratio: 1.75,
|
||
body-font: "Source Serif 4",
|
||
heading-font: "Source Sans 3",
|
||
raw-font: "Source Code Pro",
|
||
// -- Colors --
|
||
background-color: rgb("#ffffff"),
|
||
foreground-color: rgb("000000"),
|
||
body-text-color: none,
|
||
accent-color: transit-red,
|
||
// -- Body --
|
||
body,
|
||
) = {
|
||
// ── Type scale (major third × 1.25) --
|
||
let scale = 1.25
|
||
let fine-size = font-size * calc.pow(scale, -2) // ~7pt
|
||
let caption-size = font-size * calc.pow(scale, -1) // ~8.8pt
|
||
let lead-size = font-size * calc.pow(scale, 1) // ~13.75pt
|
||
let heading-size = font-size * calc.pow(scale, 2) // ~17.2pt
|
||
let display-size = font-size * calc.pow(scale, 3) // ~21.5pt
|
||
let title-size = font-size * calc.pow(scale, 4) // ~26.8pt
|
||
|
||
// ── Derived colors --
|
||
let secondary-content-color = color.mix(
|
||
(foreground-color, 5),
|
||
(background-color, 3),
|
||
space: oklab,
|
||
)
|
||
let secondary-accent-color = color.mix(
|
||
(accent-color, 2),
|
||
(background-color, 1),
|
||
space: oklab,
|
||
)
|
||
let link-color = color.mix(
|
||
(accent-color, 3),
|
||
(foreground-color, 1),
|
||
space: oklab,
|
||
)
|
||
|
||
// ── Baseline grid --
|
||
let line-height = font-size * leading-ratio
|
||
|
||
// ── Spatial system (multiples of line-height) --
|
||
let margin-vertical = if margin-y != none { margin-y } else { line-height * 2.2 }
|
||
let margin-horizontal = if margin-x != none { margin-x } else { line-height * 2.8 }
|
||
let header-gap = if header-skip != none { header-skip } else { line-height * 1.1 }
|
||
|
||
// ── Configure metadata --
|
||
set document(title: title, date: date)
|
||
if author != none {
|
||
set document(author: author)
|
||
}
|
||
|
||
// ── Configure text --
|
||
let body-fill = if body-text-color != none { body-text-color } else { foreground-color }
|
||
set text(
|
||
font: body-font,
|
||
size: font-size,
|
||
fill: body-fill,
|
||
ligatures: true,
|
||
discretionary-ligatures: true,
|
||
lang: language,
|
||
)
|
||
show raw: set text(font: raw-font)
|
||
|
||
// ── Configure paragraphs --
|
||
set par(
|
||
leading: line-height - font-size,
|
||
spacing: line-height,
|
||
justify: true,
|
||
justification-limits: (
|
||
spacing: (
|
||
min: 100% * 2 / 3,
|
||
max: 100% * 3 / 2,
|
||
),
|
||
tracking: (
|
||
min: -0.01em,
|
||
max: 0.01em,
|
||
),
|
||
),
|
||
)
|
||
|
||
// ── Configure page --
|
||
set page(
|
||
paper: paper-size,
|
||
fill: background-color,
|
||
margin: (
|
||
x: margin-horizontal,
|
||
top: margin-vertical,
|
||
bottom: margin-vertical,
|
||
),
|
||
header: if show-header {
|
||
context {
|
||
if counter(page).get().first() > 1 {
|
||
v(header-gap)
|
||
grid(
|
||
columns: (1fr, 1fr),
|
||
align(left, text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
weight: "medium",
|
||
if author != none { author } else { "" },
|
||
)),
|
||
align(right, text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
weight: "medium",
|
||
title,
|
||
)),
|
||
)
|
||
v(line-height / 2)
|
||
}
|
||
}
|
||
} else {
|
||
none
|
||
},
|
||
header-ascent: if show-header { line-height / 2 } else { 0pt },
|
||
footer: context {
|
||
v(line-height / 2)
|
||
if page-numbering-started.at(here()) {
|
||
align(right, text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
counter(page).display("1 / 1", both: true),
|
||
))
|
||
}
|
||
},
|
||
footer-descent: line-height,
|
||
)
|
||
|
||
// ── Configure headings --
|
||
let report-heading(it, size, above, below, title-fill: foreground-color) = block(
|
||
width: 100%,
|
||
breakable: false,
|
||
above: above,
|
||
below: below,
|
||
)[
|
||
#context {
|
||
if it.numbering != none {
|
||
let number = counter(heading).display(it.numbering)
|
||
text(font: heading-font, size: size, weight: "bold")[
|
||
#text(fill: accent-color)[#number]
|
||
#h(0.35em)
|
||
#text(fill: title-fill)[#it.body]
|
||
]
|
||
} else {
|
||
text(
|
||
font: heading-font,
|
||
size: size,
|
||
weight: "bold",
|
||
fill: title-fill,
|
||
it.body,
|
||
)
|
||
}
|
||
}
|
||
]
|
||
|
||
show heading.where(level: 1): it => {
|
||
report-heading(it, heading-size, line-height * 2, line-height, title-fill: accent-color)
|
||
}
|
||
|
||
show heading.where(level: 2): it => {
|
||
report-heading(it, lead-size, line-height * 2, line-height * 0.85)
|
||
}
|
||
|
||
show heading.where(level: 3): it => {
|
||
report-heading(it, font-size, line-height, line-height * 0.75)
|
||
}
|
||
|
||
show heading.where(level: 4): it => {
|
||
report-heading(it, caption-size, line-height * 0.85, line-height * 0.65)
|
||
}
|
||
|
||
// ── Configure lists --
|
||
set list(
|
||
indent: 0pt,
|
||
body-indent: line-height / 2,
|
||
marker: (
|
||
text(
|
||
font: heading-font,
|
||
fill: accent-color,
|
||
weight: "bold",
|
||
)[•],
|
||
text(
|
||
font: heading-font,
|
||
fill: accent-color,
|
||
weight: "bold",
|
||
)[‣],
|
||
text(
|
||
font: heading-font,
|
||
fill: accent-color,
|
||
weight: "bold",
|
||
)[–],
|
||
),
|
||
)
|
||
set enum(
|
||
indent: 0pt,
|
||
body-indent: line-height / 2,
|
||
numbering: (..nums) => text(
|
||
font: heading-font,
|
||
fill: accent-color,
|
||
weight: "medium",
|
||
numbering("1.a.i.", ..nums),
|
||
),
|
||
)
|
||
set terms(
|
||
indent: 0pt,
|
||
separator: h(0.5em, weak: true),
|
||
)
|
||
show terms.item: it => par({
|
||
text(
|
||
font: heading-font,
|
||
weight: "medium",
|
||
fill: accent-color,
|
||
it.term,
|
||
)
|
||
h(0.5em)
|
||
it.description
|
||
})
|
||
|
||
// -- Configure block quotes --
|
||
show quote.where(block: true): it => {
|
||
v(line-height * 0.75, weak: true)
|
||
block(width: 100%)[
|
||
#block(
|
||
width: 100%,
|
||
inset: (left: tw(3.5)),
|
||
stroke: (left: quote-bar-stroke + quote-bar-color),
|
||
)[
|
||
#text(font: body-font, fill: color-body-text, it.body)
|
||
#if it.attribution != none {
|
||
v(line-height / 2, weak: true)
|
||
align(right, text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
[— #it.attribution],
|
||
))
|
||
}
|
||
]
|
||
]
|
||
v(line-height * 0.75, weak: true)
|
||
}
|
||
|
||
// -- Configure tables --
|
||
show table: it => {
|
||
if it.has("label") and it.label == <table-full-width> {
|
||
it
|
||
} else {
|
||
[#table(
|
||
columns: (1fr,) * it.columns.len(),
|
||
..it.children,
|
||
)#label("table-full-width")]
|
||
}
|
||
}
|
||
set table(
|
||
stroke: none,
|
||
inset: (x: line-height / 2, y: line-height / 2),
|
||
fill: (x, y) => if y == 0 {
|
||
accent-color
|
||
} else if calc.even(y) {
|
||
color-block-bg
|
||
} else {
|
||
none
|
||
},
|
||
)
|
||
show table.cell.where(y: 0): it => {
|
||
text(
|
||
font: heading-font,
|
||
weight: "bold",
|
||
size: caption-size,
|
||
fill: background-color,
|
||
it,
|
||
)
|
||
}
|
||
|
||
// ── Configure code --
|
||
show raw.where(block: true): block.with(
|
||
inset: (x: line-height, y: line-height / 2),
|
||
width: 100%,
|
||
fill: color-block-bg,
|
||
radius: code-block-radius,
|
||
)
|
||
show raw.where(block: true): set par(justify: false)
|
||
show raw.where(block: false): box.with(
|
||
inset: (x: 3pt, y: 0pt),
|
||
outset: (y: 3pt),
|
||
fill: color-block-bg,
|
||
radius: code-inline-radius,
|
||
)
|
||
show raw.where(block: false): set text(fill: foreground-color)
|
||
|
||
// ── Configure links --
|
||
show link: it => {
|
||
if type(it.dest) != str {
|
||
it
|
||
} else {
|
||
text(fill: accent-color, underline(it.body))
|
||
}
|
||
}
|
||
|
||
// ── Title --
|
||
if cover-page {
|
||
// Full cover page
|
||
page(
|
||
margin: 0pt,
|
||
header: none,
|
||
footer: none,
|
||
)[
|
||
#place(left + top, rect(
|
||
width: line-height,
|
||
height: 100%,
|
||
fill: accent-color,
|
||
))
|
||
#pad(
|
||
left: line-height + margin-horizontal,
|
||
right: margin-horizontal,
|
||
top: margin-vertical * 2,
|
||
bottom: margin-vertical,
|
||
)[
|
||
#v(1fr)
|
||
#text(
|
||
font: heading-font,
|
||
size: title-size,
|
||
weight: "bold",
|
||
upper(title),
|
||
)
|
||
#v(line-height)
|
||
#if subtitle != none {
|
||
text(
|
||
font: heading-font,
|
||
size: heading-size,
|
||
weight: "light",
|
||
subtitle,
|
||
)
|
||
v(line-height)
|
||
}
|
||
#v(2fr)
|
||
#if author != none {
|
||
grid(
|
||
columns: (1fr, 1fr),
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
weight: "medium",
|
||
author,
|
||
),
|
||
align(right, if date != none {
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
date.display(date-format),
|
||
)
|
||
}),
|
||
)
|
||
} else if date != none {
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
date.display(date-format),
|
||
)
|
||
}
|
||
]
|
||
]
|
||
} else if inline-title {
|
||
// Inline title block on first page
|
||
// Accent bar bleeding to page edges
|
||
place(
|
||
left + top,
|
||
dx: -margin-horizontal,
|
||
dy: -margin-vertical,
|
||
rect(
|
||
width: line-height,
|
||
height: 100% + margin-vertical * 2,
|
||
fill: accent-color,
|
||
),
|
||
)
|
||
// Title block
|
||
v(line-height)
|
||
text(
|
||
font: heading-font,
|
||
size: display-size,
|
||
weight: "bold",
|
||
upper(title),
|
||
)
|
||
v(line-height / 2)
|
||
if subtitle != none {
|
||
text(
|
||
font: heading-font,
|
||
size: lead-size,
|
||
weight: "light",
|
||
subtitle,
|
||
)
|
||
v(line-height / 2)
|
||
}
|
||
if author != none {
|
||
grid(
|
||
columns: (1fr, 1fr),
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
weight: "medium",
|
||
author,
|
||
),
|
||
align(right, if date != none {
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
date.display("[day] [month repr:long] [year]"),
|
||
)
|
||
}),
|
||
)
|
||
} else if date != none {
|
||
text(
|
||
font: heading-font,
|
||
size: caption-size,
|
||
fill: secondary-content-color,
|
||
date.display("[day] [month repr:long] [year]"),
|
||
)
|
||
}
|
||
v(line-height * 2)
|
||
}
|
||
|
||
// ── Body --
|
||
body
|
||
}
|