A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
at main 191 lines 4.6 kB view raw
1import { forwardRef, useImperativeHandle, useMemo } from 'react' 2import type { 3 DocumentSource, 4 DocumentFormat, 5 DocumentInfo, 6 PolyRenderError, 7 ToolbarConfig, 8 PdfOptions, 9 CodeOptions, 10 CsvOptions, 11 EpubOptions, 12} from '@polyrender/core' 13import { useDocumentRenderer } from './useDocumentRenderer.js' 14 15export interface DocumentViewerProps { 16 /** The document to render. Pass null/undefined to show nothing. */ 17 source: DocumentSource | null | undefined 18 19 /** Explicit format override. If omitted, auto-detected from source. */ 20 format?: DocumentFormat 21 22 /** Color theme. Defaults to 'dark'. */ 23 theme?: 'light' | 'dark' | 'system' 24 25 /** Additional CSS class(es) for the root container. */ 26 className?: string 27 28 /** Inline styles for the outer wrapper div (set width/height here). */ 29 style?: React.CSSProperties 30 31 /** Page to display initially (1-indexed). Defaults to 1. */ 32 initialPage?: number 33 34 /** Initial zoom level. */ 35 zoom?: number | 'fit-width' | 'fit-page' | 'auto' 36 37 /** Toolbar configuration. true = default, false = hidden. */ 38 toolbar?: boolean | ToolbarConfig 39 40 /** Show page numbers. */ 41 showPageNumbers?: boolean 42 43 // --- Callbacks --- 44 onReady?: (info: DocumentInfo) => void 45 onPageChange?: (page: number, totalPages: number) => void 46 onZoomChange?: (zoom: number) => void 47 onError?: (error: PolyRenderError) => void 48 onLoadingChange?: (loading: boolean) => void 49 50 // --- Format-specific options --- 51 pdf?: PdfOptions 52 code?: CodeOptions 53 csv?: CsvOptions 54 epub?: EpubOptions 55} 56 57export interface DocumentViewerRef { 58 /** Navigate to a specific page (1-indexed). */ 59 goToPage: (page: number) => void 60 /** Set zoom level. */ 61 setZoom: (zoom: number | 'fit-width' | 'fit-page') => void 62 /** Get current page number. */ 63 getCurrentPage: () => number 64 /** Get total page count. */ 65 getPageCount: () => number 66 /** Get current zoom level. */ 67 getZoom: () => number 68} 69 70/** 71 * React component for rendering documents of any supported format. 72 * 73 * Wraps the framework-agnostic `@polyrender/core` library with React lifecycle 74 * management, ref-based imperative API, and automatic cleanup. 75 * 76 * @example 77 * ```tsx 78 * import { DocumentViewer } from '@polyrender/react' 79 * import '@polyrender/core/styles.css' 80 * 81 * function App() { 82 * return ( 83 * <DocumentViewer 84 * source={{ type: 'url', url: '/report.pdf' }} 85 * theme="dark" 86 * style={{ width: '100%', height: '80vh' }} 87 * onReady={(info) => console.log(`Loaded ${info.pageCount} pages`)} 88 * /> 89 * ) 90 * } 91 * ``` 92 * 93 * @example Chunked / pre-rendered pages 94 * ```tsx 95 * <DocumentViewer 96 * source={{ 97 * type: 'pages', 98 * pages: { 99 * totalPages: 200, 100 * fetchPage: async (n) => ({ 101 * pageNumber: n, 102 * imageUrl: `/api/pages/${n}.webp`, 103 * width: 1654, 104 * height: 2339, 105 * }), 106 * }, 107 * }} 108 * /> 109 * ``` 110 * 111 * @example Imperative control via ref 112 * ```tsx 113 * const viewerRef = useRef<DocumentViewerRef>(null) 114 * 115 * <DocumentViewer ref={viewerRef} source={source} /> 116 * <button onClick={() => viewerRef.current?.goToPage(10)}>Go to page 10</button> 117 * ``` 118 */ 119export const DocumentViewer = forwardRef<DocumentViewerRef, DocumentViewerProps>( 120 function DocumentViewer(props, ref) { 121 const { 122 source, 123 format, 124 theme, 125 className, 126 style, 127 initialPage, 128 zoom, 129 toolbar, 130 showPageNumbers, 131 onReady, 132 onPageChange, 133 onZoomChange, 134 onError, 135 onLoadingChange, 136 pdf, 137 code, 138 csv, 139 epub, 140 } = props 141 142 const { 143 containerRef, 144 state, 145 goToPage, 146 setZoom, 147 } = useDocumentRenderer({ 148 source: source ?? undefined, 149 format, 150 theme, 151 className, 152 initialPage, 153 zoom, 154 toolbar, 155 showPageNumbers, 156 onReady, 157 onPageChange, 158 onZoomChange, 159 onError, 160 onLoadingChange, 161 pdf, 162 code, 163 csv, 164 epub, 165 }) 166 167 // Expose imperative API via ref 168 useImperativeHandle(ref, () => ({ 169 goToPage, 170 setZoom, 171 getCurrentPage: () => state.currentPage, 172 getPageCount: () => state.totalPages, 173 getZoom: () => state.zoom, 174 }), [goToPage, setZoom, state.currentPage, state.totalPages, state.zoom]) 175 176 const containerStyle = useMemo<React.CSSProperties>(() => ({ 177 width: '100%', 178 height: '100%', 179 minHeight: 200, 180 ...style, 181 }), [style]) 182 183 return ( 184 <div 185 ref={containerRef as any} 186 style={containerStyle} 187 data-polyrender-wrapper="" 188 /> 189 ) 190 }, 191)