fontkit caches glyphs per font for the whole process and pdfkit embeds composite components with no code points, so setting ':' before '.' in Poppins corrupted every later render in the same process (text layer showed '1,200;00', line breaks shifted). primeFontGlyphs() now creates the component glyphs with their cmap code point at font registration, awaited by renderCore and browser init. Regression test and Quirk 13 added; the warm-up workarounds in the template tests are removed.
7.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Voiced is an offline-first GST invoicing desktop app (Tauri v2, React 19, IBM Carbon, @react-pdf/renderer 4.9.0, SQLite via rusqlite) built by Bongbetic for vendor Arun P. Targets: Void Linux (xbps, WebKitGTK 2.50) and Windows (NSIS, WebView2). handoff.md holds the current project state and next steps; the approved plan is /home/soubarna/.claude/plans/take-a-look-at-logical-sunbeam.md. The README's "Project layout" section is out of date; trust this file and the code.
Commands
npm run build # tsc --noEmit && vite build (the type-check gate)
npm test # vitest run, ~7 min; run on a quiet machine (load starves the sweep tests)
npx vitest run src/pdf/templates/monolith # one folder
npx vitest run path/to/file.test.tsx -t "name" # one test
npm run fonts:verify # bundled fonts match public/fonts/manifest.json (regenerate with fonts:manifest)
npm run logo:measure # regenerate src/pdf/templates/slots.generated.ts from the Canva PDFs; must be idempotent
npm run thumbnails # regenerate public/templates/*.png (unchanged templates must stay byte-identical)
npm run templates:compare # Canva vs Voiced side-by-side PNGs (scripts/templates/compare.mjs, output under /tmp)
cargo test --manifest-path src-tauri/Cargo.toml # Rust unit tests (gst, db migrations, logo, commands)
npm run app:dev # tauri dev (served from localhost:1420; does NOT exercise the tauri:// protocol)
npm run app:build # production build, custom-protocol
npm run app:build:windows # NSIS via cargo-xwin cross-compile (see README)
./packaging/void/build-xbps.sh # Void .xbps into build/void/repo
Real-webview self-test (the only check that runs the production build): build with npm run tauri -- build --debug --no-bundle --features custom-protocol, then bash /tmp/run-selftest.sh. The app is inert unless VOICED_SELFTEST_OUT is set; the script runs the debug binary with temp XDG dirs and the report lands in /tmp/voiced-e2e/report.json (ok: true expected). /tmp is not durable: if the script is gone, recreate it with that env var, XDG_DATA_HOME/XDG_CONFIG_HOME temp dirs and a timeout 180.
Architecture
Rust backend (src-tauri/src)
db.rsowns the schema as numbered migrations (rusqlite_migration, M1–M5); it copies the DB aside before migrating. Add schema changes as new migrations, never edit old ones. Issued invoices carry snapshots (vendor, client, GST, layout fingerprint) and are immutable.gst.rsdoes all money and tax maths in integer paise. Tax heads (CGST+SGST vs IGST vs UTGST) derive from (supplier state, place of supply); a mismatch with the chosen head is rejected. An unregistered vendor gets a plain "Invoice" with no tax. GSTIN is checked with the mod-36 checksum.logo.rstrims, quantises and measures logos (aspect, density, wordmark/mark/tall, knockout variant for dark surfaces).commands/*.rsare the Tauri commands;raw.rscarries binary payloads over raw-body IPC (ASCII headers, ArrayBuffer response).archive.rsstores each issued PDF content-addressed underapp_local_data_dir/archive/<sha256>.pdf.selftest.rsis the unattended end-to-end hook described above.
Frontend (src)
views/ and components/ are the Carbon UI; lib/api.ts is the typed wrapper over the Tauri commands; lib/exportFlow.ts and hooks/useInvoiceExport drive export (searchable or flattened) and archiving.
PDF engine (src/pdf) — the part that needs several files to understand
Pipeline: model/build (buildRenderModel) → engine/geometry (computeFrame: A4/Letter plus margin presets) → a template Layout → InvoicePdf/InvoicePage → render/core (renderCore: bytes, normalized layout tree, auditLayout issues, layout fingerprint). Rendering runs in a module Web Worker through a latest-wins render/client (FIFO assemble lane, watchdog, main-thread fallback). Preview uses pdf.js; flatten rasterises pages at 300 DPI and re-assembles them as an image-only PDF.
- Templates live in
templates/<family>/and are registered intemplates/registry.ts(picker order) andcatalog.ts(labels, thumbnails). Each hastokens.ts,plan.ts,Layout.tsxand a test. They implementtemplates/contract.ts. 14 exist: Classic plus the 13 Canva templates, specified indocs/templates/*.md(measured from the Canva PDFs intemplates/, which is untracked on purpose: never stagetemplates/or.commandcode/). - Logo placement is slot-based (
engine/logo.ts,templates/slots.*): the user's logo fills the Canva placeholder box, contained, with a shrink-only weight factor. Template placeholder logos are never reproduced. Some templates use opt-in slots (slots.optin.ts), default off. - Tests that guard layout: template harness and sweep tests (
harness*.ts,harness.sweep*.test.tsx: 1–45 rows, long text, A4/Letter, no overlap), layout lint, fingerprint goldens (goldens.test.ts) andquirks.test.tsx. The Node goldens must equal the fingerprints the real webview reports. - Glyph priming: fontkit caches glyphs process-wide and pdfkit embeds composite components with no code points, which corrupted later renders (":" before "." gave "₹1,200;00").
primeFontGlyphs()infonts/register.tsfixes it and runs inrenderCoreand browser init. Do not widen it to every cmap glyph (breaks Inter's contextual hyphen); it uses fontkit private APIs, so re-check on fontkit upgrades. - Fonts: TTF only, bundled in
public/fonts/<family>/with a generated manifest; every family needs normal and italic sources. WOFF2 embeds blank glyphs. Roles/type tokens are infonts/roles.ts.
react-pdf 4.9.0 rules (all pinned by quirks.test.tsx; break them and layouts hang or overlap)
- Text lines are cached at first measure, so give flex children pinned widths (
width=minWidth=maxWidth) with a{flexGrow:1, flexBasis:0}fill child. - No
lineHeighton Page/View/render-prop ancestors; set an absolute'Npt'lineHeight on each staticText. Hyphenation is off. fixed,wrap,minPresenceAheadare key-presence checks: spread them only when meaningful (blocks/pdfProps).- A non-fixed page-level absolute
Svg/Imagehangs pagination. Render props must returnText/Viewonly. maxLinesdoes not clip a single over-long word (useFitText). TrailingletterSpacingneedsTrackedcompensation.- A fixed table header as the first child of the table
Viewrepeats on each page; useTableGuard/BottomSpacerwithminPresenceAhead.
Conventions
- Commit only after the gates pass (
npm run build, fullnpm test,fonts:verify, and the self-test for engine/template changes). Work is on stacked phase branches (phase-0-hotfix→phase-a-foundation→phase-b-engine→phase-c-templates); create the next phase branch from the previous one. - Windows/WebView2 has never been run on a real machine; the app window UI has only been exercised headless.