@relevaince/document-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +248 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +225 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bounding box in normalised coordinates (0–1 range),
|
|
5
|
+
* origin at top-left of the page.
|
|
6
|
+
*/
|
|
7
|
+
type BBox = {
|
|
8
|
+
x1: number;
|
|
9
|
+
y1: number;
|
|
10
|
+
x2: number;
|
|
11
|
+
y2: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Describes *what* to show and optionally *where* to focus.
|
|
15
|
+
*
|
|
16
|
+
* Consumers pass a `ViewerTarget` to `<DocumentViewer>` each time
|
|
17
|
+
* the user clicks a citation.
|
|
18
|
+
*/
|
|
19
|
+
type ViewerTarget = {
|
|
20
|
+
/** Public URL of the document (PDF / DOCX). */
|
|
21
|
+
url: string;
|
|
22
|
+
/** Explicit file type — auto-detected from URL extension when omitted. */
|
|
23
|
+
fileType?: "pdf" | "docx";
|
|
24
|
+
/** 1-indexed page to scroll to. */
|
|
25
|
+
page?: number;
|
|
26
|
+
/** Bounding box to highlight (normalised 0–1 coords). */
|
|
27
|
+
bbox?: BBox;
|
|
28
|
+
/** Cited text snippet — for future text-search highlighting. */
|
|
29
|
+
text?: string;
|
|
30
|
+
/** Display title shown in the viewer header. */
|
|
31
|
+
title?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type DocumentViewerProps = {
|
|
35
|
+
/** The document + focus target. Pass `null` to hide the viewer. */
|
|
36
|
+
target: ViewerTarget | null;
|
|
37
|
+
/** Override the default pdfjs web-worker URL. */
|
|
38
|
+
pdfWorkerUrl?: string;
|
|
39
|
+
/** Custom loading indicator. */
|
|
40
|
+
loadingComponent?: React.ReactNode;
|
|
41
|
+
/** Custom error state. */
|
|
42
|
+
errorComponent?: React.ReactNode;
|
|
43
|
+
/** Fires when the document begins loading. */
|
|
44
|
+
onLoadStart?: () => void;
|
|
45
|
+
/** Fires when the document has finished loading. */
|
|
46
|
+
onLoadSuccess?: () => void;
|
|
47
|
+
/** Fires when the document fails to load. */
|
|
48
|
+
onLoadError?: (err: Error) => void;
|
|
49
|
+
/** Optional className for the outermost wrapper. */
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Top-level component that routes to the correct renderer
|
|
55
|
+
* based on the target's file type.
|
|
56
|
+
*
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <DocumentViewer
|
|
59
|
+
* target={{
|
|
60
|
+
* url: "https://example.com/report.pdf",
|
|
61
|
+
* page: 3,
|
|
62
|
+
* bbox: { x1: 0.1, y1: 0.2, x2: 0.5, y2: 0.3 },
|
|
63
|
+
* }}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function DocumentViewer({ target, pdfWorkerUrl, loadingComponent, errorComponent, onLoadStart, onLoadSuccess, onLoadError, className, }: DocumentViewerProps): react_jsx_runtime.JSX.Element | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Infer document type from URL extension.
|
|
71
|
+
* Strips query-strings and fragments before matching.
|
|
72
|
+
*/
|
|
73
|
+
declare function getFileType(url: string): "pdf" | "docx" | "unknown";
|
|
74
|
+
|
|
75
|
+
export { type BBox, DocumentViewer, type DocumentViewerProps, type ViewerTarget, getFileType };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bounding box in normalised coordinates (0–1 range),
|
|
5
|
+
* origin at top-left of the page.
|
|
6
|
+
*/
|
|
7
|
+
type BBox = {
|
|
8
|
+
x1: number;
|
|
9
|
+
y1: number;
|
|
10
|
+
x2: number;
|
|
11
|
+
y2: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Describes *what* to show and optionally *where* to focus.
|
|
15
|
+
*
|
|
16
|
+
* Consumers pass a `ViewerTarget` to `<DocumentViewer>` each time
|
|
17
|
+
* the user clicks a citation.
|
|
18
|
+
*/
|
|
19
|
+
type ViewerTarget = {
|
|
20
|
+
/** Public URL of the document (PDF / DOCX). */
|
|
21
|
+
url: string;
|
|
22
|
+
/** Explicit file type — auto-detected from URL extension when omitted. */
|
|
23
|
+
fileType?: "pdf" | "docx";
|
|
24
|
+
/** 1-indexed page to scroll to. */
|
|
25
|
+
page?: number;
|
|
26
|
+
/** Bounding box to highlight (normalised 0–1 coords). */
|
|
27
|
+
bbox?: BBox;
|
|
28
|
+
/** Cited text snippet — for future text-search highlighting. */
|
|
29
|
+
text?: string;
|
|
30
|
+
/** Display title shown in the viewer header. */
|
|
31
|
+
title?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type DocumentViewerProps = {
|
|
35
|
+
/** The document + focus target. Pass `null` to hide the viewer. */
|
|
36
|
+
target: ViewerTarget | null;
|
|
37
|
+
/** Override the default pdfjs web-worker URL. */
|
|
38
|
+
pdfWorkerUrl?: string;
|
|
39
|
+
/** Custom loading indicator. */
|
|
40
|
+
loadingComponent?: React.ReactNode;
|
|
41
|
+
/** Custom error state. */
|
|
42
|
+
errorComponent?: React.ReactNode;
|
|
43
|
+
/** Fires when the document begins loading. */
|
|
44
|
+
onLoadStart?: () => void;
|
|
45
|
+
/** Fires when the document has finished loading. */
|
|
46
|
+
onLoadSuccess?: () => void;
|
|
47
|
+
/** Fires when the document fails to load. */
|
|
48
|
+
onLoadError?: (err: Error) => void;
|
|
49
|
+
/** Optional className for the outermost wrapper. */
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Top-level component that routes to the correct renderer
|
|
55
|
+
* based on the target's file type.
|
|
56
|
+
*
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <DocumentViewer
|
|
59
|
+
* target={{
|
|
60
|
+
* url: "https://example.com/report.pdf",
|
|
61
|
+
* page: 3,
|
|
62
|
+
* bbox: { x1: 0.1, y1: 0.2, x2: 0.5, y2: 0.3 },
|
|
63
|
+
* }}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function DocumentViewer({ target, pdfWorkerUrl, loadingComponent, errorComponent, onLoadStart, onLoadSuccess, onLoadError, className, }: DocumentViewerProps): react_jsx_runtime.JSX.Element | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Infer document type from URL extension.
|
|
71
|
+
* Strips query-strings and fragments before matching.
|
|
72
|
+
*/
|
|
73
|
+
declare function getFileType(url: string): "pdf" | "docx" | "unknown";
|
|
74
|
+
|
|
75
|
+
export { type BBox, DocumentViewer, type DocumentViewerProps, type ViewerTarget, getFileType };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DocumentViewer: () => DocumentViewer,
|
|
24
|
+
getFileType: () => getFileType
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/utils/getFileType.ts
|
|
29
|
+
function getFileType(url) {
|
|
30
|
+
const clean = url.split("?")[0].split("#")[0].toLowerCase();
|
|
31
|
+
if (clean.endsWith(".pdf")) return "pdf";
|
|
32
|
+
if (clean.endsWith(".docx") || clean.endsWith(".doc")) return "docx";
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/components/renderers/PdfRenderer.tsx
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
var import_react_pdf_highlighter_extended = require("react-pdf-highlighter-extended");
|
|
39
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
40
|
+
var CITATION_ID = "citation-highlight";
|
|
41
|
+
var DEFAULT_WORKER_URL = "https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs";
|
|
42
|
+
function bboxToHighlight(target) {
|
|
43
|
+
if (!target.bbox) return null;
|
|
44
|
+
const page = target.page ?? 1;
|
|
45
|
+
const { x1, y1, x2, y2 } = target.bbox;
|
|
46
|
+
const scaledRect = {
|
|
47
|
+
x1,
|
|
48
|
+
y1,
|
|
49
|
+
x2,
|
|
50
|
+
y2,
|
|
51
|
+
width: 1,
|
|
52
|
+
height: 1,
|
|
53
|
+
pageNumber: page
|
|
54
|
+
};
|
|
55
|
+
const position = {
|
|
56
|
+
boundingRect: scaledRect,
|
|
57
|
+
rects: [scaledRect]
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
id: CITATION_ID,
|
|
61
|
+
type: "area",
|
|
62
|
+
position
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function CitationHighlightRenderer() {
|
|
66
|
+
const { highlight, isScrolledTo } = (0, import_react_pdf_highlighter_extended.useHighlightContainerContext)();
|
|
67
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
68
|
+
import_react_pdf_highlighter_extended.AreaHighlight,
|
|
69
|
+
{
|
|
70
|
+
highlight,
|
|
71
|
+
isScrolledTo,
|
|
72
|
+
style: {
|
|
73
|
+
background: isScrolledTo ? "rgba(255, 171, 0, 0.35)" : "rgba(255, 171, 0, 0.2)",
|
|
74
|
+
border: "1.5px solid rgba(255, 171, 0, 0.6)",
|
|
75
|
+
borderRadius: 2,
|
|
76
|
+
transition: "background 200ms ease"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function PdfRenderer({
|
|
82
|
+
target,
|
|
83
|
+
workerUrl,
|
|
84
|
+
loadingComponent,
|
|
85
|
+
errorComponent,
|
|
86
|
+
onLoadSuccess,
|
|
87
|
+
onLoadError
|
|
88
|
+
}) {
|
|
89
|
+
const highlighterRef = (0, import_react.useRef)(null);
|
|
90
|
+
const loadNotifiedRef = (0, import_react.useRef)(false);
|
|
91
|
+
const highlights = (0, import_react.useMemo)(() => {
|
|
92
|
+
const h = bboxToHighlight(target);
|
|
93
|
+
return h ? [h] : [];
|
|
94
|
+
}, [target]);
|
|
95
|
+
(0, import_react.useEffect)(() => {
|
|
96
|
+
if (highlights.length === 0) return;
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
highlighterRef.current?.scrollToHighlight(highlights[0]);
|
|
99
|
+
}, 400);
|
|
100
|
+
return () => clearTimeout(timer);
|
|
101
|
+
}, [highlights]);
|
|
102
|
+
const handleUtilsRef = (0, import_react.useCallback)((utils) => {
|
|
103
|
+
highlighterRef.current = utils;
|
|
104
|
+
}, []);
|
|
105
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: "100%", height: "100%", position: "relative" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
106
|
+
import_react_pdf_highlighter_extended.PdfLoader,
|
|
107
|
+
{
|
|
108
|
+
document: target.url,
|
|
109
|
+
workerSrc: workerUrl ?? DEFAULT_WORKER_URL,
|
|
110
|
+
beforeLoad: () => loadingComponent ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
111
|
+
"div",
|
|
112
|
+
{
|
|
113
|
+
style: {
|
|
114
|
+
display: "flex",
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
width: "100%",
|
|
118
|
+
height: "100%",
|
|
119
|
+
color: "#888",
|
|
120
|
+
fontSize: 14,
|
|
121
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
122
|
+
},
|
|
123
|
+
children: "Loading PDF\u2026"
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
errorMessage: (error) => {
|
|
127
|
+
onLoadError?.(error);
|
|
128
|
+
return errorComponent ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
style: {
|
|
132
|
+
display: "flex",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
justifyContent: "center",
|
|
135
|
+
width: "100%",
|
|
136
|
+
height: "100%",
|
|
137
|
+
color: "#ef4444",
|
|
138
|
+
fontSize: 14,
|
|
139
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
140
|
+
},
|
|
141
|
+
children: "Failed to load PDF"
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
children: (pdfDocument) => {
|
|
146
|
+
if (!loadNotifiedRef.current) {
|
|
147
|
+
loadNotifiedRef.current = true;
|
|
148
|
+
onLoadSuccess?.();
|
|
149
|
+
}
|
|
150
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
151
|
+
import_react_pdf_highlighter_extended.PdfHighlighter,
|
|
152
|
+
{
|
|
153
|
+
pdfDocument,
|
|
154
|
+
highlights,
|
|
155
|
+
utilsRef: handleUtilsRef,
|
|
156
|
+
style: {
|
|
157
|
+
width: "100%",
|
|
158
|
+
height: "100%"
|
|
159
|
+
},
|
|
160
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CitationHighlightRenderer, {})
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
) });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/components/renderers/DocxRenderer.tsx
|
|
169
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
170
|
+
function DocxRenderer({ target }) {
|
|
171
|
+
const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
|
|
172
|
+
target.url
|
|
173
|
+
)}`;
|
|
174
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
175
|
+
"iframe",
|
|
176
|
+
{
|
|
177
|
+
src: officeUrl,
|
|
178
|
+
title: target.title ?? "Document preview",
|
|
179
|
+
style: {
|
|
180
|
+
width: "100%",
|
|
181
|
+
height: "100%",
|
|
182
|
+
border: "none",
|
|
183
|
+
borderRadius: 6,
|
|
184
|
+
background: "#fff"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/components/DocumentViewer.tsx
|
|
191
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
192
|
+
function DocumentViewer({
|
|
193
|
+
target,
|
|
194
|
+
pdfWorkerUrl,
|
|
195
|
+
loadingComponent,
|
|
196
|
+
errorComponent,
|
|
197
|
+
onLoadStart,
|
|
198
|
+
onLoadSuccess,
|
|
199
|
+
onLoadError,
|
|
200
|
+
className
|
|
201
|
+
}) {
|
|
202
|
+
if (!target) return null;
|
|
203
|
+
const type = target.fileType ?? getFileType(target.url);
|
|
204
|
+
const wrapperStyle = {
|
|
205
|
+
width: "100%",
|
|
206
|
+
height: "100%",
|
|
207
|
+
position: "relative",
|
|
208
|
+
overflow: "hidden",
|
|
209
|
+
borderRadius: 8,
|
|
210
|
+
background: "#fafafa"
|
|
211
|
+
};
|
|
212
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className, style: wrapperStyle, children: [
|
|
213
|
+
type === "pdf" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
214
|
+
PdfRenderer,
|
|
215
|
+
{
|
|
216
|
+
target,
|
|
217
|
+
workerUrl: pdfWorkerUrl,
|
|
218
|
+
loadingComponent,
|
|
219
|
+
errorComponent,
|
|
220
|
+
onLoadSuccess,
|
|
221
|
+
onLoadError
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
type === "docx" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DocxRenderer, { target }),
|
|
225
|
+
type === "unknown" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
226
|
+
"div",
|
|
227
|
+
{
|
|
228
|
+
style: {
|
|
229
|
+
display: "flex",
|
|
230
|
+
alignItems: "center",
|
|
231
|
+
justifyContent: "center",
|
|
232
|
+
width: "100%",
|
|
233
|
+
height: "100%",
|
|
234
|
+
color: "#888",
|
|
235
|
+
fontSize: 14,
|
|
236
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
237
|
+
},
|
|
238
|
+
children: "Unsupported file type"
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
] });
|
|
242
|
+
}
|
|
243
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
244
|
+
0 && (module.exports = {
|
|
245
|
+
DocumentViewer,
|
|
246
|
+
getFileType
|
|
247
|
+
});
|
|
248
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils/getFileType.ts","../src/components/renderers/PdfRenderer.tsx","../src/components/renderers/DocxRenderer.tsx","../src/components/DocumentViewer.tsx"],"sourcesContent":["/* ------------------------------------------------------------------ */\n/* Components */\n/* ------------------------------------------------------------------ */\nexport { DocumentViewer } from \"./components/DocumentViewer\";\n\n/* ------------------------------------------------------------------ */\n/* Types */\n/* ------------------------------------------------------------------ */\nexport type { ViewerTarget, BBox } from \"./types/ViewerTarget\";\nexport type { DocumentViewerProps } from \"./types/DocumentViewerProps\";\n\n/* ------------------------------------------------------------------ */\n/* Utilities */\n/* ------------------------------------------------------------------ */\nexport { getFileType } from \"./utils/getFileType\";\n","/**\n * Infer document type from URL extension.\n * Strips query-strings and fragments before matching.\n */\nexport function getFileType(url: string): \"pdf\" | \"docx\" | \"unknown\" {\n const clean = url.split(\"?\")[0].split(\"#\")[0].toLowerCase();\n\n if (clean.endsWith(\".pdf\")) return \"pdf\";\n if (clean.endsWith(\".docx\") || clean.endsWith(\".doc\")) return \"docx\";\n\n return \"unknown\";\n}\n","import React, { useRef, useEffect, useMemo, useCallback } from \"react\";\nimport {\n PdfLoader,\n PdfHighlighter,\n AreaHighlight,\n useHighlightContainerContext,\n} from \"react-pdf-highlighter-extended\";\nimport type {\n Highlight,\n ScaledPosition,\n PdfHighlighterUtils,\n} from \"react-pdf-highlighter-extended\";\nimport type { ViewerTarget } from \"../../types/ViewerTarget\";\n\n/* ------------------------------------------------------------------ */\n/* Constants */\n/* ------------------------------------------------------------------ */\n\nconst CITATION_ID = \"citation-highlight\";\n\nconst DEFAULT_WORKER_URL =\n \"https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs\";\n\n/* ------------------------------------------------------------------ */\n/* BBox → Highlight conversion */\n/* ------------------------------------------------------------------ */\n\n/**\n * Convert our normalised (0-1) BBox into the Scaled coordinate system\n * used by react-pdf-highlighter-extended.\n *\n * The library internally computes:\n * viewportX = (viewportWidth × scaled.x1) / scaled.width\n *\n * So `scaled.x1 / scaled.width` must equal the normalised value.\n * We simply store the 0-1 value directly and set width / height to 1.\n */\nfunction bboxToHighlight(target: ViewerTarget): Highlight | null {\n if (!target.bbox) return null;\n\n const page = target.page ?? 1;\n const { x1, y1, x2, y2 } = target.bbox;\n\n const scaledRect = {\n x1,\n y1,\n x2,\n y2,\n width: 1,\n height: 1,\n pageNumber: page,\n };\n\n const position: ScaledPosition = {\n boundingRect: scaledRect,\n rects: [scaledRect],\n };\n\n return {\n id: CITATION_ID,\n type: \"area\",\n position,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Highlight child renderer */\n/* ------------------------------------------------------------------ */\n\n/**\n * Rendered once per highlight by PdfHighlighter's internal loop.\n * Uses context to access the highlight's viewport position.\n */\nfunction CitationHighlightRenderer() {\n const { highlight, isScrolledTo } = useHighlightContainerContext();\n\n return (\n <AreaHighlight\n highlight={highlight}\n isScrolledTo={isScrolledTo}\n style={{\n background: isScrolledTo\n ? \"rgba(255, 171, 0, 0.35)\"\n : \"rgba(255, 171, 0, 0.2)\",\n border: \"1.5px solid rgba(255, 171, 0, 0.6)\",\n borderRadius: 2,\n transition: \"background 200ms ease\",\n }}\n />\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* PdfRenderer */\n/* ------------------------------------------------------------------ */\n\ninterface PdfRendererProps {\n target: ViewerTarget;\n workerUrl?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n onLoadSuccess?: () => void;\n onLoadError?: (err: Error) => void;\n}\n\nexport function PdfRenderer({\n target,\n workerUrl,\n loadingComponent,\n errorComponent,\n onLoadSuccess,\n onLoadError,\n}: PdfRendererProps) {\n const highlighterRef = useRef<PdfHighlighterUtils | null>(null);\n const loadNotifiedRef = useRef(false);\n\n /* Build highlight array from target bbox */\n const highlights = useMemo(() => {\n const h = bboxToHighlight(target);\n return h ? [h] : [];\n }, [target]);\n\n /* Scroll to the citation highlight once the viewer is ready */\n useEffect(() => {\n if (highlights.length === 0) return;\n\n const timer = setTimeout(() => {\n highlighterRef.current?.scrollToHighlight(highlights[0]);\n }, 400);\n\n return () => clearTimeout(timer);\n }, [highlights]);\n\n /* Stable callback so we don't re-trigger PdfHighlighter renders */\n const handleUtilsRef = useCallback((utils: PdfHighlighterUtils) => {\n highlighterRef.current = utils;\n }, []);\n\n return (\n <div style={{ width: \"100%\", height: \"100%\", position: \"relative\" }}>\n <PdfLoader\n document={target.url}\n workerSrc={workerUrl ?? DEFAULT_WORKER_URL}\n beforeLoad={() =>\n loadingComponent ?? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#888\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Loading PDF…\n </div>\n )\n }\n errorMessage={(error) => {\n onLoadError?.(error);\n return (\n errorComponent ?? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#ef4444\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Failed to load PDF\n </div>\n )\n );\n }}\n >\n {(pdfDocument) => {\n /* Notify once */\n if (!loadNotifiedRef.current) {\n loadNotifiedRef.current = true;\n onLoadSuccess?.();\n }\n\n return (\n <PdfHighlighter\n pdfDocument={pdfDocument}\n highlights={highlights}\n utilsRef={handleUtilsRef}\n style={{\n width: \"100%\",\n height: \"100%\",\n }}\n >\n <CitationHighlightRenderer />\n </PdfHighlighter>\n );\n }}\n </PdfLoader>\n </div>\n );\n}\n","import React from \"react\";\nimport type { ViewerTarget } from \"../../types/ViewerTarget\";\n\n/**\n * Renders DOCX / DOC files via the free Microsoft Office Online embed viewer.\n * The document URL must be publicly accessible.\n */\nexport function DocxRenderer({ target }: { target: ViewerTarget }) {\n const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(\n target.url\n )}`;\n\n return (\n <iframe\n src={officeUrl}\n title={target.title ?? \"Document preview\"}\n style={{\n width: \"100%\",\n height: \"100%\",\n border: \"none\",\n borderRadius: 6,\n background: \"#fff\",\n }}\n />\n );\n}\n","import React from \"react\";\nimport type { DocumentViewerProps } from \"../types/DocumentViewerProps\";\nimport { getFileType } from \"../utils/getFileType\";\nimport { PdfRenderer } from \"./renderers/PdfRenderer\";\nimport { DocxRenderer } from \"./renderers/DocxRenderer\";\n\n/**\n * Top-level component that routes to the correct renderer\n * based on the target's file type.\n *\n * ```tsx\n * <DocumentViewer\n * target={{\n * url: \"https://example.com/report.pdf\",\n * page: 3,\n * bbox: { x1: 0.1, y1: 0.2, x2: 0.5, y2: 0.3 },\n * }}\n * />\n * ```\n */\nexport function DocumentViewer({\n target,\n pdfWorkerUrl,\n loadingComponent,\n errorComponent,\n onLoadStart,\n onLoadSuccess,\n onLoadError,\n className,\n}: DocumentViewerProps) {\n if (!target) return null;\n\n const type = target.fileType ?? getFileType(target.url);\n\n const wrapperStyle: React.CSSProperties = {\n width: \"100%\",\n height: \"100%\",\n position: \"relative\",\n overflow: \"hidden\",\n borderRadius: 8,\n background: \"#fafafa\",\n };\n\n return (\n <div className={className} style={wrapperStyle}>\n {type === \"pdf\" && (\n <PdfRenderer\n target={target}\n workerUrl={pdfWorkerUrl}\n loadingComponent={loadingComponent}\n errorComponent={errorComponent}\n onLoadSuccess={onLoadSuccess}\n onLoadError={onLoadError}\n />\n )}\n\n {type === \"docx\" && <DocxRenderer target={target} />}\n\n {type === \"unknown\" && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#888\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Unsupported file type\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["import_jsx_runtime","import_jsx_runtime"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/utils/getFileType.ts
|
|
2
|
+
function getFileType(url) {
|
|
3
|
+
const clean = url.split("?")[0].split("#")[0].toLowerCase();
|
|
4
|
+
if (clean.endsWith(".pdf")) return "pdf";
|
|
5
|
+
if (clean.endsWith(".docx") || clean.endsWith(".doc")) return "docx";
|
|
6
|
+
return "unknown";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/components/renderers/PdfRenderer.tsx
|
|
10
|
+
import { useRef, useEffect, useMemo, useCallback } from "react";
|
|
11
|
+
import {
|
|
12
|
+
PdfLoader,
|
|
13
|
+
PdfHighlighter,
|
|
14
|
+
AreaHighlight,
|
|
15
|
+
useHighlightContainerContext
|
|
16
|
+
} from "react-pdf-highlighter-extended";
|
|
17
|
+
import { jsx } from "react/jsx-runtime";
|
|
18
|
+
var CITATION_ID = "citation-highlight";
|
|
19
|
+
var DEFAULT_WORKER_URL = "https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs";
|
|
20
|
+
function bboxToHighlight(target) {
|
|
21
|
+
if (!target.bbox) return null;
|
|
22
|
+
const page = target.page ?? 1;
|
|
23
|
+
const { x1, y1, x2, y2 } = target.bbox;
|
|
24
|
+
const scaledRect = {
|
|
25
|
+
x1,
|
|
26
|
+
y1,
|
|
27
|
+
x2,
|
|
28
|
+
y2,
|
|
29
|
+
width: 1,
|
|
30
|
+
height: 1,
|
|
31
|
+
pageNumber: page
|
|
32
|
+
};
|
|
33
|
+
const position = {
|
|
34
|
+
boundingRect: scaledRect,
|
|
35
|
+
rects: [scaledRect]
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
id: CITATION_ID,
|
|
39
|
+
type: "area",
|
|
40
|
+
position
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function CitationHighlightRenderer() {
|
|
44
|
+
const { highlight, isScrolledTo } = useHighlightContainerContext();
|
|
45
|
+
return /* @__PURE__ */ jsx(
|
|
46
|
+
AreaHighlight,
|
|
47
|
+
{
|
|
48
|
+
highlight,
|
|
49
|
+
isScrolledTo,
|
|
50
|
+
style: {
|
|
51
|
+
background: isScrolledTo ? "rgba(255, 171, 0, 0.35)" : "rgba(255, 171, 0, 0.2)",
|
|
52
|
+
border: "1.5px solid rgba(255, 171, 0, 0.6)",
|
|
53
|
+
borderRadius: 2,
|
|
54
|
+
transition: "background 200ms ease"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function PdfRenderer({
|
|
60
|
+
target,
|
|
61
|
+
workerUrl,
|
|
62
|
+
loadingComponent,
|
|
63
|
+
errorComponent,
|
|
64
|
+
onLoadSuccess,
|
|
65
|
+
onLoadError
|
|
66
|
+
}) {
|
|
67
|
+
const highlighterRef = useRef(null);
|
|
68
|
+
const loadNotifiedRef = useRef(false);
|
|
69
|
+
const highlights = useMemo(() => {
|
|
70
|
+
const h = bboxToHighlight(target);
|
|
71
|
+
return h ? [h] : [];
|
|
72
|
+
}, [target]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (highlights.length === 0) return;
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
highlighterRef.current?.scrollToHighlight(highlights[0]);
|
|
77
|
+
}, 400);
|
|
78
|
+
return () => clearTimeout(timer);
|
|
79
|
+
}, [highlights]);
|
|
80
|
+
const handleUtilsRef = useCallback((utils) => {
|
|
81
|
+
highlighterRef.current = utils;
|
|
82
|
+
}, []);
|
|
83
|
+
return /* @__PURE__ */ jsx("div", { style: { width: "100%", height: "100%", position: "relative" }, children: /* @__PURE__ */ jsx(
|
|
84
|
+
PdfLoader,
|
|
85
|
+
{
|
|
86
|
+
document: target.url,
|
|
87
|
+
workerSrc: workerUrl ?? DEFAULT_WORKER_URL,
|
|
88
|
+
beforeLoad: () => loadingComponent ?? /* @__PURE__ */ jsx(
|
|
89
|
+
"div",
|
|
90
|
+
{
|
|
91
|
+
style: {
|
|
92
|
+
display: "flex",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
justifyContent: "center",
|
|
95
|
+
width: "100%",
|
|
96
|
+
height: "100%",
|
|
97
|
+
color: "#888",
|
|
98
|
+
fontSize: 14,
|
|
99
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
100
|
+
},
|
|
101
|
+
children: "Loading PDF\u2026"
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
errorMessage: (error) => {
|
|
105
|
+
onLoadError?.(error);
|
|
106
|
+
return errorComponent ?? /* @__PURE__ */ jsx(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
style: {
|
|
110
|
+
display: "flex",
|
|
111
|
+
alignItems: "center",
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
width: "100%",
|
|
114
|
+
height: "100%",
|
|
115
|
+
color: "#ef4444",
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
118
|
+
},
|
|
119
|
+
children: "Failed to load PDF"
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
children: (pdfDocument) => {
|
|
124
|
+
if (!loadNotifiedRef.current) {
|
|
125
|
+
loadNotifiedRef.current = true;
|
|
126
|
+
onLoadSuccess?.();
|
|
127
|
+
}
|
|
128
|
+
return /* @__PURE__ */ jsx(
|
|
129
|
+
PdfHighlighter,
|
|
130
|
+
{
|
|
131
|
+
pdfDocument,
|
|
132
|
+
highlights,
|
|
133
|
+
utilsRef: handleUtilsRef,
|
|
134
|
+
style: {
|
|
135
|
+
width: "100%",
|
|
136
|
+
height: "100%"
|
|
137
|
+
},
|
|
138
|
+
children: /* @__PURE__ */ jsx(CitationHighlightRenderer, {})
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
) });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/components/renderers/DocxRenderer.tsx
|
|
147
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
148
|
+
function DocxRenderer({ target }) {
|
|
149
|
+
const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
|
|
150
|
+
target.url
|
|
151
|
+
)}`;
|
|
152
|
+
return /* @__PURE__ */ jsx2(
|
|
153
|
+
"iframe",
|
|
154
|
+
{
|
|
155
|
+
src: officeUrl,
|
|
156
|
+
title: target.title ?? "Document preview",
|
|
157
|
+
style: {
|
|
158
|
+
width: "100%",
|
|
159
|
+
height: "100%",
|
|
160
|
+
border: "none",
|
|
161
|
+
borderRadius: 6,
|
|
162
|
+
background: "#fff"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/components/DocumentViewer.tsx
|
|
169
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
170
|
+
function DocumentViewer({
|
|
171
|
+
target,
|
|
172
|
+
pdfWorkerUrl,
|
|
173
|
+
loadingComponent,
|
|
174
|
+
errorComponent,
|
|
175
|
+
onLoadStart,
|
|
176
|
+
onLoadSuccess,
|
|
177
|
+
onLoadError,
|
|
178
|
+
className
|
|
179
|
+
}) {
|
|
180
|
+
if (!target) return null;
|
|
181
|
+
const type = target.fileType ?? getFileType(target.url);
|
|
182
|
+
const wrapperStyle = {
|
|
183
|
+
width: "100%",
|
|
184
|
+
height: "100%",
|
|
185
|
+
position: "relative",
|
|
186
|
+
overflow: "hidden",
|
|
187
|
+
borderRadius: 8,
|
|
188
|
+
background: "#fafafa"
|
|
189
|
+
};
|
|
190
|
+
return /* @__PURE__ */ jsxs("div", { className, style: wrapperStyle, children: [
|
|
191
|
+
type === "pdf" && /* @__PURE__ */ jsx3(
|
|
192
|
+
PdfRenderer,
|
|
193
|
+
{
|
|
194
|
+
target,
|
|
195
|
+
workerUrl: pdfWorkerUrl,
|
|
196
|
+
loadingComponent,
|
|
197
|
+
errorComponent,
|
|
198
|
+
onLoadSuccess,
|
|
199
|
+
onLoadError
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
type === "docx" && /* @__PURE__ */ jsx3(DocxRenderer, { target }),
|
|
203
|
+
type === "unknown" && /* @__PURE__ */ jsx3(
|
|
204
|
+
"div",
|
|
205
|
+
{
|
|
206
|
+
style: {
|
|
207
|
+
display: "flex",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
width: "100%",
|
|
211
|
+
height: "100%",
|
|
212
|
+
color: "#888",
|
|
213
|
+
fontSize: 14,
|
|
214
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
215
|
+
},
|
|
216
|
+
children: "Unsupported file type"
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
] });
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
DocumentViewer,
|
|
223
|
+
getFileType
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/getFileType.ts","../src/components/renderers/PdfRenderer.tsx","../src/components/renderers/DocxRenderer.tsx","../src/components/DocumentViewer.tsx"],"sourcesContent":["/**\n * Infer document type from URL extension.\n * Strips query-strings and fragments before matching.\n */\nexport function getFileType(url: string): \"pdf\" | \"docx\" | \"unknown\" {\n const clean = url.split(\"?\")[0].split(\"#\")[0].toLowerCase();\n\n if (clean.endsWith(\".pdf\")) return \"pdf\";\n if (clean.endsWith(\".docx\") || clean.endsWith(\".doc\")) return \"docx\";\n\n return \"unknown\";\n}\n","import React, { useRef, useEffect, useMemo, useCallback } from \"react\";\nimport {\n PdfLoader,\n PdfHighlighter,\n AreaHighlight,\n useHighlightContainerContext,\n} from \"react-pdf-highlighter-extended\";\nimport type {\n Highlight,\n ScaledPosition,\n PdfHighlighterUtils,\n} from \"react-pdf-highlighter-extended\";\nimport type { ViewerTarget } from \"../../types/ViewerTarget\";\n\n/* ------------------------------------------------------------------ */\n/* Constants */\n/* ------------------------------------------------------------------ */\n\nconst CITATION_ID = \"citation-highlight\";\n\nconst DEFAULT_WORKER_URL =\n \"https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs\";\n\n/* ------------------------------------------------------------------ */\n/* BBox → Highlight conversion */\n/* ------------------------------------------------------------------ */\n\n/**\n * Convert our normalised (0-1) BBox into the Scaled coordinate system\n * used by react-pdf-highlighter-extended.\n *\n * The library internally computes:\n * viewportX = (viewportWidth × scaled.x1) / scaled.width\n *\n * So `scaled.x1 / scaled.width` must equal the normalised value.\n * We simply store the 0-1 value directly and set width / height to 1.\n */\nfunction bboxToHighlight(target: ViewerTarget): Highlight | null {\n if (!target.bbox) return null;\n\n const page = target.page ?? 1;\n const { x1, y1, x2, y2 } = target.bbox;\n\n const scaledRect = {\n x1,\n y1,\n x2,\n y2,\n width: 1,\n height: 1,\n pageNumber: page,\n };\n\n const position: ScaledPosition = {\n boundingRect: scaledRect,\n rects: [scaledRect],\n };\n\n return {\n id: CITATION_ID,\n type: \"area\",\n position,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Highlight child renderer */\n/* ------------------------------------------------------------------ */\n\n/**\n * Rendered once per highlight by PdfHighlighter's internal loop.\n * Uses context to access the highlight's viewport position.\n */\nfunction CitationHighlightRenderer() {\n const { highlight, isScrolledTo } = useHighlightContainerContext();\n\n return (\n <AreaHighlight\n highlight={highlight}\n isScrolledTo={isScrolledTo}\n style={{\n background: isScrolledTo\n ? \"rgba(255, 171, 0, 0.35)\"\n : \"rgba(255, 171, 0, 0.2)\",\n border: \"1.5px solid rgba(255, 171, 0, 0.6)\",\n borderRadius: 2,\n transition: \"background 200ms ease\",\n }}\n />\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* PdfRenderer */\n/* ------------------------------------------------------------------ */\n\ninterface PdfRendererProps {\n target: ViewerTarget;\n workerUrl?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n onLoadSuccess?: () => void;\n onLoadError?: (err: Error) => void;\n}\n\nexport function PdfRenderer({\n target,\n workerUrl,\n loadingComponent,\n errorComponent,\n onLoadSuccess,\n onLoadError,\n}: PdfRendererProps) {\n const highlighterRef = useRef<PdfHighlighterUtils | null>(null);\n const loadNotifiedRef = useRef(false);\n\n /* Build highlight array from target bbox */\n const highlights = useMemo(() => {\n const h = bboxToHighlight(target);\n return h ? [h] : [];\n }, [target]);\n\n /* Scroll to the citation highlight once the viewer is ready */\n useEffect(() => {\n if (highlights.length === 0) return;\n\n const timer = setTimeout(() => {\n highlighterRef.current?.scrollToHighlight(highlights[0]);\n }, 400);\n\n return () => clearTimeout(timer);\n }, [highlights]);\n\n /* Stable callback so we don't re-trigger PdfHighlighter renders */\n const handleUtilsRef = useCallback((utils: PdfHighlighterUtils) => {\n highlighterRef.current = utils;\n }, []);\n\n return (\n <div style={{ width: \"100%\", height: \"100%\", position: \"relative\" }}>\n <PdfLoader\n document={target.url}\n workerSrc={workerUrl ?? DEFAULT_WORKER_URL}\n beforeLoad={() =>\n loadingComponent ?? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#888\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Loading PDF…\n </div>\n )\n }\n errorMessage={(error) => {\n onLoadError?.(error);\n return (\n errorComponent ?? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#ef4444\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Failed to load PDF\n </div>\n )\n );\n }}\n >\n {(pdfDocument) => {\n /* Notify once */\n if (!loadNotifiedRef.current) {\n loadNotifiedRef.current = true;\n onLoadSuccess?.();\n }\n\n return (\n <PdfHighlighter\n pdfDocument={pdfDocument}\n highlights={highlights}\n utilsRef={handleUtilsRef}\n style={{\n width: \"100%\",\n height: \"100%\",\n }}\n >\n <CitationHighlightRenderer />\n </PdfHighlighter>\n );\n }}\n </PdfLoader>\n </div>\n );\n}\n","import React from \"react\";\nimport type { ViewerTarget } from \"../../types/ViewerTarget\";\n\n/**\n * Renders DOCX / DOC files via the free Microsoft Office Online embed viewer.\n * The document URL must be publicly accessible.\n */\nexport function DocxRenderer({ target }: { target: ViewerTarget }) {\n const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(\n target.url\n )}`;\n\n return (\n <iframe\n src={officeUrl}\n title={target.title ?? \"Document preview\"}\n style={{\n width: \"100%\",\n height: \"100%\",\n border: \"none\",\n borderRadius: 6,\n background: \"#fff\",\n }}\n />\n );\n}\n","import React from \"react\";\nimport type { DocumentViewerProps } from \"../types/DocumentViewerProps\";\nimport { getFileType } from \"../utils/getFileType\";\nimport { PdfRenderer } from \"./renderers/PdfRenderer\";\nimport { DocxRenderer } from \"./renderers/DocxRenderer\";\n\n/**\n * Top-level component that routes to the correct renderer\n * based on the target's file type.\n *\n * ```tsx\n * <DocumentViewer\n * target={{\n * url: \"https://example.com/report.pdf\",\n * page: 3,\n * bbox: { x1: 0.1, y1: 0.2, x2: 0.5, y2: 0.3 },\n * }}\n * />\n * ```\n */\nexport function DocumentViewer({\n target,\n pdfWorkerUrl,\n loadingComponent,\n errorComponent,\n onLoadStart,\n onLoadSuccess,\n onLoadError,\n className,\n}: DocumentViewerProps) {\n if (!target) return null;\n\n const type = target.fileType ?? getFileType(target.url);\n\n const wrapperStyle: React.CSSProperties = {\n width: \"100%\",\n height: \"100%\",\n position: \"relative\",\n overflow: \"hidden\",\n borderRadius: 8,\n background: \"#fafafa\",\n };\n\n return (\n <div className={className} style={wrapperStyle}>\n {type === \"pdf\" && (\n <PdfRenderer\n target={target}\n workerUrl={pdfWorkerUrl}\n loadingComponent={loadingComponent}\n errorComponent={errorComponent}\n onLoadSuccess={onLoadSuccess}\n onLoadError={onLoadError}\n />\n )}\n\n {type === \"docx\" && <DocxRenderer target={target} />}\n\n {type === \"unknown\" && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: \"100%\",\n height: \"100%\",\n color: \"#888\",\n fontSize: 14,\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}\n >\n Unsupported file type\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","jsx"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relevaince/document-viewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Citation-driven document viewer for Relevaince",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18",
|
|
24
|
+
"react-dom": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"pdfjs-dist": "^4.4.168",
|
|
28
|
+
"react-pdf-highlighter-extended": "^8.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.3.28",
|
|
32
|
+
"@types/react-dom": "^18.3.7",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"document-viewer",
|
|
38
|
+
"pdf",
|
|
39
|
+
"docx",
|
|
40
|
+
"citation",
|
|
41
|
+
"react"
|
|
42
|
+
],
|
|
43
|
+
"author": "Relevaince",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/relevaince/document-viewer"
|
|
51
|
+
}
|
|
52
|
+
}
|