|
// ============================================================================= |
|
// Puterize Typst Template — lib.typ |
|
// ============================================================================= |
|
// Blauw-cyaan gradient huisstijl met oranje accenten. |
|
// Fonts: DM Sans (body/display) + Red Hat Mono (code). |
|
// Kleuren trouw aan favicon.svg: #0E80EB (blauw), #00CAF0 (cyaan), #F0A040 (oranje). |
|
// ============================================================================= |
|
|
|
// ── COLOURS (from favicon.svg) ──────────────────────────────────────────────── |
|
|
|
#let puterize-blue = rgb("#0E80EB") |
|
#let puterize-cyan = rgb("#00CAF0") |
|
#let puterize-orange = rgb("#F0A040") |
|
#let puterize-gold = rgb("#F6D040") |
|
|
|
#let puterize-ink = rgb("#0F172A") |
|
#let puterize-ink-soft = rgb("#334155") |
|
#let puterize-muted = rgb("#64748B") |
|
#let puterize-muted-soft = rgb("#94A3B8") |
|
|
|
#let puterize-rule = rgb("#E2E8F0") |
|
#let puterize-rule-soft = rgb("#F1F5F9") |
|
#let puterize-blue-soft = rgb("#EFF6FF") |
|
#let puterize-blue-pale = rgb("#DBEAFE") |
|
|
|
#let puterize-code-bg = rgb("#0F172A") |
|
#let puterize-code-fg = rgb("#E2E8F0") |
|
|
|
// ── FONTS ───────────────────────────────────────────────────────────────────── |
|
|
|
#let puterize-display = "DM Sans 9pt" |
|
#let puterize-body = "DM Sans 9pt" |
|
#let puterize-mono = "Red Hat Mono" |
|
|
|
// ── HELPERS ─────────────────────────────────────────────────────────────────── |
|
|
|
/// Convert content to a plain string (for accent-word splitting). |
|
/// Handles special characters like apostrophes gracefully. |
|
#let to-string(content) = { |
|
let s = repr(content) |
|
// Strip outer brackets if the content was a sequence |
|
if s.starts-with("[") and s.ends-with("]") { |
|
s = s.slice(1, s.len() - 1) |
|
} |
|
// Collapse whitespace |
|
s.split(" ").filter(w => w != "").join(" ") |
|
} |
|
|
|
/// Pad a number to two digits: 1 → "01". |
|
#let pad2(n) = { |
|
let s = str(n) |
|
if s.len() < 2 { "0" + s } else { s } |
|
} |
|
|
|
/// Title where the *last word* is coloured blue. |
|
/// Ends with an orange dot — the signature Puterize punctuation. |
|
/// Falls back gracefully when content contains special characters. |
|
#let accent-title(title, size: 32pt) = { |
|
let raw-str = to-string(title) |
|
let words = raw-str.split(" ") |
|
// Detect if repr() leaked Typst syntax — fall back to plain rendering |
|
let is-clean = words.all(w => |
|
not w.contains("(") and |
|
not w.contains(")") and |
|
not w.contains("[") and |
|
not w.contains("]") and |
|
not w.contains("#") and |
|
not w.contains("smartquote") |
|
) |
|
if not is-clean or words.len() <= 1 { |
|
// Fallback: render title as-is without word-splitting |
|
[ |
|
#set text(size: size, weight: 700, font: puterize-display) |
|
#title |
|
#text(fill: puterize-orange, size: size * 1.15)[.] |
|
] |
|
} else { |
|
let last = words.pop() |
|
let pre = words.join(" ") |
|
[ |
|
#set text(size: size, weight: 700, font: puterize-display) |
|
#if pre != "" [ |
|
#pre |
|
#h(0.15em) |
|
] |
|
#text(fill: puterize-blue)[#last]#text(fill: puterize-orange, size: size * 1.15)[.] |
|
] |
|
} |
|
} |
|
|
|
/// A thin horizontal rule with blue→cyan linear gradient. |
|
#let gradient-rule(width: 100%, height: 4pt) = { |
|
block( |
|
width: width, |
|
height: height, |
|
fill: gradient.linear( |
|
(puterize-blue, 0%), |
|
(puterize-cyan, 100%), |
|
angle: 90deg, |
|
), |
|
radius: 0.5pt, |
|
) |
|
} |
|
|
|
// ── TEMPLATE ────────────────────────────────────────────────────────────────── |
|
|
|
#let puterize-doc( |
|
// cover |
|
title-pre: "Project", |
|
title-accent: "document", |
|
eyebrow: "Technisch document · Puterize", |
|
subtitle: "", |
|
// meta |
|
doc-id: "PUTERIZE · DOC", |
|
doc-name: "Document", |
|
version: "1.0", |
|
author: "Kaiyin Zhong", |
|
distribution: "Intern team", |
|
// header / footer |
|
brand-tag: "Puterize · Infrastructuur · 2026", |
|
footer-tag: "Puterize · Intern document", |
|
date: none, |
|
// content |
|
toc-intro: none, |
|
body, |
|
) = { |
|
// derive date once |
|
let the-date = if date != none { date } else { |
|
datetime.today().display("[day]-[month]-[year]") |
|
} |
|
let toc-text = if toc-intro != none { toc-intro } else { subtitle } |
|
|
|
// ── DOCUMENT-WIDE SETTINGS ────────────────────────────────────────────────── |
|
|
|
set document(title: title-pre + " " + title-accent) |
|
set text( |
|
size: 10pt, |
|
font: puterize-body, |
|
tracking: -0.005em, |
|
lang: "nl", |
|
) |
|
set par( |
|
leading: 0.78em, |
|
spacing: 1.0em, |
|
justify: false, |
|
) |
|
|
|
// ── INLINE MARKUP ─────────────────────────────────────────────────────────── |
|
|
|
show strong: it => { |
|
text(weight: 600, fill: puterize-ink, it) |
|
} |
|
|
|
show emph: it => { |
|
text(style: "italic", fill: puterize-ink-soft, it) |
|
} |
|
|
|
show link: it => { |
|
set text(fill: puterize-blue, weight: 500) |
|
underline( |
|
stroke: (paint: puterize-cyan, thickness: 0.6pt, dash: "dotted"), |
|
offset: 2pt, |
|
it, |
|
) |
|
} |
|
|
|
// Inline code — light blue chip |
|
show raw.where(block: false): it => { |
|
box( |
|
fill: puterize-blue-soft, |
|
stroke: (paint: puterize-blue-pale, thickness: 0.5pt), |
|
radius: 3pt, |
|
outset: (y: 1.5pt, x: 3.5pt), |
|
inset: (y: 1pt, x: 0pt), |
|
text(font: puterize-mono, size: 9pt, fill: puterize-ink, it), |
|
) |
|
} |
|
|
|
// ── BLOCK CODE ────────────────────────────────────────────────────────────── |
|
|
|
show raw.where(block: true): it => { |
|
block( |
|
fill: puterize-code-bg, |
|
stroke: (left: (paint: puterize-blue, thickness: 3pt)), |
|
radius: 6pt, |
|
inset: (top: 14pt, bottom: 14pt, left: 16pt, right: 16pt), |
|
outset: (y: 10pt), |
|
)[ |
|
#text( |
|
font: puterize-mono, |
|
size: 8.5pt, |
|
fill: puterize-code-fg, |
|
it, |
|
) |
|
] |
|
} |
|
|
|
// ── LISTS ─────────────────────────────────────────────────────────────────── |
|
|
|
// All lists — orange markers, comfortable spacing |
|
show list: it => { |
|
set list( |
|
marker: text(fill: puterize-orange, weight: 700, size: 1.1em)[•], |
|
indent: 2pt, |
|
body-indent: 8pt, |
|
) |
|
it |
|
} |
|
|
|
// ── TABLES ────────────────────────────────────────────────────────────────── |
|
// Set table defaults globally (does not affect grid elements like cover grids) |
|
|
|
set table( |
|
stroke: ( |
|
top: (paint: puterize-rule, thickness: 0.5pt), |
|
bottom: (paint: puterize-rule, thickness: 0.5pt), |
|
y: (paint: puterize-rule, thickness: 0.3pt), |
|
x: (paint: puterize-rule, thickness: 0.3pt), |
|
), |
|
inset: (x: 10pt, y: 7pt), |
|
fill: (x, y) => if y == 0 { puterize-blue-soft }, |
|
rows: 1, |
|
) |
|
|
|
// ── CALLOUTS (blockquote override) — panel accent with gradient top-stripe ── |
|
|
|
show quote.where(block: true): it => { |
|
block( |
|
fill: puterize-blue-soft, |
|
stroke: (paint: puterize-blue-pale, thickness: 0.8pt), |
|
radius: 8pt, |
|
inset: (top: 16pt, bottom: 14pt, left: 16pt, right: 16pt), |
|
outset: (y: 12pt), |
|
)[ |
|
// Gradient top-stripe via a zero-height block the full width |
|
#block( |
|
width: 100%, |
|
height: 3pt, |
|
fill: gradient.linear( |
|
(puterize-blue, 0%), |
|
(rgb("#DBEAFE"), 100%), |
|
angle: 0deg, |
|
), |
|
) |
|
#v(2pt) |
|
#text(font: puterize-mono, size: 7.5pt, fill: puterize-orange, weight: 600)[OPMERKING] |
|
#v(6pt) |
|
#text(size: 10pt, fill: puterize-ink-soft, it.body) |
|
] |
|
} |
|
|
|
// ── HEADINGS ──────────────────────────────────────────────────────────────── |
|
|
|
// Enable heading numbering for TOC (our show rules override display) |
|
set heading(numbering: "1") |
|
|
|
// Chapter counter for H1 display (start at 0, increments before each chapter) |
|
let ch-num = counter("ch-num") |
|
// Don't call update(0) which triggers increment; we start at 0 naturally |
|
|
|
// H1 — Chapter: page break → watermarked number → accent title → gradient rule |
|
show heading.where(level: 1): it => { |
|
pagebreak(weak: false) |
|
ch-num.step() |
|
let n = ch-num.get().first() |
|
|
|
v(4pt) |
|
align(left)[ |
|
// Watermarked section number — 48pt, very faint, behind the title |
|
#place( |
|
top + left, |
|
dx: -6pt, |
|
dy: 0pt, |
|
)[ |
|
#text( |
|
font: puterize-display, |
|
size: 48pt, |
|
weight: 700, |
|
fill: puterize-rule-soft, |
|
tracking: -0.02em, |
|
)[#pad2(n)] |
|
] |
|
// Chapter label |
|
#text(font: puterize-mono, size: 8pt, fill: puterize-blue, weight: 600)[ |
|
HOOFDSTUK #pad2(n) |
|
] |
|
#v(4pt) |
|
#accent-title(it.body, size: 28pt) |
|
#v(10pt) |
|
#gradient-rule(width: 100%, height: 3pt) |
|
#v(16pt) |
|
] |
|
} |
|
|
|
// H2 — orange square marker + DM Sans SemiBold |
|
show heading.where(level: 2): it => { |
|
v(22pt) |
|
[ |
|
#block( |
|
fill: puterize-orange, |
|
width: 8pt, |
|
height: 8pt, |
|
radius: 1.2pt, |
|
) |
|
#h(8pt) |
|
#text(font: puterize-display, weight: 600, size: 14pt, fill: puterize-ink, it.body) |
|
] |
|
v(8pt) |
|
} |
|
|
|
// H3 — DM Sans Medium, softer ink |
|
show heading.where(level: 3): it => { |
|
v(14pt) |
|
[ |
|
#text(font: puterize-display, weight: 500, size: 11.5pt, fill: puterize-ink-soft, it.body) |
|
] |
|
v(5pt) |
|
} |
|
|
|
// ── COVER PAGE ────────────────────────────────────────────────────────────── |
|
|
|
let cover = { |
|
set page( |
|
paper: "a4", |
|
margin: (top: 48mm, bottom: 28mm, left: 28mm, right: 28mm), |
|
fill: puterize-blue-soft, |
|
) |
|
// Cover has no header/footer |
|
set page(numbering: none) |
|
|
|
// Top gradient bar |
|
place(top + left, dx: 0pt, dy: 0pt)[ |
|
#gradient-rule(width: 100%, height: 5pt) |
|
] |
|
|
|
// Eyebrow |
|
text(font: puterize-mono, size: 9pt, fill: puterize-muted)[#eyebrow] |
|
|
|
v(18pt) |
|
|
|
// Title — DM Sans Black, 52pt |
|
text(font: puterize-display, size: 52pt, tracking: -0.035em, fill: puterize-ink)[ |
|
#title-pre |
|
#h(0.2em) |
|
#text(fill: puterize-blue)[#title-accent]#text(fill: puterize-orange, size: 58pt)[.] |
|
] |
|
|
|
v(12pt) |
|
|
|
// Subtitle |
|
if subtitle != "" [ |
|
text(font: puterize-body, size: 13pt, fill: puterize-ink-soft, style: "italic")[#subtitle] |
|
v(8pt) |
|
] |
|
|
|
// Bottom bar with meta |
|
place(bottom + left, dx: 0pt, dy: 0pt)[ |
|
#block( |
|
width: 100%, |
|
stroke: (top: (paint: puterize-blue, thickness: 1pt)), |
|
)[ |
|
#v(14pt) |
|
#grid( |
|
columns: (1fr, 1fr), |
|
row-gutter: 8pt, |
|
column-gutter: 16pt, |
|
// Left: brand |
|
[ |
|
#text(font: puterize-display, weight: 700, size: 12pt, fill: puterize-blue)[Puterize] |
|
#v(2pt) |
|
#text(font: puterize-mono, size: 7.5pt, fill: puterize-muted)[Document template] |
|
], |
|
// Right: meta table |
|
[ |
|
#set text(font: puterize-mono, size: 7.5pt) |
|
#grid( |
|
columns: (auto, 1fr), |
|
column-gutter: 16pt, |
|
row-gutter: 4pt, |
|
[Document:], text(fill: puterize-ink, weight: 500)[#doc-name], |
|
[Versie:], text(fill: puterize-ink, weight: 500)[#version], |
|
[Datum:], text(fill: puterize-ink, weight: 500)[#the-date], |
|
[Auteur:], text(fill: puterize-ink, weight: 500)[#author], |
|
[Distributie:], text(fill: puterize-ink, weight: 500)[#distribution], |
|
) |
|
], |
|
) |
|
] |
|
] |
|
} |
|
|
|
cover |
|
|
|
// ── PAGE SETUP (after cover) ──────────────────────────────────────────────── |
|
|
|
set page( |
|
paper: "a4", |
|
margin: (top: 28mm, bottom: 24mm, left: 28mm, right: 28mm), |
|
// header |
|
header: [ |
|
#set text(font: puterize-mono, size: 7.5pt) |
|
#block( |
|
width: 100%, |
|
stroke: (bottom: (paint: puterize-blue, thickness: 0.8pt)), |
|
)[ |
|
#grid( |
|
columns: (1fr, 1fr), |
|
[ |
|
#text(fill: puterize-muted)[#doc-id] |
|
], |
|
align(right)[ |
|
#text(fill: puterize-muted)[#brand-tag] |
|
], |
|
) |
|
#v(6pt) |
|
] |
|
], |
|
// footer |
|
footer: [ |
|
#set text(font: puterize-mono, size: 7.5pt) |
|
#block( |
|
width: 100%, |
|
stroke: (top: (paint: puterize-blue, thickness: 0.8pt)), |
|
)[ |
|
#v(6pt) |
|
#grid( |
|
columns: (1fr, 1fr), |
|
[ |
|
#text(fill: puterize-muted)[#footer-tag] |
|
], |
|
align(right)[ |
|
#text(fill: puterize-muted-soft)[#context { counter(page).display() }] |
|
], |
|
) |
|
] |
|
], |
|
numbering: "1", |
|
footer-descent: 6pt, |
|
header-ascent: 6pt, |
|
) |
|
|
|
// ── TABLE OF CONTENTS ─────────────────────────────────────────────────────── |
|
|
|
pagebreak(weak: false) |
|
|
|
v(10pt) |
|
align(left)[ |
|
#text(font: puterize-mono, size: 9pt, fill: puterize-blue, weight: 600)[INHOUD] |
|
#v(6pt) |
|
#accent-title([Wat in dit document staat], size: 30pt) |
|
] |
|
|
|
v(14pt) |
|
|
|
if toc-text != "" [ |
|
text(font: puterize-body, size: 11pt, fill: puterize-ink-soft)[#toc-text] |
|
v(20pt) |
|
] |
|
|
|
// TOC with custom-styled entries via a fresh counter |
|
let toc-counter = counter("toc-num") |
|
toc-counter.update(0) |
|
|
|
show outline.entry: it => { |
|
toc-counter.step() |
|
let n = toc-counter.get().first() |
|
if it.level == 1 { |
|
[ |
|
#text(font: puterize-mono, size: 8.5pt, fill: puterize-blue, weight: 600)[#pad2(n)] |
|
#h(10pt) |
|
#text(font: puterize-display, size: 12pt, weight: 600, fill: puterize-ink)[#it.element.body] |
|
#v(3pt) |
|
#line( |
|
length: 100%, |
|
stroke: (paint: puterize-rule, thickness: 0.5pt, dash: "dashed"), |
|
) |
|
#v(12pt) |
|
] |
|
} |
|
} |
|
|
|
set outline( |
|
indent: 0pt, |
|
) |
|
|
|
outline(target: heading.where(level: 1)) |
|
|
|
pagebreak(weak: false) |
|
|
|
// ── CONTENT ───────────────────────────────────────────────────────────────── |
|
|
|
body |
|
} |