From 0a5ee63ceb1fc42894d2386fc8166d8b30e10406 Mon Sep 17 00:00:00 2001 From: Shane Murphy Date: Sun, 14 Jun 2026 21:07:49 +0200 Subject: [PATCH 1/2] feat: export themes preset bundles for coherent visual styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a module-scope `themes` dict — `classic`, `modern`, `minimal` — of vetted preset bundles. Each theme bundles `accent`, `font`, `columnRatio`, and `headerTextAlign` — the four knobs that interact visually. Callers spread a theme over their own overrides: #import "@preview/altacv:1.1.0": alta, themes #alta(cv, preferences: themes.modern + (imageSize: 7em)) Closes #58. Design: - `classic` re-declares current defaults — themes are by-name bundles, not pointers to "whatever the default is right now". A future defaults change will not silently shift `classic`. - `imagePosition` deliberately omitted: it is only meaningful when `basics.image` is set, so baking it into every bundle would either force image-only assumptions or no-op for image-less CVs. - Themes use `palettes.*` for accents (not raw hex), so any future palette adjustment ripples through. - `modern` (`Inter`) and `minimal` (`Source Sans 3`) fonts are aspirational — neither is on CI. The fixture overrides `font: "Lato"` for those themes so `make test` is silent; users with the named font installed get the intended typography, others get Typst's standard font-fallback warning. README flags this. --- README.md | 26 +++++++++++++++++++- examples/tests/themes.pdf | Bin 0 -> 47844 bytes internal/presets.typ | 46 +++++++++++++++++++++++++++++++++++ lib.typ | 2 +- tests/themes.typ | 49 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 examples/tests/themes.pdf create mode 100644 tests/themes.typ diff --git a/README.md b/README.md index edaedd1..66e6aa0 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,29 @@ Reorder by overriding either array; both are concatenated in order: Drop the portrait via `basics.image: none` for a fully text-only header. +### Theme presets + +`themes` is a dict of vetted preset bundles — coherent combinations of `accent`, `font`, `columnRatio`, and `headerTextAlign` that are tedious to dial in by hand. Each theme is a partial `preferences` dict; spread it over your own overrides: + +```typst +#import "@preview/altacv:1.1.1": alta, themes // x-release-please-version + +#alta(cv, preferences: themes.modern) + +// Spread + override: any preferences key (built-in or theme) can be +// tweaked after the spread, so you keep the theme's identity but +// adjust individual knobs. +#alta(cv, preferences: themes.modern + (imageSize: 7em)) +``` + +| Theme | Accent | Font | Layout | +|---|---|---|---| +| `classic` | `palettes.teal` | `Lato` | 64/36 split, left-aligned header | +| `modern` | `palettes.navy` | `Inter` | 50/50 split, centred header | +| `minimal` | `palettes.charcoal` | `Source Sans 3` | 55/45 split, left-aligned header | + +Themes only touch keys that interact visually — anything they don't set (paper, margins, image sizing, section order, …) falls back to `_default_preferences`. The `Inter` and `Source Sans 3` fonts must be installed on the build host; missing fonts fall back to Typst's defaults rather than panicking, so a missing font yields a warning but still renders. + ### Labels All display strings the template emits. Override any subset via `labels:`; the rest fall back to English defaults. Unknown keys panic. Use for translation or local renaming. @@ -470,10 +493,11 @@ The defaults live in [`internal/labels-en.toml`](internal/labels-en.toml) — a | `divider()` | Dashed grey rule used between entries within a section. | | `styled-link(content, dest: none)` | Accent-coloured italic styling for entry titles (publications, awards, projects). Wraps in a link when `dest` is supplied. | | `palettes` | Dict of curated accent presets — `teal`, `navy`, `crimson`, `forest`, `plum`, `charcoal`. Use as `accent: palettes.navy`. | +| `themes` | Dict of vetted preset bundles — `classic`, `modern`, `minimal`. Each is a partial `preferences` dict combining accent, font, and layout. Use as `preferences: themes.modern`. | | `maps-providers` | Dict of map deep-link URL templates — `google`, `apple`, `bing`, `duckduckgo`, `osm`. Use as `mapsProvider: maps-providers.osm`. | ```typst -#import "@preview/altacv:1.1.1": alta, tag, divider, palettes, maps-providers // x-release-please-version +#import "@preview/altacv:1.1.1": alta, tag, divider, palettes, themes, maps-providers // x-release-please-version ``` The contact bar is rendered from `basics.email`, `basics.phone`, `basics.location`, `basics.url`, `basics.profiles`. Visual separators are stripped from the `tel:` dialable part. Suppress or swap deep links via `preferences.linkContactInfo` and `preferences.mapsProvider`. diff --git a/examples/tests/themes.pdf b/examples/tests/themes.pdf new file mode 100644 index 0000000000000000000000000000000000000000..72ef40d7408f0df8c7f7bc7073c44de2708b4825 GIT binary patch literal 47844 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
  • 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 Date: Sun, 14 Jun 2026 22:31:38 +0200 Subject: [PATCH 2/2] refactor(themes): align classic with default columnRatio + simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `themes.classic.columnRatio: 0.64 → 0.65` so the 'canonical alta look' matches `_default_preferences.columnRatio: 0.65`. Update the README table cell accordingly (64/36 → 65/35). - Trim the 27-line preamble above `themes` in `internal/presets.typ` to match the brevity of sibling presets (`palettes`, `maps-providers`); the spread-and-override usage now lives only in the README to remove the source-vs-docs drift surface. - Replace the three hand-written `#alta(...)` calls in `tests/themes.typ` with a `for key in themes.keys()` loop mirroring `tests/palettes.typ`, so adding a future theme auto-extends the fixture instead of needing a parallel edit. - Add `assert.eq` pins on `themes.{classic,modern,minimal}.font` at the top of the fixture so a typo in the declared theme font is caught there (the CI-host font fallback would otherwise mask it via an identical-looking snapshot). --- README.md | 2 +- examples/tests/themes.pdf | Bin 47844 -> 47844 bytes internal/presets.typ | 29 ++++++-------------------- tests/themes.typ | 42 ++++++++++++++++++-------------------- 4 files changed, 27 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 66e6aa0..a0aea65 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,7 @@ Drop the portrait via `basics.image: none` for a fully text-only header. | Theme | Accent | Font | Layout | |---|---|---|---| -| `classic` | `palettes.teal` | `Lato` | 64/36 split, left-aligned header | +| `classic` | `palettes.teal` | `Lato` | 65/35 split, left-aligned header | | `modern` | `palettes.navy` | `Inter` | 50/50 split, centred header | | `minimal` | `palettes.charcoal` | `Source Sans 3` | 55/45 split, left-aligned header | diff --git a/examples/tests/themes.pdf b/examples/tests/themes.pdf index 72ef40d7408f0df8c7f7bc7073c44de2708b4825..8c8826a30819ea1f88c3fe88c4ef5dc7c89a05ec 100644 GIT binary patch delta 1177 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 diff --git a/internal/presets.typ b/internal/presets.typ index 051af60..d08e926 100644 --- a/internal/presets.typ +++ b/internal/presets.typ @@ -25,36 +25,19 @@ osm: "https://www.openstreetmap.org/search?query={q}", ) -// Vetted preset bundles of preferences that hang together visually. -// Each theme is a partial `preferences` dict — accent + font + the -// layout knobs that interact with them. Callers spread a theme over -// their own overrides: +// Vetted preset bundles spread over caller overrides via dict-merge: // // #alta(cv, preferences: themes.modern + (imageSize: 7em)) // -// Themes only touch the knobs that interact (accent, font, columnRatio, -// headerTextAlign); everything else falls back to `_default_preferences`, -// so a theme stays a small, focused identity rather than a full config. -// -// `imagePosition` is deliberately omitted — it's only meaningful when -// `basics.image` is set, and baking it into every theme would either -// force image-only assumptions or no-op for image-less CVs. Users add -// `imagePosition` themselves via the spread-and-override pattern. -// -// `classic` re-declares the current defaults rather than being an alias. -// Themes are bundles by name, not pointers to "whatever the default is -// right now" — `classic` is allowed to drift from defaults so the named -// look stays stable. -// -// Fonts on `modern` (Inter) and `minimal` (Source Sans 3) are aspirational -// — neither is installed on the CI host. Typst falls back rather than -// panicking, so a user without the font still gets a working render; the -// README flags this so the user can install or override. +// `classic` re-declares the current defaults rather than aliasing them +// — themes are bundles by name, not pointers to "whatever the default +// is right now", so a future default shift won't silently move `classic`. +// `imagePosition` is omitted because it's only meaningful with a portrait. #let themes = ( classic: ( font: "Lato", accent: palettes.teal, - columnRatio: 0.64, + columnRatio: 0.65, headerTextAlign: "left", ), modern: ( diff --git a/tests/themes.typ b/tests/themes.typ index 9d14387..adf6746 100644 --- a/tests/themes.typ +++ b/tests/themes.typ @@ -1,19 +1,18 @@ -// `themes` exports vetted preset bundles of preferences that hang -// together visually — accent + font + columnRatio + headerTextAlign. -// Callers spread a theme over their own overrides: -// -// #alta(cv, preferences: themes.classic) -// #alta(cv, preferences: themes.modern + (imageSize: 7em)) -// -// One document per built-in theme, plus one demonstrating the -// spread-and-override pattern. The modern / minimal theme fonts -// (Inter, Source Sans 3) aren't installed on the CI host, so each -// non-classic theme is rendered with `font` overridden back to -// `Lato` — that keeps the fixture warning-free while still -// exercising the theme spread on every other knob. +// One page per built-in theme + a final page demonstrating +// spread-and-override. `font` is forced back to `Lato` for the +// non-classic themes because CI lacks `Inter` / `Source Sans 3`, +// so the snapshot stays warning-free while every other theme knob +// (accent, columnRatio, headerTextAlign) still gets exercised. #import "../lib.typ": alta, themes +// Pin the declared font on each theme so a typo in `internal/presets.typ` +// (e.g. `"Intr"`) is caught here rather than silently falling back to +// the system default and producing an identical-looking snapshot. +#assert.eq(themes.classic.font, "Lato") +#assert.eq(themes.modern.font, "Inter") +#assert.eq(themes.minimal.font, "Source Sans 3") + #let cv = ( basics: ( name: "Sample User", @@ -32,15 +31,14 @@ skills: ((name: "Languages", keywords: ("Scala", "Python")),), ) -#alta(cv, preferences: themes.classic) - -#pagebreak() - -#alta(cv, preferences: themes.modern + (font: "Lato")) - -#pagebreak() - -#alta(cv, preferences: themes.minimal + (font: "Lato")) +#for (i, key) in themes.keys().enumerate() { + if i > 0 { pagebreak() } + let theme = themes.at(key) + // `font: "Lato"` is a no-op for `classic` and the CI-host workaround + // for `modern` / `minimal`; merging unconditionally keeps the loop + // body uniform across themes. + alta(cv, preferences: theme + (font: "Lato")) +} #pagebreak()