Paginated table and list rows used the page-local map index for citation paths, so hover and click on later pages looked up the first page of field_metadata.
Co-authored-by: Cursor <cursoragent@cursor.com>
My previous commit guessed at this race and fixed the wrong window; CI
came back with the same six errors. Reproduced it locally instead: the
trigger is `--coverage`, whose v8 instrumentation slows execution enough
to widen the window. `pnpm vitest run --coverage --project storybook
stories/file-preview/pdf-preview.stories.tsx`, with the stories pointed
at the local fixture so documents actually load, reproduces all six.
Instrumenting react-pdf showed the real sequence, all of it inside the
`Upload And Preview` story, which swaps the url mid-play:
loadDocument RUN x2 <- document B starts while A is in flight
destroy EXECUTED <- A torn down
PageMount x6 <- all with a null messageHandler
react-pdf's loader schedules `loadingTask.destroy()` on cleanup but never
cancels that task's pending RESOLVE dispatch, so a superseded document
still lands in context and is then destroyed. Any <Page> mounting against
it throws. No gating on our own state can prevent that — the stale pdf
arrives from inside react-pdf after we have already moved on.
Keying <Document> by url gives each source a fresh instance, so the
stale resolve dispatches into an unmounted reducer and is a no-op.
Verified against the reproduction: 216 page mounts, all with a live
transport, zero errors (was 152 live / 6 dead).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJXCdQUoysxwoDBaDoDp2b
Self-hosting the worker made pdf.js use a real Worker in CI where the
unpkg script had been falling back to the in-process fake worker, which
turned a latent race into six unhandled errors on every run:
TypeError: Cannot read properties of null (reading 'sendWithPromise')
at WorkerTransport.getPage
On a `url` change the effect cleanup cleared `file`, so react-pdf
destroyed the document's worker transport — but `numPages` was left set,
and it alone gated the page list. Any <Page> mounting in that commit
called getPage() on the dead transport. The virtualized list mounts and
unmounts pages continuously, so the window was easy to hit once page
loads became genuinely async; `UploadAndPreview` swaps the url mid-story
and hit it six times under CI's timing.
Clear `numPages` alongside `file`, and include `file` in the render
guard so pages can never outlive their document.
The regression test mocks react-pdf with the same contract the real
library has — a Page mounting without a Document file throws — so the
invariant is pinned deterministically rather than depending on pdf.js
timing. It fails without this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJXCdQUoysxwoDBaDoDp2b
The PDF viewer set `pdfjs.GlobalWorkerOptions.workerSrc` to a script on
unpkg.com, and pointed `cMapUrl`/`wasmUrl` at the same CDN. The worker
executes with the host page's privileges, so a hijacked or compromised
CDN response had full access to the application origin — on every page
that previews a document. It also forced consuming apps to allow
https://unpkg.com in their CSP `script-src` and `worker-src`.
Resolve the worker from the `pdfjs-dist` package installed alongside the
library so bundlers emit it as a same-origin asset, and add
`configurePdfjs()` for apps that need to point at self-hosted copies
explicitly. `pdfjs-dist` is pinned to the exact build react-pdf uses,
since pdf.js rejects a worker whose version differs from the API's; a
test guards that pin against drift.
`cMapUrl`/`wasmUrl` are no longer set by default — they are directories
bundlers cannot resolve automatically, so they become opt-in via
`configurePdfjs()`. Documented in the package README.
The es2017 tsup target lowered `import.meta` to `{}`, which would have
silently reduced the worker to pdf.js's main-thread fallback, so
`import-meta` is now preserved for the ESM output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJXCdQUoysxwoDBaDoDp2b
- Fix P1: Separator aria-orientation is perpendicular to group orientation
in v4 (horizontal group → vertical separator). Flip CSS selectors from
aria-[orientation=vertical] to aria-[orientation=horizontal] for the
vertical-group case.
- Fix P1: Implement autoSaveId via useDefaultLayout hook. v4 removed
autoSaveId as a Group prop; it now requires useDefaultLayout({ id,
storage }) which returns defaultLayout + onLayoutChanged. Added a
separate component to handle the hook call.
- Fix P2: Add missing props (disabled) to ResizablePanelGroupProps and
ResizablePanelProps that were previously available via passthrough.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upgrade react-resizable-panels from ^3.0.3 to ^4.1.0 (resolves to 4.7.6)
to fix a known race condition in useGroupRef/usePanelRef hooks that caused
intermittent "No group found for id" errors during SSR hydration with
React 19's useId().
Define owned prop types for ResizablePanelGroup, ResizablePanel, and
ResizableHandle instead of re-exporting the dependency's types. This
ensures future dependency upgrades don't leak breaking changes to
consumers.
Key changes:
- Own prop types for all three components (no dependency type leakage)
- Accept both `direction` and `orientation` on ResizablePanelGroup
- Adapt v4 onResize callback (PanelSize → number) for backwards compat
- Update CSS selectors: data-[panel-group-direction] → aria-[orientation]
- Update story file to use orientation prop
- Bump version to 4.2.0 (non-breaking for consumers)
Ref: https://github.com/bvaughn/react-resizable-panels/issues/204
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ButtonGroup container with horizontal/vertical orientation
- ButtonGroupItem with shared variant/size context from parent
- Position-aware border-radius for connected button styling
- Supports individual variant/size overrides per item
- Full test coverage with 14 passing tests
Co-authored-by: Terry Zhao <zhaotai5541@gmail.com>