@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
@@ -0,0 +1,733 @@
1
+ "use client";
2
+ import {
3
+ FileViewerFind,
4
+ isFindShortcut
5
+ } from "./chunk-PAIVK4N6.js";
6
+ import {
7
+ FileViewerPager
8
+ } from "./chunk-LRPZ373O.js";
9
+ import {
10
+ FileViewerRotate,
11
+ FileViewerZoom
12
+ } from "./chunk-PKYDKXCO.js";
13
+ import {
14
+ FileViewerContext,
15
+ useFileViewer
16
+ } from "./chunk-CLOI4HBA.js";
17
+ import {
18
+ resolveHighlights
19
+ } from "./chunk-FQHZEQUH.js";
20
+ import {
21
+ FIND_MATCH_LIMIT,
22
+ findMatchId
23
+ } from "./chunk-CO2VJRSU.js";
24
+ import {
25
+ DEFAULT_ZOOM,
26
+ stepZoom
27
+ } from "./chunk-SASY77ZL.js";
28
+ import {
29
+ createDefaultRegistry
30
+ } from "./chunk-QIPKBOZJ.js";
31
+ import {
32
+ ViewerError,
33
+ isAbort,
34
+ isModuleNotFound,
35
+ parserMissingError,
36
+ toViewerError
37
+ } from "./chunk-JHFC2KBI.js";
38
+
39
+ // src/file-viewer/file-viewer.tsx
40
+ import {
41
+ Button,
42
+ cn,
43
+ downloadBlob,
44
+ fileIconFor,
45
+ IconButton,
46
+ normalizeFileSource,
47
+ normalizeQuoteTextWithOffsets,
48
+ queryToRanges,
49
+ resolveFileKind,
50
+ Separator,
51
+ Skeleton,
52
+ StatePanel,
53
+ Text,
54
+ useLocale
55
+ } from "@elabs-ai/components-ui";
56
+ import { DownloadIcon, EyeOffIcon, SearchXIcon } from "lucide-react";
57
+ import {
58
+ forwardRef,
59
+ useCallback,
60
+ useDeferredValue,
61
+ useEffect,
62
+ useMemo,
63
+ useRef,
64
+ useState
65
+ } from "react";
66
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
67
+ var NO_HIGHLIGHTS = [];
68
+ var NO_RESOLVED = [];
69
+ var NO_SUPPORT = [];
70
+ var NO_MATCHES = { highlights: NO_HIGHLIGHTS, total: 0, truncated: false };
71
+ function FileViewerProvider({
72
+ source,
73
+ registry: registryProp,
74
+ loading = false,
75
+ baseHeadingLevel = 2,
76
+ highlights: highlightsProp,
77
+ defaultHighlights,
78
+ onHighlightsChange,
79
+ activeHighlightId: activeHighlightIdProp,
80
+ defaultActiveHighlightId = null,
81
+ onActiveHighlightChange,
82
+ pageNumber: pageNumberProp,
83
+ defaultPageNumber = 1,
84
+ onPageNumberChange,
85
+ zoom: zoomProp,
86
+ defaultZoom = "fit-width",
87
+ onZoomChange,
88
+ rotation: rotationProp,
89
+ defaultRotation = 0,
90
+ onRotationChange,
91
+ children
92
+ }) {
93
+ const fallbackRegistry = useMemo(() => createDefaultRegistry(), []);
94
+ const registry = registryProp ?? fallbackRegistry;
95
+ const [attempt, setAttempt] = useState(0);
96
+ const [state, setState] = useState({ status: "empty", capabilities: {} });
97
+ const resolved = useMemo(() => source ? normalizeFileSource(source) : void 0, [source]);
98
+ useEffect(() => {
99
+ if (!resolved) {
100
+ setState({ status: "empty", capabilities: {} });
101
+ return;
102
+ }
103
+ const controller = new AbortController();
104
+ let instance;
105
+ let cancelled = false;
106
+ const kind = resolveFileKind(resolved.name, resolved.mediaType);
107
+ const manifest = registry.detect(kind);
108
+ setState({
109
+ status: "loading",
110
+ source: resolved,
111
+ capabilities: manifest?.capabilities ?? {}
112
+ });
113
+ void (async () => {
114
+ try {
115
+ if (!manifest) {
116
+ throw new ViewerError("unsupported-format", `No adapter can open "${resolved.name}".`, {
117
+ fileName: resolved.name
118
+ });
119
+ }
120
+ const adapter = await registry.load(manifest.id);
121
+ const parser = adapter.create();
122
+ instance = parser;
123
+ const document = await parser.load(resolved, { signal: controller.signal });
124
+ if (cancelled) return;
125
+ setState({
126
+ status: "ready",
127
+ source: resolved,
128
+ document,
129
+ adapter,
130
+ capabilities: manifest.capabilities ?? {}
131
+ });
132
+ } catch (error) {
133
+ if (cancelled || isAbort(error)) return;
134
+ const missingPeer = isModuleNotFound(error) && manifest ? parserMissingError(manifest.id, manifest.requires ?? [], {
135
+ fileName: resolved.name,
136
+ cause: error
137
+ }) : void 0;
138
+ setState({
139
+ status: "error",
140
+ source: resolved,
141
+ capabilities: manifest?.capabilities ?? {},
142
+ error: missingPeer ?? toViewerError(error, "parse-failed", { fileName: resolved.name })
143
+ });
144
+ }
145
+ })();
146
+ return () => {
147
+ cancelled = true;
148
+ controller.abort();
149
+ instance?.dispose?.();
150
+ resolved.revoke();
151
+ };
152
+ }, [resolved, registry, attempt]);
153
+ const highlightsControlled = highlightsProp !== void 0;
154
+ const [ownHighlights, setOwnHighlights] = useState(defaultHighlights ?? NO_HIGHLIGHTS);
155
+ const highlights = highlightsControlled ? highlightsProp : ownHighlights;
156
+ const setHighlights = useCallback(
157
+ (next) => {
158
+ if (!highlightsControlled) setOwnHighlights(next);
159
+ onHighlightsChange?.(next);
160
+ },
161
+ [highlightsControlled, onHighlightsChange]
162
+ );
163
+ const activeControlled = activeHighlightIdProp !== void 0;
164
+ const [ownActiveId, setOwnActiveId] = useState(defaultActiveHighlightId);
165
+ const activeHighlightId = activeControlled ? activeHighlightIdProp : ownActiveId;
166
+ const setActiveHighlight = useCallback(
167
+ (id) => {
168
+ if (!activeControlled) setOwnActiveId(id);
169
+ onActiveHighlightChange?.(id);
170
+ },
171
+ [activeControlled, onActiveHighlightChange]
172
+ );
173
+ const pageCount = state.document?.pageCount ?? 0;
174
+ const pageControlled = pageNumberProp !== void 0;
175
+ const [ownPage, setOwnPage] = useState(defaultPageNumber);
176
+ const rawPage = pageControlled ? pageNumberProp : ownPage;
177
+ const pageNumber = pageCount > 0 ? Math.min(Math.max(1, rawPage), pageCount) : Math.max(1, rawPage);
178
+ const goToPage = useCallback(
179
+ (page) => {
180
+ const next = Math.max(1, Math.round(page));
181
+ if (!pageControlled) setOwnPage(next);
182
+ onPageNumberChange?.(next);
183
+ },
184
+ [pageControlled, onPageNumberChange]
185
+ );
186
+ const zoomControlled = zoomProp !== void 0;
187
+ const [ownZoom, setOwnZoom] = useState(defaultZoom);
188
+ const zoom = zoomControlled ? zoomProp : ownZoom;
189
+ const setZoom = useCallback(
190
+ (next) => {
191
+ if (!zoomControlled) setOwnZoom(next);
192
+ onZoomChange?.(next);
193
+ },
194
+ [zoomControlled, onZoomChange]
195
+ );
196
+ const [reportedZoom, setReportedZoom] = useState(DEFAULT_ZOOM);
197
+ const effectiveZoom = typeof zoom === "number" ? zoom : reportedZoom;
198
+ const rotationControlled = rotationProp !== void 0;
199
+ const [ownRotation, setOwnRotation] = useState(defaultRotation);
200
+ const rotation = rotationControlled ? rotationProp : ownRotation;
201
+ const setRotation = useCallback(
202
+ (next) => {
203
+ if (!rotationControlled) setOwnRotation(next);
204
+ onRotationChange?.(next);
205
+ },
206
+ [rotationControlled, onRotationChange]
207
+ );
208
+ const previousSource = useRef(resolved);
209
+ useEffect(() => {
210
+ if (previousSource.current === resolved) return;
211
+ previousSource.current = resolved;
212
+ setOwnPage(1);
213
+ setOwnRotation(0);
214
+ }, [resolved]);
215
+ const [find, setFind] = useState({
216
+ open: false,
217
+ query: "",
218
+ caseSensitive: false,
219
+ activeIndex: 0
220
+ });
221
+ const [findParts, setFindParts] = useState(0);
222
+ const registerFind = useCallback(() => {
223
+ setFindParts((count) => count + 1);
224
+ return () => setFindParts((count) => count - 1);
225
+ }, []);
226
+ const text = state.document?.text;
227
+ const capabilities = state.capabilities;
228
+ const support = capabilities.highlight ?? NO_SUPPORT;
229
+ const canFind = (capabilities.search ?? true) && text !== void 0 && support.includes("range");
230
+ const query = useDeferredValue(find.query);
231
+ const findMatches = useMemo(() => {
232
+ if (!find.open || !canFind || text === void 0 || query.length === 0) return NO_MATCHES;
233
+ const ranges = queryToRanges(text, query, find.caseSensitive);
234
+ return {
235
+ highlights: ranges.slice(0, FIND_MATCH_LIMIT).map(([start, end], index) => ({
236
+ id: findMatchId(index),
237
+ address: { kind: "range", start, end },
238
+ source: "search"
239
+ })),
240
+ total: ranges.length,
241
+ truncated: ranges.length > FIND_MATCH_LIMIT
242
+ };
243
+ }, [find.open, find.caseSensitive, canFind, text, query]);
244
+ const normalized = useMemo(
245
+ () => text === void 0 ? void 0 : normalizeQuoteTextWithOffsets(text),
246
+ [text]
247
+ );
248
+ const activeFindId = find.open && findMatches.highlights.length > 0 ? findMatchId(find.activeIndex) : void 0;
249
+ const currentHighlightId = activeFindId ?? activeHighlightId;
250
+ const resolvedHighlights = useMemo(() => {
251
+ if (highlights.length === 0 && findMatches.highlights.length === 0) return NO_RESOLVED;
252
+ return resolveHighlights([...highlights, ...findMatches.highlights], {
253
+ normalized,
254
+ textLength: text?.length,
255
+ truncated: state.document?.textTruncated,
256
+ supported: support,
257
+ activeId: currentHighlightId
258
+ });
259
+ }, [
260
+ highlights,
261
+ findMatches.highlights,
262
+ normalized,
263
+ text?.length,
264
+ state.document?.textTruncated,
265
+ support,
266
+ currentHighlightId
267
+ ]);
268
+ const stepCitation = useCallback(
269
+ (delta) => {
270
+ const list = resolvedHighlights.filter(
271
+ (highlight) => highlight.source === "citation" && highlight.status === "resolved"
272
+ );
273
+ if (list.length === 0) return;
274
+ const at = list.findIndex((highlight) => highlight.id === activeHighlightId);
275
+ const to = at === -1 ? delta === 1 ? 0 : list.length - 1 : (at + delta + list.length) % list.length;
276
+ setActiveHighlight(list[to].id);
277
+ },
278
+ [resolvedHighlights, activeHighlightId, setActiveHighlight]
279
+ );
280
+ const stepFind = useCallback(
281
+ (delta) => {
282
+ const count = findMatches.highlights.length;
283
+ if (count === 0) return;
284
+ setFind((current) => ({
285
+ ...current,
286
+ activeIndex: (current.activeIndex + delta + count) % count
287
+ }));
288
+ },
289
+ [findMatches.highlights.length]
290
+ );
291
+ const findState = useMemo(
292
+ () => ({
293
+ open: find.open,
294
+ query: find.query,
295
+ caseSensitive: find.caseSensitive,
296
+ matches: findMatches.total,
297
+ truncated: findMatches.truncated,
298
+ activeIndex: find.activeIndex
299
+ }),
300
+ [find, findMatches.total, findMatches.truncated]
301
+ );
302
+ const actions = useMemo(
303
+ () => ({
304
+ reload: () => setAttempt((n) => n + 1),
305
+ setHighlights,
306
+ setActiveHighlight,
307
+ nextHighlight: () => stepCitation(1),
308
+ previousHighlight: () => stepCitation(-1),
309
+ openFind: () => setFind((current) => ({ ...current, open: true })),
310
+ // The query survives a close, so re-opening resumes where the reader was;
311
+ // only the box goes away.
312
+ closeFind: () => setFind((current) => ({ ...current, open: false })),
313
+ setFindQuery: (next) => setFind((current) => ({ ...current, query: next, activeIndex: 0 })),
314
+ setFindCaseSensitive: (next) => setFind((current) => ({ ...current, caseSensitive: next, activeIndex: 0 })),
315
+ nextFindMatch: () => stepFind(1),
316
+ previousFindMatch: () => stepFind(-1),
317
+ goToPage,
318
+ // Stop at the ends rather than wrap: a document is not a carousel, and a
319
+ // reader who holds "next" past the last page expects to stay there.
320
+ nextPage: () => goToPage(Math.min(pageNumber + 1, pageCount || pageNumber + 1)),
321
+ previousPage: () => goToPage(pageNumber - 1),
322
+ setZoom,
323
+ // Stepping from `effectiveZoom`, not from `zoom`: after a fit the ladder
324
+ // has to continue from what is actually on screen.
325
+ zoomIn: () => setZoom(stepZoom(effectiveZoom, 1)),
326
+ zoomOut: () => setZoom(stepZoom(effectiveZoom, -1)),
327
+ setRotation,
328
+ rotate: (quarterTurns) => setRotation(((rotation + quarterTurns * 90) % 360 + 360) % 360),
329
+ reportZoom: setReportedZoom,
330
+ registerFind
331
+ }),
332
+ [
333
+ setHighlights,
334
+ setActiveHighlight,
335
+ stepCitation,
336
+ stepFind,
337
+ registerFind,
338
+ goToPage,
339
+ pageNumber,
340
+ pageCount,
341
+ setZoom,
342
+ effectiveZoom,
343
+ setRotation,
344
+ rotation
345
+ ]
346
+ );
347
+ const value = useMemo(
348
+ () => ({
349
+ state: {
350
+ // A parent's `loading` can add the not-ready state but never clear a
351
+ // real error — losing a failure to a stale prop is worse than a late
352
+ // spinner.
353
+ ...loading && state.status !== "error" ? { ...state, status: "loading" } : state,
354
+ highlights,
355
+ activeHighlightId,
356
+ find: findState,
357
+ pageNumber,
358
+ pageCount,
359
+ zoom,
360
+ effectiveZoom,
361
+ rotation
362
+ },
363
+ actions,
364
+ registry,
365
+ meta: {
366
+ baseHeadingLevel,
367
+ resolvedHighlights,
368
+ currentHighlightId,
369
+ highlightSupport: support,
370
+ canFind,
371
+ hasFind: findParts > 0
372
+ }
373
+ }),
374
+ [
375
+ state,
376
+ loading,
377
+ registry,
378
+ baseHeadingLevel,
379
+ highlights,
380
+ activeHighlightId,
381
+ findState,
382
+ actions,
383
+ resolvedHighlights,
384
+ currentHighlightId,
385
+ support,
386
+ canFind,
387
+ findParts,
388
+ pageNumber,
389
+ pageCount,
390
+ zoom,
391
+ effectiveZoom,
392
+ rotation
393
+ ]
394
+ );
395
+ return /* @__PURE__ */ jsx(FileViewerContext, { value, children });
396
+ }
397
+ var FileViewerFrame = forwardRef(
398
+ function FileViewerFrame2({ className, children, onKeyDown, ...props }, ref) {
399
+ const { state, actions, meta } = useFileViewer();
400
+ const { t } = useLocale();
401
+ const frame = useRef(null);
402
+ const wasOpen = useRef(false);
403
+ const findOpen = state.find.open && meta.canFind;
404
+ useEffect(() => {
405
+ if (wasOpen.current && !findOpen) {
406
+ frame.current?.querySelector('[data-slot="file-viewer-content"]')?.focus({ preventScroll: true });
407
+ }
408
+ wasOpen.current = findOpen;
409
+ }, [findOpen]);
410
+ return /* @__PURE__ */ jsx(
411
+ "section",
412
+ {
413
+ ref: (node) => {
414
+ frame.current = node;
415
+ if (typeof ref === "function") ref(node);
416
+ else if (ref) ref.current = node;
417
+ },
418
+ "data-slot": "file-viewer",
419
+ "aria-label": t("viewer.label"),
420
+ className: cn(
421
+ "bg-card text-card-foreground border-border flex h-full min-h-0 flex-col overflow-hidden rounded-lg border shadow-sm",
422
+ className
423
+ ),
424
+ onKeyDown: (event) => {
425
+ onKeyDown?.(event);
426
+ if (!event.defaultPrevented && meta.canFind && meta.hasFind && isFindShortcut(event)) {
427
+ event.preventDefault();
428
+ actions.openFind();
429
+ }
430
+ },
431
+ ...props,
432
+ children
433
+ }
434
+ );
435
+ }
436
+ );
437
+ var FileViewerToolbar = forwardRef(
438
+ function FileViewerToolbar2({ className, actions, children, ...props }, ref) {
439
+ const { state } = useFileViewer();
440
+ const { t } = useLocale();
441
+ const source = state.source;
442
+ const Glyph = fileIconFor(source?.name ?? "", source?.mediaType);
443
+ const [downloadFailed, setDownloadFailed] = useState(false);
444
+ if (!source) return null;
445
+ const download = () => {
446
+ setDownloadFailed(false);
447
+ void source.bytes().then((bytes) => {
448
+ downloadBlob(new Blob([bytes], { type: source.mediaType }), source.name);
449
+ }).catch((error) => {
450
+ console.error("FileViewer: download failed", error);
451
+ setDownloadFailed(true);
452
+ });
453
+ };
454
+ return /* @__PURE__ */ jsxs(
455
+ "div",
456
+ {
457
+ ref,
458
+ "data-slot": "file-viewer-toolbar",
459
+ className: cn(
460
+ // The divider is the ONLY cue between toolbar and content (same fill,
461
+ // no elevation change) — WCAG 1.4.11, so the strong rung.
462
+ "border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2",
463
+ className
464
+ ),
465
+ ...props,
466
+ children: [
467
+ /* @__PURE__ */ jsx(Glyph, { "aria-hidden": "true", className: "text-muted-foreground size-4 shrink-0" }),
468
+ /* @__PURE__ */ jsx(Text, { className: "min-w-0 flex-1 truncate", title: source.name, children: source.name }),
469
+ children,
470
+ actions,
471
+ downloadFailed ? /* @__PURE__ */ jsx(Text, { role: "alert", className: "text-destructive-text shrink-0 truncate", children: t("viewer.error.readFailedBody", { name: source.name, format: source.mediaType }) }) : null,
472
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "h-4" }),
473
+ /* @__PURE__ */ jsx(
474
+ IconButton,
475
+ {
476
+ variant: "ghost",
477
+ label: t("viewer.download", { name: source.name }),
478
+ icon: /* @__PURE__ */ jsx(DownloadIcon, { "aria-hidden": "true" }),
479
+ onClick: download
480
+ }
481
+ )
482
+ ]
483
+ }
484
+ );
485
+ }
486
+ );
487
+ var FileViewerHighlightStatus = forwardRef(
488
+ function FileViewerHighlightStatus2({ className, ...props }, ref) {
489
+ const { state, meta } = useFileViewer();
490
+ const { t } = useLocale();
491
+ const missed = meta.resolvedHighlights.filter(
492
+ (highlight) => highlight.source === "citation" && (highlight.status === "not-found" || highlight.status === "unsupported")
493
+ );
494
+ if (missed.length === 0) return null;
495
+ const first = missed[0];
496
+ const message = first.status === "unsupported" ? t("viewer.highlight.unsupported", {
497
+ format: state.source?.extension.toUpperCase() || ""
498
+ }) : first.reason === "truncated" ? t("viewer.highlight.notFoundTruncated") : t("viewer.highlight.notFound");
499
+ return /* @__PURE__ */ jsxs(
500
+ "div",
501
+ {
502
+ ref,
503
+ "data-slot": "file-viewer-highlight-status",
504
+ role: "status",
505
+ "aria-live": "polite",
506
+ className: cn(
507
+ // Information, not a failure — a neutral muted row, never the
508
+ // destructive tone. The divider is the sole cue between it and the
509
+ // content below (WCAG 1.4.11, strong rung).
510
+ "border-border-strong text-muted-foreground flex shrink-0 items-center gap-2 border-b px-3 py-2",
511
+ className
512
+ ),
513
+ ...props,
514
+ children: [
515
+ /* @__PURE__ */ jsx(SearchXIcon, { "aria-hidden": "true", className: "size-4 shrink-0" }),
516
+ /* @__PURE__ */ jsx("span", { className: "text-meta min-w-0 flex-1 truncate", children: message })
517
+ ]
518
+ }
519
+ );
520
+ }
521
+ );
522
+ var FileViewerSkeleton = forwardRef(
523
+ function FileViewerSkeleton2({ className, ...props }, ref) {
524
+ const { state } = useFileViewer();
525
+ const { t } = useLocale();
526
+ return /* @__PURE__ */ jsxs(
527
+ "div",
528
+ {
529
+ ref,
530
+ "data-slot": "file-viewer-skeleton",
531
+ role: "status",
532
+ "aria-live": "polite",
533
+ className: cn("flex h-full flex-col gap-3 p-4", className),
534
+ ...props,
535
+ children: [
536
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("viewer.loading", { name: state.source?.name ?? "" }) }),
537
+ /* @__PURE__ */ jsx(Skeleton, { "aria-hidden": "true", className: "h-4 w-2/5" }),
538
+ /* @__PURE__ */ jsx(Skeleton, { "aria-hidden": "true", className: "h-4 w-4/5" }),
539
+ /* @__PURE__ */ jsx(Skeleton, { "aria-hidden": "true", className: "h-4 w-3/5" }),
540
+ /* @__PURE__ */ jsx(Skeleton, { "aria-hidden": "true", className: "min-h-24 flex-1" })
541
+ ]
542
+ }
543
+ );
544
+ }
545
+ );
546
+ var ERROR_MESSAGES = {
547
+ "unsupported-format": {
548
+ title: "viewer.error.unsupportedFormat",
549
+ body: "viewer.error.unsupportedFormatBody"
550
+ },
551
+ "parser-missing": {
552
+ title: "viewer.error.parserMissing",
553
+ body: "viewer.error.parserMissingBody"
554
+ },
555
+ "read-failed": { title: "viewer.error.readFailed", body: "viewer.error.readFailedBody" },
556
+ "parse-failed": { title: "viewer.error.parseFailed", body: "viewer.error.parseFailedBody" },
557
+ // Neither should ever reach the UI: a protocol mismatch throws at registration
558
+ // and an abort is swallowed as a superseded load. Mapped anyway so the panel
559
+ // can never render an empty title.
560
+ "protocol-mismatch": { title: "viewer.error.parseFailed", body: "viewer.error.parseFailedBody" },
561
+ aborted: { title: "viewer.error.readFailed", body: "viewer.error.readFailedBody" }
562
+ };
563
+ var CAPABILITY_GAPS = /* @__PURE__ */ new Set(["unsupported-format", "parser-missing"]);
564
+ var FileViewerError = forwardRef(
565
+ function FileViewerError2({ className, ...props }, ref) {
566
+ const { state } = useFileViewer();
567
+ const { t } = useLocale();
568
+ const error = state.error;
569
+ if (!error) return null;
570
+ const message = ERROR_MESSAGES[error.code];
571
+ const vars = {
572
+ name: state.source?.name ?? "",
573
+ format: state.source?.extension.toUpperCase() || t("viewer.error.parseFailed"),
574
+ packages: error.packages?.join(", ") ?? ""
575
+ };
576
+ const isGap = CAPABILITY_GAPS.has(error.code);
577
+ return /* @__PURE__ */ jsx(
578
+ "div",
579
+ {
580
+ ref,
581
+ "data-slot": "file-viewer-error",
582
+ className: cn("flex min-h-full flex-col justify-center p-4", className),
583
+ ...isGap ? { role: "status", "aria-live": "polite" } : {},
584
+ ...props,
585
+ children: /* @__PURE__ */ jsx(
586
+ StatePanel,
587
+ {
588
+ kind: isGap ? "empty" : "error",
589
+ className: isGap ? "border-solid" : void 0,
590
+ icon: isGap ? /* @__PURE__ */ jsx(EyeOffIcon, { "aria-hidden": "true" }) : void 0,
591
+ title: t(message.title),
592
+ description: t(message.body, vars),
593
+ actions: error.code === "read-failed" || error.code === "parse-failed" ? /* @__PURE__ */ jsx(RetryButton, {}) : void 0
594
+ }
595
+ )
596
+ }
597
+ );
598
+ }
599
+ );
600
+ function RetryButton() {
601
+ const { actions } = useFileViewer();
602
+ const { t } = useLocale();
603
+ return /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: actions.reload, children: t("viewer.retry") });
604
+ }
605
+ var FileViewerEmpty = forwardRef(
606
+ function FileViewerEmpty2({ className, ...props }, ref) {
607
+ const { t } = useLocale();
608
+ return /* @__PURE__ */ jsx(
609
+ "div",
610
+ {
611
+ ref,
612
+ "data-slot": "file-viewer-empty",
613
+ className: cn("flex min-h-full flex-col justify-center p-4", className),
614
+ ...props,
615
+ children: /* @__PURE__ */ jsx(StatePanel, { kind: "empty", title: t("viewer.empty"), description: t("viewer.emptyBody") })
616
+ }
617
+ );
618
+ }
619
+ );
620
+ var FileViewerContent = forwardRef(
621
+ function FileViewerContent2({ className, ...props }, ref) {
622
+ const { state, actions, meta } = useFileViewer();
623
+ const { t } = useLocale();
624
+ return /* @__PURE__ */ jsxs(
625
+ "div",
626
+ {
627
+ ref,
628
+ "data-slot": "file-viewer-content",
629
+ role: "region",
630
+ "aria-label": t("viewer.content"),
631
+ tabIndex: 0,
632
+ className: cn("focus-ring min-h-0 flex-1 overflow-auto p-4", className),
633
+ ...props,
634
+ children: [
635
+ state.status === "empty" && /* @__PURE__ */ jsx(FileViewerEmpty, {}),
636
+ state.status === "loading" && /* @__PURE__ */ jsx(FileViewerSkeleton, {}),
637
+ state.status === "error" && /* @__PURE__ */ jsx(FileViewerError, {}),
638
+ state.status === "ready" && state.adapter && state.document && state.source && /* @__PURE__ */ jsx(
639
+ state.adapter.Renderer,
640
+ {
641
+ document: state.document,
642
+ source: state.source,
643
+ baseHeadingLevel: meta.baseHeadingLevel,
644
+ highlights: meta.resolvedHighlights,
645
+ activeHighlightId: meta.currentHighlightId,
646
+ pageNumber: state.pageNumber,
647
+ onPageChange: actions.goToPage,
648
+ zoom: state.zoom,
649
+ onZoomResolved: actions.reportZoom,
650
+ rotation: state.rotation
651
+ }
652
+ )
653
+ ]
654
+ }
655
+ );
656
+ }
657
+ );
658
+ var FileViewer = forwardRef(function FileViewer2({
659
+ // Every provider prop is destructured BY NAME, and the rest goes to the
660
+ // frame. Peeling off only some of them and spreading the remainder was a
661
+ // silent bug: `baseHeadingLevel` type-checked, never reached the provider,
662
+ // and landed on the `<section>` as an unknown DOM attribute. Adding a
663
+ // provider prop above without adding it here re-creates that exactly.
664
+ source,
665
+ registry,
666
+ loading,
667
+ baseHeadingLevel,
668
+ highlights,
669
+ defaultHighlights,
670
+ onHighlightsChange,
671
+ activeHighlightId,
672
+ defaultActiveHighlightId,
673
+ onActiveHighlightChange,
674
+ pageNumber,
675
+ defaultPageNumber,
676
+ onPageNumberChange,
677
+ zoom,
678
+ defaultZoom,
679
+ onZoomChange,
680
+ rotation,
681
+ defaultRotation,
682
+ onRotationChange,
683
+ children,
684
+ ...props
685
+ }, ref) {
686
+ return /* @__PURE__ */ jsx(
687
+ FileViewerProvider,
688
+ {
689
+ source,
690
+ registry,
691
+ loading,
692
+ baseHeadingLevel,
693
+ highlights,
694
+ defaultHighlights,
695
+ onHighlightsChange,
696
+ activeHighlightId,
697
+ defaultActiveHighlightId,
698
+ onActiveHighlightChange,
699
+ pageNumber,
700
+ defaultPageNumber,
701
+ onPageNumberChange,
702
+ zoom,
703
+ defaultZoom,
704
+ onZoomChange,
705
+ rotation,
706
+ defaultRotation,
707
+ onRotationChange,
708
+ children: /* @__PURE__ */ jsx(FileViewerFrame, { ref, ...props, children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
709
+ /* @__PURE__ */ jsxs(FileViewerToolbar, { children: [
710
+ /* @__PURE__ */ jsx(FileViewerPager, {}),
711
+ /* @__PURE__ */ jsx(FileViewerZoom, {}),
712
+ /* @__PURE__ */ jsx(FileViewerRotate, {})
713
+ ] }),
714
+ /* @__PURE__ */ jsx(FileViewerFind, {}),
715
+ /* @__PURE__ */ jsx(FileViewerHighlightStatus, {}),
716
+ /* @__PURE__ */ jsx(FileViewerContent, {})
717
+ ] }) })
718
+ }
719
+ );
720
+ });
721
+
722
+ export {
723
+ FileViewerProvider,
724
+ FileViewerFrame,
725
+ FileViewerToolbar,
726
+ FileViewerHighlightStatus,
727
+ FileViewerSkeleton,
728
+ FileViewerError,
729
+ FileViewerEmpty,
730
+ FileViewerContent,
731
+ FileViewer
732
+ };
733
+ //# sourceMappingURL=chunk-T4Y4YRHJ.js.map