@elabs-ai/components-viewer 4.2.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/{code-adapter-ADZ4UOGN.js → adapters/code/code-adapter.js} +15 -64
  2. package/dist/adapters/code/code-adapter.js.map +1 -0
  3. package/dist/adapters/code/code-language.js +10 -0
  4. package/dist/adapters/code/code-language.js.map +1 -0
  5. package/dist/adapters/code/code-manifest.js +10 -0
  6. package/dist/adapters/code/code-manifest.js.map +1 -0
  7. package/dist/adapters/code/code-theme.js +8 -0
  8. package/dist/adapters/code/code-theme.js.map +1 -0
  9. package/dist/{csv-adapter-7HOFZ5Z6.js → adapters/csv/csv-adapter.js} +13 -9
  10. package/dist/adapters/csv/csv-adapter.js.map +1 -0
  11. package/dist/adapters/csv/csv-manifest.js +9 -0
  12. package/dist/adapters/csv/csv-manifest.js.map +1 -0
  13. package/dist/{docx-adapter-5CQDHWTD.js → adapters/docx/docx-adapter.js} +17 -175
  14. package/dist/adapters/docx/docx-adapter.js.map +1 -0
  15. package/dist/adapters/docx/docx-manifest.js +9 -0
  16. package/dist/adapters/docx/docx-manifest.js.map +1 -0
  17. package/dist/adapters/docx/docx-model.js +21 -0
  18. package/dist/adapters/docx/docx-model.js.map +1 -0
  19. package/dist/{image-adapter-WOHZR24J.js → adapters/image/image-adapter.js} +4 -3
  20. package/dist/adapters/image/image-adapter.js.map +1 -0
  21. package/dist/adapters/image/image-manifest.js +9 -0
  22. package/dist/adapters/image/image-manifest.js.map +1 -0
  23. package/dist/adapters/index.js +56 -0
  24. package/dist/adapters/index.js.map +1 -0
  25. package/dist/{json-adapter-ZUW5GQHE.js → adapters/json/json-adapter.js} +4 -3
  26. package/dist/adapters/json/json-adapter.js.map +1 -0
  27. package/dist/adapters/json/json-manifest.js +9 -0
  28. package/dist/adapters/json/json-manifest.js.map +1 -0
  29. package/dist/{markdown-adapter-YC6WTBS4.js → adapters/markdown/markdown-adapter.js} +14 -36
  30. package/dist/adapters/markdown/markdown-adapter.js.map +1 -0
  31. package/dist/adapters/markdown/markdown-manifest.js +9 -0
  32. package/dist/adapters/markdown/markdown-manifest.js.map +1 -0
  33. package/dist/adapters/markdown/markdown-marks.js +10 -0
  34. package/dist/adapters/markdown/markdown-marks.js.map +1 -0
  35. package/dist/{media-adapter-MCTB4GBH.js → adapters/media/media-adapter.js} +4 -3
  36. package/dist/adapters/media/media-adapter.js.map +1 -0
  37. package/dist/adapters/media/media-manifest.js +9 -0
  38. package/dist/adapters/media/media-manifest.js.map +1 -0
  39. package/dist/{pdf-adapter-5PMKEXUD.js → adapters/pdf/pdf-adapter.js} +31 -81
  40. package/dist/adapters/pdf/pdf-adapter.js.map +1 -0
  41. package/dist/adapters/pdf/pdf-engine.js +14 -0
  42. package/dist/adapters/pdf/pdf-engine.js.map +1 -0
  43. package/dist/adapters/pdf/pdf-manifest.js +9 -0
  44. package/dist/adapters/pdf/pdf-manifest.js.map +1 -0
  45. package/dist/adapters/pdf/pdf-text.js +20 -0
  46. package/dist/adapters/pdf/pdf-text.js.map +1 -0
  47. package/dist/{pptx-adapter-BONWFHYT.js → adapters/pptx/pptx-adapter.js} +34 -135
  48. package/dist/adapters/pptx/pptx-adapter.js.map +1 -0
  49. package/dist/adapters/pptx/pptx-manifest.js +9 -0
  50. package/dist/adapters/pptx/pptx-manifest.js.map +1 -0
  51. package/dist/adapters/pptx/pptx-model.js +37 -0
  52. package/dist/adapters/pptx/pptx-model.js.map +1 -0
  53. package/dist/{text-adapter-NFNWB5W3.js → adapters/text/text-adapter.js} +8 -5
  54. package/dist/adapters/text/text-adapter.js.map +1 -0
  55. package/dist/adapters/text/text-manifest.js +9 -0
  56. package/dist/adapters/text/text-manifest.js.map +1 -0
  57. package/dist/{xlsx-adapter-O3XIVLFC.js → adapters/xlsx/xlsx-adapter.js} +12 -8
  58. package/dist/adapters/xlsx/xlsx-adapter.js.map +1 -0
  59. package/dist/adapters/xlsx/xlsx-manifest.js +9 -0
  60. package/dist/adapters/xlsx/xlsx-manifest.js.map +1 -0
  61. package/dist/chunk-3HKQFFZW.js +31 -0
  62. package/dist/chunk-3HKQFFZW.js.map +1 -0
  63. package/dist/chunk-5VJWILZ7.js +182 -0
  64. package/dist/chunk-5VJWILZ7.js.map +1 -0
  65. package/dist/{chunk-UL43NGUG.js → chunk-5YE2MUS2.js} +1 -1
  66. package/dist/{chunk-5OMONA2G.js → chunk-6FVJ4D7W.js} +3 -15
  67. package/dist/chunk-6FVJ4D7W.js.map +1 -0
  68. package/dist/{chunk-RGWDIQJR.js → chunk-7EASJBQM.js} +2 -2
  69. package/dist/chunk-7PGGKQVY.js +26 -0
  70. package/dist/chunk-7PGGKQVY.js.map +1 -0
  71. package/dist/{chunk-XBOR2YXS.js → chunk-AK7F3IZE.js} +9 -30
  72. package/dist/chunk-AK7F3IZE.js.map +1 -0
  73. package/dist/chunk-AOYEL7O3.js +34 -0
  74. package/dist/chunk-AOYEL7O3.js.map +1 -0
  75. package/dist/chunk-BDUFH66C.js +61 -0
  76. package/dist/chunk-BDUFH66C.js.map +1 -0
  77. package/dist/chunk-BWMXMHVZ.js +107 -0
  78. package/dist/chunk-BWMXMHVZ.js.map +1 -0
  79. package/dist/chunk-CIT2HHRI.js +9 -0
  80. package/dist/chunk-CIT2HHRI.js.map +1 -0
  81. package/dist/chunk-CLOI4HBA.js +18 -0
  82. package/dist/chunk-CLOI4HBA.js.map +1 -0
  83. package/dist/chunk-CMYBLW7D.js +21 -0
  84. package/dist/chunk-CMYBLW7D.js.map +1 -0
  85. package/dist/chunk-CO2VJRSU.js +18 -0
  86. package/dist/chunk-CO2VJRSU.js.map +1 -0
  87. package/dist/{chunk-3U3MESBU.js → chunk-EA72HA4I.js} +2 -2
  88. package/dist/{chunk-KOMDRWOU.js → chunk-FDT4QOIG.js} +2 -2
  89. package/dist/chunk-FQHZEQUH.js +90 -0
  90. package/dist/chunk-FQHZEQUH.js.map +1 -0
  91. package/dist/chunk-G27UJZWH.js +82 -0
  92. package/dist/chunk-G27UJZWH.js.map +1 -0
  93. package/dist/{chunk-BCF3RWRL.js → chunk-GH4UBYST.js} +2 -2
  94. package/dist/{chunk-2NQ4RSJ3.js → chunk-GNBD53AT.js} +2 -2
  95. package/dist/{chunk-VO2273Z2.js → chunk-HOQZ7OPH.js} +2 -18
  96. package/dist/chunk-HOQZ7OPH.js.map +1 -0
  97. package/dist/{chunk-GGR7BNW4.js → chunk-HUUJIFFT.js} +2 -2
  98. package/dist/{chunk-FELIAGKI.js → chunk-I5XD2NJ6.js} +2 -2
  99. package/dist/chunk-IB4N72FA.js +21 -0
  100. package/dist/chunk-IB4N72FA.js.map +1 -0
  101. package/dist/{chunk-4OFQYALG.js → chunk-IQ6Z4X3K.js} +8 -68
  102. package/dist/chunk-IQ6Z4X3K.js.map +1 -0
  103. package/dist/{chunk-AORNMH77.js → chunk-JHFC2KBI.js} +1 -5
  104. package/dist/chunk-JHFC2KBI.js.map +1 -0
  105. package/dist/chunk-LRPZ373O.js +93 -0
  106. package/dist/chunk-LRPZ373O.js.map +1 -0
  107. package/dist/chunk-M77ICVWP.js +24 -0
  108. package/dist/chunk-M77ICVWP.js.map +1 -0
  109. package/dist/chunk-MJUH32ZK.js +24 -0
  110. package/dist/chunk-MJUH32ZK.js.map +1 -0
  111. package/dist/chunk-PAIVK4N6.js +162 -0
  112. package/dist/chunk-PAIVK4N6.js.map +1 -0
  113. package/dist/chunk-PKYDKXCO.js +129 -0
  114. package/dist/chunk-PKYDKXCO.js.map +1 -0
  115. package/dist/chunk-QIPKBOZJ.js +59 -0
  116. package/dist/chunk-QIPKBOZJ.js.map +1 -0
  117. package/dist/chunk-SASY77ZL.js +29 -0
  118. package/dist/chunk-SASY77ZL.js.map +1 -0
  119. package/dist/{chunk-SLXRUYT3.js → chunk-SBG7WPGO.js} +2 -44
  120. package/dist/chunk-SBG7WPGO.js.map +1 -0
  121. package/dist/chunk-T4Y4YRHJ.js +733 -0
  122. package/dist/chunk-T4Y4YRHJ.js.map +1 -0
  123. package/dist/chunk-TLCNGQKL.js +37 -0
  124. package/dist/chunk-TLCNGQKL.js.map +1 -0
  125. package/dist/chunk-US5KT3TR.js +141 -0
  126. package/dist/chunk-US5KT3TR.js.map +1 -0
  127. package/dist/{chunk-HMG2ERXH.js → chunk-YA72QE57.js} +2 -2
  128. package/dist/{chunk-H3OAON3D.js → chunk-YWHFMCS4.js} +2 -2
  129. package/dist/{chunk-UJIJECEF.js → chunk-ZAVXZRIO.js} +2 -2
  130. package/dist/components/grid-text.js +19 -0
  131. package/dist/components/grid-text.js.map +1 -0
  132. package/dist/components/marked-text.js +10 -0
  133. package/dist/components/marked-text.js.map +1 -0
  134. package/dist/components/sheet-table.js +14 -0
  135. package/dist/components/sheet-table.js.map +1 -0
  136. package/dist/core/errors.js +18 -0
  137. package/dist/core/errors.js.map +1 -0
  138. package/dist/core/highlight-marks.js +10 -0
  139. package/dist/core/highlight-marks.js.map +1 -0
  140. package/dist/core/highlight-resolve.js +10 -0
  141. package/dist/core/highlight-resolve.js.map +1 -0
  142. package/dist/core/highlight.js +12 -0
  143. package/dist/core/highlight.js.map +1 -0
  144. package/dist/core/registry.js +12 -0
  145. package/dist/core/registry.js.map +1 -0
  146. package/dist/core/scroll-host.js +10 -0
  147. package/dist/core/scroll-host.js.map +1 -0
  148. package/dist/core/text-index.js +14 -0
  149. package/dist/core/text-index.js.map +1 -0
  150. package/dist/core/types.js +8 -0
  151. package/dist/core/types.js.map +1 -0
  152. package/dist/core/use-highlight-scroll.js +11 -0
  153. package/dist/core/use-highlight-scroll.js.map +1 -0
  154. package/dist/core/use-page-control.js +9 -0
  155. package/dist/core/use-page-control.js.map +1 -0
  156. package/dist/core/use-paged-scroll.js +10 -0
  157. package/dist/core/use-paged-scroll.js.map +1 -0
  158. package/dist/core/use-viewport-size.js +10 -0
  159. package/dist/core/use-viewport-size.js.map +1 -0
  160. package/dist/core/zoom.js +16 -0
  161. package/dist/core/zoom.js.map +1 -0
  162. package/dist/file-viewer/file-viewer-context.js +11 -0
  163. package/dist/file-viewer/file-viewer-context.js.map +1 -0
  164. package/dist/file-viewer/file-viewer-find.js +13 -0
  165. package/dist/file-viewer/file-viewer-find.js.map +1 -0
  166. package/dist/file-viewer/file-viewer-pager.js +10 -0
  167. package/dist/file-viewer/file-viewer-pager.js.map +1 -0
  168. package/dist/file-viewer/file-viewer-zoom.js +13 -0
  169. package/dist/file-viewer/file-viewer-zoom.js.map +1 -0
  170. package/dist/file-viewer/file-viewer.js +48 -0
  171. package/dist/file-viewer/file-viewer.js.map +1 -0
  172. package/dist/index.js +78 -1274
  173. package/dist/index.js.map +1 -1
  174. package/package.json +5 -5
  175. package/src/file-viewer/file-viewer-recipes.stories.tsx +406 -0
  176. package/src/file-viewer/file-viewer.stories.tsx +2 -2
  177. package/dist/chunk-4OFQYALG.js.map +0 -1
  178. package/dist/chunk-5OMONA2G.js.map +0 -1
  179. package/dist/chunk-AORNMH77.js.map +0 -1
  180. package/dist/chunk-SLXRUYT3.js.map +0 -1
  181. package/dist/chunk-VO2273Z2.js.map +0 -1
  182. package/dist/chunk-XBOR2YXS.js.map +0 -1
  183. package/dist/code-adapter-ADZ4UOGN.js.map +0 -1
  184. package/dist/csv-adapter-7HOFZ5Z6.js.map +0 -1
  185. package/dist/docx-adapter-5CQDHWTD.js.map +0 -1
  186. package/dist/image-adapter-WOHZR24J.js.map +0 -1
  187. package/dist/json-adapter-ZUW5GQHE.js.map +0 -1
  188. package/dist/markdown-adapter-YC6WTBS4.js.map +0 -1
  189. package/dist/media-adapter-MCTB4GBH.js.map +0 -1
  190. package/dist/pdf-adapter-5PMKEXUD.js.map +0 -1
  191. package/dist/pptx-adapter-BONWFHYT.js.map +0 -1
  192. package/dist/text-adapter-NFNWB5W3.js.map +0 -1
  193. package/dist/xlsx-adapter-O3XIVLFC.js.map +0 -1
  194. /package/dist/{chunk-UL43NGUG.js.map → chunk-5YE2MUS2.js.map} +0 -0
  195. /package/dist/{chunk-RGWDIQJR.js.map → chunk-7EASJBQM.js.map} +0 -0
  196. /package/dist/{chunk-3U3MESBU.js.map → chunk-EA72HA4I.js.map} +0 -0
  197. /package/dist/{chunk-KOMDRWOU.js.map → chunk-FDT4QOIG.js.map} +0 -0
  198. /package/dist/{chunk-BCF3RWRL.js.map → chunk-GH4UBYST.js.map} +0 -0
  199. /package/dist/{chunk-2NQ4RSJ3.js.map → chunk-GNBD53AT.js.map} +0 -0
  200. /package/dist/{chunk-GGR7BNW4.js.map → chunk-HUUJIFFT.js.map} +0 -0
  201. /package/dist/{chunk-FELIAGKI.js.map → chunk-I5XD2NJ6.js.map} +0 -0
  202. /package/dist/{chunk-HMG2ERXH.js.map → chunk-YA72QE57.js.map} +0 -0
  203. /package/dist/{chunk-H3OAON3D.js.map → chunk-YWHFMCS4.js.map} +0 -0
  204. /package/dist/{chunk-UJIJECEF.js.map → chunk-ZAVXZRIO.js.map} +0 -0
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ // src/core/types.ts
4
+ var PROTOCOL_VERSION = 1;
5
+
6
+ export {
7
+ PROTOCOL_VERSION
8
+ };
9
+ //# sourceMappingURL=chunk-CIT2HHRI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/types.ts"],"sourcesContent":["/**\n * The adapter protocol — what a format plugin declares, what it produces, and\n * what renders it.\n *\n * ## The two halves, and why they are split\n *\n * A registry entry is an eager {@link AdapterManifest} plus a lazy loader. The\n * manifest is plain data available the moment the registry is created, so the\n * chrome can decide which controls to show and whether a file is openable at all\n * **without downloading a parser**. The loader is a `() => import(…)` that pulls\n * the parser AND its renderer together, only once a file of that kind is opened.\n *\n * ## Adapters emit DATA, not HTML (ADR 0024 §3)\n *\n * An adapter's `load()` returns an {@link AdapterDocument} — a model — and ships\n * a `Renderer` that draws it with brand-ui components. No adapter returns an\n * HTML string, so no adapter can smuggle a colour, a font or a border past the\n * token layer. This is the single decision that keeps the viewer themeable.\n */\n\nimport type {\n DocumentAddressKind,\n FileCategory,\n ProseHeadingLevel,\n ResolvedFileSource,\n} from \"@elabs-ai/components-ui\";\nimport type { ComponentType } from \"react\";\n\nimport type { ResolvedHighlight } from \"./highlight\";\nimport type { TextIndex } from \"./text-index\";\n\n/**\n * The adapter protocol version. Bumped only when the shapes below change in a\n * way an existing adapter would not survive; an adapter declaring a different\n * version is rejected with a named error rather than crashing at render.\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * A quarter-turn clockwise rotation applied to the whole document.\n *\n * Document-level, not per-page: turning one page of a scan and leaving the rest\n * is an AUTHORING act (it changes the file), and this package reads files.\n */\nexport type DocumentRotation = 0 | 90 | 180 | 270;\n\n/** Scale the content to a dimension of the viewport instead of to a fixed number. */\nexport type ZoomFit = \"fit-width\" | \"fit-page\";\n\n/** One page's size at scale 1, in CSS pixels. */\nexport interface PageSize {\n width: number;\n height: number;\n}\n\n/**\n * What the chrome asks a renderer to draw at.\n *\n * A fit mode is a REQUEST, not a scale: only the renderer knows how wide its own\n * viewport is and how big the page is, so it resolves the request and reports the\n * number back through {@link AdapterRendererProps.onZoomResolved}. Resolving it\n * in the provider would mean the shell measuring a box it does not own.\n */\nexport type ZoomLevel = number | ZoomFit;\n\n/** What the chrome may offer for a format, known before any parser is fetched. */\nexport interface AdapterCapabilities {\n /** The document is paginated — show the pager. */\n pages?: boolean;\n /** The rendered content can be scaled — show the zoom control. */\n zoom?: boolean;\n /** The rendered content can be rotated. */\n rotate?: boolean;\n /**\n * The document exposes text that can be searched.\n *\n * An OVERRIDE, not the source of truth: the shell offers find-in-document\n * whenever a format has both a text projection and a text-based\n * {@link AdapterCapabilities.highlight} kind, and reads this only when an\n * adapter says otherwise. Left as the source of truth it would deny the find\n * box to PDF — the format readers most expect it on — purely because that\n * manifest predates the feature and never declared the flag.\n *\n * It overrides in the OFF direction only, so a declared `true` is inert: it\n * cannot grant find to a format with no text or no range-painting renderer,\n * because the box would then have nothing to show for a match.\n */\n search?: boolean;\n /**\n * Which {@link DocumentAddress} kinds this adapter's `Renderer` can actually\n * paint. Absent means none.\n *\n * On the EAGER manifest on purpose: an app can decide whether to show a\n * \"jump to the cited passage\" affordance before it downloads pdf.js.\n *\n * It is a promise the renderer has to keep — `pnpm viewer-highlight:check`\n * fails a manifest that declares a kind its adapter never exercises.\n */\n highlight?: readonly DocumentAddressKind[];\n /** The document has an outline/table of contents for the sidebar. */\n outline?: boolean;\n /** The document can produce page thumbnails for the sidebar. */\n thumbnails?: boolean;\n /** A plain-text projection exists — enables the raw/source toggle and copy. */\n text?: boolean;\n}\n\n/**\n * What an adapter can open. At least one of `extensions` / `mediaTypes` /\n * `categories` must be present, or nothing will ever route to it.\n */\nexport interface AdapterManifest {\n /**\n * Stable identity and the OVERRIDE key: registering a different loader under\n * an existing id replaces it, which is how a consumer swaps a built-in.\n */\n id: string;\n /** Must equal {@link PROTOCOL_VERSION}. */\n protocol: number;\n /**\n * Tie-break when several adapters claim the same file. Built-ins are `0`;\n * register above it to win, below it to act only as a fallback.\n */\n priority?: number;\n /** Extensions claimed, lowercase and without the dot. The most specific match. */\n extensions?: string[];\n /** MIME types claimed. A trailing slash makes it a prefix (`\"image/\"`). */\n mediaTypes?: string[];\n /** Coarse categories claimed. The broadest match — a catch-all fallback. */\n categories?: FileCategory[];\n /** Which chrome controls apply. Absent keys are `false`. */\n capabilities?: AdapterCapabilities;\n /**\n * Optional peer packages this adapter dynamically imports. Purely\n * informational to the registry — it is what the \"install this to open that\"\n * message names when the import fails to resolve.\n */\n requires?: string[];\n}\n\n/**\n * An adapter's parsed output. Adapters EXTEND this with their own payload; the\n * fields here are the parts the shell itself understands, so it can drive the\n * pager, the search box and the raw toggle without knowing the format.\n */\nexport interface AdapterDocument {\n /** Adapter-defined discriminant, matched by that adapter's own `Renderer`. */\n readonly kind: string;\n /** Page count for a paginated document. Omit for single-surface formats. */\n readonly pageCount?: number;\n /**\n * Each page's size at scale 1, in document order.\n *\n * What lets a continuously-scrolling renderer reserve the right height for a\n * page it has not drawn yet, so the scrollbar means something from the first\n * frame instead of growing as pages arrive. May be SHORTER than\n * {@link AdapterDocument.pageCount} — measuring 900 pages up front costs 900\n * round-trips to a worker, so an adapter is free to describe the pages it\n * already touched and let the renderer assume the rest match.\n */\n readonly pageSizes?: readonly PageSize[];\n /** Plain-text projection, when the format has one. Powers search, copy and the raw view. */\n readonly text?: string;\n /**\n * Whether {@link AdapterDocument.text} is CAPPED rather than complete.\n *\n * Changes what \"we couldn't find that passage\" means: absent from the whole\n * document, or simply past the part we previewed. Those deserve different\n * sentences, and only the adapter knows which one is true.\n */\n readonly textTruncated?: boolean;\n /**\n * The map from {@link AdapterDocument.text} back into this adapter's own\n * model, so a character range can become a block, a page or a cell.\n *\n * The `ref` type is the adapter's business — its own `Renderer` is the only\n * thing that reads it, which is why it is `unknown` here.\n */\n readonly textIndex?: TextIndex<unknown>;\n}\n\n/** Everything an adapter is given while loading a document. */\nexport interface AdapterLoadContext {\n /** Cancels the load — always honour it; a stale parse must not win a race. */\n signal?: AbortSignal;\n /** Report determinate progress in `0..1` where the format allows it. */\n onProgress?: (fraction: number) => void;\n}\n\n/** A parser instance. One per document — adapters may hold per-document state. */\nexport interface FileAdapter {\n load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<AdapterDocument>;\n /** Release anything the document holds (workers, object URLs, decoded buffers). */\n dispose?(): void;\n}\n\n/** Props every adapter `Renderer` receives. */\nexport interface AdapterRendererProps {\n /** The value this adapter's own `load()` returned — narrow it by `kind`. */\n document: AdapterDocument;\n /** The source, for identity, `alt` text and download actions. */\n source: ResolvedFileSource;\n className?: string;\n /**\n * The heading rung a document's own top-level heading renders at.\n *\n * A viewed file carries its OWN heading tree, and that tree has to be\n * relative to the page hosting it: a README's `#` inside an app that already\n * has an `<h1>` would otherwise put two `h1`s in a screen reader's flat\n * heading list. `FileViewerContent` passes the provider's value; an adapter\n * that renders headings offsets by it (`clampHeadingLevel(own + base - 1)`),\n * and one that renders none ignores it. Same seam as\n * `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.\n *\n * Optional, so adding it is not a `PROTOCOL_VERSION` change.\n */\n baseHeadingLevel?: ProseHeadingLevel;\n /**\n * The parts of the document to mark, already located — citations from the\n * app and the viewer's own find-in-document matches, in document order.\n *\n * The shell has done LOCATE (a quote is now a character range); the renderer\n * does MAP and PAINT, because only it knows that offsets 812–847 are the\n * second half of block 14. An adapter renders only the entries whose `status`\n * is `\"resolved\"` and whose `address.kind` it declared, and ignores the rest\n * — the shell is what tells the reader about a miss.\n *\n * Optional, so adding it is not a `PROTOCOL_VERSION` change.\n */\n highlights?: readonly ResolvedHighlight[];\n /**\n * Which highlight the viewer is pointed at. The renderer scrolls it into\n * view, pages to it if the format paginates, and draws it distinctly.\n *\n * Also present as `active` on the entry itself; this is the shortcut for a\n * renderer that only needs to know whether anything changed.\n */\n activeHighlightId?: string | null;\n /**\n * Which page the reader is on, 1-based — the provider's, not the renderer's.\n *\n * Page, zoom and rotation are PROVIDER state (ADR 0026) so a control can live\n * anywhere: the shell's own toolbar, an app's page header, a deep link. While\n * these were `useState` inside each renderer nothing outside the canvas could\n * read or drive them.\n *\n * Uncontrolled when absent — a renderer used outside a `FileViewerProvider`\n * keeps its own page, so it still works standalone. Optional, so adding it is\n * not a `PROTOCOL_VERSION` change.\n */\n pageNumber?: number;\n /**\n * Report a page the RENDERER navigated to on its own — turning to a cited\n * page, or (once pages scroll continuously) the page scrolled into view. The\n * provider is what keeps the pager's number honest.\n */\n onPageChange?: (page: number) => void;\n /** The scale, or a fit mode to resolve. Absent means the renderer's own default. */\n zoom?: ZoomLevel;\n /**\n * Report the scale a {@link ZoomFit} actually resolved to.\n *\n * Only a renderer knows this — it is the one measuring its viewport. Without\n * it the chrome cannot show a percentage for a fitted page, and the reader's\n * next \"zoom in\" would step from the last fixed stop rather than from what is\n * on screen. A renderer that never resolves a fit mode never calls it.\n */\n onZoomResolved?: (scale: number) => void;\n /** Quarter-turns clockwise. Ignored by renderers whose manifest omits `rotate`. */\n rotation?: DocumentRotation;\n}\n\n/**\n * What an adapter module exports. `manifest` is re-declared here so the registry\n * can verify the loaded module is the one the eager manifest promised.\n */\nexport interface AdapterModule {\n manifest: AdapterManifest;\n /** Fresh instance per document — never a shared singleton. */\n create(): FileAdapter;\n Renderer: ComponentType<AdapterRendererProps>;\n}\n\n/** A lazy adapter loader. Always `() => import(\"…\")` so nothing heavy is eager. */\nexport type AdapterLoader = () => Promise<AdapterModule | { default: AdapterModule }>;\n"],"mappings":";;;AAoCO,IAAM,mBAAmB;","names":[]}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ // src/file-viewer/file-viewer-context.tsx
4
+ import { createContext, use } from "react";
5
+ var FileViewerContext = createContext(null);
6
+ function useFileViewer() {
7
+ const value = use(FileViewerContext);
8
+ if (!value) {
9
+ throw new Error("useFileViewer must be used inside a <FileViewerProvider> (or <FileViewer>).");
10
+ }
11
+ return value;
12
+ }
13
+
14
+ export {
15
+ FileViewerContext,
16
+ useFileViewer
17
+ };
18
+ //# sourceMappingURL=chunk-CLOI4HBA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/file-viewer/file-viewer-context.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * The context contract behind every `FileViewer` part.\n *\n * Per `component-api.md` (\"Lift state into the Provider; expose a\n * `state` / `actions` interface\"), `FileViewerProvider` is the only thing that\n * knows HOW a file is loaded. Parts read the interface below, so a sibling\n * control placed outside the visual frame but inside the provider — a download\n * button in a page header, a format badge in a breadcrumb — reads and drives the\n * same state with no prop-drilling.\n *\n * This module is the CONTRACT only; the provider itself lives beside the parts\n * it feeds, in `file-viewer.tsx`.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { createContext, use } from \"react\";\n\nimport type { ViewerError } from \"../core/errors\";\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"../core/highlight\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type {\n AdapterCapabilities,\n AdapterDocument,\n AdapterModule,\n DocumentRotation,\n ZoomLevel,\n} from \"../core/types\";\n\n/**\n * Where a file is in its journey to the screen.\n *\n * `loading` means \"no renderable content yet\" — the canonical signal from\n * `.claude/rules/loading-states.md`, rendered as a layout-shaped skeleton.\n * There is no separate `isStreaming`: a file arrives settled or not at all.\n */\nexport type FileViewerStatus = \"empty\" | \"loading\" | \"ready\" | \"error\";\n\n/**\n * The LOAD half of the state — everything the fetch-and-parse effect owns.\n *\n * Split from the highlight half because the two have different lifetimes: this\n * one is replaced wholesale each time a file is opened, while the citations\n * pointing into it are a prop the app controls and outlive any single parse.\n * Folding them together would mean every `setState` in the load effect had to\n * remember to carry the highlights forward.\n */\nexport interface FileViewerLoadState {\n status: FileViewerStatus;\n /** The resolved source, available as soon as there IS one — before any read. */\n source?: ResolvedFileSource;\n /** The adapter's parsed output. Only in `ready`. */\n document?: AdapterDocument;\n /** The adapter module that produced it, for its `Renderer`. Only in `ready`. */\n adapter?: AdapterModule;\n /** What the chrome may offer. Known from the manifest BEFORE the parser loads. */\n capabilities: AdapterCapabilities;\n /** Only in `error`. Always a `ViewerError`, so `code` can drive the message. */\n error?: ViewerError;\n}\n\n/**\n * How the open document is being LOOKED at — which page, at what scale, turned\n * which way.\n *\n * Separate from the load state because it survives nothing and owns nothing: it\n * is pure view, reset (page, rotation) or carried (zoom) when a new file opens.\n * It lives in the provider rather than inside each adapter's `Renderer` so a\n * control can sit anywhere — the shell toolbar, an app's own page header, a\n * deep link — instead of only inside the canvas (ADR 0026).\n */\nexport interface FileViewerViewState {\n /** 1-based. `1` for a format that does not paginate. */\n pageNumber: number;\n /** `0` until a paginated document is ready, and for formats with no pages. */\n pageCount: number;\n /** What was ASKED for: a fixed scale, or a fit mode the renderer resolves. */\n zoom: ZoomLevel;\n /**\n * What that resolved to, as a number — what the zoom control shows and what\n * `zoomIn`/`zoomOut` step from. Equal to `zoom` whenever `zoom` is a number.\n */\n effectiveZoom: number;\n /** Quarter-turns clockwise. Reset to `0` when a different file is opened. */\n rotation: DocumentRotation;\n}\n\nexport interface FileViewerState extends FileViewerLoadState, FileViewerViewState {\n /**\n * The parts of the document to point at, as REQUESTED. What was actually\n * located is `meta.resolvedHighlights` — the request survives a miss so the\n * chrome can say \"we couldn't find that passage\" instead of showing nothing.\n */\n highlights: readonly DocumentHighlight[];\n /** Which one the viewer is pointed at. `null` is \"none\", explicitly. */\n activeHighlightId: string | null;\n find: FileViewerFindState;\n}\n\n/** What find-in-document is doing right now. */\nexport interface FileViewerFindState {\n /** Whether the search box is showing. */\n open: boolean;\n query: string;\n caseSensitive: boolean;\n /** How many matches the current query has. */\n matches: number;\n /** Whether that count hit `FIND_MATCH_LIMIT` and is therefore a floor. */\n truncated: boolean;\n /**\n * Which match is current, 0-based — the \"3\" in \"3 of 12\", minus one.\n *\n * Deliberately NOT the same knob as `activeHighlightId`. Find is the viewer's\n * own, and an app that controls `activeHighlightId` to drive citations would\n * otherwise have to also honour every keystroke of a search it never asked\n * for, or silently break next/previous.\n */\n activeIndex: number;\n}\n\nexport interface FileViewerActions {\n /** Re-run the load. The retry action on the error state. */\n reload: () => void;\n /**\n * Replace the citations. While `highlights` is controlled this writes no local\n * state — but it still calls `onHighlightsChange`, so the owner can accept the\n * request. Mirroring the platform: a controlled input reports, it does not\n * self-update.\n */\n setHighlights: (highlights: readonly DocumentHighlight[]) => void;\n /** Point the viewer at one highlight, or at none. */\n setActiveHighlight: (id: string | null) => void;\n /** Move to the next/previous CITATION in document order, wrapping around. */\n nextHighlight: () => void;\n previousHighlight: () => void;\n openFind: () => void;\n closeFind: () => void;\n setFindQuery: (query: string) => void;\n setFindCaseSensitive: (caseSensitive: boolean) => void;\n /** Move to the next/previous SEARCH match, wrapping around. */\n nextFindMatch: () => void;\n previousFindMatch: () => void;\n /**\n * Turn to a page, 1-based. Clamped to the document — an out-of-range page is\n * a caller's arithmetic slip, not a reason to blank the canvas.\n */\n goToPage: (page: number) => void;\n /** Turn one page. Both stop at the ends rather than wrapping: a document is not a carousel. */\n nextPage: () => void;\n previousPage: () => void;\n /** Draw at a fixed scale, or hand the renderer a fit mode to resolve. */\n setZoom: (zoom: ZoomLevel) => void;\n /**\n * Step to the next stop above/below what is currently ON SCREEN — so zooming\n * in from a fitted page continues from the fitted scale, not from wherever the\n * fixed ladder was last parked.\n */\n zoomIn: () => void;\n zoomOut: () => void;\n setRotation: (rotation: DocumentRotation) => void;\n /** Turn the document a quarter-turn: `1` clockwise, `-1` counter-clockwise. */\n rotate: (quarterTurns: 1 | -1) => void;\n /**\n * The renderer's report channel for {@link FileViewerViewState.effectiveZoom}\n * — not for app code. `FileViewerContent` wires it to the adapter's\n * `onZoomResolved`, the same way `registerFind` is wired to a part rather than\n * called by a consumer.\n */\n reportZoom: (scale: number) => void;\n /**\n * Tell the viewer a find part is mounted; call the returned function on\n * unmount. `FileViewerFind` does this for you — it exists so the frame knows\n * whether taking Ctrl/Cmd+F off the browser leads anywhere.\n */\n registerFind: () => () => void;\n}\n\nexport interface FileViewerContextValue {\n state: FileViewerState;\n actions: FileViewerActions;\n registry: ViewerRegistry;\n meta: {\n /**\n * The rung a viewed document's own top-level heading renders at. Passed to\n * every adapter `Renderer`; see `AdapterRendererProps.baseHeadingLevel`.\n */\n baseHeadingLevel: ProseHeadingLevel;\n /**\n * Citations and find matches, LOCATED, in document order, numbered — what\n * an adapter `Renderer` is handed and what the chrome counts.\n */\n resolvedHighlights: readonly ResolvedHighlight[];\n /**\n * Which highlight the viewer is EFFECTIVELY pointed at, and what an adapter\n * `Renderer` receives as `activeHighlightId`.\n *\n * Not the same knob as `state.activeHighlightId`: that one is the citation\n * the app controls, while the reader stepping through find matches is also\n * \"current\". Find outranks the citation while its box is open and matching,\n * so navigation and scrolling follow whichever the reader is actually moving.\n */\n currentHighlightId: string | null;\n /** Which address kinds this document's adapter declared it can paint. */\n highlightSupport: HighlightSupport;\n /**\n * Whether find-in-document applies to the open document. Derived, not read\n * straight off the manifest — see `AdapterCapabilities.search`.\n */\n canFind: boolean;\n /**\n * Whether a `FileViewerFind` part is composed into this viewer.\n *\n * Separate from {@link canFind}, which only says the ADAPTER could paint a\n * match. The frame needs both before it takes Ctrl/Cmd+F away from the\n * browser: intercepting the shortcut with nowhere to type leaves a keyboard\n * reader with no find at all.\n */\n hasFind: boolean;\n };\n}\n\n/**\n * Exported for `FileViewerProvider` (which lives with the parts it feeds, in\n * `file-viewer.tsx`) — NOT part of the package's public surface.\n */\nexport const FileViewerContext = createContext<FileViewerContextValue | null>(null);\n\n/**\n * Read the viewer state. Throws outside a provider — a part that silently\n * rendered nothing would be far harder to diagnose than a named error.\n */\nexport function useFileViewer(): FileViewerContextValue {\n const value = use(FileViewerContext);\n if (!value) {\n throw new Error(\"useFileViewer must be used inside a <FileViewerProvider> (or <FileViewer>).\");\n }\n return value;\n}\n"],"mappings":";;;AAiBA,SAAS,eAAe,WAAW;AAiN5B,IAAM,oBAAoB,cAA6C,IAAI;AAM3E,SAAS,gBAAwC;AACtD,QAAM,QAAQ,IAAI,iBAAiB;AACnC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,6EAA6E;AAAA,EAC/F;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import {
3
+ CODE_EXTENSIONS
4
+ } from "./chunk-6FVJ4D7W.js";
5
+ import {
6
+ PROTOCOL_VERSION
7
+ } from "./chunk-CIT2HHRI.js";
8
+
9
+ // src/adapters/code/code-manifest.ts
10
+ var codeManifest = {
11
+ id: "code",
12
+ protocol: PROTOCOL_VERSION,
13
+ extensions: CODE_EXTENSIONS,
14
+ capabilities: { text: true, search: true, highlight: ["quote", "range"] },
15
+ requires: ["shiki"]
16
+ };
17
+
18
+ export {
19
+ codeManifest
20
+ };
21
+ //# sourceMappingURL=chunk-CMYBLW7D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/code/code-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\nimport { CODE_EXTENSIONS } from \"./code-language\";\n\n/**\n * Eager, data-only (see `image-manifest.ts`).\n *\n * Claims EXTENSIONS only — never the `code` category. That is what keeps the\n * plain-text adapter as the backstop: a `.log`, a `.env` or an unknown\n * extension still opens as text instead of failing to find a grammar, and a\n * consumer who never installs Shiki loses highlighting rather than the file.\n */\nexport const codeManifest: AdapterManifest = {\n id: \"code\",\n protocol: PROTOCOL_VERSION,\n extensions: CODE_EXTENSIONS,\n capabilities: { text: true, search: true, highlight: [\"quote\", \"range\"] },\n requires: [\"shiki\"],\n};\n"],"mappings":";;;;;;;;;AAYO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,cAAc,EAAE,MAAM,MAAM,QAAQ,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACxE,UAAU,CAAC,OAAO;AACpB;","names":[]}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ // src/core/highlight.ts
4
+ var FIND_MATCH_LIMIT = 2e3;
5
+ var FIND_ID_PREFIX = "find:";
6
+ function findMatchId(index) {
7
+ return `${FIND_ID_PREFIX}${String(index)}`;
8
+ }
9
+ function isFindMatchId(id) {
10
+ return id.startsWith(FIND_ID_PREFIX);
11
+ }
12
+
13
+ export {
14
+ FIND_MATCH_LIMIT,
15
+ findMatchId,
16
+ isFindMatchId
17
+ };
18
+ //# sourceMappingURL=chunk-CO2VJRSU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/highlight.ts"],"sourcesContent":["/**\n * What the viewer is pointed AT — the vocabulary shared by citations and\n * find-in-document.\n *\n * A {@link DocumentHighlight} is what a caller asks for; a\n * {@link ResolvedHighlight} is what the viewer worked out and what an adapter's\n * `Renderer` is handed. Keeping the two apart is what lets \"we could not find\n * that passage\" be a rendered STATE rather than a silent no-op: the request\n * survives even when the location does not, so the chrome still has something\n * to name.\n *\n * The address vocabulary itself lives in `@elabs-ai/components-ui`\n * (`DocumentAddress`) because the producer of a citation and this consumer are\n * sibling packages that may not import each other. Everything here is\n * adapter-protocol detail and stays in this package.\n */\n\nimport type {\n DocumentAddress,\n DocumentAddressKind,\n DocumentRect,\n MatchRange,\n} from \"@elabs-ai/components-ui\";\n\n/**\n * Where a highlight came from. Both paint identically — only which one is\n * ACTIVE differs — but the origin decides who owns the list: citations are a\n * controlled prop the app supplies, search matches are the viewer's own.\n */\nexport type HighlightSource = \"citation\" | \"search\";\n\n/** A request to point the viewer at part of the open document. */\nexport interface DocumentHighlight {\n /**\n * Stable per highlight. It is what `activeHighlightId` names and what React\n * keys on, so a list that renumbers between renders must not renumber ids.\n */\n id: string;\n /** Which part of the document. */\n address: DocumentAddress;\n /**\n * Short human label — the answer's claim, the source's title. Announced when\n * this highlight becomes active, so prefer something a listener can act on\n * over \"Citation 3\".\n */\n label?: string;\n /** Defaults to `\"citation\"`. */\n source?: HighlightSource;\n}\n\n/**\n * How a request turned out.\n *\n * `unsupported` is a CAPABILITY GAP, not a failure — the same distinction the\n * error panel already draws. \"This build can't locate a rect in a Word file\"\n * is news about what we shipped; it is not the reader's mistake, and it is not\n * retryable.\n */\nexport type HighlightStatus = \"pending\" | \"resolved\" | \"not-found\" | \"unsupported\";\n\n/** Why a passage was not found — the two are genuinely different news. */\nexport type HighlightMissReason =\n /** Searched the whole projection; the passage is not in it. */\n | \"absent\"\n /** The projection is capped, and the passage may lie past the cap. */\n | \"truncated\";\n\n/** A request, plus where it landed. What every adapter `Renderer` receives. */\nexport interface ResolvedHighlight {\n id: string;\n label?: string;\n source: HighlightSource;\n status: HighlightStatus;\n /** The original request, so a renderer can honour a kind the shell cannot. */\n address: DocumentAddress;\n /** Whether the viewer is currently pointed at this one. */\n active: boolean;\n /**\n * 1-based position among the highlights of the same `source` that resolved —\n * the \"3\" in \"3 of 12\". Absent when this one did not resolve, so a miss never\n * silently consumes a number the reader is counting through.\n */\n index?: number;\n /** Offsets into `document.text`, once located. */\n range?: MatchRange;\n /**\n * 1-based page, set only for a `rect` address — the one kind with no range to\n * derive a position from.\n *\n * A `quote` or `range` deliberately leaves this empty even when the caller\n * supplied a page hint: where the passage actually landed is knowable from the\n * adapter's own index, and a stale hint used as an instruction would page the\n * reader somewhere the mark is not.\n */\n page?: number;\n /** Geometry, for a `rect` address. */\n rects?: readonly DocumentRect[];\n /** Only on `not-found`. */\n reason?: HighlightMissReason;\n}\n\n/**\n * The most matches find-in-document will paint.\n *\n * A one-letter query against a 2 MB log matches hundreds of thousands of times;\n * every one of those is a DOM element. The cap keeps typing responsive, and the\n * chrome says so rather than quietly showing a wrong total.\n */\nexport const FIND_MATCH_LIMIT = 2000;\n\n/** Prefix reserved for the viewer's own search matches. */\nconst FIND_ID_PREFIX = \"find:\";\n\n/** The id for the nth (0-based) find match. */\nexport function findMatchId(index: number): string {\n return `${FIND_ID_PREFIX}${String(index)}`;\n}\n\n/** Whether an id belongs to find-in-document rather than to a caller. */\nexport function isFindMatchId(id: string): boolean {\n return id.startsWith(FIND_ID_PREFIX);\n}\n\n/** Which address kinds an adapter honours. Absent means none — the safe default. */\nexport type HighlightSupport = readonly DocumentAddressKind[];\n"],"mappings":";;;AA4GO,IAAM,mBAAmB;AAGhC,IAAM,iBAAiB;AAGhB,SAAS,YAAY,OAAuB;AACjD,SAAO,GAAG,cAAc,GAAG,OAAO,KAAK,CAAC;AAC1C;AAGO,SAAS,cAAc,IAAqB;AACjD,SAAO,GAAG,WAAW,cAAc;AACrC;","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
- } from "./chunk-AORNMH77.js";
4
+ } from "./chunk-CIT2HHRI.js";
5
5
 
6
6
  // src/adapters/csv/csv-manifest.ts
7
7
  var csvManifest = {
@@ -18,4 +18,4 @@ var csvManifest = {
18
18
  export {
19
19
  csvManifest
20
20
  };
21
- //# sourceMappingURL=chunk-3U3MESBU.js.map
21
+ //# sourceMappingURL=chunk-EA72HA4I.js.map
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
- } from "./chunk-AORNMH77.js";
4
+ } from "./chunk-CIT2HHRI.js";
5
5
 
6
6
  // src/adapters/pptx/pptx-manifest.ts
7
7
  var pptxManifest = {
@@ -18,4 +18,4 @@ var pptxManifest = {
18
18
  export {
19
19
  pptxManifest
20
20
  };
21
- //# sourceMappingURL=chunk-KOMDRWOU.js.map
21
+ //# sourceMappingURL=chunk-FDT4QOIG.js.map
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ // src/core/highlight-resolve.ts
4
+ import {
5
+ normalizeQuoteText
6
+ } from "@elabs-ai/components-ui";
7
+ function locateQuote(normalized, address) {
8
+ const needle = normalizeQuoteText(address.text);
9
+ if (needle.length === 0) return void 0;
10
+ const starts = [];
11
+ for (let at = normalized.text.indexOf(needle); at !== -1; ) {
12
+ starts.push(at);
13
+ at = normalized.text.indexOf(needle, at + 1);
14
+ }
15
+ if (starts.length === 0) return void 0;
16
+ const toRange = (start) => [
17
+ normalized.offsets[start],
18
+ normalized.offsets[start + needle.length]
19
+ ];
20
+ if (address.occurrence !== void 0) {
21
+ const chosen = starts[address.occurrence - 1];
22
+ return chosen === void 0 ? void 0 : toRange(chosen);
23
+ }
24
+ const near = address.near?.offset;
25
+ if (near !== void 0) {
26
+ let best = starts[0];
27
+ let bestDistance = Number.POSITIVE_INFINITY;
28
+ for (const start of starts) {
29
+ const distance = Math.abs(normalized.offsets[start] - near);
30
+ if (distance < bestDistance) {
31
+ bestDistance = distance;
32
+ best = start;
33
+ }
34
+ }
35
+ return toRange(best);
36
+ }
37
+ return toRange(starts[0]);
38
+ }
39
+ function positionOf(highlight) {
40
+ if (highlight.range) return highlight.range[0];
41
+ if (highlight.page !== void 0) return highlight.page;
42
+ return Number.POSITIVE_INFINITY;
43
+ }
44
+ function resolveOne(highlight, context) {
45
+ const source = highlight.source ?? "citation";
46
+ const address = highlight.address;
47
+ const base = { id: highlight.id, label: highlight.label, source, address, active: false };
48
+ if (!context.supported.includes(address.kind)) {
49
+ return { ...base, status: "unsupported" };
50
+ }
51
+ if (address.kind === "rect") {
52
+ return { ...base, status: "resolved", page: address.page, rects: address.rects };
53
+ }
54
+ const miss = () => ({
55
+ ...base,
56
+ status: "not-found",
57
+ reason: context.truncated ? "truncated" : "absent"
58
+ });
59
+ if (address.kind === "range") {
60
+ const length = context.textLength;
61
+ if (length === void 0) return miss();
62
+ const start = Math.max(0, Math.min(address.start, length));
63
+ const end = Math.max(start, Math.min(address.end, length));
64
+ if (end === start) return miss();
65
+ return { ...base, status: "resolved", range: [start, end] };
66
+ }
67
+ if (!context.normalized) return miss();
68
+ const range = locateQuote(context.normalized, address);
69
+ if (!range) return miss();
70
+ return { ...base, status: "resolved", range };
71
+ }
72
+ function resolveHighlights(highlights, context) {
73
+ const resolved = highlights.map((highlight) => resolveOne(highlight, context)).sort((a, b) => positionOf(a) - positionOf(b));
74
+ const counters = /* @__PURE__ */ new Map();
75
+ return resolved.map((highlight) => {
76
+ const next = highlight.status === "resolved" ? (counters.get(highlight.source) ?? 0) + 1 : void 0;
77
+ if (next !== void 0) counters.set(highlight.source, next);
78
+ return {
79
+ ...highlight,
80
+ index: next,
81
+ active: context.activeId != null && context.activeId === highlight.id
82
+ };
83
+ });
84
+ }
85
+
86
+ export {
87
+ locateQuote,
88
+ resolveHighlights
89
+ };
90
+ //# sourceMappingURL=chunk-FQHZEQUH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/highlight-resolve.ts"],"sourcesContent":["/**\n * Turning a request into a location — the LOCATE step, and the only step that\n * runs outside the adapter.\n *\n * The funnel is three stages with three homes. **Locate** (here) answers \"where\n * in the text projection is this?\" and produces character offsets. **Map** (the\n * adapter's renderer) turns those offsets into its own model — block 14, page 3,\n * cell B7. **Paint** (also the renderer) draws it and scrolls to it.\n *\n * Locate lives in the shell rather than in each adapter because its OUTCOME is\n * chrome state, not pixels: \"3 of 12\", \"we couldn't find that passage\", \"this\n * build can't locate a box in a Word file\". Every adapter would otherwise write\n * that logic again, slightly differently, and the shell would have no way to\n * count what it is showing. It is also pure — no DOM, no engine — so it is\n * testable without jsdom.\n */\n\nimport {\n normalizeQuoteText,\n type MatchRange,\n type NormalizedText,\n type QuoteAddress,\n} from \"@elabs-ai/components-ui\";\n\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"./highlight\";\n\n/**\n * Find a quoted passage in a normalized projection and map it back to raw\n * offsets.\n *\n * Both sides are folded first (whitespace, quote glyphs, case), because a\n * citation is re-typed by a model or extracted by a different tool and will\n * essentially never be byte-identical to what our parser produced.\n *\n * Ambiguity is resolved by the caller's own hints and never guessed at: an\n * explicit `occurrence` wins, then proximity to `near.offset`, then the first\n * match. Returning the first match silently would put a citation on the wrong\n * paragraph of a document that repeats a heading.\n */\nexport function locateQuote(\n normalized: NormalizedText,\n address: QuoteAddress,\n): MatchRange | undefined {\n const needle = normalizeQuoteText(address.text);\n if (needle.length === 0) return undefined;\n\n const starts: number[] = [];\n for (let at = normalized.text.indexOf(needle); at !== -1; ) {\n starts.push(at);\n // Step by one, not by the needle's length: overlapping occurrences of a\n // repeated phrase (\"na na na\") are still distinct places in the document.\n at = normalized.text.indexOf(needle, at + 1);\n }\n if (starts.length === 0) return undefined;\n\n const toRange = (start: number): MatchRange => [\n normalized.offsets[start] as number,\n normalized.offsets[start + needle.length] as number,\n ];\n\n if (address.occurrence !== undefined) {\n const chosen = starts[address.occurrence - 1];\n // An out-of-range occurrence is a miss, not a fallback to the first: the\n // caller asked for the fourth of three, and quietly marking the first would\n // be a confidently wrong citation.\n return chosen === undefined ? undefined : toRange(chosen);\n }\n\n const near = address.near?.offset;\n if (near !== undefined) {\n let best = starts[0] as number;\n let bestDistance = Number.POSITIVE_INFINITY;\n for (const start of starts) {\n const distance = Math.abs((normalized.offsets[start] as number) - near);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = start;\n }\n }\n return toRange(best);\n }\n\n return toRange(starts[0] as number);\n}\n\nexport interface HighlightResolveContext {\n /**\n * The document's text projection, folded once with its offset map. Absent\n * when the format has no text projection at all.\n *\n * Pre-folded rather than raw because find-in-document re-resolves on every\n * keystroke, and folding a 2 MB projection per keystroke is what turns a\n * search box into a stutter.\n */\n normalized?: NormalizedText;\n /** Length of the RAW projection, for clamping `range` addresses. */\n textLength?: number;\n /** Whether that projection is capped, which changes what a miss MEANS. */\n truncated?: boolean;\n /** Which address kinds this document's adapter honours. */\n supported: HighlightSupport;\n /** The highlight the viewer is currently pointed at. */\n activeId?: string | null;\n}\n\n/** Sort key: where in the document this landed. Unresolved sorts last. */\nfunction positionOf(highlight: ResolvedHighlight): number {\n if (highlight.range) return highlight.range[0];\n if (highlight.page !== undefined) return highlight.page;\n return Number.POSITIVE_INFINITY;\n}\n\nfunction resolveOne(\n highlight: DocumentHighlight,\n context: HighlightResolveContext,\n): ResolvedHighlight {\n const source = highlight.source ?? \"citation\";\n const address = highlight.address;\n const base = { id: highlight.id, label: highlight.label, source, address, active: false };\n\n // The load-bearing contract: match only the kinds this adapter DECLARED, and\n // report the rest. No exhaustive switch, no `never` fallthrough — that is\n // what lets a fourth address kind be added later without breaking every\n // consumer that predates it.\n if (!context.supported.includes(address.kind)) {\n return { ...base, status: \"unsupported\" };\n }\n\n if (address.kind === \"rect\") {\n // Geometry needs no text and cannot fail to \"locate\" — whether the page\n // exists is the renderer's business, since only it knows the page count.\n return { ...base, status: \"resolved\", page: address.page, rects: address.rects };\n }\n\n const miss = (): ResolvedHighlight => ({\n ...base,\n status: \"not-found\",\n reason: context.truncated ? \"truncated\" : \"absent\",\n });\n\n if (address.kind === \"range\") {\n const length = context.textLength;\n if (length === undefined) return miss();\n const start = Math.max(0, Math.min(address.start, length));\n const end = Math.max(start, Math.min(address.end, length));\n // A range clamped to nothing is a miss, not a zero-width mark: the offsets\n // were computed against a longer projection than the one we have.\n if (end === start) return miss();\n // No `page`. A producer's page number is a hint, and a wrong one used as an\n // instruction turns the pager to a blank page while the mark sits elsewhere.\n // Where the range actually landed is knowable from the adapter's own index,\n // so that is what navigation uses; `page` stays authoritative only for\n // `rect`, which has no range to derive it from.\n return { ...base, status: \"resolved\", range: [start, end] };\n }\n\n if (!context.normalized) return miss();\n const range = locateQuote(context.normalized, address);\n if (!range) return miss();\n return { ...base, status: \"resolved\", range };\n}\n\n/**\n * Resolve every request, in document order, numbered per source.\n *\n * Document order rather than the caller's, because \"next match\" has to mean the\n * next one down the page — an app listing citations in relevance order would\n * otherwise send the reader jumping backwards. Numbering is per `source` so the\n * find box's \"3 of 12\" counts search matches only, and never the citations\n * painted beside them.\n */\nexport function resolveHighlights(\n highlights: readonly DocumentHighlight[],\n context: HighlightResolveContext,\n): ResolvedHighlight[] {\n const resolved = highlights\n .map((highlight) => resolveOne(highlight, context))\n .sort((a, b) => positionOf(a) - positionOf(b));\n\n const counters = new Map<string, number>();\n return resolved.map((highlight) => {\n const next =\n highlight.status === \"resolved\" ? (counters.get(highlight.source) ?? 0) + 1 : undefined;\n if (next !== undefined) counters.set(highlight.source, next);\n return {\n ...highlight,\n index: next,\n active: context.activeId != null && context.activeId === highlight.id,\n };\n });\n}\n"],"mappings":";;;AAiBA;AAAA,EACE;AAAA,OAIK;AAiBA,SAAS,YACd,YACA,SACwB;AACxB,QAAM,SAAS,mBAAmB,QAAQ,IAAI;AAC9C,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,SAAmB,CAAC;AAC1B,WAAS,KAAK,WAAW,KAAK,QAAQ,MAAM,GAAG,OAAO,MAAM;AAC1D,WAAO,KAAK,EAAE;AAGd,SAAK,WAAW,KAAK,QAAQ,QAAQ,KAAK,CAAC;AAAA,EAC7C;AACA,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,UAAU,CAAC,UAA8B;AAAA,IAC7C,WAAW,QAAQ,KAAK;AAAA,IACxB,WAAW,QAAQ,QAAQ,OAAO,MAAM;AAAA,EAC1C;AAEA,MAAI,QAAQ,eAAe,QAAW;AACpC,UAAM,SAAS,OAAO,QAAQ,aAAa,CAAC;AAI5C,WAAO,WAAW,SAAY,SAAY,QAAQ,MAAM;AAAA,EAC1D;AAEA,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,SAAS,QAAW;AACtB,QAAI,OAAO,OAAO,CAAC;AACnB,QAAI,eAAe,OAAO;AAC1B,eAAW,SAAS,QAAQ;AAC1B,YAAM,WAAW,KAAK,IAAK,WAAW,QAAQ,KAAK,IAAe,IAAI;AACtE,UAAI,WAAW,cAAc;AAC3B,uBAAe;AACf,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO,QAAQ,IAAI;AAAA,EACrB;AAEA,SAAO,QAAQ,OAAO,CAAC,CAAW;AACpC;AAuBA,SAAS,WAAW,WAAsC;AACxD,MAAI,UAAU,MAAO,QAAO,UAAU,MAAM,CAAC;AAC7C,MAAI,UAAU,SAAS,OAAW,QAAO,UAAU;AACnD,SAAO,OAAO;AAChB;AAEA,SAAS,WACP,WACA,SACmB;AACnB,QAAM,SAAS,UAAU,UAAU;AACnC,QAAM,UAAU,UAAU;AAC1B,QAAM,OAAO,EAAE,IAAI,UAAU,IAAI,OAAO,UAAU,OAAO,QAAQ,SAAS,QAAQ,MAAM;AAMxF,MAAI,CAAC,QAAQ,UAAU,SAAS,QAAQ,IAAI,GAAG;AAC7C,WAAO,EAAE,GAAG,MAAM,QAAQ,cAAc;AAAA,EAC1C;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAG3B,WAAO,EAAE,GAAG,MAAM,QAAQ,YAAY,MAAM,QAAQ,MAAM,OAAO,QAAQ,MAAM;AAAA,EACjF;AAEA,QAAM,OAAO,OAA0B;AAAA,IACrC,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,QAAQ,QAAQ,YAAY,cAAc;AAAA,EAC5C;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC5B,UAAM,SAAS,QAAQ;AACvB,QAAI,WAAW,OAAW,QAAO,KAAK;AACtC,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,OAAO,MAAM,CAAC;AACzD,UAAM,MAAM,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ,KAAK,MAAM,CAAC;AAGzD,QAAI,QAAQ,MAAO,QAAO,KAAK;AAM/B,WAAO,EAAE,GAAG,MAAM,QAAQ,YAAY,OAAO,CAAC,OAAO,GAAG,EAAE;AAAA,EAC5D;AAEA,MAAI,CAAC,QAAQ,WAAY,QAAO,KAAK;AACrC,QAAM,QAAQ,YAAY,QAAQ,YAAY,OAAO;AACrD,MAAI,CAAC,MAAO,QAAO,KAAK;AACxB,SAAO,EAAE,GAAG,MAAM,QAAQ,YAAY,MAAM;AAC9C;AAWO,SAAS,kBACd,YACA,SACqB;AACrB,QAAM,WAAW,WACd,IAAI,CAAC,cAAc,WAAW,WAAW,OAAO,CAAC,EACjD,KAAK,CAAC,GAAG,MAAM,WAAW,CAAC,IAAI,WAAW,CAAC,CAAC;AAE/C,QAAM,WAAW,oBAAI,IAAoB;AACzC,SAAO,SAAS,IAAI,CAAC,cAAc;AACjC,UAAM,OACJ,UAAU,WAAW,cAAc,SAAS,IAAI,UAAU,MAAM,KAAK,KAAK,IAAI;AAChF,QAAI,SAAS,OAAW,UAAS,IAAI,UAAU,QAAQ,IAAI;AAC3D,WAAO;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,MACP,QAAQ,QAAQ,YAAY,QAAQ,QAAQ,aAAa,UAAU;AAAA,IACrE;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import {
3
+ spanAt
4
+ } from "./chunk-5YE2MUS2.js";
5
+ import {
6
+ localizeRanges
7
+ } from "./chunk-HOQZ7OPH.js";
8
+
9
+ // src/adapters/pdf/pdf-text.ts
10
+ var PDF_ITEM_SEPARATOR = " ";
11
+ var PDF_PAGE_SEPARATOR = "\n\n";
12
+ function pageTextChunk(items) {
13
+ const spans = [];
14
+ const parts = [];
15
+ let cursor = 0;
16
+ items.forEach((item, index) => {
17
+ if (index > 0) cursor += PDF_ITEM_SEPARATOR.length;
18
+ parts.push(item.str);
19
+ spans.push({ start: cursor, end: cursor + item.str.length });
20
+ cursor += item.str.length;
21
+ });
22
+ return { text: parts.join(PDF_ITEM_SEPARATOR), spans };
23
+ }
24
+ var FALLBACK_ADVANCE = 0.5;
25
+ function rangeBoxes(spans, chunk, pageStart, marks) {
26
+ if (marks.ranges.length === 0) return [];
27
+ const boxes = [];
28
+ for (const span of spans) {
29
+ const item = chunk.spans[span.item];
30
+ if (!item || item.end === item.start) continue;
31
+ const local = localizeRanges(marks, pageStart + item.start, pageStart + item.end);
32
+ if (local.ranges.length === 0) continue;
33
+ const length = item.end - item.start;
34
+ const advance = span.width > 0 ? span.width / length : span.fontSize * FALLBACK_ADVANCE;
35
+ local.ranges.forEach(([from, to], index) => {
36
+ boxes.push({
37
+ left: span.left + advance * from,
38
+ top: span.top,
39
+ width: advance * (to - from),
40
+ height: span.fontSize,
41
+ active: index === local.activeIndex
42
+ });
43
+ });
44
+ }
45
+ return boxes;
46
+ }
47
+ function rectBoxes(highlights, size) {
48
+ const boxes = [];
49
+ for (const highlight of highlights) {
50
+ for (const rect of highlight.rects ?? []) {
51
+ boxes.push({
52
+ left: clampFraction(rect.x) * size.width,
53
+ top: clampFraction(rect.y) * size.height,
54
+ width: clampFraction(rect.width) * size.width,
55
+ height: clampFraction(rect.height) * size.height,
56
+ active: highlight.active
57
+ });
58
+ }
59
+ }
60
+ return boxes;
61
+ }
62
+ function clampFraction(value) {
63
+ if (!Number.isFinite(value)) return 0;
64
+ return Math.max(0, Math.min(1, value));
65
+ }
66
+ function pageOfHighlight(highlight, index) {
67
+ if (!highlight || highlight.status !== "resolved") return void 0;
68
+ if (highlight.page !== void 0) return highlight.page;
69
+ if (!highlight.range || !index) return void 0;
70
+ const span = spanAt(index, highlight.range[0]);
71
+ return typeof span?.ref === "number" ? span.ref : void 0;
72
+ }
73
+
74
+ export {
75
+ PDF_ITEM_SEPARATOR,
76
+ PDF_PAGE_SEPARATOR,
77
+ pageTextChunk,
78
+ rangeBoxes,
79
+ rectBoxes,
80
+ pageOfHighlight
81
+ };
82
+ //# sourceMappingURL=chunk-G27UJZWH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/pdf/pdf-text.ts"],"sourcesContent":["/**\n * PDF text ↔ page geometry — the MAP half of the highlight funnel for a raster.\n *\n * A PDF page is pixels. There is no DOM text to wrap in a `<mark>`, so the paint\n * step here is a layer of BOXES over the canvas, and this module is what turns\n * \"characters 812–847 of the projection\" into those boxes.\n *\n * ## Why the projection is rebuilt rather than remembered\n *\n * `AdapterDocument.text` is assembled at LOAD time, one page at a time; the\n * renderer re-fetches the current page's text content anyway (it needs the same\n * items to position the selectable text layer). So both sides call\n * {@link pageTextChunk}, and the projection and the geometry agree by\n * construction instead of by two hand-kept copies of the same join rule.\n *\n * ## Why the boxes are proportional, and what that costs\n *\n * pdf.js reports one box per text ITEM — a run of glyphs — and no per-character\n * geometry. When a range covers only part of an item, the box is cut\n * proportionally by character count. That is a linear approximation of a\n * proportional font, so it can be off by a glyph or two inside a long run; PDF\n * items are usually a word or a line fragment, where the error is invisible.\n *\n * The alternative — DOM ranges over the transparent text layer — would be WORSE\n * here, not better: that layer renders in a generic font at pdf.js's transform,\n * so its glyph advances do not match the raster underneath it at all. The item\n * box comes from the file itself and does.\n */\n\nimport type { DocumentRect } from \"@elabs-ai/components-ui\";\n\nimport type { ResolvedHighlight } from \"../../core/highlight\";\nimport { localizeRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\n\n/** Written between two text items of the same page. */\nexport const PDF_ITEM_SEPARATOR = \" \";\n\n/** Written between two pages of the projection. */\nexport const PDF_PAGE_SEPARATOR = \"\\n\\n\";\n\n/** The part of a pdf.js text item this module needs. */\nexport interface PdfTextItemLike {\n str: string;\n}\n\n/** Half-open `[start, end)` into the page's own chunk, one per item. */\nexport interface PdfItemSpan {\n start: number;\n end: number;\n}\n\n/** One page's contribution to the projection, plus where each item landed in it. */\nexport interface PdfPageChunk {\n text: string;\n /** Index-aligned with the items passed in — including the empty ones. */\n spans: readonly PdfItemSpan[];\n}\n\n/**\n * Join a page's text items the way the projection does, and record each item's\n * offsets.\n *\n * Empty items keep their place: they still consume a separator, so an item's\n * index into this array is the same index pdf.js handed back. Dropping them\n * would shift every following offset by one and put every mark a character off.\n */\nexport function pageTextChunk(items: readonly PdfTextItemLike[]): PdfPageChunk {\n const spans: PdfItemSpan[] = [];\n const parts: string[] = [];\n let cursor = 0;\n\n items.forEach((item, index) => {\n if (index > 0) cursor += PDF_ITEM_SEPARATOR.length;\n parts.push(item.str);\n spans.push({ start: cursor, end: cursor + item.str.length });\n cursor += item.str.length;\n });\n\n return { text: parts.join(PDF_ITEM_SEPARATOR), spans };\n}\n\n/** A positioned text item, in CSS pixels relative to the rendered page. */\nexport interface PdfTextSpan {\n text: string;\n left: number;\n top: number;\n /** The item's own advance width. `0` when pdf.js did not report one. */\n width: number;\n fontSize: number;\n /** Index into the page's original items — the seam back to the projection. */\n item: number;\n}\n\n/** A rectangle to paint over the page, in CSS pixels. */\nexport interface PdfHighlightBox {\n left: number;\n top: number;\n width: number;\n height: number;\n /** Whether this box is the highlight the viewer is currently pointed at. */\n active: boolean;\n}\n\n/**\n * Fallback advance per character when pdf.js reports no width.\n *\n * Half an em is the rough mean advance of Latin text; it only ever decides how\n * wide a box is drawn, never where the reader is taken.\n */\nconst FALLBACK_ADVANCE = 0.5;\n\n/**\n * Boxes for every marked range that falls on this page.\n *\n * `pageStart` is where this page's chunk begins in the projection, which is what\n * makes a whole-document offset comparable with a per-page item offset.\n */\nexport function rangeBoxes(\n spans: readonly PdfTextSpan[],\n chunk: PdfPageChunk,\n pageStart: number,\n marks: MarkRanges,\n): PdfHighlightBox[] {\n if (marks.ranges.length === 0) return [];\n\n const boxes: PdfHighlightBox[] = [];\n for (const span of spans) {\n const item = chunk.spans[span.item];\n if (!item || item.end === item.start) continue;\n\n const local = localizeRanges(marks, pageStart + item.start, pageStart + item.end);\n if (local.ranges.length === 0) continue;\n\n const length = item.end - item.start;\n const advance = span.width > 0 ? span.width / length : span.fontSize * FALLBACK_ADVANCE;\n local.ranges.forEach(([from, to], index) => {\n boxes.push({\n left: span.left + advance * from,\n top: span.top,\n width: advance * (to - from),\n height: span.fontSize,\n active: index === local.activeIndex,\n });\n });\n }\n return boxes;\n}\n\n/**\n * Boxes for `rect` addresses, whose geometry is already the answer.\n *\n * Fractions rather than pixels is what makes them survive zoom and a resize: the\n * same address is correct at 50% and at 300% because it is multiplied by the\n * page's CURRENT rendered size every time.\n */\nexport function rectBoxes(\n highlights: readonly ResolvedHighlight[],\n size: { width: number; height: number },\n): PdfHighlightBox[] {\n const boxes: PdfHighlightBox[] = [];\n for (const highlight of highlights) {\n for (const rect of highlight.rects ?? []) {\n boxes.push({\n left: clampFraction(rect.x) * size.width,\n top: clampFraction(rect.y) * size.height,\n width: clampFraction(rect.width) * size.width,\n height: clampFraction(rect.height) * size.height,\n active: highlight.active,\n });\n }\n }\n return boxes;\n}\n\n/** Keep a foreign document's geometry inside the page it claims to be on. */\nfunction clampFraction(value: DocumentRect[\"x\"]): number {\n if (!Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\n/**\n * Which page a highlight is on, so activating it can turn to that page.\n *\n * A `rect` carries its own page — geometry has no range to look up. Everything\n * else is looked up through the document's own index, because the ref type is the\n * adapter's business and this adapter's is the page number. A caller's page HINT\n * is deliberately not consulted: it is the resolver's job to leave it off\n * (`ResolvedHighlight.page`), since a stale hint would turn to a page the mark is\n * not on. A range landing on a page separator, or past the extraction cap, has no\n * page and the pager stays put rather than jumping somewhere arbitrary.\n */\nexport function pageOfHighlight(\n highlight: ResolvedHighlight | undefined,\n index: TextIndex<unknown> | undefined,\n): number | undefined {\n if (!highlight || highlight.status !== \"resolved\") return undefined;\n if (highlight.page !== undefined) return highlight.page;\n if (!highlight.range || !index) return undefined;\n const span = spanAt(index, highlight.range[0]);\n return typeof span?.ref === \"number\" ? span.ref : undefined;\n}\n"],"mappings":";;;;;;;;;AAoCO,IAAM,qBAAqB;AAG3B,IAAM,qBAAqB;AA4B3B,SAAS,cAAc,OAAiD;AAC7E,QAAM,QAAuB,CAAC;AAC9B,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAS;AAEb,QAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,QAAI,QAAQ,EAAG,WAAU,mBAAmB;AAC5C,UAAM,KAAK,KAAK,GAAG;AACnB,UAAM,KAAK,EAAE,OAAO,QAAQ,KAAK,SAAS,KAAK,IAAI,OAAO,CAAC;AAC3D,cAAU,KAAK,IAAI;AAAA,EACrB,CAAC;AAED,SAAO,EAAE,MAAM,MAAM,KAAK,kBAAkB,GAAG,MAAM;AACvD;AA8BA,IAAM,mBAAmB;AAQlB,SAAS,WACd,OACA,OACA,WACA,OACmB;AACnB,MAAI,MAAM,OAAO,WAAW,EAAG,QAAO,CAAC;AAEvC,QAAM,QAA2B,CAAC;AAClC,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,MAAM,MAAM,KAAK,IAAI;AAClC,QAAI,CAAC,QAAQ,KAAK,QAAQ,KAAK,MAAO;AAEtC,UAAM,QAAQ,eAAe,OAAO,YAAY,KAAK,OAAO,YAAY,KAAK,GAAG;AAChF,QAAI,MAAM,OAAO,WAAW,EAAG;AAE/B,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,UAAM,UAAU,KAAK,QAAQ,IAAI,KAAK,QAAQ,SAAS,KAAK,WAAW;AACvE,UAAM,OAAO,QAAQ,CAAC,CAAC,MAAM,EAAE,GAAG,UAAU;AAC1C,YAAM,KAAK;AAAA,QACT,MAAM,KAAK,OAAO,UAAU;AAAA,QAC5B,KAAK,KAAK;AAAA,QACV,OAAO,WAAW,KAAK;AAAA,QACvB,QAAQ,KAAK;AAAA,QACb,QAAQ,UAAU,MAAM;AAAA,MAC1B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,SAAO;AACT;AASO,SAAS,UACd,YACA,MACmB;AACnB,QAAM,QAA2B,CAAC;AAClC,aAAW,aAAa,YAAY;AAClC,eAAW,QAAQ,UAAU,SAAS,CAAC,GAAG;AACxC,YAAM,KAAK;AAAA,QACT,MAAM,cAAc,KAAK,CAAC,IAAI,KAAK;AAAA,QACnC,KAAK,cAAc,KAAK,CAAC,IAAI,KAAK;AAAA,QAClC,OAAO,cAAc,KAAK,KAAK,IAAI,KAAK;AAAA,QACxC,QAAQ,cAAc,KAAK,MAAM,IAAI,KAAK;AAAA,QAC1C,QAAQ,UAAU;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,cAAc,OAAkC;AACvD,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAaO,SAAS,gBACd,WACA,OACoB;AACpB,MAAI,CAAC,aAAa,UAAU,WAAW,WAAY,QAAO;AAC1D,MAAI,UAAU,SAAS,OAAW,QAAO,UAAU;AACnD,MAAI,CAAC,UAAU,SAAS,CAAC,MAAO,QAAO;AACvC,QAAM,OAAO,OAAO,OAAO,UAAU,MAAM,CAAC,CAAC;AAC7C,SAAO,OAAO,MAAM,QAAQ,WAAW,KAAK,MAAM;AACpD;","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
- } from "./chunk-AORNMH77.js";
4
+ } from "./chunk-CIT2HHRI.js";
5
5
 
6
6
  // src/adapters/json/json-manifest.ts
7
7
  var jsonManifest = {
@@ -15,4 +15,4 @@ var jsonManifest = {
15
15
  export {
16
16
  jsonManifest
17
17
  };
18
- //# sourceMappingURL=chunk-BCF3RWRL.js.map
18
+ //# sourceMappingURL=chunk-GH4UBYST.js.map
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  localizeRanges
4
- } from "./chunk-VO2273Z2.js";
4
+ } from "./chunk-HOQZ7OPH.js";
5
5
 
6
6
  // src/components/marked-text.tsx
7
7
  import { MatchHighlight } from "@elabs-ai/components-ui";
@@ -15,4 +15,4 @@ function MarkedText({ text, marks, start }) {
15
15
  export {
16
16
  MarkedText
17
17
  };
18
- //# sourceMappingURL=chunk-2NQ4RSJ3.js.map
18
+ //# sourceMappingURL=chunk-GNBD53AT.js.map
@@ -32,24 +32,8 @@ function localizeRanges({ ranges, activeIndex }, start, end) {
32
32
  return local.length === 0 ? EMPTY : { ranges: local, activeIndex: localActive };
33
33
  }
34
34
 
35
- // src/core/use-highlight-scroll.ts
36
- import { useEffect } from "react";
37
- import { useReducedMotion } from "@elabs-ai/components-tokens";
38
- var ACTIVE_HIGHLIGHT_SELECTOR = '[data-slot="match-highlight-mark"][data-active],[data-slot="highlight-rect"][data-active],[data-slot="highlight-block"][data-active]';
39
- function useScrollActiveHighlightIntoView(containerRef, activeHighlightId, paintKey) {
40
- const reducedMotion = useReducedMotion();
41
- useEffect(() => {
42
- if (!activeHighlightId) return;
43
- const target = containerRef.current?.querySelector(ACTIVE_HIGHLIGHT_SELECTOR);
44
- if (!(target instanceof HTMLElement) || typeof target.scrollIntoView !== "function") return;
45
- target.scrollIntoView({ block: "center", behavior: reducedMotion ? "auto" : "smooth" });
46
- }, [containerRef, activeHighlightId, paintKey, reducedMotion]);
47
- }
48
-
49
35
  export {
50
36
  toMarkRanges,
51
- localizeRanges,
52
- ACTIVE_HIGHLIGHT_SELECTOR,
53
- useScrollActiveHighlightIntoView
37
+ localizeRanges
54
38
  };
55
- //# sourceMappingURL=chunk-VO2273Z2.js.map
39
+ //# sourceMappingURL=chunk-HOQZ7OPH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/highlight-marks.ts"],"sourcesContent":["/**\n * The MAP + PAINT half of the highlight funnel, for adapters whose document IS\n * its own text projection — plain text, code, and anything else that renders\n * `document.text` verbatim.\n *\n * The shell has already done LOCATE (every quote is now a character range). All\n * that is left is to hand those ranges to `MatchHighlight` and say which one is\n * current. An adapter with a richer model (pages, blocks, cells) maps the same\n * ranges through its own `textIndex` instead and does not use this.\n */\n\nimport { normalizeRanges, type MatchRange } from \"@elabs-ai/components-ui\";\n\nimport type { ResolvedHighlight } from \"./highlight\";\n\n/** What `MatchHighlight` takes: merged ranges, plus which merged mark is current. */\nexport interface MarkRanges {\n /** Clamped, sorted and merged — the same ranges `MatchHighlight` will paint. */\n readonly ranges: readonly MatchRange[];\n /** Index into {@link MarkRanges.ranges}, or `-1` when none is current. */\n readonly activeIndex: number;\n}\n\nconst EMPTY: MarkRanges = { ranges: [], activeIndex: -1 };\n\n/**\n * Fold resolved highlights into the ranges a `<mark>` layer paints.\n *\n * Merging happens HERE rather than being left to `MatchHighlight`, because the\n * two would otherwise disagree about what \"the third mark\" is: overlapping\n * citations collapse into one mark, and an `activeIndex` counted against the\n * unmerged list would then point at the wrong one — or past the end. The active\n * entry is located by which merged range CONTAINS its start, so it stays right\n * no matter how many requests collapsed into that mark.\n *\n * Non-`range` addresses and unresolved entries are skipped: an adapter paints\n * what it can and the shell is what tells the reader about the rest.\n */\nexport function toMarkRanges(\n highlights: readonly ResolvedHighlight[] | undefined,\n textLength: number,\n): MarkRanges {\n if (!highlights || highlights.length === 0) return EMPTY;\n\n const located = highlights.filter(\n (highlight): highlight is ResolvedHighlight & { range: MatchRange } =>\n highlight.status === \"resolved\" && highlight.range !== undefined,\n );\n if (located.length === 0) return EMPTY;\n\n const ranges = normalizeRanges(\n located.map((highlight) => highlight.range),\n textLength,\n );\n\n const active = located.find((highlight) => highlight.active);\n const activeStart = active?.range[0];\n const activeIndex =\n activeStart === undefined\n ? -1\n : ranges.findIndex(([start, end]) => activeStart >= start && activeStart < end);\n\n return { ranges, activeIndex };\n}\n\n/**\n * Re-express document ranges relative to one slice of the document — a code\n * token, a table cell, a block of prose.\n *\n * A renderer that draws its text in pieces cannot hand whole-document offsets to\n * a `<mark>` layer that only knows about the piece in front of it. Ranges are\n * clipped to the slice and rebased to 0; the ones that miss it entirely drop\n * out, which is also how a caller learns there is nothing to mark here (an empty\n * `ranges`). A range spanning several slices survives in each of them, so a\n * citation crossing a line break stays one visual run.\n */\nexport function localizeRanges(\n { ranges, activeIndex }: MarkRanges,\n start: number,\n end: number,\n): MarkRanges {\n if (ranges.length === 0 || start >= end) return EMPTY;\n\n const local: MatchRange[] = [];\n let localActive = -1;\n ranges.forEach(([from, to], index) => {\n const clippedFrom = Math.max(from, start);\n const clippedTo = Math.min(to, end);\n if (clippedFrom >= clippedTo) return;\n if (index === activeIndex) localActive = local.length;\n local.push([clippedFrom - start, clippedTo - start]);\n });\n\n return local.length === 0 ? EMPTY : { ranges: local, activeIndex: localActive };\n}\n"],"mappings":";;;AAWA,SAAS,uBAAwC;AAYjD,IAAM,QAAoB,EAAE,QAAQ,CAAC,GAAG,aAAa,GAAG;AAejD,SAAS,aACd,YACA,YACY;AACZ,MAAI,CAAC,cAAc,WAAW,WAAW,EAAG,QAAO;AAEnD,QAAM,UAAU,WAAW;AAAA,IACzB,CAAC,cACC,UAAU,WAAW,cAAc,UAAU,UAAU;AAAA,EAC3D;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,SAAS;AAAA,IACb,QAAQ,IAAI,CAAC,cAAc,UAAU,KAAK;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,KAAK,CAAC,cAAc,UAAU,MAAM;AAC3D,QAAM,cAAc,QAAQ,MAAM,CAAC;AACnC,QAAM,cACJ,gBAAgB,SACZ,KACA,OAAO,UAAU,CAAC,CAAC,OAAO,GAAG,MAAM,eAAe,SAAS,cAAc,GAAG;AAElF,SAAO,EAAE,QAAQ,YAAY;AAC/B;AAaO,SAAS,eACd,EAAE,QAAQ,YAAY,GACtB,OACA,KACY;AACZ,MAAI,OAAO,WAAW,KAAK,SAAS,IAAK,QAAO;AAEhD,QAAM,QAAsB,CAAC;AAC7B,MAAI,cAAc;AAClB,SAAO,QAAQ,CAAC,CAAC,MAAM,EAAE,GAAG,UAAU;AACpC,UAAM,cAAc,KAAK,IAAI,MAAM,KAAK;AACxC,UAAM,YAAY,KAAK,IAAI,IAAI,GAAG;AAClC,QAAI,eAAe,UAAW;AAC9B,QAAI,UAAU,YAAa,eAAc,MAAM;AAC/C,UAAM,KAAK,CAAC,cAAc,OAAO,YAAY,KAAK,CAAC;AAAA,EACrD,CAAC;AAED,SAAO,MAAM,WAAW,IAAI,QAAQ,EAAE,QAAQ,OAAO,aAAa,YAAY;AAChF;","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
- } from "./chunk-AORNMH77.js";
4
+ } from "./chunk-CIT2HHRI.js";
5
5
 
6
6
  // src/adapters/image/image-manifest.ts
7
7
  var imageManifest = {
@@ -15,4 +15,4 @@ var imageManifest = {
15
15
  export {
16
16
  imageManifest
17
17
  };
18
- //# sourceMappingURL=chunk-GGR7BNW4.js.map
18
+ //# sourceMappingURL=chunk-HUUJIFFT.js.map
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  PROTOCOL_VERSION
4
- } from "./chunk-AORNMH77.js";
4
+ } from "./chunk-CIT2HHRI.js";
5
5
 
6
6
  // src/adapters/docx/docx-manifest.ts
7
7
  var docxManifest = {
@@ -19,4 +19,4 @@ var docxManifest = {
19
19
  export {
20
20
  docxManifest
21
21
  };
22
- //# sourceMappingURL=chunk-FELIAGKI.js.map
22
+ //# sourceMappingURL=chunk-I5XD2NJ6.js.map
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ // src/core/use-highlight-scroll.ts
4
+ import { useEffect } from "react";
5
+ import { useReducedMotion } from "@elabs-ai/components-tokens";
6
+ var ACTIVE_HIGHLIGHT_SELECTOR = '[data-slot="match-highlight-mark"][data-active],[data-slot="highlight-rect"][data-active],[data-slot="highlight-block"][data-active]';
7
+ function useScrollActiveHighlightIntoView(containerRef, activeHighlightId, paintKey) {
8
+ const reducedMotion = useReducedMotion();
9
+ useEffect(() => {
10
+ if (!activeHighlightId) return;
11
+ const target = containerRef.current?.querySelector(ACTIVE_HIGHLIGHT_SELECTOR);
12
+ if (!(target instanceof HTMLElement) || typeof target.scrollIntoView !== "function") return;
13
+ target.scrollIntoView({ block: "center", behavior: reducedMotion ? "auto" : "smooth" });
14
+ }, [containerRef, activeHighlightId, paintKey, reducedMotion]);
15
+ }
16
+
17
+ export {
18
+ ACTIVE_HIGHLIGHT_SELECTOR,
19
+ useScrollActiveHighlightIntoView
20
+ };
21
+ //# sourceMappingURL=chunk-IB4N72FA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/use-highlight-scroll.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Bring the current highlight into view.\n *\n * Pointing at a passage is only half the job — a mark 4,000 lines down that\n * nobody scrolls to is the same as no mark at all. Every text-ish renderer needs\n * this, so it is written once here rather than per adapter.\n *\n * The current highlight is found by the stable `data-slot` selector its painter\n * emits, not by a ref: both painters produce their elements from a list — marks\n * inside `MatchHighlight`, boxes inside a page overlay — so the renderer has no\n * handle on the individual element and should not grow one just for scrolling.\n */\n\nimport { useEffect, type RefObject } from \"react\";\n\nimport { useReducedMotion } from \"@elabs-ai/components-tokens\";\n\n/**\n * How the current highlight identifies itself in the DOM, whichever way it was\n * painted: a `<mark>` in flowing text, a box over a page raster, or a whole\n * block plated because the format can only be addressed that coarsely.\n */\nexport const ACTIVE_HIGHLIGHT_SELECTOR =\n '[data-slot=\"match-highlight-mark\"][data-active],[data-slot=\"highlight-rect\"][data-active],' +\n '[data-slot=\"highlight-block\"][data-active]';\n\n/**\n * Scroll the current highlight inside `containerRef` into view whenever the\n * viewer is pointed somewhere new.\n *\n * `block: \"center\"` rather than `\"nearest\"`: a cited passage should land where a\n * reader looks, with its surrounding context visible, not flush against the top\n * edge of the pane. Smooth scrolling is skipped under a reduced-motion\n * preference — an imperative scroll is invisible to the CSS media query, so the\n * check has to be explicit.\n *\n * `paintKey` is anything whose change means the highlights were repainted — a\n * page's box list, a re-tokenized document. A renderer that paints\n * asynchronously (a PDF fetches the page's text after the raster) has no element\n * to scroll to at the moment the active id changes, so it passes the thing it\n * was waiting for and the scroll runs once that arrives.\n */\nexport function useScrollActiveHighlightIntoView(\n containerRef: RefObject<HTMLElement | null>,\n activeHighlightId: string | null | undefined,\n paintKey?: unknown,\n): void {\n const reducedMotion = useReducedMotion();\n\n useEffect(() => {\n if (!activeHighlightId) return;\n const target = containerRef.current?.querySelector(ACTIVE_HIGHLIGHT_SELECTOR);\n // jsdom ships no `scrollIntoView`; a viewer must not throw in a test run.\n if (!(target instanceof HTMLElement) || typeof target.scrollIntoView !== \"function\") return;\n target.scrollIntoView({ block: \"center\", behavior: reducedMotion ? \"auto\" : \"smooth\" });\n }, [containerRef, activeHighlightId, paintKey, reducedMotion]);\n}\n"],"mappings":";;;AAeA,SAAS,iBAAiC;AAE1C,SAAS,wBAAwB;AAO1B,IAAM,4BACX;AAmBK,SAAS,iCACd,cACA,mBACA,UACM;AACN,QAAM,gBAAgB,iBAAiB;AAEvC,YAAU,MAAM;AACd,QAAI,CAAC,kBAAmB;AACxB,UAAM,SAAS,aAAa,SAAS,cAAc,yBAAyB;AAE5E,QAAI,EAAE,kBAAkB,gBAAgB,OAAO,OAAO,mBAAmB,WAAY;AACrF,WAAO,eAAe,EAAE,OAAO,UAAU,UAAU,gBAAgB,SAAS,SAAS,CAAC;AAAA,EACxF,GAAG,CAAC,cAAc,mBAAmB,UAAU,aAAa,CAAC;AAC/D;","names":[]}
@@ -1,47 +1,11 @@
1
1
  "use client";
2
-
3
- // src/core/use-page-control.ts
4
- import { useCallback, useState } from "react";
5
- function usePageControl(pageNumber, onPageChange, pageCount) {
6
- const controlled = pageNumber !== void 0;
7
- const [own, setOwn] = useState(1);
8
- const raw = controlled ? pageNumber : own;
9
- const current = pageCount > 0 ? Math.min(Math.max(1, raw), pageCount) : Math.max(1, raw);
10
- const goToPage = useCallback(
11
- (page) => {
12
- const next = pageCount > 0 ? Math.min(Math.max(1, page), pageCount) : Math.max(1, page);
13
- if (!controlled) setOwn(next);
14
- onPageChange?.(next);
15
- },
16
- [controlled, onPageChange, pageCount]
17
- );
18
- return [current, goToPage];
19
- }
2
+ import {
3
+ findScrollHost
4
+ } from "./chunk-7PGGKQVY.js";
20
5
 
21
6
  // src/core/use-paged-scroll.ts
22
7
  import { useVirtualizer } from "@tanstack/react-virtual";
23
- import { useEffect, useLayoutEffect, useRef, useState as useState2 } from "react";
24
-
25
- // src/core/scroll-host.ts
26
- var SCROLL_HOST_SELECTOR = '[data-slot="file-viewer-content"]';
27
- function findScrollHost(node) {
28
- if (!node) return null;
29
- const shell = node.closest(SCROLL_HOST_SELECTOR);
30
- if (shell) return shell;
31
- const view = node.ownerDocument?.defaultView;
32
- if (!view) return null;
33
- let current = node.parentElement;
34
- while (current) {
35
- const { overflowY, overflowX } = view.getComputedStyle(current);
36
- if (/(auto|scroll|overlay)/.test(overflowY) || /(auto|scroll|overlay)/.test(overflowX)) {
37
- return current;
38
- }
39
- current = current.parentElement;
40
- }
41
- return null;
42
- }
43
-
44
- // src/core/use-paged-scroll.ts
8
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
45
9
  var EDGE_SLACK = 8;
46
10
  function usePagedScroll({
47
11
  count,
@@ -51,8 +15,8 @@ function usePagedScroll({
51
15
  estimateSize,
52
16
  sizeKey
53
17
  }) {
54
- const [host, setHost] = useState2(null);
55
- const [scrollMargin, setScrollMargin] = useState2(0);
18
+ const [host, setHost] = useState(null);
19
+ const [scrollMargin, setScrollMargin] = useState(0);
56
20
  useEffect(() => {
57
21
  const list = listRef.current;
58
22
  const found = findScrollHost(list) ?? list?.parentElement ?? null;
@@ -117,31 +81,7 @@ function usePagedScroll({
117
81
  return virtualizer;
118
82
  }
119
83
 
120
- // src/core/use-viewport-size.ts
121
- import { useEffect as useEffect2, useState as useState3 } from "react";
122
- function useViewportSize(ref) {
123
- const [size, setSize] = useState3();
124
- useEffect2(() => {
125
- const host = findScrollHost(ref.current) ?? ref.current?.parentElement ?? ref.current;
126
- if (!host || typeof ResizeObserver === "undefined") return;
127
- const measure = () => {
128
- const { clientWidth: width, clientHeight: height } = host;
129
- if (width === 0 || height === 0) return;
130
- setSize(
131
- (current) => current && Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : { width, height }
132
- );
133
- };
134
- measure();
135
- const observer = new ResizeObserver(measure);
136
- observer.observe(host);
137
- return () => observer.disconnect();
138
- }, [ref]);
139
- return size;
140
- }
141
-
142
84
  export {
143
- usePageControl,
144
- usePagedScroll,
145
- useViewportSize
85
+ usePagedScroll
146
86
  };
147
- //# sourceMappingURL=chunk-4OFQYALG.js.map
87
+ //# sourceMappingURL=chunk-IQ6Z4X3K.js.map