Changelog
All notable changes to LensPDF are documented here. The format follows Keep a Changelog and the project follows Semantic Versioning.
[Unreleased]
Section titled “[Unreleased]”[0.4.0-beta.30] — 2026-06-24
Section titled “[0.4.0-beta.30] — 2026-06-24”Lets a host fully self-host the pdf.js workers and drop the runtime unpkg
CDN dependency. The viewer uses two different pdfjs-dist majors — react-pdf’s
(page-render substrate) and lens-pdf’s own (createBrowserViewerServices:
separations / TAC / layers / densitometer / page raster) — and until now only
the latter’s worker URL was host-overridable; the substrate worker was pinned
to unpkg with no escape hatch short of mutating pdfjs.GlobalWorkerOptions
before importing lens-pdf. Why adopt: if your deployment can’t reach unpkg
at runtime (air-gapped plant hosts, strict CSPs, or you just don’t want a
third-party CDN in the critical path), you can now serve both workers from
your own origin.
substrateWorkerSrcprop onLensPDF/LensPDFDemo. Overrides the react-pdf page-render worker (pdfjs.GlobalWorkerOptions.workerSrc), the companion to the existingworkerSrc(which overrides the analysis pdf.js worker). Set both to self-host completely.PdfSubstrategained the matchingworkerSrcprop and applies it before<Document>mounts. The two URLs point at differentpdfjs-distversions (react-pdf’s vs lens-pdf’s) and are not interchangeable — copy each from thepdfjs-distresolved through its respective package. Defaults are unchanged (unpkg, legacy build), so existing consumers are unaffected.
[0.4.0-beta.29] — 2026-06-24
Section titled “[0.4.0-beta.29] — 2026-06-24”Makes the viewer’s own pdf.js rendering work on browsers older than Chrome
145 / Firefox 144 / Safari 18.4, and makes stageAlign="left" actually pin
the page top-left. Both were live defects: on lenspdf.com/demo the base page
rendered but every lens-pdf tool silently failed (separations, layers,
color picker, densitometer, measure, TAC heatmap, page raster), and the page
sat off-center despite stageAlign="left". Why adopt: if any of your
users are on a browser released before ~early-2026, this is the difference
between a working viewer and a blank one.
- pdf.js fallback + browser services now use the legacy build.
fallback-pdfjsandbrowser(createBrowserViewerServices) imported the modernpdfjs-distbuild and loaded the modern worker. The modern build callsMap.prototype.getOrInsertComputednatively — a method that only exists in Chrome 145+ / Firefox 144+ / Safari 18.4+ — in both its main-thread and worker code, so on any older browser every fallback raster threwgetOrInsertComputed is not a functionand the dependent tools went blank (the base react-pdf page kept rendering becausepdfjsWorker.tsalready pinned react-pdf to the legacy worker — this extends that same fix to lens-pdf’s own pdf.js usage). Both modules now importpdfjs-dist/legacy/build/pdf.mjsand defaultworkerSrcto the matchinglegacy/build/pdf.worker.min.mjs, which ship the core-js polyfills. ThedefaultPdfWorkerSrc/defaultBrowserWorkerSrcexports now resolve to the legacy worker URL. stageAlign="left"now actually anchors the page top-left.PdfSubstrateset theTransformComponent’salignItems/justifyContenttoflex-start, butTransformWrapper’scenterOnInitapplied an initial translate that re-centered the content regardless.centerOnInitis now disabled when pinning (stageAlign="left"or mobile), so the page starts flush against the stage’s top-left corner. Panning is unaffected.
[0.4.0-beta.28] — 2026-06-24
Section titled “[0.4.0-beta.28] — 2026-06-24”Lets a host render a clean, single-chrome demo (one logo, page anchored
top-left) without fragile CSS hacks. The public demo at lenspdf.com had
to hide <LensPDFDemo>’s upload header with a positional display:none
selector (because showUploadHeader={false} broke the empty-state file
picker) and override the centered page with margin:0 !important — both
keyed to an exact DOM shape, so a version bump silently re-introduced the
double logo / off-center page. This release fixes both at the source.
Why adopt: if you embed <LensPDFDemo> you can now pass
showUploadHeader={false} + stageAlign="left" and delete those CSS
workarounds.
showUploadHeader={false}no longer breaks the empty-state file picker. The hidden<input type="file">was rendered inside theshowUploadHeader-gated<header>, so suppressing the header also unmounted the input — leavingfileInputRef.currentnull and turning the empty-state “Choose a file” button (and the drag-drop swap) into a silent no-op. The input is now rendered unconditionally at the wrapper root, soshowUploadHeader={false}behaves exactly as its JSDoc has always promised: header hidden, upload affordances intact. Hosts that suppressed the duplicate-chrome header via CSS can drop that hack.
stageAlignprop on<LensPDF>and<LensPDFDemo>("center"|"left", default"center")."left"pins the rendered page to the stage’s top-left on desktop — the same flush-corner anchoring the substrate already applied on mobile — so embedded/demo hosts get a predictable page origin instead of horizontal centering, without overridingstageInnerStyle’s margin from the outside. Backward compatible: the default keeps today’s centered behavior, and mobile keeps pinning top-left regardless.
[0.4.0-beta.27] — 2026-06-19
Section titled “[0.4.0-beta.27] — 2026-06-19”Adopts codex document schema 1.5.0’s two additive dieline fields in
the codex summary adapter, so the dieline overlay lands on the right page
of a multi-page document and shows the ISO 19593 line class. Why adopt:
if your host feeds codex’s summary to <LensPDF dataConfig={{ codexSummary }}>
(or calls fromCodexSummary directly) and runs codex ≥ 1.5.0, you now get
correct page attribution and the dieline class for free — no host code
change. Purely additive and backward-compatible: older codex output (no
size.page / subtype) maps exactly as before.
DielineResult.page(number, optional) — the 1-based page the dieline geometry was measured on, read from codex’ssummary.dieline.size.page(schema 1.5.0+).fromCodexSummaryonly honours a finite, positive integer; a0/document-level or junk value is omitted so behaviour is unchanged from older codex output.DielineResult.subtype(string | null, optional) — the ISO 19593 dieline class (cut/crease/perf/score/kiss_cut/fold/bleed/emboss), read from codex’ssummary.dieline.candidates[].subtype.fromCodexSummaryprefers the chosen named candidate’s subtype, falling back to the first candidate that carries one.- Page-attributed dieline overlay.
DielineOverlaynow draws only onDielineResult.pagewhen codex reported it (via the new exported pure predicatedielineShowsOnPage), instead of painting the dieline on whatever page is shown. Whenpageis absent it falls back to the old draw-on-current-page behaviour — backward-compatible. - Dieline class surfaced in the UI. The
DielineOverlaysize chip appends the class (e.g.Dieline · cut), andDielineInfoPanelshowsClass+Pagerows when present.
Changed
Section titled “Changed”- The structural codex shapes
fromCodexSummaryreads (CodexDielineSizeShape/CodexDielineCandidateShape) gained the two optional 1.5.0 fields. The peer-dep source of truth (@printwithsynergy/codex-client) is unchanged; lens-pdf only pins the fields it consumes.
[0.4.0-beta.26] — 2026-06-19
Section titled “[0.4.0-beta.26] — 2026-06-19”- FindingsSidebar location filter. A new All / In artwork /
Informational pill row sits beside the existing severity pills. Hosts
that surface a mix of locatable findings (a
bbox/regionsthe canvas can highlight) and document-level advisories can now let reviewers focus on just the artwork-visible findings — or just the informational ones — without writing their own predicate. It composes with the tier filter and preserves the existing Located in viewer / Informational section split, and uses the samehasViewerLocationsource of truth as the canvas, so “In artwork” matches exactly what is clickable on the page. Purely additive —locationFilterdefaults to"all", so existing consumers see no change until they interact with the new pills.
[0.4.0-beta.25] — 2026-06-12
Section titled “[0.4.0-beta.25] — 2026-06-12”First release where the headless renderer carries the synergy
lens.inspect integration: hosts running workflow nodes against
@printwithsynergy/lens-server need lens-server 0.2.0 (released in
lockstep with this tag) for POST /inspect; the library itself picks up
the loupe→lens key rename and fresher dev tooling.
Changed
Section titled “Changed”- pdf.js upgraded to 6.x —
pdfjs-dist5.7.284 → 6.0.227 (major). Hosts using the pdf.js fallback adapter should review pdf.js 6’s[api-major]notes (minimum supported browsers raised, polyfills removed). CI is green across the suite; the structural codex-client surface is unaffected. - Internal annotation key renamed loupe→lens — completes the loupe-pdf → lens-pdf rename inside the annotation model. No public API change; adapters and plugin slots are untouched.
- Dev-dependency refresh (Biome 2.5.0, vitest 4.1.8, @types/react 19.2.17). No runtime dependency changes.
lens-server 0.2.0 (released with this tag)
Section titled “lens-server 0.2.0 (released with this tag)”- Added
POST /inspect— the document-inspection route synergy’slens.inspectnode calls. Returns page geometry + ink/separation summary for a PDF without rendering it. - Fixed Ghostscript ≥10 compatibility:
readPageCountnow reads bbox output from stderr, and tiffsep separation filenames in the modernpage(Cyan).tiffform are accepted. - OpenAPI 3.1 spec published + Spectral-linted for the full lens-server surface.
[0.4.0-beta.24] — 2026-06-05
Section titled “[0.4.0-beta.24] — 2026-06-05”- npm publish unblocked (second pnpm v11 gate). After the
beta.23minimumReleaseAgefix, the publish hit pnpm v11’s second strict gate —ERR_PNPM_IGNORED_BUILDS: [email protected]— which errors on un-approved dependency build scripts. Approved thecanvasnative devDependency’s prebuilt-binary install viaallowBuildsinpnpm-workspace.yaml. This is the first published build carrying thefromLintFindingspage-parity fix (frombeta.22) and the dependency age-pin (frombeta.23); both of those tags failed to publish.
[0.4.0-beta.23] — 2026-06-05
Section titled “[0.4.0-beta.23] — 2026-06-05”- npm publish unblocked. The publish workflow (pnpm v11) enforces a
supply-chain
minimumReleaseAgeguard that rejected three legitimate but freshly-published transitive lockfile entries ([email protected],[email protected],[email protected]), failing the0.4.0-beta.22release (andbeta.21before it). Pinned each to its latest >24h-old predecessor (7.8.1/7.27.0/2.1.1) viapnpm-workspace.yamloverrides; no source or behavior change frombeta.22(typecheck + 56 tests + build all pass). Ships thefromLintFindingspage-parity fix thatbeta.22could not publish.
[0.4.0-beta.22] — 2026-06-05
Section titled “[0.4.0-beta.22] — 2026-06-05”fromLintFindingsoverlay page parity — overlays no longer land one page too high. The adapter incremented lint-pdf’spage_numby one, on the false assumption that lint emits 0-indexed pages. lint-pdf’sFindingResponse.page_numis in fact already 1-indexed (src/lintpdf/api/schemas.py: “Downstream adapters MUST treat the value as already 1-indexed and pass it through unchanged to pdfjsgetPage(n)”), so every lint-sourced overlay was drawn on the wrong page (a finding on page 3 rendered on page 4). The+ 1is removed;page_numnow passes through unchanged, with a0/document-level value clamped up to page 1. Adopt this bump if you map lint findings throughfromLintFindings— your overlays now align with the page the finding actually references. The docstring also now points at lint’s 1-indexedJobResponse.codex_findings+fromCodexFindingsas the preferred path.
[0.4.0-beta.20] — 2026-06-01
Section titled “[0.4.0-beta.20] — 2026-06-01”- Fabric unavailable self-hides instead of breaking pan/zoom.
AnnotationCanvasdynamically importsfabric(optional peer dep). When a host hasn’t installed it the import previously rejected as an unhandled promise, spamming 50+ errors per session and locking up pointer events (clicks after zoom unresponsive, Move/Pan tools stopping). The component now catches the rejection and returnsnull— annotation canvas self-hides cleanly, matching theisUnwiredpattern used for other missing services. - Layer raster no longer throws on pdfjs-dist v5.
pdfjs-dist v5 enforces that
getOptionalContentConfig()andrender()are called with matchingintent. All three call sites inbrowser/index.ts(getOcgIds,buildLayerUrl,listLayers) were omitting the argument (defaulting todisplay), causing every layer raster and layer-list request to throw"Must use the same intent-argument…". All three now pass{ intent: "print" }to match the render path. - Layer toggle UI default-on state now matches print-intent rasters.
listLayerswas computingdefault_on/visibleflags from a display-intent OCG config, so the layer panel could start out of sync with what the print-intent layer composites actually showed. Now uses the same print-intent config asgetOcgIds/buildLayerUrl.
[0.4.0-beta.17] — 2026-05-29
Section titled “[0.4.0-beta.17] — 2026-05-29”- Selecting the same finding after its geometry changes now re-frames.
The substrate’s focus effect deduped on
focusKeyalone, so a host enriching the same finding in place (same id, updatedregions/bbox) would not see the view re-fit. The dedup tuple is now(key, rect contents)via a new purerectsEqualhelper inplugin/fit. Unrelated re-renders still no-op; the manual-pan-not- yanked guarantee is preserved.
Changed
Section titled “Changed”minScale/maxScaleare a single source of truth — and now configurable. The TransformWrapper’s pan/pinch limits and thecomputeFitScaleclamp the focus effect uses previously had two hardcoded0.25/4copies, so a future edit to one without the other would produce a fit asking for a scale the wrapper silently re-clamped (framing the wrong rect). Hoisted toDEFAULT_MIN_SCALE/DEFAULT_MAX_SCALEat the top ofPdfSubstrateand exposed as optional props onPdfSubstrateandLensPDFso hosts that need deeper zoom on wide-format art or high-DPI imagery can widen the range without forking. Defaults unchanged.
[0.4.0-beta.16] — 2026-05-29
Section titled “[0.4.0-beta.16] — 2026-05-29”- Built-in adapters now honor
OverlayItem.regions.fromCodexFindings,fromLintFindings,fromCallasFindings,fromPitstopFindings, andfromArtworkFindingspick up asource.regionsarray (validated as an array of[x0, y0, x1, y1]bboxes) and pass it through to the resultingOverlayItem. Existing single-bboxfindings are unaffected. Source data with multi-rect findings (the same low-res image placed in N corners, every run of a misspelled word, etc.) now light up every spot on selection and frame as one group, without per-host adapter rewrites. initialShowFindingsprop onLensPDF/LensPDFDemo. Whentrue, the viewer mounts with the finding overlay layer already enabled (boxes + F-number badges drawn on the page) instead of waiting for the user to toggle “Finding overlays” or open the Inspection tab. Defaultfalsepreserves existing behavior. Useful for review tools, embedded previews, and the showcase demo.- Demo
findings showcasemode. The Vite app underdemo/now has two top-level tabs: aLensPDFDemo-driven findings showcase (the new default) against a sample PDF with curatedbbox/ multi-region / cross-page / loc-less findings, and the original PR #3 / #4 hide- on-unwired smoke test preserved as a secondary tab. Documents the full selection → navigate → zoom-to-fit flow visually.
Changed
Section titled “Changed”- Docs —
OverlayItem.regionsdocumented indocs/plugins.md(new “Located vs. loc-less findings” contract table);docs/components.mdprops table gainedinitialShowFindingsand clarified thatselectedItemdrives navigate-and-frame;docs/architecture.mdnotesregionsalongsidebbox.
[0.4.0-beta.15] — 2026-05-29
Section titled “[0.4.0-beta.15] — 2026-05-29”- Zoom readout / slider sync after zoom-to-fit is now robust across
cross-page jumps. The beta.14 fix used a post-animation
setTimeoutinside the focus effect; whenrenderedupdated again as the new page settled, the effect’s cleanup cancelled the pending sync and thefocusKeydedup prevented rescheduling, so the host zoom stayed stale. Driven now by the TransformWrapper’sonTransformcallback so every animation tick (gesture or programmatic) feeds the resulting scale back to the host without a timer.
[0.4.0-beta.14] — 2026-05-29
Section titled “[0.4.0-beta.14] — 2026-05-29”OverlayItem.regions— optional array of extra[x0, y0, x1, y1]PDF-point rects for a finding that spans several disjoint spots on a page.FindingsOverlayDOMdraws thebboxplus every region (one F-number badge per finding), and selecting the finding frames the union of all rects.hasViewerLocationalready treated a non-emptyregionsarray as locating; the field is now first-class on the publicOverlayItemcontract.- Zoom-to-fit on finding selection. Selecting a located finding now
centers it and zooms to fit (in addition to the existing page jump),
via new
PdfSubstratefocusRect/focusKeyprops that drive react-zoom-pan-pinch’szoomToElement. Framing waits for the target page to render on a cross-page jump and de-dupes byfocusKey, so manual pan/zoom is never yanked back. The geometry lives in a new pureplugin/fitmodule (computeFitScale/unionBbox/collectItemRects/itemFocusBbox), unit-tested.
Changed
Section titled “Changed”- Loc-less findings are annotation-only. The legacy
PageCanvasno longer draws a page-level border for a selected finding that has neitherbboxnorregions; such findings are surfaced in the sidebar and navigate to their page but never produce a canvas highlight. (The liveFindingsOverlayDOMpath already behaved this way.)
- The zoom readout / slider no longer goes stale after a zoom-to-fit:
zoomToElementdoesn’t emitonZoom, so the framed scale is now synced back to the host once the fit animation settles.
[0.4.0-beta.12] — 2026-05-25
Section titled “[0.4.0-beta.12] — 2026-05-25”Changed
Section titled “Changed”- Codex overlay extraction now defers to browser idle instead of
firing in parallel with pdfjs on mount. The
codexprop triggers a full-PDF fetch +extractStream()to enrich separations / TAC / layers; running it eagerly contended with react-pdf’s own fetch and slowed first paint (especially on mobile bandwidth). It now waits forrequestIdleCallback(1.2ssetTimeoutfallback on Safari < 17 / older iOS) so the page paints from pdfjs first, then the richer codex data backfills the separation / TAC / layer panels when it streams in. Pure timing change — same end result, off the critical render path.
[0.4.0-beta.11] — 2026-05-25
Section titled “[0.4.0-beta.11] — 2026-05-25”- Loading state no longer shows a flash of white box. The
substrate’s page wrapper had a permanent
background: #fff+ shadow, so while pdfjs was parsing the user saw a small white rectangle with the spinner sitting in the middle — read as “broken / oversized loading placeholder”. The wrapper now defaults to transparent + shadowless and snaps to white+shadow the momentDocument.onLoadSuccessfires. 180ms transition smooths the swap.
[0.4.0-beta.10] — 2026-05-25
Section titled “[0.4.0-beta.10] — 2026-05-25”- PDF never rendered on mobile Safari. User reported the
loading skeleton hung indefinitely on iOS even with a valid
PDF URL. Headless playwright test against the same URL on
desktop Chromium showed the page loading fine BUT with two
net::ERR_ABORTEDon the HEAD pre-check that b9 introduced — the cleanup function tore down the HEAD on every component re-render, and on mobile Safari the request churn cascaded into pdfjs’s own internal fetch. - Removed the b9 HEAD pre-check entirely. Replaced with a
passive 30-second safety timer that surfaces a clear error
banner if
Document.onLoadSuccesshasn’t fired by then. No request abort cascade; healthy loads complete in <2s and never trigger the timer. - Switched the default worker URL to the
legacy/build/variant. Same pdf.js, broader browser compat — the standard build assumes newer JS engine features that some Safari versions can’t spin a module worker for, producing a silent hang. The legacy build sidesteps that.
Changed
Section titled “Changed”- Simplified
LensLoadingSkeleton. Dropped the page-shaped placeholder + shimmer sweep — for the brief load window it was overdesigned and read as “broken” to at least one user. Now just a small centered spinner + label + optional logo. All props (logo,label,accentColor) unchanged.
[0.4.0-beta.9] — 2026-05-25
Section titled “[0.4.0-beta.9] — 2026-05-25”- PDF substrate no longer hangs on the loading skeleton when the
URL 404s (or any non-2xx). react-pdf’s
<Document>fetches the file internally via pdfjs and on a bad response sometimes sits on the unparseable body without firingonLoadError— the user saw the spinner indefinitely. Added aHEADpre-check on string URLfileprops; if the response is non-OK, the substrate short-circuits with a clear error banner (PDF unavailable (HTTP 404)/ similar) instead of mounting<Document>at all. - Skipped for
File/ArrayBuffer/{url}inputs since those aren'''t directly HEAD-able.
[0.4.0-beta.8] — 2026-05-25
Section titled “[0.4.0-beta.8] — 2026-05-25”showUploadHeader?: booleanprop on<LensPDFDemo>(defaulttrue). Whenfalse, the upload chrome header (URL bar + file picker + brand row) is suppressed so the inner<LensPDF>top bar is the only visible chrome. Drag-and-drop on the wrapper still works as a swap path. Solves the “two stacked chromes” look the lens demo had on mobile — LensPDFDemo’s outer header sat above LensPDF’s own LensTopBar.
[0.4.0-beta.7] — 2026-05-25
Section titled “[0.4.0-beta.7] — 2026-05-25”@printwithsynergy/lens-pdf/workersubpath export — a zero-import entry point that re-exportsdefaultPdfjsWorkerSrcandREACT_PDF_BUNDLED_PDFJS_VERSION. Hosts must use this subpath when importing the constant from an SSR context (Astro frontmatter, Next.js getServerSideProps).
- SSR boot crash, take 3. b6 split the worker URL into a
leaf module but importing
defaultPdfjsWorkerSrcfrom the package root (@printwithsynergy/lens-pdf) STILL crashed Node: the barrelindex.tsre-exports from./browserwhich importspdfjs-distdirectly, so any consumer touching the barrel in an SSR context loaded the whole graph (including pdfjs-dist'''sDOMMatrixreference) regardless of which export they actually used. The new./workersubpath sidesteps the barrel entirely.
[0.4.0-beta.6] — 2026-05-25
Section titled “[0.4.0-beta.6] — 2026-05-25”- SSR boot crash, take 2:
ReferenceError: DOMMatrix is not definedfor hosts importingdefaultPdfjsWorkerSrcinto a server module. The b4 + b5 implementation readpdfjs.versionvia a re-export ofreact-pdf, which transitively importedpdfjs-dist— andpdfjs-dist’s top-level module touchesDOMMatrix, a browser API Node doesn’t have. - Moved
defaultPdfjsWorkerSrcinto a new leaf module (components/pdfjsWorker.ts) with NO imports — just a string constant pinned to thepdfjs-distversion[email protected]bundles (5.4.296). Hand-pinned for now; bump in lockstep when react-pdf updates.
[0.4.0-beta.5] — 2026-05-25
Section titled “[0.4.0-beta.5] — 2026-05-25”- SSR boot crash for hosts that import lens-pdf into a server
module (e.g. Astro frontmatter pulling
defaultPdfjsWorkerSrcfor a<link rel="preload">tag). The previous side-effect imports ofreact-pdf/dist/Page/AnnotationLayer.cssandTextLayer.cssbroke Node ESM at load time withERR_UNKNOWN_FILE_EXTENSION. Inlined both stylesheets as a JS string (reactPdfCss.ts) and inject them viadocument.headon first mount instead. Client-only execution, no SSR side effects.
[0.4.0-beta.4] — 2026-05-25
Section titled “[0.4.0-beta.4] — 2026-05-25”- Branded loading skeleton, customisable via props. Replaces the bare “Loading PDF…” / “Loading page N…” text with a page-shaped placeholder (US Letter aspect ratio) + shimmer sweep + token- coloured spinner. Reads as active work instead of a frozen tab during the first-paint window.
loadingPlaceholder?: ReactNodeprop on<LensPDF>for full override — pass any React node to replace the substrate’s loading screen entirely.LensLoadingSkeletoncomponent exported from the package root with props{ tokens, label?, logo?, accentColor? }. Hosts can mount it directly with a customlogoandlabelto keep the default look with brand chrome on top, instead of writing the whole loading state from scratch.defaultPdfjsWorkerSrcexported from the package root. Hosts can<link rel="preload" as="script" href={defaultPdfjsWorkerSrc}>the worker alongside their HTML — the cold-start fetch (~500 KB from unpkg CDN) was the biggest pre-paint delay. With preload, the browser starts downloading the worker the moment the document hits the wire, parallel with the JS bundle.
[0.4.0-beta.3] — 2026-05-25
Section titled “[0.4.0-beta.3] — 2026-05-25”- iOS Safari tab crash (“A problem repeatedly occurred”) in the
new react-pdf substrate. Root cause:
RENDER_SCALE = 2combined with the defaultwindow.devicePixelRatio(2-3 on Retina) meant react-pdf was producing a ~17 megapixel canvas per page, ~70 MB RGBA — well over iOS Safari’s per-canvas memory cap (~16 MP). pdf.js silently OOM’d and the tab crashed. - Fix: explicit
devicePixelRatio={1}on<Page>so the canvas matches the CSS pixel grid 1:1, plus droppedRENDER_SCALEfrom 2 → 1.5. Same US Letter page now renders to ~1.4 megapixels (~5.5 MB RGBA). Visual sharpness during pinch-zoom is handled by the TransformWrapper’s CSS transform, so we don’t need oversized source pixels.
[0.4.0-beta.2] — 2026-05-25
Section titled “[0.4.0-beta.2] — 2026-05-25”- PDF still failed to load with “fake worker failed” error in b1.
Root cause: react-pdf 10.x ships
pdfjs.GlobalWorkerOptions.workerSrcwith a deliberately broken sentinel default of"pdf.worker.mjs"(a bare module name with no URL base), intended to force consumers to override. The b1 guard!pdfjs.GlobalWorkerOptions.workerSrcwas false against that truthy sentinel — so my CDN URL was never applied and pdf.js fell back to its in-thread “fake worker” mode, which then tried to dynamic-import"pdf.worker.mjs"relative to the page and produced the famous “Module name … does not resolve to a valid URL” error. - Replaced the conditional with a real-URL check: we now skip the
CDN override only if the existing
workerSrclooks like an actual URL (http://,https://,blob:, or/), so hosts that want to ship a self-hosted worker still win when they set it before importing lens-pdf.
[0.4.0-beta.1] — 2026-05-25
Section titled “[0.4.0-beta.1] — 2026-05-25”- “Failed to load PDF” on consumer builds.
0.4.0-beta.0setpdfjs.GlobalWorkerOptions.workerSrcvia thenew URL(..., import.meta.url)recipe from react-pdf'''s README, which only resolves when the bundler can see lens-pdf'''s ownnode_modules. The moment a host (Astro, Next.js, Vite app) consumes lens-pdf as a compiled package, that URL resolves to nothing and pdf.js fails to spawn its worker. Switched to the same unpkg CDN pattern the rest of the library already uses (matched to the versionreact-pdfactually bundles viapdfjs.version). Hosts can still opt in to a local worker by settingpdfjs.GlobalWorkerOptions.workerSrcthemselves before mounting<LensPDF>. - Document error banner now surfaces the underlying error message (worker / network / parse failure detail) instead of just “Failed to load PDF.” — makes future debugging from a screenshot possible without attaching a remote debugger.
[0.4.0-beta.0] — 2026-05-25
Section titled “[0.4.0-beta.0] — 2026-05-25”Changed (BREAKING substrate swap — page-mode rendering)
Section titled “Changed (BREAKING substrate swap — page-mode rendering)”- New PDF render substrate: page-mode rendering now runs through
react-pdf(Mozilla pdf.js wrapper) inside areact-zoom-pan-pinchcontroller. This is the Acrobat-grade substrate — one-finger pan, pinch-zoom, double-tap zoom, momentum scroll all handled natively by the browser / pdf.js, no custom touch routing involved. Replaces the homegrown tile-fetch + canvas + overflow:auto stack that the previous dozen PRs had been fighting on iOS Safari. - New components:
PdfSubstrate(the substrate itself) andFindingsOverlayDOM(DOM-based finding bboxes + F-badges that layer inside the substrate’s overlay slot). - Affected modes:
pageandfindings. The substrate renders the primary PDF page; BoxOverlay, DielineOverlay, TACHeatmapOverlay, and the new FindingsOverlayDOM all mount inside its overlay slot and scale/pan with the page automatically. - Unaffected (legacy path):
separationandlayermodes still use the existing SeparationCanvas / LayerCanvas tile renderers, which will be migrated in a follow-up. Their built-in pan/zoom remains on the legacy overflow:auto stack.
Added dependencies
Section titled “Added dependencies”react-pdf@^10.4.1— wrapspdfjs-dist 5.x(compatible with the existingpdfjs-dist@^5.7.284direct dep).react-zoom-pan-pinch@^4.0.3— the gesture controller.
Temporarily regressed (will be re-anchored in 0.4.0-beta.1)
Section titled “Temporarily regressed (will be re-anchored in 0.4.0-beta.1)”- Annotation drawing in page mode (AnnotationCanvas needs re-mount inside the new substrate).
- Measure / color picker / densitometer tools in page mode.
- These still work in separation / layer modes via the legacy canvas path.
Migration notes
Section titled “Migration notes”Hosts pass the same props as before — pdfUrl, items,
selectedItem, decisions etc. all unchanged. The substrate swap
is transparent at the API level. Major version bump because the
underlying rendering stack is now Mozilla’s pdf.js (not custom
tile fetch) and a couple of tools are temporarily missing in page
mode.
[0.3.0-beta.101] — 2026-05-25
Section titled “[0.3.0-beta.101] — 2026-05-25”- Annotation toolbar no longer drifts off-screen on mobile. After
b100’s pan fix made
stageInnerStylewidth: max-content, the toolbar inside it (withalign-self: center) ended up centered relative to the canvas rather than the viewport — panning right on a wide PDF pushed it off the screen. Hoisted the toolbar out of the scroll container into a sibling above<section>, so it stays anchored to the viewport regardless of canvas pan. - Measurement tool now works on mobile. Switched from React
synthetic touch handlers to native
addEventListenerwith{ passive: false }. React’s synthetic touch events are passive in some bundler / React-version combinations, soe.preventDefault()was a no-op — iOS Safari would initiate its own scroll/zoom on touchmove and race the measurement state update, leaving the tool in a no-op state. The element still carriestouch-action: nonebelt-and-suspenders.
[0.3.0-beta.100] — 2026-05-25
Section titled “[0.3.0-beta.100] — 2026-05-25”- Mobile horizontal panning (round 3) — the b99
align-items: safe centerfix wasn’t enough on iOS Safari. Switched the stage<section>to plain block layout and gavestageInnerStylethe cross-browserwidth: max-content; min-width: 100%; margin: 0 autopattern. Centers the canvas when it fits the section, scrolls horizontally to either edge when it doesn’t. Tested for flexbox-centered-overflow gotchas that defeated every prior attempt.
- Per-finding visibility toggles in the Inspection panel. Each
row now ships a
Show/Hidebutton. Hidden findings are filtered out of the canvas overlay set (bboxes + F-number badges disappear) but remain in the panel list, greyed, for re-enable. Show all/Hide allbulk controls in the Inspection panel header. Buttons disable themselves when the action would be a no-op (Show all when nothing is hidden, Hide all when everything is already hidden).hiddenFindings+setHiddenFindingsadded toLensPDFShellPluginContextso third-party shell plugins can read/write the same visibility state.
Changed
Section titled “Changed”- Inspection panel selection styling — selected rows now show a 1px border, a 3px left accent stripe, and a translucent background in the tier colour (red for errors, amber for warnings, etc.). The previous 5%-white tint was almost invisible on the dark panel.
[0.3.0-beta.99] — 2026-05-25
Section titled “[0.3.0-beta.99] — 2026-05-25”- Mobile panning regression — over-wide canvases were unscrollable
on iOS Safari after b97 introduced the LensTopBar. Two root causes:
stageStyleandstageInnerStyleusedalign-items: center, which on iOS clips overflowing children on both sides so the user can’t pan to either edge of a canvas wider than the viewport. Switched toalign-items: safe center— falls back tostartwhen overflow occurs.- The new
LensTopBarusedposition: stickyinside a parent withoverflow: hidden. Sticky degrades to relative there anyway, but on iOS Safari it occasionally misroutes touch events away from sibling scroll containers. Switched toposition: relative.
- Added
-webkit-overflow-scrolling: touchand explicittouch-action: pan-x pan-ytostageStyleas belt-and-suspenders for iOS momentum scroll + unambiguous pan-target routing.
[0.3.0-beta.98] — 2026-05-25
Section titled “[0.3.0-beta.98] — 2026-05-25”Changed
Section titled “Changed”topBarActions→menuActions(renamed + relocated). The declarative host action buttons now render inside the tools menu (hamburger drawer on mobile / persistent left sidebar on desktop), pinned above the plugin panels. Top-bar real estate was getting crowded on narrow viewports — moving these into the menu keeps the top bar compact (just brand + hamburger). TheLensTopBarActiontype is nowLensMenuAction. Hosts that adopted b97 should rename the prop + import; behaviour is otherwise identical (same fields:{ id, label, href?/onClick?, download?, external?, order? }).hasAnyTool(which drives sidebar / drawer rendering) now also considersmenuActions.length > 0, so a host can ship a menu with actions even before any plugins become available.
[0.3.0-beta.97] — 2026-05-25
Section titled “[0.3.0-beta.97] — 2026-05-25”LensTopBar— persistent customizable top bar inside<LensPDF>. Layout left to right: mobile hamburger, brand logo + label,"topbar"shell-plugin slot nodes, host-injected action buttons. Hosts whose own page chrome currently wraps<LensPDF>(LintPDF logo + back-button pattern) can now drop the wrapper — LensPDF owns the chrome.topBarActions: LensTopBarAction[]prop on<LensPDF>for declarative top-bar buttons. Each action is{ id, label, href?, onClick?, download?, external?, order? }. The library renders anchors whenhrefis set and buttons otherwise. Hosts construct the array conditionally, so any action they can’t satisfy is simply omitted — no built-in “Download / Report / Back” assumption."topbar"added to theLensPDFShellSlotunion for full React-control insertion into the top bar (save-status, search, etc.). The declarativetopBarActionsroute still covers the 90% case.showTopBar?: booleanprop on<LensPDF>(defaulttrue). Hosts that already render their own chrome can opt out.LensTopBarActionis re-exported from the package root for type consumption by hosts.
Changed
Section titled “Changed”- Mobile drawer hamburger moved into
LensTopBar(was the floating FAB at the canvas corner introduced in b95/b96). The new hamburger has nohasAnyTooldependency — it stays visible whileisMobile === true, so the menu can no longer “disappear” during the async window between page paint and findings load. Theposition: absoluteFAB block is removed.
[0.3.0-beta.96] — 2026-05-25
Section titled “[0.3.0-beta.96] — 2026-05-25”- Finding-badge tooltip “Leave a note” action — tapping (mobile) or hovering (desktop) an F-number badge surfaces a tooltip with the finding title + description + a Leave-a-note button. Tapping the button auto-creates a blank annotation pre-tagged to that finding and focuses its textarea. On mobile the tools drawer auto-opens so the focus event is visible.
- Page-view overlay toggles — new
showDieline/showFindingsshell-context state with matching checkboxes in the Page / Sep / Layer tabs of the Tools panel. Independent of Inspection-mode auto-render.
Changed
Section titled “Changed”- Inspection overlays now gate on Inspection mode by default.
Fresh load on the Page tab shows only the PDF artwork — no
dieline outline, no finding bbox highlights, no F-number badges,
no TAC heatmap. Overlays auto-render when
viewerMode === "findings"(Inspection tab) or when the host/user flips the matching toggle.
[0.3.0-beta.93] — 2026-05-23
Section titled “[0.3.0-beta.93] — 2026-05-23”- Clicking a preflight finding whose
pagelands past the document end no longer surfaces the redInvalid page request.banner from pdfjs. The selection→currentPage effect in<LensPDF>now clamps the target page to[1, pageCount]before drivingsetCurrentPage, so a drifted adapter (e.g. a lint engine that emitspage_num > total - 1) cannot push the viewer into an out-of-rangedoc.getPage(n)call. Reproduced via lintpdf.com/demo against a single-page label PDF. - The “prepare page” effect now swallows pdfjs failures instead of
surfacing the raw error string in a banner. With the new clamp
ahead of it, a failure here means a transient pdfjs error or a
mid-prepare document swap — both recover on the next page change,
and the bare
Invalid page request.string was poor UX either way. fromLintFindingsrejects non-finite, non-integer, and negativepage_numvalues (NaN / floats /-1/"3") and falls back to page 1 instead of silently producingpage: NaN + 1/page: 0overlay items.
Internal
Section titled “Internal”- Added
adapters/index.test.tscovering thefromLintFindingspage-handling contract and pinning the host-side clamp math (min(max(1, pageCount), max(1, item.page))) so a future refactor of the inline expression inLensPDF.tsxcan’t drift silently.
[0.3.0-beta.92] — 2026-05-23
Section titled “[0.3.0-beta.92] — 2026-05-23”- Inspection panel rows now reflect the active selection in uncontrolled
mode.
shellPluginContext.selectedItemwas forwarding the rawselectedItemprop, so hosts that let the library own selection state (<LensPDF items={...} />withoutonItemSelect) saw the row stay unhighlighted after a click. The context now exposes the effective selection. - Clearing the selection from a shell plugin now works in uncontrolled
mode.
handleItemClickandLensPDFShellPluginContext.onSelectItemacceptOverlayItem | null; the default FindingsPanel toggle-off path routes throughctx.onSelectItem(null)instead of the host-onlyctx.onItemSelect(null). - Page-level indicator for findings without a bbox is now a static
border + glow instead of
animate-pulse. On mobile, the sidebar drawer partially covers the canvas; the pulse on the visible corner read as a stray “bottom-left blink” on every selection. - Removed the redundant
ctx.setCurrentPage(it.page)call from the FindingsPanel row handler; LensPDF’s existingeffectiveSelectedeffect already syncs the current page when selection changes.
[0.3.0-beta.88] — 2026-05-21
Section titled “[0.3.0-beta.88] — 2026-05-21”Changed
Section titled “Changed”<LensPDF>is now the complete viewer;<LensPDFDemo>is a thin wrapper. Previously<LensPDF>was a facade that delegated to<LensPDFDemo embedded>, which read backwards — the production component appeared to depend on the demo. All viewer state, services wiring, plugin slots, and rendering now live inLensPDF.tsx.<LensPDFDemo>is a small layer that owns the upload chrome (URL bar, drag-and-drop, file picker, empty state) and feeds the resolved PDF URL into<LensPDF>.LensPDFPropsis now a standalone interface with a requiredpdfUrl.LensPDFDemoPropsisOmit<LensPDFProps, "pdfUrl">plusmaxFileSizeandinitialPdfUrl. The public prop surface of both components is unchanged — this is an internal restructure.
LensPDFTooltype exported from the package root and./components.LensPDFDemoToolremains as a deprecated alias.
[0.3.0-beta.82] — 2026-05-14
Section titled “[0.3.0-beta.82] — 2026-05-14”- Numbered findings (F1…FN) in Inspection panel and on canvas. Every
finding gets a stable number in input order. The number appears as a
labelled pill badge (
F1,F2, …) drawn on the canvas (located findings only — whole-document findings get no canvas badge) and as a small chip in each Inspection panel row. - Click-to-note via F# badge. Clicking the
F{n}chip in the Inspection panel selects the finding, switches the Notes panel to it, and auto-creates a blank linked note focused for immediate typing. buildFindingNumberMap(items)helper exported from./plugin. StableMap<id, number>useful for adapter authors mapping their own findings into the viewer without duplicating the numbering logic.- Separate numbering sequences. Findings use F1…FN; hand-drawn annotations keep their own #1, #2, … counter. The two sequences never collide.
- Finding targets in the Notes panel dropdown. All findings appear at the top of the linked-note target selector so reviewers can attach prose notes to any finding without switching modes.
[0.3.0-beta.78] — 2026-05-13
Section titled “[0.3.0-beta.78] — 2026-05-13”- New
./swatchsubpath export for the Pantone Gold + process plate helpers (resolveSpotSwatch,processPlateLookup,pantoneGoldLookup,rgbToHex). Lets non-viewer hosts (marketing Codex extract panels, server-side renderers, doc generators) reuse the swatch chain WITHOUT pulling in the full./browserbundle (which transitively imports pdfjs-dist and bloats consumers that only need the lookup table). Same helpers are still exported from./browserfor viewer consumers.
[0.3.0-beta.77] — 2026-05-13
Section titled “[0.3.0-beta.77] — 2026-05-13”- Process-plate lookup added to
resolveSpotSwatch. Cyan, Magenta, Yellow, Black, and their synonyms (process-cyan, cmyk-letter shorthand, RGB plate names) now resolve to canonical primaries instead of falling through to host-provided hash-derived random colours. Lives inbrowser/pantone-gold.tsand is the new top of the resolution chain. - Public exports for the swatch helpers.
@printwithsynergy/lens-pdf/browsernow exportsresolveSpotSwatch,processPlateLookup,pantoneGoldLookup, andrgbToHexso marketing panels can reuse the same chain in their Codex extract views (where every spot rendered “Cyan” as orange because codex hash-derives swatches for process plates).
- Lint + codex marketing demo Codex extract panels rendered Black /
Cyan / Magenta / Yellow with random hash colours (orange / blue /
green / wine etc.) because they trusted codex’s
swatch_hex. The new resolution chain forces canonical CMYK primaries for known process plates and falls back to Pantone Gold- altRgb for anything else.
[0.3.0-beta.76] — 2026-05-13
Section titled “[0.3.0-beta.76] — 2026-05-13”forceInspectionPanelprop on<LensPDFDemo>/<LensPDF>. When true, the Inspection / Findings side panel stays mounted even with noitemsand renders a “no findings yet” empty state. Useful for hosts that have an in-flight preflight call (stable layout while it loads) or for demos that always advertise the panel slot. Defaultfalse— OSS hosts without preflight data don’t see an empty section.
docs/components.mdnow documents the auto-on/force-on Inspection panel behaviour, the newforceInspectionPanelprop, and the spot-colour resolution chain (spotPalette→ Pantone Gold → PDFaltRgb).
[0.3.0-beta.75] — 2026-05-13
Section titled “[0.3.0-beta.75] — 2026-05-13”Changed
Section titled “Changed”- Mobile hamburger menu moved from header-right to header-left, beside the brand/logo. Primary menu triggers belong top-left on mobile (iOS / Material convention). The zoom controls keep the right cluster for one-tap zoom in/out. No desktop change — desktop retains the full toolbar on the right.
- Separations panel now renders accurate spot-colour swatches.
Resolution chain: host-provided
spotPalette[name](typically codex’ssummary.spot_colors.colors[].swatch_hexor another preflight’s swatch) → built-in Pantone Gold library (newbrowser/pantone-gold.ts, ~85 of the most-common Coated codes) → the PDF tint transform’saltRgb(parsed at extraction) → neutral grey fallback. Previously every spot rendered the same generic purple#7c3aedregardless of the actual ink, which made it impossible to distinguish (e.g.)PANTONE 225 CfromPANTONE 236 Cin the panel.
- New
spotPaletteprop on<LensPDFDemo>/<LensPDF>. Hosts with codex / external-preflight data pass the spot → hex map in here; the separations panel picks it up automatically. - Inspection / Findings panel baked into the default shell
plugins. When the host passes
itemsto LensPDF, a newInspection (N)section appears at the top of the side drawer with tier filter chips (errors / warnings / advisories / info) and a clickable list that drivesonItemSelectfor canvas highlight + page jump. Renders nothing whenitemsis empty so OSS hosts without preflight don’t see an empty section.
[0.3.0-beta.74] — 2026-05-13
Section titled “[0.3.0-beta.74] — 2026-05-13”Changed
Section titled “Changed”- Annotation toolbar now lays out as three explicit rows on
mobile — previously the toolbar used
flex-wrap: wrapand whatever organic wrap fell out of the available width, which on a typical phone broke as[tools + one swatch] / [rest of swatches + undo / redo / saved]. Wrapped the existing children in three groups (Tools / Colours / Actions), each withflex-basis: 100%when thecompactprop is true so the three groups always take one row each. Desktop layout is unchanged — withoutcompactthe groups stay inline and the outer wrapper’sflex-wrapstill handles narrow desktop viewports.
[0.3.0-beta.67] — 2026-05-11
Section titled “[0.3.0-beta.67] — 2026-05-11”- Annotation toolbar no longer escapes into the host page’s chrome
on mobile — the mobile container used
position: fixed; top: headerChromePx, but inembeddedmodeheaderChromePxis0, so the toolbar landed at viewport-top and covered the parent page’s navigation when the viewer was mounted on a marketing site. Switched toposition: sticky; top: 0, which keeps the toolbar pinned to the top of the stage scroll container without escaping upward. Desktop was already sticky; the two code paths are now unified.
[0.3.0-beta.66] — 2026-05-11
Section titled “[0.3.0-beta.66] — 2026-05-11”- Annotation toolbar no longer blocks navigation on mobile — the
fixed toolbar that sits just below the header was a single
horizontally-scrolling strip with 28-px buttons. It now wraps onto
two rows so every control is reachable without a scroll gesture,
and every hit target is sized for fingers (tool buttons 28 → 40 px,
swatches 18 → 26 px, undo/redo padding and font scaled up, custom
colour input 22 → 32 px). Desktop layout is unchanged — the wider
hits only kick in when
compactis set, which<LensPDFDemo>passes fromctx.isMobile. The mobile container no longer needsoverflowX: auto(wrap replaces scroll).
[0.3.0-beta.65] — 2026-05-11
Section titled “[0.3.0-beta.65] — 2026-05-11”Changed
Section titled “Changed”@printwithsynergy/codex-clientdeclared as optional peer dep at^1.8.1—browser/codexOverlay.tskeeps its structuralMinimalCodexClientinterface (no runtime import), andHttpClientfrom 1.8.1 satisfies it. The peer dep is marked optional inpeerDependenciesMeta, so hosts that don’t pass acodexclient to<LensPDFDemo>/<LensPDF>are unaffected.
- Shareable links (
generateShareLink,parseShareParams) and PDF validation (validatePdfFile,validatePdfUrl) pages are now wired into the Reference group of the rendered docs sidebar (previously only reachable via inbound links from README / components). - README + components.md call out the optional codex-client peer
dep alongside the existing
fabricpeer.
[0.3.0-beta.64] — 2026-05-10
Section titled “[0.3.0-beta.64] — 2026-05-10”- Custom logo + label via
ThemeTokens—logoUrl,logoText,logoMaxHeight, andlogoAltare now optional fields onThemeTokens, letting a host bundle its full visual identity (colours + logo + label) into one tokens object instead of passing separatebrandLogoUrl/brandprops. Resolution order in<LensPDFDemo>/<LensPDF>: explicit prop > tokens > built-in default. Top-bar and welcome-screen logo<img>tags now useheight+width: autoso non-square logos keep their aspect ratio.
Changed
Section titled “Changed”- “Rasterising page & computing CMYK…” loader is now a bottom pill —
was a full-viewer dim overlay that covered the artwork while
separations / TAC were warming up. Replaced with a compact pill at
the bottom-centre of the viewer (rounded, subtle shadow,
pointer-events: none) so users can keep reviewing the page underneath while the analysis raster builds.
- Security policy and Licensing pages added to the docs site
Project group, sitting next to Contributing. Security policy is
promoted from the root
SECURITY.mdinto a proper docs page; Licensing covers the AGPL-3.0-or-later terms, third-party licences, and how to request commercial alternatives.
[0.3.0-beta.63] — 2026-05-10
Section titled “[0.3.0-beta.63] — 2026-05-10”- Show all / hide all on the Separations panel — the inks panel
now has the same
All on/All offheader buttons that the layers panel ships, plus theInks (n)count next to the title. Toggle the whole CMYK + spot stack in one click instead of clicking each ink individually.
[0.3.0-beta.62] — 2026-05-10
Section titled “[0.3.0-beta.62] — 2026-05-10”- Mobile separations / TAC:
[lens-pdf] toBlob returned null— iOS Safari intermittently returnsnullfromcanvas.toBlobfor large or memory-pressured canvases (the analysis raster + each CMYK plate + spots + TAC heatmap all share one process-wide canvas budget).rasterizeBlobUrl,buildPageUrl, andbuildLayerUrlnow route through a singlecanvasToPngBlobhelper that falls back totoDataURL → fetch → blobwhentoBlobreturns null, and the analysis raster scales itself down to a 12 MP budget so large-format pages (poster / packaging dielines) still render. Hosts usingbuildPageUrldirectly with a custom DPI are unaffected by the clamp.
[0.3.0-beta.61] — 2026-05-10
Section titled “[0.3.0-beta.61] — 2026-05-10”- SSR crash on
/demo(Cannot access '_CHANNELS' before initialization) —browser/codexOverlay.tsimportedPROCESS_CHANNELSback frombrowser/index.ts, which already re-exportscodexOverlay. Under Astro / Node ESM the cycle hit a temporal-dead-zone read at request time and broke every render of routes that load the codex overlay.PROCESS_CHANNELSnow lives inbrowser/constants.ts, a leaf module both files import from, so the cycle is gone. No public API change.
[0.3.0-beta.52] — 2026-05-09
Section titled “[0.3.0-beta.52] — 2026-05-09”Changed
Section titled “Changed”- Pre-Codex runtime restore — package contents are restored to the pdf.js fallback/runtime state immediately before the Codex-backed workflow.
- Dependency audit cleanup —
fabricpeer/dev dependency ranges now target 7.3.1, clearing the vulnerabilities reported by npm audit.
[0.3.0-beta.30] — 2026-05-05
Section titled “[0.3.0-beta.30] — 2026-05-05”- Move / Pan isolation — selecting
Move / Pannow forces annotation mode back to pointer so it never leaves pen/drawing armed while users are navigating. - Sticky mobile close control — tools drawer header on mobile is now sticky with its own background/border so the close button remains visible while scrolling long tool panels.
[0.3.0-beta.29] — 2026-05-05
Section titled “[0.3.0-beta.29] — 2026-05-05”- Mobile tools drawer stacking — tools drawer/backdrop now render above top chrome on mobile so the close button is always reachable and the panel no longer appears behind the header.
[0.3.0-beta.28] — 2026-05-05
Section titled “[0.3.0-beta.28] — 2026-05-05”- On-canvas annotation numbers — each annotation now gets a visible numbered badge in the PDF viewer so users can map panel items to page elements instantly.
- Bidirectional selection sync — selecting an annotation on-canvas now syncs the notes panel target, and selecting a target in the notes panel focuses that annotation on the canvas.
Changed
Section titled “Changed”- Annotation-linked notes support multiple entries — each numbered annotation can now hold multiple linked notes (add/remove/edit), not just one note string.
[0.3.0-beta.27] — 2026-05-05
Section titled “[0.3.0-beta.27] — 2026-05-05”Changed
Section titled “Changed”- Optional backend wiring in demo shell —
LensPDFDemonow treats host-providedservicesas a hybrid override: wired backend services are used where available, and any unwired capability automatically falls back to in-browser pdf.js RGB simulation. - No hard backend dependency for viewer tools — passing partial backend services no longer disables browser-side tooling for missing capabilities, keeping install/integration of backend stacks optional.
[0.3.0-beta.26] — 2026-05-05
Section titled “[0.3.0-beta.26] — 2026-05-05”- Explicit Move / Pan tool — tool panel now includes a dedicated neutral pointer mode so users can intentionally return to navigation without sampling/measuring/annotating.
Changed
Section titled “Changed”- Tool-load UX — sidebar now shows a deterministic
Loading tools…indicator while service-driven tool availability is being resolved, preventing controls from appearing progressively.
[0.3.0-beta.25] — 2026-05-05
Section titled “[0.3.0-beta.25] — 2026-05-05”- Tool-click viewer crash regression —
AnnotationCanvaswas being re-initialized on parent re-renders after the shell-plugin refactor, which could tear down Fabric during tool toggles. Canvas init is now page-scoped again, and annotation-history callbacks are stabilized.
[0.3.0-beta.24] — 2026-05-05
Section titled “[0.3.0-beta.24] — 2026-05-05”- Plugin-first viewer shell primitives — new reusable shell plugin
API (
LensPDFShellPlugin,resolveShellPlugins,pluginsForSlot,computeFeatureAvailability) and first-party defaults (createDefaultShellPlugins) for panel + toolbar composition. - Preset-based composition —
LensPDFDemonow accepts apreset(demo/minimal) andpluginsoverrides so hosts can replace built-in sidebar/menu blocks without forking component code.
Changed
Section titled “Changed”- LensPDF / LensPDFDemo composition — sidebar + annotation toolbar rendering now goes through slot plugins instead of hardcoded branches, making built-ins modular and reusable for custom viewers.
- Capability gating defaults — feature visibility now centralises in
computeFeatureAvailability(default-on, auto-hide when services are unwired or data is absent).
- Updated component + plugin docs with shell-plugin usage and override examples for custom viewer assembly.
[0.3.0-beta.23] — 2026-05-05
Section titled “[0.3.0-beta.23] — 2026-05-05”- Sticky-note text editing — sticky notes were being created as a
grouped paper-rect + textbox object, which made text-edit entry
unreliable in Fabric 6 interaction paths. Notes now instantiate as a
single editable
fabric.Textboxwith opaque pastel background, padding, and shadow, then immediately enter editing mode. - Mobile tools toggle overlap — menu toggles now use a clear open/ close pattern that avoids overlaying the first drawer controls: mobile top-bar toggle remains a hamburger, embedded FAB hides while drawer is open, and the drawer itself includes an in-panel close button.
[0.3.0-beta.22] — 2026-05-05
Section titled “[0.3.0-beta.22] — 2026-05-05”- Flat-PDF layer fallback row — when a PDF has no OCGs, the
Layers panel now shows a synthetic
Artwork (flattened PDF)row instead of an empty state. This is UI-only metadata (synthetic,kind: "flattened-artwork") and does not alter real layer data. - Color picker ink swatches — process and spot rows in
ColorPickerToolnow render swatch chips next to each ink name, matching densitometer readability and using stable spot-color hashing.
Changed
Section titled “Changed”- Layers mode fallback rendering — for synthetic-only layer sets
(flat PDFs),
LensPDFDemorendersPageCanvasin Layers mode so users still see artwork while toggling the fallback row. - LayerInfo typing — added optional provenance metadata:
synthetic?: booleanandkind?: "ocg" | "flattened-artwork".
[0.3.0-beta.21] — 2026-05-04
Section titled “[0.3.0-beta.21] — 2026-05-04”- Mobile tools drawer — the floating hamburger sat in the
top-left of the stage at
z-index: 60, so it covered the annotation toolbar and, when the drawer was open, overlapped the first rows of the sidebar (e.g. the “Mode” heading). The tools toggle now lives in the stacked marketing header on narrow viewports (44px touch target). Embedded mode (no URL bar) still uses a corner FAB, moved to the top-right so it doesn’t cover the left-aligned pen tool. - Drawer vs header — the dimmer and slide-in
asidestart below the measured header height (ResizeObserveron the<header>) so the URL row and ☰ stay interactive; header usesz-index: 100above the drawer (56). - See-through drawer —
sidebarStylenow sets an opaquebackground: tokens.bg. The dimmer is a heavierrgba(0,0,0,.72)with no backdrop blur so the stage doesn’t show through the panel. - Mobile URL bar — header is a column on small screens: full-
width field,
minHeight: 44/fontSize: 16(iOS won’t auto-zoom),LoadandUpload PDFas two equal full-width buttons. - Annotation list in the drawer — new
comfortableprop on<AnnotationThread>(on whenuseIsMobile) increases padding and delete / jump control sizes. Stage padding is tighter on mobile; the annotation toolbar row can scroll horizontally if needed.
[0.3.0-beta.20] — 2026-05-04
Section titled “[0.3.0-beta.20] — 2026-05-04”- Visible annotation tooltips — hover / keyboard focus on every
toolbar control opens a fixed-position tooltip chip (
role="tooltip") near the control, not only the delayed nativetitleattribute. Applies to tools, colour swatches, custom-colour input, undo/redo, hide-notes, and the Saved label.
Changed
Section titled “Changed”- Toolbar tool order — Pen is now the leftmost tool so drawing works immediately; Select (“pointer”) is second with a mouse-pointer SVG icon and explicit copy that it only grabs annotations you’ve already placed (empty canvas = nothing to select — not broken).
- Default annotation mode —
<LensPDFDemo>starts with the pen tool active instead of select, matching the new order.
[0.3.0-beta.19] — 2026-05-04
Section titled “[0.3.0-beta.19] — 2026-05-04”Changed
Section titled “Changed”- Layers empty-state copy — clarified that zero optional content groups (OCGs) is normal for flat PDFs: artwork still appears on the page composite in Page mode; OCG “layers” only exist when the file was authored with that structure (Acrobat / InDesign).
[0.3.0-beta.18] — 2026-05-04
Section titled “[0.3.0-beta.18] — 2026-05-04”Changed
Section titled “Changed”- Demo disclaimer relocated — the long “LensPDF supports full CMYK + spot inks…” footer in the sidebar was eating vertical space on every active session, even though the message is only useful before a PDF is loaded. Moved the entire paragraph onto the empty / upload screen so it greets new visitors once and the working sidebar stays compact.
[0.3.0-beta.17] — 2026-05-04
Section titled “[0.3.0-beta.17] — 2026-05-04”- Annotation list never updated after a drawing was saved —
AnnotationThreadonly loadedannotationService.list()once on mount, so even though<AnnotationCanvas>was correctly persisting fabric JSON viasaveForPageand the browser service was firingnotify(), the sidebar list stayed stuck on “No annotations yet.” Added arefreshKey?: numberprop on<AnnotationThread>(covered as auseEffectdependency) and wired it up in<LensPDFDemo>to the version tick fromuseBrowserViewerServicesVersion. Hosts using a wired backend can pass any monotonic counter — when annotations land, bump it and the thread re-fetches.
[0.3.0-beta.16] — 2026-05-04
Section titled “[0.3.0-beta.16] — 2026-05-04”LayerPanelrendered borked in non-Tailwind hosts — the layer list was using shadcn-style classes (flex,space-y-3,text-slate-200,hover:bg-slate-800,text-destructive) which silently dropped in hosts whose Tailwind config doesn’t scan the package. The “All On / All Off” buttons collapsed and checkbox rows lost their alignment so toggling individual layers felt broken even though the underlying OCG enumeration was correct. Rewrote with inline styles matching the rest of the sidebar (sticky-style header, padded toggle rows, scoped spinner keyframes, italic muted empty-state copy).
- Sticky-note redesign — sticky notes are now grouped paper
cards: an opaque pastel rect derived from the active stroke
colour (mixed 72 % toward white so the note never goes
see-through), stroked in the active colour, with a 14 px inner
padding, rounded corners, and a soft drop-shadow that lifts the
card off the page. The body Textbox sits inside the rect and
the whole thing moves / scales as one Fabric
Group. - Show / hide sticky notes — the annotation toolbar gained a
toggle button (
Hide notes↔Show notes) that flips thevisibleflag on every sticky-note group on the canvas. Notes are not deleted; toggling back restores them. NewshowStickyNotesprop on<AnnotationCanvas>and matchingstickyNotesVisible/onToggleStickyNotesprops on<AnnotationToolbar>expose the same control to custom compositions.
[0.3.0-beta.15] — 2026-05-04
Section titled “[0.3.0-beta.15] — 2026-05-04”- Pen tool drew nothing on fabric v6 — fabric v6 stopped
auto-instantiating
canvas.freeDrawingBrush, so toggling pen mode setisDrawingMode = trueagainst an undefined brush and theif (canvas.freeDrawingBrush)colour / width branch silently no-opped.AnnotationCanvasnow constructs aPencilBrushat init so the pen, free-hand strokes, and the existing colour-on-stroke-change effect all work. - Annotation toolbar scrolled away with the page — the toolbar
was a flow sibling of the canvas inside the stage scroll
container, so zooming / scrolling pushed it out of reach.
Wrapped it in a
position: sticky; top: 0; z-index: 30div so it stays pinned while the canvas scrolls underneath.
Changed
Section titled “Changed”- Descriptive tool tooltips — every annotation toolbar control
now carries a self-explanatory
title: per-tool descriptions (e.g. “Free-hand pen — draw freely with the active colour”, “Sticky note — click to drop an editable tinted note card”), per-swatch “Use #ef4444 as the active stroke / fill colour”, a custom-colour-wheel hint, undo / redo verbs, and a saving-state status line.
[0.3.0-beta.14] — 2026-05-04
Section titled “[0.3.0-beta.14] — 2026-05-04”AnnotationThreadrendered as a malformed pill in hosts without the package’s Tailwind classes — empty state collapsed into an oddly-shaped capsule because every layout class (flex,gap-2,p-4,text-slate-400,text-primary,text-destructive) silently dropped. Replaced with inline styling that matches the rest of the sidebar (border, dark fill, italic muted empty-state copy, scoped spinner keyframes).- Nested scroll in the demo sidebar — the annotations panel
wrapped
AnnotationThreadin its ownmaxHeight: 200; overflow-y: auto, while the sidebar itself already scrolled. Removed the inner scroll region so the thread expands inline and the sidebar handles all scrolling.
Changed
Section titled “Changed”- Tool labels — sidebar entries renamed from “Color picker (RGB
- TAC)” / “Densitometer (CMYK)” to “Color picker” / “Densitometer”. Both tools work on every detected ink (CMYK + spots) so the parenthetical limitation was misleading.
- Tool swatches — added inline colour swatches next to each tool name. Color picker shows a rainbow conic ring (samples any colour); densitometer shows a CMYK quadrant chip (process + spot density readout).
[0.3.0-beta.12] — 2026-05-04
Section titled “[0.3.0-beta.12] — 2026-05-04”Changed
Section titled “Changed”- Documentation refresh — README, component reference, and
CHANGELOG brought current with the 0.3.0 series.
<LensPDF>is now the headline integration tier; the demo wrapper is positioned as a marketing-page convenience.
[0.3.0-beta.11] — 2026-05-04
Section titled “[0.3.0-beta.11] — 2026-05-04”- Mobile responsive layout — new
useIsMobile()hook drives a shared breakpoint (max-width: 767px). On mobile the persistent tools sidebar collapses into a slide-in drawer anchored to the left edge (~85vw, max320 px,transform-animated) with a floating☰toggle and tap-outside backdrop. Color picker / densitometer readouts switch from floating tooltip to full-width bottom sheets so the readout is always legible regardless of where the user taps.
MeasureToolreadout legibility — replaced the Tailwind-onlybg-green-900/90chip with an opaque inline-styled card (dark slate background, green border, mint mono-font readout, drop shadow) so measurements stay readable over light artwork, photos, and ruler ticks. The drag-hint banner got the same treatment.- Tailwind dependency removed from
ColorPickerTool,DensitometerTool, andMeasureTooloverlays — they now render correctly in any host regardless of whether the host’s Tailwind config scans the package.
[0.3.0-beta.10] — 2026-05-04
Section titled “[0.3.0-beta.10] — 2026-05-04”Changed
Section titled “Changed”- Demo disclaimer copy — both sidebar disclaimers now lead with
“LensPDF supports full CMYK + spot inks with no approximation
when a backend (Ghostscript / MuPDF + ICC profiles) is wired
through the
servicesprop”. The RGB-derived path is presented as the fallback the demo runs in, not the only mode the package supports.
[0.3.0-beta.9] — 2026-05-04
Section titled “[0.3.0-beta.9] — 2026-05-04”Changed
Section titled “Changed”LensPDFDemosource split — every CSS-in-JS helper (shellStyle,topbarStyle,sidebarStyle,stageStyle, …) moved out into a siblingLensPDFDemo.styles.ts(270 lines). The main component file dropped from 1620 → 1373 lines so the React tree is visible without scrolling past inline style objects.- Top-of-file JSDoc rewritten to lead with “Most consumers
should not import this directly. Use
<LensPDF>instead — it’s a one-liner production drop-in.” Documents the file’s internal organisation (styles file + per-feature canvas / overlay / panel components).
[0.3.0-beta.8] — 2026-05-04
Section titled “[0.3.0-beta.8] — 2026-05-04”- TAC heatmap missed spot inks —
buildHeatmapUrlpreviously coloured every pixel fromrgbToCmykonly, while the densitometer and color picker added each detected spot ink’s coverage estimate to the same pixel’s TAC. PDFs declaring spot inks now get a heatmap that matches the readout: process CMYK + every detected spot ink, summed viaestimateInkCoveragewith the same cosine-similarity heuristic. Pure CMYK files behave identically to before.
[0.3.0-beta.7] — 2026-05-04
Section titled “[0.3.0-beta.7] — 2026-05-04”- Demo overlays misaligned with the page —
LensPDFDemowas computing its outer canvas-area div fromPTS_TO_PX = 96/72whilePageCanvasrendered usingDEFAULT_DPI/72 = 150/72. The parent div was ~36% smaller than the rendered page so every absolute- positioned overlay (TAC heatmap, separation canvas, layer canvas, annotation canvas, dieline / box overlays) landed on the top-left ~64% of the page and shifted relative to the actual content. SwitchingPTS_TO_PXtoDEFAULT_DPI / 72makes the parent agree with whatPageCanvasrenders so all overlays now register pixel-perfect.
[0.3.0-beta.6] — 2026-05-04
Section titled “[0.3.0-beta.6] — 2026-05-04”Changed
Section titled “Changed”- Demo footer copy — dropped the marketing “Everything runs in your browser via pdf.js” line from the sidebar footer and the empty-state upload prompt. CMYK / TAC approximation disclaimer and max-upload hint stay because they’re useful technical caveats.
[0.3.0-beta.5] — 2026-05-04
Section titled “[0.3.0-beta.5] — 2026-05-04”- Sticky note tool in the annotation toolbar — drops a fabric
Textboxstyled as a sticky-note card (180 px wide, tinted background derived from the active stroke colour, matching border, dark ink) at the click point and immediately enters edit mode with the placeholder pre-selected. Participates in undo / redo, auto-save, and the existing JSON serialisation.
[0.3.0-beta.4] — 2026-05-04
Section titled “[0.3.0-beta.4] — 2026-05-04”- Tools / overlays collapsed in hosts without Tailwind — every
overlay component (annotation, color picker, densitometer,
measure, TAC heatmap, separation, layer, box, dieline) was
relying on Tailwind utility classes (
absolute,inset-0,cursor-crosshair) for positioning. In hosts whose Tailwind config didn’t pick up the package’s compiled JS, those overlays collapsed to 0×0 and pointer events fell through to the page canvas — so annotation tools, color picker, etc. appeared non-functional. Replaced positioning classes with inlinestyleprops throughout. AnnotationCanvasupper-canvas sizing — explicitwidth/heightattributes on the underlying<canvas>element so fabric.js sizes its event-receiving upper-canvas correctly.- OCG layer enumeration hardened — handles both
MapandObjectliteral shapes returned by pdf.js’sgetOptionalContentConfig().getGroups(), falls back to walking the/OCProperties /D /Ordertree, queries names through bothgetGroup(id)andgetGroups()[id], and passes the proper{ type: "OCG", id }shape toisVisible(). Caches the OCG list against every page (OCGs are document-level) and emits a console warning whenlistLayersfails so reviewers can diagnose PDFs without optional content groups.
[0.3.0-beta.3] — 2026-05-04
Section titled “[0.3.0-beta.3] — 2026-05-04”<LensPDF>component — drop-in production viewer. Thin wrapper around<LensPDFDemo>withembedded=trueand a clean prop surface:pdfUrlis the single required prop, no upload chrome, plus full preflight integration (items,selectedItem,onItemSelect,dieline,showBoxOverlays,cropToTrim,onPageChange,onZoomChange,onError).- Spot-ink detection in
createBrowserViewerServices— regex-scans raw PDF bytes for/Separationand/DeviceNcolour spaces, decodes PDF name encoding, maps known spot families (Pantone, Reflex Blue, Warm Red, etc.) to sRGB and falls back to a hash-derived hue otherwise.estimateInkCoverage()projects each pixel onto the spot’s subtractive direction so densitometer, color picker, and the inks panel report values for every detected CMYK + spot. - Per-spot separation plates —
getChannelImageUrlnow builds grayscale rasters for every detected ink (process and spot) so the separations canvas can isolate any plate. AnnotationToolbarportability — every shadcn-style class replaced with inline styling so the toolbar renders identically in any host regardless of Tailwind / CSS framework.
Changed
Section titled “Changed”- Demo viewer mode UX — three mutually-exclusive primary canvases (Page / Separations / Layers) replace the previous overlay-stack. Inks default ON; untick a plate to preview without it (matches Acrobat’s Output Preview).
[0.3.0-beta.2] — 2026-05-04
Section titled “[0.3.0-beta.2] — 2026-05-04”- Client-side
createBrowserViewerServices— fullViewerServicesimplementation backed by pdf.js. Every viewer- only feature (page tiles at multiple DPIs, channel rasters, TAC heatmap, color sample, densitometer, layer rendering, in-memory annotations) works on any PDF the browser can fetch with no backend required. prepare(pageNum)lifecycle method — eagerly pre-builds every channel + heatmap + layer for a page so non-reactive canvases (separations, layers) don’t latch onto an empty URL before the analysis raster lands.- Multi-DPI tile cache —
PageCanvasrequests an effective DPI bucketed off the current zoom; the browser services build and cache rasters per(pageNum, dpi)so zoom doesn’t degrade the page tile. - OCG-aware
LayerCanvas— uses pdf.js’sOptionalContentConfigto render a single OCG with transparent background.
Changed
Section titled “Changed”- CMYK approximation —
rgbToCmykswitched from a basic closed-form to a “rich-black” formula (additive CMY + K based onmin(C,M,Y) * 0.8) so the densitometer / TAC heatmap actually trip on solid black instead of reporting K=0% for pure black.
[0.3.0-beta.1] — 2026-05-04
Section titled “[0.3.0-beta.1] — 2026-05-04”Changed
Section titled “Changed”- Publish target — moved package consumption away from GitHub
source and onto the public npm registry under the
@printwithsynergyscope. Marketing site now installs from npm.
[0.2.0-beta.3] — 2026-05-04
Section titled “[0.2.0-beta.3] — 2026-05-04”Changed
Section titled “Changed”- Publish target — moved from GitHub Packages to public npm registry.
[0.2.0-beta.2] — 2026-05-04
Section titled “[0.2.0-beta.2] — 2026-05-04”- ESM compatibility — added post-build script to rename
.jsxfiles to.jsfor Node.js ESM compatibility. Component files are now resolvable without explicit extensions in imports.
[0.2.0-beta.1] — 2026-05-04
Section titled “[0.2.0-beta.1] — 2026-05-04”- Build output — package was not built before publishing. Added
dist/to published artifacts. All component files now included indist/components/(.jsx+.d.ts).
[0.2.0] — 2026-05-04
Section titled “[0.2.0] — 2026-05-04”<LensPDFDemo>component — drop-in interactive demo with file upload, URL paste, drag-and-drop, client-side validation, sidebar controls, theming, and fullscreen mode. Zero boilerplate — config and data only. See docs/components.md.useLensPDF()hook — manages all viewer state (pages, zoom, layers, tools, fallback adapter, context values). Pair with<LensPDFProvider>for the “hook + provider” integration tier.<LensPDFProvider>component — thin wrapper that mounts bothViewerHostContextandViewerServicesContextfrom auseLensPDF()return value.- Slot props on
<LensPDFViewer>—header,sidebar, andfooterrender props let hosts replace default regions without losing the rest of the viewer chrome.LensPDFViewerStatetype exposes the viewer state to slot callbacks. defaultUnwiredServices— exported from/hostso consumers don’t need to recreate the 30-linemarkUnwiredstub object.pageInfoFromDimensions()— helper in/typesthat builds a completePageInfofrom just page number, width, and height.darkThemeTokens— dark palette preset exported from/pluginalongside the existingdefaultThemeTokens.validatePdfFile()/validatePdfUrl()— client-side PDF validation (magic bytes, MIME type, file size) exported from/host. See docs/validation.md.generateShareLink()/parseShareParams()— build and parse shareable viewer URLs with query params for PDF URL, fullscreen, zoom, page, mode, tools, and theme. See docs/share-links.md.typesVersionsinpackage.json— consumers usingmoduleResolution: "node"can now resolve sub-path type declarations without switching to"bundler".
Changed
Section titled “Changed”- Version bump to
0.2.0. - README rewritten with a 4-tier decision tree (Demo → Viewer → Hook+Provider → Full Custom) plus shareable-link and validation sections.
[0.1.0-beta.3] — 2026-05-04
Section titled “[0.1.0-beta.3] — 2026-05-04”<LensPDFViewer>mobile chrome: at viewports under 768 px the toolbar overflowed and the tools clipped off-screen, leaving no way to reach color picker / measure / layers. Replaced with a hamburger that opens a left-sliding drawer (matching the existingMobileDrawer’s design language:bg-slate-900,border-white/[0.06],DrawerSection/DrawerItemstyling). Layers gets its own separate slide-in drawer so toggling layers doesn’t dismiss the tools menu.- Toolbar look brought into agreement with the rest of the
package — Tailwind
bg-slate-900/text-slate-300/hover:bg-slate-800instead of the inline-styled neutral chrome that0.1.0-beta.2shipped. Active tool buttons honourtokens.accentfor brand colour. LensPDFViewerbrandprop added — optional label rendered in the top-left of the toolbar and as the mobile drawer header.- Layers control hides when the PDF has no OCGs (was rendering the toggle button anyway, then showing an empty layer list).
[0.1.0-beta.2] — 2026-05-04
Section titled “[0.1.0-beta.2] — 2026-05-04”The first published version. Public API may still change before
0.1.0 proper based on early-adopter feedback.
<LensPDFViewer>composition — one-line drop-in viewer:<LensPDFViewer pdfUrl="…" />auto-discovers page count, page dimensions, and OCG layers from the PDF; renders all pages in a scrollable list (or one at a time withmode="single"); ships a responsive default toolbar with zoom, layers, color picker, and measure tool; reflows to a bottom-drawer layout under 768 px. Keeps every existing lower-level component export public and unchanged for hosts with bespoke layouts.@printwithsynergy/lens-pdf/fallback-pdfjssubpath — new entry point with a staticimport "pdfjs-dist"so bundlers (Vite, webpack, esbuild) trace the dep correctly without consumers having to side-effect-import it. ExportscreatePdfJsFallbackanddefaultPdfWorkerSrc. Hosts that need the fallback should import from here.pdfjs-distis now a regulardependenciesentry (was an optional peer). Hosts that use the new subpath get it transitively; the bundle cost is paid only by code paths that actually touch the fallback.defaultPdfWorkerSrc— exported pdf.js worker URL, pinned to the bundledpdfjs-distversion via unpkg.<LensPDFViewer>uses it by default; hosts override via theworkerSrcprop.- Reference server — optional Node + Ghostscript backend under
server/. Exposes the HTTP contract thatservices.separations,services.densitometer,services.tacHeatmap,services.colorSample, andservices.pageImagesmap onto. Driven by Ghostscript’stiffsepdevice for real CMYK + spot-ink rendering. Dockerfile + Cloudflare-friendly cache headers (immutable, max-age=31536000,Cache-Tag: job-{id}) included. - Capability detection —
markUnwired/isUnwiredhelpers on every no-op default service, plus auseFallbackMode(service)hook returning"wired" | "fallback" | "hidden". Components self-hide when their backing service is unwired. - In-browser PDF fallback adapter — covers
PageCanvas,PageNavigator,MeasureTool,LayerPanel, andColorPickerTooldirectly from a PDF blob. Components that need real ink data (SeparationCanvas,DensitometerTool,TACHeatmapOverlay) stay hidden — pdf.js can’t reconstruct CMYK from rendered RGB. - Debug logging —
host.debugflag emits a one-shotconsole.infoper self-hidden component, deduped per component name. - Demo app —
demo/is a small Vite app that flips between empty, pdf.js-fallback, and fully-mocked host contexts for hands-on smoke testing. - Tests — first vitest suite covering
isUnwired/markUnwired. - Public-repo readiness —
CHANGELOG,SECURITY,CODE_OF_CONDUCT, rootCONTRIBUTING, GitHub issue + PR templates, README badges (CI / license / React). - Docs —
docs/architecture.md,docs/services.md,docs/fallback.md,docs/server.md,docs/components.md,docs/plugins.md,docs/measurement-units.md,docs/theming.md,docs/contributing.md.docs.jsonsidebar config + YAML frontmatter on every page drivinglenspdf.com. - GitHub Packages publish workflow — pushing a
v*tag triggers the workflow, which builds, tests, and publishes tonpm.pkg.github.com. Pre-release tags publish under thebetadist-tag.
Changed
Section titled “Changed”- The
createPdfJsFallbackre-export from@printwithsynergy/lens-pdf/hostis now@deprecated— it still works (dynamic import) for back-compat, but new code should use the/fallback-pdfjssubpath. - Breaking — type rename:
PreflightSourceMode→FindingsSourceMode,ViewerConfig.preflight_source→findings_source. - Breaking — neutral defaults:
MobileDrawerbrand fallback is now"PDF Viewer"(was"Preflight"); anonymous-mode report title is"PDF Report"(was"Preflight Report"). - JSDoc + docs scrub — every “LintPDF as canonical host” reference replaced with generic phrasing. The viewer is now host-agnostic in both runtime and prose.
- Tooling — standardised on
npm. CI runsnpm install / npm test / npm run build.
- Production “createPdfJsFallback requires pdfjs-dist to be
installed” error in apps that imported the fallback through
/host. The dynamic import wasn’t traced by bundlers, so consumers had to add a side-effectimport "pdfjs-dist"to make it resolvable. The new/fallback-pdfjssubpath fixes this for all bundlers without consumer changes. Surface that hit it:demo.lenspdf.com.
Removed
Section titled “Removed”- All product-specific terminology from the public surface —
grep -rni "preflight|lintpdf|lint-pdf|thinkneverland"returns nothing in source or docs.
[0.1.0] — internal extraction
Section titled “[0.1.0] — internal extraction”First internal version of LensPDF, extracted from an upstream SaaS
monorepo as the host-agnostic OSS viewer core. Never published.
Superseded by 0.1.0-beta.2.