Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ Every theme, font, layout, and behaviour knob lives in `preferences`. Override a
|---|---|---|
| `font` | `"Lato"` | Primary font family. Must be installed. |
| `bodySize` | `10pt` | Base text size. Sub-elements scale via em-multipliers. |
| `density` | `"comfortable"` | Single knob that scales every vertical-spacing em-token uniformly — block above/below, `v()` gaps, `par` leading/spacing, `list` spacing. `"compact"` (× 0.85) tightens for a one-page fit, `"comfortable"` (× 1.0) is the historical default, `"spacious"` (× 1.15) opens it up for print presentation. Text sizes, icon dimensions, and rating-dot geometry are unaffected — use `bodySize` for those. Unknown values panic. |
| `paper` | `"a4"` | Paper size string passed to Typst's `set page(paper: ...)`. `"a4"`, `"us-letter"`, `"a5"`, `"us-legal"`, and the rest of [Typst's named papers](https://typst.app/docs/reference/layout/page/#parameters-paper). |
| `margin` | `(x: 0.9cm, y: 1.5cm)` | Page margins. Anything `set page(margin: ...)` accepts. |
| `accent` | `palettes.teal` | Theme colour for headings, accent rules, tags, dots. Use a built-in preset — `palettes.{teal,navy,crimson,forest,plum,charcoal}` — or any `rgb(...)` value. |
Expand Down
Binary file added examples/tests/density.pdf
Binary file not shown.
14 changes: 8 additions & 6 deletions internal/header.typ
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// `_format_location` / `_url_encode` helpers that feed the maps deep
// link (single-caller helpers, kept adjacent to their consumer).

#import "state.typ": _body_size_state, _accent_state, _emphasis_colour
#import "state.typ": _body_size_state, _accent_state, _spacing_scale_state, _emphasis_colour
#import "presets.typ": maps-providers
#import "icons.typ": icon, _profile_networks, _network_aliases

Expand Down Expand Up @@ -146,11 +146,12 @@
context {
let body-size = _body_size_state.get()
let accent = _accent_state.get()
let scale = _spacing_scale_state.get()

let header-text = align(text-align, {
block(
spacing: 0pt,
below: 1.2 * body-size,
below: 1.2 * scale * body-size,
text(
2.5 * body-size,
fill: accent,
Expand All @@ -162,7 +163,7 @@
if "label" in basics and basics.label != none {
block(
spacing: 0pt,
below: 0.8 * body-size,
below: 0.8 * scale * body-size,
text(1.2 * body-size, fill: _emphasis_colour, weight: "bold", basics.label),
)
}
Expand Down Expand Up @@ -308,7 +309,7 @@
// the document width, regardless of how the text above /
// below is aligned.
let centred-photo = block(
spacing: 0.8 * body-size,
spacing: 0.8 * scale * body-size,
width: 100%,
align(center, photo),
)
Expand All @@ -335,7 +336,8 @@
let summary = basics.at("summary", default: none)
if summary == none or summary == "" or summary == [] { return }
let body-size = _body_size_state.get()
v(0.8 * body-size)
let scale = _spacing_scale_state.get()
v(0.8 * scale * body-size)
par(summary)
v(0.4 * body-size)
v(0.4 * scale * body-size)
}
6 changes: 6 additions & 0 deletions internal/layout.typ
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@
// Every spacing token is an em-multiplier of this, so changing one
// knob scales the whole document proportionally.
bodySize: 10pt,
// Scales every spacing em-token uniformly (see `_density_scales`
// in `internal/state.typ`). `"comfortable"` (1.0×) preserves the
// historical layout byte-for-byte; `"compact"` (0.85×) tightens
// for one-page CVs; `"spacious"` (1.15×) opens it up for print
// presentation. Text sizes are unaffected.
density: "comfortable",
paper: "a4",
margin: (x: 0.9cm, y: 1.5cm),
// `palettes.teal` — see the `palettes` dict for the curated set
Expand Down
18 changes: 11 additions & 7 deletions internal/primitives.typ
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
// from `lib.typ` for callers composing custom layouts; the leading-
// underscore helpers are used internally by the section renderers.

#import "state.typ": _body_size_state, _accent_state, _body_colour, _divider_colour
#import "state.typ": _body_size_state, _accent_state, _spacing_scale_state, _body_colour, _divider_colour
#import "icons.typ": icon

// Bold accent-coloured line — designed for the company / institution
// row beneath a role or education entry.
#let name(body) = context {
let body-size = _body_size_state.get()
let accent = _accent_state.get()
let scale = _spacing_scale_state.get()
block(
above: 0pt,
below: 0.6 * body-size,
below: 0.6 * scale * body-size,
text(weight: "bold", fill: accent, body),
)
}
Expand All @@ -23,9 +24,10 @@
#let term(period, location: none) = context {
if period == none and location == none { return }
let body-size = _body_size_state.get()
let scale = _spacing_scale_state.get()
block(
above: 0pt,
below: 0.8 * body-size,
below: 0.8 * scale * body-size,
inset: (left: 0.3 * body-size),
text(0.9 * body-size, {
if period != none {
Expand Down Expand Up @@ -77,12 +79,13 @@
#let divider() = context {
let body-size = _body_size_state.get()
if here().position().y < 2cm { return }
v(0.3 * body-size)
let scale = _spacing_scale_state.get()
v(0.3 * scale * body-size)
line(
length: 100%,
stroke: (paint: _divider_colour, thickness: 0.6pt, dash: "dashed"),
)
v(0.3 * body-size)
v(0.3 * scale * body-size)
}

// Like `divider()` but with a leading label that sits slightly indented
Expand All @@ -94,8 +97,9 @@
// the parent section title.
#let _labelled_divider(label) = context {
let body-size = _body_size_state.get()
let scale = _spacing_scale_state.get()
let stroke = (paint: _divider_colour, thickness: 0.6pt, dash: "dashed")
v(0.3 * body-size)
v(0.3 * scale * body-size)
pad(left: 0.6 * body-size, grid(
columns: (1.3em, auto, 1fr),
column-gutter: 0.5 * body-size,
Expand All @@ -108,7 +112,7 @@
),
line(length: 100%, stroke: stroke),
))
v(0.3 * body-size)
v(0.3 * scale * body-size)
}

// Interleaves `divider()` between items; the trailing one is suppressed
Expand Down
17 changes: 17 additions & 0 deletions internal/state.typ
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@
#let _body_size_state = state("alta-body-size", 10pt)
#let _accent_state = state("alta-accent", palettes.teal)
#let _max_rating_state = state("alta-max-rating", 5)
// Multiplier applied to every spacing em-token (block above/below,
// `v()`, par.spacing/leading, list.spacing). Driven by
// `preferences.density`; defaults to 1.0 so "comfortable" reproduces
// the historical layout byte-for-byte.
#let _spacing_scale_state = state("alta-spacing-scale", 1.0)

// `preferences.density` → multiplier on every spacing em-token.
// 0.85 / 1.0 / 1.15 keeps the three presets visibly distinct without
// either crushing lines together or pushing the one-page CV onto two.
// Text sizes, icon dimensions, and rating-dot geometry are
// deliberately left alone — density is purely vertical whitespace,
// so font-size scaling stays the job of `bodySize`.
#let _density_scales = (
compact: 0.85,
comfortable: 1.0,
spacious: 1.15,
)

// Accent is configurable via `alta(preferences: (accent: ...))`; the
// rest are opinionated visual constants.
Expand Down
30 changes: 20 additions & 10 deletions lib.typ
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// independent of text size.

#import "internal/presets.typ": palettes, maps-providers
#import "internal/state.typ": _body_size_state, _accent_state, _max_rating_state, _body_colour, _emphasis_colour
#import "internal/state.typ": _body_size_state, _accent_state, _max_rating_state, _spacing_scale_state, _density_scales, _body_colour, _emphasis_colour
#import "internal/defaults.typ": _default_labels
#import "internal/validation.typ": _strict_merge, _check_bool
#import "internal/text.typ": _present, styled-link
Expand Down Expand Up @@ -140,9 +140,19 @@
}
let accent = preferences.accent
let body-size = preferences.bodySize
let density = preferences.density
if density not in _density_scales {
let quote(k) = "\"" + k + "\""
panic(
"density must be one of " + _density_scales.keys().map(quote).join(", ")
+ ", got: " + repr(density),
)
}
let scale = _density_scales.at(density)
_accent_state.update(accent)
_body_size_state.update(body-size)
_max_rating_state.update(max-rating)
_spacing_scale_state.update(scale)

// PDF metadata is sourced from `basics` (title, author, description)
// and the JSON Resume `meta` block (date, keywords). Each optional
Expand Down Expand Up @@ -191,34 +201,34 @@
margin: preferences.margin,
footer: resolved-footer,
)
set par(leading: 0.55em, spacing: 0.7em)
set par(leading: 0.55 * scale * 1em, spacing: 0.7 * scale * 1em)
set list(
marker: text(0.85em, "•"),
indent: 0pt,
body-indent: 0.4 * body-size,
spacing: 0.55em,
spacing: 0.55 * scale * 1em,
)

// Heading levels map to semantic CV roles:
// == section title (e.g. Experience)
// === role / qualification line
// ==== sub-grouping (publication type)
show heading.where(level: 2): it => block(sticky: true)[
#v(0.6 * body-size)
#v(0.6 * scale * body-size)
#text(1.7 * body-size, fill: accent, weight: "bold", upper(it.body))
#v(-0.7 * body-size)
#v(-0.7 * scale * body-size)
#line(length: 100%, stroke: 2pt + accent)
#v(0.2 * body-size)
#v(0.2 * scale * body-size)
]
show heading.where(level: 3): it => block(
above: 1.0 * body-size,
below: 0.8 * body-size,
above: 1.0 * scale * body-size,
below: 0.8 * scale * body-size,
sticky: true,
text(1.2 * body-size, fill: _emphasis_colour, weight: "regular", it.body),
)
show heading.where(level: 4): it => block(
above: 0.6 * body-size,
below: 0.6 * body-size,
above: 0.6 * scale * body-size,
below: 0.6 * scale * body-size,
sticky: true,
text(1.2 * body-size, fill: _emphasis_colour, weight: "bold", it.body),
)
Expand Down
14 changes: 9 additions & 5 deletions sections/projects.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#import "../internal/text.typ": _present, styled-link
#import "../internal/primitives.typ": term, _join_with_dividers, _tag_row
#import "../internal/dates.typ": _format_date_range
#import "../internal/state.typ": _spacing_scale_state, _body_size_state

#let _projects(entries, labels, prefs) = {
let valid = entries.filter(p => _present(p.at("name", default: none)))
Expand All @@ -19,11 +20,14 @@
if _present(description) {
// Softer than `name()` (which is bold + accent) so the
// description doesn't compete visually with a linked title.
// Wrapped in a block so the gap to the term row below matches
// the institution-line → term spacing in `_experience` /
// `_education`; using a bare `linebreak()` here leaves no
// paragraph spacing.
block(below: 0.6em, emph(description))
// Mirrors `name()`'s `below` so the description → term gap
// matches the institution-line → term gap in `_experience` /
// `_education`; literal `0.6em` would skip the density scale
// and break that contract under non-default density.
context block(
below: 0.6 * _spacing_scale_state.get() * _body_size_state.get(),
emph(description),
)
}
term(_format_date_range(project, prefs, labels))
for bullet in project.at("highlights", default: ()) [- #bullet]
Expand Down
5 changes: 3 additions & 2 deletions sections/skills.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// `_name_keywords_section`; the two public renderers differ only in
// which `labels.*` heading they pass.

#import "../internal/state.typ": _body_size_state
#import "../internal/state.typ": _body_size_state, _spacing_scale_state
#import "../internal/primitives.typ": tag, _tag_row

// `text("-")` (not `[-]`) — markup-bracketed `-` parses as a list-item
Expand All @@ -20,7 +20,8 @@
if visible.len() == 0 { return }
context {
let body-size = _body_size_state.get()
let row-gap = 0.7 * body-size
let scale = _spacing_scale_state.get()
let row-gap = 0.7 * scale * body-size
[== #heading]
for group in visible {
block(above: 0pt, below: row-gap, par(hanging-indent: 1em, leading: row-gap, {
Expand Down
64 changes: 64 additions & 0 deletions tests/density.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// `preferences.density` scales every spacing em-token uniformly so a
// single knob trades vertical breathing room for fit-on-one-page.
// Three pages exercise each preset against the same data; visual diff
// against the default ("comfortable") confirms the multiplier reaches
// section headings, divider rules, par.spacing, the in-section block
// above/below tokens, and the header / summary gaps.

#import "../lib.typ": alta

#let cv = (
basics: (
name: "Jane Doe",
label: "Senior Software Engineer",
summary: [Backend engineer; the summary block exercises the
pre/post `v()` gaps that flank it.],
email: "jane@example.com",
phone: "+353 1 555 0100",
location: "Dublin, Ireland",
),
work: (
(
name: "Acme Corp",
position: "Senior Software Engineer",
location: "Dublin, Ireland",
startDate: "Jan 2022",
highlights: ([Led the migration.], [Designed the platform.]),
),
(
name: "Foo Ltd",
position: "Software Engineer",
startDate: "Jan 2018",
endDate: "Dec 2021",
highlights: ([Shipped the thing.],),
),
),
skills: (
(name: "Languages", keywords: ("Scala", "Python")),
(name: "Infra", keywords: ("Kafka", "AWS", "Kubernetes")),
),
languages: (
(language: "English", fluency: "Native"),
(language: "Irish", fluency: "Professional Working"),
),
education: (
(institution: "Example U", studyType: "B.Sc.", endDate: "2017"),
),
certificates: (
(name: "CKA", issuer: "CNCF"),
),
)
Comment on lines +10 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding a projects section to cover the sections/projects.typ spacing fix.

The PR objectives mention a latent bug fix in sections/projects.typ where hardcoded 0.6em spacing now scales with density. The test CV data doesn't include a projects section, so the regression test won't exercise that fix or catch future regressions in project description spacing.

📋 Suggested addition to CV data
   education: (
     (institution: "Example U", studyType: "B.Sc.", endDate: "2017"),
   ),
+  projects: (
+    (
+      name: "Open Source Tool",
+      description: [Built a CLI tool; exercises the project description spacing that was fixed.],
+      highlights: ([Released v1.0.],),
+    ),
+  ),
   certificates: (
     (name: "CKA", issuer: "CNCF"),
   ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#let cv = (
basics: (
name: "Jane Doe",
label: "Senior Software Engineer",
summary: [Backend engineer; the summary block exercises the
pre/post `v()` gaps that flank it.],
email: "jane@example.com",
phone: "+353 1 555 0100",
location: "Dublin, Ireland",
),
work: (
(
name: "Acme Corp",
position: "Senior Software Engineer",
location: "Dublin, Ireland",
startDate: "Jan 2022",
highlights: ([Led the migration.], [Designed the platform.]),
),
(
name: "Foo Ltd",
position: "Software Engineer",
startDate: "Jan 2018",
endDate: "Dec 2021",
highlights: ([Shipped the thing.],),
),
),
skills: (
(name: "Languages", keywords: ("Scala", "Python")),
(name: "Infra", keywords: ("Kafka", "AWS", "Kubernetes")),
),
languages: (
(language: "English", fluency: "Native"),
(language: "Irish", fluency: "Professional Working"),
),
education: (
(institution: "Example U", studyType: "B.Sc.", endDate: "2017"),
),
certificates: (
(name: "CKA", issuer: "CNCF"),
),
)
`#let` cv = (
basics: (
name: "Jane Doe",
label: "Senior Software Engineer",
summary: [Backend engineer; the summary block exercises the
pre/post `v()` gaps that flank it.],
email: "jane@example.com",
phone: "+353 1 555 0100",
location: "Dublin, Ireland",
),
work: (
(
name: "Acme Corp",
position: "Senior Software Engineer",
location: "Dublin, Ireland",
startDate: "Jan 2022",
highlights: ([Led the migration.], [Designed the platform.]),
),
(
name: "Foo Ltd",
position: "Software Engineer",
startDate: "Jan 2018",
endDate: "Dec 2021",
highlights: ([Shipped the thing.],),
),
),
skills: (
(name: "Languages", keywords: ("Scala", "Python")),
(name: "Infra", keywords: ("Kafka", "AWS", "Kubernetes")),
),
languages: (
(language: "English", fluency: "Native"),
(language: "Irish", fluency: "Professional Working"),
),
education: (
(institution: "Example U", studyType: "B.Sc.", endDate: "2017"),
),
projects: (
(
name: "Open Source Tool",
description: [Built a CLI tool; exercises the project description spacing that was fixed.],
highlights: ([Released v1.0.],),
),
),
certificates: (
(name: "CKA", issuer: "CNCF"),
),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/density.typ` around lines 10 - 50, The test CV data in the cv variable
is missing a projects section, which means the density spacing fix in
sections/projects.typ (where hardcoded 0.6em spacing now scales with density) is
not being exercised by the regression test. Add a projects section to the cv
data structure with appropriate project entries (name, description, startDate,
endDate, etc.) to ensure the spacing behavior is tested and any future
regressions in project description spacing are caught.


// 1. Compact — em spacing tokens × 0.85. Tighter than the default.
#alta(cv, preferences: (density: "compact"))

#pagebreak()

// 2. Comfortable — em spacing tokens × 1.0. The default; explicit
// here so the regression is anchored even if the default changes.
#alta(cv, preferences: (density: "comfortable"))

#pagebreak()

// 3. Spacious — em spacing tokens × 1.15. Roomier than the default.
#alta(cv, preferences: (density: "spacious"))
Loading