@tangle-network/ui 11.24.0 → 11.26.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 (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +22 -0
  3. package/dist/chat/agent-timeline.d.ts +13 -1
  4. package/dist/chat/agent-timeline.js +25 -4
  5. package/dist/chat/chat-message.d.ts +13 -1
  6. package/dist/chat/chat-message.js +9 -5
  7. package/dist/chat/message-author.d.ts +24 -0
  8. package/dist/chat/message-author.js +53 -0
  9. package/dist/chat/user-message.d.ts +12 -2
  10. package/dist/chat/user-message.js +52 -16
  11. package/dist/chat.d.ts +3 -1
  12. package/dist/chat.js +2 -1
  13. package/dist/editor/editor-peers.js +1 -21
  14. package/dist/files/file-card.d.ts +30 -0
  15. package/dist/files/file-card.js +162 -0
  16. package/dist/files/file-list.d.ts +25 -0
  17. package/dist/files/file-list.js +88 -0
  18. package/dist/files/file-preview.js +16 -62
  19. package/dist/files/pdf-loader.d.ts +22 -0
  20. package/dist/files/pdf-loader.js +82 -0
  21. package/dist/files/pdf-viewer.d.ts +32 -0
  22. package/dist/files/pdf-viewer.js +567 -0
  23. package/dist/files.d.ts +5 -1
  24. package/dist/files.js +5 -1
  25. package/dist/index.d.ts +9 -4
  26. package/dist/index.js +6 -1
  27. package/dist/lib/optional-peer.js +29 -0
  28. package/dist/primitives.d.ts +2 -2
  29. package/dist/types/message.d.ts +19 -0
  30. package/dist/types.d.ts +2 -2
  31. package/package.json +7 -2
  32. package/src/chat/agent-timeline.tsx +33 -3
  33. package/src/chat/chat-message.tsx +37 -10
  34. package/src/chat/index.ts +2 -0
  35. package/src/chat/message-author.stories.tsx +170 -0
  36. package/src/chat/message-author.test.tsx +189 -0
  37. package/src/chat/message-author.tsx +52 -0
  38. package/src/chat/user-message.tsx +63 -8
  39. package/src/editor/editor-peers.ts +3 -25
  40. package/src/files/file-artifact-pane.test.tsx +12 -2
  41. package/src/files/file-card.test.tsx +62 -0
  42. package/src/files/file-card.tsx +210 -0
  43. package/src/files/file-list.stories.tsx +121 -0
  44. package/src/files/file-list.test.tsx +81 -0
  45. package/src/files/file-list.tsx +167 -0
  46. package/src/files/file-preview.tsx +25 -94
  47. package/src/files/index.ts +9 -0
  48. package/src/files/pdf-loader.test.ts +130 -0
  49. package/src/files/pdf-loader.ts +169 -0
  50. package/src/files/pdf-viewer.stories.tsx +81 -0
  51. package/src/files/pdf-viewer.test.tsx +245 -0
  52. package/src/files/pdf-viewer.tsx +677 -0
  53. package/src/files/pdfjs-worker.d.ts +3 -0
  54. package/src/lib/optional-peer.ts +30 -0
  55. package/src/markdown/code-block.test.tsx +2 -1
  56. package/src/types/message.ts +20 -0
@@ -0,0 +1,162 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { IconTile } from "../primitives/icon-tile.js";
4
+ import { formatBytes } from "../utils/format.js";
5
+ import { getFormatLabel, resolveFilePreviewKind } from "./file-format.js";
6
+ import * as React from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Download, File, FileAudio, FileCode, FileImage, FileJson, FileSpreadsheet, FileText, FileVideo } from "lucide-react";
9
+ //#region src/files/file-card.tsx
10
+ /**
11
+ * FileCard — one file, described: its kind, name, size, date and uploader,
12
+ * with Download and any actions the host adds. FilePreview shows it for a file
13
+ * it cannot render inline; a host can show it anywhere a file stands on its own.
14
+ */
15
+ const KIND_ICON = {
16
+ pdf: {
17
+ icon: FileText,
18
+ tone: "orange"
19
+ },
20
+ image: {
21
+ icon: FileImage,
22
+ tone: "pink"
23
+ },
24
+ video: {
25
+ icon: FileVideo,
26
+ tone: "violet"
27
+ },
28
+ audio: {
29
+ icon: FileAudio,
30
+ tone: "cyan"
31
+ },
32
+ csv: {
33
+ icon: FileSpreadsheet,
34
+ tone: "lime"
35
+ },
36
+ spreadsheet: {
37
+ icon: FileSpreadsheet,
38
+ tone: "lime"
39
+ },
40
+ code: {
41
+ icon: FileCode,
42
+ tone: "blue"
43
+ },
44
+ json: {
45
+ icon: FileJson,
46
+ tone: "blue"
47
+ },
48
+ yaml: {
49
+ icon: FileCode,
50
+ tone: "blue"
51
+ },
52
+ markdown: {
53
+ icon: FileText,
54
+ tone: "teal"
55
+ },
56
+ text: {
57
+ icon: FileText,
58
+ tone: "teal"
59
+ },
60
+ binary: {
61
+ icon: File,
62
+ tone: "brown"
63
+ }
64
+ };
65
+ /** The kind's icon on its category tone. Decorative: the filename names the file. */
66
+ function FileKindTile({ filename, mimeType, size = "md", className }) {
67
+ const { icon: Icon, tone } = KIND_ICON[resolveFilePreviewKind(filename, mimeType)];
68
+ return /* @__PURE__ */ jsx(IconTile, {
69
+ tone,
70
+ size,
71
+ icon: /* @__PURE__ */ jsx(Icon, {}),
72
+ className
73
+ });
74
+ }
75
+ function toDate(value) {
76
+ if (value === void 0) return void 0;
77
+ const date = value instanceof Date ? value : new Date(value);
78
+ return Number.isNaN(date.getTime()) ? void 0 : date;
79
+ }
80
+ const DATE_FORMAT = new Intl.DateTimeFormat(void 0, { dateStyle: "medium" });
81
+ /**
82
+ * A dot between facts for the eye, a comma for a screen reader, which would
83
+ * otherwise read the facts as one run-on word or announce "middle dot".
84
+ */
85
+ function FactSeparator() {
86
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
87
+ "aria-hidden": "true",
88
+ children: " · "
89
+ }), /* @__PURE__ */ jsx("span", {
90
+ className: "sr-only",
91
+ children: ", "
92
+ })] });
93
+ }
94
+ /**
95
+ * The kind, size and date as one muted line, each part only when known.
96
+ * The date follows the reader's locale, which a server render may not share,
97
+ * so the element tolerates a hydration difference.
98
+ */
99
+ function FileFacts({ filename, mimeType, size, date, showKind = false, className }) {
100
+ const parsed = toDate(date);
101
+ const parts = [];
102
+ if (showKind) parts.push(getFormatLabel(resolveFilePreviewKind(filename, mimeType)));
103
+ if (size !== void 0) parts.push(formatBytes(size));
104
+ if (parsed) parts.push(/* @__PURE__ */ jsx("time", {
105
+ dateTime: parsed.toISOString(),
106
+ suppressHydrationWarning: true,
107
+ children: DATE_FORMAT.format(parsed)
108
+ }, "date"));
109
+ if (parts.length === 0) return null;
110
+ return /* @__PURE__ */ jsx("span", {
111
+ className: cn("text-xs text-muted-foreground", className),
112
+ children: parts.map((part, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [index > 0 && /* @__PURE__ */ jsx(FactSeparator, {}), part] }, index))
113
+ });
114
+ }
115
+ /** "Dana Ruiz (Client)", or the name alone. */
116
+ function uploaderLabel(uploader) {
117
+ return uploader.role ? `${uploader.name} (${uploader.role})` : uploader.name;
118
+ }
119
+ const FileCard = React.forwardRef(({ filename, mimeType, size, date, uploader, description, onDownload, actions, className, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
120
+ ref,
121
+ className: cn("flex min-w-0 flex-col items-center justify-center rounded-[var(--radius-md)] border border-border bg-card px-6 py-12 text-center", className),
122
+ ...props,
123
+ children: [
124
+ /* @__PURE__ */ jsx(FileKindTile, {
125
+ filename,
126
+ mimeType,
127
+ size: "lg"
128
+ }),
129
+ /* @__PURE__ */ jsx("p", {
130
+ className: "mt-3 line-clamp-2 max-w-full text-sm font-medium text-foreground [overflow-wrap:anywhere]",
131
+ title: filename,
132
+ children: filename
133
+ }),
134
+ /* @__PURE__ */ jsx(FileFacts, {
135
+ filename,
136
+ size,
137
+ date,
138
+ className: "mt-1"
139
+ }),
140
+ uploader && /* @__PURE__ */ jsxs("p", {
141
+ className: "mt-0.5 max-w-full truncate text-xs text-muted-foreground",
142
+ children: ["Uploaded by ", uploaderLabel(uploader)]
143
+ }),
144
+ description && /* @__PURE__ */ jsx("div", {
145
+ className: "mt-3 max-w-md text-sm text-muted-foreground",
146
+ children: description
147
+ }),
148
+ (onDownload || actions) && /* @__PURE__ */ jsxs("div", {
149
+ className: "mt-5 flex flex-wrap items-center justify-center gap-2",
150
+ children: [onDownload && /* @__PURE__ */ jsxs(Button, {
151
+ type: "button",
152
+ variant: "outline",
153
+ size: "sm",
154
+ onClick: onDownload,
155
+ children: [/* @__PURE__ */ jsx(Download, {}), "Download"]
156
+ }), actions]
157
+ })
158
+ ]
159
+ }));
160
+ FileCard.displayName = "FileCard";
161
+ //#endregion
162
+ export { FactSeparator, FileCard, FileFacts, FileKindTile, uploaderLabel };
@@ -0,0 +1,25 @@
1
+ import { FileDetails } from "./file-card.js";
2
+ import * as React from "react";
3
+ //#region src/files/file-list.d.ts
4
+ export interface FileListItem extends FileDetails {
5
+ /** Stable key, and what `activeId` matches. */
6
+ id: string;
7
+ }
8
+ export interface FileListProps<T extends FileListItem = FileListItem> extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
9
+ files: readonly T[];
10
+ /** Opens a file, usually in a preview. Makes each row a button. */
11
+ onOpen?: (file: T) => void;
12
+ /** Adds a Download button to each row. */
13
+ onDownload?: (file: T) => void;
14
+ /**
15
+ * Adds a Delete button to each row. It calls this at once, so a host that
16
+ * wants a confirmation asks for it here.
17
+ */
18
+ onDelete?: (file: T) => void;
19
+ /** The open file. Its row is marked as the current one. */
20
+ activeId?: string;
21
+ /** Shown in place of the list when `files` is empty. */
22
+ empty?: React.ReactNode;
23
+ }
24
+ export declare function FileList<T extends FileListItem>({ files, onOpen, onDownload, onDelete, activeId, empty, className, ...props }: FileListProps<T>): React.JSX.Element;
25
+ //#endregion
@@ -0,0 +1,88 @@
1
+ import { focusRingInset } from "../lib/focus.js";
2
+ import { cn } from "../lib/utils.js";
3
+ import { Button } from "../primitives/button.js";
4
+ import { FactSeparator, FileFacts, FileKindTile, uploaderLabel } from "./file-card.js";
5
+ import "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { Download, Trash2 } from "lucide-react";
8
+ //#region src/files/file-list.tsx
9
+ function FileList({ files, onOpen, onDownload, onDelete, activeId, empty = "No files yet.", className, ...props }) {
10
+ if (files.length === 0) return /* @__PURE__ */ jsx("div", {
11
+ className: cn("rounded-[var(--radius-md)] border border-border bg-card px-4 py-10 text-center text-sm text-muted-foreground", className),
12
+ ...props,
13
+ children: empty
14
+ });
15
+ return /* @__PURE__ */ jsx("ul", {
16
+ className: cn("divide-y divide-border overflow-hidden rounded-[var(--radius-md)] border border-border bg-card", className),
17
+ ...props,
18
+ children: files.map((file) => {
19
+ const active = file.id === activeId;
20
+ const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FileKindTile, {
21
+ filename: file.filename,
22
+ mimeType: file.mimeType
23
+ }), /* @__PURE__ */ jsxs("span", {
24
+ className: "grid min-w-0 flex-1",
25
+ children: [
26
+ /* @__PURE__ */ jsx("span", {
27
+ className: "line-clamp-2 text-sm font-medium text-foreground [overflow-wrap:anywhere] sm:line-clamp-1",
28
+ children: file.filename
29
+ }),
30
+ /* @__PURE__ */ jsxs("span", {
31
+ className: "truncate text-xs text-muted-foreground",
32
+ children: [/* @__PURE__ */ jsx(FileFacts, {
33
+ filename: file.filename,
34
+ mimeType: file.mimeType,
35
+ size: file.size,
36
+ date: file.date,
37
+ showKind: true
38
+ }), file.uploader && /* @__PURE__ */ jsxs("span", {
39
+ className: "hidden sm:inline",
40
+ children: [/* @__PURE__ */ jsx(FactSeparator, {}), uploaderLabel(file.uploader)]
41
+ })]
42
+ }),
43
+ file.uploader && /* @__PURE__ */ jsx("span", {
44
+ className: "truncate text-xs text-muted-foreground sm:hidden",
45
+ children: uploaderLabel(file.uploader)
46
+ })
47
+ ]
48
+ })] });
49
+ return /* @__PURE__ */ jsxs("li", {
50
+ className: cn("flex items-center gap-1 pr-2", active ? "bg-primary/10" : onOpen && "hover:bg-[var(--bg-hover)]"),
51
+ children: [
52
+ onOpen ? /* @__PURE__ */ jsx("button", {
53
+ type: "button",
54
+ onClick: () => onOpen(file),
55
+ "aria-current": active ? "true" : void 0,
56
+ className: cn("flex min-w-0 flex-1 items-center gap-3 py-2.5 pl-3 text-left", focusRingInset),
57
+ children: body
58
+ }) : /* @__PURE__ */ jsx("div", {
59
+ "aria-current": active ? "true" : void 0,
60
+ className: "flex min-w-0 flex-1 items-center gap-3 py-2.5 pl-3",
61
+ children: body
62
+ }),
63
+ onDownload && /* @__PURE__ */ jsx(Button, {
64
+ type: "button",
65
+ variant: "ghost",
66
+ size: "icon",
67
+ "aria-label": `Download ${file.filename}`,
68
+ title: "Download",
69
+ onClick: () => onDownload(file),
70
+ children: /* @__PURE__ */ jsx(Download, {})
71
+ }),
72
+ onDelete && /* @__PURE__ */ jsx(Button, {
73
+ type: "button",
74
+ variant: "ghost",
75
+ size: "icon",
76
+ "aria-label": `Delete ${file.filename}`,
77
+ title: "Delete",
78
+ className: "hover:text-[var(--surface-danger-text)]",
79
+ onClick: () => onDelete(file),
80
+ children: /* @__PURE__ */ jsx(Trash2, {})
81
+ })
82
+ ]
83
+ }, file.id);
84
+ })
85
+ });
86
+ }
87
+ //#endregion
88
+ export { FileList };
@@ -1,19 +1,19 @@
1
1
  import { focusRingInset } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
- import { Button } from "../primitives/button.js";
4
3
  import { CodeBlock, CopyButton } from "../markdown/code-block.js";
5
- import { formatBytes } from "../utils/format.js";
6
4
  import { fileExtension, getCodeLanguage, getFormatLabel, resolveFilePreviewKind } from "./file-format.js";
7
5
  import { Markdown } from "../markdown/markdown.js";
6
+ import { FileCard } from "./file-card.js";
7
+ import { PdfViewer } from "./pdf-viewer.js";
8
8
  import { useState } from "react";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
- import { Download, FileSpreadsheet, FileText, Music, X } from "lucide-react";
10
+ import { Download, Music, X } from "lucide-react";
11
11
  //#region src/files/file-preview.tsx
12
12
  /**
13
13
  * FilePreview — universal file renderer.
14
14
  *
15
15
  * Renders any file type:
16
- * - PDF: embedded viewer at full pane height
16
+ * - PDF: PdfViewer's rendered pages at full pane height
17
17
  * - Images: fit to the pane on a checker ground; click toggles natural size
18
18
  * - Video / audio: native players
19
19
  * - CSV: sticky-header table, capped at CSV_PREVIEW_ROW_LIMIT rows
@@ -146,21 +146,6 @@ function ImagePreview({ src, filename }) {
146
146
  })
147
147
  });
148
148
  }
149
- function PdfPreview({ blobUrl, filename, size, onDownload }) {
150
- return /* @__PURE__ */ jsx("object", {
151
- data: blobUrl,
152
- type: "application/pdf",
153
- title: filename,
154
- className: "min-h-[24rem] w-full flex-1 rounded-[var(--radius-md)]",
155
- children: /* @__PURE__ */ jsx(DownloadCard, {
156
- filename,
157
- size,
158
- sentence: "This browser does not show PDFs inline.",
159
- onDownload,
160
- className: "h-full"
161
- })
162
- });
163
- }
164
149
  function VideoPreview({ src, filename }) {
165
150
  return /* @__PURE__ */ jsx("div", {
166
151
  className: "flex min-h-[12rem] flex-1 items-center justify-center overflow-hidden rounded-[var(--radius-md)] border border-border bg-background",
@@ -207,55 +192,23 @@ function MarkdownPreview({ content, urlTransform }) {
207
192
  })
208
193
  });
209
194
  }
210
- /**
211
- * One muted sentence plus the filename, its size when known, and a Download
212
- * button when the host wires `onDownload`. Serves spreadsheets, binaries, and
213
- * every kind whose source (blob or text) is missing.
214
- */
215
- function DownloadCard({ filename, size, sentence, icon: Icon = FileText, onDownload, className }) {
216
- return /* @__PURE__ */ jsxs("div", {
217
- className: cn("flex flex-col items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border bg-background px-6 py-16 text-center", className),
218
- children: [
219
- /* @__PURE__ */ jsx(Icon, { className: "mb-3 h-12 w-12 text-muted-foreground opacity-40" }),
220
- /* @__PURE__ */ jsx("p", {
221
- className: "max-w-full truncate text-sm font-medium text-foreground",
222
- children: filename
223
- }),
224
- size !== void 0 && /* @__PURE__ */ jsx("p", {
225
- className: "mt-1 text-xs text-muted-foreground",
226
- children: formatBytes(size)
227
- }),
228
- /* @__PURE__ */ jsx("p", {
229
- className: "mt-3 max-w-md text-sm text-muted-foreground",
230
- children: sentence
231
- }),
232
- onDownload && /* @__PURE__ */ jsxs(Button, {
233
- type: "button",
234
- variant: "outline",
235
- size: "sm",
236
- className: "mt-5",
237
- onClick: onDownload,
238
- children: [/* @__PURE__ */ jsx(Download, {}), "Download"]
239
- })
240
- ]
241
- });
242
- }
243
- function PreviewBody({ kind, filename, content, blobUrl, size, urlTransform, onDownload }) {
195
+ function PreviewBody({ kind, filename, mimeType, content, blobUrl, size, urlTransform, onDownload }) {
244
196
  const hasText = typeof content === "string";
245
- const card = (sentence, icon) => /* @__PURE__ */ jsx(DownloadCard, {
197
+ const card = (sentence) => /* @__PURE__ */ jsx(FileCard, {
246
198
  filename,
199
+ mimeType,
247
200
  size,
248
- sentence,
249
- icon,
201
+ description: sentence,
250
202
  onDownload
251
203
  });
252
204
  switch (kind) {
253
205
  case "pdf":
254
- if (blobUrl) return /* @__PURE__ */ jsx(PdfPreview, {
255
- blobUrl,
206
+ if (blobUrl) return /* @__PURE__ */ jsx(PdfViewer, {
207
+ src: blobUrl,
256
208
  filename,
257
209
  size,
258
- onDownload
210
+ onDownload,
211
+ className: "min-h-[24rem] flex-1"
259
212
  });
260
213
  return card(NEEDS_DOWNLOAD_LINK);
261
214
  case "image":
@@ -280,10 +233,10 @@ function PreviewBody({ kind, filename, content, blobUrl, size, urlTransform, onD
280
233
  src: blobUrl,
281
234
  filename
282
235
  });
283
- return card(NEEDS_DOWNLOAD_LINK, Music);
236
+ return card(NEEDS_DOWNLOAD_LINK);
284
237
  case "csv":
285
238
  if (hasText) return /* @__PURE__ */ jsx(CsvPreview, { content });
286
- return card(NO_INLINE_CONTENT, FileSpreadsheet);
239
+ return card(NO_INLINE_CONTENT);
287
240
  case "code":
288
241
  case "json":
289
242
  case "yaml":
@@ -302,7 +255,7 @@ function PreviewBody({ kind, filename, content, blobUrl, size, urlTransform, onD
302
255
  case "text":
303
256
  if (hasText) return /* @__PURE__ */ jsx(TextPreview, { content });
304
257
  return card(NO_INLINE_CONTENT);
305
- case "spreadsheet": return card("Download to open this workbook in a spreadsheet app.", FileSpreadsheet);
258
+ case "spreadsheet": return card("Download to open this workbook in a spreadsheet app.");
306
259
  case "binary":
307
260
  if (hasText) return /* @__PURE__ */ jsx(TextPreview, { content });
308
261
  return card(blobUrl ? NO_INLINE_PREVIEW : NEEDS_DOWNLOAD_LINK);
@@ -346,6 +299,7 @@ function FilePreview({ filename, content, blobUrl, mimeType, size, urlTransform,
346
299
  children: /* @__PURE__ */ jsx(PreviewBody, {
347
300
  kind,
348
301
  filename,
302
+ mimeType,
349
303
  content,
350
304
  blobUrl,
351
305
  size,
@@ -0,0 +1,22 @@
1
+ import "pdfjs-dist";
2
+ //#region src/files/pdf-loader.d.ts
3
+ export interface PdfViewerConfig {
4
+ /**
5
+ * URL of `pdfjs-dist/legacy/build/pdf.worker.min.mjs` from the installed
6
+ * pdfjs-dist, served by the app. Moves PDF parsing off the main thread.
7
+ */
8
+ workerSrc?: string;
9
+ /**
10
+ * Base URL, ending in `/`, under which the app serves the `wasm/`, `cmaps/`,
11
+ * `standard_fonts/` and `iccs/` directories of the installed pdfjs-dist.
12
+ * Needed for scanned pages, CJK text and non-embedded fonts.
13
+ */
14
+ assetsUrl?: string;
15
+ }
16
+ /**
17
+ * Sets the page-wide pdf.js worker and asset locations. Call it once, before
18
+ * the first `PdfViewer` renders; a worker set afterwards applies only when no
19
+ * document has loaded the main-thread worker yet.
20
+ */
21
+ export declare function configurePdfViewer(next: PdfViewerConfig): void;
22
+ //#endregion
@@ -0,0 +1,82 @@
1
+ import { isMissingPeerError, rethrow } from "../lib/optional-peer.js";
2
+ //#region src/files/pdf-loader.ts
3
+ let config = {};
4
+ /**
5
+ * Sets the page-wide pdf.js worker and asset locations. Call it once, before
6
+ * the first `PdfViewer` renders; a worker set afterwards applies only when no
7
+ * document has loaded the main-thread worker yet.
8
+ */
9
+ function configurePdfViewer(next) {
10
+ config = { ...next };
11
+ }
12
+ /**
13
+ * The optional peer behind `PdfViewer`. `scripts/validate-dist.mjs` reads this
14
+ * list and requires every import of it to be dynamic and caught.
15
+ */
16
+ const DEFERRED_PEERS = ["pdfjs-dist"];
17
+ /** pdfjs-dist is absent, or resolved to a stub with none of its API. */
18
+ var MissingPdfjsError = class extends Error {
19
+ constructor(options) {
20
+ super("PdfViewer needs the optional peer pdfjs-dist. Install pdfjs-dist to render PDF pages.", options);
21
+ this.name = "MissingPdfjsError";
22
+ }
23
+ };
24
+ function isMissingPdfjsError(error) {
25
+ return error instanceof Error && error.name === "MissingPdfjsError";
26
+ }
27
+ /**
28
+ * Turns a resolution failure that names pdfjs-dist into `MissingPdfjsError`.
29
+ * Any other rejection, such as a chunk that did not download, passes through.
30
+ */
31
+ function asMissingPdfjsError(error) {
32
+ if (!isMissingPeerError(error)) return error;
33
+ const message = error.message;
34
+ return DEFERRED_PEERS.some((name) => message.includes(name)) ? new MissingPdfjsError({ cause: error }) : error;
35
+ }
36
+ async function importPdfjs() {
37
+ let pdfjs;
38
+ try {
39
+ pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs").catch(rethrow);
40
+ } catch (error) {
41
+ throw asMissingPdfjsError(error);
42
+ }
43
+ if (typeof pdfjs.getDocument !== "function") throw new MissingPdfjsError();
44
+ return pdfjs;
45
+ }
46
+ let pending = null;
47
+ let warnedAboutAssets = false;
48
+ /**
49
+ * Resolves pdf.js with its worker configured. A missing peer stays missing for
50
+ * the session, so its rejection is kept; any other failure, such as a chunk
51
+ * that did not download, is dropped so the next viewer tries again.
52
+ */
53
+ function loadPdfjs() {
54
+ pending ??= importPdfjs().catch((error) => {
55
+ if (!isMissingPdfjsError(error)) pending = null;
56
+ throw error;
57
+ });
58
+ return pending.then(async (pdfjs) => {
59
+ const { GlobalWorkerOptions } = pdfjs;
60
+ if (config.workerSrc) GlobalWorkerOptions.workerSrc = config.workerSrc;
61
+ else if (!GlobalWorkerOptions.workerSrc && !GlobalWorkerOptions.workerPort) await import("pdfjs-dist/legacy/build/pdf.worker.min.mjs").catch(rethrow);
62
+ if (!config.assetsUrl && !warnedAboutAssets) {
63
+ warnedAboutAssets = true;
64
+ console.warn("PdfViewer: no assetsUrl is configured, so JBIG2, CCITT fax and JPEG 2000 images (most black-and-white scans) will not render. See configurePdfViewer.");
65
+ }
66
+ return pdfjs;
67
+ });
68
+ }
69
+ /** `getDocument` asset options for the configured `assetsUrl`, or none. */
70
+ function pdfAssetParams() {
71
+ if (!config.assetsUrl) return {};
72
+ const base = new URL(config.assetsUrl, document.baseURI).href.replace(/\/?$/, "/");
73
+ return {
74
+ wasmUrl: `${base}wasm/`,
75
+ cMapUrl: `${base}cmaps/`,
76
+ cMapPacked: true,
77
+ standardFontDataUrl: `${base}standard_fonts/`,
78
+ iccUrl: `${base}iccs/`
79
+ };
80
+ }
81
+ //#endregion
82
+ export { MissingPdfjsError, asMissingPdfjsError, configurePdfViewer, isMissingPdfjsError, loadPdfjs, pdfAssetParams };
@@ -0,0 +1,32 @@
1
+ //#region src/files/pdf-viewer.d.ts
2
+ /**
3
+ * PdfViewer — renders a PDF's pages to canvas with pdf.js, so a document reads
4
+ * the same in every browser, phones included. A continuous column of pages
5
+ * with page navigation, zoom and fit-to-width, and Download.
6
+ *
7
+ * Only pages within a screen of the visible area hold a canvas; the rest keep
8
+ * their size and release theirs, so a long document stays inside the canvas
9
+ * memory a phone allows.
10
+ *
11
+ * When pdf.js is not installed or cannot open the file, the viewer says so
12
+ * and falls back to the browser's own viewer through `<object>`, and from
13
+ * there to a FileCard where the browser has none (Android Chrome).
14
+ */
15
+ export interface PdfViewerProps {
16
+ /**
17
+ * The PDF: an object URL, a data URL, or an http(s) URL. A URL on another
18
+ * origin must allow this one through CORS; when it does not, the viewer
19
+ * falls back to the browser's viewer.
20
+ */
21
+ src: string;
22
+ /** Names the document for assistive technology and the fallback card. */
23
+ filename?: string;
24
+ /** Size in bytes, shown on the fallback card. */
25
+ size?: number;
26
+ /** Adds a Download button to the toolbar and the fallback card. */
27
+ onDownload?: () => void;
28
+ /** Give the viewer a height; it scrolls its pages inside it. */
29
+ className?: string;
30
+ }
31
+ export declare function PdfViewer({ src, filename, size, onDownload, className }: PdfViewerProps): import("react").JSX.Element;
32
+ //#endregion