@idealyst/pdf 1.2.129

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/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@idealyst/pdf",
3
+ "version": "1.2.129",
4
+ "description": "Cross-platform PDF viewer for React and React Native",
5
+ "readme": "README.md",
6
+ "main": "src/index.ts",
7
+ "module": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "react-native": "src/index.native.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/IdealystIO/idealyst-framework.git",
13
+ "directory": "packages/pdf"
14
+ },
15
+ "author": "Idealyst <contact@idealyst.io>",
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "react-native": "./src/index.native.ts",
23
+ "import": "./src/index.ts",
24
+ "require": "./src/index.ts",
25
+ "types": "./src/index.ts"
26
+ }
27
+ },
28
+ "scripts": {
29
+ "prepublishOnly": "echo 'Publishing TypeScript source directly'",
30
+ "publish:npm": "npm publish"
31
+ },
32
+ "peerDependencies": {
33
+ "pdfjs-dist": ">=3.0.0",
34
+ "react": ">=16.8.0",
35
+ "react-native": ">=0.60.0",
36
+ "react-native-blob-util": ">=0.19.0",
37
+ "react-native-pdf": ">=6.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "pdfjs-dist": {
41
+ "optional": true
42
+ },
43
+ "react-native": {
44
+ "optional": true
45
+ },
46
+ "react-native-blob-util": {
47
+ "optional": true
48
+ },
49
+ "react-native-pdf": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@types/react": "^19.1.0",
55
+ "pdfjs-dist": "^4.0.0",
56
+ "react": "^19.1.0",
57
+ "react-native": "^0.80.1",
58
+ "react-native-blob-util": "^0.19.0",
59
+ "react-native-pdf": "^6.7.0",
60
+ "typescript": "^5.0.0"
61
+ },
62
+ "files": [
63
+ "src",
64
+ "README.md"
65
+ ],
66
+ "keywords": [
67
+ "react",
68
+ "react-native",
69
+ "pdf",
70
+ "viewer",
71
+ "document",
72
+ "cross-platform",
73
+ "pdfjs"
74
+ ]
75
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * PDFViewer - Native implementation
3
+ *
4
+ * Uses react-native-pdf for rendering PDF documents on React Native.
5
+ */
6
+
7
+ import React, {
8
+ forwardRef,
9
+ useImperativeHandle,
10
+ useRef,
11
+ useState,
12
+ useCallback,
13
+ } from 'react';
14
+ import { View, Text, StyleSheet } from 'react-native';
15
+ import Pdf from 'react-native-pdf';
16
+ import type { PDFViewerProps, PDFViewerRef, PDFSource } from './types';
17
+
18
+ /**
19
+ * Normalize a PDFSource into a react-native-pdf compatible source object.
20
+ */
21
+ function resolveSource(source: PDFSource): { uri: string } {
22
+ if (typeof source === 'string') {
23
+ return { uri: source };
24
+ }
25
+ if ('uri' in source) {
26
+ return { uri: source.uri };
27
+ }
28
+ // base64 → data URI for react-native-pdf
29
+ return { uri: `data:application/pdf;base64,${source.base64}` };
30
+ }
31
+
32
+ /**
33
+ * Convert fitPolicy string to react-native-pdf numeric value.
34
+ * 0 = fit width, 1 = fit height, 2 = fit both
35
+ */
36
+ function resolveFitPolicy(fitPolicy: 'width' | 'height' | 'both'): 0 | 1 | 2 {
37
+ switch (fitPolicy) {
38
+ case 'width':
39
+ return 0;
40
+ case 'height':
41
+ return 1;
42
+ case 'both':
43
+ return 2;
44
+ default:
45
+ return 0;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * PDFViewer component for React Native.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import { PDFViewer } from '@idealyst/pdf';
55
+ *
56
+ * <PDFViewer
57
+ * source="https://example.com/document.pdf"
58
+ * onLoad={({ totalPages }) => console.log('Pages:', totalPages)}
59
+ * onPageChange={(page, total) => console.log(page, '/', total)}
60
+ * style={{ flex: 1 }}
61
+ * />
62
+ * ```
63
+ */
64
+ export const PDFViewer = forwardRef<PDFViewerRef, PDFViewerProps>((props, ref) => {
65
+ const {
66
+ source,
67
+ page = 1,
68
+ onPageChange,
69
+ onLoad,
70
+ onError,
71
+ zoomEnabled = true,
72
+ minZoom = 1,
73
+ maxZoom = 5,
74
+ direction = 'vertical',
75
+ showPageIndicator = true,
76
+ fitPolicy = 'width',
77
+ style,
78
+ testID,
79
+ } = props;
80
+
81
+ const pdfRef = useRef<any>(null);
82
+ const [currentPage, setCurrentPage] = useState(page);
83
+ const [totalPages, setTotalPages] = useState(0);
84
+ const [controlledPage, setControlledPage] = useState(page);
85
+
86
+ // Update controlled page when prop changes
87
+ React.useEffect(() => {
88
+ setControlledPage(page);
89
+ }, [page]);
90
+
91
+ const handleLoadComplete = useCallback(
92
+ (numberOfPages: number) => {
93
+ setTotalPages(numberOfPages);
94
+ onLoad?.({ totalPages: numberOfPages });
95
+ },
96
+ [onLoad]
97
+ );
98
+
99
+ const handlePageChanged = useCallback(
100
+ (pageNum: number, numPages: number) => {
101
+ setCurrentPage(pageNum);
102
+ setTotalPages(numPages);
103
+ onPageChange?.(pageNum, numPages);
104
+ },
105
+ [onPageChange]
106
+ );
107
+
108
+ const handleError = useCallback(
109
+ (error: any) => {
110
+ const err = error instanceof Error ? error : new Error(String(error));
111
+ onError?.(err);
112
+ },
113
+ [onError]
114
+ );
115
+
116
+ // Imperative handle
117
+ useImperativeHandle(
118
+ ref,
119
+ () => ({
120
+ goToPage: (targetPage: number) => {
121
+ setControlledPage(targetPage);
122
+ },
123
+ setZoom: (_level: number) => {
124
+ // react-native-pdf does not support imperative zoom control;
125
+ // zoom is handled by user gestures within the configured min/max range
126
+ },
127
+ }),
128
+ []
129
+ );
130
+
131
+ const resolvedSource = resolveSource(source);
132
+
133
+ return (
134
+ <View style={[styles.container, style]} testID={testID}>
135
+ <Pdf
136
+ ref={pdfRef}
137
+ source={resolvedSource}
138
+ page={controlledPage}
139
+ horizontal={direction === 'horizontal'}
140
+ fitPolicy={resolveFitPolicy(fitPolicy)}
141
+ minScale={minZoom}
142
+ maxScale={maxZoom}
143
+ enablePaging={false}
144
+ enableAntialiasing={true}
145
+ enableAnnotationRendering={true}
146
+ onLoadComplete={handleLoadComplete}
147
+ onPageChanged={handlePageChanged}
148
+ onError={handleError}
149
+ style={styles.pdf}
150
+ />
151
+ {showPageIndicator && totalPages > 0 && (
152
+ <View style={styles.indicator}>
153
+ <Text style={styles.indicatorText}>
154
+ {currentPage} / {totalPages}
155
+ </Text>
156
+ </View>
157
+ )}
158
+ </View>
159
+ );
160
+ });
161
+
162
+ PDFViewer.displayName = 'PDFViewer';
163
+
164
+ const styles = StyleSheet.create({
165
+ container: {
166
+ flex: 1,
167
+ position: 'relative',
168
+ },
169
+ pdf: {
170
+ flex: 1,
171
+ },
172
+ indicator: {
173
+ position: 'absolute',
174
+ bottom: 16,
175
+ right: 16,
176
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
177
+ paddingHorizontal: 12,
178
+ paddingVertical: 4,
179
+ borderRadius: 4,
180
+ },
181
+ indicatorText: {
182
+ color: '#fff',
183
+ fontSize: 14,
184
+ },
185
+ });
@@ -0,0 +1,367 @@
1
+ /**
2
+ * PDFViewer - Web implementation
3
+ *
4
+ * Uses pdfjs-dist (Mozilla PDF.js) for rendering PDF documents on the web.
5
+ */
6
+
7
+ import React, {
8
+ forwardRef,
9
+ useImperativeHandle,
10
+ useRef,
11
+ useEffect,
12
+ useState,
13
+ useCallback,
14
+ } from 'react';
15
+ import * as pdfjs from 'pdfjs-dist';
16
+ import type { PDFDocumentProxy } from 'pdfjs-dist';
17
+ import type { PDFViewerProps, PDFViewerRef, PDFSource } from './types';
18
+
19
+ // Configure the PDF.js worker
20
+ // Users can override this by setting pdfjs.GlobalWorkerOptions.workerSrc before rendering
21
+ if (!pdfjs.GlobalWorkerOptions.workerSrc) {
22
+ pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`;
23
+ }
24
+
25
+ /**
26
+ * Resolve a PDFSource into a pdfjs-dist compatible source parameter.
27
+ */
28
+ function resolveSource(source: PDFSource): string | { data: Uint8Array } {
29
+ if (typeof source === 'string') return source;
30
+ if ('uri' in source) return source.uri;
31
+
32
+ // base64 → Uint8Array
33
+ const binaryStr = atob(source.base64);
34
+ const bytes = new Uint8Array(binaryStr.length);
35
+ for (let i = 0; i < binaryStr.length; i++) {
36
+ bytes[i] = binaryStr.charCodeAt(i);
37
+ }
38
+ return { data: bytes };
39
+ }
40
+
41
+ /**
42
+ * PDFViewer component for web.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * import { PDFViewer } from '@idealyst/pdf';
47
+ *
48
+ * <PDFViewer
49
+ * source="https://example.com/document.pdf"
50
+ * onLoad={({ totalPages }) => console.log('Pages:', totalPages)}
51
+ * onPageChange={(page, total) => console.log(page, '/', total)}
52
+ * style={{ flex: 1 }}
53
+ * />
54
+ * ```
55
+ */
56
+ export const PDFViewer = forwardRef<PDFViewerRef, PDFViewerProps>((props, ref) => {
57
+ const {
58
+ source,
59
+ page = 1,
60
+ onPageChange,
61
+ onLoad,
62
+ onError,
63
+ zoomEnabled = true,
64
+ minZoom = 1,
65
+ maxZoom = 5,
66
+ direction = 'vertical',
67
+ showPageIndicator = true,
68
+ fitPolicy = 'width',
69
+ style,
70
+ testID,
71
+ } = props;
72
+
73
+ const containerRef = useRef<HTMLDivElement>(null);
74
+ const canvasContainerRef = useRef<HTMLDivElement>(null);
75
+ const pdfDocRef = useRef<PDFDocumentProxy | null>(null);
76
+ const pageRefs = useRef<Map<number, HTMLCanvasElement>>(new Map());
77
+ const [totalPages, setTotalPages] = useState(0);
78
+ const [currentPage, setCurrentPage] = useState(page);
79
+ const [zoom, setZoom] = useState(1);
80
+ const [isLoading, setIsLoading] = useState(true);
81
+ const loadIdRef = useRef(0);
82
+
83
+ // Render a single page to a canvas
84
+ const renderPage = useCallback(
85
+ async (doc: PDFDocumentProxy, pageNum: number, canvas: HTMLCanvasElement, scale: number) => {
86
+ const pdfPage = await doc.getPage(pageNum);
87
+ const viewport = pdfPage.getViewport({ scale });
88
+
89
+ canvas.width = viewport.width;
90
+ canvas.height = viewport.height;
91
+
92
+ const context = canvas.getContext('2d');
93
+ if (!context) return;
94
+
95
+ await pdfPage.render({
96
+ canvasContext: context,
97
+ viewport,
98
+ }).promise;
99
+ },
100
+ []
101
+ );
102
+
103
+ // Calculate scale based on fitPolicy and container size
104
+ const calculateScale = useCallback(
105
+ (doc: PDFDocumentProxy, containerWidth: number, containerHeight: number) => {
106
+ // We need the first page to determine dimensions
107
+ return doc.getPage(1).then((firstPage) => {
108
+ const unscaledViewport = firstPage.getViewport({ scale: 1 });
109
+ const pageWidth = unscaledViewport.width;
110
+ const pageHeight = unscaledViewport.height;
111
+
112
+ switch (fitPolicy) {
113
+ case 'width':
114
+ return containerWidth / pageWidth;
115
+ case 'height':
116
+ return containerHeight / pageHeight;
117
+ case 'both': {
118
+ const scaleW = containerWidth / pageWidth;
119
+ const scaleH = containerHeight / pageHeight;
120
+ return Math.min(scaleW, scaleH);
121
+ }
122
+ default:
123
+ return 1;
124
+ }
125
+ });
126
+ },
127
+ [fitPolicy]
128
+ );
129
+
130
+ // Load and render the PDF document
131
+ useEffect(() => {
132
+ const id = ++loadIdRef.current;
133
+ setIsLoading(true);
134
+
135
+ const loadPDF = async () => {
136
+ try {
137
+ // Destroy previous document
138
+ if (pdfDocRef.current) {
139
+ await pdfDocRef.current.destroy();
140
+ pdfDocRef.current = null;
141
+ }
142
+
143
+ const resolved = resolveSource(source);
144
+ const doc = await pdfjs.getDocument(resolved).promise;
145
+
146
+ if (id !== loadIdRef.current) {
147
+ doc.destroy();
148
+ return;
149
+ }
150
+
151
+ pdfDocRef.current = doc;
152
+ const numPages = doc.numPages;
153
+ setTotalPages(numPages);
154
+ setIsLoading(false);
155
+ onLoad?.({ totalPages: numPages });
156
+
157
+ // Get container dimensions for scale calculation
158
+ const container = containerRef.current;
159
+ if (!container) return;
160
+
161
+ const containerWidth = container.clientWidth;
162
+ const containerHeight = container.clientHeight;
163
+ const baseScale = await calculateScale(doc, containerWidth, containerHeight);
164
+
165
+ // Clear previous canvases
166
+ const canvasContainer = canvasContainerRef.current;
167
+ if (!canvasContainer) return;
168
+ canvasContainer.innerHTML = '';
169
+ pageRefs.current.clear();
170
+
171
+ // Render all pages
172
+ for (let i = 1; i <= numPages; i++) {
173
+ const canvas = document.createElement('canvas');
174
+ canvas.style.display = 'block';
175
+ canvas.style.margin = direction === 'vertical' ? '8px auto' : '8px';
176
+ canvas.dataset.page = String(i);
177
+ canvasContainer.appendChild(canvas);
178
+ pageRefs.current.set(i, canvas);
179
+
180
+ await renderPage(doc, i, canvas, baseScale * zoom);
181
+ }
182
+ } catch (err) {
183
+ if (id !== loadIdRef.current) return;
184
+ setIsLoading(false);
185
+ const error = err instanceof Error ? err : new Error(String(err));
186
+ onError?.(error);
187
+ }
188
+ };
189
+
190
+ loadPDF();
191
+
192
+ return () => {
193
+ loadIdRef.current++;
194
+ if (pdfDocRef.current) {
195
+ pdfDocRef.current.destroy();
196
+ pdfDocRef.current = null;
197
+ }
198
+ };
199
+ }, [source]);
200
+
201
+ // Re-render on zoom change
202
+ useEffect(() => {
203
+ if (!pdfDocRef.current || !containerRef.current) return;
204
+
205
+ const doc = pdfDocRef.current;
206
+ const container = containerRef.current;
207
+ const containerWidth = container.clientWidth;
208
+ const containerHeight = container.clientHeight;
209
+
210
+ const rerender = async () => {
211
+ const baseScale = await calculateScale(doc, containerWidth, containerHeight);
212
+ const scale = baseScale * zoom;
213
+
214
+ const entries = Array.from(pageRefs.current.entries());
215
+ for (let i = 0; i < entries.length; i++) {
216
+ const [pageNum, canvas] = entries[i];
217
+ await renderPage(doc, pageNum, canvas, scale);
218
+ }
219
+ };
220
+
221
+ rerender();
222
+ }, [zoom, calculateScale, renderPage]);
223
+
224
+ // Scroll to page when `page` prop changes
225
+ useEffect(() => {
226
+ const canvas = pageRefs.current.get(page);
227
+ if (canvas) {
228
+ canvas.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
229
+ setCurrentPage(page);
230
+ }
231
+ }, [page]);
232
+
233
+ // Track current page via scroll position
234
+ useEffect(() => {
235
+ const container = canvasContainerRef.current;
236
+ if (!container || totalPages === 0) return;
237
+
238
+ const handleScroll = () => {
239
+ const scrollContainer = container.parentElement;
240
+ if (!scrollContainer) return;
241
+
242
+ const scrollTop = scrollContainer.scrollTop;
243
+ const scrollLeft = scrollContainer.scrollLeft;
244
+
245
+ let closestPage = 1;
246
+ let closestDistance = Infinity;
247
+
248
+ const scrollEntries = Array.from(pageRefs.current.entries());
249
+ for (let i = 0; i < scrollEntries.length; i++) {
250
+ const [pageNum, canvas] = scrollEntries[i];
251
+ const distance =
252
+ direction === 'vertical'
253
+ ? Math.abs(canvas.offsetTop - scrollTop)
254
+ : Math.abs(canvas.offsetLeft - scrollLeft);
255
+
256
+ if (distance < closestDistance) {
257
+ closestDistance = distance;
258
+ closestPage = pageNum;
259
+ }
260
+ }
261
+
262
+ if (closestPage !== currentPage) {
263
+ setCurrentPage(closestPage);
264
+ onPageChange?.(closestPage, totalPages);
265
+ }
266
+ };
267
+
268
+ const scrollContainer = container.parentElement;
269
+ scrollContainer?.addEventListener('scroll', handleScroll, { passive: true });
270
+ return () => scrollContainer?.removeEventListener('scroll', handleScroll);
271
+ }, [totalPages, currentPage, direction, onPageChange]);
272
+
273
+ // Wheel zoom handler
274
+ useEffect(() => {
275
+ if (!zoomEnabled) return;
276
+ const container = containerRef.current;
277
+ if (!container) return;
278
+
279
+ const handleWheel = (e: WheelEvent) => {
280
+ if (!e.ctrlKey && !e.metaKey) return;
281
+ e.preventDefault();
282
+
283
+ setZoom((prev) => {
284
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
285
+ return Math.min(maxZoom, Math.max(minZoom, prev + delta));
286
+ });
287
+ };
288
+
289
+ container.addEventListener('wheel', handleWheel, { passive: false });
290
+ return () => container.removeEventListener('wheel', handleWheel);
291
+ }, [zoomEnabled, minZoom, maxZoom]);
292
+
293
+ // Imperative handle
294
+ useImperativeHandle(
295
+ ref,
296
+ () => ({
297
+ goToPage: (targetPage: number) => {
298
+ const canvas = pageRefs.current.get(targetPage);
299
+ if (canvas) {
300
+ canvas.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
301
+ setCurrentPage(targetPage);
302
+ }
303
+ },
304
+ setZoom: (level: number) => {
305
+ setZoom(Math.min(maxZoom, Math.max(minZoom, level)));
306
+ },
307
+ }),
308
+ [minZoom, maxZoom]
309
+ );
310
+
311
+ const containerStyle: React.CSSProperties = {
312
+ position: 'relative',
313
+ width: '100%',
314
+ height: '100%',
315
+ overflow: 'auto',
316
+ backgroundColor: '#f0f0f0',
317
+ ...(style as any),
318
+ };
319
+
320
+ const canvasContainerStyle: React.CSSProperties = {
321
+ display: 'flex',
322
+ flexDirection: direction === 'vertical' ? 'column' : 'row',
323
+ alignItems: 'center',
324
+ minHeight: '100%',
325
+ };
326
+
327
+ const indicatorStyle: React.CSSProperties = {
328
+ position: 'absolute',
329
+ bottom: 16,
330
+ right: 16,
331
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
332
+ color: '#fff',
333
+ padding: '4px 12px',
334
+ borderRadius: 4,
335
+ fontSize: 14,
336
+ fontFamily: 'system-ui, sans-serif',
337
+ pointerEvents: 'none',
338
+ zIndex: 10,
339
+ };
340
+
341
+ return (
342
+ <div ref={containerRef} style={containerStyle} data-testid={testID}>
343
+ <div ref={canvasContainerRef} style={canvasContainerStyle} />
344
+ {showPageIndicator && totalPages > 0 && (
345
+ <div style={indicatorStyle}>
346
+ {currentPage} / {totalPages}
347
+ </div>
348
+ )}
349
+ {isLoading && (
350
+ <div
351
+ style={{
352
+ position: 'absolute',
353
+ inset: 0,
354
+ display: 'flex',
355
+ alignItems: 'center',
356
+ justifyContent: 'center',
357
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
358
+ }}
359
+ >
360
+ Loading PDF...
361
+ </div>
362
+ )}
363
+ </div>
364
+ );
365
+ });
366
+
367
+ PDFViewer.displayName = 'PDFViewer';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @idealyst/pdf - Native exports
3
+ *
4
+ * Cross-platform PDF viewer for React and React Native.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { PDFViewer, PDFViewerRef } from '@idealyst/pdf';
9
+ *
10
+ * // Basic usage
11
+ * <PDFViewer source="https://example.com/document.pdf" style={{ flex: 1 }} />
12
+ *
13
+ * // With local file
14
+ * <PDFViewer source={{ uri: '/path/to/local.pdf' }} />
15
+ *
16
+ * // With ref for imperative control
17
+ * const pdfRef = useRef<PDFViewerRef>(null);
18
+ *
19
+ * <PDFViewer ref={pdfRef} source="https://example.com/document.pdf" />
20
+ *
21
+ * pdfRef.current?.goToPage(5);
22
+ * ```
23
+ */
24
+
25
+ // Type exports
26
+ export type {
27
+ PDFSource,
28
+ PDFDocumentInfo,
29
+ PDFViewerProps,
30
+ PDFViewerRef,
31
+ FitPolicy,
32
+ PDFDirection,
33
+ } from './types';
34
+
35
+ // Component export
36
+ export { PDFViewer } from './PDFViewer.native';
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @idealyst/pdf - Web exports
3
+ *
4
+ * Cross-platform PDF viewer for React and React Native.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { PDFViewer, PDFViewerRef } from '@idealyst/pdf';
9
+ *
10
+ * // Basic usage
11
+ * <PDFViewer source="https://example.com/document.pdf" style={{ flex: 1 }} />
12
+ *
13
+ * // With callbacks
14
+ * <PDFViewer
15
+ * source={{ uri: 'https://example.com/document.pdf' }}
16
+ * onLoad={({ totalPages }) => console.log('Pages:', totalPages)}
17
+ * onPageChange={(page, total) => console.log(page, '/', total)}
18
+ * />
19
+ *
20
+ * // With ref for imperative control
21
+ * const pdfRef = useRef<PDFViewerRef>(null);
22
+ *
23
+ * <PDFViewer ref={pdfRef} source="https://example.com/document.pdf" />
24
+ *
25
+ * pdfRef.current?.goToPage(5);
26
+ * pdfRef.current?.setZoom(2);
27
+ * ```
28
+ */
29
+
30
+ // Type exports
31
+ export type {
32
+ PDFSource,
33
+ PDFDocumentInfo,
34
+ PDFViewerProps,
35
+ PDFViewerRef,
36
+ FitPolicy,
37
+ PDFDirection,
38
+ } from './types';
39
+
40
+ // Component export
41
+ export { PDFViewer } from './PDFViewer';
package/src/types.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Type definitions for @idealyst/pdf
3
+ */
4
+
5
+ import type { ViewStyle } from 'react-native';
6
+
7
+ /**
8
+ * PDF source - URL string, object with uri, or base64-encoded data
9
+ */
10
+ export type PDFSource = string | { uri: string } | { base64: string };
11
+
12
+ /**
13
+ * Information about a loaded PDF document
14
+ */
15
+ export interface PDFDocumentInfo {
16
+ /** Total number of pages */
17
+ totalPages: number;
18
+ }
19
+
20
+ /**
21
+ * Fit policy for rendering pages
22
+ * - 'width': fit page width to container
23
+ * - 'height': fit page height to container
24
+ * - 'both': fit entire page within container
25
+ */
26
+ export type FitPolicy = 'width' | 'height' | 'both';
27
+
28
+ /**
29
+ * Scroll/page direction
30
+ */
31
+ export type PDFDirection = 'horizontal' | 'vertical';
32
+
33
+ /**
34
+ * Props for the PDFViewer component
35
+ */
36
+ export interface PDFViewerProps {
37
+ /**
38
+ * PDF source - URL string, { uri } object, or { base64 } data.
39
+ *
40
+ * @example
41
+ * // URL string
42
+ * source="https://example.com/document.pdf"
43
+ *
44
+ * // URI object
45
+ * source={{ uri: 'https://example.com/document.pdf' }}
46
+ *
47
+ * // Base64 encoded
48
+ * source={{ base64: 'JVBERi0xLjQK...' }}
49
+ */
50
+ source: PDFSource;
51
+
52
+ /**
53
+ * Current page number (1-indexed).
54
+ * When provided, the viewer scrolls to this page.
55
+ * @default 1
56
+ */
57
+ page?: number;
58
+
59
+ /**
60
+ * Called when the visible page changes.
61
+ * @param page - Current page number (1-indexed)
62
+ * @param totalPages - Total number of pages in the document
63
+ */
64
+ onPageChange?: (page: number, totalPages: number) => void;
65
+
66
+ /**
67
+ * Called when the PDF document loads successfully.
68
+ */
69
+ onLoad?: (info: PDFDocumentInfo) => void;
70
+
71
+ /**
72
+ * Called when an error occurs loading the PDF.
73
+ */
74
+ onError?: (error: Error) => void;
75
+
76
+ /**
77
+ * Enable pinch-to-zoom / scroll-to-zoom.
78
+ * @default true
79
+ */
80
+ zoomEnabled?: boolean;
81
+
82
+ /**
83
+ * Minimum zoom level.
84
+ * @default 1
85
+ */
86
+ minZoom?: number;
87
+
88
+ /**
89
+ * Maximum zoom level.
90
+ * @default 5
91
+ */
92
+ maxZoom?: number;
93
+
94
+ /**
95
+ * Scroll direction for page navigation.
96
+ * @default 'vertical'
97
+ */
98
+ direction?: PDFDirection;
99
+
100
+ /**
101
+ * Show a page indicator overlay (e.g. "3 / 10").
102
+ * @default true
103
+ */
104
+ showPageIndicator?: boolean;
105
+
106
+ /**
107
+ * How pages should be scaled to fit the container.
108
+ * @default 'width'
109
+ */
110
+ fitPolicy?: FitPolicy;
111
+
112
+ /**
113
+ * Container style.
114
+ */
115
+ style?: ViewStyle;
116
+
117
+ /**
118
+ * Test ID for testing frameworks.
119
+ */
120
+ testID?: string;
121
+ }
122
+
123
+ /**
124
+ * Ref methods for imperative PDFViewer control
125
+ */
126
+ export interface PDFViewerRef {
127
+ /**
128
+ * Navigate to a specific page (1-indexed).
129
+ */
130
+ goToPage: (page: number) => void;
131
+
132
+ /**
133
+ * Set the zoom level programmatically.
134
+ */
135
+ setZoom: (level: number) => void;
136
+ }