@elabs-ai/components-viewer 4.2.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/{code-adapter-ADZ4UOGN.js → adapters/code/code-adapter.js} +15 -64
  2. package/dist/adapters/code/code-adapter.js.map +1 -0
  3. package/dist/adapters/code/code-language.js +10 -0
  4. package/dist/adapters/code/code-language.js.map +1 -0
  5. package/dist/adapters/code/code-manifest.js +10 -0
  6. package/dist/adapters/code/code-manifest.js.map +1 -0
  7. package/dist/adapters/code/code-theme.js +8 -0
  8. package/dist/adapters/code/code-theme.js.map +1 -0
  9. package/dist/{csv-adapter-7HOFZ5Z6.js → adapters/csv/csv-adapter.js} +13 -9
  10. package/dist/adapters/csv/csv-adapter.js.map +1 -0
  11. package/dist/adapters/csv/csv-manifest.js +9 -0
  12. package/dist/adapters/csv/csv-manifest.js.map +1 -0
  13. package/dist/{docx-adapter-5CQDHWTD.js → adapters/docx/docx-adapter.js} +17 -175
  14. package/dist/adapters/docx/docx-adapter.js.map +1 -0
  15. package/dist/adapters/docx/docx-manifest.js +9 -0
  16. package/dist/adapters/docx/docx-manifest.js.map +1 -0
  17. package/dist/adapters/docx/docx-model.js +21 -0
  18. package/dist/adapters/docx/docx-model.js.map +1 -0
  19. package/dist/{image-adapter-WOHZR24J.js → adapters/image/image-adapter.js} +4 -3
  20. package/dist/adapters/image/image-adapter.js.map +1 -0
  21. package/dist/adapters/image/image-manifest.js +9 -0
  22. package/dist/adapters/image/image-manifest.js.map +1 -0
  23. package/dist/adapters/index.js +56 -0
  24. package/dist/adapters/index.js.map +1 -0
  25. package/dist/{json-adapter-ZUW5GQHE.js → adapters/json/json-adapter.js} +4 -3
  26. package/dist/adapters/json/json-adapter.js.map +1 -0
  27. package/dist/adapters/json/json-manifest.js +9 -0
  28. package/dist/adapters/json/json-manifest.js.map +1 -0
  29. package/dist/{markdown-adapter-YC6WTBS4.js → adapters/markdown/markdown-adapter.js} +14 -36
  30. package/dist/adapters/markdown/markdown-adapter.js.map +1 -0
  31. package/dist/adapters/markdown/markdown-manifest.js +9 -0
  32. package/dist/adapters/markdown/markdown-manifest.js.map +1 -0
  33. package/dist/adapters/markdown/markdown-marks.js +10 -0
  34. package/dist/adapters/markdown/markdown-marks.js.map +1 -0
  35. package/dist/{media-adapter-MCTB4GBH.js → adapters/media/media-adapter.js} +4 -3
  36. package/dist/adapters/media/media-adapter.js.map +1 -0
  37. package/dist/adapters/media/media-manifest.js +9 -0
  38. package/dist/adapters/media/media-manifest.js.map +1 -0
  39. package/dist/{pdf-adapter-5PMKEXUD.js → adapters/pdf/pdf-adapter.js} +31 -81
  40. package/dist/adapters/pdf/pdf-adapter.js.map +1 -0
  41. package/dist/adapters/pdf/pdf-engine.js +14 -0
  42. package/dist/adapters/pdf/pdf-engine.js.map +1 -0
  43. package/dist/adapters/pdf/pdf-manifest.js +9 -0
  44. package/dist/adapters/pdf/pdf-manifest.js.map +1 -0
  45. package/dist/adapters/pdf/pdf-text.js +20 -0
  46. package/dist/adapters/pdf/pdf-text.js.map +1 -0
  47. package/dist/{pptx-adapter-BONWFHYT.js → adapters/pptx/pptx-adapter.js} +34 -135
  48. package/dist/adapters/pptx/pptx-adapter.js.map +1 -0
  49. package/dist/adapters/pptx/pptx-manifest.js +9 -0
  50. package/dist/adapters/pptx/pptx-manifest.js.map +1 -0
  51. package/dist/adapters/pptx/pptx-model.js +37 -0
  52. package/dist/adapters/pptx/pptx-model.js.map +1 -0
  53. package/dist/{text-adapter-NFNWB5W3.js → adapters/text/text-adapter.js} +8 -5
  54. package/dist/adapters/text/text-adapter.js.map +1 -0
  55. package/dist/adapters/text/text-manifest.js +9 -0
  56. package/dist/adapters/text/text-manifest.js.map +1 -0
  57. package/dist/{xlsx-adapter-O3XIVLFC.js → adapters/xlsx/xlsx-adapter.js} +12 -8
  58. package/dist/adapters/xlsx/xlsx-adapter.js.map +1 -0
  59. package/dist/adapters/xlsx/xlsx-manifest.js +9 -0
  60. package/dist/adapters/xlsx/xlsx-manifest.js.map +1 -0
  61. package/dist/chunk-3HKQFFZW.js +31 -0
  62. package/dist/chunk-3HKQFFZW.js.map +1 -0
  63. package/dist/chunk-5VJWILZ7.js +182 -0
  64. package/dist/chunk-5VJWILZ7.js.map +1 -0
  65. package/dist/{chunk-UL43NGUG.js → chunk-5YE2MUS2.js} +1 -1
  66. package/dist/{chunk-5OMONA2G.js → chunk-6FVJ4D7W.js} +3 -15
  67. package/dist/chunk-6FVJ4D7W.js.map +1 -0
  68. package/dist/{chunk-RGWDIQJR.js → chunk-7EASJBQM.js} +2 -2
  69. package/dist/chunk-7PGGKQVY.js +26 -0
  70. package/dist/chunk-7PGGKQVY.js.map +1 -0
  71. package/dist/{chunk-XBOR2YXS.js → chunk-AK7F3IZE.js} +9 -30
  72. package/dist/chunk-AK7F3IZE.js.map +1 -0
  73. package/dist/chunk-AOYEL7O3.js +34 -0
  74. package/dist/chunk-AOYEL7O3.js.map +1 -0
  75. package/dist/chunk-BDUFH66C.js +61 -0
  76. package/dist/chunk-BDUFH66C.js.map +1 -0
  77. package/dist/chunk-BWMXMHVZ.js +107 -0
  78. package/dist/chunk-BWMXMHVZ.js.map +1 -0
  79. package/dist/chunk-CIT2HHRI.js +9 -0
  80. package/dist/chunk-CIT2HHRI.js.map +1 -0
  81. package/dist/chunk-CLOI4HBA.js +18 -0
  82. package/dist/chunk-CLOI4HBA.js.map +1 -0
  83. package/dist/chunk-CMYBLW7D.js +21 -0
  84. package/dist/chunk-CMYBLW7D.js.map +1 -0
  85. package/dist/chunk-CO2VJRSU.js +18 -0
  86. package/dist/chunk-CO2VJRSU.js.map +1 -0
  87. package/dist/{chunk-3U3MESBU.js → chunk-EA72HA4I.js} +2 -2
  88. package/dist/{chunk-KOMDRWOU.js → chunk-FDT4QOIG.js} +2 -2
  89. package/dist/chunk-FQHZEQUH.js +90 -0
  90. package/dist/chunk-FQHZEQUH.js.map +1 -0
  91. package/dist/chunk-G27UJZWH.js +82 -0
  92. package/dist/chunk-G27UJZWH.js.map +1 -0
  93. package/dist/{chunk-BCF3RWRL.js → chunk-GH4UBYST.js} +2 -2
  94. package/dist/{chunk-2NQ4RSJ3.js → chunk-GNBD53AT.js} +2 -2
  95. package/dist/{chunk-VO2273Z2.js → chunk-HOQZ7OPH.js} +2 -18
  96. package/dist/chunk-HOQZ7OPH.js.map +1 -0
  97. package/dist/{chunk-GGR7BNW4.js → chunk-HUUJIFFT.js} +2 -2
  98. package/dist/{chunk-FELIAGKI.js → chunk-I5XD2NJ6.js} +2 -2
  99. package/dist/chunk-IB4N72FA.js +21 -0
  100. package/dist/chunk-IB4N72FA.js.map +1 -0
  101. package/dist/{chunk-4OFQYALG.js → chunk-IQ6Z4X3K.js} +8 -68
  102. package/dist/chunk-IQ6Z4X3K.js.map +1 -0
  103. package/dist/{chunk-AORNMH77.js → chunk-JHFC2KBI.js} +1 -5
  104. package/dist/chunk-JHFC2KBI.js.map +1 -0
  105. package/dist/chunk-LRPZ373O.js +93 -0
  106. package/dist/chunk-LRPZ373O.js.map +1 -0
  107. package/dist/chunk-M77ICVWP.js +24 -0
  108. package/dist/chunk-M77ICVWP.js.map +1 -0
  109. package/dist/chunk-MJUH32ZK.js +24 -0
  110. package/dist/chunk-MJUH32ZK.js.map +1 -0
  111. package/dist/chunk-PAIVK4N6.js +162 -0
  112. package/dist/chunk-PAIVK4N6.js.map +1 -0
  113. package/dist/chunk-PKYDKXCO.js +129 -0
  114. package/dist/chunk-PKYDKXCO.js.map +1 -0
  115. package/dist/chunk-QIPKBOZJ.js +59 -0
  116. package/dist/chunk-QIPKBOZJ.js.map +1 -0
  117. package/dist/chunk-SASY77ZL.js +29 -0
  118. package/dist/chunk-SASY77ZL.js.map +1 -0
  119. package/dist/{chunk-SLXRUYT3.js → chunk-SBG7WPGO.js} +2 -44
  120. package/dist/chunk-SBG7WPGO.js.map +1 -0
  121. package/dist/chunk-T4Y4YRHJ.js +733 -0
  122. package/dist/chunk-T4Y4YRHJ.js.map +1 -0
  123. package/dist/chunk-TLCNGQKL.js +37 -0
  124. package/dist/chunk-TLCNGQKL.js.map +1 -0
  125. package/dist/chunk-US5KT3TR.js +141 -0
  126. package/dist/chunk-US5KT3TR.js.map +1 -0
  127. package/dist/{chunk-HMG2ERXH.js → chunk-YA72QE57.js} +2 -2
  128. package/dist/{chunk-H3OAON3D.js → chunk-YWHFMCS4.js} +2 -2
  129. package/dist/{chunk-UJIJECEF.js → chunk-ZAVXZRIO.js} +2 -2
  130. package/dist/components/grid-text.js +19 -0
  131. package/dist/components/grid-text.js.map +1 -0
  132. package/dist/components/marked-text.js +10 -0
  133. package/dist/components/marked-text.js.map +1 -0
  134. package/dist/components/sheet-table.js +14 -0
  135. package/dist/components/sheet-table.js.map +1 -0
  136. package/dist/core/errors.js +18 -0
  137. package/dist/core/errors.js.map +1 -0
  138. package/dist/core/highlight-marks.js +10 -0
  139. package/dist/core/highlight-marks.js.map +1 -0
  140. package/dist/core/highlight-resolve.js +10 -0
  141. package/dist/core/highlight-resolve.js.map +1 -0
  142. package/dist/core/highlight.js +12 -0
  143. package/dist/core/highlight.js.map +1 -0
  144. package/dist/core/registry.js +12 -0
  145. package/dist/core/registry.js.map +1 -0
  146. package/dist/core/scroll-host.js +10 -0
  147. package/dist/core/scroll-host.js.map +1 -0
  148. package/dist/core/text-index.js +14 -0
  149. package/dist/core/text-index.js.map +1 -0
  150. package/dist/core/types.js +8 -0
  151. package/dist/core/types.js.map +1 -0
  152. package/dist/core/use-highlight-scroll.js +11 -0
  153. package/dist/core/use-highlight-scroll.js.map +1 -0
  154. package/dist/core/use-page-control.js +9 -0
  155. package/dist/core/use-page-control.js.map +1 -0
  156. package/dist/core/use-paged-scroll.js +10 -0
  157. package/dist/core/use-paged-scroll.js.map +1 -0
  158. package/dist/core/use-viewport-size.js +10 -0
  159. package/dist/core/use-viewport-size.js.map +1 -0
  160. package/dist/core/zoom.js +16 -0
  161. package/dist/core/zoom.js.map +1 -0
  162. package/dist/file-viewer/file-viewer-context.js +11 -0
  163. package/dist/file-viewer/file-viewer-context.js.map +1 -0
  164. package/dist/file-viewer/file-viewer-find.js +13 -0
  165. package/dist/file-viewer/file-viewer-find.js.map +1 -0
  166. package/dist/file-viewer/file-viewer-pager.js +10 -0
  167. package/dist/file-viewer/file-viewer-pager.js.map +1 -0
  168. package/dist/file-viewer/file-viewer-zoom.js +13 -0
  169. package/dist/file-viewer/file-viewer-zoom.js.map +1 -0
  170. package/dist/file-viewer/file-viewer.js +48 -0
  171. package/dist/file-viewer/file-viewer.js.map +1 -0
  172. package/dist/index.js +78 -1274
  173. package/dist/index.js.map +1 -1
  174. package/package.json +5 -5
  175. package/src/file-viewer/file-viewer-recipes.stories.tsx +406 -0
  176. package/src/file-viewer/file-viewer.stories.tsx +2 -2
  177. package/dist/chunk-4OFQYALG.js.map +0 -1
  178. package/dist/chunk-5OMONA2G.js.map +0 -1
  179. package/dist/chunk-AORNMH77.js.map +0 -1
  180. package/dist/chunk-SLXRUYT3.js.map +0 -1
  181. package/dist/chunk-VO2273Z2.js.map +0 -1
  182. package/dist/chunk-XBOR2YXS.js.map +0 -1
  183. package/dist/code-adapter-ADZ4UOGN.js.map +0 -1
  184. package/dist/csv-adapter-7HOFZ5Z6.js.map +0 -1
  185. package/dist/docx-adapter-5CQDHWTD.js.map +0 -1
  186. package/dist/image-adapter-WOHZR24J.js.map +0 -1
  187. package/dist/json-adapter-ZUW5GQHE.js.map +0 -1
  188. package/dist/markdown-adapter-YC6WTBS4.js.map +0 -1
  189. package/dist/media-adapter-MCTB4GBH.js.map +0 -1
  190. package/dist/pdf-adapter-5PMKEXUD.js.map +0 -1
  191. package/dist/pptx-adapter-BONWFHYT.js.map +0 -1
  192. package/dist/text-adapter-NFNWB5W3.js.map +0 -1
  193. package/dist/xlsx-adapter-O3XIVLFC.js.map +0 -1
  194. /package/dist/{chunk-UL43NGUG.js.map → chunk-5YE2MUS2.js.map} +0 -0
  195. /package/dist/{chunk-RGWDIQJR.js.map → chunk-7EASJBQM.js.map} +0 -0
  196. /package/dist/{chunk-3U3MESBU.js.map → chunk-EA72HA4I.js.map} +0 -0
  197. /package/dist/{chunk-KOMDRWOU.js.map → chunk-FDT4QOIG.js.map} +0 -0
  198. /package/dist/{chunk-BCF3RWRL.js.map → chunk-GH4UBYST.js.map} +0 -0
  199. /package/dist/{chunk-2NQ4RSJ3.js.map → chunk-GNBD53AT.js.map} +0 -0
  200. /package/dist/{chunk-GGR7BNW4.js.map → chunk-HUUJIFFT.js.map} +0 -0
  201. /package/dist/{chunk-FELIAGKI.js.map → chunk-I5XD2NJ6.js.map} +0 -0
  202. /package/dist/{chunk-HMG2ERXH.js.map → chunk-YA72QE57.js.map} +0 -0
  203. /package/dist/{chunk-H3OAON3D.js.map → chunk-YWHFMCS4.js.map} +0 -0
  204. /package/dist/{chunk-UJIJECEF.js.map → chunk-ZAVXZRIO.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/use-paged-scroll.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Continuous scrolling for a paginated document, over the shell's own viewport.\n *\n * ## Why the pages are virtualized\n *\n * A 900-page PDF is 900 canvases, 900 text layers and 900 rasterization jobs. A\n * plain stack would allocate all of them before the first page appeared. Only the\n * pages near the viewport are mounted, so the cost is a function of the WINDOW,\n * not of the document.\n *\n * ## Why the virtualizer scrolls someone else's element\n *\n * `FileViewerContent` is the one scroll boundary (`viewer-components.md`), so the\n * list is a CHILD of the scrolling box rather than being one. That is what\n * `scrollMargin` is for: every measurement is offset by where the list starts\n * inside the pane, which is the pane's own padding.\n *\n * ## The page number is a two-way binding, and that is the whole difficulty\n *\n * The shell owns `pageNumber` (ADR 0026), so it flows two ways: the reader\n * scrolls and the pager must follow; the reader types \"7\" and the document must\n * scroll. Naively wiring both makes them fight — a scroll reports page 3, the\n * prop becomes 3, and the \"the prop changed, scroll to it\" effect snaps the\n * reader back to the top of page 3 mid-gesture. Each direction therefore\n * remembers what it last said, and ignores an echo of its own words.\n */\n\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { findScrollHost } from \"./scroll-host\";\n\n/**\n * Slack under the viewport's top edge, in CSS pixels.\n *\n * Without it the page whose last pixel row is still visible counts as the page\n * being read, so the pager flickers back a page at every boundary.\n */\nconst EDGE_SLACK = 8;\n\nexport interface PagedScrollOptions {\n /** Pages in the document. `0` disables the virtualizer entirely. */\n count: number;\n /**\n * The element the pages are laid out in. Must be a descendant of the scroll\n * host — its `offsetTop` is what `scrollMargin` is derived from.\n */\n listRef: RefObject<HTMLElement | null>;\n /** The page the shell wants shown, 1-based. */\n pageNumber: number;\n /** Report the page the reader scrolled to, 1-based. */\n goToPage: (page: number) => void;\n /** Height of page `index` at the current scale, in CSS pixels. */\n estimateSize: (index: number) => number;\n /**\n * A value that changes whenever EVERY page's size changes — the scale, in\n * practice. Every cached measurement is thrown away and the reader is put back\n * where they were reading, at the new size.\n */\n sizeKey?: unknown;\n}\n\n/**\n * @returns The virtualizer. Render `getVirtualItems()` inside `listRef`'s\n * element, sized to `getTotalSize()`, translated by `start - scrollMargin`.\n */\nexport function usePagedScroll({\n count,\n listRef,\n pageNumber,\n goToPage,\n estimateSize,\n sizeKey,\n}: PagedScrollOptions): Virtualizer<HTMLElement, Element> {\n // Resolved after mount rather than passed in: a renderer may be composed into\n // the shell, into a consumer's own frame, or mounted bare in a test, and only\n // the DOM knows which. State (not a ref) so the virtualizer re-reads it.\n const [host, setHost] = useState<HTMLElement | null>(null);\n const [scrollMargin, setScrollMargin] = useState(0);\n useEffect(() => {\n const list = listRef.current;\n const found = findScrollHost(list) ?? list?.parentElement ?? null;\n setHost(found);\n if (!list || !found) return;\n // Deliberately geometric rather than `offsetTop`: `offsetTop` is measured\n // from the nearest POSITIONED ancestor, and the scrolling pane is not one —\n // so it would report the distance to some outer frame and put every page at\n // the wrong offset. This is the list's true start inside the scrolled\n // content, which is the pane's own padding.\n setScrollMargin(\n list.getBoundingClientRect().top - found.getBoundingClientRect().top + found.scrollTop,\n );\n }, [listRef]);\n\n const virtualizer = useVirtualizer({\n count,\n getScrollElement: () => host,\n estimateSize,\n overscan: 1,\n scrollMargin,\n measureElement: (element, entry, instance) => {\n const measured =\n entry?.borderBoxSize?.[0]?.blockSize ?? element.getBoundingClientRect().height;\n // A hidden pane measures zero, and so does a layout-less environment.\n // Recording that would collapse the page to nothing and — because the\n // measurement is cached — leave it collapsed after the pane is shown\n // again. Keeping the estimate is the same guard `useViewportSize` carries,\n // for the same reason.\n if (measured > 0) return measured;\n const index = Number(element.getAttribute(\"data-index\"));\n return Number.isNaN(index) ? 0 : instance.options.estimateSize(index);\n },\n });\n\n const items = virtualizer.getVirtualItems();\n const scrollOffset = virtualizer.scrollOffset ?? 0;\n\n // The page under the viewport's top edge — the one a reader would name if you\n // asked what page they are on.\n const top = items.find((item) => item.start + item.size > scrollOffset + EDGE_SLACK);\n const active = top ? top.index + 1 : 1;\n\n // Where the reader is WITHIN that page, so a scale change can put them back.\n // Recorded on every scroll rather than captured at the moment of the change:\n // by then the measurements have already been reset.\n const anchor = useRef({ index: 0, fraction: 0 });\n useEffect(() => {\n if (!top || top.size <= 0) return;\n // Deliberately NOT clamped at the low end. At the very top of a document the\n // reader is ABOVE page 1's start — the pane's own padding sits between them —\n // and flooring that to 0 would scroll the padding away the first time the\n // scale resolves, so simply opening a file nudges the view.\n const fraction = (scrollOffset - top.start) / top.size;\n anchor.current = { index: top.index, fraction: Math.min(fraction, 1) };\n }, [top, scrollOffset]);\n\n // ── The page number, in both directions ──────────────────────────────────\n // Both start at 0 — a page number that cannot occur — so the FIRST value of\n // `pageNumber` still counts as something the shell asked for. A shell that\n // mounts already pointing at page 140 (a deep link, a citation restored from a\n // URL) has made a request that nothing has honoured yet.\n const reported = useRef(0);\n const requested = useRef(0);\n\n const lastActive = useRef(active);\n useEffect(() => {\n // Only a CHANGE in the page under the top edge is news. Reporting the\n // initial value would answer the deep link above with \"you are on page 1\"\n // before the scroll that honours it has even been attempted.\n if (count === 0 || active === lastActive.current) return;\n lastActive.current = active;\n if (active === pageNumber) return;\n reported.current = active;\n goToPage(active);\n }, [active, pageNumber, count, goToPage]);\n\n useEffect(() => {\n // `host` is resolved in an effect, so the first pass through here has\n // nothing to scroll — and virtual-core silently drops a scroll with no\n // element. Waiting for it is what makes the mount-time request land.\n if (!host || count === 0 || requested.current === pageNumber) return;\n requested.current = pageNumber;\n // Our own report coming back around. Scrolling here would fight the gesture\n // that produced it.\n if (pageNumber === reported.current || pageNumber === active) return;\n virtualizer.scrollToIndex(pageNumber - 1, { align: \"start\" });\n }, [host, pageNumber, active, count, virtualizer]);\n\n // ── Re-anchor after a scale change ───────────────────────────────────────\n const lastSizeKey = useRef(sizeKey);\n useLayoutEffect(() => {\n if (lastSizeKey.current === sizeKey) return;\n lastSizeKey.current = sizeKey;\n if (count === 0) return;\n\n // Measured heights describe the OLD scale, so keeping them would place every\n // page at the wrong offset until it happened to be re-measured.\n virtualizer.measure();\n\n const { index, fraction } = anchor.current;\n // At the very top there is no reading position to preserve, and scrolling to\n // page 1's own start would scroll the pane's top padding away — so simply\n // opening a file and letting its fit resolve would nudge the view.\n if (index === 0 && fraction <= 0) return;\n const start = virtualizer.getOffsetForIndex(index, \"start\")?.[0];\n if (start === undefined) return;\n const size = virtualizer.measurementsCache[index]?.size ?? 0;\n virtualizer.scrollToOffset(start + fraction * size, { align: \"start\" });\n }, [sizeKey, count, virtualizer]);\n\n return virtualizer;\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +1,5 @@
1
1
  "use client";
2
2
 
3
- // src/core/types.ts
4
- var PROTOCOL_VERSION = 1;
5
-
6
3
  // src/core/errors.ts
7
4
  var ViewerError = class extends Error {
8
5
  code;
@@ -47,7 +44,6 @@ function parserMissingError(adapterId, packages, options = {}) {
47
44
  }
48
45
 
49
46
  export {
50
- PROTOCOL_VERSION,
51
47
  ViewerError,
52
48
  isViewerError,
53
49
  isAbort,
@@ -55,4 +51,4 @@ export {
55
51
  isModuleNotFound,
56
52
  parserMissingError
57
53
  };
58
- //# sourceMappingURL=chunk-AORNMH77.js.map
54
+ //# sourceMappingURL=chunk-JHFC2KBI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts"],"sourcesContent":["/**\n * Typed failures, so the UI can decide what to SHOW without string-matching a\n * message.\n *\n * The `message` on these errors is developer-facing — it goes to the console and\n * to bug reports. Everything the user reads is looked up from {@link ViewerErrorCode}\n * through `t()` (ADR 0017), which is why the code, not the prose, is the contract.\n */\n\n/** Why a file could not be shown. */\nexport type ViewerErrorCode =\n /** No registered adapter claims this file. */\n | \"unsupported-format\"\n /** An adapter was built against a different registry protocol. */\n | \"protocol-mismatch\"\n /** The adapter exists but its optional peer dependency is not installed. */\n | \"parser-missing\"\n /** The bytes could not be obtained (network, permissions, a revoked blob). */\n | \"read-failed\"\n /** The bytes arrived but the parser rejected them. */\n | \"parse-failed\"\n /** The load was cancelled — a new source, or an unmounted view. */\n | \"aborted\";\n\nexport interface ViewerErrorOptions extends ErrorOptions {\n /** The file this is about, for the developer-facing message. */\n fileName?: string;\n /** For `parser-missing`: the packages a consumer has to install. */\n packages?: string[];\n}\n\n/** A failure a `FileViewer` can render, carrying a machine-readable {@link ViewerErrorCode}. */\nexport class ViewerError extends Error {\n readonly code: ViewerErrorCode;\n readonly fileName?: string;\n readonly packages?: string[];\n\n constructor(code: ViewerErrorCode, message: string, options: ViewerErrorOptions = {}) {\n super(message, { cause: options.cause });\n this.name = \"ViewerError\";\n this.code = code;\n this.fileName = options.fileName;\n this.packages = options.packages;\n }\n}\n\n/** True for a {@link ViewerError}, without relying on `instanceof` across bundles. */\nexport function isViewerError(value: unknown): value is ViewerError {\n return value instanceof ViewerError || (value instanceof Error && value.name === \"ViewerError\");\n}\n\n/** True when the failure is a cancellation rather than something to report. */\nexport function isAbort(value: unknown): boolean {\n if (isViewerError(value)) return value.code === \"aborted\";\n return value instanceof Error && (value.name === \"AbortError\" || value.name === \"TimeoutError\");\n}\n\n/**\n * Wrap an unknown throw as a {@link ViewerError}, preserving an existing one and\n * mapping an abort to the `aborted` code so callers do not report cancellations\n * as failures.\n */\nexport function toViewerError(\n value: unknown,\n fallbackCode: ViewerErrorCode,\n options: ViewerErrorOptions = {},\n): ViewerError {\n if (isViewerError(value)) return value;\n if (isAbort(value)) {\n return new ViewerError(\"aborted\", \"The file load was cancelled.\", { ...options, cause: value });\n }\n const message = value instanceof Error ? value.message : String(value);\n return new ViewerError(fallbackCode, message, { ...options, cause: value });\n}\n\n/**\n * A dynamic `import()` of an OPTIONAL peer that is not installed.\n *\n * Bundlers and Node report this differently (`ERR_MODULE_NOT_FOUND`,\n * `Cannot find module`, `Failed to resolve module specifier`, Vite's\n * `Failed to fetch dynamically imported module`), so match on the shapes rather\n * than one runtime's wording. A false positive here is cheap — the user is told\n * to install a package instead of seeing a raw parse error — and a false\n * negative is the anyview failure mode this exists to avoid: an unhelpful\n * module-resolution stack trace where a \"install papaparse to open CSV files\"\n * message belongs.\n */\nexport function isModuleNotFound(value: unknown): boolean {\n if (!(value instanceof Error)) return false;\n if (\"code\" in value && value.code === \"ERR_MODULE_NOT_FOUND\") return true;\n return /cannot find module|failed to resolve|dynamically imported module|module not found/i.test(\n value.message,\n );\n}\n\n/**\n * The `parser-missing` error, worded the same wherever the peer turns out to be\n * absent.\n *\n * There are TWO such places, which is why this is a function and not a literal\n * at one call site. The registry catches it when the adapter MODULE cannot be\n * fetched; `FileViewerProvider` catches it when the module loaded fine and the\n * adapter's own `await import(\"mammoth\")` — inside `load()`, where every parser\n * engine is actually reached — rejects. Without the second, a consumer who\n * skipped an optional peer was told their file was damaged and offered a retry\n * that could never work.\n */\nexport function parserMissingError(\n adapterId: string,\n packages: string[],\n options: ViewerErrorOptions = {},\n): ViewerError {\n return new ViewerError(\n \"parser-missing\",\n packages.length > 0\n ? `The \"${adapterId}\" adapter needs ${packages.join(\", \")}, which is not installed.`\n : `The \"${adapterId}\" adapter could not be loaded.`,\n { ...options, packages },\n );\n}\n"],"mappings":";;;AAgCO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAAuB,SAAiB,UAA8B,CAAC,GAAG;AACpF,UAAM,SAAS,EAAE,OAAO,QAAQ,MAAM,CAAC;AACvC,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,WAAW,QAAQ;AAAA,EAC1B;AACF;AAGO,SAAS,cAAc,OAAsC;AAClE,SAAO,iBAAiB,eAAgB,iBAAiB,SAAS,MAAM,SAAS;AACnF;AAGO,SAAS,QAAQ,OAAyB;AAC/C,MAAI,cAAc,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO,iBAAiB,UAAU,MAAM,SAAS,gBAAgB,MAAM,SAAS;AAClF;AAOO,SAAS,cACd,OACA,cACA,UAA8B,CAAC,GAClB;AACb,MAAI,cAAc,KAAK,EAAG,QAAO;AACjC,MAAI,QAAQ,KAAK,GAAG;AAClB,WAAO,IAAI,YAAY,WAAW,gCAAgC,EAAE,GAAG,SAAS,OAAO,MAAM,CAAC;AAAA,EAChG;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,IAAI,YAAY,cAAc,SAAS,EAAE,GAAG,SAAS,OAAO,MAAM,CAAC;AAC5E;AAcO,SAAS,iBAAiB,OAAyB;AACxD,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,MAAI,UAAU,SAAS,MAAM,SAAS,uBAAwB,QAAO;AACrE,SAAO,qFAAqF;AAAA,IAC1F,MAAM;AAAA,EACR;AACF;AAcO,SAAS,mBACd,WACA,UACA,UAA8B,CAAC,GAClB;AACb,SAAO,IAAI;AAAA,IACT;AAAA,IACA,SAAS,SAAS,IACd,QAAQ,SAAS,mBAAmB,SAAS,KAAK,IAAI,CAAC,8BACvD,QAAQ,SAAS;AAAA,IACrB,EAAE,GAAG,SAAS,SAAS;AAAA,EACzB;AACF;","names":[]}
@@ -0,0 +1,93 @@
1
+ "use client";
2
+ import {
3
+ useFileViewer
4
+ } from "./chunk-CLOI4HBA.js";
5
+
6
+ // src/file-viewer/file-viewer-pager.tsx
7
+ import { cn, IconButton, Input, useLocale } from "@elabs-ai/components-ui";
8
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
9
+ import { forwardRef, useState } from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ var FileViewerPager = forwardRef(
12
+ function FileViewerPager2({ className, ...props }, ref) {
13
+ const { state, actions } = useFileViewer();
14
+ const { t, formatNumber } = useLocale();
15
+ const [draft, setDraft] = useState(null);
16
+ const { pageNumber, pageCount } = state;
17
+ if (!state.capabilities.pages || pageCount === 0) return null;
18
+ const commit = () => {
19
+ if (draft === null) return;
20
+ const parsed = Number.parseInt(draft, 10);
21
+ setDraft(null);
22
+ if (Number.isNaN(parsed)) return;
23
+ actions.goToPage(parsed);
24
+ };
25
+ return /* @__PURE__ */ jsxs(
26
+ "div",
27
+ {
28
+ ref,
29
+ "data-slot": "file-viewer-pager",
30
+ role: "group",
31
+ "aria-label": t("viewer.pager.controls"),
32
+ className: cn("flex shrink-0 items-center gap-1", className),
33
+ ...props,
34
+ children: [
35
+ /* @__PURE__ */ jsx(
36
+ IconButton,
37
+ {
38
+ variant: "ghost",
39
+ size: "icon-sm",
40
+ label: t("viewer.pager.previous"),
41
+ icon: /* @__PURE__ */ jsx(ChevronLeftIcon, { "aria-hidden": "true" }),
42
+ disabled: pageNumber <= 1,
43
+ onClick: actions.previousPage
44
+ }
45
+ ),
46
+ /* @__PURE__ */ jsx(
47
+ Input,
48
+ {
49
+ type: "text",
50
+ inputMode: "numeric",
51
+ autoComplete: "off",
52
+ spellCheck: false,
53
+ "aria-label": t("viewer.pager.pageNumber"),
54
+ value: draft ?? String(pageNumber),
55
+ onChange: (event) => setDraft(event.target.value),
56
+ onBlur: commit,
57
+ onKeyDown: (event) => {
58
+ if (event.key === "Enter") {
59
+ event.preventDefault();
60
+ commit();
61
+ } else if (event.key === "Escape") {
62
+ setDraft(null);
63
+ }
64
+ },
65
+ className: "h-7 w-12 px-1 text-center tabular-nums"
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx("span", { className: "text-meta text-muted-foreground whitespace-nowrap tabular-nums", children: t("viewer.pager.of", { total: formatNumber(pageCount) }) }),
69
+ /* @__PURE__ */ jsx(
70
+ IconButton,
71
+ {
72
+ variant: "ghost",
73
+ size: "icon-sm",
74
+ label: t("viewer.pager.next"),
75
+ icon: /* @__PURE__ */ jsx(ChevronRightIcon, { "aria-hidden": "true" }),
76
+ disabled: pageNumber >= pageCount,
77
+ onClick: actions.nextPage
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("viewer.pager.status", {
81
+ page: formatNumber(pageNumber),
82
+ total: formatNumber(pageCount)
83
+ }) })
84
+ ]
85
+ }
86
+ );
87
+ }
88
+ );
89
+
90
+ export {
91
+ FileViewerPager
92
+ };
93
+ //# sourceMappingURL=chunk-LRPZ373O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/file-viewer/file-viewer-pager.tsx"],"sourcesContent":["\"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"],"mappings":";;;;;;AAoBA,SAAS,IAAI,YAAY,OAAO,iBAAiB;AACjD,SAAS,iBAAiB,wBAAwB;AAClD,SAAS,YAAY,gBAAqC;AAiCpD,SAYU,KAZV;AA3BC,IAAM,kBAAkB;AAAA,EAC7B,SAASA,iBAAgB,EAAE,WAAW,GAAG,MAAM,GAAG,KAAK;AACrD,UAAM,EAAE,OAAO,QAAQ,IAAI,cAAc;AACzC,UAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AAKtC,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AAEtD,UAAM,EAAE,YAAY,UAAU,IAAI;AAIlC,QAAI,CAAC,MAAM,aAAa,SAAS,cAAc,EAAG,QAAO;AAEzD,UAAM,SAAS,MAAM;AACnB,UAAI,UAAU,KAAM;AACpB,YAAM,SAAS,OAAO,SAAS,OAAO,EAAE;AACxC,eAAS,IAAI;AAGb,UAAI,OAAO,MAAM,MAAM,EAAG;AAC1B,cAAQ,SAAS,MAAM;AAAA,IACzB;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,aAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAY,EAAE,uBAAuB;AAAA,QACrC,WAAW,GAAG,oCAAoC,SAAS;AAAA,QAC1D,GAAG;AAAA,QAEJ;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,OAAO,EAAE,uBAAuB;AAAA,cAChC,MAAM,oBAAC,mBAAgB,eAAY,QAAO;AAAA,cAC1C,UAAU,cAAc;AAAA,cACxB,SAAS,QAAQ;AAAA;AAAA,UACnB;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cAIC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAa;AAAA,cACb,YAAY;AAAA,cACZ,cAAY,EAAE,yBAAyB;AAAA,cACvC,OAAO,SAAS,OAAO,UAAU;AAAA,cACjC,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,cAChD,QAAQ;AAAA,cACR,WAAW,CAAC,UAAU;AACpB,oBAAI,MAAM,QAAQ,SAAS;AACzB,wBAAM,eAAe;AACrB,yBAAO;AAAA,gBACT,WAAW,MAAM,QAAQ,UAAU;AAGjC,2BAAS,IAAI;AAAA,gBACf;AAAA,cACF;AAAA,cACA,WAAU;AAAA;AAAA,UACZ;AAAA,UACA,oBAAC,UAAK,WAAU,kEACb,YAAE,mBAAmB,EAAE,OAAO,aAAa,SAAS,EAAE,CAAC,GAC1D;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,OAAO,EAAE,mBAAmB;AAAA,cAC5B,MAAM,oBAAC,oBAAiB,eAAY,QAAO;AAAA,cAC3C,UAAU,cAAc;AAAA,cACxB,SAAS,QAAQ;AAAA;AAAA,UACnB;AAAA,UAKA,oBAAC,UAAK,MAAK,UAAS,aAAU,UAAS,WAAU,WAC9C,YAAE,uBAAuB;AAAA,YACxB,MAAM,aAAa,UAAU;AAAA,YAC7B,OAAO,aAAa,SAAS;AAAA,UAC/B,CAAC,GACH;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;","names":["FileViewerPager"]}
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import {
3
+ PROTOCOL_VERSION
4
+ } from "./chunk-CIT2HHRI.js";
5
+
6
+ // src/adapters/pdf/pdf-manifest.ts
7
+ var pdfManifest = {
8
+ id: "pdf",
9
+ protocol: PROTOCOL_VERSION,
10
+ extensions: ["pdf"],
11
+ mediaTypes: ["application/pdf"],
12
+ capabilities: {
13
+ pages: true,
14
+ zoom: true,
15
+ text: true,
16
+ highlight: ["quote", "range", "rect"]
17
+ },
18
+ requires: ["pdfjs-dist"]
19
+ };
20
+
21
+ export {
22
+ pdfManifest
23
+ };
24
+ //# sourceMappingURL=chunk-M77ICVWP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/pdf/pdf-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only. Split from the adapter module so `createDefaultRegistry`\n * can answer \"can this be opened, and what controls apply\" without pulling\n * pdf.js — a ~1 MB engine — into the entry chunk.\n *\n * `requires` is what the \"install this\" message names when `pdfjs-dist` is not\n * present: the adapter is registered either way, so the reader is told what is\n * missing instead of being told the file type is unsupported.\n *\n * The only adapter that declares `rect`: a PDF page has a fixed coordinate\n * space, so a producer that knows where a passage sits on the page — an OCR\n * pipeline, a layout-aware chunker — can address it geometrically and skip the\n * text projection entirely.\n */\nexport const pdfManifest: AdapterManifest = {\n id: \"pdf\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"pdf\"],\n mediaTypes: [\"application/pdf\"],\n capabilities: {\n pages: true,\n zoom: true,\n text: true,\n highlight: [\"quote\", \"range\", \"rect\"],\n },\n requires: [\"pdfjs-dist\"],\n};\n"],"mappings":";;;;;;AAiBO,IAAM,cAA+B;AAAA,EAC1C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,KAAK;AAAA,EAClB,YAAY,CAAC,iBAAiB;AAAA,EAC9B,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW,CAAC,SAAS,SAAS,MAAM;AAAA,EACtC;AAAA,EACA,UAAU,CAAC,YAAY;AACzB;","names":[]}
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ // src/core/use-page-control.ts
4
+ import { useCallback, useState } from "react";
5
+ function usePageControl(pageNumber, onPageChange, pageCount) {
6
+ const controlled = pageNumber !== void 0;
7
+ const [own, setOwn] = useState(1);
8
+ const raw = controlled ? pageNumber : own;
9
+ const current = pageCount > 0 ? Math.min(Math.max(1, raw), pageCount) : Math.max(1, raw);
10
+ const goToPage = useCallback(
11
+ (page) => {
12
+ const next = pageCount > 0 ? Math.min(Math.max(1, page), pageCount) : Math.max(1, page);
13
+ if (!controlled) setOwn(next);
14
+ onPageChange?.(next);
15
+ },
16
+ [controlled, onPageChange, pageCount]
17
+ );
18
+ return [current, goToPage];
19
+ }
20
+
21
+ export {
22
+ usePageControl
23
+ };
24
+ //# sourceMappingURL=chunk-MJUH32ZK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/use-page-control.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The page half of the renderer contract, as one hook.\n *\n * `AdapterRendererProps.pageNumber` is optional on purpose: inside a\n * `FileViewerProvider` the shell owns the page so a control can live anywhere,\n * but a `Renderer` mounted on its own — a test, an embed that wants nothing but\n * the canvas — must still be able to turn a page. That is the ordinary\n * controlled/uncontrolled trio (`component-api.md`), and both paginated adapters\n * need exactly the same six lines of it.\n */\n\nimport { useCallback, useState } from \"react\";\n\n/**\n * @param pageNumber The controlled page, or `undefined` for uncontrolled.\n * @param onPageChange Reported on every navigation, controlled or not.\n * @param pageCount Upper bound. `0` while the count is not known yet.\n * @returns The page to render, and the function that navigates to another.\n */\nexport function usePageControl(\n pageNumber: number | undefined,\n onPageChange: ((page: number) => void) | undefined,\n pageCount: number,\n): readonly [number, (page: number) => void] {\n const controlled = pageNumber !== undefined;\n const [own, setOwn] = useState(1);\n\n // Clamped on READ, not on write: a controlled owner that has not yet heard\n // about a shorter document would otherwise render page 40 of 12 and blank the\n // canvas, and clamping its prop back at it would fight the owner.\n const raw = controlled ? pageNumber : own;\n const current = pageCount > 0 ? Math.min(Math.max(1, raw), pageCount) : Math.max(1, raw);\n\n const goToPage = useCallback(\n (page: number) => {\n const next = pageCount > 0 ? Math.min(Math.max(1, page), pageCount) : Math.max(1, page);\n // Mirroring the platform: a controlled value is never written locally.\n if (!controlled) setOwn(next);\n onPageChange?.(next);\n },\n [controlled, onPageChange, pageCount],\n );\n\n return [current, goToPage] as const;\n}\n"],"mappings":";;;AAaA,SAAS,aAAa,gBAAgB;AAQ/B,SAAS,eACd,YACA,cACA,WAC2C;AAC3C,QAAM,aAAa,eAAe;AAClC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAKhC,QAAM,MAAM,aAAa,aAAa;AACtC,QAAM,UAAU,YAAY,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,GAAG,GAAG,SAAS,IAAI,KAAK,IAAI,GAAG,GAAG;AAEvF,QAAM,WAAW;AAAA,IACf,CAAC,SAAiB;AAChB,YAAM,OAAO,YAAY,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS,IAAI,KAAK,IAAI,GAAG,IAAI;AAEtF,UAAI,CAAC,WAAY,QAAO,IAAI;AAC5B,qBAAe,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,YAAY,cAAc,SAAS;AAAA,EACtC;AAEA,SAAO,CAAC,SAAS,QAAQ;AAC3B;","names":[]}
@@ -0,0 +1,162 @@
1
+ "use client";
2
+ import {
3
+ useFileViewer
4
+ } from "./chunk-CLOI4HBA.js";
5
+ import {
6
+ FIND_MATCH_LIMIT
7
+ } from "./chunk-CO2VJRSU.js";
8
+
9
+ // src/file-viewer/file-viewer-find.tsx
10
+ import {
11
+ cn,
12
+ IconButton,
13
+ InputGroup,
14
+ InputGroupAddon,
15
+ InputGroupButton,
16
+ InputGroupInput,
17
+ Separator,
18
+ useLocale
19
+ } from "@elabs-ai/components-ui";
20
+ import { CaseSensitiveIcon, ChevronDownIcon, ChevronUpIcon, SearchIcon, XIcon } from "lucide-react";
21
+ import {
22
+ forwardRef,
23
+ useEffect,
24
+ useRef
25
+ } from "react";
26
+ import { jsx, jsxs } from "react/jsx-runtime";
27
+ function isFindShortcut(event) {
28
+ return event.key.toLowerCase() === "f" && (event.metaKey || event.ctrlKey);
29
+ }
30
+ var FileViewerFind = forwardRef(
31
+ function FileViewerFind2({ className, ...props }, ref) {
32
+ const { state, actions, meta } = useFileViewer();
33
+ const { t, formatNumber } = useLocale();
34
+ const input = useRef(null);
35
+ const find = state.find;
36
+ const open = find.open && meta.canFind;
37
+ const { registerFind } = actions;
38
+ useEffect(() => registerFind(), [registerFind]);
39
+ useEffect(() => {
40
+ if (open) input.current?.focus();
41
+ }, [open]);
42
+ if (!open) return null;
43
+ const hasQuery = find.query.length > 0;
44
+ const hasMatches = find.matches > 0;
45
+ const onKeyDown = (event) => {
46
+ if (event.key === "Escape") {
47
+ event.preventDefault();
48
+ event.stopPropagation();
49
+ actions.closeFind();
50
+ return;
51
+ }
52
+ if (event.key === "Enter") {
53
+ event.preventDefault();
54
+ if (event.shiftKey) actions.previousFindMatch();
55
+ else actions.nextFindMatch();
56
+ }
57
+ };
58
+ return /* @__PURE__ */ jsxs(
59
+ "div",
60
+ {
61
+ ref,
62
+ "data-slot": "file-viewer-find",
63
+ role: "search",
64
+ "aria-label": t("viewer.find.label"),
65
+ className: cn(
66
+ // The divider is the only cue between this row and the content below
67
+ // it — same fill, no elevation change (WCAG 1.4.11, strong rung).
68
+ "border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2",
69
+ className
70
+ ),
71
+ ...props,
72
+ children: [
73
+ /* @__PURE__ */ jsxs(InputGroup, { className: "h-8 max-w-72", children: [
74
+ /* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(SearchIcon, { "aria-hidden": "true" }) }),
75
+ /* @__PURE__ */ jsx(
76
+ InputGroupInput,
77
+ {
78
+ ref: input,
79
+ type: "text",
80
+ value: find.query,
81
+ "aria-label": t("viewer.find.label"),
82
+ placeholder: t("viewer.find.placeholder"),
83
+ autoComplete: "off",
84
+ spellCheck: false,
85
+ onChange: (event) => actions.setFindQuery(event.target.value),
86
+ onKeyDown
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(InputGroupAddon, { align: "inline-end", children: /* @__PURE__ */ jsx(
90
+ InputGroupButton,
91
+ {
92
+ size: "icon-xs",
93
+ "aria-pressed": find.caseSensitive,
94
+ "aria-label": t("viewer.find.caseSensitive"),
95
+ title: t("viewer.find.caseSensitive"),
96
+ onClick: () => actions.setFindCaseSensitive(!find.caseSensitive),
97
+ children: /* @__PURE__ */ jsx(CaseSensitiveIcon, { "aria-hidden": "true" })
98
+ }
99
+ ) })
100
+ ] }),
101
+ /* @__PURE__ */ jsxs(
102
+ "span",
103
+ {
104
+ role: "status",
105
+ "aria-live": "polite",
106
+ className: "text-meta text-muted-foreground min-w-0 flex-1 truncate tabular-nums",
107
+ children: [
108
+ !hasQuery ? null : hasMatches ? t("viewer.find.count", {
109
+ index: formatNumber(find.activeIndex + 1),
110
+ total: formatNumber(Math.min(find.matches, FIND_MATCH_LIMIT))
111
+ }) : t("viewer.find.none"),
112
+ find.truncated ? ` ${t("viewer.find.capped", {
113
+ limit: formatNumber(FIND_MATCH_LIMIT),
114
+ total: formatNumber(find.matches)
115
+ })}` : null
116
+ ]
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsx(
120
+ IconButton,
121
+ {
122
+ variant: "ghost",
123
+ size: "icon-sm",
124
+ "aria-disabled": !hasMatches,
125
+ label: t("viewer.find.previous"),
126
+ icon: /* @__PURE__ */ jsx(ChevronUpIcon, { "aria-hidden": "true" }),
127
+ onClick: actions.previousFindMatch
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsx(
131
+ IconButton,
132
+ {
133
+ variant: "ghost",
134
+ size: "icon-sm",
135
+ "aria-disabled": !hasMatches,
136
+ label: t("viewer.find.next"),
137
+ icon: /* @__PURE__ */ jsx(ChevronDownIcon, { "aria-hidden": "true" }),
138
+ onClick: actions.nextFindMatch
139
+ }
140
+ ),
141
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-4" }),
142
+ /* @__PURE__ */ jsx(
143
+ IconButton,
144
+ {
145
+ variant: "ghost",
146
+ size: "icon-sm",
147
+ label: t("viewer.find.close"),
148
+ icon: /* @__PURE__ */ jsx(XIcon, { "aria-hidden": "true" }),
149
+ onClick: actions.closeFind
150
+ }
151
+ )
152
+ ]
153
+ }
154
+ );
155
+ }
156
+ );
157
+
158
+ export {
159
+ isFindShortcut,
160
+ FileViewerFind
161
+ };
162
+ //# sourceMappingURL=chunk-PAIVK4N6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/file-viewer/file-viewer-find.tsx"],"sourcesContent":["\"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"],"mappings":"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","names":["FileViewerFind"]}
@@ -0,0 +1,129 @@
1
+ "use client";
2
+ import {
3
+ useFileViewer
4
+ } from "./chunk-CLOI4HBA.js";
5
+ import {
6
+ VIEWER_ZOOM_STEPS,
7
+ canStepZoom,
8
+ isZoomFit
9
+ } from "./chunk-SASY77ZL.js";
10
+
11
+ // src/file-viewer/file-viewer-zoom.tsx
12
+ import {
13
+ cn,
14
+ IconButton,
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectSeparator,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ useLocale
22
+ } from "@elabs-ai/components-ui";
23
+ import { RotateCwIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
24
+ import { forwardRef } from "react";
25
+ import { jsx, jsxs } from "react/jsx-runtime";
26
+ var FileViewerZoom = forwardRef(
27
+ function FileViewerZoom2({ className, ...props }, ref) {
28
+ const { state, actions } = useFileViewer();
29
+ const { t, formatNumber } = useLocale();
30
+ if (!state.capabilities.zoom) return null;
31
+ const { zoom, effectiveZoom } = state;
32
+ const percent = (scale) => formatNumber(scale, { style: "percent", maximumFractionDigits: 0 });
33
+ return /* @__PURE__ */ jsxs(
34
+ "div",
35
+ {
36
+ ref,
37
+ "data-slot": "file-viewer-zoom",
38
+ role: "group",
39
+ "aria-label": t("viewer.zoom.controls"),
40
+ className: cn("flex shrink-0 items-center gap-1", className),
41
+ ...props,
42
+ children: [
43
+ /* @__PURE__ */ jsx(
44
+ IconButton,
45
+ {
46
+ variant: "ghost",
47
+ size: "icon-sm",
48
+ label: t("viewer.zoom.out"),
49
+ icon: /* @__PURE__ */ jsx(ZoomOutIcon, { "aria-hidden": "true" }),
50
+ disabled: !canStepZoom(effectiveZoom, -1),
51
+ onClick: actions.zoomOut
52
+ }
53
+ ),
54
+ /* @__PURE__ */ jsxs(
55
+ Select,
56
+ {
57
+ value: zoomToValue(zoom),
58
+ onValueChange: (value) => actions.setZoom(valueToZoom(value)),
59
+ children: [
60
+ /* @__PURE__ */ jsx(
61
+ SelectTrigger,
62
+ {
63
+ size: "sm",
64
+ "aria-label": t("viewer.zoom.level"),
65
+ className: "h-7 w-24 gap-1 px-2",
66
+ children: /* @__PURE__ */ jsx(SelectValue, { children: isZoomFit(zoom) ? t(zoom === "fit-width" ? "viewer.zoom.fitWidth" : "viewer.zoom.fitPage") : percent(zoom) })
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
70
+ /* @__PURE__ */ jsx(SelectItem, { value: "fit-width", children: t("viewer.zoom.fitWidth") }),
71
+ /* @__PURE__ */ jsx(SelectItem, { value: "fit-page", children: t("viewer.zoom.fitPage") }),
72
+ /* @__PURE__ */ jsx(SelectSeparator, {}),
73
+ VIEWER_ZOOM_STEPS.map((step) => /* @__PURE__ */ jsx(SelectItem, { value: String(step), children: percent(step) }, step))
74
+ ] })
75
+ ]
76
+ }
77
+ ),
78
+ /* @__PURE__ */ jsx(
79
+ IconButton,
80
+ {
81
+ variant: "ghost",
82
+ size: "icon-sm",
83
+ label: t("viewer.zoom.in"),
84
+ icon: /* @__PURE__ */ jsx(ZoomInIcon, { "aria-hidden": "true" }),
85
+ disabled: !canStepZoom(effectiveZoom, 1),
86
+ onClick: actions.zoomIn
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("viewer.zoom.status", { level: percent(effectiveZoom) }) })
90
+ ]
91
+ }
92
+ );
93
+ }
94
+ );
95
+ function zoomToValue(zoom) {
96
+ return isZoomFit(zoom) ? zoom : String(zoom);
97
+ }
98
+ function valueToZoom(value) {
99
+ if (value === "fit-width" || value === "fit-page") return value;
100
+ const parsed = Number.parseFloat(value);
101
+ return Number.isNaN(parsed) ? 1 : parsed;
102
+ }
103
+ var FileViewerRotate = forwardRef(
104
+ function FileViewerRotate2({ className, ...props }, ref) {
105
+ const { state, actions } = useFileViewer();
106
+ const { t } = useLocale();
107
+ if (!state.capabilities.rotate) return null;
108
+ return /* @__PURE__ */ jsx(
109
+ IconButton,
110
+ {
111
+ ref,
112
+ "data-slot": "file-viewer-rotate",
113
+ variant: "ghost",
114
+ size: "icon-sm",
115
+ label: t("viewer.rotate"),
116
+ icon: /* @__PURE__ */ jsx(RotateCwIcon, { "aria-hidden": "true" }),
117
+ className: cn("shrink-0", className),
118
+ onClick: () => actions.rotate(1),
119
+ ...props
120
+ }
121
+ );
122
+ }
123
+ );
124
+
125
+ export {
126
+ FileViewerZoom,
127
+ FileViewerRotate
128
+ };
129
+ //# sourceMappingURL=chunk-PKYDKXCO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/file-viewer/file-viewer-zoom.tsx"],"sourcesContent":["\"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":["FileViewerZoom","FileViewerRotate"]}
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import {
3
+ createRegistry
4
+ } from "./chunk-BWMXMHVZ.js";
5
+ import {
6
+ pptxManifest
7
+ } from "./chunk-FDT4QOIG.js";
8
+ import {
9
+ textManifest
10
+ } from "./chunk-ZAVXZRIO.js";
11
+ import {
12
+ xlsxManifest
13
+ } from "./chunk-7EASJBQM.js";
14
+ import {
15
+ mediaManifest
16
+ } from "./chunk-YA72QE57.js";
17
+ import {
18
+ pdfManifest
19
+ } from "./chunk-M77ICVWP.js";
20
+ import {
21
+ imageManifest
22
+ } from "./chunk-HUUJIFFT.js";
23
+ import {
24
+ jsonManifest
25
+ } from "./chunk-GH4UBYST.js";
26
+ import {
27
+ markdownManifest
28
+ } from "./chunk-YWHFMCS4.js";
29
+ import {
30
+ codeManifest
31
+ } from "./chunk-CMYBLW7D.js";
32
+ import {
33
+ csvManifest
34
+ } from "./chunk-EA72HA4I.js";
35
+ import {
36
+ docxManifest
37
+ } from "./chunk-I5XD2NJ6.js";
38
+
39
+ // src/adapters/index.ts
40
+ function createDefaultRegistry() {
41
+ const registry = createRegistry();
42
+ registry.register(imageManifest, () => import("./adapters/image/image-adapter.js"));
43
+ registry.register(jsonManifest, () => import("./adapters/json/json-adapter.js"));
44
+ registry.register(csvManifest, () => import("./adapters/csv/csv-adapter.js"));
45
+ registry.register(pdfManifest, () => import("./adapters/pdf/pdf-adapter.js"));
46
+ registry.register(mediaManifest, () => import("./adapters/media/media-adapter.js"));
47
+ registry.register(docxManifest, () => import("./adapters/docx/docx-adapter.js"));
48
+ registry.register(xlsxManifest, () => import("./adapters/xlsx/xlsx-adapter.js"));
49
+ registry.register(pptxManifest, () => import("./adapters/pptx/pptx-adapter.js"));
50
+ registry.register(markdownManifest, () => import("./adapters/markdown/markdown-adapter.js"));
51
+ registry.register(codeManifest, () => import("./adapters/code/code-adapter.js"));
52
+ registry.register(textManifest, () => import("./adapters/text/text-adapter.js"));
53
+ return registry;
54
+ }
55
+
56
+ export {
57
+ createDefaultRegistry
58
+ };
59
+ //# sourceMappingURL=chunk-QIPKBOZJ.js.map