Quantities can now be time, count, distance, area, weight or volume, or a custom unit, instead of only
second/minute/hour/session/unit.
- Registry in src/lib/units.ts (mirrored in src-tauri/src/units.rs): grouped built-in units plus a custom unit of 1-12
characters. A rate line's unit is validated when an invoice or a preset is saved; migration M12 drops the old unit
allow-list from item_presets and keeps every row.
- Time is typed as h:mm: "4:30" hours counts as 4.5, "4:20" as 4.333... (kept exact, so rate x quantity rounds once, the
same in TypeScript and Rust). Other units take plain decimals up to 3 places. A bad quantity blocks issuing.
- The PDF quantity column shows the unit ("4.5 hr", "12.75 km"); the rate keeps "per hour" / "/hr". RenderItem carries
priceText, perText and rateShort, so no template parses the rate text with a regex over five hard-coded units.
- Quantity columns are sized from the widest word once a quantity with its unit is wider than 72 pt, so one long
custom unit cannot squeeze the description (the serenity and citrus-split tables overflowed in the new fixture).
Classic's quantity column is now content-sized.
- ERPNext: the default UOM map comes from the registry, with fractional-capable UOMs for time, distance, area, weight
and volume. A custom unit is sent as Nos. A quantity that is not exact to 3 decimals (4:20 hours) is still refused for
push, as before.
- Goldens and template thumbnails regenerated; the template harness has a "units" fixture with the longest texts.
Claude-Session: https://claude.ai/code/session_01PZypiWDfMkDTeEPeXjRhW5
The template harness, tests and thumbnails now use a neutral vendor, bank account and
a generated sample wordmark (scripts/logo/make-sample-logo.mjs). Layout goldens and the
14 picker thumbnails are regenerated. Fixture strings keep their original lengths where
layout tests depend on them.
Logos (PNG, JPEG, WebP) are decoded, trimmed to their ink box, downscaled
to 1200 px and re-encoded as a content-addressed print PNG plus a white
knockout variant when the logo has alpha; aspect, ink density, mean colour
and kind (wordmark, mark, tall) are stored with the settings and frozen in
the issue snapshot. Legacy logos are derived at startup. The Test Vendor logo
PDF shrinks from 334 KB to 92 KB. Settings gains a Branding section with
white and dark previews and a business-name toggle (migration M5).
issueAndArchive keeps an issued invoice even when rendering or archiving
fails (Issued - not archived, with a Render & archive action). The export
menu offers a searchable PDF or a 300 DPI image-only PDF whose metadata is
the invoice number alone; flatten failures offer Retry or searchable export
and never silently lower the resolution. History re-exports the original
as issued by default and warns when a re-render's layout fingerprint
differs. The render client gains a FIFO assemble lane that previews cannot
supersede. A Diagnostics panel runs fixtures in the real worker and
compares against goldens generated from Node.
Classic now renders through buildRenderModel, computeFrame, the template
and renderCore, with S.No and HSN/SAC columns, GST slots only through
checked Slot components, taxable value, reverse-charge line, cancelled tag,
continuation band and Page n of N. Letter absorbs its height deficit in
elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item
sweep and asserts zero audit errors, repeated headers, no split blocks,
rupee extraction and deterministic output. The legacy InvoiceDocument is
removed.
TableGuard, BottomSpacer, FirstPageHeader and PageChrome implement the
verified pagination rules; renderCore returns bytes, a normalised layout
tree, audit issues and a layout fingerprint. auditLayout detects overlaps,
ink overflow, truncation, header-only pages and split blocks. Quirk tests
pin the react-pdf behaviours the design depends on, and a child-process
test pins the page-level absolute Svg hang.
Txt always sets an absolute line height; RenderTxt never does. Row pins
every cell and stacks when the fill column would be too narrow. IdText and
FitText shrink then chunk or truncate. The fontkit measurer matches react-pdf
text widths exactly. A TypeScript-AST lint test enforces the layout rules
(no lineHeight outside Txt, no non-literal fixed/wrap/minPresenceAhead,
flexGrow needs flexBasis, no stray absolute positioning).
react-pdf wraps text once at the first flex measure; when yoga then shrank
the vendor column the cached lines ran under the INVOICE meta column. The
header now pins column widths (299 + 24 + 192 pt), sizes the logo from its
own aspect (stacked above the text when it would leave under 160 pt), uses
absolute per-Text line heights, and turns hyphenation off.
Also: repeating table header with an orphan-header guard, wrap=false rows,
Page n of N footer, GSTIN printed only when present, one Text per address
line, HTML twin header grid. The light-theme header showed the white-text
wordmark; the ternary was inverted.
Adds vitest + pdfjs-dist harness that fails on the old document (address
overlaps Invoice No.) and passes on the new one.