A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

rename package

aria f2af499e 4b32fcaa

+959 -828
+23 -23
README.md
··· 1 - # DocView 1 + # PolyRender 2 2 3 3 A framework-agnostic, universal document renderer for the browser. Render PDFs, EPUBs, DOCX files, CSVs, source code, and plain text — with optional support for pre-rendered page images and chunked streaming for large documents. 4 4 5 - **Core** (`@docview/core`) is a vanilla TypeScript library with zero framework dependencies. **React** (`@docview/react`) provides a thin wrapper component and hook. Both are designed for drop-in use in any web project. 5 + **Core** (`@polyrender/core`) is a vanilla TypeScript library with zero framework dependencies. **React** (`@polyrender/react`) provides a thin wrapper component and hook. Both are designed for drop-in use in any web project. 6 6 7 7 ## Features 8 8 ··· 19 19 20 20 ```bash 21 21 # Core (vanilla JS) 22 - npm install @docview/core 22 + npm install @polyrender/core 23 23 24 24 # React wrapper 25 - npm install @docview/react 25 + npm install @polyrender/react 26 26 27 27 # Install peer dependencies for the formats you need: 28 28 npm install pdfjs-dist # PDF ··· 39 39 ### Vanilla JS 40 40 41 41 ```typescript 42 - import { DocView } from '@docview/core' 43 - import '@docview/core/styles.css' 42 + import { PolyRender } from '@polyrender/core' 43 + import '@polyrender/core/styles.css' 44 44 45 - const viewer = new DocView(document.getElementById('viewer')!, { 45 + const viewer = new PolyRender(document.getElementById('viewer')!, { 46 46 source: { type: 'url', url: '/document.pdf' }, 47 47 theme: 'dark', 48 48 toolbar: true, ··· 65 65 ### React 66 66 67 67 ```tsx 68 - import { DocumentViewer } from '@docview/react' 69 - import '@docview/core/styles.css' 68 + import { DocumentViewer } from '@polyrender/react' 69 + import '@polyrender/core/styles.css' 70 70 71 71 function App() { 72 72 return ( ··· 85 85 86 86 ```tsx 87 87 import { useRef } from 'react' 88 - import { DocumentViewer, type DocumentViewerRef } from '@docview/react' 89 - import '@docview/core/styles.css' 88 + import { DocumentViewer, type DocumentViewerRef } from '@polyrender/react' 89 + import '@polyrender/core/styles.css' 90 90 91 91 function App() { 92 92 const viewerRef = useRef<DocumentViewerRef>(null) ··· 109 109 ### React Hook (headless) 110 110 111 111 ```tsx 112 - import { useDocumentRenderer } from '@docview/react' 113 - import '@docview/core/styles.css' 112 + import { useDocumentRenderer } from '@polyrender/react' 113 + import '@polyrender/core/styles.css' 114 114 115 115 function CustomViewer({ url }: { url: string }) { 116 116 const { containerRef, state, goToPage, setZoom } = useDocumentRenderer({ ··· 135 135 136 136 ## Document Sources 137 137 138 - DocView accepts four types of document sources: 138 + PolyRender accepts four types of document sources: 139 139 140 140 ### File (binary data) 141 141 ··· 226 226 227 227 ## Theming 228 228 229 - DocView uses CSS custom properties for all visual styling. Override any `--dv-*` variable to customize: 229 + PolyRender uses CSS custom properties for all visual styling. Override any `--dv-*` variable to customize: 230 230 231 231 ```css 232 232 /* Custom theme */ 233 - .my-viewer .docview { 233 + .my-viewer .polyrender { 234 234 --dv-bg: #1e1e2e; 235 235 --dv-surface: #2a2a3e; 236 236 --dv-text: #cdd6f4; ··· 320 320 Register a renderer for any format: 321 321 322 322 ```typescript 323 - import { DocView, BaseRenderer, type DocViewOptions, type DocumentFormat } from '@docview/core' 323 + import { PolyRender, BaseRenderer, type PolyRenderOptions, type DocumentFormat } from '@polyrender/core' 324 324 325 325 class MarkdownRenderer extends BaseRenderer { 326 326 readonly format: DocumentFormat = 'custom-markdown' 327 327 328 - protected async onMount(viewport: HTMLElement, options: DocViewOptions) { 328 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions) { 329 329 // Your rendering logic here 330 330 const text = await this.loadText(options.source) 331 331 const html = myMarkdownLib.render(text) ··· 337 337 } 338 338 339 339 // Register globally 340 - DocView.registerRenderer('custom-markdown', () => new MarkdownRenderer()) 340 + PolyRender.registerRenderer('custom-markdown', () => new MarkdownRenderer()) 341 341 342 342 // Use it 343 - new DocView(container, { 343 + new PolyRender(container, { 344 344 source: { type: 'url', url: '/readme.md' }, 345 345 format: 'custom-markdown', 346 346 }) ··· 372 372 373 373 ``` 374 374 packages/ 375 - ├── core/ @docview/core — Framework-agnostic TypeScript core 375 + ├── core/ @polyrender/core — Framework-agnostic TypeScript core 376 376 │ ├── src/ 377 377 │ │ ├── types.ts # All interfaces and types 378 - │ │ ├── docview.ts # Main DocView class 378 + │ │ ├── polyrender.ts # Main PolyRender class 379 379 │ │ ├── renderer.ts # Abstract base renderer 380 380 │ │ ├── registry.ts # Format → renderer factory mapping 381 381 │ │ ├── toolbar.ts # Built-in toolbar DOM builder ··· 391 391 │ │ ├── code.ts # Code (highlight.js) 392 392 │ │ └── text.ts # Plain text 393 393 │ └── package.json 394 - └── react/ @docview/react — React wrapper 394 + └── react/ @polyrender/react — React wrapper 395 395 ├── src/ 396 396 │ ├── DocumentViewer.tsx # Drop-in component 397 397 │ ├── useDocumentRenderer.ts # Headless hook
+79 -70
examples/basic/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>DocView — Basic Example</title> 7 - <style> 8 - * { 9 - margin: 0; 10 - padding: 0; 11 - box-sizing: border-box; 12 - } 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>PolyRender — Basic Example</title> 7 + <style> 8 + * { 9 + margin: 0; 10 + padding: 0; 11 + box-sizing: border-box; 12 + } 13 13 14 - body { 15 - font-family: system-ui, -apple-system, sans-serif; 16 - background: #0f0f0f; 17 - color: #e0e0e0; 18 - display: flex; 19 - flex-direction: column; 20 - align-items: center; 21 - min-height: 100vh; 22 - padding: 2rem; 23 - } 14 + body { 15 + font-family: 16 + system-ui, 17 + -apple-system, 18 + sans-serif; 19 + background: #0f0f0f; 20 + color: #e0e0e0; 21 + display: flex; 22 + flex-direction: column; 23 + align-items: center; 24 + min-height: 100vh; 25 + padding: 2rem; 26 + } 24 27 25 - h1 { 26 - font-size: 1.5rem; 27 - font-weight: 600; 28 - margin-bottom: 0.5rem; 29 - color: #fff; 30 - } 28 + h1 { 29 + font-size: 1.5rem; 30 + font-weight: 600; 31 + margin-bottom: 0.5rem; 32 + color: #fff; 33 + } 31 34 32 - .subtitle { 33 - font-size: 0.875rem; 34 - color: #888; 35 - margin-bottom: 1.5rem; 36 - } 35 + .subtitle { 36 + font-size: 0.875rem; 37 + color: #888; 38 + margin-bottom: 1.5rem; 39 + } 37 40 38 - #viewer { 39 - width: 100%; 40 - max-width: 900px; 41 - height: 80vh; 42 - border-radius: 8px; 43 - overflow: hidden; 44 - border: 1px solid #2a2a2a; 45 - } 41 + #viewer { 42 + width: 100%; 43 + max-width: 900px; 44 + height: 80vh; 45 + border-radius: 8px; 46 + overflow: hidden; 47 + border: 1px solid #2a2a2a; 48 + } 46 49 47 - .controls { 48 - margin-top: 1rem; 49 - display: flex; 50 - gap: 0.5rem; 51 - } 50 + .controls { 51 + margin-top: 1rem; 52 + display: flex; 53 + gap: 0.5rem; 54 + } 52 55 53 - .controls label { 54 - font-size: 0.875rem; 55 - color: #aaa; 56 - display: flex; 57 - align-items: center; 58 - gap: 0.5rem; 59 - } 56 + .controls label { 57 + font-size: 0.875rem; 58 + color: #aaa; 59 + display: flex; 60 + align-items: center; 61 + gap: 0.5rem; 62 + } 60 63 61 - .controls input[type="file"] { 62 - font-size: 0.8rem; 63 - color: #ccc; 64 - } 65 - </style> 66 - </head> 67 - <body> 68 - <h1>DocView — Basic Example</h1> 69 - <p class="subtitle">Drop-in document viewer. Pick a file below to view it.</p> 70 - <div class="controls"> 71 - <label> 72 - Open a document: 73 - <input type="file" id="file-input" accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" /> 74 - </label> 75 - </div> 76 - <div id="viewer"></div> 77 - <script type="module" src="/src/main.ts"></script> 78 - </body> 64 + .controls input[type="file"] { 65 + font-size: 0.8rem; 66 + color: #ccc; 67 + } 68 + </style> 69 + </head> 70 + <body> 71 + <h1>PolyRender — Basic Example</h1> 72 + <p class="subtitle"> 73 + Drop-in document viewer. Pick a file below to view it. 74 + </p> 75 + <div class="controls"> 76 + <label> 77 + Open a document: 78 + <input 79 + type="file" 80 + id="file-input" 81 + accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" 82 + /> 83 + </label> 84 + </div> 85 + <div id="viewer"></div> 86 + <script type="module" src="/src/main.ts"></script> 87 + </body> 79 88 </html>
+2 -2
examples/basic/package.json
··· 1 1 { 2 - "name": "@docview/example-basic", 2 + "name": "@polyrender/example-basic", 3 3 "private": true, 4 4 "version": "0.0.0", 5 5 "type": "module", ··· 9 9 "preview": "vite preview" 10 10 }, 11 11 "dependencies": { 12 - "@docview/core": "workspace:*", 12 + "@polyrender/core": "workspace:*", 13 13 "pdfjs-dist": ">=4.0.0", 14 14 "epubjs": ">=0.3.0", 15 15 "docx-preview": ">=0.3.0",
+5 -5
examples/basic/src/main.ts
··· 1 - import { DocView } from '@docview/core' 1 + import { PolyRender } from '@polyrender/core' 2 2 import '../../../packages/core/src/styles.css' 3 3 import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url' 4 4 5 5 const viewerEl = document.getElementById('viewer')! 6 6 const fileInput = document.getElementById('file-input') as HTMLInputElement 7 7 8 - let viewer: DocView | null = null 8 + let viewer: PolyRender | null = null 9 9 10 10 fileInput.addEventListener('change', () => { 11 11 const file = fileInput.files?.[0] ··· 17 17 viewer = null 18 18 } 19 19 20 - // Create a new DocView instance with the selected file 21 - viewer = new DocView(viewerEl, { 20 + // Create a new PolyRender instance with the selected file 21 + viewer = new PolyRender(viewerEl, { 22 22 source: { 23 23 type: 'file', 24 24 data: file, ··· 33 33 console.log(`Loaded "${file.name}" — ${info.pageCount} page(s), format: ${info.format}`) 34 34 }, 35 35 onError: (err) => { 36 - console.error('DocView error:', err) 36 + console.error('PolyRender error:', err) 37 37 }, 38 38 }) 39 39 })
+3 -3
examples/basic/vite.config.ts
··· 33 33 ].join('\n') 34 34 35 35 return { 36 - name: 'resolve-docview-peer-deps', 36 + name: 'resolve-polyrender-peer-deps', 37 37 enforce: 'pre', 38 38 transform(code: string, id: string) { 39 - // Only transform the @docview/core bundle 40 - if (!id.includes('docview')) return 39 + // Only transform the @polyrender/core bundle 40 + if (!id.includes('polyrender')) return 41 41 if (!code.includes('moduleName')) return 42 42 43 43 // Replace `await import(moduleName)` or `await import(\n moduleName\n)`
+82 -64
examples/vanilla/build.js
··· 2 2 * Minimal esbuild build script — replaces Vite entirely. 3 3 * 4 4 * The only "magic" is an esbuild plugin that rewrites the variable-based 5 - * dynamic `import(moduleName)` in @docview/core's `requirePeerDep` into 5 + * dynamic `import(moduleName)` in @polyrender/core's `requirePeerDep` into 6 6 * static import() calls that esbuild can bundle. 7 7 * 8 8 * This is the same fundamental fix needed in Vite, webpack, or any other 9 9 * bundler — browsers cannot resolve bare specifiers from `import(variable)`. 10 10 */ 11 - import { build, context } from 'esbuild' 12 - import { cpSync, existsSync, mkdirSync } from 'fs' 13 - import { resolve, dirname } from 'path' 14 - import { fileURLToPath } from 'url' 11 + import { build, context } from "esbuild"; 12 + import { cpSync, existsSync, mkdirSync } from "fs"; 13 + import { resolve, dirname } from "path"; 14 + import { fileURLToPath } from "url"; 15 15 16 - const __dirname = dirname(fileURLToPath(import.meta.url)) 17 - const isWatch = process.argv.includes('--watch') 16 + const __dirname = dirname(fileURLToPath(import.meta.url)); 17 + const isWatch = process.argv.includes("--watch"); 18 18 19 - /** Plugin to resolve @docview/core's dynamic peer-dep imports. */ 19 + /** Plugin to resolve @polyrender/core's dynamic peer-dep imports. */ 20 20 const resolvePeerDeps = { 21 - name: 'resolve-peer-deps', 22 - setup(build) { 23 - build.onLoad({ filter: /docview.*\.(js|ts)$/ }, async (args) => { 24 - const fs = await import('fs') 25 - let contents = fs.readFileSync(args.path, 'utf8') 21 + name: "resolve-peer-deps", 22 + setup(build) { 23 + build.onLoad({ filter: /polyrender.*\.(js|ts)$/ }, async (args) => { 24 + const fs = await import("fs"); 25 + let contents = fs.readFileSync(args.path, "utf8"); 26 26 27 - if (contents.includes('moduleName') && contents.includes('import(')) { 28 - // Map of peer dep names → static imports 29 - const peerDeps = [ 30 - 'pdfjs-dist', 'epubjs', 'docx-preview', 31 - 'papaparse', 'highlight.js', 'jszip', 'xlsx', 32 - ] 33 - const cases = peerDeps 34 - .map(d => ` case '${d}': return import('${d}').then(m => m.default || m);`) 35 - .join('\n') 27 + if ( 28 + contents.includes("moduleName") && 29 + contents.includes("import(") 30 + ) { 31 + // Map of peer dep names → static imports 32 + const peerDeps = [ 33 + "pdfjs-dist", 34 + "epubjs", 35 + "docx-preview", 36 + "papaparse", 37 + "highlight.js", 38 + "jszip", 39 + "xlsx", 40 + ]; 41 + const cases = peerDeps 42 + .map( 43 + (d) => 44 + ` case '${d}': return import('${d}').then(m => m.default || m);`, 45 + ) 46 + .join("\n"); 36 47 37 - const replacement = [ 38 - '(async (name) => { switch(name) {', 39 - cases, 40 - " default: throw new Error(`Unknown peer dep: ${name}`);", 41 - ' }})(moduleName)', 42 - ].join('\n') 48 + const replacement = [ 49 + "(async (name) => { switch(name) {", 50 + cases, 51 + " default: throw new Error(`Unknown peer dep: ${name}`);", 52 + " }})(moduleName)", 53 + ].join("\n"); 43 54 44 - contents = contents.replace( 45 - /await\s+import\(\s*(?:\/\*.*?\*\/\s*)?moduleName\s*\)/g, 46 - `await ${replacement}`, 47 - ) 48 - } 55 + contents = contents.replace( 56 + /await\s+import\(\s*(?:\/\*.*?\*\/\s*)?moduleName\s*\)/g, 57 + `await ${replacement}`, 58 + ); 59 + } 49 60 50 - return { contents, loader: args.path.endsWith('.ts') ? 'ts' : 'js' } 51 - }) 52 - }, 53 - } 61 + return { 62 + contents, 63 + loader: args.path.endsWith(".ts") ? "ts" : "js", 64 + }; 65 + }); 66 + }, 67 + }; 54 68 55 69 // Ensure dist directory exists 56 - const distDir = resolve(__dirname, 'dist') 57 - if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true }) 70 + const distDir = resolve(__dirname, "dist"); 71 + if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true }); 58 72 59 73 // Copy index.html to dist 60 - cpSync(resolve(__dirname, 'index.html'), resolve(distDir, 'index.html')) 74 + cpSync(resolve(__dirname, "index.html"), resolve(distDir, "index.html")); 61 75 62 76 // Copy styles.css to dist 63 - const stylesPath = resolve(__dirname, '../../packages/core/src/styles.css') 77 + const stylesPath = resolve(__dirname, "../../packages/core/src/styles.css"); 64 78 if (existsSync(stylesPath)) { 65 - cpSync(stylesPath, resolve(distDir, 'styles.css')) 79 + cpSync(stylesPath, resolve(distDir, "styles.css")); 66 80 } 67 81 68 82 // Copy pdfjs worker to dist 69 - const workerGlob = resolve(__dirname, 'node_modules/pdfjs-dist/build') 83 + const workerGlob = resolve(__dirname, "node_modules/pdfjs-dist/build"); 70 84 if (existsSync(workerGlob)) { 71 - const workerFiles = ['pdf.worker.min.mjs', 'pdf.worker.mjs', 'pdf.worker.min.js'] 72 - for (const wf of workerFiles) { 73 - const src = resolve(workerGlob, wf) 74 - if (existsSync(src)) { 75 - cpSync(src, resolve(distDir, wf)) 76 - break 85 + const workerFiles = [ 86 + "pdf.worker.min.mjs", 87 + "pdf.worker.mjs", 88 + "pdf.worker.min.js", 89 + ]; 90 + for (const wf of workerFiles) { 91 + const src = resolve(workerGlob, wf); 92 + if (existsSync(src)) { 93 + cpSync(src, resolve(distDir, wf)); 94 + break; 95 + } 77 96 } 78 - } 79 97 } 80 98 81 99 const buildOptions = { 82 - entryPoints: [resolve(__dirname, 'src/main.ts')], 83 - bundle: true, 84 - format: 'esm', 85 - outdir: distDir, 86 - sourcemap: true, 87 - target: 'es2022', 88 - plugins: [resolvePeerDeps], 89 - logLevel: 'info', 90 - } 100 + entryPoints: [resolve(__dirname, "src/main.ts")], 101 + bundle: true, 102 + format: "esm", 103 + outdir: distDir, 104 + sourcemap: true, 105 + target: "es2022", 106 + plugins: [resolvePeerDeps], 107 + logLevel: "info", 108 + }; 91 109 92 110 if (isWatch) { 93 - const ctx = await context(buildOptions) 94 - await ctx.watch() 95 - console.log('Watching for changes...') 111 + const ctx = await context(buildOptions); 112 + await ctx.watch(); 113 + console.log("Watching for changes..."); 96 114 } else { 97 - await build(buildOptions) 98 - console.log(`\nBuild complete! Serve with:\n npx serve dist\n`) 115 + await build(buildOptions); 116 + console.log(`\nBuild complete! Serve with:\n npx serve dist\n`); 99 117 }
+80 -71
examples/vanilla/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>DocView — Vanilla TS Example</title> 7 - <link rel="stylesheet" href="./styles.css" /> 8 - <style> 9 - * { 10 - margin: 0; 11 - padding: 0; 12 - box-sizing: border-box; 13 - } 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>PolyRender — Vanilla TS Example</title> 7 + <link rel="stylesheet" href="./styles.css" /> 8 + <style> 9 + * { 10 + margin: 0; 11 + padding: 0; 12 + box-sizing: border-box; 13 + } 14 14 15 - body { 16 - font-family: system-ui, -apple-system, sans-serif; 17 - background: #0f0f0f; 18 - color: #e0e0e0; 19 - display: flex; 20 - flex-direction: column; 21 - align-items: center; 22 - min-height: 100vh; 23 - padding: 2rem; 24 - } 15 + body { 16 + font-family: 17 + system-ui, 18 + -apple-system, 19 + sans-serif; 20 + background: #0f0f0f; 21 + color: #e0e0e0; 22 + display: flex; 23 + flex-direction: column; 24 + align-items: center; 25 + min-height: 100vh; 26 + padding: 2rem; 27 + } 25 28 26 - h1 { 27 - font-size: 1.5rem; 28 - font-weight: 600; 29 - margin-bottom: 0.5rem; 30 - color: #fff; 31 - } 29 + h1 { 30 + font-size: 1.5rem; 31 + font-weight: 600; 32 + margin-bottom: 0.5rem; 33 + color: #fff; 34 + } 32 35 33 - .subtitle { 34 - font-size: 0.875rem; 35 - color: #888; 36 - margin-bottom: 1.5rem; 37 - } 36 + .subtitle { 37 + font-size: 0.875rem; 38 + color: #888; 39 + margin-bottom: 1.5rem; 40 + } 38 41 39 - #viewer { 40 - width: 100%; 41 - max-width: 900px; 42 - height: 80vh; 43 - border-radius: 8px; 44 - overflow: hidden; 45 - border: 1px solid #2a2a2a; 46 - } 42 + #viewer { 43 + width: 100%; 44 + max-width: 900px; 45 + height: 80vh; 46 + border-radius: 8px; 47 + overflow: hidden; 48 + border: 1px solid #2a2a2a; 49 + } 47 50 48 - .controls { 49 - margin-top: 1rem; 50 - display: flex; 51 - gap: 0.5rem; 52 - } 51 + .controls { 52 + margin-top: 1rem; 53 + display: flex; 54 + gap: 0.5rem; 55 + } 53 56 54 - .controls label { 55 - font-size: 0.875rem; 56 - color: #aaa; 57 - display: flex; 58 - align-items: center; 59 - gap: 0.5rem; 60 - } 57 + .controls label { 58 + font-size: 0.875rem; 59 + color: #aaa; 60 + display: flex; 61 + align-items: center; 62 + gap: 0.5rem; 63 + } 61 64 62 - .controls input[type="file"] { 63 - font-size: 0.8rem; 64 - color: #ccc; 65 - } 66 - </style> 67 - </head> 68 - <body> 69 - <h1>DocView — Vanilla TS Example</h1> 70 - <p class="subtitle">No Vite. Just TypeScript + esbuild. Pick a file below to view it.</p> 71 - <div class="controls"> 72 - <label> 73 - Open a document: 74 - <input type="file" id="file-input" accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" /> 75 - </label> 76 - </div> 77 - <div id="viewer"></div> 78 - <script type="module" src="./main.js"></script> 79 - </body> 65 + .controls input[type="file"] { 66 + font-size: 0.8rem; 67 + color: #ccc; 68 + } 69 + </style> 70 + </head> 71 + <body> 72 + <h1>PolyRender — Vanilla TS Example</h1> 73 + <p class="subtitle"> 74 + No Vite. Just TypeScript + esbuild. Pick a file below to view it. 75 + </p> 76 + <div class="controls"> 77 + <label> 78 + Open a document: 79 + <input 80 + type="file" 81 + id="file-input" 82 + accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" 83 + /> 84 + </label> 85 + </div> 86 + <div id="viewer"></div> 87 + <script type="module" src="./main.js"></script> 88 + </body> 80 89 </html>
+3 -3
examples/vanilla/package.json
··· 1 1 { 2 - "name": "@docview/example-vanilla", 2 + "name": "@polyrender/example-vanilla", 3 3 "private": true, 4 4 "version": "0.0.0", 5 5 "type": "module", ··· 9 9 "dev": "node build.js --watch" 10 10 }, 11 11 "dependencies": { 12 - "@docview/core": "workspace:*", 12 + "@polyrender/core": "workspace:*", 13 13 "pdfjs-dist": ">=4.0.0", 14 14 "epubjs": ">=0.3.0", 15 15 "docx-preview": ">=0.3.0", ··· 22 22 "typescript": "^5.5.0", 23 23 "esbuild": "^0.25.0" 24 24 } 25 - } 25 + }
+5 -5
examples/vanilla/src/main.ts
··· 1 - import { DocView } from '@docview/core' 1 + import { PolyRender } from '@polyrender/core' 2 2 3 3 const viewerEl = document.getElementById('viewer')! 4 4 const fileInput = document.getElementById('file-input') as HTMLInputElement 5 5 6 - let viewer: DocView | null = null 6 + let viewer: PolyRender | null = null 7 7 8 8 fileInput.addEventListener('change', () => { 9 9 const file = fileInput.files?.[0] ··· 15 15 viewer = null 16 16 } 17 17 18 - // Create a new DocView instance with the selected file 19 - viewer = new DocView(viewerEl, { 18 + // Create a new PolyRender instance with the selected file 19 + viewer = new PolyRender(viewerEl, { 20 20 source: { 21 21 type: 'file', 22 22 data: file, ··· 32 32 console.log(`Loaded "${file.name}" — ${info.pageCount} page(s), format: ${info.format}`) 33 33 }, 34 34 onError: (err) => { 35 - console.error('DocView error:', err) 35 + console.error('PolyRender error:', err) 36 36 }, 37 37 }) 38 38 })
+1 -1
package.json
··· 1 1 { 2 - "name": "docview-monorepo", 2 + "name": "polyrender-monorepo", 3 3 "private": true, 4 4 "version": "0.0.0", 5 5 "description": "A framework-agnostic, universal document renderer with optional chunked loading",
+1 -1
packages/core/package.json
··· 1 1 { 2 - "name": "@docview/core", 2 + "name": "@polyrender/core", 3 3 "version": "0.1.0", 4 4 "description": "Framework-agnostic universal document renderer — PDF, EPUB, DOCX, CSV, code, and plain text with optional chunked/paged loading", 5 5 "license": "Zlib",
+23 -23
packages/core/src/docview.ts packages/core/src/polyrender.ts
··· 1 1 import type { 2 - DocViewOptions, 3 - DocViewState, 2 + PolyRenderOptions, 3 + PolyRenderState, 4 4 DocumentFormat, 5 5 Renderer, 6 6 RendererFactory, 7 - DocViewEventMap, 8 - DocViewEventType, 7 + PolyRenderEventMap, 8 + PolyRenderEventType, 9 9 ToolbarConfig, 10 10 } from './types.js' 11 - import { DocViewError } from './types.js' 11 + import { PolyRenderError } from './types.js' 12 12 import { registry } from './registry.js' 13 13 import { detectFormat, getRendererFormat, clearElement } from './utils.js' 14 14 import { createToolbar, type ToolbarHandle } from './toolbar.js' ··· 24 24 } 25 25 26 26 /** 27 - * DocView — Universal Document Viewer 27 + * PolyRender — Universal Document Viewer 28 28 * 29 29 * Framework-agnostic entry point. Creates a document viewer inside a container 30 30 * element, auto-detecting the format and loading the appropriate renderer. 31 31 * 32 32 * @example 33 33 * ```ts 34 - * import { DocView } from '@docview/core' 35 - * import '@docview/core/styles.css' 34 + * import { PolyRender } from '@polyrender/core' 35 + * import '@polyrender/core/styles.css' 36 36 * 37 - * const viewer = new DocView(document.getElementById('viewer')!, { 37 + * const viewer = new PolyRender(document.getElementById('viewer')!, { 38 38 * source: { type: 'url', url: '/document.pdf' }, 39 39 * theme: 'dark', 40 40 * onReady: (info) => console.log('Loaded:', info.pageCount, 'pages'), ··· 47 47 * viewer.destroy() 48 48 * ``` 49 49 */ 50 - export class DocView { 50 + export class PolyRender { 51 51 private container: HTMLElement 52 - private options: DocViewOptions 52 + private options: PolyRenderOptions 53 53 private renderer: Renderer | null = null 54 54 private toolbar: ToolbarHandle | null = null 55 55 private root: HTMLElement 56 56 private listeners = new Map<string, Set<(data: unknown) => void>>() 57 57 private destroyed = false 58 58 59 - constructor(container: HTMLElement, options: DocViewOptions) { 59 + constructor(container: HTMLElement, options: PolyRenderOptions) { 60 60 ensureRegistered() 61 61 62 62 this.container = container ··· 64 64 65 65 // Create root element 66 66 this.root = document.createElement('div') 67 - this.root.className = `docview${options.className ? ` ${options.className}` : ''}` 67 + this.root.className = `polyrender${options.className ? ` ${options.className}` : ''}` 68 68 this.root.setAttribute('data-theme', this.resolveTheme(options.theme)) 69 69 container.appendChild(this.root) 70 70 71 71 // Initialize asynchronously 72 72 this.init().catch((err) => { 73 - const error = err instanceof DocViewError 73 + const error = err instanceof PolyRenderError 74 74 ? err 75 - : new DocViewError('UNKNOWN', String(err), err) 75 + : new PolyRenderError('UNKNOWN', String(err), err) 76 76 options.onError?.(error) 77 77 this.emit('error', error) 78 78 }) ··· 110 110 } 111 111 112 112 /** Get current viewer state. */ 113 - getState(): DocViewState { 113 + getState(): PolyRenderState { 114 114 if (!this.renderer) { 115 115 return { 116 116 loading: true, ··· 132 132 } 133 133 134 134 /** Update options (theme, zoom, etc.) without re-mounting. */ 135 - async update(changed: Partial<DocViewOptions>): Promise<void> { 135 + async update(changed: Partial<PolyRenderOptions>): Promise<void> { 136 136 Object.assign(this.options, changed) 137 137 138 138 if (changed.theme) { 139 139 this.root.setAttribute('data-theme', this.resolveTheme(changed.theme)) 140 140 } 141 141 if (changed.className !== undefined) { 142 - this.root.className = `docview${changed.className ? ` ${changed.className}` : ''}` 142 + this.root.className = `polyrender${changed.className ? ` ${changed.className}` : ''}` 143 143 } 144 144 145 145 await this.renderer?.update(changed) 146 146 } 147 147 148 148 /** Subscribe to events. Returns an unsubscribe function. */ 149 - on<K extends DocViewEventType>( 149 + on<K extends PolyRenderEventType>( 150 150 event: K, 151 - callback: (data: DocViewEventMap[K]) => void, 151 + callback: (data: PolyRenderEventMap[K]) => void, 152 152 ): () => void { 153 153 if (!this.listeners.has(event)) { 154 154 this.listeners.set(event, new Set()) ··· 193 193 const format = explicitFormat ?? detectedFormat 194 194 195 195 if (!format) { 196 - throw new DocViewError( 196 + throw new PolyRenderError( 197 197 'FORMAT_DETECTION_FAILED', 198 198 'Could not detect the document format. Provide a `format` option or ensure ' + 199 199 'the source has a recognizable filename, URL extension, or MIME type.', ··· 206 206 // Create renderer 207 207 const renderer = registry.create(rendererFormat) 208 208 if (!renderer) { 209 - throw new DocViewError( 209 + throw new PolyRenderError( 210 210 'FORMAT_UNSUPPORTED', 211 211 `No renderer registered for format "${rendererFormat}". ` + 212 212 `Available formats: ${registry.formats().join(', ')}`, ··· 297 297 } 298 298 } 299 299 300 - private emit<K extends DocViewEventType>(event: K, data: DocViewEventMap[K]): void { 300 + private emit<K extends PolyRenderEventType>(event: K, data: PolyRenderEventMap[K]): void { 301 301 const callbacks = this.listeners.get(event) 302 302 if (callbacks) { 303 303 for (const cb of callbacks) {
+7 -7
packages/core/src/index.ts
··· 1 1 // Main entry point 2 - export { DocView } from './docview.js' 2 + export { PolyRender } from './polyrender.js' 3 3 4 4 // Types 5 5 export type { ··· 22 22 TextFetchAdapter, 23 23 24 24 // Options 25 - DocViewOptions, 25 + PolyRenderOptions, 26 26 PdfOptions, 27 27 CodeOptions, 28 28 CsvOptions, ··· 33 33 34 34 // State & Info 35 35 DocumentInfo, 36 - DocViewState, 36 + PolyRenderState, 37 37 DocumentFormat, 38 38 39 39 // Renderer interface (for custom renderers) ··· 41 41 RendererFactory, 42 42 43 43 // Events 44 - DocViewEventMap, 45 - DocViewEventType, 44 + PolyRenderEventMap, 45 + PolyRenderEventType, 46 46 47 47 // Errors 48 - DocViewErrorCode, 48 + PolyRenderErrorCode, 49 49 } from './types.js' 50 50 51 - export { DocViewError } from './types.js' 51 + export { PolyRenderError } from './types.js' 52 52 53 53 // Registry (for custom renderer registration) 54 54 export { registry } from './registry.js'
+1 -1
packages/core/src/registry.ts
··· 3 3 /** 4 4 * Registry mapping document formats to their renderer factories. 5 5 * Built-in renderers are registered by default. Consumers can register 6 - * custom renderers for new formats via `DocView.registerRenderer()`. 6 + * custom renderers for new formats via `PolyRender.registerRenderer()`. 7 7 */ 8 8 class FormatRegistry { 9 9 private factories = new Map<DocumentFormat, RendererFactory>()
+12 -12
packages/core/src/renderer.ts
··· 1 1 import type { 2 2 Renderer, 3 - DocViewOptions, 3 + PolyRenderOptions, 4 4 DocumentFormat, 5 5 DocumentInfo, 6 - DocViewState, 6 + PolyRenderState, 7 7 } from './types.js' 8 - import { DocViewError } from './types.js' 8 + import { PolyRenderError } from './types.js' 9 9 import { el, clearElement } from './utils.js' 10 10 11 11 /** ··· 23 23 24 24 protected container!: HTMLElement 25 25 protected viewport!: HTMLElement 26 - protected options!: DocViewOptions 27 - protected state: DocViewState = { 26 + protected options!: PolyRenderOptions 27 + protected state: PolyRenderState = { 28 28 loading: true, 29 29 error: null, 30 30 currentPage: 1, ··· 33 33 documentInfo: null, 34 34 } 35 35 36 - async mount(container: HTMLElement, options: DocViewOptions): Promise<void> { 36 + async mount(container: HTMLElement, options: PolyRenderOptions): Promise<void> { 37 37 this.container = container 38 38 this.options = options 39 39 this.state.currentPage = options.initialPage ?? 1 ··· 47 47 try { 48 48 await this.onMount(this.viewport, options) 49 49 } catch (err) { 50 - const error = err instanceof DocViewError 50 + const error = err instanceof PolyRenderError 51 51 ? err 52 - : new DocViewError('RENDER_FAILED', String(err), err) 52 + : new PolyRenderError('RENDER_FAILED', String(err), err) 53 53 this.state.error = error 54 54 this.state.loading = false 55 55 this.showError(error) ··· 57 57 } 58 58 } 59 59 60 - async update(changed: Partial<DocViewOptions>): Promise<void> { 60 + async update(changed: Partial<PolyRenderOptions>): Promise<void> { 61 61 Object.assign(this.options, changed) 62 62 await this.onUpdate(changed) 63 63 } ··· 99 99 // --- Subclass hooks --- 100 100 101 101 /** Render the document into the viewport. */ 102 - protected abstract onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> 102 + protected abstract onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> 103 103 104 104 /** Clean up format-specific resources. */ 105 105 protected abstract onDestroy(): void 106 106 107 107 /** React to option changes. Default: no-op. */ 108 - protected async onUpdate(_changed: Partial<DocViewOptions>): Promise<void> {} 108 + protected async onUpdate(_changed: Partial<PolyRenderOptions>): Promise<void> {} 109 109 110 110 /** Navigate to a page in the rendered content. Default: no-op. */ 111 111 protected onPageChange(_page: number): void {} ··· 140 140 } 141 141 142 142 /** Show an error message in the viewport. */ 143 - protected showError(error: DocViewError): void { 143 + protected showError(error: PolyRenderError): void { 144 144 clearElement(this.viewport) 145 145 const errorEl = el('div', 'dv-error') 146 146 errorEl.innerHTML = `
+4 -4
packages/core/src/renderers/browse-pages.ts
··· 1 1 import type { 2 - DocViewOptions, 2 + PolyRenderOptions, 3 3 DocumentFormat, 4 4 PageData, 5 5 PageFetchAdapter, ··· 7 7 TextLayerData, 8 8 TextFetchAdapter, 9 9 } from '../types.js' 10 - import { DocViewError } from '../types.js' 10 + import { PolyRenderError } from '../types.js' 11 11 import { BaseRenderer } from '../renderer.js' 12 12 import { el, clamp, debounce } from '../utils.js' 13 13 ··· 31 31 private observer: IntersectionObserver | null = null 32 32 private debouncedScroll: ReturnType<typeof debounce> | null = null 33 33 34 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 34 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 35 35 const loadingEl = this.showLoading('Loading pages…') 36 36 const source = options.source as PagesSource 37 37 ··· 53 53 54 54 const totalPages = this.fetchAdapter?.totalPages ?? this.pages.length 55 55 if (totalPages === 0) { 56 - throw new DocViewError('RENDER_FAILED', 'No pages provided.') 56 + throw new PolyRenderError('RENDER_FAILED', 'No pages provided.') 57 57 } 58 58 59 59 // Create pages container
+4 -4
packages/core/src/renderers/chunked-pdf.ts
··· 1 1 import type { 2 - DocViewOptions, 2 + PolyRenderOptions, 3 3 DocumentFormat, 4 4 ChunkedSource, 5 5 ChunkData, 6 6 ChunkFetchAdapter, 7 7 PageFetchAdapter, 8 8 } from '../types.js' 9 - import { DocViewError } from '../types.js' 9 + import { PolyRenderError } from '../types.js' 10 10 import { BaseRenderer } from '../renderer.js' 11 11 import { el, clamp, debounce, requirePeerDep, toArrayBuffer } from '../utils.js' 12 12 ··· 42 42 private debouncedScroll: ReturnType<typeof debounce> | null = null 43 43 private pdfjsLib: unknown = null 44 44 45 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 45 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 46 46 const loadingEl = this.showLoading('Loading document…') 47 47 48 48 const source = options.source as ChunkedSource ··· 66 66 } catch { 67 67 // If pdfjs not available, we can still show browse pages 68 68 if (!this.browseAdapter && !source.browsePages) { 69 - throw new DocViewError( 69 + throw new PolyRenderError( 70 70 'PEER_DEPENDENCY_MISSING', 71 71 'Chunked PDF rendering requires pdfjs-dist. Install it with: npm install pdfjs-dist', 72 72 )
+5 -5
packages/core/src/renderers/code.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toText, fetchAsBuffer, requirePeerDep, getLanguageFromExtension } from '../utils.js' 4 4 ··· 20 20 private codeContainer!: HTMLElement 21 21 private hljs: HighlightJS | null = null 22 22 23 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 23 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 24 24 this.showLoading('Loading file…') 25 25 26 26 // Try to load highlight.js (optional peer dep) ··· 89 89 }) 90 90 } 91 91 92 - private async loadText(options: DocViewOptions): Promise<string> { 92 + private async loadText(options: PolyRenderOptions): Promise<string> { 93 93 const source = options.source 94 94 if (source.type === 'file') return toText(source.data) 95 95 if (source.type === 'url') { ··· 99 99 return '' 100 100 } 101 101 102 - private detectLanguage(options: DocViewOptions): string | null { 102 + private detectLanguage(options: PolyRenderOptions): string | null { 103 103 // From explicit format 104 104 const format = options.format 105 105 if (format && format !== 'code') { ··· 133 133 .replace(/"/g, '&quot;') 134 134 } 135 135 136 - private getFilename(options: DocViewOptions): string | undefined { 136 + private getFilename(options: PolyRenderOptions): string | undefined { 137 137 const source = options.source 138 138 if ('filename' in source && source.filename) return source.filename 139 139 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+4 -4
packages/core/src/renderers/csv.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toText, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 4 ··· 30 30 private sortCol = -1 31 31 private sortAsc = true 32 32 33 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 33 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 34 34 this.showLoading('Parsing data…') 35 35 36 36 const Papa = await requirePeerDep<PapaParse>('papaparse', 'CSV') ··· 156 156 this.renderTable(true) 157 157 } 158 158 159 - private async loadText(options: DocViewOptions): Promise<string> { 159 + private async loadText(options: PolyRenderOptions): Promise<string> { 160 160 const source = options.source 161 161 if (source.type === 'file') return toText(source.data) 162 162 if (source.type === 'url') { ··· 166 166 return '' 167 167 } 168 168 169 - private getFilename(options: DocViewOptions): string | undefined { 169 + private getFilename(options: PolyRenderOptions): string | undefined { 170 170 const source = options.source 171 171 if ('filename' in source && source.filename) return source.filename 172 172 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+4 -4
packages/core/src/renderers/docx.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 4 ··· 35 35 36 36 private docxContainer!: HTMLElement 37 37 38 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 38 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 39 39 this.showLoading('Rendering document…') 40 40 41 41 const docxPreview = await requirePeerDep<DocxPreview>('docx-preview', 'DOCX') ··· 71 71 }) 72 72 } 73 73 74 - private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 74 + private async loadData(options: PolyRenderOptions): Promise<ArrayBuffer> { 75 75 const source = options.source 76 76 if (source.type === 'file') return toArrayBuffer(source.data) 77 77 if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 78 78 throw new Error('DOCX renderer requires a file or url source.') 79 79 } 80 80 81 - private getFilename(options: DocViewOptions): string | undefined { 81 + private getFilename(options: PolyRenderOptions): string | undefined { 82 82 const source = options.source 83 83 if ('filename' in source && source.filename) return source.filename 84 84 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+3 -3
packages/core/src/renderers/epub.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 4 ··· 52 52 private epubContainer!: HTMLElement 53 53 private keyHandler: ((e: KeyboardEvent) => void) | null = null 54 54 55 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 55 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 56 56 this.showLoading('Loading book…') 57 57 58 58 const epubjs = await requirePeerDep<EpubJSModule>('epubjs', 'EPUB') ··· 163 163 this.rendition?.display(String(page - 1)) 164 164 } 165 165 166 - private getFilename(options: DocViewOptions): string | undefined { 166 + private getFilename(options: PolyRenderOptions): string | undefined { 167 167 const source = options.source 168 168 if ('filename' in source && source.filename) return source.filename 169 169 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+4 -4
packages/core/src/renderers/ods.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 4 ··· 35 35 private sortCol = -1 36 36 private sortAsc = true 37 37 38 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 38 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 39 39 this.showLoading('Parsing spreadsheet…') 40 40 41 41 const XLSX = await requirePeerDep<XLSXLib>('xlsx', 'ODS') ··· 213 213 } 214 214 } 215 215 216 - private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 216 + private async loadData(options: PolyRenderOptions): Promise<ArrayBuffer> { 217 217 const source = options.source 218 218 if (source.type === 'file') return toArrayBuffer(source.data) 219 219 if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 220 220 throw new Error('ODS renderer requires a file or url source.') 221 221 } 222 222 223 - private getFilename(options: DocViewOptions): string | undefined { 223 + private getFilename(options: PolyRenderOptions): string | undefined { 224 224 const source = options.source 225 225 if ('filename' in source && source.filename) return source.filename 226 226 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+4 -4
packages/core/src/renderers/odt.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 4 ··· 27 27 28 28 private odtContainer!: HTMLElement 29 29 30 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 30 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 31 31 this.showLoading('Rendering ODT document…') 32 32 33 33 const JSZipLib = await requirePeerDep<JSZip>('jszip', 'ODT') ··· 265 265 .replace(/"/g, '&quot;') 266 266 } 267 267 268 - private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 268 + private async loadData(options: PolyRenderOptions): Promise<ArrayBuffer> { 269 269 const source = options.source 270 270 if (source.type === 'file') return toArrayBuffer(source.data) 271 271 if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 272 272 throw new Error('ODT renderer requires a file or url source.') 273 273 } 274 274 275 - private getFilename(options: DocViewOptions): string | undefined { 275 + private getFilename(options: PolyRenderOptions): string | undefined { 276 276 const source = options.source 277 277 if ('filename' in source && source.filename) return source.filename 278 278 if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0]
+6 -6
packages/core/src/renderers/pdf.ts
··· 1 - import type { DocViewOptions, DocumentFormat, DocumentInfo } from '../types.js' 2 - import { DocViewError } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat, DocumentInfo } from '../types.js' 2 + import { PolyRenderError } from '../types.js' 3 3 import { BaseRenderer } from '../renderer.js' 4 4 import { 5 5 el, ··· 58 58 private resizeObserver: ResizeObserver | null = null 59 59 private debouncedRender: ReturnType<typeof debounce> | null = null 60 60 61 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 61 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 62 62 const loadingEl = this.showLoading('Loading PDF…') 63 63 64 64 // Load pdfjs-dist ··· 182 182 } 183 183 184 184 private async loadSource( 185 - options: DocViewOptions, 185 + options: PolyRenderOptions, 186 186 ): Promise<ArrayBuffer | string> { 187 187 const source = options.source 188 188 if (source.type === 'url') { ··· 192 192 if (source.type === 'file') { 193 193 return toArrayBuffer(source.data) 194 194 } 195 - throw new DocViewError( 195 + throw new PolyRenderError( 196 196 'SOURCE_LOAD_FAILED', 197 197 'PDF renderer requires a file or url source.', 198 198 ) ··· 334 334 return this.baseScale * (mode === 'fit-page' ? 0.95 : 1) 335 335 } 336 336 337 - private getFilename(options: DocViewOptions): string | undefined { 337 + private getFilename(options: PolyRenderOptions): string | undefined { 338 338 const src = options.source 339 339 if ('filename' in src && src.filename) return src.filename 340 340 if (src.type === 'url') {
+5 -5
packages/core/src/renderers/text.ts
··· 1 - import type { DocViewOptions, DocumentFormat } from '../types.js' 1 + import type { PolyRenderOptions, DocumentFormat } from '../types.js' 2 2 import { BaseRenderer } from '../renderer.js' 3 3 import { el, toText, fetchAsBuffer } from '../utils.js' 4 4 ··· 11 11 12 12 private textContainer!: HTMLElement 13 13 14 - protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 14 + protected async onMount(viewport: HTMLElement, options: PolyRenderOptions): Promise<void> { 15 15 this.showLoading('Loading text…') 16 16 17 17 const text = await this.loadText(options) ··· 34 34 }) 35 35 } 36 36 37 - private async loadText(options: DocViewOptions): Promise<string> { 37 + private async loadText(options: PolyRenderOptions): Promise<string> { 38 38 const source = options.source 39 39 if (source.type === 'file') { 40 40 return toText(source.data) ··· 46 46 return '' 47 47 } 48 48 49 - private getExtension(options: DocViewOptions): string { 49 + private getExtension(options: PolyRenderOptions): string { 50 50 const source = options.source 51 51 const name = ('filename' in source ? source.filename : undefined) 52 52 ?? (source.type === 'url' ? source.url : '') ··· 54 54 return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '' 55 55 } 56 56 57 - private getFilename(options: DocViewOptions): string | undefined { 57 + private getFilename(options: PolyRenderOptions): string | undefined { 58 58 const source = options.source 59 59 if ('filename' in source && source.filename) return source.filename 60 60 if (source.type === 'url') {
+526 -428
packages/core/src/styles.css
··· 1 1 /* ========================================================================== 2 - DocView — CSS Variables Theme System 2 + PolyRender — CSS Variables Theme System 3 3 Override any --dv-* variable to customize the viewer appearance. 4 4 ========================================================================== */ 5 5 6 6 /* --- Base / Dark Theme (default) --- */ 7 - .docview { 8 - /* Typography */ 9 - --dv-font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 - --dv-font-mono: 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; 11 - --dv-font-size-sm: 12px; 12 - --dv-font-size: 14px; 13 - --dv-font-size-lg: 16px; 14 - --dv-line-height: 1.5; 7 + .polyrender { 8 + /* Typography */ 9 + --dv-font-sans: 10 + system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 11 + sans-serif; 12 + --dv-font-mono: 13 + "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; 14 + --dv-font-size-sm: 12px; 15 + --dv-font-size: 14px; 16 + --dv-font-size-lg: 16px; 17 + --dv-line-height: 1.5; 15 18 16 - /* Spacing & Radii */ 17 - --dv-radius-sm: 4px; 18 - --dv-radius: 6px; 19 - --dv-radius-lg: 10px; 20 - --dv-spacing-xs: 4px; 21 - --dv-spacing-sm: 8px; 22 - --dv-spacing: 12px; 23 - --dv-spacing-lg: 16px; 24 - --dv-spacing-xl: 24px; 19 + /* Spacing & Radii */ 20 + --dv-radius-sm: 4px; 21 + --dv-radius: 6px; 22 + --dv-radius-lg: 10px; 23 + --dv-spacing-xs: 4px; 24 + --dv-spacing-sm: 8px; 25 + --dv-spacing: 12px; 26 + --dv-spacing-lg: 16px; 27 + --dv-spacing-xl: 24px; 25 28 26 - /* Transitions */ 27 - --dv-transition-fast: 100ms ease; 28 - --dv-transition: 150ms ease; 29 - --dv-transition-slow: 250ms ease; 29 + /* Transitions */ 30 + --dv-transition-fast: 100ms ease; 31 + --dv-transition: 150ms ease; 32 + --dv-transition-slow: 250ms ease; 30 33 31 - /* Colors — Dark */ 32 - --dv-bg: #0d1117; 33 - --dv-surface: #161b22; 34 - --dv-surface-raised: #1c2129; 35 - --dv-border: #30363d; 36 - --dv-border-subtle: #21262d; 37 - --dv-text: #e6edf3; 38 - --dv-text-secondary: #8b949e; 39 - --dv-text-muted: #484f58; 40 - --dv-accent: #58a6ff; 41 - --dv-accent-hover: #79b8ff; 42 - --dv-accent-subtle: rgba(88, 166, 255, 0.15); 43 - --dv-error: #f85149; 44 - --dv-warning: #d29922; 34 + /* Colors — Dark */ 35 + --dv-bg: #0d1117; 36 + --dv-surface: #161b22; 37 + --dv-surface-raised: #1c2129; 38 + --dv-border: #30363d; 39 + --dv-border-subtle: #21262d; 40 + --dv-text: #e6edf3; 41 + --dv-text-secondary: #8b949e; 42 + --dv-text-muted: #484f58; 43 + --dv-accent: #58a6ff; 44 + --dv-accent-hover: #79b8ff; 45 + --dv-accent-subtle: rgba(88, 166, 255, 0.15); 46 + --dv-error: #f85149; 47 + --dv-warning: #d29922; 45 48 46 - /* Document Page */ 47 - --dv-page-bg: #ffffff; 48 - --dv-page-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2); 49 - --dv-page-gap: 12px; 49 + /* Document Page */ 50 + --dv-page-bg: #ffffff; 51 + --dv-page-shadow: 52 + 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2); 53 + --dv-page-gap: 12px; 50 54 51 - /* Toolbar */ 52 - --dv-toolbar-bg: #161b22; 53 - --dv-toolbar-border: #30363d; 54 - --dv-toolbar-height: 44px; 55 + /* Toolbar */ 56 + --dv-toolbar-bg: #161b22; 57 + --dv-toolbar-border: #30363d; 58 + --dv-toolbar-height: 44px; 55 59 56 - /* Scrollbar */ 57 - --dv-scrollbar-size: 8px; 58 - --dv-scrollbar-thumb: #484f58; 59 - --dv-scrollbar-thumb-hover: #6e7681; 60 - --dv-scrollbar-track: transparent; 60 + /* Scrollbar */ 61 + --dv-scrollbar-size: 8px; 62 + --dv-scrollbar-thumb: #484f58; 63 + --dv-scrollbar-thumb-hover: #6e7681; 64 + --dv-scrollbar-track: transparent; 61 65 62 - /* Table (CSV) */ 63 - --dv-table-header-bg: #161b22; 64 - --dv-table-row-hover: rgba(136, 146, 158, 0.08); 65 - --dv-table-stripe: rgba(136, 146, 158, 0.04); 66 + /* Table (CSV) */ 67 + --dv-table-header-bg: #161b22; 68 + --dv-table-row-hover: rgba(136, 146, 158, 0.08); 69 + --dv-table-stripe: rgba(136, 146, 158, 0.04); 66 70 67 - /* Code */ 68 - --dv-code-bg: #0d1117; 69 - --dv-code-gutter: #484f58; 70 - --dv-code-gutter-bg: #0d1117; 71 - --dv-code-highlight-line: rgba(88, 166, 255, 0.1); 71 + /* Code */ 72 + --dv-code-bg: #0d1117; 73 + --dv-code-gutter: #484f58; 74 + --dv-code-gutter-bg: #0d1117; 75 + --dv-code-highlight-line: rgba(88, 166, 255, 0.1); 72 76 } 73 77 74 78 /* --- Light Theme --- */ 75 - .docview[data-theme='light'] { 76 - --dv-bg: #ffffff; 77 - --dv-surface: #f6f8fa; 78 - --dv-surface-raised: #ffffff; 79 - --dv-border: #d0d7de; 80 - --dv-border-subtle: #e1e4e8; 81 - --dv-text: #1f2328; 82 - --dv-text-secondary: #656d76; 83 - --dv-text-muted: #8b949e; 84 - --dv-accent: #0969da; 85 - --dv-accent-hover: #0550ae; 86 - --dv-accent-subtle: rgba(9, 105, 218, 0.08); 87 - --dv-error: #cf222e; 88 - --dv-warning: #9a6700; 89 - --dv-page-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04); 90 - --dv-toolbar-bg: #f6f8fa; 91 - --dv-toolbar-border: #d0d7de; 92 - --dv-scrollbar-thumb: #c1c8cf; 93 - --dv-scrollbar-thumb-hover: #afb8c1; 94 - --dv-table-header-bg: #f6f8fa; 95 - --dv-table-row-hover: rgba(31, 35, 40, 0.04); 96 - --dv-table-stripe: rgba(31, 35, 40, 0.02); 97 - --dv-code-bg: #f6f8fa; 98 - --dv-code-gutter: #8b949e; 99 - --dv-code-gutter-bg: #f6f8fa; 100 - --dv-code-highlight-line: rgba(9, 105, 218, 0.08); 79 + .polyrender[data-theme="light"] { 80 + --dv-bg: #ffffff; 81 + --dv-surface: #f6f8fa; 82 + --dv-surface-raised: #ffffff; 83 + --dv-border: #d0d7de; 84 + --dv-border-subtle: #e1e4e8; 85 + --dv-text: #1f2328; 86 + --dv-text-secondary: #656d76; 87 + --dv-text-muted: #8b949e; 88 + --dv-accent: #0969da; 89 + --dv-accent-hover: #0550ae; 90 + --dv-accent-subtle: rgba(9, 105, 218, 0.08); 91 + --dv-error: #cf222e; 92 + --dv-warning: #9a6700; 93 + --dv-page-shadow: 94 + 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04); 95 + --dv-toolbar-bg: #f6f8fa; 96 + --dv-toolbar-border: #d0d7de; 97 + --dv-scrollbar-thumb: #c1c8cf; 98 + --dv-scrollbar-thumb-hover: #afb8c1; 99 + --dv-table-header-bg: #f6f8fa; 100 + --dv-table-row-hover: rgba(31, 35, 40, 0.04); 101 + --dv-table-stripe: rgba(31, 35, 40, 0.02); 102 + --dv-code-bg: #f6f8fa; 103 + --dv-code-gutter: #8b949e; 104 + --dv-code-gutter-bg: #f6f8fa; 105 + --dv-code-highlight-line: rgba(9, 105, 218, 0.08); 101 106 } 102 107 103 - 104 108 /* ========================================================================== 105 109 Root Container 106 110 ========================================================================== */ 107 111 108 - .docview { 109 - position: relative; 110 - display: flex; 111 - flex-direction: column; 112 - width: 100%; 113 - height: 100%; 114 - min-height: 200px; 115 - overflow: hidden; 116 - background: var(--dv-bg); 117 - color: var(--dv-text); 118 - font-family: var(--dv-font-sans); 119 - font-size: var(--dv-font-size); 120 - line-height: var(--dv-line-height); 121 - border-radius: var(--dv-radius); 122 - border: 1px solid var(--dv-border); 112 + .polyrender { 113 + position: relative; 114 + display: flex; 115 + flex-direction: column; 116 + width: 100%; 117 + height: 100%; 118 + min-height: 200px; 119 + overflow: hidden; 120 + background: var(--dv-bg); 121 + color: var(--dv-text); 122 + font-family: var(--dv-font-sans); 123 + font-size: var(--dv-font-size); 124 + line-height: var(--dv-line-height); 125 + border-radius: var(--dv-radius); 126 + border: 1px solid var(--dv-border); 123 127 } 124 - 125 128 126 129 /* ========================================================================== 127 130 Toolbar 128 131 ========================================================================== */ 129 132 130 133 .dv-toolbar { 131 - display: flex; 132 - align-items: center; 133 - gap: var(--dv-spacing-sm); 134 - height: var(--dv-toolbar-height); 135 - min-height: var(--dv-toolbar-height); 136 - padding: 0 var(--dv-spacing); 137 - background: var(--dv-toolbar-bg); 138 - border-bottom: 1px solid var(--dv-toolbar-border); 139 - user-select: none; 140 - flex-shrink: 0; 141 - z-index: 10; 134 + display: flex; 135 + align-items: center; 136 + gap: var(--dv-spacing-sm); 137 + height: var(--dv-toolbar-height); 138 + min-height: var(--dv-toolbar-height); 139 + padding: 0 var(--dv-spacing); 140 + background: var(--dv-toolbar-bg); 141 + border-bottom: 1px solid var(--dv-toolbar-border); 142 + user-select: none; 143 + flex-shrink: 0; 144 + z-index: 10; 142 145 } 143 146 144 - .dv-toolbar[data-position='bottom'] { 145 - border-bottom: none; 146 - border-top: 1px solid var(--dv-toolbar-border); 147 - order: 1; 147 + .dv-toolbar[data-position="bottom"] { 148 + border-bottom: none; 149 + border-top: 1px solid var(--dv-toolbar-border); 150 + order: 1; 148 151 } 149 152 150 153 .dv-toolbar-group { 151 - display: flex; 152 - align-items: center; 153 - gap: var(--dv-spacing-xs); 154 + display: flex; 155 + align-items: center; 156 + gap: var(--dv-spacing-xs); 154 157 } 155 158 156 159 .dv-toolbar-spacer { 157 - flex: 1; 160 + flex: 1; 158 161 } 159 162 160 163 .dv-toolbar-separator { 161 - width: 1px; 162 - height: 20px; 163 - background: var(--dv-border); 164 - margin: 0 var(--dv-spacing-xs); 164 + width: 1px; 165 + height: 20px; 166 + background: var(--dv-border); 167 + margin: 0 var(--dv-spacing-xs); 165 168 } 166 169 167 170 .dv-toolbar-btn { 168 - display: inline-flex; 169 - align-items: center; 170 - justify-content: center; 171 - width: 32px; 172 - height: 32px; 173 - padding: 0; 174 - border: none; 175 - border-radius: var(--dv-radius-sm); 176 - background: transparent; 177 - color: var(--dv-text-secondary); 178 - cursor: pointer; 179 - transition: background var(--dv-transition-fast), color var(--dv-transition-fast); 180 - flex-shrink: 0; 171 + display: inline-flex; 172 + align-items: center; 173 + justify-content: center; 174 + width: 32px; 175 + height: 32px; 176 + padding: 0; 177 + border: none; 178 + border-radius: var(--dv-radius-sm); 179 + background: transparent; 180 + color: var(--dv-text-secondary); 181 + cursor: pointer; 182 + transition: 183 + background var(--dv-transition-fast), 184 + color var(--dv-transition-fast); 185 + flex-shrink: 0; 181 186 } 182 187 183 188 .dv-toolbar-btn:hover { 184 - background: var(--dv-accent-subtle); 185 - color: var(--dv-text); 189 + background: var(--dv-accent-subtle); 190 + color: var(--dv-text); 186 191 } 187 192 188 193 .dv-toolbar-btn:active { 189 - background: var(--dv-border); 194 + background: var(--dv-border); 190 195 } 191 196 192 197 .dv-toolbar-btn[disabled] { 193 - opacity: 0.35; 194 - cursor: default; 195 - pointer-events: none; 198 + opacity: 0.35; 199 + cursor: default; 200 + pointer-events: none; 196 201 } 197 202 198 203 .dv-toolbar-btn svg { 199 - width: 16px; 200 - height: 16px; 204 + width: 16px; 205 + height: 16px; 201 206 } 202 207 203 208 .dv-toolbar-label { 204 - font-size: var(--dv-font-size-sm); 205 - color: var(--dv-text-secondary); 206 - white-space: nowrap; 207 - overflow: hidden; 208 - text-overflow: ellipsis; 209 - max-width: 200px; 209 + font-size: var(--dv-font-size-sm); 210 + color: var(--dv-text-secondary); 211 + white-space: nowrap; 212 + overflow: hidden; 213 + text-overflow: ellipsis; 214 + max-width: 200px; 210 215 } 211 216 212 217 .dv-page-input { 213 - width: 48px; 214 - height: 28px; 215 - padding: 0 var(--dv-spacing-xs); 216 - border: 1px solid var(--dv-border); 217 - border-radius: var(--dv-radius-sm); 218 - background: var(--dv-bg); 219 - color: var(--dv-text); 220 - font-family: var(--dv-font-mono); 221 - font-size: var(--dv-font-size-sm); 222 - text-align: center; 223 - outline: none; 218 + width: 48px; 219 + height: 28px; 220 + padding: 0 var(--dv-spacing-xs); 221 + border: 1px solid var(--dv-border); 222 + border-radius: var(--dv-radius-sm); 223 + background: var(--dv-bg); 224 + color: var(--dv-text); 225 + font-family: var(--dv-font-mono); 226 + font-size: var(--dv-font-size-sm); 227 + text-align: center; 228 + outline: none; 224 229 } 225 230 226 231 .dv-page-input:focus { 227 - border-color: var(--dv-accent); 228 - box-shadow: 0 0 0 2px var(--dv-accent-subtle); 232 + border-color: var(--dv-accent); 233 + box-shadow: 0 0 0 2px var(--dv-accent-subtle); 229 234 } 230 - 231 235 232 236 /* ========================================================================== 233 237 Viewport (Scrollable Document Area) 234 238 ========================================================================== */ 235 239 236 240 .dv-viewport { 237 - flex: 1; 238 - overflow: auto; 239 - position: relative; 240 - background: var(--dv-bg); 241 + flex: 1; 242 + overflow: auto; 243 + position: relative; 244 + background: var(--dv-bg); 241 245 } 242 246 243 247 /* Custom scrollbar */ 244 248 .dv-viewport::-webkit-scrollbar { 245 - width: var(--dv-scrollbar-size); 246 - height: var(--dv-scrollbar-size); 249 + width: var(--dv-scrollbar-size); 250 + height: var(--dv-scrollbar-size); 247 251 } 248 252 249 253 .dv-viewport::-webkit-scrollbar-thumb { 250 - background: var(--dv-scrollbar-thumb); 251 - border-radius: 999px; 254 + background: var(--dv-scrollbar-thumb); 255 + border-radius: 999px; 252 256 } 253 257 254 258 .dv-viewport::-webkit-scrollbar-thumb:hover { 255 - background: var(--dv-scrollbar-thumb-hover); 259 + background: var(--dv-scrollbar-thumb-hover); 256 260 } 257 261 258 262 .dv-viewport::-webkit-scrollbar-track { 259 - background: var(--dv-scrollbar-track); 263 + background: var(--dv-scrollbar-track); 260 264 } 261 265 262 266 .dv-viewport { 263 - scrollbar-width: thin; 264 - scrollbar-color: var(--dv-scrollbar-thumb) var(--dv-scrollbar-track); 267 + scrollbar-width: thin; 268 + scrollbar-color: var(--dv-scrollbar-thumb) var(--dv-scrollbar-track); 265 269 } 266 270 267 - 268 271 /* ========================================================================== 269 272 Page Container (for paginated renderers: PDF, images, etc.) 270 273 ========================================================================== */ 271 274 272 275 .dv-pages { 273 - display: flex; 274 - flex-direction: column; 275 - align-items: center; 276 - padding: var(--dv-spacing-lg); 277 - gap: var(--dv-page-gap); 278 - min-height: 100%; 276 + display: flex; 277 + flex-direction: column; 278 + align-items: center; 279 + padding: var(--dv-spacing-lg); 280 + gap: var(--dv-page-gap); 281 + min-height: 100%; 279 282 } 280 283 281 284 .dv-page { 282 - position: relative; 283 - background: var(--dv-page-bg); 284 - box-shadow: var(--dv-page-shadow); 285 - border-radius: 2px; 286 - overflow: hidden; 287 - flex-shrink: 0; 285 + position: relative; 286 + background: var(--dv-page-bg); 287 + box-shadow: var(--dv-page-shadow); 288 + border-radius: 2px; 289 + overflow: hidden; 290 + flex-shrink: 0; 288 291 } 289 292 290 293 .dv-page canvas { 291 - display: block; 294 + display: block; 292 295 } 293 296 294 297 .dv-page img { 295 - display: block; 296 - width: 100%; 297 - height: auto; 298 + display: block; 299 + width: 100%; 300 + height: auto; 298 301 } 299 302 300 303 /* Text layer overlay for copy/paste over rendered pages */ 301 304 .dv-text-layer { 302 - position: absolute; 303 - top: 0; 304 - left: 0; 305 - right: 0; 306 - bottom: 0; 307 - overflow: hidden; 308 - opacity: 0.25; 309 - line-height: 1; 310 - pointer-events: all; 305 + position: absolute; 306 + top: 0; 307 + left: 0; 308 + right: 0; 309 + bottom: 0; 310 + overflow: hidden; 311 + opacity: 0.25; 312 + line-height: 1; 313 + pointer-events: all; 311 314 } 312 315 313 316 .dv-text-layer span { 314 - position: absolute; 315 - white-space: pre; 316 - color: transparent; 317 - cursor: text; 317 + position: absolute; 318 + white-space: pre; 319 + color: transparent; 320 + cursor: text; 318 321 } 319 322 320 323 .dv-text-layer span::selection { 321 - background: var(--dv-accent); 322 - color: transparent; 323 - opacity: 0.4; 324 + background: var(--dv-accent); 325 + color: transparent; 326 + opacity: 0.4; 324 327 } 325 - 326 328 327 329 /* ========================================================================== 328 330 Loading & Error States 329 331 ========================================================================== */ 330 332 331 333 .dv-loading { 332 - display: flex; 333 - flex-direction: column; 334 - align-items: center; 335 - justify-content: center; 336 - gap: var(--dv-spacing); 337 - padding: var(--dv-spacing-xl); 338 - min-height: 200px; 339 - color: var(--dv-text-secondary); 340 - font-size: var(--dv-font-size); 334 + display: flex; 335 + flex-direction: column; 336 + align-items: center; 337 + justify-content: center; 338 + gap: var(--dv-spacing); 339 + padding: var(--dv-spacing-xl); 340 + min-height: 200px; 341 + color: var(--dv-text-secondary); 342 + font-size: var(--dv-font-size); 341 343 } 342 344 343 345 .dv-spinner { 344 - width: 24px; 345 - height: 24px; 346 - border: 2px solid var(--dv-border); 347 - border-top-color: var(--dv-accent); 348 - border-radius: 50%; 349 - animation: dv-spin 0.6s linear infinite; 346 + width: 24px; 347 + height: 24px; 348 + border: 2px solid var(--dv-border); 349 + border-top-color: var(--dv-accent); 350 + border-radius: 50%; 351 + animation: dv-spin 0.6s linear infinite; 350 352 } 351 353 352 354 @keyframes dv-spin { 353 - to { transform: rotate(360deg); } 355 + to { 356 + transform: rotate(360deg); 357 + } 354 358 } 355 359 356 360 .dv-error { 357 - display: flex; 358 - flex-direction: column; 359 - align-items: center; 360 - justify-content: center; 361 - gap: var(--dv-spacing-sm); 362 - padding: var(--dv-spacing-xl); 363 - min-height: 200px; 364 - color: var(--dv-error); 365 - text-align: center; 361 + display: flex; 362 + flex-direction: column; 363 + align-items: center; 364 + justify-content: center; 365 + gap: var(--dv-spacing-sm); 366 + padding: var(--dv-spacing-xl); 367 + min-height: 200px; 368 + color: var(--dv-error); 369 + text-align: center; 366 370 } 367 371 368 372 .dv-error-code { 369 - font-family: var(--dv-font-mono); 370 - font-size: var(--dv-font-size-sm); 371 - color: var(--dv-text-muted); 373 + font-family: var(--dv-font-mono); 374 + font-size: var(--dv-font-size-sm); 375 + color: var(--dv-text-muted); 372 376 } 373 377 374 378 .dv-error-message { 375 - font-size: var(--dv-font-size); 376 - max-width: 400px; 379 + font-size: var(--dv-font-size); 380 + max-width: 400px; 377 381 } 378 - 379 382 380 383 /* ========================================================================== 381 384 Table Renderer (CSV/TSV) 382 385 ========================================================================== */ 383 386 384 387 .dv-table-container { 385 - overflow: auto; 386 - flex: 1; 387 - font-size: var(--dv-font-size-sm); 388 + overflow: auto; 389 + flex: 1; 390 + font-size: var(--dv-font-size-sm); 388 391 } 389 392 390 393 .dv-table { 391 - width: 100%; 392 - border-collapse: collapse; 393 - border-spacing: 0; 394 - font-family: var(--dv-font-mono); 395 - font-size: var(--dv-font-size-sm); 394 + width: 100%; 395 + border-collapse: collapse; 396 + border-spacing: 0; 397 + font-family: var(--dv-font-mono); 398 + font-size: var(--dv-font-size-sm); 396 399 } 397 400 398 401 .dv-table thead { 399 - position: sticky; 400 - top: 0; 401 - z-index: 5; 402 + position: sticky; 403 + top: 0; 404 + z-index: 5; 402 405 } 403 406 404 407 .dv-table th { 405 - background: var(--dv-table-header-bg); 406 - border-bottom: 2px solid var(--dv-border); 407 - padding: var(--dv-spacing-sm) var(--dv-spacing); 408 - text-align: left; 409 - font-weight: 600; 410 - color: var(--dv-text); 411 - white-space: nowrap; 412 - cursor: default; 413 - user-select: none; 408 + background: var(--dv-table-header-bg); 409 + border-bottom: 2px solid var(--dv-border); 410 + padding: var(--dv-spacing-sm) var(--dv-spacing); 411 + text-align: left; 412 + font-weight: 600; 413 + color: var(--dv-text); 414 + white-space: nowrap; 415 + cursor: default; 416 + user-select: none; 414 417 } 415 418 416 - .dv-table th[data-sortable='true'] { 417 - cursor: pointer; 419 + .dv-table th[data-sortable="true"] { 420 + cursor: pointer; 418 421 } 419 422 420 - .dv-table th[data-sortable='true']:hover { 421 - background: var(--dv-surface-raised); 423 + .dv-table th[data-sortable="true"]:hover { 424 + background: var(--dv-surface-raised); 422 425 } 423 426 424 427 .dv-table td { 425 - padding: var(--dv-spacing-xs) var(--dv-spacing); 426 - border-bottom: 1px solid var(--dv-border-subtle); 427 - color: var(--dv-text); 428 - max-width: 300px; 429 - overflow: hidden; 430 - text-overflow: ellipsis; 431 - white-space: nowrap; 428 + padding: var(--dv-spacing-xs) var(--dv-spacing); 429 + border-bottom: 1px solid var(--dv-border-subtle); 430 + color: var(--dv-text); 431 + max-width: 300px; 432 + overflow: hidden; 433 + text-overflow: ellipsis; 434 + white-space: nowrap; 432 435 } 433 436 434 437 .dv-table tbody tr:nth-child(even) { 435 - background: var(--dv-table-stripe); 438 + background: var(--dv-table-stripe); 436 439 } 437 440 438 441 .dv-table tbody tr:hover { 439 - background: var(--dv-table-row-hover); 442 + background: var(--dv-table-row-hover); 440 443 } 441 444 442 445 .dv-table-row-number { 443 - color: var(--dv-text-muted); 444 - min-width: 48px; 445 - text-align: right; 446 - padding-right: var(--dv-spacing) !important; 447 - user-select: none; 446 + color: var(--dv-text-muted); 447 + min-width: 48px; 448 + text-align: right; 449 + padding-right: var(--dv-spacing) !important; 450 + user-select: none; 448 451 } 449 - 450 452 451 453 /* ========================================================================== 452 454 Code Renderer 453 455 ========================================================================== */ 454 456 455 457 .dv-code-container { 456 - display: flex; 457 - flex: 1; 458 - overflow: auto; 459 - font-family: var(--dv-font-mono); 460 - font-size: var(--dv-font-size); 461 - line-height: 1.6; 462 - background: var(--dv-code-bg); 463 - tab-size: 2; 458 + display: flex; 459 + flex: 1; 460 + overflow: auto; 461 + font-family: var(--dv-font-mono); 462 + font-size: var(--dv-font-size); 463 + line-height: 1.6; 464 + background: var(--dv-code-bg); 465 + tab-size: 2; 464 466 } 465 467 466 468 .dv-code-gutter { 467 - position: sticky; 468 - left: 0; 469 - display: flex; 470 - flex-direction: column; 471 - padding: var(--dv-spacing) 0; 472 - text-align: right; 473 - color: var(--dv-code-gutter); 474 - background: var(--dv-code-gutter-bg); 475 - user-select: none; 476 - flex-shrink: 0; 477 - border-right: 1px solid var(--dv-border-subtle); 478 - z-index: 1; 469 + position: sticky; 470 + left: 0; 471 + display: flex; 472 + flex-direction: column; 473 + padding: var(--dv-spacing) 0; 474 + text-align: right; 475 + color: var(--dv-code-gutter); 476 + background: var(--dv-code-gutter-bg); 477 + user-select: none; 478 + flex-shrink: 0; 479 + border-right: 1px solid var(--dv-border-subtle); 480 + z-index: 1; 479 481 } 480 482 481 483 .dv-code-gutter-line { 482 - padding: 0 var(--dv-spacing) 0 var(--dv-spacing-lg); 483 - min-width: 48px; 484 + padding: 0 var(--dv-spacing) 0 var(--dv-spacing-lg); 485 + min-width: 48px; 484 486 } 485 487 486 488 .dv-code-body { 487 - flex: 1; 488 - padding: var(--dv-spacing); 489 - overflow-x: auto; 490 - white-space: pre; 489 + flex: 1; 490 + padding: var(--dv-spacing); 491 + overflow-x: auto; 492 + white-space: pre; 491 493 } 492 494 493 495 .dv-code-body.dv-word-wrap { 494 - white-space: pre-wrap; 495 - word-break: break-all; 496 + white-space: pre-wrap; 497 + word-break: break-all; 496 498 } 497 499 498 500 /* --- Built-in Syntax Highlighting (Dark) --- */ 499 - .docview .dv-code-body .hljs-keyword, 500 - .docview .dv-code-body .hljs-selector-tag, 501 - .docview .dv-code-body .hljs-built_in, 502 - .docview .dv-code-body .hljs-type { color: #ff7b72; } 501 + .polyrender .dv-code-body .hljs-keyword, 502 + .polyrender .dv-code-body .hljs-selector-tag, 503 + .polyrender .dv-code-body .hljs-built_in, 504 + .polyrender .dv-code-body .hljs-type { 505 + color: #ff7b72; 506 + } 503 507 504 - .docview .dv-code-body .hljs-string, 505 - .docview .dv-code-body .hljs-addition { color: #a5d6ff; } 508 + .polyrender .dv-code-body .hljs-string, 509 + .polyrender .dv-code-body .hljs-addition { 510 + color: #a5d6ff; 511 + } 506 512 507 - .docview .dv-code-body .hljs-number, 508 - .docview .dv-code-body .hljs-literal { color: #79c0ff; } 513 + .polyrender .dv-code-body .hljs-number, 514 + .polyrender .dv-code-body .hljs-literal { 515 + color: #79c0ff; 516 + } 509 517 510 - .docview .dv-code-body .hljs-comment, 511 - .docview .dv-code-body .hljs-quote { color: #8b949e; font-style: italic; } 518 + .polyrender .dv-code-body .hljs-comment, 519 + .polyrender .dv-code-body .hljs-quote { 520 + color: #8b949e; 521 + font-style: italic; 522 + } 512 523 513 - .docview .dv-code-body .hljs-function, 514 - .docview .dv-code-body .hljs-title { color: #d2a8ff; } 524 + .polyrender .dv-code-body .hljs-function, 525 + .polyrender .dv-code-body .hljs-title { 526 + color: #d2a8ff; 527 + } 515 528 516 - .docview .dv-code-body .hljs-title.function_ { color: #d2a8ff; } 517 - .docview .dv-code-body .hljs-title.class_ { color: #f0883e; } 529 + .polyrender .dv-code-body .hljs-title.function_ { 530 + color: #d2a8ff; 531 + } 532 + .polyrender .dv-code-body .hljs-title.class_ { 533 + color: #f0883e; 534 + } 518 535 519 - .docview .dv-code-body .hljs-variable, 520 - .docview .dv-code-body .hljs-template-variable { color: #ffa657; } 536 + .polyrender .dv-code-body .hljs-variable, 537 + .polyrender .dv-code-body .hljs-template-variable { 538 + color: #ffa657; 539 + } 521 540 522 - .docview .dv-code-body .hljs-attr, 523 - .docview .dv-code-body .hljs-attribute { color: #79c0ff; } 541 + .polyrender .dv-code-body .hljs-attr, 542 + .polyrender .dv-code-body .hljs-attribute { 543 + color: #79c0ff; 544 + } 524 545 525 - .docview .dv-code-body .hljs-tag { color: #7ee787; } 546 + .polyrender .dv-code-body .hljs-tag { 547 + color: #7ee787; 548 + } 526 549 527 - .docview .dv-code-body .hljs-name { color: #7ee787; } 550 + .polyrender .dv-code-body .hljs-name { 551 + color: #7ee787; 552 + } 528 553 529 - .docview .dv-code-body .hljs-regexp { color: #a5d6ff; } 554 + .polyrender .dv-code-body .hljs-regexp { 555 + color: #a5d6ff; 556 + } 530 557 531 - .docview .dv-code-body .hljs-symbol, 532 - .docview .dv-code-body .hljs-bullet { color: #ffa657; } 558 + .polyrender .dv-code-body .hljs-symbol, 559 + .polyrender .dv-code-body .hljs-bullet { 560 + color: #ffa657; 561 + } 533 562 534 - .docview .dv-code-body .hljs-meta, 535 - .docview .dv-code-body .hljs-meta .hljs-keyword { color: #79c0ff; } 563 + .polyrender .dv-code-body .hljs-meta, 564 + .polyrender .dv-code-body .hljs-meta .hljs-keyword { 565 + color: #79c0ff; 566 + } 536 567 537 - .docview .dv-code-body .hljs-deletion { color: #ffa198; background: rgba(255, 129, 130, 0.1); } 538 - .docview .dv-code-body .hljs-addition { background: rgba(63, 185, 80, 0.1); } 568 + .polyrender .dv-code-body .hljs-deletion { 569 + color: #ffa198; 570 + background: rgba(255, 129, 130, 0.1); 571 + } 572 + .polyrender .dv-code-body .hljs-addition { 573 + background: rgba(63, 185, 80, 0.1); 574 + } 539 575 540 - .docview .dv-code-body .hljs-params { color: #e6edf3; } 576 + .polyrender .dv-code-body .hljs-params { 577 + color: #e6edf3; 578 + } 541 579 542 - .docview .dv-code-body .hljs-operator { color: #ff7b72; } 580 + .polyrender .dv-code-body .hljs-operator { 581 + color: #ff7b72; 582 + } 543 583 544 - .docview .dv-code-body .hljs-property { color: #79c0ff; } 584 + .polyrender .dv-code-body .hljs-property { 585 + color: #79c0ff; 586 + } 545 587 546 - .docview .dv-code-body .hljs-punctuation { color: #8b949e; } 588 + .polyrender .dv-code-body .hljs-punctuation { 589 + color: #8b949e; 590 + } 547 591 548 - .docview .dv-code-body .hljs-emphasis { font-style: italic; } 549 - .docview .dv-code-body .hljs-strong { font-weight: bold; } 592 + .polyrender .dv-code-body .hljs-emphasis { 593 + font-style: italic; 594 + } 595 + .polyrender .dv-code-body .hljs-strong { 596 + font-weight: bold; 597 + } 550 598 551 - .docview .dv-code-body .hljs-section { color: #79c0ff; font-weight: bold; } 599 + .polyrender .dv-code-body .hljs-section { 600 + color: #79c0ff; 601 + font-weight: bold; 602 + } 552 603 553 - .docview .dv-code-body .hljs-link { color: #58a6ff; text-decoration: underline; } 604 + .polyrender .dv-code-body .hljs-link { 605 + color: #58a6ff; 606 + text-decoration: underline; 607 + } 554 608 555 609 /* --- Syntax Highlighting (Light Override) --- */ 556 - .docview[data-theme='light'] .dv-code-body .hljs-keyword, 557 - .docview[data-theme='light'] .dv-code-body .hljs-selector-tag, 558 - .docview[data-theme='light'] .dv-code-body .hljs-built_in, 559 - .docview[data-theme='light'] .dv-code-body .hljs-type { color: #cf222e; } 610 + .polyrender[data-theme="light"] .dv-code-body .hljs-keyword, 611 + .polyrender[data-theme="light"] .dv-code-body .hljs-selector-tag, 612 + .polyrender[data-theme="light"] .dv-code-body .hljs-built_in, 613 + .polyrender[data-theme="light"] .dv-code-body .hljs-type { 614 + color: #cf222e; 615 + } 560 616 561 - .docview[data-theme='light'] .dv-code-body .hljs-string, 562 - .docview[data-theme='light'] .dv-code-body .hljs-addition { color: #0a3069; } 617 + .polyrender[data-theme="light"] .dv-code-body .hljs-string, 618 + .polyrender[data-theme="light"] .dv-code-body .hljs-addition { 619 + color: #0a3069; 620 + } 563 621 564 - .docview[data-theme='light'] .dv-code-body .hljs-number, 565 - .docview[data-theme='light'] .dv-code-body .hljs-literal { color: #0550ae; } 622 + .polyrender[data-theme="light"] .dv-code-body .hljs-number, 623 + .polyrender[data-theme="light"] .dv-code-body .hljs-literal { 624 + color: #0550ae; 625 + } 566 626 567 - .docview[data-theme='light'] .dv-code-body .hljs-comment, 568 - .docview[data-theme='light'] .dv-code-body .hljs-quote { color: #6e7781; font-style: italic; } 627 + .polyrender[data-theme="light"] .dv-code-body .hljs-comment, 628 + .polyrender[data-theme="light"] .dv-code-body .hljs-quote { 629 + color: #6e7781; 630 + font-style: italic; 631 + } 569 632 570 - .docview[data-theme='light'] .dv-code-body .hljs-function, 571 - .docview[data-theme='light'] .dv-code-body .hljs-title { color: #8250df; } 633 + .polyrender[data-theme="light"] .dv-code-body .hljs-function, 634 + .polyrender[data-theme="light"] .dv-code-body .hljs-title { 635 + color: #8250df; 636 + } 572 637 573 - .docview[data-theme='light'] .dv-code-body .hljs-title.function_ { color: #8250df; } 574 - .docview[data-theme='light'] .dv-code-body .hljs-title.class_ { color: #953800; } 638 + .polyrender[data-theme="light"] .dv-code-body .hljs-title.function_ { 639 + color: #8250df; 640 + } 641 + .polyrender[data-theme="light"] .dv-code-body .hljs-title.class_ { 642 + color: #953800; 643 + } 575 644 576 - .docview[data-theme='light'] .dv-code-body .hljs-variable, 577 - .docview[data-theme='light'] .dv-code-body .hljs-template-variable { color: #953800; } 645 + .polyrender[data-theme="light"] .dv-code-body .hljs-variable, 646 + .polyrender[data-theme="light"] .dv-code-body .hljs-template-variable { 647 + color: #953800; 648 + } 578 649 579 - .docview[data-theme='light'] .dv-code-body .hljs-attr, 580 - .docview[data-theme='light'] .dv-code-body .hljs-attribute { color: #0550ae; } 650 + .polyrender[data-theme="light"] .dv-code-body .hljs-attr, 651 + .polyrender[data-theme="light"] .dv-code-body .hljs-attribute { 652 + color: #0550ae; 653 + } 581 654 582 - .docview[data-theme='light'] .dv-code-body .hljs-tag { color: #116329; } 583 - .docview[data-theme='light'] .dv-code-body .hljs-name { color: #116329; } 655 + .polyrender[data-theme="light"] .dv-code-body .hljs-tag { 656 + color: #116329; 657 + } 658 + .polyrender[data-theme="light"] .dv-code-body .hljs-name { 659 + color: #116329; 660 + } 584 661 585 - .docview[data-theme='light'] .dv-code-body .hljs-regexp { color: #0a3069; } 662 + .polyrender[data-theme="light"] .dv-code-body .hljs-regexp { 663 + color: #0a3069; 664 + } 586 665 587 - .docview[data-theme='light'] .dv-code-body .hljs-symbol, 588 - .docview[data-theme='light'] .dv-code-body .hljs-bullet { color: #953800; } 666 + .polyrender[data-theme="light"] .dv-code-body .hljs-symbol, 667 + .polyrender[data-theme="light"] .dv-code-body .hljs-bullet { 668 + color: #953800; 669 + } 589 670 590 - .docview[data-theme='light'] .dv-code-body .hljs-meta, 591 - .docview[data-theme='light'] .dv-code-body .hljs-meta .hljs-keyword { color: #0550ae; } 671 + .polyrender[data-theme="light"] .dv-code-body .hljs-meta, 672 + .polyrender[data-theme="light"] .dv-code-body .hljs-meta .hljs-keyword { 673 + color: #0550ae; 674 + } 592 675 593 - .docview[data-theme='light'] .dv-code-body .hljs-deletion { color: #82071e; background: rgba(255, 129, 130, 0.1); } 594 - .docview[data-theme='light'] .dv-code-body .hljs-addition { background: rgba(63, 185, 80, 0.1); } 595 - 596 - .docview[data-theme='light'] .dv-code-body .hljs-params { color: #1f2328; } 597 - .docview[data-theme='light'] .dv-code-body .hljs-operator { color: #cf222e; } 598 - .docview[data-theme='light'] .dv-code-body .hljs-property { color: #0550ae; } 599 - .docview[data-theme='light'] .dv-code-body .hljs-punctuation { color: #6e7781; } 600 - .docview[data-theme='light'] .dv-code-body .hljs-section { color: #0550ae; font-weight: bold; } 601 - .docview[data-theme='light'] .dv-code-body .hljs-link { color: #0969da; text-decoration: underline; } 676 + .polyrender[data-theme="light"] .dv-code-body .hljs-deletion { 677 + color: #82071e; 678 + background: rgba(255, 129, 130, 0.1); 679 + } 680 + .polyrender[data-theme="light"] .dv-code-body .hljs-addition { 681 + background: rgba(63, 185, 80, 0.1); 682 + } 602 683 684 + .polyrender[data-theme="light"] .dv-code-body .hljs-params { 685 + color: #1f2328; 686 + } 687 + .polyrender[data-theme="light"] .dv-code-body .hljs-operator { 688 + color: #cf222e; 689 + } 690 + .polyrender[data-theme="light"] .dv-code-body .hljs-property { 691 + color: #0550ae; 692 + } 693 + .polyrender[data-theme="light"] .dv-code-body .hljs-punctuation { 694 + color: #6e7781; 695 + } 696 + .polyrender[data-theme="light"] .dv-code-body .hljs-section { 697 + color: #0550ae; 698 + font-weight: bold; 699 + } 700 + .polyrender[data-theme="light"] .dv-code-body .hljs-link { 701 + color: #0969da; 702 + text-decoration: underline; 703 + } 603 704 604 705 /* ========================================================================== 605 706 Text Renderer 606 707 ========================================================================== */ 607 708 608 709 .dv-text-container { 609 - flex: 1; 610 - overflow: auto; 611 - padding: var(--dv-spacing-lg); 612 - font-family: var(--dv-font-sans); 613 - font-size: var(--dv-font-size-lg); 614 - line-height: 1.7; 615 - color: var(--dv-text); 616 - white-space: pre-wrap; 617 - word-wrap: break-word; 618 - max-width: 72ch; 619 - margin: 0 auto; 710 + flex: 1; 711 + overflow: auto; 712 + padding: var(--dv-spacing-lg); 713 + font-family: var(--dv-font-sans); 714 + font-size: var(--dv-font-size-lg); 715 + line-height: 1.7; 716 + color: var(--dv-text); 717 + white-space: pre-wrap; 718 + word-wrap: break-word; 719 + max-width: 72ch; 720 + margin: 0 auto; 620 721 } 621 722 622 723 .dv-text-container.dv-monospace { 623 - font-family: var(--dv-font-mono); 624 - font-size: var(--dv-font-size); 625 - max-width: none; 724 + font-family: var(--dv-font-mono); 725 + font-size: var(--dv-font-size); 726 + max-width: none; 626 727 } 627 - 628 728 629 729 /* ========================================================================== 630 730 EPUB Renderer 631 731 ========================================================================== */ 632 732 633 733 .dv-epub-container { 634 - flex: 1; 635 - overflow: hidden; 636 - position: relative; 734 + flex: 1; 735 + overflow: hidden; 736 + position: relative; 637 737 } 638 738 639 739 .dv-epub-container iframe { 640 - border: none; 641 - width: 100%; 642 - height: 100%; 740 + border: none; 741 + width: 100%; 742 + height: 100%; 643 743 } 644 - 645 744 646 745 /* ========================================================================== 647 746 DOCX Renderer 648 747 ========================================================================== */ 649 748 650 749 .dv-docx-container { 651 - flex: 1; 652 - overflow: auto; 653 - padding: var(--dv-spacing-lg); 654 - background: var(--dv-bg); 750 + flex: 1; 751 + overflow: auto; 752 + padding: var(--dv-spacing-lg); 753 + background: var(--dv-bg); 655 754 } 656 755 657 756 /* docx-preview renders into a wrapper div — scope some resets */ 658 757 .dv-docx-container .docx-wrapper { 659 - background: transparent !important; 660 - padding: 0 !important; 758 + background: transparent !important; 759 + padding: 0 !important; 661 760 } 662 761 663 762 .dv-docx-container .docx-wrapper > section.docx { 664 - background: var(--dv-page-bg) !important; 665 - box-shadow: var(--dv-page-shadow) !important; 666 - margin: 0 auto var(--dv-page-gap) !important; 667 - border-radius: 2px; 763 + background: var(--dv-page-bg) !important; 764 + box-shadow: var(--dv-page-shadow) !important; 765 + margin: 0 auto var(--dv-page-gap) !important; 766 + border-radius: 2px; 668 767 } 669 - 670 768 671 769 /* ========================================================================== 672 770 Page Image Renderer (browse pages / pre-rendered) 673 771 ========================================================================== */ 674 772 675 773 .dv-browse-page { 676 - position: relative; 677 - display: flex; 678 - align-items: center; 679 - justify-content: center; 774 + position: relative; 775 + display: flex; 776 + align-items: center; 777 + justify-content: center; 680 778 } 681 779 682 780 .dv-browse-page img { 683 - display: block; 684 - max-width: 100%; 685 - height: auto; 686 - background: var(--dv-page-bg); 687 - box-shadow: var(--dv-page-shadow); 688 - border-radius: 2px; 781 + display: block; 782 + max-width: 100%; 783 + height: auto; 784 + background: var(--dv-page-bg); 785 + box-shadow: var(--dv-page-shadow); 786 + border-radius: 2px; 689 787 } 690 788 691 789 .dv-browse-page-placeholder { 692 - display: flex; 693 - align-items: center; 694 - justify-content: center; 695 - background: var(--dv-surface); 696 - border-radius: 2px; 697 - color: var(--dv-text-muted); 698 - font-size: var(--dv-font-size-sm); 790 + display: flex; 791 + align-items: center; 792 + justify-content: center; 793 + background: var(--dv-surface); 794 + border-radius: 2px; 795 + color: var(--dv-text-muted); 796 + font-size: var(--dv-font-size-sm); 699 797 }
+4 -4
packages/core/src/toolbar.ts
··· 1 - import type { ToolbarConfig, DocViewState } from './types.js' 1 + import type { ToolbarConfig, PolyRenderState } from './types.js' 2 2 import { el, svgIcon, icons } from './utils.js' 3 3 4 4 export interface ToolbarActions { ··· 16 16 /** Root toolbar element. */ 17 17 element: HTMLElement 18 18 /** Update displayed state (page, total, zoom). */ 19 - updateState(state: DocViewState): void 19 + updateState(state: PolyRenderState): void 20 20 /** Destroy and clean up listeners. */ 21 21 destroy(): void 22 22 } ··· 28 28 export function createToolbar( 29 29 config: ToolbarConfig, 30 30 actions: ToolbarActions, 31 - initialState: DocViewState, 31 + initialState: PolyRenderState, 32 32 ): ToolbarHandle { 33 33 const toolbar = el('div', 'dv-toolbar') 34 34 if (config.position === 'bottom') { ··· 136 136 137 137 return { 138 138 element: toolbar, 139 - updateState(state: DocViewState) { 139 + updateState(state: PolyRenderState) { 140 140 if (pageInput) pageInput.value = String(state.currentPage) 141 141 if (pageLabel) pageLabel.textContent = `/ ${state.totalPages}` 142 142 if (zoomLabel) zoomLabel.textContent = `${Math.round(state.zoom * 100)}%`
+19 -19
packages/core/src/types.ts
··· 1 1 // --------------------------------------------------------------------------- 2 - // Document Sources — what consumers pass in to tell DocView what to render 2 + // Document Sources — what consumers pass in to tell PolyRender what to render 3 3 // --------------------------------------------------------------------------- 4 4 5 5 /** A direct file provided as binary data. */ ··· 151 151 152 152 153 153 // --------------------------------------------------------------------------- 154 - // Options — configuration for the DocView instance 154 + // Options — configuration for the PolyRender instance 155 155 // --------------------------------------------------------------------------- 156 156 157 - export interface DocViewOptions { 157 + export interface PolyRenderOptions { 158 158 /** The document to render. */ 159 159 source: DocumentSource 160 160 /** Explicit format override. If omitted, auto-detected from source. */ ··· 180 180 /** Called when the visible page changes. */ 181 181 onPageChange?: (page: number, totalPages: number) => void 182 182 /** Called on unrecoverable errors. */ 183 - onError?: (error: DocViewError) => void 183 + onError?: (error: PolyRenderError) => void 184 184 /** Called when zoom level changes. */ 185 185 onZoomChange?: (zoom: number) => void 186 186 /** Called when loading state changes. */ ··· 290 290 fileSize?: number 291 291 } 292 292 293 - /** Current viewer state, queryable from a DocView instance. */ 294 - export interface DocViewState { 293 + /** Current viewer state, queryable from a PolyRender instance. */ 294 + export interface PolyRenderState { 295 295 /** Whether the document is currently loading. */ 296 296 loading: boolean 297 297 /** Current error, if any. */ 298 - error: DocViewError | null 298 + error: PolyRenderError | null 299 299 /** Current 1-indexed page number. */ 300 300 currentPage: number 301 301 /** Total page count. */ ··· 311 311 // Errors 312 312 // --------------------------------------------------------------------------- 313 313 314 - export type DocViewErrorCode = 314 + export type PolyRenderErrorCode = 315 315 | 'FORMAT_UNSUPPORTED' 316 316 | 'FORMAT_DETECTION_FAILED' 317 317 | 'PEER_DEPENDENCY_MISSING' ··· 321 321 | 'PAGE_OUT_OF_RANGE' 322 322 | 'UNKNOWN' 323 323 324 - export class DocViewError extends Error { 325 - code: DocViewErrorCode 324 + export class PolyRenderError extends Error { 325 + code: PolyRenderErrorCode 326 326 detail?: unknown 327 327 328 - constructor(code: DocViewErrorCode, message: string, detail?: unknown) { 328 + constructor(code: PolyRenderErrorCode, message: string, detail?: unknown) { 329 329 super(message) 330 - this.name = 'DocViewError' 330 + this.name = 'PolyRenderError' 331 331 this.code = code 332 332 this.detail = detail 333 333 } ··· 363 363 364 364 /** 365 365 * The contract every format renderer must fulfill. Each renderer manages 366 - * its own DOM subtree within the provided container element. The DocView 366 + * its own DOM subtree within the provided container element. The PolyRender 367 367 * orchestrator calls these methods in response to user actions and source 368 368 * changes. 369 369 */ ··· 376 376 * Called once after the renderer is created. The container is an empty div 377 377 * scoped to this renderer — the renderer owns its entire DOM subtree. 378 378 */ 379 - mount(container: HTMLElement, options: DocViewOptions): Promise<void> 379 + mount(container: HTMLElement, options: PolyRenderOptions): Promise<void> 380 380 381 381 /** 382 382 * React to changed options (theme, zoom, etc.) without full re-mount. 383 383 * Only the changed fields will be present. 384 384 */ 385 - update(changed: Partial<DocViewOptions>): Promise<void> 385 + update(changed: Partial<PolyRenderOptions>): Promise<void> 386 386 387 387 /** Navigate to a specific page (1-indexed). */ 388 388 goToPage(page: number): void ··· 411 411 412 412 /** 413 413 * Factory function that creates a renderer instance. Registered in the 414 - * format registry so DocView can instantiate the right renderer for each 414 + * format registry so PolyRender can instantiate the right renderer for each 415 415 * document format. 416 416 */ 417 417 export type RendererFactory = () => Renderer ··· 421 421 // Events (for vanilla JS event-driven usage) 422 422 // --------------------------------------------------------------------------- 423 423 424 - export interface DocViewEventMap { 424 + export interface PolyRenderEventMap { 425 425 ready: DocumentInfo 426 426 pagechange: { page: number; totalPages: number } 427 427 zoomchange: { zoom: number } 428 - error: DocViewError 428 + error: PolyRenderError 429 429 loadingchange: { loading: boolean } 430 430 destroy: void 431 431 } 432 432 433 - export type DocViewEventType = keyof DocViewEventMap 433 + export type PolyRenderEventType = keyof PolyRenderEventMap
+2 -2
packages/core/src/utils.ts
··· 1 - import type { DocumentSource, DocumentFormat, DocViewError } from './types.js' 2 - import { DocViewError as DVError } from './types.js' 1 + import type { DocumentSource, DocumentFormat, PolyRenderError } from './types.js' 2 + import { PolyRenderError as DVError } from './types.js' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // DOM Helpers
+3 -3
packages/react/package.json
··· 1 1 { 2 - "name": "@docview/react", 2 + "name": "@polyrender/react", 3 3 "version": "0.1.0", 4 - "description": "React components for the DocView universal document renderer", 4 + "description": "React components for the PolyRender universal document renderer", 5 5 "license": "Zlib", 6 6 "type": "module", 7 7 "main": "./dist/index.cjs", ··· 26 26 "typecheck": "tsc --noEmit" 27 27 }, 28 28 "dependencies": { 29 - "@docview/core": "workspace:*" 29 + "@polyrender/core": "workspace:*" 30 30 }, 31 31 "peerDependencies": { 32 32 "react": ">=18.0.0",
+7 -7
packages/react/src/DocumentViewer.tsx
··· 3 3 DocumentSource, 4 4 DocumentFormat, 5 5 DocumentInfo, 6 - DocViewError, 6 + PolyRenderError, 7 7 ToolbarConfig, 8 8 PdfOptions, 9 9 CodeOptions, 10 10 CsvOptions, 11 11 EpubOptions, 12 - } from '@docview/core' 12 + } from '@polyrender/core' 13 13 import { useDocumentRenderer } from './useDocumentRenderer.js' 14 14 15 15 export interface DocumentViewerProps { ··· 44 44 onReady?: (info: DocumentInfo) => void 45 45 onPageChange?: (page: number, totalPages: number) => void 46 46 onZoomChange?: (zoom: number) => void 47 - onError?: (error: DocViewError) => void 47 + onError?: (error: PolyRenderError) => void 48 48 onLoadingChange?: (loading: boolean) => void 49 49 50 50 // --- Format-specific options --- ··· 70 70 /** 71 71 * React component for rendering documents of any supported format. 72 72 * 73 - * Wraps the framework-agnostic `@docview/core` library with React lifecycle 73 + * Wraps the framework-agnostic `@polyrender/core` library with React lifecycle 74 74 * management, ref-based imperative API, and automatic cleanup. 75 75 * 76 76 * @example 77 77 * ```tsx 78 - * import { DocumentViewer } from '@docview/react' 79 - * import '@docview/core/styles.css' 78 + * import { DocumentViewer } from '@polyrender/react' 79 + * import '@polyrender/core/styles.css' 80 80 * 81 81 * function App() { 82 82 * return ( ··· 184 184 <div 185 185 ref={containerRef as any} 186 186 style={containerStyle} 187 - data-docview-wrapper="" 187 + data-polyrender-wrapper="" 188 188 /> 189 189 ) 190 190 },
+8 -8
packages/react/src/index.ts
··· 7 7 export type { UseDocumentRendererOptions, UseDocumentRendererReturn } from './useDocumentRenderer.js' 8 8 9 9 // Re-export core types for convenience (so consumers don't need to 10 - // install @docview/core separately just for types) 10 + // install @polyrender/core separately just for types) 11 11 export type { 12 12 DocumentSource, 13 13 FileSource, ··· 21 21 PageFetchAdapter, 22 22 ChunkFetchAdapter, 23 23 TextFetchAdapter, 24 - DocViewOptions, 24 + PolyRenderOptions, 25 25 PdfOptions, 26 26 CodeOptions, 27 27 CsvOptions, 28 28 EpubOptions, 29 29 ToolbarConfig, 30 30 DocumentInfo, 31 - DocViewState, 31 + PolyRenderState, 32 32 DocumentFormat, 33 - DocViewEventMap, 34 - DocViewEventType, 35 - DocViewErrorCode, 36 - } from '@docview/core' 33 + PolyRenderEventMap, 34 + PolyRenderEventType, 35 + PolyRenderErrorCode, 36 + } from '@polyrender/core' 37 37 38 - export { DocViewError, DocView } from '@docview/core' 38 + export { PolyRenderError, PolyRender } from '@polyrender/core'
+16 -16
packages/react/src/useDocumentRenderer.ts
··· 1 1 import { useRef, useEffect, useState, useCallback } from 'react' 2 2 import type { 3 - DocViewOptions, 4 - DocViewState, 3 + PolyRenderOptions, 4 + PolyRenderState, 5 5 DocumentInfo, 6 - DocViewError, 6 + PolyRenderError, 7 7 DocumentSource, 8 - } from '@docview/core' 9 - import { DocView } from '@docview/core' 8 + } from '@polyrender/core' 9 + import { PolyRender } from '@polyrender/core' 10 10 11 11 export interface UseDocumentRendererOptions 12 - extends Omit<DocViewOptions, 'source' | 'onReady' | 'onPageChange' | 'onZoomChange' | 'onError' | 'onLoadingChange'> { 12 + extends Omit<PolyRenderOptions, 'source' | 'onReady' | 'onPageChange' | 'onZoomChange' | 'onError' | 'onLoadingChange'> { 13 13 source: DocumentSource | null | undefined 14 14 onReady?: (info: DocumentInfo) => void 15 15 onPageChange?: (page: number, totalPages: number) => void 16 16 onZoomChange?: (zoom: number) => void 17 - onError?: (error: DocViewError) => void 17 + onError?: (error: PolyRenderError) => void 18 18 onLoadingChange?: (loading: boolean) => void 19 19 } 20 20 ··· 22 22 /** Ref to attach to the container div. */ 23 23 containerRef: React.RefObject<HTMLDivElement | null> 24 24 /** Current viewer state. */ 25 - state: DocViewState 25 + state: PolyRenderState 26 26 /** Navigate to a page. */ 27 27 goToPage: (page: number) => void 28 28 /** Set zoom level. */ ··· 30 30 /** Whether the viewer is mounted and ready. */ 31 31 ready: boolean 32 32 /** Current error, if any. */ 33 - error: DocViewError | null 33 + error: PolyRenderError | null 34 34 } 35 35 36 36 /** 37 - * React hook for the DocView document renderer. 37 + * React hook for the PolyRender document renderer. 38 38 * 39 - * Manages the lifecycle of a DocView instance, bridging its imperative API 39 + * Manages the lifecycle of a PolyRender instance, bridging its imperative API 40 40 * to React's declarative model. Handles mounting, updating, and cleanup. 41 41 * 42 42 * @example ··· 60 60 options: UseDocumentRendererOptions, 61 61 ): UseDocumentRendererReturn { 62 62 const containerRef = useRef<HTMLDivElement | null>(null) 63 - const instanceRef = useRef<DocView | null>(null) 63 + const instanceRef = useRef<PolyRender | null>(null) 64 64 const optionsRef = useRef(options) 65 65 66 - const [state, setState] = useState<DocViewState>({ 66 + const [state, setState] = useState<PolyRenderState>({ 67 67 loading: true, 68 68 error: null, 69 69 currentPage: 1, ··· 73 73 }) 74 74 75 75 const [ready, setReady] = useState(false) 76 - const [error, setError] = useState<DocViewError | null>(null) 76 + const [error, setError] = useState<PolyRenderError | null>(null) 77 77 78 78 // Keep options ref current 79 79 optionsRef.current = options ··· 93 93 setError(null) 94 94 setState((s) => ({ ...s, loading: true, error: null })) 95 95 96 - const instance = new DocView(container, { 96 + const instance = new PolyRender(container, { 97 97 ...options, 98 98 source: options.source, 99 99 onReady: (info) => { ··· 139 139 useEffect(() => { 140 140 if (!instanceRef.current) return 141 141 142 - const changed: Partial<DocViewOptions> = {} 142 + const changed: Partial<PolyRenderOptions> = {} 143 143 if (options.theme) changed.theme = options.theme 144 144 if (options.className !== undefined) changed.className = options.className 145 145 if (options.zoom !== undefined) changed.zoom = options.zoom
+1 -1
packages/react/tsup.config.ts
··· 6 6 dts: true, 7 7 sourcemap: true, 8 8 clean: true, 9 - external: ['react', 'react-dom', '@docview/core'], 9 + external: ['react', 'react-dom', '@polyrender/core'], 10 10 esbuildOptions(options) { 11 11 options.jsx = 'automatic' 12 12 },
+3 -6
pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 devDependencies: 11 - pdfjs-dist: 12 - specifier: ^5.5.207 13 - version: 5.5.207 14 11 typescript: 15 12 specifier: ^5.5.0 16 13 version: 5.9.3 17 14 18 15 examples/basic: 19 16 dependencies: 20 - '@docview/core': 17 + '@polyrender/core': 21 18 specifier: workspace:* 22 19 version: link:../../packages/core 23 20 docx-preview: ··· 51 48 52 49 examples/vanilla: 53 50 dependencies: 54 - '@docview/core': 51 + '@polyrender/core': 55 52 specifier: workspace:* 56 53 version: link:../../packages/core 57 54 docx-preview: ··· 116 113 117 114 packages/react: 118 115 dependencies: 119 - '@docview/core': 116 + '@polyrender/core': 120 117 specifier: workspace:* 121 118 version: link:../core 122 119 devDependencies: