Skip to content

feat: add preferences.qrCode for a header QR matrix linking to basics.url#88

Closed
smur89 wants to merge 4 commits into
mainfrom
feat/issue-59-header-qr-code
Closed

feat: add preferences.qrCode for a header QR matrix linking to basics.url#88
smur89 wants to merge 4 commits into
mainfrom
feat/issue-59-header-qr-code

Conversation

@smur89

@smur89 smur89 commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Adds preferences.qrCode — opt-in, default none — which renders a small accent-coloured QR matrix in the header on the side opposite the portrait. qrCode: "url" encodes basics.url; any other string is encoded verbatim, useful when the printed CV should point at a tracked landing page distinct from the canonical homepage. Closes #59.

Rendered output

QR + portrait (default imagePosition: "right") — QR lands on the left, photo on the right, header text in the middle. With no portrait the QR still lands on the side opposite imagePosition. With imagePosition: "center", the photo stays stacked above/below the text block and the QR sits beside the text row. The matrix is wrapped in link() so a digital PDF reader can click through to the same destination the QR encodes — the QR is for print, the click is for screen. CI uploads the example-pdf artifact as usual.

Test plan

  • typst compile --root . examples/example.typ examples/example.pdf succeeds
  • Each tests/*.typ fixture compiles
  • Added a fixture (tests/header_qr_code.typ) covering "url" form, explicit URL, QR + portrait on each side, and accent-themed QR
  • Updated the README — new "Header QR code" section, preferences table row, and a cross-reference from the basics.url line

Notes for the reviewer

New third-party dependency. QR generation is delegated to @preview/zebra:0.1.0 — a small, MIT-licensed generator that emits native Typst vector paths. It's the first third-party Typst package altacv depends on, so callers will see a one-time download on first compile after upgrading. I picked zebra over @preview/cades (which pulls in @preview/jogs, a JS interpreter) and @preview/tiaoma (Zint-via-WASM, larger) — zebra is ~170 lines + one WASM blob, no transitive Typst deps, and the only one that emits vector paths rather than rasterised images. Worth a vet on your side.

Default size is hardcoded at 3.5em. The issue spec calls for ~3em. I didn't add a qrSize preference to keep scope tight — happy to follow up if you'd like one.

basics.url is also rendered in the contact bar (separate feature already on main). The QR path reuses the same field via qrCode: "url", so a single basics.url value drives both the contact-bar link and the QR — no duplication.

QR colour defaults to preferences.accent. Built-in accent (#00796B, dark teal) gives plenty of contrast on white. Very pale custom accents could in principle scan worse; deferred until anyone complains.

JSON Resume alignment

QR rendering is a template-side presentation preference, not part of the JSON Resume schema. When qrCode: "url", the matrix encodes the schema's canonical basics.url; otherwise it encodes any caller-supplied string. No schema keys are introduced or repurposed.

Summary by CodeRabbit

  • New Features

    • Added optional header QR-code support to the CV template.
    • Configure preferences.qrCode with none, "url" (uses basics.url), or any non-empty URL string.
    • QR placement now adapts to imagePosition, rendering alongside the portrait/image as appropriate.
    • QR styling can inherit the document accent color.
  • Documentation

    • Documented the new qrCode preference, accepted values, QR placement behavior, and render defaults.
  • Tests

    • Added coverage for QR generation, placement variants, and accent theming.

@smur89 smur89 force-pushed the feat/issue-59-header-qr-code branch 6 times, most recently from d5b3f8b to 612cdf7 Compare June 14, 2026 09:13
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an optional preferences.qrCode key to the altacv Typst template. A new _resolve_qr_url helper maps the value to a URL, and _qr_code renders an accent-colored, clickable matrix via @preview/zebra. The _header layout is reworked to position the QR opposite the portrait or beside the text. alta() gains a default and validation. Tests and README are updated.

Changes

Header QR Code Feature

Layer / File(s) Summary
zebra import and QR URL/render helpers
lib.typ
Imports qrcode from @preview/zebra:0.1.0, adds _resolve_qr_url to resolve preferences.qrCode to a destination URL (panicking on invalid "url" when basics.url is absent), and adds _qr_code to produce an accent-filled, link()-wrapped QR element.
Header layout integration for QR placement
lib.typ
Adds qr-code parameter to _header, computes qr-url via _resolve_qr_url, and reworks the photo/text grid so QR renders beside header-text for image-position: "center" or in the opposing grid cell for left/right portrait positions, with dynamic column computation to drop empty cells.
alta() defaults, validation, and call wiring
lib.typ
Adds qrCode: none to _default_preferences, adds a validation panic for non-none non-string or empty-string preferences.qrCode, and passes the validated value to _header as qr-code.
Test scenarios and README documentation
tests/header_qr_code.typ, README.md
Adds five #alta() test cases covering "url" mode, explicit URL, QR+portrait default and flipped imagePosition, and accent theming. README gains an expanded basics.url note, a new "Header QR code" section, and a qrCode row in the preferences table.

Sequence Diagram(s)

sequenceDiagram
    participant User as Document Author
    participant alta as alta()
    participant _header as _header()
    participant _resolve_qr_url as _resolve_qr_url()
    participant _qr_code as _qr_code()
    participant zebra as `@preview/zebra` qrcode

    User->>alta: preferences.qrCode = "url" | string | none
    alta->>alta: validate qrCode (panic if invalid)
    alta->>_header: qr-code = preferences.qrCode
    _header->>_resolve_qr_url: qr-code, basics
    _resolve_qr_url-->>_header: resolved URL or none
    _header->>_qr_code: url, accent, size
    _qr_code->>zebra: qrcode(url, fill: accent)
    zebra-->>_qr_code: QR matrix content
    _qr_code-->>_header: link(url, QR element)
    _header-->>alta: header with QR in grid layout
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A tiny square of dots appears,
Where once the header only steered —
Scan it quick with phone in hand,
My CV travels cross the land!
"url" or string, your choice is clear,
The zebra weaves your QR here. 🦓

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature addition: introducing preferences.qrCode for header QR code functionality linking to basics.url.
Description check ✅ Passed The description comprehensively covers the feature with summary, rendered output details, completed test plan checkboxes, and thorough reviewer notes including dependency justification and design decisions.
Linked Issues check ✅ Passed All objectives from #59 are met: preferences.qrCode implemented with 'url'/'string'/none modes, positioned opposite portrait, uses basics.url when 'url' is set, and leverages @preview/zebra for lightweight QR generation.
Out of Scope Changes check ✅ Passed All changes align with the PR objectives: README documentation updates, preferences.qrCode implementation in lib.typ, and new test fixture for QR rendering—no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-59-header-qr-code

Comment @coderabbitai help to get the list of available commands and usage tips.

@smur89 smur89 force-pushed the feat/issue-59-header-qr-code branch from 5ed3c04 to ce56256 Compare June 14, 2026 10:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/header_qr_code.typ (1)

1-76: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding test coverage for imagePosition: "center" with QR code.

The current test suite covers QR with imagePosition: "left" and "right", but not "center". According to the implementation in lib.typ (lines 698-728), when imagePosition is "center", the QR sits beside the header text row while the photo stacks above/below. This layout mode is not explicitly tested.

📋 Suggested test case
`#pagebreak`()

`#alta`(
  (basics: (
    name: "QR + Centred Portrait",
    label: "imagePosition: \"center\", QR beside text",
    email: "qr@example.com",
    url: "https://example.com/cv",
    image: read("../examples/avatar-placeholder.svg", encoding: none),
  )),
  preferences: (qrCode: "url", imagePosition: "center"),
)
🤖 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/header_qr_code.typ` around lines 1 - 76, Add a new test case to the
header_qr_code.typ file that exercises the imagePosition "center" layout with QR
code enabled. The existing tests cover QR with default/right positioning and
with left positioning, but do not cover the center positioning mode. Insert a
new `#pagebreak`() followed by an `#alta`() call that sets both preferences.qrCode
to "url" and preferences.imagePosition to "center" while including a portrait
image in the basics, mirroring the structure of the other test cases in the file
to ensure this layout mode is properly tested.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@tests/header_qr_code.typ`:
- Around line 1-76: Add a new test case to the header_qr_code.typ file that
exercises the imagePosition "center" layout with QR code enabled. The existing
tests cover QR with default/right positioning and with left positioning, but do
not cover the center positioning mode. Insert a new `#pagebreak`() followed by an
`#alta`() call that sets both preferences.qrCode to "url" and
preferences.imagePosition to "center" while including a portrait image in the
basics, mirroring the structure of the other test cases in the file to ensure
this layout mode is properly tested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a5e75577-8adf-42c0-958f-ef40e5ef8c18

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed3c04 and ce56256.

⛔ Files ignored due to path filters (1)
  • examples/tests/header_qr_code.pdf is excluded by !**/*.pdf
📒 Files selected for processing (3)
  • README.md
  • lib.typ
  • tests/header_qr_code.typ

@smur89 smur89 force-pushed the feat/issue-59-header-qr-code branch from ce56256 to f33912b Compare June 14, 2026 11:11
Shane Murphy added 4 commits June 14, 2026 15:14
….url

A printed CV loses the clickability of digital PDFs. The new
`preferences.qrCode` preference renders a small accent-coloured QR
matrix in the header on the side opposite the portrait, so a phone
camera can take a reader straight to the candidate's homepage.

`qrCode: "url"` encodes `basics.url` (now consumed for the first time
by the template); any other string is encoded verbatim, useful when
the printed CV should point at a tracked landing page distinct from
the canonical homepage. Default `none` keeps the existing layout
untouched.

QR generation is delegated to `@preview/zebra` — a small, MIT-licensed
generator that emits native Typst vector paths, so the matrix renders
crisply at any size and inherits the document's accent colour via
`fill`. This is the first third-party Typst package altacv depends
on; documented in the README.

Closes #59
@smur89 smur89 force-pushed the feat/issue-59-header-qr-code branch from 2e49bef to 5b2c4ac Compare June 14, 2026 13:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/header_qr_code.typ (1)

17-76: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a centered-portrait QR scenario to cover the dedicated center-layout branch.

Current fixture misses the imagePosition: "center" path (with QR present), which has separate composition logic in _header.

Suggested test addition
+#pagebreak()
+
+#alta(
+  (basics: (
+    name: "QR + Portrait (Centered)",
+    label: "Centered photo, QR beside text row",
+    email: "qr@example.com",
+    url: "https://example.com/cv",
+    image: read("../examples/avatar-placeholder.svg", encoding: none),
+  )),
+  preferences: (
+    qrCode: "url",
+    imagePosition: "center",
+    imageStackOrder: "above",
+  ),
+)
🤖 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/header_qr_code.typ` around lines 17 - 76, Add a new test case to the
header_qr_code.typ file that covers the `imagePosition: "center"` scenario when
QR code is enabled. Create a new alta() call with a basics section containing
name, label, email, url, and an image field (using the same
avatar-placeholder.svg as the other portrait examples), and a preferences
section that includes both qrCode: "url" and imagePosition: "center" to exercise
the dedicated center-layout composition logic in _header.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@lib.typ`:
- Around line 665-668: The _qr_code function sets quiet-zone to 0, which
violates the ISO/IEC 18004 QR code standard and causes scanning failures in
print environments. Change the quiet-zone parameter in the qrcode call from 0 to
4 to comply with the standard and ensure proper scanning reliability in printed
outputs.

In `@README.md`:
- Line 306: The documentation contains contradictory statements about how
imagePosition behaves when qrCode is enabled but no portrait is present. The
qrCode table entry and the "Header QR code" section have conflicting
descriptions. Review both the imagePosition documentation and the "Header QR
code" section above line 306 in README.md, then update the qrCode description to
clarify the exact behavior of imagePosition when no portrait exists but qrCode
is enabled. Ensure the statements are consistent and accurately reflect whether
imagePosition is used, ignored, or conditionally applied in this scenario.

---

Outside diff comments:
In `@tests/header_qr_code.typ`:
- Around line 17-76: Add a new test case to the header_qr_code.typ file that
covers the `imagePosition: "center"` scenario when QR code is enabled. Create a
new alta() call with a basics section containing name, label, email, url, and an
image field (using the same avatar-placeholder.svg as the other portrait
examples), and a preferences section that includes both qrCode: "url" and
imagePosition: "center" to exercise the dedicated center-layout composition
logic in _header.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 861fe52f-24f0-41d7-8888-589191ebeb02

📥 Commits

Reviewing files that changed from the base of the PR and between ce56256 and 5b2c4ac.

⛔ Files ignored due to path filters (2)
  • examples/preview.png is excluded by !**/*.png
  • examples/tests/header_qr_code.pdf is excluded by !**/*.pdf
📒 Files selected for processing (3)
  • README.md
  • lib.typ
  • tests/header_qr_code.typ

Comment thread lib.typ
Comment on lines +665 to +668
#let _qr_code(url, size, accent) = link(
url,
qrcode(url, width: size, quiet-zone: 0, fill: accent),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For @preview/zebra:0.1.0 qrcode(), what is the recommended quiet-zone for reliable scanning, and does QR guidance still require a 4-module quiet zone?

💡 Result:

For the @preview/zebra:0.1.0 package in Typst, the recommended quiet zone for reliable scanning is 4 modules wide on all sides [1][2]. Regarding the QR code standard, yes, the requirement for a 4-module quiet zone remains active and essential [3][4][5]. The ISO/IEC 18004 specification (including the most recent 2024 version) explicitly defines the quiet zone as a region that must be free of all other markings, surrounding the symbol on all four sides with a width of 4 modules (often denoted as 4X) [3][4][6]. In the context of the zebra package: - The quiet-zone argument accepts an integer, a boolean, or none (defaulting to 0) [1][2]. - When set to true, the package automatically applies the recommended quiet zone of 4 modules for QR codes (and 1 module for Data Matrix) [1][2]. Ignoring this margin is a frequent cause of scanning failures, as the quiet zone is critical for the scanner to detect the boundaries of the code against its background [7][8]. Ensure this area remains clear of text, logos, or other design elements [7][9].

Citations:


Restore a non-zero QR quiet zone for print reliability.

Setting quiet-zone: 0 violates the ISO/IEC 18004 QR code standard, which requires a 4-module quiet zone on all sides. This is a frequent cause of scanning failures, especially in print/PDF viewers. The @preview/zebra package recommends a 4-module quiet zone for QR codes. Adjust as shown below.

Suggested change
 `#let` _qr_code(url, size, accent) = link(
   url,
-  qrcode(url, width: size, quiet-zone: 0, fill: accent),
+  qrcode(url, width: size, quiet-zone: 4, fill: accent),
 )
📝 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 _qr_code(url, size, accent) = link(
url,
qrcode(url, width: size, quiet-zone: 0, fill: accent),
)
`#let` _qr_code(url, size, accent) = link(
url,
qrcode(url, width: size, quiet-zone: 4, fill: accent),
)
🤖 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 `@lib.typ` around lines 665 - 668, The _qr_code function sets quiet-zone to 0,
which violates the ISO/IEC 18004 QR code standard and causes scanning failures
in print environments. Change the quiet-zone parameter in the qrcode call from 0
to 4 to comply with the standard and ensure proper scanning reliability in
printed outputs.

Comment thread README.md
| `imagePosition` | `"right"` | Where the portrait sits in the header — `"left"` or `"right"` (two-column header) or `"center"` (portrait on its own centred row, stacked with the text block). Ignored when no image is supplied. |
| `imageStackOrder` | `"above"` | Stack order when `imagePosition` is `"center"` — `"above"` puts the portrait above the name/label/contact block; `"below"` puts it underneath (the "photo as sign-off" look). Ignored for `"left"` / `"right"` positions. |
| `headerTextAlign` | `"left"` | Horizontal alignment of the header text (name, label, contact bar). Applies whether or not `basics.image` is set, so it also centres the header on image-less CVs. One of `"left"`, `"right"`, `"center"`. The default keeps every line starting at the same edge regardless of which side the photo is on; flip to `"right"` for the mirrored "text hugs the opposite edge" look. |
| `qrCode` | `none` | Renders a small accent-coloured QR matrix in the header opposite the portrait. `none` (default) skips it. `"url"` encodes `basics.url` (panicking if it's missing or empty). Any other string is encoded verbatim, useful when the printed CV should point at a tracked landing page distinct from `basics.url`. See "Header QR code" above for layout details. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify imagePosition interaction when qrCode is enabled and no portrait is present.

The table text says imagePosition is ignored without basics.image, but QR docs now describe no-portrait QR placement as depending on imagePosition. Please align those statements to avoid contradictory behavior docs.

🤖 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 `@README.md` at line 306, The documentation contains contradictory statements
about how imagePosition behaves when qrCode is enabled but no portrait is
present. The qrCode table entry and the "Header QR code" section have
conflicting descriptions. Review both the imagePosition documentation and the
"Header QR code" section above line 306 in README.md, then update the qrCode
description to clarify the exact behavior of imagePosition when no portrait
exists but qrCode is enabled. Ensure the statements are consistent and
accurately reflect whether imagePosition is used, ignored, or conditionally
applied in this scenario.

@smur89

smur89 commented Jun 14, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #126 — rebuilt against the post-refactor module layout (internal//sections/ split, internal/labels-en.toml extraction, example_full.typ demo). Leaving open for your review/close.

@smur89 smur89 closed this Jun 14, 2026
smur89 added a commit that referenced this pull request Jun 14, 2026
….url (#126)

## Summary

Adds `preferences.qrCode` — opt-in, default `none` — which renders a
small accent-coloured QR matrix in the header on the side opposite the
portrait. `qrCode: auto` encodes `basics.url`; any non-empty string is
encoded verbatim, useful when the printed CV should point at a tracked
landing page distinct from the canonical homepage.

Closes #59. Supersedes #88 (PR predates the `lib.typ` →
`internal/`/`sections/` split + the `internal/labels-en.toml`
extraction).

## Notes for the reviewer

**New third-party dependency.** QR generation is delegated to
[`@preview/zebra:0.1.0`](https://typst.app/universe/package/zebra) —
MIT, ~170 LOC + one WASM blob, no transitive Typst deps, emits native
vector paths rather than rasterised images. This is the first
third-party Typst package altacv depends on, so callers will see a
one-time download on first compile after upgrading. Picked zebra over
`@preview/cades` (pulls in `@preview/jogs`, a JS interpreter) and
`@preview/tiaoma` (Zint-via-WASM, larger). Worth a vet on your side.

**Implementation lives in `internal/qr.typ`** so the zebra import stays
a one-file change if a different backend becomes preferable.
`internal/header.typ` consumes the resolved data via a small module API.

**The QR matrix is wrapped in `link()`** so a digital PDF reader can
click through to the same destination the QR encodes — print users scan,
screen users click.

**`auto` sentinel, not `"url"`.** The "encode `basics.url`" path uses
Typst's `auto` keyword — the language's idiomatic "derive from context"
value — so the API splits cleanly into `none` / `auto` / `"<verbatim
URL>"` and the literal three-letter string `"url"` stays available as a
valid verbatim payload.

**Centred-photo + QR keeps text page-centred.** Under `imagePosition:
"center"` + `headerTextAlign: "center"` + a QR, the text-row grid uses
`(1fr, auto, 1fr)` with an empty spacer mirroring the QR column, so the
header text sits on the page midline instead of drifting left of it.

## Test plan

- [x] `make test` — `tests/header_qr_code.typ` plus all pre-existing
fixtures green.
- [x] Nine-page fixture covers:
  1. `auto` form, no portrait
  2. Explicit URL string, no portrait
  3. QR + portrait (default — photo right, QR left)
  4. QR + portrait (`imagePosition: "left"` — flipped)
  5. QR themed by a custom `accent`
  6. Centred portrait + QR + `headerTextAlign: "center"`
  7. QR with no portrait + `imagePosition: "center"`
  8. Centred portrait + QR + `imageStackOrder: "below"`
9. Centred portrait + QR + default `headerTextAlign: "left"` (orthogonal
axes)
- [x] Tracked `examples/tests/header_qr_code.pdf` regenerated to match.
- [x] README updated — preference table row, dedicated "Header QR code"
subsection, cross-reference from `basics.url`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added an optional header QR code, controlled by `preferences.qrCode`
(`none`, `auto`, or any non-empty string).
* With `auto`, the QR encodes `basics.url`; otherwise the provided value
is used verbatim.
* The QR renders as a clickable link and automatically positions
alongside portrait and `imagePosition`, including centred header layouts
and accent styling.

* **Documentation**
* Updated the README with QR configuration, placement behaviour, and
preview text.

* **Tests**
* Added additional scenarios covering more portrait/QR alignment
combinations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Shane Murphy <shane@swissborg.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: QR code in header linking to basics.url

1 participant