grelin_templates/tech-report/data/metropole-grelin.typ
MORRO b8b2f5fe14 Polish tech-report template layout, typography, and usage guide.
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>
2026-07-08 14:27:33 +08:00

466 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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
}