@elabs-ai/components-viewer 4.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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/chunk-2NQ4RSJ3.js +18 -0
  4. package/dist/chunk-2NQ4RSJ3.js.map +1 -0
  5. package/dist/chunk-3U3MESBU.js +21 -0
  6. package/dist/chunk-3U3MESBU.js.map +1 -0
  7. package/dist/chunk-4OFQYALG.js +147 -0
  8. package/dist/chunk-4OFQYALG.js.map +1 -0
  9. package/dist/chunk-5OMONA2G.js +87 -0
  10. package/dist/chunk-5OMONA2G.js.map +1 -0
  11. package/dist/chunk-AORNMH77.js +58 -0
  12. package/dist/chunk-AORNMH77.js.map +1 -0
  13. package/dist/chunk-BCF3RWRL.js +18 -0
  14. package/dist/chunk-BCF3RWRL.js.map +1 -0
  15. package/dist/chunk-FELIAGKI.js +22 -0
  16. package/dist/chunk-FELIAGKI.js.map +1 -0
  17. package/dist/chunk-GGR7BNW4.js +18 -0
  18. package/dist/chunk-GGR7BNW4.js.map +1 -0
  19. package/dist/chunk-H3OAON3D.js +22 -0
  20. package/dist/chunk-H3OAON3D.js.map +1 -0
  21. package/dist/chunk-HMG2ERXH.js +17 -0
  22. package/dist/chunk-HMG2ERXH.js.map +1 -0
  23. package/dist/chunk-KOMDRWOU.js +21 -0
  24. package/dist/chunk-KOMDRWOU.js.map +1 -0
  25. package/dist/chunk-NMA57QZ7.js +117 -0
  26. package/dist/chunk-NMA57QZ7.js.map +1 -0
  27. package/dist/chunk-RGWDIQJR.js +26 -0
  28. package/dist/chunk-RGWDIQJR.js.map +1 -0
  29. package/dist/chunk-SLXRUYT3.js +91 -0
  30. package/dist/chunk-SLXRUYT3.js.map +1 -0
  31. package/dist/chunk-UJIJECEF.js +21 -0
  32. package/dist/chunk-UJIJECEF.js.map +1 -0
  33. package/dist/chunk-UL43NGUG.js +68 -0
  34. package/dist/chunk-UL43NGUG.js.map +1 -0
  35. package/dist/chunk-VO2273Z2.js +55 -0
  36. package/dist/chunk-VO2273Z2.js.map +1 -0
  37. package/dist/code-adapter-ADZ4UOGN.js +226 -0
  38. package/dist/code-adapter-ADZ4UOGN.js.map +1 -0
  39. package/dist/csv-adapter-6VU3FFVU.js +100 -0
  40. package/dist/csv-adapter-6VU3FFVU.js.map +1 -0
  41. package/dist/docx-adapter-5CQDHWTD.js +383 -0
  42. package/dist/docx-adapter-5CQDHWTD.js.map +1 -0
  43. package/dist/image-adapter-WOHZR24J.js +132 -0
  44. package/dist/image-adapter-WOHZR24J.js.map +1 -0
  45. package/dist/index.d.ts +1353 -0
  46. package/dist/index.js +1364 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/json-adapter-ZUW5GQHE.js +90 -0
  49. package/dist/json-adapter-ZUW5GQHE.js.map +1 -0
  50. package/dist/markdown-adapter-YC6WTBS4.js +214 -0
  51. package/dist/markdown-adapter-YC6WTBS4.js.map +1 -0
  52. package/dist/media-adapter-MCTB4GBH.js +78 -0
  53. package/dist/media-adapter-MCTB4GBH.js.map +1 -0
  54. package/dist/pdf-adapter-5PMKEXUD.js +424 -0
  55. package/dist/pdf-adapter-5PMKEXUD.js.map +1 -0
  56. package/dist/pptx-adapter-6GEQLS2Z.js +351 -0
  57. package/dist/pptx-adapter-6GEQLS2Z.js.map +1 -0
  58. package/dist/text-adapter-NFNWB5W3.js +81 -0
  59. package/dist/text-adapter-NFNWB5W3.js.map +1 -0
  60. package/dist/xlsx-adapter-CM2Y6AKQ.js +167 -0
  61. package/dist/xlsx-adapter-CM2Y6AKQ.js.map +1 -0
  62. package/package.json +103 -0
  63. package/src/adapters/code/code-adapter.test.tsx +221 -0
  64. package/src/adapters/code/code-adapter.tsx +272 -0
  65. package/src/adapters/code/code-language.test.ts +45 -0
  66. package/src/adapters/code/code-language.ts +100 -0
  67. package/src/adapters/code/code-manifest.ts +19 -0
  68. package/src/adapters/code/code-theme.test.ts +46 -0
  69. package/src/adapters/code/code-theme.ts +99 -0
  70. package/src/adapters/csv/csv-adapter.test.tsx +169 -0
  71. package/src/adapters/csv/csv-adapter.tsx +146 -0
  72. package/src/adapters/csv/csv-manifest.ts +20 -0
  73. package/src/adapters/docx/docx-adapter.test.tsx +192 -0
  74. package/src/adapters/docx/docx-adapter.tsx +328 -0
  75. package/src/adapters/docx/docx-manifest.ts +18 -0
  76. package/src/adapters/docx/docx-model.test.ts +181 -0
  77. package/src/adapters/docx/docx-model.ts +294 -0
  78. package/src/adapters/image/image-adapter.test.tsx +141 -0
  79. package/src/adapters/image/image-adapter.tsx +191 -0
  80. package/src/adapters/image/image-manifest.ts +15 -0
  81. package/src/adapters/index.ts +64 -0
  82. package/src/adapters/json/json-adapter.test.tsx +77 -0
  83. package/src/adapters/json/json-adapter.tsx +141 -0
  84. package/src/adapters/json/json-manifest.ts +11 -0
  85. package/src/adapters/markdown/markdown-adapter.test.tsx +228 -0
  86. package/src/adapters/markdown/markdown-adapter.tsx +353 -0
  87. package/src/adapters/markdown/markdown-manifest.ts +21 -0
  88. package/src/adapters/markdown/markdown-marks.test.ts +85 -0
  89. package/src/adapters/markdown/markdown-marks.ts +119 -0
  90. package/src/adapters/media/media-adapter.test.tsx +79 -0
  91. package/src/adapters/media/media-adapter.tsx +123 -0
  92. package/src/adapters/media/media-manifest.ts +17 -0
  93. package/src/adapters/office-fixture.ts +239 -0
  94. package/src/adapters/pdf/pdf-adapter.test.tsx +489 -0
  95. package/src/adapters/pdf/pdf-adapter.tsx +599 -0
  96. package/src/adapters/pdf/pdf-engine.ts +171 -0
  97. package/src/adapters/pdf/pdf-fixture.ts +27 -0
  98. package/src/adapters/pdf/pdf-manifest.ts +30 -0
  99. package/src/adapters/pdf/pdf-text.test.ts +171 -0
  100. package/src/adapters/pdf/pdf-text.ts +202 -0
  101. package/src/adapters/pptx/pptx-adapter.test.tsx +241 -0
  102. package/src/adapters/pptx/pptx-adapter.tsx +341 -0
  103. package/src/adapters/pptx/pptx-manifest.ts +19 -0
  104. package/src/adapters/pptx/pptx-model.test.ts +219 -0
  105. package/src/adapters/pptx/pptx-model.ts +248 -0
  106. package/src/adapters/text/text-adapter.test.tsx +125 -0
  107. package/src/adapters/text/text-adapter.tsx +117 -0
  108. package/src/adapters/text/text-manifest.ts +19 -0
  109. package/src/adapters/xlsx/xlsx-adapter.test.tsx +159 -0
  110. package/src/adapters/xlsx/xlsx-adapter.tsx +250 -0
  111. package/src/adapters/xlsx/xlsx-manifest.ts +26 -0
  112. package/src/components/grid-text.test.ts +68 -0
  113. package/src/components/grid-text.ts +77 -0
  114. package/src/components/marked-text.tsx +36 -0
  115. package/src/components/sheet-table.tsx +142 -0
  116. package/src/core/errors.ts +120 -0
  117. package/src/core/highlight-marks.test.ts +95 -0
  118. package/src/core/highlight-marks.ts +95 -0
  119. package/src/core/highlight-resolve.test.ts +208 -0
  120. package/src/core/highlight-resolve.ts +191 -0
  121. package/src/core/highlight.ts +125 -0
  122. package/src/core/registry.test.ts +172 -0
  123. package/src/core/registry.ts +188 -0
  124. package/src/core/scroll-host.ts +42 -0
  125. package/src/core/text-index.test.ts +95 -0
  126. package/src/core/text-index.ts +159 -0
  127. package/src/core/types.ts +285 -0
  128. package/src/core/use-highlight-scroll.ts +59 -0
  129. package/src/core/use-page-control.ts +47 -0
  130. package/src/core/use-paged-scroll.ts +194 -0
  131. package/src/core/use-viewport-size.ts +54 -0
  132. package/src/core/zoom.ts +56 -0
  133. package/src/file-viewer/file-viewer-context.tsx +239 -0
  134. package/src/file-viewer/file-viewer-find.tsx +204 -0
  135. package/src/file-viewer/file-viewer-pager.tsx +120 -0
  136. package/src/file-viewer/file-viewer-zoom.tsx +166 -0
  137. package/src/file-viewer/file-viewer.stories.tsx +947 -0
  138. package/src/file-viewer/file-viewer.test.tsx +947 -0
  139. package/src/file-viewer/file-viewer.tsx +1143 -0
  140. package/src/index.ts +157 -0
@@ -0,0 +1,383 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ docxManifest
5
+ } from "./chunk-FELIAGKI.js";
6
+ import {
7
+ MarkedText
8
+ } from "./chunk-2NQ4RSJ3.js";
9
+ import {
10
+ createTextIndexBuilder
11
+ } from "./chunk-UL43NGUG.js";
12
+ import {
13
+ toMarkRanges,
14
+ useScrollActiveHighlightIntoView
15
+ } from "./chunk-VO2273Z2.js";
16
+ import {
17
+ toViewerError
18
+ } from "./chunk-AORNMH77.js";
19
+
20
+ // src/adapters/docx/docx-adapter.tsx
21
+ import {
22
+ cn,
23
+ ProseHeading,
24
+ ProseLink,
25
+ ProseList,
26
+ ProseListItem,
27
+ ProseText,
28
+ StatePanel,
29
+ Table,
30
+ TableBody,
31
+ TableCell,
32
+ TableHead,
33
+ TableHeader,
34
+ TableRow,
35
+ useLocale
36
+ } from "@elabs-ai/components-ui";
37
+ import { useMemo, useRef } from "react";
38
+
39
+ // src/adapters/docx/docx-model.ts
40
+ var SAFE_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
41
+ function safeHref(value) {
42
+ if (!value) return void 0;
43
+ try {
44
+ const url = new URL(value, "https://viewer.invalid");
45
+ return SAFE_SCHEMES.includes(url.protocol) && !value.startsWith("#") ? url.href : void 0;
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ }
50
+ function collectRuns(node, style = {}, out = []) {
51
+ for (const child of Array.from(node.childNodes)) {
52
+ if (child.nodeType === 3) {
53
+ const text = child.nodeValue ?? "";
54
+ if (text) out.push({ text, ...style });
55
+ continue;
56
+ }
57
+ if (child.nodeType !== 1) continue;
58
+ const element = child;
59
+ const tag = element.tagName.toLowerCase();
60
+ if (tag === "br") {
61
+ out.push({ text: "\n", ...style });
62
+ continue;
63
+ }
64
+ const next = { ...style };
65
+ if (tag === "strong" || tag === "b") next.bold = true;
66
+ if (tag === "em" || tag === "i") next.italic = true;
67
+ if (tag === "a") {
68
+ const href = safeHref(element.getAttribute("href"));
69
+ if (href) next.href = href;
70
+ }
71
+ collectRuns(element, next, out);
72
+ }
73
+ return out;
74
+ }
75
+ function compact(runs) {
76
+ const merged = [];
77
+ for (const run of runs) {
78
+ const last = merged[merged.length - 1];
79
+ if (last && last.bold === run.bold && last.italic === run.italic && last.href === run.href) {
80
+ last.text += run.text;
81
+ } else {
82
+ merged.push({ ...run });
83
+ }
84
+ }
85
+ return merged.filter((run) => run.text.trim() !== "" || run.text.includes("\n"));
86
+ }
87
+ function textOf(node) {
88
+ return compact(collectRuns(node)).map((run) => run.text).join("").trim();
89
+ }
90
+ function listItems(element) {
91
+ const items = [];
92
+ for (const child of Array.from(element.children)) {
93
+ if (child.tagName.toLowerCase() !== "li") continue;
94
+ const runs = compact(collectRuns(child));
95
+ if (runs.length > 0) items.push(runs);
96
+ for (const nested of Array.from(child.children)) {
97
+ const tag = nested.tagName.toLowerCase();
98
+ if (tag === "ul" || tag === "ol") items.push(...listItems(nested));
99
+ }
100
+ }
101
+ return items;
102
+ }
103
+ function tableBlock(element) {
104
+ const rows = Array.from(element.querySelectorAll("tr"));
105
+ if (rows.length === 0) return void 0;
106
+ const cellsOf = (row) => Array.from(row.children).filter((cell) => ["td", "th"].includes(cell.tagName.toLowerCase())).map(textOf);
107
+ const [firstRow, ...restRows] = rows;
108
+ if (!firstRow) return void 0;
109
+ const isHeader = firstRow.querySelector("th") !== null;
110
+ return isHeader ? { type: "table", head: cellsOf(firstRow), rows: restRows.map(cellsOf) } : { type: "table", rows: rows.map(cellsOf) };
111
+ }
112
+ var HEADING_LEVELS = {
113
+ h1: 1,
114
+ h2: 2,
115
+ h3: 3,
116
+ h4: 4,
117
+ h5: 5,
118
+ h6: 6
119
+ };
120
+ function htmlToBlocks(html, parse) {
121
+ const document = parse(`<body>${html}</body>`);
122
+ const blocks = [];
123
+ const walk = (parent) => {
124
+ for (const element of Array.from(parent.children)) {
125
+ const tag = element.tagName.toLowerCase();
126
+ const level = HEADING_LEVELS[tag];
127
+ if (level) {
128
+ const runs = compact(collectRuns(element));
129
+ if (runs.length > 0) blocks.push({ type: "heading", level, runs });
130
+ continue;
131
+ }
132
+ if (tag === "p") {
133
+ const image = element.querySelector("img");
134
+ const runs = compact(collectRuns(element));
135
+ if (image && runs.length === 0) {
136
+ blocks.push(imageBlock(image));
137
+ continue;
138
+ }
139
+ if (runs.length > 0) blocks.push({ type: "paragraph", runs });
140
+ continue;
141
+ }
142
+ if (tag === "ul" || tag === "ol") {
143
+ const items = listItems(element);
144
+ if (items.length > 0) blocks.push({ type: "list", ordered: tag === "ol", items });
145
+ continue;
146
+ }
147
+ if (tag === "table") {
148
+ const table = tableBlock(element);
149
+ if (table) blocks.push(table);
150
+ continue;
151
+ }
152
+ if (tag === "img") {
153
+ blocks.push(imageBlock(element));
154
+ continue;
155
+ }
156
+ walk(element);
157
+ }
158
+ };
159
+ walk(document.body);
160
+ return blocks;
161
+ }
162
+ function imageBlock(element) {
163
+ const alt = element.getAttribute("alt")?.trim();
164
+ return {
165
+ type: "image",
166
+ // mammoth inlines images as `data:` URIs by default, so there is no remote
167
+ // origin here — and no `src` at all is better than a URL we did not vet.
168
+ src: element.getAttribute("src") ?? "",
169
+ ...alt ? { alt } : {}
170
+ };
171
+ }
172
+ var DOCX_LIST_BULLET = "\u2022 ";
173
+ var DOCX_CELL_SEPARATOR = " ";
174
+ var DOCX_HEAD_ROW = -1;
175
+ function blocksToTextWithMap(blocks) {
176
+ const builder = createTextIndexBuilder();
177
+ blocks.forEach((block, index) => {
178
+ if (block.type === "heading" || block.type === "paragraph") {
179
+ builder.push(block.runs.map((run) => run.text).join(""), { block: index });
180
+ return;
181
+ }
182
+ if (block.type === "list") {
183
+ block.items.forEach((item, item_) => {
184
+ builder.push(`${DOCX_LIST_BULLET}${item.map((run) => run.text).join("")}`, {
185
+ block: index,
186
+ item: item_
187
+ });
188
+ });
189
+ return;
190
+ }
191
+ if (block.type === "table") {
192
+ if (block.head) {
193
+ builder.push(block.head.join(DOCX_CELL_SEPARATOR), { block: index, row: DOCX_HEAD_ROW });
194
+ }
195
+ block.rows.forEach((row, rowIndex) => {
196
+ builder.push(row.join(DOCX_CELL_SEPARATOR), { block: index, row: rowIndex });
197
+ });
198
+ }
199
+ });
200
+ return builder.build();
201
+ }
202
+
203
+ // src/adapters/docx/docx-adapter.tsx
204
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
205
+ function parseMarkup(markup) {
206
+ return new DOMParser().parseFromString(markup, "text/html");
207
+ }
208
+ var DocxAdapter = class {
209
+ async load(source, context) {
210
+ let buffer;
211
+ try {
212
+ buffer = await source.bytes(context.signal);
213
+ } catch (error) {
214
+ throw toViewerError(error, "read-failed", { fileName: source.name });
215
+ }
216
+ const mammoth = await import("mammoth");
217
+ try {
218
+ const result = await mammoth.convertToHtml({ arrayBuffer: buffer });
219
+ const blocks = htmlToBlocks(result.value, parseMarkup);
220
+ const textIndex = blocksToTextWithMap(blocks);
221
+ return {
222
+ kind: "docx",
223
+ blocks,
224
+ // mammoth's messages are about the CONVERSION, not about the file being
225
+ // broken — a document that lost one custom style still reads fine, so
226
+ // they are carried as notes rather than raised as a failure.
227
+ warnings: result.messages.map((message) => message.message),
228
+ text: textIndex.text,
229
+ textIndex
230
+ };
231
+ } catch (error) {
232
+ throw toViewerError(error, "parse-failed", { fileName: source.name });
233
+ }
234
+ }
235
+ };
236
+ function Runs({ runs, marks, start }) {
237
+ let offset = start;
238
+ return /* @__PURE__ */ jsx(Fragment, { children: runs.map((run, index) => {
239
+ const from = offset;
240
+ if (offset !== void 0) offset += run.text.length;
241
+ const content = /* @__PURE__ */ jsx(MarkedText, { text: run.text, marks, start: from });
242
+ const bolded = run.bold ? /* @__PURE__ */ jsx("strong", { children: content }) : content;
243
+ const styled = run.italic ? /* @__PURE__ */ jsx("em", { children: bolded }) : bolded;
244
+ if (run.href) {
245
+ return /* @__PURE__ */ jsx(ProseLink, { href: run.href, children: styled }, index);
246
+ }
247
+ return /* @__PURE__ */ jsx("span", { children: styled }, index);
248
+ }) });
249
+ }
250
+ var EMPTY_SPANS = [];
251
+ function cellStart(row, index, rowStart) {
252
+ if (rowStart === void 0) return void 0;
253
+ let offset = rowStart;
254
+ for (let i = 0; i < index; i += 1) {
255
+ offset += (row[i]?.length ?? 0) + DOCX_CELL_SEPARATOR.length;
256
+ }
257
+ return offset;
258
+ }
259
+ function Block({
260
+ block,
261
+ baseHeadingLevel,
262
+ marks,
263
+ spans
264
+ }) {
265
+ const startOf = (match) => spans.find((span) => match(span.ref))?.start;
266
+ if (block.type === "heading") {
267
+ return (
268
+ // Offset, not absolute: Word's "Heading 1" is the top of THAT document,
269
+ // not of the page showing it. Never past h6 — an h7 is not an element.
270
+ /* @__PURE__ */ jsx(
271
+ ProseHeading,
272
+ {
273
+ level: Math.min(6, Math.max(1, block.level + baseHeadingLevel - 1)),
274
+ children: /* @__PURE__ */ jsx(Runs, { runs: block.runs, marks, start: startOf(() => true) })
275
+ }
276
+ )
277
+ );
278
+ }
279
+ if (block.type === "paragraph") {
280
+ return /* @__PURE__ */ jsx(ProseText, { className: "whitespace-pre-wrap", children: /* @__PURE__ */ jsx(Runs, { runs: block.runs, marks, start: startOf(() => true) }) });
281
+ }
282
+ if (block.type === "list") {
283
+ return /* @__PURE__ */ jsx(ProseList, { ordered: block.ordered, children: block.items.map((item, index) => {
284
+ const start = startOf((ref) => ref.item === index);
285
+ return /* @__PURE__ */ jsx(ProseListItem, { children: /* @__PURE__ */ jsx(
286
+ Runs,
287
+ {
288
+ runs: item,
289
+ marks,
290
+ start: start === void 0 ? void 0 : start + DOCX_LIST_BULLET.length
291
+ }
292
+ ) }, index);
293
+ }) });
294
+ }
295
+ if (block.type === "image") {
296
+ return /* @__PURE__ */ jsx(
297
+ "img",
298
+ {
299
+ src: block.src,
300
+ alt: block.alt ?? "",
301
+ ...block.alt ? {} : { "aria-hidden": true },
302
+ className: "my-2 block h-auto max-w-full rounded-md"
303
+ }
304
+ );
305
+ }
306
+ const headStart = startOf((ref) => ref.row === DOCX_HEAD_ROW);
307
+ return /* @__PURE__ */ jsxs(Table, { className: "my-2", children: [
308
+ block.head && /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: block.head.map((cell, index) => /* @__PURE__ */ jsx(TableHead, { scope: "col", children: /* @__PURE__ */ jsx(
309
+ MarkedText,
310
+ {
311
+ text: cell,
312
+ marks,
313
+ start: cellStart(block.head ?? [], index, headStart)
314
+ }
315
+ ) }, index)) }) }),
316
+ /* @__PURE__ */ jsx(TableBody, { children: block.rows.map((row, rowIndex) => {
317
+ const rowStart = startOf((ref) => ref.row === rowIndex);
318
+ return /* @__PURE__ */ jsx(TableRow, { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx(TableCell, { className: "whitespace-pre-wrap align-top", children: /* @__PURE__ */ jsx(
319
+ MarkedText,
320
+ {
321
+ text: cell,
322
+ marks,
323
+ start: cellStart(row, cellIndex, rowStart)
324
+ }
325
+ ) }, cellIndex)) }, rowIndex);
326
+ }) })
327
+ ] });
328
+ }
329
+ function DocxRenderer({
330
+ document: doc,
331
+ className,
332
+ baseHeadingLevel = 2,
333
+ highlights,
334
+ activeHighlightId
335
+ }) {
336
+ const docx = doc;
337
+ const { t } = useLocale();
338
+ const container = useRef(null);
339
+ const marks = useMemo(
340
+ () => toMarkRanges(highlights, docx.text?.length ?? 0),
341
+ [highlights, docx.text]
342
+ );
343
+ const spansByBlock = useMemo(() => {
344
+ const map = /* @__PURE__ */ new Map();
345
+ for (const span of docx.textIndex?.spans ?? []) {
346
+ const list = map.get(span.ref.block);
347
+ if (list) list.push(span);
348
+ else map.set(span.ref.block, [span]);
349
+ }
350
+ return map;
351
+ }, [docx.textIndex]);
352
+ useScrollActiveHighlightIntoView(container, activeHighlightId);
353
+ if (docx.blocks.length === 0) {
354
+ return /* @__PURE__ */ jsx("div", { className: cn("flex min-h-full flex-col justify-center p-4", className), children: /* @__PURE__ */ jsx(StatePanel, { kind: "empty", title: t("viewer.docx.empty") }) });
355
+ }
356
+ return (
357
+ // `max-w-prose` because this is genuine multi-sentence prose in a pane that
358
+ // can be very wide — the one case styling-and-tokens.md says to cap the
359
+ // measure. Centred so the column does not hug one edge. No `overflow-auto`:
360
+ // `FileViewerContent` is the scroll boundary, and a second one clips the
361
+ // last paragraph above the outer pane's padding.
362
+ /* @__PURE__ */ jsx("article", { ref: container, className: cn("mx-auto max-w-prose space-y-2", className), children: docx.blocks.map((block, index) => /* @__PURE__ */ jsx(
363
+ Block,
364
+ {
365
+ block,
366
+ baseHeadingLevel,
367
+ marks,
368
+ spans: spansByBlock.get(index) ?? EMPTY_SPANS
369
+ },
370
+ index
371
+ )) })
372
+ );
373
+ }
374
+ var adapterModule = {
375
+ manifest: docxManifest,
376
+ create: () => new DocxAdapter(),
377
+ Renderer: DocxRenderer
378
+ };
379
+ var docx_adapter_default = adapterModule;
380
+ export {
381
+ docx_adapter_default as default
382
+ };
383
+ //# sourceMappingURL=docx-adapter-5CQDHWTD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/docx/docx-adapter.tsx","../src/adapters/docx/docx-model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Word adapter — a `.docx` becomes blocks, and blocks become brand-ui prose.\n *\n * mammoth does the hard part (unzipping OOXML, resolving styles, numbering,\n * images); `docx-model.ts` then parses its HTML into a model and discards the\n * markup, so nothing here writes `innerHTML`. That single decision is what makes\n * a Word document themeable, Trusted-Types-safe and free of a sanitizer\n * dependency — the reasoning lives in `docx-model.ts`.\n *\n * What a reader gets is the document's STRUCTURE in this system's typography:\n * real headings, real lists, a real `Table`, images with their own alt text. It\n * is not a pixel reproduction of Word's page layout — no page breaks, no columns,\n * no margins — and it does not pretend to be. For a byte-faithful rendering the\n * honest answer is to download the file, which the toolbar already offers.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport {\n cn,\n ProseHeading,\n ProseLink,\n ProseList,\n ProseListItem,\n ProseText,\n StatePanel,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\n\nimport { useMemo, useRef } from \"react\";\n\nimport { MarkedText } from \"../../components/marked-text\";\nimport { toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport type { TextIndex, TextSpan } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport {\n blocksToTextWithMap,\n DOCX_CELL_SEPARATOR,\n DOCX_HEAD_ROW,\n DOCX_LIST_BULLET,\n htmlToBlocks,\n type DocxBlock,\n type DocxRef,\n type DocxRun,\n} from \"./docx-model\";\nimport { docxManifest } from \"./docx-manifest\";\n\nexport interface DocxDocument extends AdapterDocument {\n kind: \"docx\";\n blocks: DocxBlock[];\n /** Non-fatal conversion notes mammoth reported (an unsupported style, a dropped field). */\n warnings: string[];\n /** Which block, item or row each stretch of `text` came from. */\n textIndex?: TextIndex<DocxRef>;\n}\n\n/**\n * Parse markup with the DOM the runtime already has.\n *\n * `DOMParser` is the browser's own parser and jsdom's in tests — it does NOT\n * execute anything it parses, which is why building the model from it is safe\n * even though the input is third-party markup.\n */\nfunction parseMarkup(markup: string): Document {\n return new DOMParser().parseFromString(markup, \"text/html\");\n}\n\nclass DocxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<DocxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const mammoth = await import(\"mammoth\");\n\n try {\n const result = await mammoth.convertToHtml({ arrayBuffer: buffer });\n const blocks = htmlToBlocks(result.value, parseMarkup);\n const textIndex = blocksToTextWithMap(blocks);\n return {\n kind: \"docx\",\n blocks,\n // mammoth's messages are about the CONVERSION, not about the file being\n // broken — a document that lost one custom style still reads fine, so\n // they are carried as notes rather than raised as a failure.\n warnings: result.messages.map((message) => message.message),\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction Runs({ runs, marks, start }: { runs: DocxRun[]; marks: MarkRanges; start?: number }) {\n let offset = start;\n return (\n <>\n {runs.map((run, index) => {\n const from = offset;\n if (offset !== undefined) offset += run.text.length;\n // Runs are joined with nothing in the projection, so a run's offset is\n // simply the sum of the ones before it — the same accumulation the code\n // adapter does over syntax tokens.\n const content = <MarkedText text={run.text} marks={marks} start={from} />;\n const bolded = run.bold ? <strong>{content}</strong> : content;\n const styled = run.italic ? <em>{bolded}</em> : bolded;\n if (run.href) {\n return (\n <ProseLink key={index} href={run.href}>\n {styled}\n </ProseLink>\n );\n }\n return <span key={index}>{styled}</span>;\n })}\n </>\n );\n}\n\n/** Shared empty list, so a block with no spans does not remount on every render. */\nconst EMPTY_SPANS: readonly TextSpan<DocxRef>[] = [];\n\n/** The projection offset of one row's `index`-th cell, given the row's own start. */\nfunction cellStart(row: readonly string[], index: number, rowStart?: number): number | undefined {\n if (rowStart === undefined) return undefined;\n let offset = rowStart;\n for (let i = 0; i < index; i += 1) {\n offset += (row[i]?.length ?? 0) + DOCX_CELL_SEPARATOR.length;\n }\n return offset;\n}\n\nfunction Block({\n block,\n baseHeadingLevel,\n marks,\n spans,\n}: {\n block: DocxBlock;\n baseHeadingLevel: number;\n marks: MarkRanges;\n /** The projection spans belonging to THIS block, in document order. */\n spans: readonly TextSpan<DocxRef>[];\n}) {\n const startOf = (match: (ref: DocxRef) => boolean) =>\n spans.find((span) => match(span.ref))?.start;\n\n if (block.type === \"heading\") {\n return (\n // Offset, not absolute: Word's \"Heading 1\" is the top of THAT document,\n // not of the page showing it. Never past h6 — an h7 is not an element.\n <ProseHeading\n level={Math.min(6, Math.max(1, block.level + baseHeadingLevel - 1)) as ProseHeadingLevel}\n >\n <Runs runs={block.runs} marks={marks} start={startOf(() => true)} />\n </ProseHeading>\n );\n }\n if (block.type === \"paragraph\") {\n return (\n <ProseText className=\"whitespace-pre-wrap\">\n <Runs runs={block.runs} marks={marks} start={startOf(() => true)} />\n </ProseText>\n );\n }\n if (block.type === \"list\") {\n return (\n <ProseList ordered={block.ordered}>\n {block.items.map((item, index) => {\n // The bullet is in the projection but not in the DOM — the list\n // element draws it — so an item's own text starts after it.\n const start = startOf((ref) => ref.item === index);\n return (\n <ProseListItem key={index}>\n <Runs\n runs={item}\n marks={marks}\n start={start === undefined ? undefined : start + DOCX_LIST_BULLET.length}\n />\n </ProseListItem>\n );\n })}\n </ProseList>\n );\n }\n if (block.type === \"image\") {\n return (\n <img\n src={block.src}\n // The document's own alt text when the author wrote one. An image with\n // none is decoration as far as the reader can tell, and a filename would\n // be noise, not a description.\n alt={block.alt ?? \"\"}\n {...(block.alt ? {} : { \"aria-hidden\": true })}\n className=\"my-2 block h-auto max-w-full rounded-md\"\n />\n );\n }\n const headStart = startOf((ref) => ref.row === DOCX_HEAD_ROW);\n return (\n <Table className=\"my-2\">\n {block.head && (\n <TableHeader>\n <TableRow>\n {block.head.map((cell, index) => (\n <TableHead key={index} scope=\"col\">\n <MarkedText\n text={cell}\n marks={marks}\n start={cellStart(block.head ?? [], index, headStart)}\n />\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n )}\n <TableBody>\n {block.rows.map((row, rowIndex) => {\n const rowStart = startOf((ref) => ref.row === rowIndex);\n return (\n <TableRow key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <TableCell key={cellIndex} className=\"whitespace-pre-wrap align-top\">\n <MarkedText\n text={cell}\n marks={marks}\n start={cellStart(row, cellIndex, rowStart)}\n />\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n );\n}\n\nfunction DocxRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n}: AdapterRendererProps) {\n const docx = doc as DocxDocument;\n const { t } = useLocale();\n const container = useRef<HTMLElement>(null);\n\n const marks = useMemo(\n () => toMarkRanges(highlights, docx.text?.length ?? 0),\n [highlights, docx.text],\n );\n // Grouped once per document rather than searched per block: a long report is\n // thousands of blocks, and a linear scan inside the render loop would make it\n // quadratic on every keystroke of a find-as-you-type.\n const spansByBlock = useMemo(() => {\n const map = new Map<number, TextSpan<DocxRef>[]>();\n for (const span of docx.textIndex?.spans ?? []) {\n const list = map.get(span.ref.block);\n if (list) list.push(span);\n else map.set(span.ref.block, [span]);\n }\n return map;\n }, [docx.textIndex]);\n\n useScrollActiveHighlightIntoView(container, activeHighlightId);\n\n if (docx.blocks.length === 0) {\n return (\n <div className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}>\n <StatePanel kind=\"empty\" title={t(\"viewer.docx.empty\")} />\n </div>\n );\n }\n\n return (\n // `max-w-prose` because this is genuine multi-sentence prose in a pane that\n // can be very wide — the one case styling-and-tokens.md says to cap the\n // measure. Centred so the column does not hug one edge. No `overflow-auto`:\n // `FileViewerContent` is the scroll boundary, and a second one clips the\n // last paragraph above the outer pane's padding.\n <article ref={container} className={cn(\"mx-auto max-w-prose space-y-2\", className)}>\n {docx.blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n baseHeadingLevel={baseHeadingLevel}\n marks={marks}\n spans={spansByBlock.get(index) ?? EMPTY_SPANS}\n />\n ))}\n </article>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: docxManifest,\n create: () => new DocxAdapter(),\n Renderer: DocxRenderer,\n};\n\nexport default adapterModule;\n","/**\n * Word markup → a block model.\n *\n * ## Why this exists at all\n *\n * mammoth converts a `.docx` into an HTML **string**. Rendering that string\n * would mean `dangerouslySetInnerHTML`, which is three separate problems: it\n * puts third-party markup past the token layer, it needs a sanitizer to be safe\n * at all, and it makes the surface unusable under a Trusted-Types policy (a raw\n * `innerHTML` write throws during commit and takes the React root down with it —\n * see `docs/CSP-AND-NETWORK.md`).\n *\n * So the HTML is parsed and thrown away, and what survives is a small model of\n * blocks and inline runs that React renders as real elements. **The parse IS the\n * allowlist**: a tag this file does not name contributes nothing but its text,\n * so there is no denylist to keep up to date, no `<script>` to strip, and no\n * sanitizer dependency. It is also why the viewer adds no CSP sink.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\n\n/** A styled span of text inside a block. */\nexport interface DocxRun {\n text: string;\n bold?: boolean;\n italic?: boolean;\n /** Present only for a link whose scheme survived {@link safeHref}. */\n href?: string;\n}\n\nexport type DocxBlock =\n | { type: \"heading\"; level: 1 | 2 | 3 | 4 | 5 | 6; runs: DocxRun[] }\n | { type: \"paragraph\"; runs: DocxRun[] }\n | { type: \"list\"; ordered: boolean; items: DocxRun[][] }\n | { type: \"table\"; head?: string[]; rows: string[][] }\n | { type: \"image\"; src: string; alt?: string };\n\n/** Schemes a link may keep. Anything else (notably `javascript:`) becomes plain text. */\nconst SAFE_SCHEMES = [\"http:\", \"https:\", \"mailto:\", \"tel:\"];\n\n/**\n * The href, or `undefined` when it is not a scheme we will hand to the browser.\n * Relative hrefs are dropped too — a Word document's internal bookmarks point at\n * anchors that do not exist on the host page.\n */\nexport function safeHref(value: string | null): string | undefined {\n if (!value) return undefined;\n try {\n const url = new URL(value, \"https://viewer.invalid\");\n return SAFE_SCHEMES.includes(url.protocol) && !value.startsWith(\"#\") ? url.href : undefined;\n } catch {\n return undefined;\n }\n}\n\ninterface RunStyle {\n bold?: boolean;\n italic?: boolean;\n href?: string;\n}\n\n/** Collect the inline runs under a node, carrying style down the tree. */\nfunction collectRuns(node: Node, style: RunStyle = {}, out: DocxRun[] = []): DocxRun[] {\n for (const child of Array.from(node.childNodes)) {\n if (child.nodeType === 3 /* TEXT_NODE */) {\n const text = child.nodeValue ?? \"\";\n if (text) out.push({ text, ...style });\n continue;\n }\n if (child.nodeType !== 1 /* ELEMENT_NODE */) continue;\n\n const element = child as Element;\n const tag = element.tagName.toLowerCase();\n if (tag === \"br\") {\n out.push({ text: \"\\n\", ...style });\n continue;\n }\n const next: RunStyle = { ...style };\n if (tag === \"strong\" || tag === \"b\") next.bold = true;\n if (tag === \"em\" || tag === \"i\") next.italic = true;\n if (tag === \"a\") {\n const href = safeHref(element.getAttribute(\"href\"));\n if (href) next.href = href;\n }\n collectRuns(element, next, out);\n }\n return out;\n}\n\n/** Merge adjacent runs that carry identical styling — fewer, longer spans. */\nfunction compact(runs: DocxRun[]): DocxRun[] {\n const merged: DocxRun[] = [];\n for (const run of runs) {\n const last = merged[merged.length - 1];\n if (last && last.bold === run.bold && last.italic === run.italic && last.href === run.href) {\n last.text += run.text;\n } else {\n merged.push({ ...run });\n }\n }\n return merged.filter((run) => run.text.trim() !== \"\" || run.text.includes(\"\\n\"));\n}\n\nfunction textOf(node: Node): string {\n return compact(collectRuns(node))\n .map((run) => run.text)\n .join(\"\")\n .trim();\n}\n\nfunction listItems(element: Element): DocxRun[][] {\n const items: DocxRun[][] = [];\n for (const child of Array.from(element.children)) {\n if (child.tagName.toLowerCase() !== \"li\") continue;\n // A nested list inside an `<li>` is flattened into its own items rather than\n // being rendered as a sub-list: the depth is not worth a recursive component\n // in a preview, and losing it never loses the WORDS.\n const runs = compact(collectRuns(child));\n if (runs.length > 0) items.push(runs);\n for (const nested of Array.from(child.children)) {\n const tag = nested.tagName.toLowerCase();\n if (tag === \"ul\" || tag === \"ol\") items.push(...listItems(nested));\n }\n }\n return items;\n}\n\nfunction tableBlock(element: Element): DocxBlock | undefined {\n const rows = Array.from(element.querySelectorAll(\"tr\"));\n if (rows.length === 0) return undefined;\n\n const cellsOf = (row: Element) =>\n Array.from(row.children)\n .filter((cell) => [\"td\", \"th\"].includes(cell.tagName.toLowerCase()))\n .map(textOf);\n\n const [firstRow, ...restRows] = rows;\n if (!firstRow) return undefined;\n\n // Word only emits `<th>` when the author marked a header row. Without it the\n // first row is ordinary data, and promoting it would invent a header.\n const isHeader = firstRow.querySelector(\"th\") !== null;\n return isHeader\n ? { type: \"table\", head: cellsOf(firstRow), rows: restRows.map(cellsOf) }\n : { type: \"table\", rows: rows.map(cellsOf) };\n}\n\nconst HEADING_LEVELS: Record<string, 1 | 2 | 3 | 4 | 5 | 6> = {\n h1: 1,\n h2: 2,\n h3: 3,\n h4: 4,\n h5: 5,\n h6: 6,\n};\n\n/**\n * Parse mammoth's HTML into blocks.\n *\n * Takes a `DOMParser` factory so the caller decides where the parser comes from\n * — the browser's own in an app, jsdom's in a test — instead of this module\n * reaching for a global that may not exist on a server.\n */\nexport function htmlToBlocks(html: string, parse: (markup: string) => Document): DocxBlock[] {\n const document = parse(`<body>${html}</body>`);\n const blocks: DocxBlock[] = [];\n\n const walk = (parent: Element) => {\n for (const element of Array.from(parent.children)) {\n const tag = element.tagName.toLowerCase();\n const level = HEADING_LEVELS[tag];\n\n if (level) {\n const runs = compact(collectRuns(element));\n if (runs.length > 0) blocks.push({ type: \"heading\", level, runs });\n continue;\n }\n if (tag === \"p\") {\n // A `<p>` holding only an image is the image, not an empty paragraph.\n const image = element.querySelector(\"img\");\n const runs = compact(collectRuns(element));\n if (image && runs.length === 0) {\n blocks.push(imageBlock(image));\n continue;\n }\n if (runs.length > 0) blocks.push({ type: \"paragraph\", runs });\n continue;\n }\n if (tag === \"ul\" || tag === \"ol\") {\n const items = listItems(element);\n if (items.length > 0) blocks.push({ type: \"list\", ordered: tag === \"ol\", items });\n continue;\n }\n if (tag === \"table\") {\n const table = tableBlock(element);\n if (table) blocks.push(table);\n continue;\n }\n if (tag === \"img\") {\n blocks.push(imageBlock(element));\n continue;\n }\n // Anything else — a `div`, a `section`, a tag mammoth grew in a later\n // version — contributes its CHILDREN, never itself. That is what makes\n // this an allowlist that cannot silently let markup through.\n walk(element);\n }\n };\n\n walk(document.body);\n return blocks;\n}\n\nfunction imageBlock(element: Element): DocxBlock {\n const alt = element.getAttribute(\"alt\")?.trim();\n return {\n type: \"image\",\n // mammoth inlines images as `data:` URIs by default, so there is no remote\n // origin here — and no `src` at all is better than a URL we did not vet.\n src: element.getAttribute(\"src\") ?? \"\",\n ...(alt ? { alt } : {}),\n };\n}\n\n/** Written before a list item's own text in the projection. */\nexport const DOCX_LIST_BULLET = \"• \";\n\n/** Written between two cells of the same row. */\nexport const DOCX_CELL_SEPARATOR = \"\\t\";\n\n/** The header row's `row`, so one field can name every row of a table. */\nexport const DOCX_HEAD_ROW = -1;\n\n/**\n * Where a stretch of the projection came from in the block model.\n *\n * Deliberately one shape with optional fields rather than a union: the renderer\n * looks a span up by \"this block, that item\" in three places, and a union would\n * make each of those a type narrowing exercise for no gain.\n */\nexport interface DocxRef {\n /** Index into the `blocks` array. */\n block: number;\n /** Which item, for a `list` block. */\n item?: number;\n /** Which row, for a `table` block. {@link DOCX_HEAD_ROW} is the header. */\n row?: number;\n}\n\n/**\n * Plain-text projection of a parsed document, plus the map back into the blocks.\n *\n * Rows rather than cells are the finest ref: one builder has one separator, and\n * a row's cells are joined by a tab while the rows themselves are joined by a\n * newline. The renderer walks a row's cells the same way it walks a paragraph's\n * runs — accumulating lengths — so the granularity of the MARK is still the\n * character, even though the granularity of the REF is the row.\n */\nexport function blocksToTextWithMap(blocks: DocxBlock[]): TextIndex<DocxRef> {\n const builder = createTextIndexBuilder<DocxRef>();\n blocks.forEach((block, index) => {\n if (block.type === \"heading\" || block.type === \"paragraph\") {\n builder.push(block.runs.map((run) => run.text).join(\"\"), { block: index });\n return;\n }\n if (block.type === \"list\") {\n block.items.forEach((item, item_) => {\n builder.push(`${DOCX_LIST_BULLET}${item.map((run) => run.text).join(\"\")}`, {\n block: index,\n item: item_,\n });\n });\n return;\n }\n if (block.type === \"table\") {\n if (block.head) {\n builder.push(block.head.join(DOCX_CELL_SEPARATOR), { block: index, row: DOCX_HEAD_ROW });\n }\n block.rows.forEach((row, rowIndex) => {\n builder.push(row.join(DOCX_CELL_SEPARATOR), { block: index, row: rowIndex });\n });\n }\n });\n return builder.build();\n}\n\n/**\n * Plain-text projection of a parsed document — powers search, copy and the raw\n * view. A thin wrapper so the projection has exactly one definition: it is the\n * index's own text, never a second assembly that could drift from it.\n */\nexport function blocksToText(blocks: DocxBlock[]): string {\n return blocksToTextWithMap(blocks).text;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,132 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ imageManifest
5
+ } from "./chunk-GGR7BNW4.js";
6
+ import {
7
+ toViewerError
8
+ } from "./chunk-AORNMH77.js";
9
+
10
+ // src/adapters/image/image-adapter.tsx
11
+ import { cn, StatePanel, useLocale } from "@elabs-ai/components-ui";
12
+ import { useEffect, useState } from "react";
13
+ import { jsx } from "react/jsx-runtime";
14
+ function measure(url, signal) {
15
+ if (typeof Image !== "function") return Promise.resolve(void 0);
16
+ return new Promise((resolve) => {
17
+ const img = new Image();
18
+ const done = (value) => {
19
+ img.onload = null;
20
+ img.onerror = null;
21
+ resolve(value);
22
+ };
23
+ img.onload = () => done({ width: img.naturalWidth, height: img.naturalHeight });
24
+ img.onerror = () => done(void 0);
25
+ signal?.addEventListener("abort", () => done(void 0), { once: true });
26
+ img.src = url;
27
+ });
28
+ }
29
+ var ImageAdapter = class {
30
+ #source;
31
+ async load(source, context) {
32
+ this.#source = source;
33
+ try {
34
+ const url = await source.url(context.signal);
35
+ const size = await measure(url, context.signal);
36
+ return { kind: "image", url, ...size };
37
+ } catch (error) {
38
+ throw toViewerError(error, "read-failed", { fileName: source.name });
39
+ }
40
+ }
41
+ dispose() {
42
+ this.#source?.revoke();
43
+ this.#source = void 0;
44
+ }
45
+ };
46
+ function ImageRenderer({
47
+ document: doc,
48
+ source,
49
+ className,
50
+ zoom = "fit-page",
51
+ rotation = 0
52
+ }) {
53
+ const image = doc;
54
+ const { t } = useLocale();
55
+ const [failed, setFailed] = useState(false);
56
+ useEffect(() => setFailed(false), [image.url]);
57
+ if (failed) {
58
+ return /* @__PURE__ */ jsx("div", { className: cn("flex min-h-full flex-col justify-center p-4", className), children: /* @__PURE__ */ jsx(
59
+ StatePanel,
60
+ {
61
+ kind: "error",
62
+ title: t("viewer.error.imageFailedTitle"),
63
+ description: t("viewer.error.imageFailed", { name: source.name })
64
+ }
65
+ ) });
66
+ }
67
+ const fitting = typeof zoom !== "number";
68
+ const quarter = rotation === 90 || rotation === 270;
69
+ const scaled = fitting || image.width === void 0 || image.height === void 0 ? void 0 : { width: image.width * zoom, height: image.height * zoom };
70
+ const boxed = quarter && scaled !== void 0;
71
+ const img = /* @__PURE__ */ jsx(
72
+ "img",
73
+ {
74
+ src: image.url,
75
+ alt: source.alt ?? "",
76
+ width: image.width,
77
+ height: image.height,
78
+ onError: () => setFailed(true),
79
+ className: cn(
80
+ "block object-contain",
81
+ !quarter && zoom === "fit-page" && "max-h-full max-w-full",
82
+ !quarter && zoom === "fit-width" && "h-auto max-w-full",
83
+ // Turned on its side, the pane's HEIGHT caps the image's width and its
84
+ // width caps the height. Container units read the pane directly, so the
85
+ // fit needs no measurement. Both fit modes converge here deliberately:
86
+ // an image whose rotated width filled the pane would overflow the top
87
+ // edge as well, which is the unreachable half described above.
88
+ fitting && quarter && "max-h-[100cqw] max-w-[100cqh]",
89
+ // At a fixed scale the intrinsic cap has to come off, or "200%" would
90
+ // silently stop at the pane's width.
91
+ !fitting && "max-w-none",
92
+ boxed && "absolute top-1/2 left-1/2"
93
+ ),
94
+ style: {
95
+ ...scaled,
96
+ ...rotation === 0 ? void 0 : {
97
+ transform: boxed ? `translate(-50%, -50%) rotate(${rotation}deg)` : `rotate(${rotation}deg)`
98
+ }
99
+ }
100
+ }
101
+ );
102
+ return (
103
+ // Centred on both axes: a small image pinned to the top-left of a tall pane
104
+ // reads as a layout accident. While fitting, the box takes the pane's height
105
+ // so `max-h-full` has something definite to resolve against; at a fixed
106
+ // scale it grows instead, and `FileViewerContent` scrolls it.
107
+ /* @__PURE__ */ jsx(
108
+ "div",
109
+ {
110
+ className: cn(
111
+ "flex items-center justify-center",
112
+ fitting ? "h-full" : "min-h-full",
113
+ // Only a size container can answer `cqh`, and only a definite height
114
+ // makes one — which is exactly the fitting case.
115
+ fitting && quarter && "[container-type:size]",
116
+ className
117
+ ),
118
+ children: quarter && scaled ? /* @__PURE__ */ jsx("div", { className: "relative shrink-0", style: { width: scaled.height, height: scaled.width }, children: img }) : img
119
+ }
120
+ )
121
+ );
122
+ }
123
+ var adapterModule = {
124
+ manifest: imageManifest,
125
+ create: () => new ImageAdapter(),
126
+ Renderer: ImageRenderer
127
+ };
128
+ var image_adapter_default = adapterModule;
129
+ export {
130
+ image_adapter_default as default
131
+ };
132
+ //# sourceMappingURL=image-adapter-WOHZR24J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/image/image-adapter.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Image adapter — the reference implementation of \"adapters emit data\".\n *\n * It renders an `<img>`, never a `<canvas>`. anyview draws images to a canvas\n * (`ImageAdapter.ts:118`, `PageRenderer.tsx:128`), which throws away the alt\n * text, the browser's own decoding and zoom, and the ability to select or save\n * the image — with no route back, because its API has no alt-text path at all.\n * Here the description travels on the `FileSource` itself, so a screen reader\n * user gets the same information a sighted user does.\n */\n\nimport { cn, StatePanel, useLocale } from \"@elabs-ai/components-ui\";\nimport { useEffect, useState } from \"react\";\n\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { imageManifest } from \"./image-manifest\";\nimport { toViewerError } from \"../../core/errors\";\nimport type { ResolvedFileSource } from \"@elabs-ai/components-ui\";\n\nexport interface ImageDocument extends AdapterDocument {\n kind: \"image\";\n /** A URL an `<img>` can load — the source's own URL, or a minted object URL. */\n url: string;\n /** Intrinsic size, once decoded. Used to reserve the box and to label the file. */\n width?: number;\n height?: number;\n}\n\n/** Decode just enough to learn the intrinsic size. Never rejects the load. */\nfunction measure(\n url: string,\n signal?: AbortSignal,\n): Promise<{ width: number; height: number } | undefined> {\n if (typeof Image !== \"function\") return Promise.resolve(undefined);\n return new Promise((resolve) => {\n const img = new Image();\n const done = (value?: { width: number; height: number }) => {\n img.onload = null;\n img.onerror = null;\n resolve(value);\n };\n img.onload = () => done({ width: img.naturalWidth, height: img.naturalHeight });\n // A measurement failure is not a load failure — the <img> will surface it.\n img.onerror = () => done(undefined);\n signal?.addEventListener(\"abort\", () => done(undefined), { once: true });\n img.src = url;\n });\n}\n\nclass ImageAdapter implements FileAdapter {\n #source?: ResolvedFileSource;\n\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<ImageDocument> {\n this.#source = source;\n try {\n const url = await source.url(context.signal);\n const size = await measure(url, context.signal);\n return { kind: \"image\", url, ...size };\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n }\n\n dispose(): void {\n // Releases the object URL this load minted, if any. A remote public image\n // never minted one, and `revoke()` is a no-op there.\n this.#source?.revoke();\n this.#source = undefined;\n }\n}\n\nfunction ImageRenderer({\n document: doc,\n source,\n className,\n zoom = \"fit-page\",\n rotation = 0,\n}: AdapterRendererProps) {\n const image = doc as ImageDocument;\n const { t } = useLocale();\n const [failed, setFailed] = useState(false);\n\n useEffect(() => setFailed(false), [image.url]);\n\n if (failed) {\n // A terminal, settled failure (loading-states.md): the browser tried and\n // gave up, so this is not a transient not-ready state.\n return (\n <div className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}>\n <StatePanel\n kind=\"error\"\n title={t(\"viewer.error.imageFailedTitle\")}\n description={t(\"viewer.error.imageFailed\", { name: source.name })}\n />\n </div>\n );\n }\n\n const fitting = typeof zoom !== \"number\";\n // A quarter turn swaps the axes: what was the image's height is now the width\n // the reader sees. Everything below that treats 90/270 differently is that one\n // fact — 180 leaves the bounds alone and needs none of it.\n const quarter = rotation === 90 || rotation === 270;\n const scaled =\n fitting || image.width === undefined || image.height === undefined\n ? undefined\n : { width: image.width * zoom, height: image.height * zoom };\n // A `transform` does not change layout, so a turned image at a fixed scale\n // needs a box with the ROTATED bounds. Without one it overflows the pane on\n // every side, and the half above the top edge is unreachable — scrolling only\n // ever reaches transform overflow past the END edges.\n const boxed = quarter && scaled !== undefined;\n\n const img = (\n <img\n src={image.url}\n // An image with no author description is decorative to AT — an empty alt\n // is correct and deliberate, not a missing label.\n alt={source.alt ?? \"\"}\n width={image.width}\n height={image.height}\n onError={() => setFailed(true)}\n className={cn(\n \"block object-contain\",\n !quarter && zoom === \"fit-page\" && \"max-h-full max-w-full\",\n !quarter && zoom === \"fit-width\" && \"h-auto max-w-full\",\n // Turned on its side, the pane's HEIGHT caps the image's width and its\n // width caps the height. Container units read the pane directly, so the\n // fit needs no measurement. Both fit modes converge here deliberately:\n // an image whose rotated width filled the pane would overflow the top\n // edge as well, which is the unreachable half described above.\n fitting && quarter && \"max-h-[100cqw] max-w-[100cqh]\",\n // At a fixed scale the intrinsic cap has to come off, or \"200%\" would\n // silently stop at the pane's width.\n !fitting && \"max-w-none\",\n boxed && \"absolute top-1/2 left-1/2\",\n )}\n style={{\n ...scaled,\n ...(rotation === 0\n ? undefined\n : {\n transform: boxed\n ? `translate(-50%, -50%) rotate(${rotation}deg)`\n : `rotate(${rotation}deg)`,\n }),\n }}\n />\n );\n\n return (\n // Centred on both axes: a small image pinned to the top-left of a tall pane\n // reads as a layout accident. While fitting, the box takes the pane's height\n // so `max-h-full` has something definite to resolve against; at a fixed\n // scale it grows instead, and `FileViewerContent` scrolls it.\n <div\n className={cn(\n \"flex items-center justify-center\",\n fitting ? \"h-full\" : \"min-h-full\",\n // Only a size container can answer `cqh`, and only a definite height\n // makes one — which is exactly the fitting case.\n fitting && quarter && \"[container-type:size]\",\n className,\n )}\n >\n {quarter && scaled ? (\n <div className=\"relative shrink-0\" style={{ width: scaled.height, height: scaled.width }}>\n {img}\n </div>\n ) : (\n img\n )}\n </div>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: imageManifest,\n create: () => new ImageAdapter(),\n Renderer: ImageRenderer,\n};\n\nexport default adapterModule;\n"],"mappings":"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","names":[]}