@elabs-ai/components-viewer 4.2.0 → 5.0.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
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/file-viewer/file-viewer.tsx","../src/core/registry.ts","../src/adapters/index.ts","../src/core/highlight.ts","../src/core/highlight-resolve.ts","../src/file-viewer/file-viewer-find.tsx","../src/file-viewer/file-viewer-context.tsx","../src/file-viewer/file-viewer-pager.tsx","../src/file-viewer/file-viewer-zoom.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `FileViewer` — the compound shell.\n *\n * Every part is composed from `@elabs-ai/components-ui` primitives;\n * this package contributes file LOGIC (detection, parsing, the page model) and\n * never a parallel widget set. There is no `showToolbar` boolean: compose the\n * parts you want, or render `<FileViewer>` for the batteries-included default.\n *\n * Surface separation (`styling-and-tokens.md`): the frame is one raised `card`\n * with a border, the toolbar is divided from the content by a single `border-b`\n * — the sole structural cue between two same-fill regions, so it takes the\n * strong rung.\n */\n\nimport {\n Button,\n cn,\n downloadBlob,\n fileIconFor,\n IconButton,\n normalizeFileSource,\n normalizeQuoteTextWithOffsets,\n queryToRanges,\n resolveFileKind,\n Separator,\n Skeleton,\n StatePanel,\n Text,\n useLocale,\n type FileSource,\n type ProseHeadingLevel,\n} from \"@elabs-ai/components-ui\";\nimport { DownloadIcon, EyeOffIcon, SearchXIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useCallback,\n useDeferredValue,\n useEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { createDefaultRegistry } from \"../adapters\";\nimport {\n ViewerError,\n isAbort,\n isModuleNotFound,\n parserMissingError,\n toViewerError,\n type ViewerErrorCode,\n} from \"../core/errors\";\nimport {\n FIND_MATCH_LIMIT,\n findMatchId,\n type DocumentHighlight,\n type HighlightSupport,\n type ResolvedHighlight,\n} from \"../core/highlight\";\nimport { resolveHighlights } from \"../core/highlight-resolve\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type { DocumentRotation, ZoomLevel } from \"../core/types\";\nimport { DEFAULT_ZOOM, stepZoom } from \"../core/zoom\";\nimport { FileViewerFind, isFindShortcut } from \"./file-viewer-find\";\nimport { FileViewerPager } from \"./file-viewer-pager\";\nimport { FileViewerRotate, FileViewerZoom } from \"./file-viewer-zoom\";\nimport {\n FileViewerContext,\n useFileViewer,\n type FileViewerContextValue,\n type FileViewerFindState,\n type FileViewerLoadState,\n} from \"./file-viewer-context\";\n\n/** Stable empties, so a provider with no citations does not re-render on identity. */\nconst NO_HIGHLIGHTS: readonly DocumentHighlight[] = [];\nconst NO_RESOLVED: readonly ResolvedHighlight[] = [];\nconst NO_SUPPORT: HighlightSupport = [];\n\n/* -------------------------------------------------------------------------- */\n/* Provider */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProviderProps {\n /** The file to show. `undefined` is the empty state, not an error. */\n source?: FileSource;\n /**\n * Adapters available to this viewer. Defaults to the built-ins.\n * Pass your own to add a format, drop one, or override a built-in.\n */\n registry?: ViewerRegistry;\n /**\n * Force the not-ready state while a parent fetches the source itself.\n * ORed with the viewer's own loading — a parent can add loading, never remove it.\n */\n loading?: boolean;\n /**\n * The rung a viewed document's own top-level heading renders at. Default `2`.\n *\n * A file carries its OWN heading tree, and that tree is only correct relative\n * to the page hosting it: a README's `#` rendered as an `<h1>` inside an app\n * that already has one puts two `h1`s in a screen reader's flat heading list,\n * and the frame's `<section aria-label>` does not fix that — most screen\n * readers list headings flat, not per landmark. The default assumes the\n * common case, a viewer embedded BELOW the page's own heading; pass `1` when\n * the viewer genuinely is the page.\n *\n * Adapters that render no headings ignore it. Same seam as\n * `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.\n */\n baseHeadingLevel?: ProseHeadingLevel;\n /**\n * The parts of the document to point at — an answer's citations, a search\n * result's context, anything the app already knows about the file.\n *\n * A PROP rather than provider-only state because citations originate outside\n * the viewer entirely: the chat pane that produced them usually lives in\n * another route, and it owns which one the reader clicked. Pass\n * `defaultHighlights` instead to let the viewer own them.\n */\n highlights?: readonly DocumentHighlight[];\n /** Uncontrolled initial citations. Ignored when `highlights` is supplied. */\n defaultHighlights?: readonly DocumentHighlight[];\n onHighlightsChange?: (highlights: readonly DocumentHighlight[]) => void;\n /**\n * Which citation the viewer is pointed at. `null` is \"none\" explicitly;\n * `undefined` is what selects uncontrolled mode, so the two are not\n * interchangeable here.\n */\n activeHighlightId?: string | null;\n defaultActiveHighlightId?: string | null;\n onActiveHighlightChange?: (id: string | null) => void;\n /**\n * Which page the viewer is on, 1-based — controlled.\n *\n * A trio (`component-api.md`) rather than provider-only state because the page\n * is routinely something the APP owns: a deep link to page 7, a URL the reader\n * can share, a position restored from a \"continue reading\" record. Clamped to\n * the document on read, so an out-of-range value degrades to the nearest real\n * page instead of blanking the canvas.\n */\n pageNumber?: number;\n /** Uncontrolled initial page. Ignored when `pageNumber` is supplied. */\n defaultPageNumber?: number;\n onPageNumberChange?: (page: number) => void;\n /**\n * The scale to draw at, or a fit mode — controlled. Persisting a reader's\n * preferred zoom across files and sessions is the reason this is a prop.\n */\n zoom?: ZoomLevel;\n /**\n * Uncontrolled initial zoom. Ignored when `zoom` is supplied.\n *\n * Defaults to `\"fit-width\"`, not `1`: a viewer's job on open is to show the\n * document, and a 4000px scan or an A4 page at 100% in a 600px pane shows its\n * top-left corner. Every reader-facing PDF viewer opens fitted for the same\n * reason. Pass `1` for true 100%.\n */\n defaultZoom?: ZoomLevel;\n onZoomChange?: (zoom: ZoomLevel) => void;\n /** Quarter-turns clockwise — controlled. */\n rotation?: DocumentRotation;\n /** Uncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`. */\n defaultRotation?: DocumentRotation;\n onRotationChange?: (rotation: DocumentRotation) => void;\n children: ReactNode;\n}\n\n/** Matches for the current query, plus whether the cap swallowed any. */\ninterface FindMatches {\n highlights: readonly DocumentHighlight[];\n total: number;\n truncated: boolean;\n}\n\nconst NO_MATCHES: FindMatches = { highlights: NO_HIGHLIGHTS, total: 0, truncated: false };\n\nexport function FileViewerProvider({\n source,\n registry: registryProp,\n loading = false,\n baseHeadingLevel = 2,\n highlights: highlightsProp,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId: activeHighlightIdProp,\n defaultActiveHighlightId = null,\n onActiveHighlightChange,\n pageNumber: pageNumberProp,\n defaultPageNumber = 1,\n onPageNumberChange,\n zoom: zoomProp,\n defaultZoom = \"fit-width\",\n onZoomChange,\n rotation: rotationProp,\n defaultRotation = 0,\n onRotationChange,\n children,\n}: FileViewerProviderProps) {\n // A default registry per provider, not per module: one screen's `register()`\n // override must not leak into another's.\n const fallbackRegistry = useMemo(() => createDefaultRegistry(), []);\n const registry = registryProp ?? fallbackRegistry;\n\n const [attempt, setAttempt] = useState(0);\n const [state, setState] = useState<FileViewerLoadState>({ status: \"empty\", capabilities: {} });\n\n const resolved = useMemo(() => (source ? normalizeFileSource(source) : undefined), [source]);\n\n useEffect(() => {\n if (!resolved) {\n setState({ status: \"empty\", capabilities: {} });\n return;\n }\n\n const controller = new AbortController();\n let instance: { dispose?: () => void } | undefined;\n let cancelled = false;\n\n const kind = resolveFileKind(resolved.name, resolved.mediaType);\n const manifest = registry.detect(kind);\n\n // Capabilities come from the eager manifest, so the chrome is correct from\n // the first frame — before a single byte of parser is fetched.\n setState({\n status: \"loading\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n });\n\n void (async () => {\n try {\n if (!manifest) {\n throw new ViewerError(\"unsupported-format\", `No adapter can open \"${resolved.name}\".`, {\n fileName: resolved.name,\n });\n }\n const adapter = await registry.load(manifest.id);\n // A fresh instance per document — adapters may hold per-document state.\n const parser = adapter.create();\n instance = parser;\n const document = await parser.load(resolved, { signal: controller.signal });\n if (cancelled) return;\n setState({\n status: \"ready\",\n source: resolved,\n document,\n adapter,\n capabilities: manifest.capabilities ?? {},\n });\n } catch (error) {\n // A cancelled load is a superseded one — reporting it would flash an\n // error every time the user picks a different file.\n if (cancelled || isAbort(error)) return;\n // Every parser engine is reached by a dynamic `import()` INSIDE the\n // adapter's own `load()`, so \"the optional peer is not installed\" lands\n // here, not on `registry.load()` above. Falling through to `parse-failed`\n // would tell the reader their file is damaged and offer a retry that can\n // never succeed.\n const missingPeer =\n isModuleNotFound(error) && manifest\n ? parserMissingError(manifest.id, manifest.requires ?? [], {\n fileName: resolved.name,\n cause: error,\n })\n : undefined;\n setState({\n status: \"error\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n error: missingPeer ?? toViewerError(error, \"parse-failed\", { fileName: resolved.name }),\n });\n }\n })();\n\n return () => {\n cancelled = true;\n controller.abort();\n instance?.dispose?.();\n // Releases any object URL the source minted for this load.\n resolved.revoke();\n };\n }, [resolved, registry, attempt]);\n\n /* ---- Citations: a prop the app may control, or the viewer's own --------- */\n\n const highlightsControlled = highlightsProp !== undefined;\n const [ownHighlights, setOwnHighlights] = useState(defaultHighlights ?? NO_HIGHLIGHTS);\n const highlights = highlightsControlled ? highlightsProp : ownHighlights;\n const setHighlights = useCallback(\n (next: readonly DocumentHighlight[]) => {\n // Mirroring the platform: a controlled value is never written locally, so\n // the component can't drift from the prop that owns it.\n if (!highlightsControlled) setOwnHighlights(next);\n onHighlightsChange?.(next);\n },\n [highlightsControlled, onHighlightsChange],\n );\n\n const activeControlled = activeHighlightIdProp !== undefined;\n const [ownActiveId, setOwnActiveId] = useState<string | null>(defaultActiveHighlightId);\n const activeHighlightId = activeControlled ? activeHighlightIdProp : ownActiveId;\n const setActiveHighlight = useCallback(\n (id: string | null) => {\n if (!activeControlled) setOwnActiveId(id);\n onActiveHighlightChange?.(id);\n },\n [activeControlled, onActiveHighlightChange],\n );\n\n /* ---- View: which page, at what scale, turned which way ------------------ */\n\n const pageCount = state.document?.pageCount ?? 0;\n\n const pageControlled = pageNumberProp !== undefined;\n const [ownPage, setOwnPage] = useState(defaultPageNumber);\n const rawPage = pageControlled ? pageNumberProp : ownPage;\n // Clamped on READ. Writing a clamped value back at a controlled owner would\n // fight it, and a document whose page count shrank (a different file, same\n // provider) must not blank the canvas while the owner catches up.\n const pageNumber =\n pageCount > 0 ? Math.min(Math.max(1, rawPage), pageCount) : Math.max(1, rawPage);\n const goToPage = useCallback(\n (page: number) => {\n const next = Math.max(1, Math.round(page));\n if (!pageControlled) setOwnPage(next);\n onPageNumberChange?.(next);\n },\n [pageControlled, onPageNumberChange],\n );\n\n const zoomControlled = zoomProp !== undefined;\n const [ownZoom, setOwnZoom] = useState<ZoomLevel>(defaultZoom);\n const zoom = zoomControlled ? zoomProp : ownZoom;\n const setZoom = useCallback(\n (next: ZoomLevel) => {\n if (!zoomControlled) setOwnZoom(next);\n onZoomChange?.(next);\n },\n [zoomControlled, onZoomChange],\n );\n\n // What a fit mode actually became. Only the renderer can know it — it is the\n // one measuring its viewport — so this is a report, not a derivation.\n const [reportedZoom, setReportedZoom] = useState(DEFAULT_ZOOM);\n const effectiveZoom = typeof zoom === \"number\" ? zoom : reportedZoom;\n\n const rotationControlled = rotationProp !== undefined;\n const [ownRotation, setOwnRotation] = useState<DocumentRotation>(defaultRotation);\n const rotation = rotationControlled ? rotationProp : ownRotation;\n const setRotation = useCallback(\n (next: DocumentRotation) => {\n if (!rotationControlled) setOwnRotation(next);\n onRotationChange?.(next);\n },\n [rotationControlled, onRotationChange],\n );\n\n // Opening a DIFFERENT file starts at its first page, the right way up. Zoom\n // deliberately survives: a reader who zoomed in is reading at that size, and\n // snapping back to 100% on every file fights them.\n //\n // Compared against the previous source rather than run on mount, so a\n // `defaultPageNumber` (deep link, restored position) is not clobbered by its\n // own first render.\n const previousSource = useRef(resolved);\n useEffect(() => {\n if (previousSource.current === resolved) return;\n previousSource.current = resolved;\n setOwnPage(1);\n setOwnRotation(0);\n }, [resolved]);\n\n /* ---- Find-in-document: entirely the viewer's own ------------------------ */\n\n const [find, setFind] = useState({\n open: false,\n query: \"\",\n caseSensitive: false,\n activeIndex: 0,\n });\n\n // Whether a `FileViewerFind` part is actually composed into this viewer.\n //\n // The frame swallows Ctrl/Cmd+F, and swallowing it without a box to show is\n // strictly worse than not intercepting at all: the reader loses the browser's\n // own find and gets nothing in return. The parts are composable by design, so\n // \"the adapter could paint a match\" is not the same question as \"there is\n // somewhere to type\" — the find part answers the second by registering itself.\n const [findParts, setFindParts] = useState(0);\n const registerFind = useCallback(() => {\n setFindParts((count) => count + 1);\n return () => setFindParts((count) => count - 1);\n }, []);\n\n const text = state.document?.text;\n const capabilities = state.capabilities;\n const support = capabilities.highlight ?? NO_SUPPORT;\n\n // The declared `search` flag is an override in the OFF direction only. Read\n // as the source of truth it would deny the find box to PDF — the format\n // readers expect it on most — purely because that manifest predates the\n // feature. Read as \"can this adapter paint what find produces\" it is right by\n // construction: find emits `range` addresses, so `range` support is the bar.\n const canFind = (capabilities.search ?? true) && text !== undefined && support.includes(\"range\");\n\n // Deferred so a keystroke paints immediately and the (potentially large)\n // match scan lands a frame later, instead of blocking the caret.\n const query = useDeferredValue(find.query);\n const findMatches = useMemo<FindMatches>(() => {\n if (!find.open || !canFind || text === undefined || query.length === 0) return NO_MATCHES;\n // Deliberately NOT run through `normalizeRanges`: it merges ADJACENT\n // ranges, which is right for a fuzzy matcher painting one contiguous mark\n // and wrong here — searching \"l\" in \"hello\" finds two matches, and merging\n // them into one would make the counter disagree with what a reader counts.\n // A single needle's matches never overlap, so there is nothing to merge.\n const ranges = queryToRanges(text, query, find.caseSensitive);\n return {\n highlights: ranges.slice(0, FIND_MATCH_LIMIT).map(([start, end], index) => ({\n id: findMatchId(index),\n address: { kind: \"range\" as const, start, end },\n source: \"search\" as const,\n })),\n total: ranges.length,\n truncated: ranges.length > FIND_MATCH_LIMIT,\n };\n }, [find.open, find.caseSensitive, canFind, text, query]);\n\n /* ---- Locate: the one step that runs outside the adapter ----------------- */\n\n // Folded once per document, not once per keystroke: find re-resolves on every\n // character, and folding a 2 MB projection each time is what turns a search\n // box into a stutter.\n const normalized = useMemo(\n () => (text === undefined ? undefined : normalizeQuoteTextWithOffsets(text)),\n [text],\n );\n\n // Two things can claim to be \"the current one\": a citation the app pointed at,\n // and the match the reader is stepping through. The precedence is stated once,\n // here — while the find box is open and matching, find wins, because it is what\n // the reader's own keystrokes are moving. Leaving both active would paint the\n // reader's match like every other match while a citation kept the active plate.\n const activeFindId =\n find.open && findMatches.highlights.length > 0 ? findMatchId(find.activeIndex) : undefined;\n\n /**\n * The id the RENDERER is pointed at — the effective one, not the citation knob.\n *\n * A renderer that owns a pager or a tab strip navigates on this, and the shared\n * scroll hook fires on it. Handing it `state.activeHighlightId` instead would\n * leave find-in-document unable to scroll to, page to, or switch sheet to its\n * own match whenever no citation happened to be active.\n */\n const currentHighlightId = activeFindId ?? activeHighlightId;\n\n const resolvedHighlights = useMemo<readonly ResolvedHighlight[]>(() => {\n if (highlights.length === 0 && findMatches.highlights.length === 0) return NO_RESOLVED;\n return resolveHighlights([...highlights, ...findMatches.highlights], {\n normalized,\n textLength: text?.length,\n truncated: state.document?.textTruncated,\n supported: support,\n activeId: currentHighlightId,\n });\n }, [\n highlights,\n findMatches.highlights,\n normalized,\n text?.length,\n state.document?.textTruncated,\n support,\n currentHighlightId,\n ]);\n\n /* ---- Stepping ---------------------------------------------------------- */\n\n const stepCitation = useCallback(\n (delta: 1 | -1) => {\n const list = resolvedHighlights.filter(\n (highlight) => highlight.source === \"citation\" && highlight.status === \"resolved\",\n );\n if (list.length === 0) return;\n const at = list.findIndex((highlight) => highlight.id === activeHighlightId);\n // Nothing active yet: \"next\" starts at the top of the document and\n // \"previous\" at the bottom, rather than both landing on the first.\n const to =\n at === -1 ? (delta === 1 ? 0 : list.length - 1) : (at + delta + list.length) % list.length;\n setActiveHighlight((list[to] as ResolvedHighlight).id);\n },\n [resolvedHighlights, activeHighlightId, setActiveHighlight],\n );\n\n const stepFind = useCallback(\n (delta: 1 | -1) => {\n const count = findMatches.highlights.length;\n if (count === 0) return;\n setFind((current) => ({\n ...current,\n activeIndex: (current.activeIndex + delta + count) % count,\n }));\n },\n [findMatches.highlights.length],\n );\n\n const findState = useMemo<FileViewerFindState>(\n () => ({\n open: find.open,\n query: find.query,\n caseSensitive: find.caseSensitive,\n matches: findMatches.total,\n truncated: findMatches.truncated,\n activeIndex: find.activeIndex,\n }),\n [find, findMatches.total, findMatches.truncated],\n );\n\n const actions = useMemo(\n () => ({\n reload: () => setAttempt((n) => n + 1),\n setHighlights,\n setActiveHighlight,\n nextHighlight: () => stepCitation(1),\n previousHighlight: () => stepCitation(-1),\n openFind: () => setFind((current) => ({ ...current, open: true })),\n // The query survives a close, so re-opening resumes where the reader was;\n // only the box goes away.\n closeFind: () => setFind((current) => ({ ...current, open: false })),\n setFindQuery: (next: string) =>\n setFind((current) => ({ ...current, query: next, activeIndex: 0 })),\n setFindCaseSensitive: (next: boolean) =>\n setFind((current) => ({ ...current, caseSensitive: next, activeIndex: 0 })),\n nextFindMatch: () => stepFind(1),\n previousFindMatch: () => stepFind(-1),\n goToPage,\n // Stop at the ends rather than wrap: a document is not a carousel, and a\n // reader who holds \"next\" past the last page expects to stay there.\n nextPage: () => goToPage(Math.min(pageNumber + 1, pageCount || pageNumber + 1)),\n previousPage: () => goToPage(pageNumber - 1),\n setZoom,\n // Stepping from `effectiveZoom`, not from `zoom`: after a fit the ladder\n // has to continue from what is actually on screen.\n zoomIn: () => setZoom(stepZoom(effectiveZoom, 1)),\n zoomOut: () => setZoom(stepZoom(effectiveZoom, -1)),\n setRotation,\n rotate: (quarterTurns: 1 | -1) =>\n setRotation(((((rotation + quarterTurns * 90) % 360) + 360) % 360) as DocumentRotation),\n reportZoom: setReportedZoom,\n registerFind,\n }),\n [\n setHighlights,\n setActiveHighlight,\n stepCitation,\n stepFind,\n registerFind,\n goToPage,\n pageNumber,\n pageCount,\n setZoom,\n effectiveZoom,\n setRotation,\n rotation,\n ],\n );\n\n const value = useMemo<FileViewerContextValue>(\n () => ({\n state: {\n // A parent's `loading` can add the not-ready state but never clear a\n // real error — losing a failure to a stale prop is worse than a late\n // spinner.\n ...(loading && state.status !== \"error\" ? { ...state, status: \"loading\" as const } : state),\n highlights,\n activeHighlightId,\n find: findState,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n },\n actions,\n registry,\n meta: {\n baseHeadingLevel,\n resolvedHighlights,\n currentHighlightId,\n highlightSupport: support,\n canFind,\n hasFind: findParts > 0,\n },\n }),\n [\n state,\n loading,\n registry,\n baseHeadingLevel,\n highlights,\n activeHighlightId,\n findState,\n actions,\n resolvedHighlights,\n currentHighlightId,\n support,\n canFind,\n findParts,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n ],\n );\n\n return <FileViewerContext value={value}>{children}</FileViewerContext>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Frame */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerFrameProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The bordered surface the toolbar and content sit in, and the scope of the\n * viewer's find shortcut.\n *\n * Ctrl/Cmd+F is handled HERE rather than on `document`: a page may hold several\n * viewers, or a viewer beside an editor that has its own find, and a\n * document-level listener would let whichever mounted last win. Bound to the\n * frame, the shortcut belongs to whichever viewer the reader is actually inside,\n * and the browser's own find is untouched everywhere else on the page.\n */\nexport const FileViewerFrame = forwardRef<HTMLDivElement, FileViewerFrameProps>(\n function FileViewerFrame({ className, children, onKeyDown, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n const frame = useRef<HTMLElement | null>(null);\n const wasOpen = useRef(false);\n\n // Closing the box must hand the caret back, or a keyboard reader is left\n // with focus on nothing at the top of the document.\n const findOpen = state.find.open && meta.canFind;\n useEffect(() => {\n if (wasOpen.current && !findOpen) {\n frame.current\n ?.querySelector<HTMLElement>('[data-slot=\"file-viewer-content\"]')\n ?.focus({ preventScroll: true });\n }\n wasOpen.current = findOpen;\n }, [findOpen]);\n\n return (\n <section\n ref={(node) => {\n frame.current = node;\n if (typeof ref === \"function\") ref(node as HTMLDivElement | null);\n else if (ref) ref.current = node as HTMLDivElement | null;\n }}\n data-slot=\"file-viewer\"\n aria-label={t(\"viewer.label\")}\n className={cn(\n \"bg-card text-card-foreground border-border flex h-full min-h-0 flex-col overflow-hidden rounded-lg border shadow-sm\",\n className,\n )}\n onKeyDown={(event) => {\n // The frame renders a `<section>` while its public props type says\n // `HTMLDivElement` — a pre-existing signature. One cast here beats\n // widening the exported ref type and breaking every caller's\n // `useRef<HTMLDivElement>`.\n onKeyDown?.(event as ReactKeyboardEvent<HTMLDivElement>);\n // Only intercept the browser's shortcut when this viewer can actually\n // honour it — otherwise the reader loses their browser find and gets\n // nothing back. Both halves are needed: an adapter that can paint a\n // match (`canFind`) AND a find part composed in to type into\n // (`hasFind`). A hand-composed frame that omits the part keeps the\n // browser's own find, which is the right answer for it.\n if (!event.defaultPrevented && meta.canFind && meta.hasFind && isFindShortcut(event)) {\n event.preventDefault();\n actions.openFind();\n }\n }}\n {...props}\n >\n {children}\n </section>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Toolbar */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerToolbarProps extends HTMLAttributes<HTMLDivElement> {\n /** Extra controls, placed after the built-in actions. */\n actions?: ReactNode;\n}\n\n/**\n * The identity row: glyph, name, actions.\n *\n * No `role=\"toolbar\"` — that role promises roving-tabindex arrow-key navigation,\n * which this row does not implement. The same decision `ViewToolbar` made, and\n * the reason the P1 `Toolbar` primitive exists (ADR 0024 §5a).\n *\n * With no file it renders NOTHING: a row holding a generic glyph and a blank\n * name reads as a broken render, not as chrome. A screen that needs a permanent\n * header composes its own row around `FileViewerFrame` — that is what the parts\n * are for.\n */\nexport const FileViewerToolbar = forwardRef<HTMLDivElement, FileViewerToolbarProps>(\n function FileViewerToolbar({ className, actions, children, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const source = state.source;\n const Glyph = fileIconFor(source?.name ?? \"\", source?.mediaType);\n const [downloadFailed, setDownloadFailed] = useState(false);\n\n if (!source) return null;\n\n const download = () => {\n setDownloadFailed(false);\n void source\n .bytes()\n .then((bytes) => {\n downloadBlob(new Blob([bytes], { type: source.mediaType }), source.name);\n })\n .catch((error: unknown) => {\n console.error(\"FileViewer: download failed\", error);\n setDownloadFailed(true);\n });\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-toolbar\"\n className={cn(\n // The divider is the ONLY cue between toolbar and content (same fill,\n // no elevation change) — WCAG 1.4.11, so the strong rung.\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <Glyph aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n {/* min-w-0 is what actually lets the name truncate inside a flex row. */}\n <Text className=\"min-w-0 flex-1 truncate\" title={source.name}>\n {source.name}\n </Text>\n {children}\n {actions}\n {downloadFailed ? (\n <Text role=\"alert\" className=\"text-destructive-text shrink-0 truncate\">\n {t(\"viewer.error.readFailedBody\", { name: source.name, format: source.mediaType })}\n </Text>\n ) : null}\n <Separator orientation=\"vertical\" className=\"h-4\" />\n {/* `label` is IconButton's single source of truth — it becomes both the\n accessible name and the tooltip, so the two cannot drift. */}\n <IconButton\n variant=\"ghost\"\n label={t(\"viewer.download\", { name: source.name })}\n icon={<DownloadIcon aria-hidden=\"true\" />}\n onClick={download}\n />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Highlight status */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerHighlightStatusProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * \"We couldn't find that passage.\"\n *\n * A citation that fails to locate must not fail SILENTLY: the reader clicked a\n * source link and got a document that looks untouched, with no way to tell\n * whether the viewer is broken, the passage moved, or they mis-clicked. The\n * request survives resolution precisely so this line has something to say.\n *\n * Three different pieces of news, three sentences:\n * - `not-found` / `absent` — searched the whole projection; it is not in it.\n * - `not-found` / `truncated` — the projection is capped, so it may lie past it.\n * - `unsupported` — a CAPABILITY GAP: this build cannot point at part of that\n * format. Not the reader's fault and not retryable, same call the error panel\n * already makes for `unsupported-format`.\n *\n * Search misses are excluded: the find bar already counts its own matches, and\n * \"No matches\" there says it better than a second line here would.\n */\nexport const FileViewerHighlightStatus = forwardRef<HTMLDivElement, FileViewerHighlightStatusProps>(\n function FileViewerHighlightStatus({ className, ...props }, ref) {\n const { state, meta } = useFileViewer();\n const { t } = useLocale();\n\n const missed = meta.resolvedHighlights.filter(\n (highlight) =>\n highlight.source === \"citation\" &&\n (highlight.status === \"not-found\" || highlight.status === \"unsupported\"),\n );\n if (missed.length === 0) return null;\n\n const first = missed[0] as ResolvedHighlight;\n const message =\n first.status === \"unsupported\"\n ? t(\"viewer.highlight.unsupported\", {\n format: state.source?.extension.toUpperCase() || \"\",\n })\n : first.reason === \"truncated\"\n ? t(\"viewer.highlight.notFoundTruncated\")\n : t(\"viewer.highlight.notFound\");\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-highlight-status\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n // Information, not a failure — a neutral muted row, never the\n // destructive tone. The divider is the sole cue between it and the\n // content below (WCAG 1.4.11, strong rung).\n \"border-border-strong text-muted-foreground flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <SearchXIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"text-meta min-w-0 flex-1 truncate\">{message}</span>\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* States */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A layout-shaped skeleton, not a spinner: it occupies the box the real content\n * will, so nothing shifts when the file arrives (`loading-states.md`).\n * `aria-hidden` because `Skeleton` is decorative — the single live region on the\n * wrapper is what AT hears.\n */\nexport const FileViewerSkeleton = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerSkeleton({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-skeleton\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\"flex h-full flex-col gap-3 p-4\", className)}\n {...props}\n >\n <span className=\"sr-only\">{t(\"viewer.loading\", { name: state.source?.name ?? \"\" })}</span>\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-2/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-4/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-3/5\" />\n <Skeleton aria-hidden=\"true\" className=\"min-h-24 flex-1\" />\n </div>\n );\n },\n);\n\n/** Message keys per failure code — the code is the contract, the prose is not. */\nconst ERROR_MESSAGES: Record<ViewerErrorCode, { title: string; body: string }> = {\n \"unsupported-format\": {\n title: \"viewer.error.unsupportedFormat\",\n body: \"viewer.error.unsupportedFormatBody\",\n },\n \"parser-missing\": {\n title: \"viewer.error.parserMissing\",\n body: \"viewer.error.parserMissingBody\",\n },\n \"read-failed\": { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n \"parse-failed\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n // Neither should ever reach the UI: a protocol mismatch throws at registration\n // and an abort is swallowed as a superseded load. Mapped anyway so the panel\n // can never render an empty title.\n \"protocol-mismatch\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n aborted: { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n};\n\n/**\n * A gap in what this build can SHOW — the file is fine, we just cannot draw it.\n * Neither is retryable, and neither is the user's fault, so both are presented\n * as information rather than as a failure (see `FileViewerError`).\n */\nconst CAPABILITY_GAPS = new Set<ViewerErrorCode>([\"unsupported-format\", \"parser-missing\"]);\n\nexport const FileViewerError = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerError({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const error = state.error;\n if (!error) return null;\n\n const message = ERROR_MESSAGES[error.code];\n const vars = {\n name: state.source?.name ?? \"\",\n format: state.source?.extension.toUpperCase() || t(\"viewer.error.parseFailed\"),\n packages: error.packages?.join(\", \") ?? \"\",\n };\n\n // \"This build can't draw PDFs\" and \"the network dropped\" are different news.\n // A red alarm on the first one blames the reader for a capability we never\n // shipped, so the gap gets a neutral panel and `status`; a real failure keeps\n // the destructive panel and the `alert` StatePanel already sets.\n const isGap = CAPABILITY_GAPS.has(error.code);\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-error\"\n // min-h-full (not h-full) so a tall panel grows and scrolls instead of\n // being clipped at the top by `justify-center`.\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...(isGap ? { role: \"status\" as const, \"aria-live\": \"polite\" as const } : {})}\n {...props}\n >\n <StatePanel\n kind={isGap ? \"empty\" : \"error\"}\n // A dashed edge invites a drop; this panel accepts nothing. Solid.\n className={isGap ? \"border-solid\" : undefined}\n icon={isGap ? <EyeOffIcon aria-hidden=\"true\" /> : undefined}\n title={t(message.title)}\n description={t(message.body, vars)}\n // `unsupported-format` and `parser-missing` are not retryable — the\n // file has not changed and neither has what is installed. Offering\n // \"Try again\" there teaches users the button does nothing.\n actions={\n error.code === \"read-failed\" || error.code === \"parse-failed\" ? (\n <RetryButton />\n ) : undefined\n }\n />\n </div>\n );\n },\n);\n\n/**\n * The real `Button`, not a styled `<button>`.\n *\n * A hand-rolled `text-primary` link measured 4.14:1 against `StatePanel`'s error\n * wash and failed axe — the brand hue is tuned against `--background`/`--card`,\n * not against a tinted panel. `outline` puts the label on ordinary ink over its\n * own surface, which is contrast-safe on any panel and gives the control a\n * visible hit target besides.\n */\nfunction RetryButton() {\n const { actions } = useFileViewer();\n const { t } = useLocale();\n return (\n <Button variant=\"outline\" size=\"sm\" onClick={actions.reload}>\n {t(\"viewer.retry\")}\n </Button>\n );\n}\n\nexport const FileViewerEmpty = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerEmpty({ className, ...props }, ref) {\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-empty\"\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...props}\n >\n <StatePanel kind=\"empty\" title={t(\"viewer.empty\")} description={t(\"viewer.emptyBody\")} />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Content */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerContentProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The state switch: empty · loading · error · the adapter's own renderer.\n *\n * The adapter supplies its `Renderer` alongside its parser, so this component\n * never grows a per-format `switch` — that is what keeps formats additive.\n *\n * **This is THE scroll boundary** for any adapter that does not manage its own\n * viewport. Two nested `overflow-auto` boxes do not compose: the inner one clips\n * while the outer one's padding stays put, so a long document ends flush against\n * a band of whitespace and reads as a failed render rather than as \"scroll for\n * more\". Adapters whose content simply flows (text, code, markdown, Word) let\n * this scroll; the ones with a fixed sub-control and a scrolling body of their\n * own — PDF pages, PowerPoint slides, a sheet under its tab bar — keep theirs\n * and label it the same way.\n *\n * It is also a **focusable, named region**: a pane that scrolls but contains\n * nothing focusable cannot be reached from a keyboard at all (WCAG 2.1.1), and\n * a plain-text file contains nothing focusable by definition.\n */\nexport const FileViewerContent = forwardRef<HTMLDivElement, FileViewerContentProps>(\n function FileViewerContent({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-content\"\n role=\"region\"\n aria-label={t(\"viewer.content\")}\n tabIndex={0}\n className={cn(\"focus-ring min-h-0 flex-1 overflow-auto p-4\", className)}\n {...props}\n >\n {state.status === \"empty\" && <FileViewerEmpty />}\n {state.status === \"loading\" && <FileViewerSkeleton />}\n {state.status === \"error\" && <FileViewerError />}\n {state.status === \"ready\" && state.adapter && state.document && state.source && (\n <state.adapter.Renderer\n document={state.document}\n source={state.source}\n baseHeadingLevel={meta.baseHeadingLevel}\n highlights={meta.resolvedHighlights}\n // `meta.currentHighlightId`, NOT `state.activeHighlightId`: the\n // renderer must follow the find match too, or find can never scroll,\n // page or switch sheet.\n activeHighlightId={meta.currentHighlightId}\n // The view half (ADR 0026). A renderer that ignores these keeps\n // working — they are optional, and a format with no pages or no\n // scaling never reads them.\n pageNumber={state.pageNumber}\n onPageChange={actions.goToPage}\n zoom={state.zoom}\n onZoomResolved={actions.reportZoom}\n rotation={state.rotation}\n />\n )}\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Batteries-included */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProps\n extends\n Omit<FileViewerProviderProps, \"children\">,\n Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Replace the default composition. Rendered inside the provider AND the frame. */\n children?: ReactNode;\n}\n\n/**\n * The default composition — provider + frame + toolbar + content.\n *\n * Reach for the parts when you need a different arrangement; this covers the\n * common case in one element.\n */\nexport const FileViewer = forwardRef<HTMLDivElement, FileViewerProps>(function FileViewer(\n {\n // Every provider prop is destructured BY NAME, and the rest goes to the\n // frame. Peeling off only some of them and spreading the remainder was a\n // silent bug: `baseHeadingLevel` type-checked, never reached the provider,\n // and landed on the `<section>` as an unknown DOM attribute. Adding a\n // provider prop above without adding it here re-creates that exactly.\n source,\n registry,\n loading,\n baseHeadingLevel,\n highlights,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId,\n defaultActiveHighlightId,\n onActiveHighlightChange,\n pageNumber,\n defaultPageNumber,\n onPageNumberChange,\n zoom,\n defaultZoom,\n onZoomChange,\n rotation,\n defaultRotation,\n onRotationChange,\n children,\n ...props\n },\n ref,\n) {\n return (\n <FileViewerProvider\n source={source}\n registry={registry}\n loading={loading}\n baseHeadingLevel={baseHeadingLevel}\n highlights={highlights}\n defaultHighlights={defaultHighlights}\n onHighlightsChange={onHighlightsChange}\n activeHighlightId={activeHighlightId}\n defaultActiveHighlightId={defaultActiveHighlightId}\n onActiveHighlightChange={onActiveHighlightChange}\n pageNumber={pageNumber}\n defaultPageNumber={defaultPageNumber}\n onPageNumberChange={onPageNumberChange}\n zoom={zoom}\n defaultZoom={defaultZoom}\n onZoomChange={onZoomChange}\n rotation={rotation}\n defaultRotation={defaultRotation}\n onRotationChange={onRotationChange}\n >\n <FileViewerFrame ref={ref} {...props}>\n {children ?? (\n <>\n {/* The page and scale controls live in the identity row, not inside\n the canvas: one row of chrome per viewer, and an app that wants\n them somewhere else composes the parts itself. Each renders\n nothing for a format whose manifest does not claim it. */}\n <FileViewerToolbar>\n <FileViewerPager />\n <FileViewerZoom />\n <FileViewerRotate />\n </FileViewerToolbar>\n {/* Renders nothing until Ctrl/Cmd+F, and nothing at all for a\n format whose adapter cannot paint a range. */}\n <FileViewerFind />\n <FileViewerHighlightStatus />\n <FileViewerContent />\n </>\n )}\n </FileViewerFrame>\n </FileViewerProvider>\n );\n});\n","/**\n * The adapter registry — how a file finds its renderer.\n *\n * Architecture adapted from [anyview](https://github.com/harshpreet931/anyview)\n * (MIT). Four properties are what make it work, and all four are deliberate:\n *\n * 1. **Eager manifests, lazy loaders.** Routing and capability questions are\n * answered from plain data; the parser is fetched only when a file of that\n * kind is actually opened.\n * 2. **Priority override.** A consumer replaces a built-in by registering a\n * higher priority — no fork, no patch.\n * 3. **A fresh adapter per document.** The module (and its parser) is cached;\n * the instance is not, so per-document state cannot leak between files.\n * 4. **A protocol guard.** A mismatched adapter fails with a named error at\n * registration, not with an undefined property at render.\n */\n\nimport { type FileKind, resolveFileKind } from \"@elabs-ai/components-ui\";\n\nimport { ViewerError, isModuleNotFound, parserMissingError, toViewerError } from \"./errors\";\nimport {\n type AdapterLoader,\n type AdapterManifest,\n type AdapterModule,\n PROTOCOL_VERSION,\n} from \"./types\";\n\n/**\n * How specifically a manifest claimed a file. Higher is more specific.\n * A plain object rather than a `const enum` — the repo builds with\n * `isolatedModules`, which cannot inline one.\n */\nconst MatchScore = {\n None: 0,\n Category: 1,\n MediaTypePrefix: 2,\n MediaType: 3,\n Extension: 4,\n} as const;\n\ntype MatchScore = (typeof MatchScore)[keyof typeof MatchScore];\n\ninterface Entry {\n manifest: AdapterManifest;\n loader: AdapterLoader;\n}\n\n/** A registry of file adapters. Create with {@link createRegistry}. */\nexport interface ViewerRegistry {\n /**\n * Add an adapter, or replace one with the same `id`.\n *\n * Replacement is by id and unconditional — the caller asked for it. Priority\n * governs which of several DIFFERENT adapters wins a file, not whether a\n * registration takes effect.\n *\n * @throws {ViewerError} `protocol-mismatch` if the manifest targets another protocol.\n */\n register(manifest: AdapterManifest, loader: AdapterLoader): void;\n /**\n * Every registered manifest, highest priority first. Not the routing order —\n * specificity is per-file, so only {@link detect} can rank for a given file.\n */\n manifests(): AdapterManifest[];\n /** The manifest that should open this file, or `undefined` if none claims it. */\n detect(kind: FileKind): AdapterManifest | undefined;\n /** Convenience over {@link detect} for a name + MIME. */\n detectByName(name: string, mediaType?: string): AdapterManifest | undefined;\n /**\n * Fetch an adapter module by id, caching the MODULE (never an instance).\n *\n * @throws {ViewerError} `unsupported-format` when the id is unknown,\n * `parser-missing` when the module's optional peer is not installed,\n * `protocol-mismatch` when the loaded module disagrees with its manifest.\n */\n load(id: string): Promise<AdapterModule>;\n}\n\n/** How well `manifest` claims `kind`. `MatchScore.None` means \"not mine\". */\nexport function scoreManifest(manifest: AdapterManifest, kind: FileKind): MatchScore {\n if (kind.extension && manifest.extensions?.includes(kind.extension)) {\n return MatchScore.Extension;\n }\n for (const declared of manifest.mediaTypes ?? []) {\n if (declared.endsWith(\"/\")) {\n if (kind.mediaType.startsWith(declared)) return MatchScore.MediaTypePrefix;\n } else if (declared === kind.mediaType) {\n return MatchScore.MediaType;\n }\n }\n if (manifest.categories?.includes(kind.category)) return MatchScore.Category;\n return MatchScore.None;\n}\n\n/** Unwrap `export default` vs named exports, so an adapter can use either. */\nfunction unwrap(loaded: AdapterModule | { default: AdapterModule }): AdapterModule {\n return \"default\" in loaded && loaded.default ? loaded.default : (loaded as AdapterModule);\n}\n\nfunction assertProtocol(manifest: AdapterManifest, where: string): void {\n if (manifest.protocol !== PROTOCOL_VERSION) {\n throw new ViewerError(\n \"protocol-mismatch\",\n `Adapter \"${manifest.id}\" targets viewer protocol ${String(manifest.protocol)}, but this build speaks ${String(PROTOCOL_VERSION)} (${where}).`,\n );\n }\n}\n\n/**\n * Create an empty registry.\n *\n * Empty on purpose: the built-ins live in `createDefaultRegistry()`\n * (`src/adapters`), so a consumer who wants only their own adapters — or only\n * images — never pulls in the rest.\n */\nexport function createRegistry(): ViewerRegistry {\n const entries = new Map<string, Entry>();\n const modules = new Map<string, Promise<AdapterModule>>();\n\n function ordered(): Entry[] {\n return [...entries.values()].sort(\n (a, b) => (b.manifest.priority ?? 0) - (a.manifest.priority ?? 0),\n );\n }\n\n function detect(kind: FileKind): AdapterManifest | undefined {\n let best: { manifest: AdapterManifest; score: MatchScore; priority: number } | undefined;\n for (const { manifest } of entries.values()) {\n const score = scoreManifest(manifest, kind);\n if (score === MatchScore.None) continue;\n const priority = manifest.priority ?? 0;\n // Priority is the consumer's explicit override, so it outranks how\n // specifically an adapter happened to claim the file.\n const wins =\n !best || priority > best.priority || (priority === best.priority && score > best.score);\n if (wins) best = { manifest, score, priority };\n }\n return best?.manifest;\n }\n\n return {\n register(manifest, loader) {\n assertProtocol(manifest, \"registration\");\n entries.set(manifest.id, { manifest, loader });\n // A replaced id must not keep serving the old module.\n modules.delete(manifest.id);\n },\n\n manifests() {\n return ordered().map((entry) => entry.manifest);\n },\n\n detect,\n\n detectByName(name, mediaType) {\n return detect(resolveFileKind(name, mediaType));\n },\n\n async load(id) {\n const entry = entries.get(id);\n if (!entry) {\n throw new ViewerError(\"unsupported-format\", `No adapter is registered for \"${id}\".`);\n }\n\n let pending = modules.get(id);\n if (!pending) {\n pending = entry\n .loader()\n .then((loaded) => {\n const adapterModule = unwrap(loaded);\n assertProtocol(adapterModule.manifest, `module \"${id}\"`);\n return adapterModule;\n })\n .catch((error: unknown) => {\n // Not cached: an optional peer installed later, or a transient\n // chunk-load failure, must be retryable without a page reload.\n modules.delete(id);\n if (isModuleNotFound(error)) {\n throw parserMissingError(id, entry.manifest.requires ?? [], { cause: error });\n }\n throw toViewerError(error, \"parse-failed\");\n });\n modules.set(id, pending);\n }\n return pending;\n },\n };\n}\n","/**\n * The built-in adapters, and the registry that has them all.\n *\n * Every entry is `manifest` (eager, plain data) + `() => import(…)` (lazy). The\n * manifests are imported statically ON PURPOSE — they are a few dozen bytes of\n * data each and answer \"can this be opened, and what controls apply\" with no\n * network. The parsers and renderers behind them are not.\n *\n * `pnpm heavy-deps:check` enforces the split: a static import of `papaparse`\n * (or, from P1, `pdfjs-dist`) fails CI, because those are optional peers — a\n * static edge does not merely bloat a chunk, it makes the package unresolvable\n * for every consumer that did not install that parser.\n */\n\nimport { codeManifest } from \"./code/code-manifest\";\nimport { csvManifest } from \"./csv/csv-manifest\";\nimport { docxManifest } from \"./docx/docx-manifest\";\nimport { markdownManifest } from \"./markdown/markdown-manifest\";\nimport { imageManifest } from \"./image/image-manifest\";\nimport { jsonManifest } from \"./json/json-manifest\";\nimport { mediaManifest } from \"./media/media-manifest\";\nimport { pdfManifest } from \"./pdf/pdf-manifest\";\nimport { pptxManifest } from \"./pptx/pptx-manifest\";\nimport { textManifest } from \"./text/text-manifest\";\nimport { xlsxManifest } from \"./xlsx/xlsx-manifest\";\nimport { createRegistry, type ViewerRegistry } from \"../core/registry\";\n\nexport {\n codeManifest,\n csvManifest,\n docxManifest,\n imageManifest,\n jsonManifest,\n markdownManifest,\n mediaManifest,\n pdfManifest,\n pptxManifest,\n textManifest,\n xlsxManifest,\n};\n\n/**\n * A registry with every built-in adapter registered.\n *\n * Call it per app (or per view) rather than sharing one module-level instance,\n * so one screen's `register()` override cannot leak into another's.\n */\nexport function createDefaultRegistry(): ViewerRegistry {\n const registry = createRegistry();\n registry.register(imageManifest, () => import(\"./image/image-adapter\"));\n registry.register(jsonManifest, () => import(\"./json/json-adapter\"));\n registry.register(csvManifest, () => import(\"./csv/csv-adapter\"));\n registry.register(pdfManifest, () => import(\"./pdf/pdf-adapter\"));\n registry.register(mediaManifest, () => import(\"./media/media-adapter\"));\n registry.register(docxManifest, () => import(\"./docx/docx-adapter\"));\n registry.register(xlsxManifest, () => import(\"./xlsx/xlsx-adapter\"));\n registry.register(pptxManifest, () => import(\"./pptx/pptx-adapter\"));\n registry.register(markdownManifest, () => import(\"./markdown/markdown-adapter\"));\n registry.register(codeManifest, () => import(\"./code/code-adapter\"));\n // Registered last and claiming only broad categories, so anything above wins\n // a file it names specifically. This is the \"readable as text\" backstop.\n registry.register(textManifest, () => import(\"./text/text-adapter\"));\n return registry;\n}\n","/**\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","/**\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","\"use client\";\n\n/**\n * Find-in-document — the viewer's own Ctrl/Cmd+F.\n *\n * It paints through the SAME highlight layer citations use (`source: \"search\"`),\n * so a document never grows two mark systems that disagree about what a mark\n * looks like. Only which one is CURRENT differs, and that lives on its own knob\n * (`find.activeIndex`) so an app controlling citations does not have to honour\n * every keystroke of a search it never asked for.\n *\n * **Not a `role=\"toolbar\"`.** A toolbar is one tab stop with roving arrow keys,\n * and an `<input>` inside one has its Left/Right stolen from the caret. This is\n * `role=\"search\"` — the same call `FileViewerToolbar` already made.\n */\n\nimport {\n cn,\n IconButton,\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n Separator,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { CaseSensitiveIcon, ChevronDownIcon, ChevronUpIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useEffect,\n useRef,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { FIND_MATCH_LIMIT } from \"../core/highlight\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\n/** Whether a keyboard event is the platform's find shortcut. */\nexport function isFindShortcut(event: {\n key: string;\n metaKey: boolean;\n ctrlKey: boolean;\n}): boolean {\n // Either modifier, not `navigator.platform`: a Mac driven by an external PC\n // keyboard sends Ctrl, and sniffing the platform gets that reader wrong.\n return event.key.toLowerCase() === \"f\" && (event.metaKey || event.ctrlKey);\n}\n\nexport type FileViewerFindProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The search row. Renders nothing until `actions.openFind()` (or Ctrl/Cmd+F on\n * the frame) opens it, and nothing at all for a document whose adapter cannot\n * paint a range — an affordance that could never highlight anything is worse\n * than no affordance at all.\n */\nexport const FileViewerFind = forwardRef<HTMLDivElement, FileViewerFindProps>(\n function FileViewerFind({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t, formatNumber } = useLocale();\n const input = useRef<HTMLInputElement>(null);\n const find = state.find;\n const open = find.open && meta.canFind;\n const { registerFind } = actions;\n\n // Announce that there IS somewhere to type. The frame only takes Ctrl/Cmd+F\n // away from the browser once this has run — a shortcut swallowed with no box\n // to show leaves a keyboard reader with no find at all. Registered on mount\n // rather than when open, because the box is closed at exactly the moment the\n // shortcut has to be decided.\n useEffect(() => registerFind(), [registerFind]);\n\n // A find box that does not take the caret sends the next keystroke to the\n // document behind it — the one thing every reader expects not to happen.\n useEffect(() => {\n if (open) input.current?.focus();\n }, [open]);\n\n if (!open) return null;\n\n const hasQuery = find.query.length > 0;\n const hasMatches = find.matches > 0;\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n // Stopped here: an Escape meant for the find box must not also close a\n // Dialog the viewer happens to be sitting in.\n event.stopPropagation();\n actions.closeFind();\n return;\n }\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (event.shiftKey) actions.previousFindMatch();\n else actions.nextFindMatch();\n }\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-find\"\n role=\"search\"\n aria-label={t(\"viewer.find.label\")}\n className={cn(\n // The divider is the only cue between this row and the content below\n // it — same fill, no elevation change (WCAG 1.4.11, strong rung).\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <InputGroup className=\"h-8 max-w-72\">\n <InputGroupAddon>\n <SearchIcon aria-hidden=\"true\" />\n </InputGroupAddon>\n <InputGroupInput\n ref={input}\n // Not `type=\"search\"`: the browser's own clear affordance is\n // unlabelled and unthemeable, and this row already carries a close\n // control that does something more useful.\n type=\"text\"\n value={find.query}\n aria-label={t(\"viewer.find.label\")}\n placeholder={t(\"viewer.find.placeholder\")}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={(event) => actions.setFindQuery(event.target.value)}\n onKeyDown={onKeyDown}\n />\n <InputGroupAddon align=\"inline-end\">\n {/* A real toggle button, so the state is announced rather than\n inferred from a colour change. */}\n <InputGroupButton\n size=\"icon-xs\"\n aria-pressed={find.caseSensitive}\n aria-label={t(\"viewer.find.caseSensitive\")}\n title={t(\"viewer.find.caseSensitive\")}\n onClick={() => actions.setFindCaseSensitive(!find.caseSensitive)}\n >\n <CaseSensitiveIcon aria-hidden=\"true\" />\n </InputGroupButton>\n </InputGroupAddon>\n </InputGroup>\n\n {/* ONE live region for the whole result state: \"3 of 12\" and \"No\n matches\" replace each other rather than sitting side by side, so a\n fruitless search never reads as \"0 of 0\". */}\n <span\n role=\"status\"\n aria-live=\"polite\"\n className=\"text-meta text-muted-foreground min-w-0 flex-1 truncate tabular-nums\"\n >\n {!hasQuery\n ? null\n : hasMatches\n ? t(\"viewer.find.count\", {\n index: formatNumber(find.activeIndex + 1),\n total: formatNumber(Math.min(find.matches, FIND_MATCH_LIMIT)),\n })\n : t(\"viewer.find.none\")}\n {find.truncated\n ? ` ${t(\"viewer.find.capped\", {\n limit: formatNumber(FIND_MATCH_LIMIT),\n total: formatNumber(find.matches),\n })}`\n : null}\n </span>\n\n {/* `aria-disabled`, never the native attribute: a focused control that\n becomes `disabled` is dropped from the focus order, so a reader who\n tabbed to Next and then cleared the query would have focus silently\n fall to <body>. The actions are the real guard — both no-op with no\n matches (see interaction-guidelines.md). */}\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.previous\")}\n icon={<ChevronUpIcon aria-hidden=\"true\" />}\n onClick={actions.previousFindMatch}\n />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.next\")}\n icon={<ChevronDownIcon aria-hidden=\"true\" />}\n onClick={actions.nextFindMatch}\n />\n <Separator orientation=\"vertical\" className=\"h-4\" />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.find.close\")}\n icon={<XIcon aria-hidden=\"true\" />}\n onClick={actions.closeFind}\n />\n </div>\n );\n },\n);\n","\"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","\"use client\";\n\n/**\n * `FileViewerPager` — previous / a page you can type into / next.\n *\n * A PART, not adapter chrome. The page lives in the provider (ADR 0026), so this\n * row can sit in the viewer's own toolbar, in an app's page header, or beside a\n * thumbnail rail, and every copy of it stays in step. While the page was\n * `useState` inside `PdfRenderer` there could only ever be one pager, inside the\n * canvas.\n *\n * ## Why this is not a `role=\"toolbar\"`\n *\n * The role promises roving-tabindex arrow-key navigation, and the page field is\n * a TEXT INPUT — ArrowLeft/ArrowRight there move the caret. A toolbar that\n * swallowed them would make the number unusable to a keyboard reader, so this is\n * a plain named group of ordinary tab stops: the same call `FileViewerToolbar`\n * and `ViewToolbar` make, for the same reason.\n */\n\nimport { cn, IconButton, Input, useLocale } from \"@elabs-ai/components-ui\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { forwardRef, useState, type HTMLAttributes } from \"react\";\n\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerPagerProps = HTMLAttributes<HTMLDivElement>;\n\nexport const FileViewerPager = forwardRef<HTMLDivElement, FileViewerPagerProps>(\n function FileViewerPager({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n // The half-typed value, while the reader is typing it. `null` means \"not\n // editing\", so the field follows the document the rest of the time — a\n // citation that turns the page updates the number under the caret too.\n const [draft, setDraft] = useState<string | null>(null);\n\n const { pageNumber, pageCount } = state;\n\n // No pages, or none known yet: render nothing rather than an inert \"1 of 0\".\n // Chrome with nothing in it reads as a broken render (viewer-components.md).\n if (!state.capabilities.pages || pageCount === 0) return null;\n\n const commit = () => {\n if (draft === null) return;\n const parsed = Number.parseInt(draft, 10);\n setDraft(null);\n // A blank or nonsense entry is a reader changing their mind, not a\n // navigation — snap back to where they are instead of jumping to page 1.\n if (Number.isNaN(parsed)) return;\n actions.goToPage(parsed);\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-pager\"\n role=\"group\"\n aria-label={t(\"viewer.pager.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.previous\")}\n icon={<ChevronLeftIcon aria-hidden=\"true\" />}\n disabled={pageNumber <= 1}\n onClick={actions.previousPage}\n />\n <Input\n // Explicit: Radix's toolbar slot and several wrappers default an\n // unset `type` to `\"button\"`, which would silently turn the field\n // into a button the day this moves inside one.\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n spellCheck={false}\n aria-label={t(\"viewer.pager.pageNumber\")}\n value={draft ?? String(pageNumber)}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commit();\n } else if (event.key === \"Escape\") {\n // Abandon the edit without navigating; focus stays put, so the\n // reader can try again without re-reaching the field.\n setDraft(null);\n }\n }}\n className=\"h-7 w-12 px-1 text-center tabular-nums\"\n />\n <span className=\"text-meta text-muted-foreground whitespace-nowrap tabular-nums\">\n {t(\"viewer.pager.of\", { total: formatNumber(pageCount) })}\n </span>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.next\")}\n icon={<ChevronRightIcon aria-hidden=\"true\" />}\n disabled={pageNumber >= pageCount}\n onClick={actions.nextPage}\n />\n {/* Paging repaints a canvas, which announces nothing on its own, and the\n field's own value change is not announced either. ONE live region for\n the group carries the whole sentence — `loading-states.md`'s rule that\n a region announces once, not per element. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.pager.status\", {\n page: formatNumber(pageNumber),\n total: formatNumber(pageCount),\n })}\n </span>\n </div>\n );\n },\n);\n","\"use client\";\n\n/**\n * `FileViewerZoom` and `FileViewerRotate` — how big, and which way up.\n *\n * Parts over the provider's view state (ADR 0026), so an app can put the scale\n * control in its own header without reaching inside the canvas.\n *\n * Each renders NOTHING for a format whose manifest does not claim the capability\n * — an inert zoom control over a CSV is chrome with nothing in it, which reads\n * as a broken render (`viewer-components.md`). That absence is also the fix for\n * the `image` manifest's old lie: it declared `zoom` and `rotate` while its\n * renderer implemented neither, so the claim was invisible either way.\n */\n\nimport {\n cn,\n IconButton,\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { RotateCwIcon, ZoomInIcon, ZoomOutIcon } from \"lucide-react\";\nimport { forwardRef, type HTMLAttributes } from \"react\";\n\nimport type { ZoomLevel } from \"../core/types\";\nimport { canStepZoom, isZoomFit, VIEWER_ZOOM_STEPS } from \"../core/zoom\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerZoomProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * Zoom out · the current level · zoom in.\n *\n * The middle control is a `Select` rather than a read-out because the stops are\n * the API: a reader who wants 200% should not have to press \"+\" four times, and\n * the two fit modes have no number to press towards at all.\n *\n * Not a `role=\"toolbar\"` — see `FileViewerPager`. It sits in the same row, and\n * one row that is half roving-tabindex and half ordinary tab stops is worse for\n * a keyboard reader than one that is consistently ordinary.\n */\nexport const FileViewerZoom = forwardRef<HTMLDivElement, FileViewerZoomProps>(\n function FileViewerZoom({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n if (!state.capabilities.zoom) return null;\n\n const { zoom, effectiveZoom } = state;\n const percent = (scale: number) =>\n formatNumber(scale, { style: \"percent\", maximumFractionDigits: 0 });\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-zoom\"\n role=\"group\"\n aria-label={t(\"viewer.zoom.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.out\")}\n icon={<ZoomOutIcon aria-hidden=\"true\" />}\n // Stepping is measured against what is ON SCREEN, so a page fitted to\n // 137% still has somewhere to go in both directions.\n disabled={!canStepZoom(effectiveZoom, -1)}\n onClick={actions.zoomOut}\n />\n <Select\n value={zoomToValue(zoom)}\n onValueChange={(value) => actions.setZoom(valueToZoom(value))}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t(\"viewer.zoom.level\")}\n className=\"h-7 w-24 gap-1 px-2\"\n >\n {/* Children override the selected item's own text: a fit mode has to\n read as \"Fit width\", but a fixed stop reads better as the number\n than as a row label, and both have to fit one narrow trigger. */}\n <SelectValue>\n {isZoomFit(zoom)\n ? t(zoom === \"fit-width\" ? \"viewer.zoom.fitWidth\" : \"viewer.zoom.fitPage\")\n : percent(zoom)}\n </SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"fit-width\">{t(\"viewer.zoom.fitWidth\")}</SelectItem>\n <SelectItem value=\"fit-page\">{t(\"viewer.zoom.fitPage\")}</SelectItem>\n <SelectSeparator />\n {VIEWER_ZOOM_STEPS.map((step) => (\n <SelectItem key={step} value={String(step)}>\n {percent(step)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.in\")}\n icon={<ZoomInIcon aria-hidden=\"true\" />}\n disabled={!canStepZoom(effectiveZoom, 1)}\n onClick={actions.zoomIn}\n />\n {/* One live region for the group: neither a repainted canvas nor a\n `Select`'s own value change announces the new scale. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.zoom.status\", { level: percent(effectiveZoom) })}\n </span>\n </div>\n );\n },\n);\n\n/** `Select` speaks strings; the state is a number or a fit mode. */\nfunction zoomToValue(zoom: ZoomLevel): string {\n return isZoomFit(zoom) ? zoom : String(zoom);\n}\n\nfunction valueToZoom(value: string): ZoomLevel {\n if (value === \"fit-width\" || value === \"fit-page\") return value;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? 1 : parsed;\n}\n\nexport type FileViewerRotateProps = HTMLAttributes<HTMLButtonElement>;\n\n/**\n * One button, one quarter-turn clockwise.\n *\n * Document-level and clockwise-only on purpose: a counter-clockwise button is a\n * second control for something three presses of this one already do, and\n * per-page rotation would change the file, which is authoring rather than\n * viewing. `actions.rotate(-1)` is there for an app that wants the other one.\n */\nexport const FileViewerRotate = forwardRef<HTMLButtonElement, FileViewerRotateProps>(\n function FileViewerRotate({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t } = useLocale();\n\n if (!state.capabilities.rotate) return null;\n\n return (\n <IconButton\n ref={ref}\n data-slot=\"file-viewer-rotate\"\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.rotate\")}\n icon={<RotateCwIcon aria-hidden=\"true\" />}\n className={cn(\"shrink-0\", className)}\n onClick={() => actions.rotate(1)}\n {...props}\n />\n );\n },\n);\n"],"mappings":"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","names":["cn","IconButton","resolveFileKind","Separator","useLocale","forwardRef","useEffect","useRef","useState","FileViewerFind","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerPager","useLocale","cn","IconButton","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerZoom","useLocale","cn","IconButton","FileViewerRotate","jsx","jsxs","useState","useEffect","resolveFileKind","useRef","forwardRef","FileViewerFrame","useLocale","cn","FileViewerToolbar","Separator","IconButton","FileViewerHighlightStatus","FileViewerSkeleton","FileViewerError","FileViewerEmpty","FileViewerContent","FileViewer"]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-viewer",
3
- "version": "4.2.0",
3
+ "version": "5.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,8 +39,8 @@
39
39
  "jszip": "^3.10.1",
40
40
  "shiki": "^3.22.0",
41
41
  "streamdown": "^2.4.0",
42
- "@elabs-ai/components-tokens": "4.2.0",
43
- "@elabs-ai/components-ui": "4.2.0"
42
+ "@elabs-ai/components-tokens": "5.0.0",
43
+ "@elabs-ai/components-ui": "5.0.0"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "papaparse": {
@@ -88,9 +88,9 @@
88
88
  "typescript": "^5.7.3",
89
89
  "vitest": "^3.0.2",
90
90
  "@elabs-ai/components-eslint-config": "0.1.0",
91
- "@elabs-ai/components-tokens": "4.2.0",
91
+ "@elabs-ai/components-tokens": "5.0.0",
92
92
  "@elabs-ai/components-typescript-config": "0.1.0",
93
- "@elabs-ai/components-ui": "4.2.0"
93
+ "@elabs-ai/components-ui": "5.0.0"
94
94
  },
95
95
  "scripts": {
96
96
  "build": "tsup && node ../../scripts/link-dist-css.mjs",