Resume pdf - #45
Merged
Merged
Conversation
Authored by Zane; committing alongside the résumé PDF work. - Ministry Intern was listed as Manhattan, Kansas; it was Cebu City, Philippines. The old PDF had it right and the site data was wrong. - dad.jpg 1.26MB -> 484KB, which matters more now that the home page shows it in the latest-update teaser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extracts the résumé content out of Resume.tsx into resumeData.ts so the page and the PDF read from one source, and adds ResumePrint.tsx: a Letter-sized sheet with a dark sidebar for skills, volunteering and interests, and a main column for the role, contact row, summary, education and experience. Sized in inches and points rather than the site's rem scale, since the output is a physical page. Only current roles appear; the older experience the site keeps behind a disclosure is deliberately left off, and the main column is held under ~90% full so bullets can be added to the current role later. The route is registered only when `import.meta.env.DEV`, so the branch is dead code in a production build and the module tree-shakes out — verified absent from dist, which also dropped ~5.8KB. The sidebar is a flat fill: a decorative gradient there was verified pixel-identical with and without, because `aside` creates no stacking context and the negatively-stacked layers painted behind the sheet's own background. They were costing ~46KB of invisible rasterised gradient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bun run resume:pdf` renders the print sheet in headless Chromium and writes public/zm-resume.pdf. The download URL is unchanged — it is still a static file — but it now comes from the same data as the site. The file it replaces was a single full-page raster image with zero extractable text on a 19.9x25.7in page: unreadable to a screen reader, uncopyable, and effectively blank to the ATS software that parses résumés. The new one is one Letter page of real vector text. Guards, because the sheet clips overflow and a spill would silently lose content rather than paginate: - both axes are measured in print media and the run fails with the exact pixel overrun, reporting each column's fill so there is warning before it becomes a problem - webfonts are asserted before measuring. `document.fonts.check()` returns true when no matching face exists, so an empty face set is tested separately — otherwise an offline run would quietly emit a fallback-font PDF and stamp it as current - `resume:check` hashes the sources and fails CI when the PDF is stale. The hash covers styles.css and index.html as well as the data, since typography changes the render without touching any résumé text Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deliberately few, and none on component markup — the content is static and type-checked, so snapshots would churn on every design tweak while catching almost nothing. - tests/resume-pdf.test.ts: one Letter page, real font objects, and no regression to an image-only export. Verified against the PDF this branch replaces, which fails both: 0 font objects, 99.1% image bytes. Every image dict must be accounted for before the byte-share assertion runs, so an unparsed dict can't make it vacuously pass. - tests/contrast.test.ts: asserts the token pairings clear WCAG AA in both themes, reading the tokens out of styles.css so it can't drift. The light-mode accent and cyan are held deliberately dark and are easy to "fix" by lightening; checked that lightening --accent to 62% does fail this, at 3.70:1. - tests/browser/routes.test.ts: every route renders with no console errors and exactly one h1. Webfont requests are stubbed so a slow third party can't read as an app error. Browser-dependent tests live in tests/browser/ so `bun run test` — and therefore CI — needs no Chromium. `bun run test` globs tests/*.test.ts rather than naming files, so a new fast test isn't silently skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- package.json: resume:pdf, resume:check, test, test:browser, test:all; playwright, @types/node and @types/bun as devDependencies. Nothing in the repo used Node APIs before these scripts, so @types/node had never been installed — which is why scripts/ showed errors in the editor. - ci.yml: run resume:check and the browser-free tests. CI needs no Chromium; playwright ships no install script, so `bunx playwright install chromium` is a one-time local step, now documented. - tsconfig: scripts/ and tests/ belonged to no project, so they had no type checking at all and lost allowImportingTsExtensions. They are in tsconfig.node.json now, which is how `tsc -b` caught that the page.evaluate callbacks needed DOM types. - CLAUDE.md: document the PDF pipeline and the testing approach, and correct "No test runner is configured", which this branch made false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typo fixes authored by Zane. "Al tools" (capital A, lowercase L) would not have matched an ATS search for "AI", which is exactly the keyword the generated PDF exists to expose. resume:check caught the stale PDF on the next run, as intended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handler never ran, and would have been redundant if it had: - Vite 8 bundles with Rolldown. `onwarn` is still an accepted option, but a probe that logged on every invocation produced nothing — not for the module-directive warnings the handler suppresses, and not with a circular dependency deliberately introduced. - Both failure classes it tried to escalate are already fatal earlier: `bun run build` runs `tsc -b` first, which fails on an unresolved import (TS2882) and on a missing export, with or without the handler. Verified both ways. Config that implies protection it doesn't provide is worse than no config. If build-fails-on-warning is wanted under Rolldown, `onLog` is the live API and should be wired deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codemod authored by Zane: equivalent arbitrary values replaced with scale utilities (mb-[6pt] -> mb-2, w-[2.5in] -> w-60, gap-x-[12pt] -> gap-x-4). 1pt is 4/3px and Tailwind's step is 4px, so these are exact. Verified rather than assumed: regenerating produced identical column measurements (aside 910px, main 866px) and a PDF byte-identical apart from its timestamp, so no relayout. Sizes that carry no scale equivalent — text-[9pt], py-[0.38in], leading-[1.38] — stay explicit, so the file now mixes both notations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
biome.json only covered ts/tsx/js/jsx, so styles.css — 400 lines and the whole design system — was neither linted nor formatted. Biome 2.5.7 can do both, including Tailwind 4 directives via css.parser.tailwindDirectives, so this needs no new dependency and no stylelint. It found five `!important` declarations, all of them in the print and reduced-motion overrides, where !important is the point: those rules have to beat the animate-* utilities and the inline transition-delay that Reveal sets, or they do nothing. Suppressed as a range with the reasoning recorded, rather than weakening the rule everywhere. Verified the reformatting is cosmetic: the compiled CSS is byte-identical (md5 044f27eb…) before and after, and the résumé PDF re-renders identically — styles.css is in the PDF's source hash, so resume:check demanded a regeneration, which is the guard working as intended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- "Flate-compressed" had become "Flat-compressed". Flate is the name of the PDF filter (/FlateDecode), not a misspelling. - "sRGB" had been lowercased to "srgb" in two comments. It is the name of the colour space. The US-spelling normalisations in the same pass are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typo fix authored by Zane. Keeping the file: its targetNamespace matches public/sitemap.xml's xmlns, which is what lets the IDE resolve the schema offline and validate the sitemap. Nothing in the code references it, so it reads as dead — it isn't. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing checked public/sitemap.xml, and every loc in it was rewritten by hand at one point — trailing slashes stripped, all six lastmod dates changed — with no verification. Two layers, because they catch different things: - xmllint against sitemap.xsd, which catches malformed structure and illegal enum values. Skipped with a warning where xmllint is absent; it ships with libxml2-utils and is on GitHub's ubuntu runners. - Route parity, which no schema can know: the locs must match the paths App.tsx actually serves, excluding the catch-all and the dev-only print route. Plus no trailing slashes, one origin, parseable lastmod. Verified each assertion can fail: an illegal changefreq is rejected with the schema's own enumeration message, an unlisted route is named in the diff, and a trailing slash trips two assertions. No CI change needed — `bun run test` globs tests/*.test.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was the circular favicon mark on a transparent background, which is wrong for this slot: iOS applies its own squircle mask and composites transparency to black, so the home screen showed a circle floating in a black square. Now a full-bleed navy square with no alpha channel at all, the gradient ZM at ~62% of the tile for the padding Apple icons carry, and a subtle accent glow echoing the site's backdrop. No pre-rounded corners, since iOS supplies the mask. Checked against that mask on light and dark wallpapers. The circular treatment stays in favicon.svg and favicon.ico, where it sits in a tab strip rather than under a mask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rim follows the tile's own rounded-rect geometry rather than a circle, inset 8.5% so it survives iOS's squircle mask — its corner radius is tighter than the mask's 22.5% so the two read as concentric. Same ring-gradient as the favicon: accent at 0.9, cyan at 0.5 by 45%, transparent from 75%, on the 140deg axis. The circle-on-a-square alternative was rendered and rejected: with no fill separating circle from tile, the ring reads as a leftover from the favicon rather than a designed icon, and it costs letter size. Still full-bleed and alpha-free, checked under a simulated mask on light and dark wallpapers at both 140px and 60px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`imageDark` existed on the Project type but only the featured hero honoured it — a grid card silently ignored the prop, so setting it did nothing. Same CSS swap as the featured panel: dark:hidden on the light image, not-dark:hidden on the dark one. Both preload, so the theme toggle is instant, and there is no base/variant pair on one property to depend on source order. Verified in a browser that exactly one is displayed per theme. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bun run screenshots` captures the home hero in light and dark and writes both into src/pages/Projects/imgs/. The pair it replaces was from April, predating the entire redesign, so the Projects page was advertising a site that no longer existed. That will happen again after the next design change, hence a script rather than a manual crop. Details that make the capture repeatable: reduced-motion is emulated, which makes styles.css force every [data-reveal] visible rather than racing the IntersectionObserver and freezes the aurora drift; the theme is set in localStorage before first paint and then asserted from the html class; and fonts are awaited. 1600x900 at scale 1 — the card renders about 370px wide and the featured hero about 620px, so capturing at 2x tripled the bytes for no visible gain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the April screenshot of the old design. 90KB and 88KB at 1600x900, in line with the Flybox 2.0 pair, down from 247KB each when captured at 2x. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…riant The featured hero applied `dark:hidden` to the light screenshot unconditionally, so a featured project without an `imageDark` would render no image at all in dark mode. Flybox 2.0 happens to have one, which is why nothing looked broken — reordering the array would have exposed it. Now conditional, matching the guard ProjectCard already had. Verified in a browser that exactly one image is visible in dark mode. Also consolidates the duplicated type: Projects.tsx declared a `Project` interface with the same eight fields and optionality as ProjectCard's `Props`, kept in sync only by the spread at the call site. This branch already paid that cost twice, adding `imageDark` to each in separate commits. ProjectCard now exports `Props` and Projects.tsx imports it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audited the branch for subtraction. Removed, with the reasoning: - scripts/check-resume-pdf.ts (30 lines), its npm script and its CI step. All it did was string-compare sourceHash() against the recorded hash — an assertion, now living in tests/resume-pdf.test.ts, which already imported that module. CI still catches a stale PDF, via the test suite. Verified the absorbed assertion fails on a stale PDF and names the fix. - The PDF byte-share machinery. It was calibrated against a rasterised sidebar gradient that no longer exists, so it measured 0 of 0 — an assertion that could not fail, with a doc comment claiming ~15%. The regression it guarded (a raster with no text) is caught by the font-object check. Recorded the residual gap: an OCR'd scan would pass. - sitemap: "no trailing slashes" and "one origin" are both subsumed by the exact-array-equality parity test; the lastmod value checks are subsumed by the schema. Kept the count. - routes: a response-status assertion that cannot fail, and a /resume/print block in a suite CI never runs. - contrast: two pairings strictly dominated by the assertion beside them. - test:all, an alias for the runner's default. Left alone what the audit proposed and verification rejected: a shared dev-server helper (a lateral move, three obvious copies beat one indirection here), inlining fromRoot (net +1 line), and narrowing the PDF source hash (styles.css and index.html are live inputs to the render). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- robots.txt: 21 lines to 5. The four named user-agent groups had bodies byte-identical to the wildcard group, so they were inert. - vite.config.ts: logLevel restated Vite's own default. - ResumePrint: an inline colour on MainHeading's h2 that it already inherited from the sheet root. - CLAUDE.md: the ProjectCard paragraph was wrong on both the CSS mechanism and the scope of the imageDark swap, and the commands list still named resume:check and test:all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Authored by Zane: 597KB -> 237KB, 1536x1544. It was the largest image left on the site after dad.jpg. Verified it still decodes and renders on the Updates feed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closed
`.dark` and `[data-reveal].is-visible` are applied at runtime — the first by index.html's bootstrap script and ThemeToggle, the second by Reveal.tsx via classList — so the IDE's "selector is never used" inspection can never be right about either. `noinspection CssUnusedSymbol` with the reason recorded, since the alternative is explaining it every time it resurfaces. Both are load-bearing: without `.dark` there is no dark theme, and without `.is-visible` every scroll-reveal stays invisible. styles.css is in the PDF's source hash, so the recorded hash is updated too. The PDF itself is unchanged — verified byte-identical apart from its timestamp — so it isn't recommitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
US spelling, matching the rest of the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passed over every comment on the branch for concision — 87 lines in, 117 out.
Cuts, with the reasoning:
- resume-pdf.test.ts: the two-bullet "limits of reading raw bytes" block was
half-duplicated by an inline comment 20 lines below it. The /Font half
merged into that comment; the pixels-vs-points half moved down to the size
assertion it actually explains.
- styles.css: "!important is deliberate here too" restated the
biome-ignore-start reason 14 lines above it, so that reason now covers both
blocks.
- ResumePrint: {/* Sidebar */} and {/* Main column */} labelled an <aside> and
a <main>, and the header enumerated what each column holds, which is what
the JSX below it says.
- generate-resume-pdf.ts: dropped the note on why the /// <reference> lines
sit in the file rather than in tsconfig.node.json.
Three inaccuracies fixed while passing through:
- generate-resume-pdf.ts pointed at `bun run resume:check`, removed in
dcc9f69. Now `bun run test`, which is what carries that assertion.
- resume-sources.ts had "deliberately outside public/" above both HASH_FILE
and PDF_FILE, and the PDF is in public/. Now scoped to the hash.
- styles.css read "re-rasterize it every frame"; the honour -> honor pass had
eaten the s.
The rest is reflow — same facts, fewer lines.
ResumePrint.tsx and styles.css are in the PDF's source hash, so the recorded
hash is updated. The PDF itself is unchanged: regenerating it differed in 10
bytes, the /CreationDate and /ModDate fields, so it isn't recommitted.
Verified with biome ci, typecheck, bun run test, and bun run test:browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.