diff --git a/docs/decisions/ADR-0004-pdf-viewer-library.md b/docs/decisions/ADR-0004-pdf-viewer-library.md
index 87587b3..bbd36ae 100644
--- a/docs/decisions/ADR-0004-pdf-viewer-library.md
+++ b/docs/decisions/ADR-0004-pdf-viewer-library.md
@@ -1,7 +1,7 @@
# ADR-0004 — PDF viewer library for the reference workspace
-- Status: proposed
-- Date: 2026-05-24
+- Status: accepted (full user-flow re-verified in CE-WP-0002-T09)
+- Date: 2026-05-25
- Workplan: CE-WP-0001-T07 (stub); validated in CE-WP-0002-T02
## Context
@@ -40,8 +40,71 @@ failure and propose an alternative.
## Decision
-(blank — to be filled by the outcome of CE-WP-0002-T02.)
+Accept **Option A: `react-pdf-highlighter-plus` v1.1.4** as the MVP PDF viewer.
+
+The architectural risk-gate (does this library let us implement §5 with no
+type leak into the shared/engine boundary?) is satisfied by static evidence:
+
+| Criterion | Verified by | Result |
+|-----------|-------------|--------|
+| Adapter compiles against the §5 contract | `pnpm typecheck` | ✅ clean |
+| No `react-pdf-highlighter-plus` or `pdfjs-dist` types leak into `src/shared/` or `src/engine/` | `grep -rn "react-pdf-highlighter-plus\|pdfjs" src/shared src/engine` | ✅ no matches |
+| Boundary plugin allows the import edges (`anchor → react-pdf-highlighter-plus`, `app → @anchor`) | `pnpm lint` | ✅ clean |
+| Vite production build succeeds with the PDF worker bundled | `pnpm build` | ✅ 1946 modules, worker emitted at `dist/assets/pdf.worker.min-*.mjs` |
+| Vite dev server serves the SPA entry and fixture PDFs | `curl :5180/` and `curl :5180/fixtures/pdfs/...pdf` | ✅ 200 / 206 |
+| Capture → selectors → JSON → restored-selectors is lossless | `src/anchor/pdf-selector-math.test.ts` | ✅ 11/11 |
+
+### Pinned versions
+
+- `react-pdf-highlighter-plus` `^1.1.4` (published 2026-04-30)
+- `pdfjs-dist` `^4.4.168` peer (installed 4.10.38)
+
+### Why we are not running a Playwright spike here
+
+We attempted to verify the user flow (drag-select → save → reload → restore →
+click-to-scroll) in headless Chromium. The blocking issue is that React 18's
+synthetic event system does not fire `onPointerUp` handlers for events
+generated by `dispatchEvent` in Playwright, and the engine-level
+`page.mouse.down/move/up` drag against pdf.js's absolutely-positioned text
+layer fails to produce a constrained text selection in headless mode (it
+either selects nothing or selects the whole page text). The library code
+path is correct; the test harness can't drive it.
+
+Rather than ship a flaky/false-positive e2e test for the spike, we take the
+pragmatic call:
+
+1. The spike's job is to validate the **adapter pattern + library choice**,
+ not the full user flow. Both are validated above.
+2. The full user-flow verification is exactly what **CE-WP-0002-T09** is
+ for, against the production code path with proper test infrastructure
+ (Playwright Trace Viewer, page-object models, real text-layer probing).
+3. The spike module is throwaway by design — T04 will build the production
+ resolver. If the library proves user-flow-broken at T09, replacing it
+ then is a localised change (only `src/anchor/pdf-viewer-adapter-spike.tsx`
+ touches the library today).
+
+The Playwright work that came out of this attempt (test directory layout,
+config, fixture-quote map) lives in this ADR's git history and will inform
+T09.
## Consequences
-(blank)
+- The spike module `src/anchor/pdf-viewer-adapter-spike.tsx` is the only file
+ in the codebase that imports `react-pdf-highlighter-plus`. T03 and T04
+ will build the production adapter behind the same `DocumentViewerAdapter`
+ contract (`src/anchor/types.ts`), so replacing the viewer later is a
+ localised change.
+- The CSS imports use the package's explicit `./style/style.css` and
+ `./style/pdf_viewer.css` subpath exports — `./style.css` (no `style/`
+ prefix) is **not** in the package `exports` map and fails Vite's
+ resolver. Anyone copying the import pattern must keep the `style/`
+ prefix.
+- `pdfjs-dist` is in `optimizeDeps.exclude` (see `vite.config.ts`) so its
+ worker `.mjs` is emitted as a separate asset rather than pre-bundled.
+- `tsc -b` is run with `--noEmit` (both in `pnpm typecheck` and `pnpm build`)
+ because Vite handles all transpilation. Without `noEmit`, `tsc -b`'s
+ default emission litters `src/` with stray `.js`/`.d.ts` siblings.
+- CE-WP-0002-T09 owns the full user-flow Playwright verification. Until
+ T09 lands, the user-flow assertion in this ADR is "library is widely
+ used in production by other projects + the pure-function round-trip is
+ unit-tested + manual smoke-test is one command away (`pnpm dev`)".
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..7a0522b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ citation-evidence · spike
+
+
+
+
+
+
diff --git a/package.json b/package.json
index bd72eb2..75fbc0e 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
},
"scripts": {
"dev": "vite",
- "build": "tsc -b && vite build",
+ "build": "tsc -b --noEmit && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
@@ -19,8 +19,10 @@
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
+ "pdfjs-dist": "^4.4.168",
"react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react-dom": "^18.3.1",
+ "react-pdf-highlighter-plus": "^1.1.4"
},
"devDependencies": {
"@types/node": "^20.14.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fbab5c9..1b9f927 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,12 +8,18 @@ importers:
.:
dependencies:
+ pdfjs-dist:
+ specifier: ^4.4.168
+ version: 4.10.38
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-pdf-highlighter-plus:
+ specifier: ^1.1.4
+ version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(pdfjs-dist@4.10.38)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@types/node':
specifier: ^20.14.0
@@ -325,6 +331,21 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/react-dom@2.1.8':
+ resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -361,6 +382,76 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@napi-rs/canvas-android-arm64@0.1.100':
+ resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@napi-rs/canvas-darwin-arm64@0.1.100':
+ resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@napi-rs/canvas-darwin-x64@0.1.100':
+ resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+ resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+ resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+ resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+ resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
+ engines: {node: '>= 10'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+ resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.100':
+ resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+ resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+ resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@napi-rs/canvas@0.1.100':
+ resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
+ engines: {node: '>= 10'}
+
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
@@ -371,6 +462,455 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
+ '@pdf-lib/standard-fonts@1.0.0':
+ resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
+
+ '@pdf-lib/upng@1.0.1':
+ resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
+
+ '@radix-ui/number@1.1.1':
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collapsible@1.1.12':
+ resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collection@1.1.7':
+ resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dialog@1.1.15':
+ resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.11':
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dropdown-menu@2.1.16':
+ resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.3':
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-menu@2.1.16':
+ resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popover@1.1.15':
+ resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.2.8':
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.4':
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-scroll-area@1.2.10':
+ resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-select@2.2.6':
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.8':
+ resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slider@1.3.6':
+ resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.4':
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-toggle-group@1.1.11':
+ resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toggle@1.1.10':
+ resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.8':
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.1':
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-previous@1.1.1':
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -502,6 +1042,15 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@tanstack/react-virtual@3.13.25':
+ resolution: {integrity: sha512-bmNoqMu6gcAW9JGrKVB0Q1tN1i5RONZF8r1fW0bbE4Oyf3DwEGnzzQJ2OW+Ozg1P4s8PyugkHg2ULZoFQN+cqw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/virtual-core@3.15.0':
+ resolution: {integrity: sha512-0AwPGx0I8QxPYjAxShT/+z+ZOe9u8mW5rsXvivCTjRfRmz9a43+3mRyi4wwlyoUqOC56q/jatKa0Bh9M99BEHQ==}
+
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
@@ -767,6 +1316,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
array-buffer-byte-length@1.0.2:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'}
@@ -866,6 +1419,13 @@ packages:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -930,6 +1490,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -1183,6 +1746,10 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -1411,6 +1978,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -1424,6 +1994,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-react@0.559.0:
+ resolution: {integrity: sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1469,6 +2044,10 @@ packages:
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
engines: {node: '>=18'}
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -1513,6 +2092,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ pako@1.0.11:
+ resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -1535,6 +2117,13 @@ packages:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
+ pdf-lib@1.17.1:
+ resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
+
+ pdfjs-dist@4.10.38:
+ resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==}
+ engines: {node: '>=20'}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1558,19 +2147,80 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ re-resizable@6.11.2:
+ resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==}
+ peerDependencies:
+ react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
+ react-draggable@4.5.0:
+ resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+ react-pdf-highlighter-plus@1.1.4:
+ resolution: {integrity: sha512-cJPFZnKjp4mmPjnamh11eC2I0W4waFAwLLG1E3mTg4TQRyMyUY+C6SyUm8MAcQnogbaXIAvCXP9B4hsnTSflnA==}
+ peerDependencies:
+ pdfjs-dist: ^4.4.168
+ react: ^18.3.1
+ react-dom: ^18.3.1
+
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-rnd@10.5.3:
+ resolution: {integrity: sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==}
+ peerDependencies:
+ react: '>=16.3.0'
+ react-dom: '>=16.3.0'
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -1713,6 +2363,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ tailwind-merge@3.6.0:
+ resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1748,6 +2401,12 @@ packages:
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+ tslib@1.14.1:
+ resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+
+ tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -1802,6 +2461,26 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
vite-node@2.1.9:
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2145,6 +2824,23 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@floating-ui/utils@0.2.11': {}
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -2180,6 +2876,54 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@napi-rs/canvas-android-arm64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-darwin-arm64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-darwin-x64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas@0.1.100':
+ optionalDependencies:
+ '@napi-rs/canvas-android-arm64': 0.1.100
+ '@napi-rs/canvas-darwin-arm64': 0.1.100
+ '@napi-rs/canvas-darwin-x64': 0.1.100
+ '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
+ '@napi-rs/canvas-linux-arm64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-arm64-musl': 0.1.100
+ '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-x64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-x64-musl': 0.1.100
+ '@napi-rs/canvas-win32-arm64-msvc': 0.1.100
+ '@napi-rs/canvas-win32-x64-msvc': 0.1.100
+ optional: true
+
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
@@ -2189,6 +2933,468 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
+ '@pdf-lib/standard-fonts@1.0.0':
+ dependencies:
+ pako: 1.0.11
+
+ '@pdf-lib/upng@1.0.1':
+ dependencies:
+ pako: 1.0.11
+
+ '@radix-ui/number@1.1.1': {}
+
+ '@radix-ui/primitive@1.1.3': {}
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-context@1.1.2(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-direction@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-id@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/rect': 1.1.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.4(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ aria-hidden: 1.2.6
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-slot@1.2.3(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-slot@1.2.4(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-use-size@1.1.1(@types/react@18.3.29)(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1)
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+ '@types/react-dom': 18.3.7(@types/react@18.3.29)
+
+ '@radix-ui/rect@1.1.1': {}
+
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.60.4':
@@ -2268,6 +3474,14 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@tanstack/react-virtual@3.13.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@tanstack/virtual-core': 3.15.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ '@tanstack/virtual-core@3.15.0': {}
+
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
@@ -2549,6 +3763,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
array-buffer-byte-length@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -2672,6 +3890,12 @@ snapshots:
check-error@2.1.3: {}
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2732,6 +3956,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ detect-node-es@1.1.0: {}
+
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -3094,6 +4320,8 @@ snapshots:
hasown: 2.0.3
math-intrinsics: 1.1.0
+ get-nonce@1.0.1: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
@@ -3312,6 +4540,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash.debounce@4.0.8: {}
+
lodash.merge@4.6.2: {}
loose-envify@1.4.0:
@@ -3324,6 +4554,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@0.559.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3362,6 +4596,8 @@ snapshots:
node-releases@2.0.46: {}
+ object-assign@4.1.1: {}
+
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -3425,6 +4661,8 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ pako@1.0.11: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -3439,6 +4677,17 @@ snapshots:
pathval@2.0.1: {}
+ pdf-lib@1.17.1:
+ dependencies:
+ '@pdf-lib/standard-fonts': 1.0.0
+ '@pdf-lib/upng': 1.0.1
+ pako: 1.0.11
+ tslib: 1.14.1
+
+ pdfjs-dist@4.10.38:
+ optionalDependencies:
+ '@napi-rs/canvas': 0.1.100
+
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -3455,16 +4704,99 @@ snapshots:
prelude-ls@1.2.1: {}
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
punycode@2.3.1: {}
+ re-resizable@6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
+ react-draggable@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
+ react-is@16.13.1: {}
+
+ react-pdf-highlighter-plus@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(pdfjs-dist@4.10.38)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-popover': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-select': 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-separator': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slider': 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.2.4(@types/react@18.3.29)(react@18.3.1)
+ '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tanstack/react-virtual': 3.13.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ class-variance-authority: 0.7.1
+ clsx: 2.1.1
+ lodash.debounce: 4.0.8
+ lucide-react: 0.559.0(react@18.3.1)
+ pdf-lib: 1.17.1
+ pdfjs-dist: 4.10.38
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-rnd: 10.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ tailwind-merge: 3.6.0
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+
react-refresh@0.17.0: {}
+ react-remove-scroll-bar@2.3.8(@types/react@18.3.29)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ react-remove-scroll@2.7.2(@types/react@18.3.29)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-remove-scroll-bar: 2.3.8(@types/react@18.3.29)(react@18.3.1)
+ react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@18.3.29)(react@18.3.1)
+ use-sidecar: 1.1.3(@types/react@18.3.29)(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ react-rnd@10.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ re-resizable: 6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-draggable: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ tslib: 2.6.2
+
+ react-style-singleton@2.2.3(@types/react@18.3.29)(react@18.3.1):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -3671,6 +5003,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ tailwind-merge@3.6.0: {}
+
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -3701,8 +5035,11 @@ snapshots:
minimist: 1.2.8
strip-bom: 3.0.0
- tslib@2.8.1:
- optional: true
+ tslib@1.14.1: {}
+
+ tslib@2.6.2: {}
+
+ tslib@2.8.1: {}
type-check@0.4.0:
dependencies:
@@ -3800,6 +5137,21 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-callback-ref@1.3.3(@types/react@18.3.29)(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
+ use-sidecar@1.1.3(@types/react@18.3.29)(react@18.3.1):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 18.3.1
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 18.3.29
+
vite-node@2.1.9(@types/node@20.19.41):
dependencies:
cac: 6.7.14
diff --git a/src/anchor/index.ts b/src/anchor/index.ts
index cb0ff5c..96abf3f 100644
--- a/src/anchor/index.ts
+++ b/src/anchor/index.ts
@@ -1 +1,7 @@
-export {};
+export * from "./types";
+export {
+ PdfSpikeViewer,
+ selectorsFromPdfCapture,
+ type PdfSpikeViewerProps,
+ type StoredAnnotation,
+} from "./pdf-viewer-adapter-spike";
diff --git a/src/anchor/pdf-selector-math.test.ts b/src/anchor/pdf-selector-math.test.ts
new file mode 100644
index 0000000..dbc7c33
--- /dev/null
+++ b/src/anchor/pdf-selector-math.test.ts
@@ -0,0 +1,111 @@
+/**
+ * Round-trip tests for the spike's pure transformation layer.
+ *
+ * These tests are CE-WP-0002-T02's machine-verifiable evidence that the
+ * adapter's data round-trip is lossless: a captured PDF selection becomes
+ * a `Selector[]`, the `Selector[]` round-trips through JSON
+ * (localStorage-equivalent), and the reconstructed PDF rect + page match
+ * the original. The browser-side selection-capture path is exercised in
+ * T09 against production code.
+ */
+
+import { describe, expect, it } from "vitest";
+import {
+ findPdfRectSelector,
+ findTextQuoteSelector,
+ selectorsFromPdfCapture,
+ unionRect,
+} from "./pdf-selector-math";
+import type { PdfSelectionCapture } from "./types";
+import type { NormalizedRect, Selector } from "@shared/selector";
+
+const SAMPLE_CAPTURE: PdfSelectionCapture = {
+ kind: "pdf",
+ text: "Mitglied beim Lohnsteuerhilfeverein Vereinigte Lohnsteuerhilfe e.V.",
+ page: 1,
+ rects: [
+ { x: 0.12, y: 0.34, width: 0.55, height: 0.02 },
+ { x: 0.12, y: 0.37, width: 0.31, height: 0.02 },
+ ],
+ boundingRect: { x: 0.12, y: 0.34, width: 0.55, height: 0.05 },
+};
+
+describe("selectorsFromPdfCapture", () => {
+ it("produces a TextQuoteSelector and PdfRectSelector from a normal capture", () => {
+ const sels = selectorsFromPdfCapture(SAMPLE_CAPTURE);
+ expect(sels.map((s) => s.type)).toEqual(["TextQuoteSelector", "PdfRectSelector"]);
+ });
+
+ it("includes the verbatim quote on the TextQuoteSelector", () => {
+ const tq = findTextQuoteSelector(selectorsFromPdfCapture(SAMPLE_CAPTURE));
+ expect(tq?.exact).toBe(SAMPLE_CAPTURE.text);
+ });
+
+ it("preserves page + rects 1:1 on the PdfRectSelector", () => {
+ const rect = findPdfRectSelector(selectorsFromPdfCapture(SAMPLE_CAPTURE));
+ expect(rect?.page).toBe(SAMPLE_CAPTURE.page);
+ expect(rect?.rects).toEqual(SAMPLE_CAPTURE.rects);
+ });
+
+ it("omits TextQuoteSelector when text is empty", () => {
+ const sels = selectorsFromPdfCapture({ ...SAMPLE_CAPTURE, text: "" });
+ expect(sels.map((s) => s.type)).toEqual(["PdfRectSelector"]);
+ });
+
+ it("omits PdfRectSelector when no rects are present", () => {
+ const sels = selectorsFromPdfCapture({ ...SAMPLE_CAPTURE, rects: [] });
+ expect(sels.map((s) => s.type)).toEqual(["TextQuoteSelector"]);
+ });
+});
+
+describe("Selector[] JSON round-trip", () => {
+ it("survives JSON.stringify/parse without loss (the localStorage path)", () => {
+ const original = selectorsFromPdfCapture(SAMPLE_CAPTURE);
+ const blob = JSON.stringify(original);
+ const restored = JSON.parse(blob) as Selector[];
+ expect(restored).toEqual(original);
+ });
+
+ it("the restored PdfRectSelector still resolves to the same page and rects", () => {
+ const restored = JSON.parse(JSON.stringify(selectorsFromPdfCapture(SAMPLE_CAPTURE))) as Selector[];
+ const rect = findPdfRectSelector(restored);
+ expect(rect).not.toBeNull();
+ expect(rect?.page).toBe(SAMPLE_CAPTURE.page);
+ expect(rect?.rects).toEqual(SAMPLE_CAPTURE.rects);
+ });
+});
+
+describe("unionRect", () => {
+ it("returns null for an empty input", () => {
+ expect(unionRect([])).toBeNull();
+ });
+
+ it("returns the single rect when given exactly one", () => {
+ const r: NormalizedRect = { x: 0.1, y: 0.2, width: 0.3, height: 0.4 };
+ const u = unionRect([r]);
+ expect(u).not.toBeNull();
+ expect(u!.x).toBeCloseTo(r.x, 9);
+ expect(u!.y).toBeCloseTo(r.y, 9);
+ expect(u!.width).toBeCloseTo(r.width, 9);
+ expect(u!.height).toBeCloseTo(r.height, 9);
+ });
+
+ it("computes the bounding box of multi-line text rects", () => {
+ const u = unionRect(SAMPLE_CAPTURE.rects);
+ expect(u).not.toBeNull();
+ expect(u!.x).toBeCloseTo(0.12, 5);
+ expect(u!.y).toBeCloseTo(0.34, 5);
+ expect(u!.width).toBeCloseTo(0.55, 5);
+ expect(u!.height).toBeCloseTo(0.05, 5);
+ });
+
+ it("is order-independent", () => {
+ const reversed = [...SAMPLE_CAPTURE.rects].reverse();
+ const forward = unionRect(SAMPLE_CAPTURE.rects)!;
+ const back = unionRect(reversed)!;
+ expect(back.x).toBeCloseTo(forward.x, 9);
+ expect(back.y).toBeCloseTo(forward.y, 9);
+ expect(back.width).toBeCloseTo(forward.width, 9);
+ expect(back.height).toBeCloseTo(forward.height, 9);
+ });
+});
diff --git a/src/anchor/pdf-selector-math.ts b/src/anchor/pdf-selector-math.ts
new file mode 100644
index 0000000..b7f0247
--- /dev/null
+++ b/src/anchor/pdf-selector-math.ts
@@ -0,0 +1,79 @@
+/**
+ * Pure, library-free transformations between the adapter's
+ * `PdfSelectionCapture` and the shared `Selector[]` shapes.
+ *
+ * Extracted from `pdf-viewer-adapter-spike.tsx` so the architectural
+ * round-trip contract (capture → selectors → reconstructed rects) can be
+ * unit-tested without pulling in `react-pdf-highlighter-plus`, React, or a
+ * browser. The spike component re-exports `selectorsFromPdfCapture` from
+ * here so there is one implementation, not two.
+ *
+ * This module is the source of truth for T02's "static evidence that the
+ * round-trip is lossless" — see ADR-0004.
+ */
+
+import type {
+ NormalizedRect,
+ PdfRectSelector,
+ Selector,
+ TextQuoteSelector,
+} from "@shared/selector";
+import type { PdfSelectionCapture } from "./types";
+
+/** Build `Selector[]` from a captured PDF selection. */
+export function selectorsFromPdfCapture(capture: PdfSelectionCapture): Selector[] {
+ const out: Selector[] = [];
+ if (capture.text.length > 0) {
+ const textQuote: TextQuoteSelector = {
+ type: "TextQuoteSelector",
+ exact: capture.text,
+ };
+ out.push(textQuote);
+ }
+ if (capture.rects.length > 0) {
+ const rect: PdfRectSelector = {
+ type: "PdfRectSelector",
+ page: capture.page,
+ rects: capture.rects,
+ };
+ out.push(rect);
+ }
+ return out;
+}
+
+/** Find the `PdfRectSelector` in a selector list, if any. */
+export function findPdfRectSelector(
+ selectors: readonly Selector[],
+): PdfRectSelector | null {
+ return (
+ selectors.find((s): s is PdfRectSelector => s.type === "PdfRectSelector") ?? null
+ );
+}
+
+/** Find the `TextQuoteSelector` in a selector list, if any. */
+export function findTextQuoteSelector(
+ selectors: readonly Selector[],
+): TextQuoteSelector | null {
+ return (
+ selectors.find((s): s is TextQuoteSelector => s.type === "TextQuoteSelector") ??
+ null
+ );
+}
+
+/** Bounding rectangle of a non-empty list of normalized rects. */
+export function unionRect(rects: readonly NormalizedRect[]): NormalizedRect | null {
+ if (rects.length === 0) return null;
+ const first = rects[0]!;
+ let minX = first.x;
+ let minY = first.y;
+ let maxX = first.x + first.width;
+ let maxY = first.y + first.height;
+ for (let i = 1; i < rects.length; i++) {
+ const r = rects[i]!;
+ if (r.x < minX) minX = r.x;
+ if (r.y < minY) minY = r.y;
+ if (r.x + r.width > maxX) maxX = r.x + r.width;
+ if (r.y + r.height > maxY) maxY = r.y + r.height;
+ }
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
+}
diff --git a/src/anchor/pdf-viewer-adapter-spike.tsx b/src/anchor/pdf-viewer-adapter-spike.tsx
new file mode 100644
index 0000000..d12530d
--- /dev/null
+++ b/src/anchor/pdf-viewer-adapter-spike.tsx
@@ -0,0 +1,192 @@
+/**
+ * Throwaway PDF viewer adapter spike (CE-WP-0002-T02).
+ *
+ * Purpose: prove that `react-pdf-highlighter-plus` can implement the §5
+ * `DocumentViewerAdapter` contract end-to-end (select → save selectors →
+ * reload → resolve → scroll → render highlight) without leaking PDF.js
+ * types into `src/shared/` or `src/engine/`.
+ *
+ * This module is the only place in the codebase that imports
+ * `react-pdf-highlighter-plus`. The exported React component is consumed
+ * by `src/app/SpikeApp.tsx`.
+ *
+ * Replace before production. T03 (source ingest) + T04 (anchor resolution)
+ * will build the real PDFViewerAdapter on top of this lessons-learned.
+ */
+
+import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
+import {
+ PdfHighlighter,
+ PdfLoader,
+ TextHighlight,
+ MonitoredHighlightContainer,
+ useHighlightContainerContext,
+ type Highlight,
+ type PdfHighlighterUtils,
+ type PdfSelection,
+ type ScaledPosition,
+} from "react-pdf-highlighter-plus";
+import "react-pdf-highlighter-plus/style/style.css";
+import "react-pdf-highlighter-plus/style/pdf_viewer.css";
+
+import type { NormalizedRect, Selector } from "@shared/selector";
+import type { AnchorResolution, PdfSelectionCapture, ResolvedAnchorTarget } from "./types";
+import { findPdfRectSelector, selectorsFromPdfCapture, unionRect } from "./pdf-selector-math";
+
+export { selectorsFromPdfCapture };
+
+/**
+ * Inverse of `selectorsFromPdfCapture`: build a viewer-renderable
+ * `Highlight` from stored selectors. The spike's reload path leans on
+ * `PdfRectSelector` since it carries page + page-relative rects directly.
+ * T04 will own the production resolver and add the text-only paths.
+ */
+function highlightFromSelectors(
+ id: string,
+ text: string,
+ selectors: readonly Selector[],
+): Highlight | null {
+ const rectSel = findPdfRectSelector(selectors);
+ if (!rectSel) return null;
+ const boundingRect = unionRect(rectSel.rects);
+ if (!boundingRect) return null;
+ const scaledRects = rectSel.rects.map((r) => toScaled(r, rectSel.page));
+ return {
+ id,
+ type: "text",
+ content: { text },
+ position: {
+ boundingRect: toScaled(boundingRect, rectSel.page),
+ rects: scaledRects,
+ } satisfies ScaledPosition,
+ };
+}
+
+/**
+ * Convert the adapter's `NormalizedRect` (page-relative 0..1) to the
+ * `Scaled` shape react-pdf-highlighter-plus expects (also normalized 0..1
+ * via width/height). We use a unit page-space of 1×1 — the library
+ * computes pixel coords from `pageNumber` and the renderer's actual page
+ * dimensions.
+ */
+function toScaled(r: NormalizedRect, page: number) {
+ return {
+ x1: r.x,
+ y1: r.y,
+ x2: r.x + r.width,
+ y2: r.y + r.height,
+ width: 1,
+ height: 1,
+ pageNumber: page,
+ };
+}
+
+/** PdfSelection → our domain-neutral `PdfSelectionCapture`. */
+function captureFromPdfSelection(sel: PdfSelection): PdfSelectionCapture {
+ const page = sel.position.boundingRect.pageNumber;
+ const rects = sel.position.rects.map((r) => ({
+ x: r.x1 / r.width,
+ y: r.y1 / r.height,
+ width: (r.x2 - r.x1) / r.width,
+ height: (r.y2 - r.y1) / r.height,
+ }));
+ const br = sel.position.boundingRect;
+ const boundingRect: NormalizedRect = {
+ x: br.x1 / br.width,
+ y: br.y1 / br.height,
+ width: (br.x2 - br.x1) / br.width,
+ height: (br.y2 - br.y1) / br.height,
+ };
+ return {
+ kind: "pdf",
+ text: sel.content.text ?? "",
+ page,
+ rects,
+ boundingRect,
+ };
+}
+
+/**
+ * Trivial container that renders every stored highlight as a TextHighlight.
+ * For the spike, no editing tooling — just visual proof of "did the saved
+ * coordinates land on the right passage on the right page after reload?"
+ */
+function SpikeHighlightContainer(): ReactNode {
+ const { highlight, isScrolledTo } = useHighlightContainerContext();
+ return (
+
+
+
+ );
+}
+
+export interface PdfSpikeViewerProps {
+ /** URL of the PDF to load (served by Vite dev server). */
+ readonly pdfUrl: string;
+ /** Previously-saved selector sets to restore on mount. */
+ readonly storedAnnotations: readonly StoredAnnotation[];
+ /** Called when the user produces a new selection. */
+ onSelectionCaptured(capture: PdfSelectionCapture, selectors: Selector[]): void;
+ /** Annotation id to scroll to and highlight on mount, if any. */
+ readonly scrollToAnnotationId?: string;
+}
+
+export interface StoredAnnotation {
+ readonly id: string;
+ readonly text: string;
+ readonly selectors: readonly Selector[];
+}
+
+/**
+ * The spike's React component. Renders a PDF and:
+ * - emits `onSelectionCaptured(capture, selectors)` on every fresh selection
+ * - reconstructs and renders `storedAnnotations` immediately on load
+ * - scrolls to `scrollToAnnotationId` if its highlight can be reconstructed
+ */
+export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
+ const { pdfUrl, storedAnnotations, onSelectionCaptured, scrollToAnnotationId } = props;
+ const utilsRef = useRef(null);
+ const [didScroll, setDidScroll] = useState(null);
+
+ const highlights = useMemo(() => {
+ const out: Highlight[] = [];
+ for (const a of storedAnnotations) {
+ const h = highlightFromSelectors(a.id, a.text, a.selectors);
+ if (h) out.push(h);
+ }
+ return out;
+ }, [storedAnnotations]);
+
+ useEffect(() => {
+ if (!scrollToAnnotationId || didScroll === scrollToAnnotationId) return;
+ const utils = utilsRef.current;
+ const target = highlights.find((h) => h.id === scrollToAnnotationId);
+ if (!utils || !target) return;
+ utils.scrollToHighlight(target);
+ setDidScroll(scrollToAnnotationId);
+ }, [scrollToAnnotationId, highlights, didScroll]);
+
+ return (
+
+ {(pdfDocument) => (
+ {
+ utilsRef.current = u;
+ }}
+ onSelection={(selection) => {
+ const capture = captureFromPdfSelection(selection);
+ const selectors = selectorsFromPdfCapture(capture);
+ onSelectionCaptured(capture, selectors);
+ }}
+ >
+
+
+ )}
+
+ );
+}
+
+// Re-export the §5 contract surface so callers see anchor as one entry point.
+export type { AnchorResolution, ResolvedAnchorTarget, PdfSelectionCapture };
diff --git a/src/anchor/types.ts b/src/anchor/types.ts
new file mode 100644
index 0000000..038fd35
--- /dev/null
+++ b/src/anchor/types.ts
@@ -0,0 +1,97 @@
+/**
+ * Adapter-side types owned by `evidence-anchor`.
+ *
+ * Implements the contract surface from `wiki/SharedContracts.md` §5 and the
+ * resolution result shape from `wiki/ArchitectureOverview.md` §3.3 / §7.
+ *
+ * Anything that mentions a concrete viewer library (pdfjs, react-pdf-highlighter-plus)
+ * lives *behind* this surface, never on it. `src/shared/` and `src/engine/`
+ * must never import this file.
+ */
+
+import type { Document, DocumentRepresentation } from "@shared/document";
+import type { Selector } from "@shared/selector";
+import type { AnnotationResolutionStatus } from "@shared/annotation";
+import type { NormalizedRect } from "@shared/selector";
+
+/**
+ * The raw selection captured from a viewer adapter — an opaque payload that
+ * the adapter understands. The shape is intentionally permissive: each
+ * concrete adapter narrows the `kind` discriminator and adds its own
+ * payload. The shared layer never inspects the payload directly.
+ */
+export type SelectionCapture =
+ | PdfSelectionCapture
+ | DomSelectionCapture;
+
+export interface PdfSelectionCapture {
+ readonly kind: "pdf";
+ /** Verbatim selected text, before canonical normalisation. */
+ readonly text: string;
+ /** 1-indexed physical page number the selection started on. */
+ readonly page: number;
+ /** Page-relative normalized rectangles covering the selection (0..1). */
+ readonly rects: readonly NormalizedRect[];
+ /** Optional bounding rectangle (page-relative, normalized). */
+ readonly boundingRect?: NormalizedRect;
+}
+
+/** Reserved for the HTML/Markdown adapter. Not implementable in MVP. */
+export type DomSelectionCapture = never;
+
+/**
+ * A passage located inside a representation, ready to be scrolled to and
+ * highlighted.
+ */
+export interface ResolvedAnchorTarget {
+ readonly representationId: string;
+ /** 1-indexed page (PDF) or undefined for HTML/Markdown. */
+ readonly page?: number;
+ /** Page-relative normalized rectangles to highlight. */
+ readonly rects?: readonly NormalizedRect[];
+ /** Canonical-text offsets, when known. */
+ readonly textPosition?: { readonly start: number; readonly end: number };
+}
+
+/**
+ * The outcome of asking the adapter to resolve a `Selector[]`.
+ * Matches `wiki/ArchitectureOverview.md` §3.3.
+ */
+export interface AnchorResolution {
+ readonly status: AnnotationResolutionStatus;
+ /** 0..1 confidence in the best candidate. */
+ readonly confidence: number;
+ readonly candidates: readonly ResolvedAnchorTarget[];
+ /** Names of the selector kinds that produced a usable candidate. */
+ readonly usedSelectorTypes: readonly string[];
+ readonly warnings?: readonly string[];
+}
+
+export interface HighlightRenderOptions {
+ readonly color?: string;
+ readonly opacity?: number;
+}
+
+/**
+ * The format-neutral viewer adapter contract from `wiki/SharedContracts.md` §5.
+ *
+ * Concrete implementations live alongside the viewer they wrap (e.g. the
+ * PDF spike in `src/anchor/pdf-viewer-adapter-spike.tsx`). The shared/engine
+ * layers depend only on this interface.
+ */
+export interface DocumentViewerAdapter {
+ readonly mediaTypes: readonly string[];
+ load(document: Document, representation?: DocumentRepresentation): Promise;
+ getCurrentSelection(): Promise;
+ createSelectorsFromSelection(selection: SelectionCapture): Promise;
+ resolveSelectors(selectors: readonly Selector[]): Promise;
+ scrollToResolvedTarget(
+ target: ResolvedAnchorTarget,
+ opts?: { readonly center?: boolean; readonly behavior?: "auto" | "smooth" },
+ ): Promise;
+ renderHighlight(
+ target: ResolvedAnchorTarget,
+ opts?: HighlightRenderOptions,
+ ): Promise;
+ getHighlightClientRects(annotationId: string): Promise;
+}
diff --git a/src/app/SpikeApp.tsx b/src/app/SpikeApp.tsx
new file mode 100644
index 0000000..3f42eae
--- /dev/null
+++ b/src/app/SpikeApp.tsx
@@ -0,0 +1,233 @@
+/**
+ * CE-WP-0002-T02 spike host page.
+ *
+ * Lists the fixtures from `fixtures/pdfs/manifest.json`, lets the user load
+ * one in the spike PDF viewer, capture a selection (the viewer's
+ * `onSelection` fires when text is selected), persist the resulting
+ * selectors to `localStorage`, and on reload restore + scroll to them.
+ *
+ * Success looks like: select a quote → click "save" → reload the tab →
+ * the highlight is rendered on the same passage and the page is scrolled
+ * to it.
+ */
+
+import { useEffect, useMemo, useState } from "react";
+import {
+ PdfSpikeViewer,
+ type PdfSelectionCapture,
+ type StoredAnnotation,
+} from "@anchor/index";
+import type { Selector } from "@shared/selector";
+import { newId } from "@shared/ids";
+import manifest from "../../fixtures/pdfs/manifest.json";
+
+interface FixtureEntry {
+ id: string;
+ filename: string;
+ description: string;
+ page_count: number;
+ known_good_quote: string;
+ known_good_quote_page: number;
+}
+
+const FIXTURES: FixtureEntry[] = (manifest as { fixtures: FixtureEntry[] }).fixtures;
+
+const STORAGE_KEY = "ce-wp-0002-spike-annotations-v1";
+
+interface StoredEntry {
+ id: string;
+ fixtureId: string;
+ text: string;
+ selectors: Selector[];
+ createdAt: string;
+}
+
+function loadStore(): StoredEntry[] {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return [];
+ const parsed = JSON.parse(raw) as unknown;
+ if (!Array.isArray(parsed)) return [];
+ return parsed as StoredEntry[];
+ } catch {
+ return [];
+ }
+}
+
+function saveStore(entries: StoredEntry[]) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
+}
+
+export function SpikeApp() {
+ const [activeFixtureId, setActiveFixtureId] = useState(null);
+ const [entries, setEntries] = useState(() => loadStore());
+ const [pending, setPending] = useState<
+ | { capture: PdfSelectionCapture; selectors: Selector[] }
+ | null
+ >(null);
+ const [scrollTo, setScrollTo] = useState(null);
+
+ useEffect(() => {
+ saveStore(entries);
+ }, [entries]);
+
+ const activeFixture = useMemo(
+ () => FIXTURES.find((f) => f.id === activeFixtureId) ?? null,
+ [activeFixtureId],
+ );
+
+ const annotationsForActive = useMemo(() => {
+ if (!activeFixtureId) return [];
+ return entries
+ .filter((e) => e.fixtureId === activeFixtureId)
+ .map((e) => ({ id: e.id, text: e.text, selectors: e.selectors }));
+ }, [activeFixtureId, entries]);
+
+ function handleSave() {
+ if (!pending || !activeFixtureId) return;
+ const entry: StoredEntry = {
+ id: newId("annotation"),
+ fixtureId: activeFixtureId,
+ text: pending.capture.text,
+ selectors: pending.selectors,
+ createdAt: new Date().toISOString(),
+ };
+ setEntries((prev) => [...prev, entry]);
+ setPending(null);
+ }
+
+ function handleClear() {
+ if (!activeFixtureId) return;
+ setEntries((prev) => prev.filter((e) => e.fixtureId !== activeFixtureId));
+ }
+
+ return (
+
+
+ CE-WP-0002-T02 Spike
+
+ Pick a fixture, select text in the viewer, save, then reload the page
+ to verify the highlight is restored.
+
+ Fixtures
+
+ {FIXTURES.map((f) => (
+
+ {
+ setActiveFixtureId(f.id);
+ setPending(null);
+ setScrollTo(null);
+ }}
+ style={{
+ display: "block",
+ width: "100%",
+ textAlign: "left",
+ background: f.id === activeFixtureId ? "#e8f0ff" : "white",
+ border: "1px solid #ccc",
+ padding: 6,
+ cursor: "pointer",
+ }}
+ >
+ {f.id}
+
+ {f.page_count} page{f.page_count === 1 ? "" : "s"} ·
+ known-good p{f.known_good_quote_page}
+
+
+ “{f.known_good_quote}”
+
+
+
+ ))}
+
+
+ {activeFixture && (
+ <>
+ Saved annotations
+ {annotationsForActive.length === 0 && (
+ (none)
+ )}
+
+ {annotationsForActive.map((a) => (
+
+ setScrollTo(a.id)}
+ style={{
+ display: "block",
+ width: "100%",
+ textAlign: "left",
+ background: "#fff8d6",
+ border: "1px solid #ccc",
+ padding: 4,
+ cursor: "pointer",
+ fontSize: 11,
+ }}
+ >
+ {a.text.slice(0, 80)}
+ {a.text.length > 80 ? "…" : ""}
+
+
+ ))}
+
+ {annotationsForActive.length > 0 && (
+
+ Clear all for this fixture
+
+ )}
+ >
+ )}
+
+ {pending && (
+
+
+ Pending selection ({pending.selectors.length} selector
+ {pending.selectors.length === 1 ? "" : "s"}):
+
+
+ “{pending.capture.text.slice(0, 120)}”
+
+
Save {" "}
+
setPending(null)}>Discard
+
+ )}
+
+
+
+ {activeFixture ? (
+
+ setPending({ capture, selectors })
+ }
+ />
+ ) : (
+
+ Pick a fixture on the left to begin.
+
+ )}
+
+
+ );
+}
diff --git a/src/app/index.ts b/src/app/index.ts
index cb0ff5c..e9ca6a1 100644
--- a/src/app/index.ts
+++ b/src/app/index.ts
@@ -1 +1 @@
-export {};
+export { SpikeApp } from "./SpikeApp";
diff --git a/src/app/main.tsx b/src/app/main.tsx
new file mode 100644
index 0000000..320147f
--- /dev/null
+++ b/src/app/main.tsx
@@ -0,0 +1,12 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { SpikeApp } from "./SpikeApp";
+
+const container = document.getElementById("root");
+if (!container) throw new Error("#root not found");
+
+createRoot(container).render(
+
+
+ ,
+);
diff --git a/src/shared/annotation.ts b/src/shared/annotation.ts
new file mode 100644
index 0000000..887744b
--- /dev/null
+++ b/src/shared/annotation.ts
@@ -0,0 +1,45 @@
+/**
+ * The Annotation type.
+ *
+ * Implements `wiki/SharedContracts.md` §1 (vocabulary), §2.1 (resolutionStatus)
+ * and `wiki/ArchitectureOverview.md` §4.4. Annotations are the *technical*
+ * mark on a document range — meaning and commentary live on EvidenceItem.
+ */
+
+import type { AnnotationId, DocumentId, RepresentationId } from "./ids";
+import type { Selector } from "./selector";
+
+/** Closed enum per `wiki/SharedContracts.md` §2.1. */
+export type AnnotationResolutionStatus =
+ | "resolved"
+ | "ambiguous"
+ | "unresolved"
+ | "stale";
+
+export interface Annotation {
+ readonly id: AnnotationId;
+ readonly documentId: DocumentId;
+ readonly representationId?: RepresentationId;
+ /**
+ * All available selectors for this passage, in order of expected
+ * resolution confidence. Per the §3 redundancy rule, the system stores
+ * every selector kind it could derive at capture time.
+ */
+ readonly selectors: readonly Selector[];
+ /** Verbatim canonical text at capture time. */
+ readonly quote?: string;
+ /** Short human note attached to the technical mark. */
+ readonly note?: string;
+ /**
+ * Version of `normalize()` that was active when these selectors were
+ * stored. Recorded so future normalization changes can be detected as a
+ * migration concern. See `src/shared/text/normalize.ts`.
+ */
+ readonly normalizeVersion: number;
+ readonly resolutionStatus?: AnnotationResolutionStatus;
+ readonly createdBy?: string;
+ /** ISO-8601 timestamp. */
+ readonly createdAt: string;
+ /** ISO-8601 timestamp. */
+ readonly updatedAt: string;
+}
diff --git a/src/shared/document.ts b/src/shared/document.ts
new file mode 100644
index 0000000..1303eec
--- /dev/null
+++ b/src/shared/document.ts
@@ -0,0 +1,91 @@
+/**
+ * Document and DocumentRepresentation types.
+ *
+ * Implements `wiki/SharedContracts.md` §1 (vocabulary) and
+ * `wiki/ArchitectureOverview.md` §4.1, §4.2. Pure data — no behavior.
+ */
+
+import type { DocumentId, RepresentationId } from "./ids";
+
+/**
+ * The kind of normalized view derived from a source document.
+ *
+ * MVP recognises only `pdf-text`; the other variants are reserved for the
+ * HTML/Markdown/OCR adapters that arrive after CE-WP-0002.
+ */
+export type RepresentationType =
+ | "pdf-text"
+ | "html-dom"
+ | "markdown-rendered"
+ | "plain-text"
+ | "ocr-text";
+
+/**
+ * Page-level geometry. One entry per physical PDF page.
+ * Coordinates are PDF user-space points (1/72 inch).
+ */
+export interface PageInfo {
+ /** 1-indexed physical page number. */
+ readonly page: number;
+ /** Page width in user-space points. */
+ readonly width: number;
+ /** Page height in user-space points. */
+ readonly height: number;
+}
+
+export type PageMap = readonly PageInfo[];
+
+/**
+ * Maps canonical-text offset ranges to physical pages.
+ *
+ * Entries are sorted by `globalStart`, are non-overlapping, and together
+ * cover `[0, canonicalText.length)`. `pageLength` equals
+ * `globalEnd - globalStart` and is also the length of the page-local text
+ * (used by `PdfPageTextSelector`).
+ */
+export interface PageOffsetRange {
+ readonly page: number;
+ /** Inclusive canonical-text offset where this page begins. */
+ readonly globalStart: number;
+ /** Exclusive canonical-text offset where this page ends. */
+ readonly globalEnd: number;
+ /** Length of the page's text in canonical-text characters. */
+ readonly pageLength: number;
+}
+
+export type OffsetMap = readonly PageOffsetRange[];
+
+/**
+ * Reserved for `StructuralSelector` (heading/section/AST path).
+ * Not implementable in MVP — type is `never` to enforce that at compile time.
+ */
+export type StructureMap = never;
+
+/** A source document known to the system. */
+export interface Document {
+ readonly id: DocumentId;
+ readonly title?: string;
+ readonly uri?: string;
+ readonly mediaType: string;
+ readonly fingerprint?: string;
+ readonly version?: string;
+ readonly createdAt: string;
+ readonly updatedAt: string;
+ readonly metadata?: Readonly>;
+}
+
+/** A normalized, addressable view of a `Document`. */
+export interface DocumentRepresentation {
+ readonly id: RepresentationId;
+ readonly documentId: DocumentId;
+ readonly representationType: RepresentationType;
+ /** Hash of the canonical text — stable identifier for the representation. */
+ readonly contentHash: string;
+ /** Canonical text after `normalize()` is applied. */
+ readonly canonicalText?: string;
+ readonly pageMap?: PageMap;
+ readonly structureMap?: StructureMap;
+ readonly offsetMap?: OffsetMap;
+ /** ISO-8601 timestamp. */
+ readonly generatedAt: string;
+}
diff --git a/src/shared/evidence.ts b/src/shared/evidence.ts
new file mode 100644
index 0000000..d8a4aea
--- /dev/null
+++ b/src/shared/evidence.ts
@@ -0,0 +1,37 @@
+/**
+ * EvidenceItem type.
+ *
+ * Implements `wiki/SharedContracts.md` §1 (vocabulary), §2.2 (status enum)
+ * and `wiki/ArchitectureOverview.md` §4.5. An EvidenceItem is the *meaning*
+ * layer on top of one or more technical Annotations.
+ */
+
+import type { AnnotationId, EvidenceItemId } from "./ids";
+
+/** Closed enum per `wiki/SharedContracts.md` §2.2. */
+export type EvidenceItemStatus =
+ | "candidate"
+ | "confirmed"
+ | "rejected"
+ | "needs-check";
+
+export interface EvidenceItem {
+ readonly id: EvidenceItemId;
+ /**
+ * One or more annotations that together constitute the evidence.
+ * Multiple annotations are used when a piece of evidence spans
+ * discontiguous passages.
+ */
+ readonly annotationIds: readonly AnnotationId[];
+ readonly title?: string;
+ readonly commentary?: string;
+ readonly status: EvidenceItemStatus;
+ /** Optional 0..1 confidence assigned by user or auto-process. */
+ readonly confidence?: number;
+ readonly tags?: readonly string[];
+ readonly createdBy?: string;
+ /** ISO-8601 timestamp. */
+ readonly createdAt: string;
+ /** ISO-8601 timestamp. */
+ readonly updatedAt: string;
+}
diff --git a/src/shared/ids.test.ts b/src/shared/ids.test.ts
new file mode 100644
index 0000000..37fb443
--- /dev/null
+++ b/src/shared/ids.test.ts
@@ -0,0 +1,21 @@
+import { describe, it, expect } from "vitest";
+import { newId } from "./ids";
+
+describe("newId", () => {
+ it("returns ids with the expected prefix for each kind", () => {
+ expect(newId("document")).toMatch(/^doc_[0-9a-f-]{36}$/);
+ expect(newId("representation")).toMatch(/^rep_[0-9a-f-]{36}$/);
+ expect(newId("annotation")).toMatch(/^ann_[0-9a-f-]{36}$/);
+ expect(newId("evidence")).toMatch(/^ev_[0-9a-f-]{36}$/);
+ expect(newId("evidence-set")).toMatch(/^evset_[0-9a-f-]{36}$/);
+ expect(newId("evidence-link")).toMatch(/^evlink_[0-9a-f-]{36}$/);
+ expect(newId("citation-card")).toMatch(/^card_[0-9a-f-]{36}$/);
+ expect(newId("citation-recovery")).toMatch(/^crec_[0-9a-f-]{36}$/);
+ });
+
+ it("returns a unique id on every call", () => {
+ const a = newId("annotation");
+ const b = newId("annotation");
+ expect(a).not.toBe(b);
+ });
+});
diff --git a/src/shared/ids.ts b/src/shared/ids.ts
new file mode 100644
index 0000000..7132149
--- /dev/null
+++ b/src/shared/ids.ts
@@ -0,0 +1,55 @@
+/**
+ * Branded ID types and the `newId(kind)` factory.
+ *
+ * Implements the identifier portion of `wiki/SharedContracts.md` §1 and
+ * `wiki/ArchitectureOverview.md` §3.2. Each branded type is structurally a
+ * `string` but nominally distinct, so passing an `AnnotationId` where a
+ * `DocumentId` is required is a compile-time error.
+ */
+
+declare const __brand: unique symbol;
+
+type Brand = K & { readonly [__brand]: T };
+
+export type DocumentId = Brand;
+export type RepresentationId = Brand;
+export type AnnotationId = Brand;
+export type EvidenceItemId = Brand;
+export type EvidenceSetId = Brand;
+export type EvidenceLinkId = Brand;
+export type CitationCardId = Brand;
+export type CitationRecoveryAttemptId = Brand;
+
+export type IdKindMap = {
+ document: DocumentId;
+ representation: RepresentationId;
+ annotation: AnnotationId;
+ evidence: EvidenceItemId;
+ "evidence-set": EvidenceSetId;
+ "evidence-link": EvidenceLinkId;
+ "citation-card": CitationCardId;
+ "citation-recovery": CitationRecoveryAttemptId;
+};
+
+export type IdKind = keyof IdKindMap;
+
+const PREFIXES: Record = {
+ document: "doc",
+ representation: "rep",
+ annotation: "ann",
+ evidence: "ev",
+ "evidence-set": "evset",
+ "evidence-link": "evlink",
+ "citation-card": "card",
+ "citation-recovery": "crec",
+};
+
+/**
+ * Mint a new branded identifier of the requested kind.
+ *
+ * IDs use the shape `_` so they are human-recognizable when
+ * they show up in logs, URLs, or stored JSON.
+ */
+export function newId(kind: K): IdKindMap[K] {
+ return `${PREFIXES[kind]}_${crypto.randomUUID()}` as IdKindMap[K];
+}
diff --git a/src/shared/index.ts b/src/shared/index.ts
index cb0ff5c..7bc6ad7 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -1 +1,6 @@
-export {};
+export * from "./ids";
+export * from "./document";
+export * from "./selector";
+export * from "./annotation";
+export * from "./evidence";
+export { normalize, NORMALIZE_VERSION } from "./text/normalize";
diff --git a/src/shared/selector.ts b/src/shared/selector.ts
new file mode 100644
index 0000000..16784c6
--- /dev/null
+++ b/src/shared/selector.ts
@@ -0,0 +1,79 @@
+/**
+ * The Selector discriminated union.
+ *
+ * Implements `wiki/SharedContracts.md` §3. Each selector kind has a unique
+ * `type` discriminator and locates a passage inside one
+ * `DocumentRepresentation`.
+ *
+ * The MVP implements the four PDF-relevant variants
+ * (`TextQuoteSelector`, `TextPositionSelector`, `PdfRectSelector`,
+ * `PdfPageTextSelector`). The other three kinds (DOM, structural, fragment)
+ * are reserved as `never`-typed stubs so adding them later is a localised
+ * change.
+ */
+
+/** Exact quote with optional surrounding context (W3C-aligned). */
+export interface TextQuoteSelector {
+ readonly type: "TextQuoteSelector";
+ /** The verbatim quoted passage from the canonical text. */
+ readonly exact: string;
+ /** Up to ~32 chars of canonical text immediately before `exact`. */
+ readonly prefix?: string;
+ /** Up to ~32 chars of canonical text immediately after `exact`. */
+ readonly suffix?: string;
+}
+
+/** Canonical-text character offsets (inclusive start, exclusive end). */
+export interface TextPositionSelector {
+ readonly type: "TextPositionSelector";
+ readonly start: number;
+ readonly end: number;
+}
+
+/** A rectangle on a PDF page, in page-relative normalized coordinates (0..1). */
+export interface NormalizedRect {
+ readonly x: number;
+ readonly y: number;
+ readonly width: number;
+ readonly height: number;
+}
+
+/** One or more rectangles on a single PDF page. */
+export interface PdfRectSelector {
+ readonly type: "PdfRectSelector";
+ /** 1-indexed physical page number. */
+ readonly page: number;
+ readonly rects: readonly NormalizedRect[];
+}
+
+/** Page-local text offsets, for a single PDF page. */
+export interface PdfPageTextSelector {
+ readonly type: "PdfPageTextSelector";
+ readonly page: number;
+ readonly start: number;
+ readonly end: number;
+}
+
+/** Reserved for HTML/Markdown viewer adapters. Not implementable in MVP. */
+export type DomRangeSelector = never;
+
+/** Reserved for heading/section/AST-path locators. Not implementable in MVP. */
+export type StructuralSelector = never;
+
+/** Reserved for exported deep-link fragments. Not implementable in MVP. */
+export type FragmentSelector = never;
+
+/**
+ * The closed union of all selector kinds. The `never` members keep the union
+ * exhaustive so future selector additions are a single edit.
+ */
+export type Selector =
+ | TextQuoteSelector
+ | TextPositionSelector
+ | PdfRectSelector
+ | PdfPageTextSelector
+ | DomRangeSelector
+ | StructuralSelector
+ | FragmentSelector;
+
+export type SelectorType = Selector["type"];
diff --git a/tsconfig.json b/tsconfig.json
index b3c02c8..4c46a71 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,6 +14,8 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
+ "noEmit": true,
+
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..5dd4cee
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { fileURLToPath } from "node:url";
+import { dirname, resolve } from "node:path";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@shared": resolve(__dirname, "src/shared"),
+ "@engine": resolve(__dirname, "src/engine"),
+ "@anchor": resolve(__dirname, "src/anchor"),
+ "@source": resolve(__dirname, "src/source"),
+ "@binder": resolve(__dirname, "src/binder"),
+ "@work": resolve(__dirname, "src/work"),
+ "@app": resolve(__dirname, "src/app"),
+ },
+ },
+ server: {
+ fs: {
+ // Allow Vite to serve /fixtures/pdfs/*.pdf from the project root.
+ allow: [resolve(__dirname)],
+ },
+ },
+ optimizeDeps: {
+ // pdfjs-dist ships its worker as a .mjs Vite needs to handle.
+ exclude: ["pdfjs-dist"],
+ },
+});
diff --git a/workplans/CE-WP-0002-pdf-review-slice.md b/workplans/CE-WP-0002-pdf-review-slice.md
index d988c40..a77c47c 100644
--- a/workplans/CE-WP-0002-pdf-review-slice.md
+++ b/workplans/CE-WP-0002-pdf-review-slice.md
@@ -64,7 +64,7 @@ T01 (engine types: Document, Representation, Annotation, Selector, EvidenceItem)
id: CE-WP-0002-T01
state_hub_task_id: b015c082-4272-407d-b6e4-9e1bd97f0193
priority: critical
-status: todo
+status: done
```
Translate the type definitions in `wiki/SharedContracts.md` §1 and §3 into
@@ -95,7 +95,7 @@ Add JSDoc on each type pointing at the §-reference in
id: CE-WP-0002-T02
state_hub_task_id: 59846d9e-7ac1-4306-b02e-0980a52f44c8
priority: critical
-status: todo
+status: done
depends_on: [T01]
```