@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.
- package/CHANGELOG.md +12 -0
- package/README.md +22 -0
- package/dist/chat/agent-timeline.d.ts +13 -1
- package/dist/chat/agent-timeline.js +25 -4
- package/dist/chat/chat-message.d.ts +13 -1
- package/dist/chat/chat-message.js +9 -5
- package/dist/chat/message-author.d.ts +24 -0
- package/dist/chat/message-author.js +53 -0
- package/dist/chat/user-message.d.ts +12 -2
- package/dist/chat/user-message.js +52 -16
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -1
- package/dist/editor/editor-peers.js +1 -21
- package/dist/files/file-card.d.ts +30 -0
- package/dist/files/file-card.js +162 -0
- package/dist/files/file-list.d.ts +25 -0
- package/dist/files/file-list.js +88 -0
- package/dist/files/file-preview.js +16 -62
- package/dist/files/pdf-loader.d.ts +22 -0
- package/dist/files/pdf-loader.js +82 -0
- package/dist/files/pdf-viewer.d.ts +32 -0
- package/dist/files/pdf-viewer.js +567 -0
- package/dist/files.d.ts +5 -1
- package/dist/files.js +5 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -1
- package/dist/lib/optional-peer.js +29 -0
- package/dist/primitives.d.ts +2 -2
- package/dist/types/message.d.ts +19 -0
- package/dist/types.d.ts +2 -2
- package/package.json +7 -2
- package/src/chat/agent-timeline.tsx +33 -3
- package/src/chat/chat-message.tsx +37 -10
- package/src/chat/index.ts +2 -0
- package/src/chat/message-author.stories.tsx +170 -0
- package/src/chat/message-author.test.tsx +189 -0
- package/src/chat/message-author.tsx +52 -0
- package/src/chat/user-message.tsx +63 -8
- package/src/editor/editor-peers.ts +3 -25
- package/src/files/file-artifact-pane.test.tsx +12 -2
- package/src/files/file-card.test.tsx +62 -0
- package/src/files/file-card.tsx +210 -0
- package/src/files/file-list.stories.tsx +121 -0
- package/src/files/file-list.test.tsx +81 -0
- package/src/files/file-list.tsx +167 -0
- package/src/files/file-preview.tsx +25 -94
- package/src/files/index.ts +9 -0
- package/src/files/pdf-loader.test.ts +130 -0
- package/src/files/pdf-loader.ts +169 -0
- package/src/files/pdf-viewer.stories.tsx +81 -0
- package/src/files/pdf-viewer.test.tsx +245 -0
- package/src/files/pdf-viewer.tsx +677 -0
- package/src/files/pdfjs-worker.d.ts +3 -0
- package/src/lib/optional-peer.ts +30 -0
- package/src/markdown/code-block.test.tsx +2 -1
- 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,
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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."
|
|
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
|