office-viewer-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +390 -0
  2. package/dist/client/assets/DocxViewer-NgAiZAEg.css +1 -0
  3. package/dist/client/assets/DocxViewer-gwdjm0mw.js +60 -0
  4. package/dist/client/assets/LogoIcon-BcnkueZW.js +1 -0
  5. package/dist/client/assets/PptxViewer-CLNaZa_4.js +59 -0
  6. package/dist/client/assets/PptxViewer-CYMXzyIj.css +1 -0
  7. package/dist/client/assets/XlsxViewer-BNso6L-X.css +1 -0
  8. package/dist/client/assets/XlsxViewer-C2ErMokS.js +64 -0
  9. package/dist/client/assets/_commonjs-dynamic-modules-DaXrHM_S.js +1 -0
  10. package/dist/client/assets/form-C1byQJR4.js +1 -0
  11. package/dist/client/assets/index-BDMLGHcR.js +2 -0
  12. package/dist/client/assets/index-CKjGwz9R.js +12 -0
  13. package/dist/client/assets/jszip.min-BwIaN_vk.js +2 -0
  14. package/dist/client/assets/login-DEy3R1iD.js +1 -0
  15. package/dist/client/assets/register-CUUVGLJE.js +1 -0
  16. package/dist/client/assets/styles-3a3CPFIV.css +1 -0
  17. package/dist/client/robots.txt +2 -0
  18. package/dist/index.cjs +1806 -0
  19. package/dist/index.d.cts +16 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.js +1769 -0
  22. package/dist/server/assets/DocxViewer-Bm8UJY-7.js +469 -0
  23. package/dist/server/assets/LogoIcon-Dx0LU3or.js +26 -0
  24. package/dist/server/assets/PptxViewer-DS7Atucw.js +213 -0
  25. package/dist/server/assets/XlsxViewer-jzIgKmN2.js +841 -0
  26. package/dist/server/assets/_tanstack-start-manifest_v-CpFqMvFH.js +4 -0
  27. package/dist/server/assets/empty-plugin-adapters-BFgPZ6_d.js +6 -0
  28. package/dist/server/assets/form-CD9otjw-.js +236 -0
  29. package/dist/server/assets/index-gQHSGxNv.js +365 -0
  30. package/dist/server/assets/login-DvbAXNSQ.js +81 -0
  31. package/dist/server/assets/register-C2G9K9kP.js +102 -0
  32. package/dist/server/assets/router-F5YKPXkV.js +229 -0
  33. package/dist/server/assets/server-6Sfy37dh.js +1523 -0
  34. package/dist/server/assets/start-dMGD6DUy.js +56 -0
  35. package/dist/server/server.js +94 -0
  36. package/package.json +120 -0
package/README.md ADDED
@@ -0,0 +1,390 @@
1
+ # office-viewer
2
+
3
+ A zero-configuration React component for rendering Word, Excel, and PowerPoint files directly in the browser. No server required — everything processes locally.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ | Format | Capability |
10
+ |--------|-----------|
11
+ | **Word (.docx)** | Text, tables, images, headers/footers, continuous scroll and page-by-page navigation, DrawingML charts rendered to PNG via ECharts |
12
+ | **Excel (.xlsx)** | Cell styles and colours, merged cells, date and number formatting, formula values, multi-sheet tab bar with overflow scroll, sticky row and column headers |
13
+ | **PowerPoint (.pptx)** | All slide layouts, images, text, shapes, vertical scroll and slide-by-slide navigation with keyboard arrow key support |
14
+
15
+ - **Zero-config CSS** — styles are auto-injected at runtime; no separate CSS import required
16
+ - **Automatic format detection** — determines the file type from `File.name`, `Blob.type`, or a `filename` hint
17
+ - **Built-in toolbar** — view-mode selector (Continuous Scroll / Page Navigation) rendered internally for Word and PowerPoint; consumers own zero UI state
18
+ - **SSR-safe** — style injection is guarded by `typeof document !== "undefined"`
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install office-viewer
26
+ ```
27
+
28
+ ```bash
29
+ yarn add office-viewer
30
+ ```
31
+
32
+ ```bash
33
+ pnpm add office-viewer
34
+ ```
35
+
36
+ ### Peer dependencies
37
+
38
+ The following packages must already be present in your project:
39
+
40
+ ```bash
41
+ npm install react react-dom lucide-react
42
+ ```
43
+
44
+ | Peer | Version |
45
+ |------|---------|
46
+ | `react` | `^18.0.0 \|\| ^19.0.0` |
47
+ | `react-dom` | `^18.0.0 \|\| ^19.0.0` |
48
+ | `lucide-react` | `>=0.400.0` |
49
+
50
+ ---
51
+
52
+ ## Quick start
53
+
54
+ ```tsx
55
+ import { OfficeViewer } from "office-viewer";
56
+
57
+ export default function App() {
58
+ const [file, setFile] = React.useState<File | null>(null);
59
+
60
+ return (
61
+ <>
62
+ <input
63
+ type="file"
64
+ accept=".docx,.xlsx,.pptx"
65
+ onChange={(e) => setFile(e.target.files?.[0] ?? null)}
66
+ />
67
+
68
+ {file && (
69
+ <div style={{ height: "100vh" }}>
70
+ <OfficeViewer file={file} />
71
+ </div>
72
+ )}
73
+ </>
74
+ );
75
+ }
76
+ ```
77
+
78
+ No CSS import. No state for view mode or pagination. No configuration.
79
+
80
+ ---
81
+
82
+ ## Usage
83
+
84
+ ### From a file input (`File`)
85
+
86
+ ```tsx
87
+ import { OfficeViewer } from "office-viewer";
88
+
89
+ function FileViewer() {
90
+ const [file, setFile] = React.useState<File | null>(null);
91
+
92
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
93
+ setFile(e.target.files?.[0] ?? null);
94
+ };
95
+
96
+ return (
97
+ <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
98
+ <input type="file" accept=".docx,.xlsx,.pptx" onChange={handleChange} />
99
+ {file && <OfficeViewer file={file} style={{ flex: 1 }} />}
100
+ </div>
101
+ );
102
+ }
103
+ ```
104
+
105
+ ### From a `Blob` (e.g. a fetch response)
106
+
107
+ ```tsx
108
+ import { OfficeViewer } from "office-viewer";
109
+
110
+ function RemoteViewer({ url, filename }: { url: string; filename: string }) {
111
+ const [blob, setBlob] = React.useState<Blob | null>(null);
112
+
113
+ React.useEffect(() => {
114
+ fetch(url)
115
+ .then((r) => r.blob())
116
+ .then(setBlob);
117
+ }, [url]);
118
+
119
+ if (!blob) return <p>Loading…</p>;
120
+
121
+ return (
122
+ <div style={{ height: "80vh" }}>
123
+ {/* Pass filename so the component can detect .docx / .xlsx / .pptx */}
124
+ <OfficeViewer file={blob} filename={filename} />
125
+ </div>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### From an `ArrayBuffer`
131
+
132
+ ```tsx
133
+ import { OfficeViewer } from "office-viewer";
134
+
135
+ function BufferViewer({
136
+ buffer,
137
+ filename,
138
+ }: {
139
+ buffer: ArrayBuffer;
140
+ filename: string;
141
+ }) {
142
+ return (
143
+ <div style={{ height: "80vh" }}>
144
+ <OfficeViewer file={buffer} filename={filename} />
145
+ </div>
146
+ );
147
+ }
148
+ ```
149
+
150
+ ### Error handling
151
+
152
+ ```tsx
153
+ import { OfficeViewer } from "office-viewer";
154
+
155
+ function SafeViewer({ file }: { file: File }) {
156
+ const handleError = (err: Error) => {
157
+ console.error("Viewer error:", err.message);
158
+ // show a toast, report to Sentry, etc.
159
+ };
160
+
161
+ return (
162
+ <div style={{ height: "80vh" }}>
163
+ <OfficeViewer file={file} onError={handleError} />
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ### Custom container styling
170
+
171
+ Pass a `className` to size or position the viewer wrapper:
172
+
173
+ ```tsx
174
+ <OfficeViewer file={file} className="h-screen w-full rounded-xl shadow-lg" />
175
+ ```
176
+
177
+ ---
178
+
179
+ ## API
180
+
181
+ ### `<OfficeViewer />`
182
+
183
+ The only public component. All internal state (view mode, page/slide index, sheet selection) is managed inside the component.
184
+
185
+ ```ts
186
+ import { OfficeViewer, type OfficeViewerProps } from "office-viewer";
187
+ ```
188
+
189
+ #### Props
190
+
191
+ | Prop | Type | Required | Default | Description |
192
+ |------|------|----------|---------|-------------|
193
+ | `file` | `File \| Blob \| ArrayBuffer` | Yes | — | Document to render. The type is detected automatically. |
194
+ | `filename` | `string` | No | `""` | Filename hint for type detection when passing a bare `Blob` or `ArrayBuffer` that carries no name. Example: `"report.xlsx"` |
195
+ | `className` | `string` | No | `""` | CSS class applied to the outermost wrapper `<div>` for sizing and positioning. |
196
+ | `onError` | `(e: Error) => void` | No | — | Called when the document fails to load or render. The component also displays an inline error message. |
197
+
198
+ #### File type detection rules
199
+
200
+ | Input type | How the type is resolved |
201
+ |---|---|
202
+ | `File` | `file.name` extension (`.docx`, `.xlsx`, `.pptx`) |
203
+ | `Blob` with MIME type | `blob.type` — checks for `wordprocessingml`, `spreadsheetml`, `presentationml` |
204
+ | `Blob` without MIME type | `filename` prop extension |
205
+ | `ArrayBuffer` | `filename` prop extension |
206
+
207
+ If the type cannot be determined, an "Unsupported file type" message is shown.
208
+
209
+ #### TypeScript
210
+
211
+ The `OfficeViewerProps` interface is exported for use in wrapper components:
212
+
213
+ ```ts
214
+ import type { OfficeViewerProps } from "office-viewer";
215
+
216
+ interface MyViewerProps extends Omit<OfficeViewerProps, "onError"> {
217
+ onRenderError?: (err: Error) => void;
218
+ }
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Format details
224
+
225
+ ### Word (.docx)
226
+
227
+ - Rendered by [docx-preview](https://github.com/VolodymyrBaydalka/docxjs)
228
+ - DrawingML chart elements are pre-processed before rendering: chart XML is parsed, converted to an [ECharts](https://echarts.apache.org) option, rendered off-screen to a PNG, and inlined as an `<img>` in the document — so charts display correctly even though `docx-preview` does not natively support them
229
+ - Built-in toolbar provides two view modes:
230
+ - **Continuous Scroll** — all pages flow vertically
231
+ - **Page Navigation** — one page at a time with prev/next buttons and a page counter
232
+
233
+ ### Excel (.xlsx)
234
+
235
+ - Dual-parser strategy: [ExcelJS](https://github.com/exceljs/exceljs) supplies cell styles, fonts, and colours; [SheetJS](https://sheetjs.com) supplies formula-evaluated values
236
+ - Multi-sheet support — tab bar at the bottom with left/right scroll arrows for wide workbooks
237
+ - Sticky first row (header) and first column
238
+ - Merged cell rendering
239
+ - Date, number, currency, and percentage formatting
240
+ - Bold, italic, text colour, and background colour fidelity
241
+
242
+ ### PowerPoint (.pptx)
243
+
244
+ - Rendered by [pptx-preview](https://github.com/meienberger/pptx-preview)
245
+ - Slides re-render automatically when the container resizes (250 ms debounce via `ResizeObserver`)
246
+ - Built-in toolbar provides two view modes:
247
+ - **Vertical Scroll** — all slides flow vertically
248
+ - **Slide Navigation** — one slide at a time with prev/next buttons and a slide counter
249
+ - Keyboard navigation in Slide Navigation mode: `←`/`↑` for previous, `→`/`↓` for next
250
+
251
+ ---
252
+
253
+ ## Sizing
254
+
255
+ The component uses `width: 100%` and `flex: 1` internally. Give the wrapper a defined height so scroll and navigation modes work correctly:
256
+
257
+ ```tsx
258
+ {/* Full-page viewer */}
259
+ <div style={{ height: "100vh" }}>
260
+ <OfficeViewer file={file} />
261
+ </div>
262
+
263
+ {/* Panel inside a layout */}
264
+ <div style={{ height: "calc(100vh - 64px)" }}>
265
+ <OfficeViewer file={file} />
266
+ </div>
267
+
268
+ {/* With Tailwind CSS */}
269
+ <OfficeViewer file={file} className="h-screen" />
270
+ ```
271
+
272
+ > **Note:** A defined height is especially important for PowerPoint (Slide Navigation mode) and Excel (the grid fills the available space with sticky headers).
273
+
274
+ ---
275
+
276
+ ## Browser support
277
+
278
+ | Browser | Version |
279
+ |---------|---------|
280
+ | Chrome | 90+ |
281
+ | Edge | 90+ |
282
+ | Firefox | 90+ |
283
+ | Safari | 14+ |
284
+
285
+ Requires support for:
286
+ - `ResizeObserver`
287
+ - `ArrayBuffer` / `File.arrayBuffer()`
288
+ - CSS custom properties
289
+ - `oklch()` color function (Chrome 111+, Firefox 113+, Safari 15.4+) — used for design tokens; falls back gracefully in older browsers
290
+
291
+ ---
292
+
293
+ ## Examples
294
+
295
+ ### Next.js (App Router)
296
+
297
+ ```tsx
298
+ // app/viewer/page.tsx
299
+ "use client";
300
+
301
+ import { useState } from "react";
302
+ import { OfficeViewer } from "office-viewer";
303
+
304
+ export default function ViewerPage() {
305
+ const [file, setFile] = useState<File | null>(null);
306
+
307
+ return (
308
+ <div className="flex h-screen flex-col">
309
+ <header className="flex h-16 items-center px-6 border-b">
310
+ <input
311
+ type="file"
312
+ accept=".docx,.xlsx,.pptx"
313
+ onChange={(e) => setFile(e.target.files?.[0] ?? null)}
314
+ />
315
+ </header>
316
+ <main className="flex-1 overflow-hidden">
317
+ {file && <OfficeViewer file={file} className="h-full" />}
318
+ </main>
319
+ </div>
320
+ );
321
+ }
322
+ ```
323
+
324
+ ### Vite + React
325
+
326
+ ```tsx
327
+ // src/App.tsx
328
+ import { useState } from "react";
329
+ import { OfficeViewer } from "office-viewer";
330
+
331
+ export default function App() {
332
+ const [file, setFile] = useState<File | null>(null);
333
+
334
+ return (
335
+ <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
336
+ <div style={{ padding: "1rem", borderBottom: "1px solid #e2e8f0" }}>
337
+ <input
338
+ type="file"
339
+ accept=".docx,.xlsx,.pptx"
340
+ onChange={(e) => setFile(e.target.files?.[0] ?? null)}
341
+ />
342
+ </div>
343
+ <div style={{ flex: 1, overflow: "hidden" }}>
344
+ {file && <OfficeViewer file={file} />}
345
+ </div>
346
+ </div>
347
+ );
348
+ }
349
+ ```
350
+
351
+ ### Fetching a document from an API
352
+
353
+ ```tsx
354
+ import { useEffect, useState } from "react";
355
+ import { OfficeViewer } from "office-viewer";
356
+
357
+ function DocumentViewer({ documentId }: { documentId: string }) {
358
+ const [buffer, setBuffer] = useState<ArrayBuffer | null>(null);
359
+ const [filename, setFilename] = useState("");
360
+ const [loading, setLoading] = useState(true);
361
+
362
+ useEffect(() => {
363
+ setLoading(true);
364
+ fetch(`/api/documents/${documentId}`)
365
+ .then(async (res) => {
366
+ // Read filename from Content-Disposition header if present
367
+ const disposition = res.headers.get("Content-Disposition") ?? "";
368
+ const match = disposition.match(/filename="?([^"]+)"?/);
369
+ if (match) setFilename(match[1]);
370
+ return res.arrayBuffer();
371
+ })
372
+ .then((ab) => { setBuffer(ab); setLoading(false); });
373
+ }, [documentId]);
374
+
375
+ if (loading) return <p>Loading document…</p>;
376
+ if (!buffer) return <p>Failed to load document.</p>;
377
+
378
+ return (
379
+ <div style={{ height: "80vh" }}>
380
+ <OfficeViewer file={buffer} filename={filename} />
381
+ </div>
382
+ );
383
+ }
384
+ ```
385
+
386
+ ---
387
+
388
+ ## License
389
+
390
+ MIT © Virtualan Software
@@ -0,0 +1 @@
1
+ .ov-docx{flex-direction:column;display:flex}.ov-docx__loading{color:var(--ov-muted-fg);flex-direction:column;align-items:center;gap:.5rem;padding-block:4rem;display:flex}.ov-docx__loading-icon{width:1.5rem;height:1.5rem;animation:1s linear infinite ov-spin}.ov-docx__loading-text{font-size:.875rem}.ov-docx__error{margin-inline:auto;border:1px solid color-mix(in oklch,var(--ov-destructive) 30%,transparent);background:color-mix(in oklch,var(--ov-destructive) 5%,transparent);max-width:32rem;color:var(--ov-destructive);border-radius:.5rem;margin-top:3rem;padding:1rem 1.5rem;font-size:.875rem}.ov-docx__body{justify-content:center;padding-block:2rem;display:flex;position:relative}.ov-docx__render{width:100%}.ov-docx__nav{z-index:10;justify-content:center;align-items:center;gap:.75rem;padding-bottom:.5rem;display:flex;position:sticky;bottom:1.5rem}.ov-docx__nav-btn{cursor:pointer;background:#fff;border:2px solid #e2e8f0;border-radius:9999px;justify-content:center;align-items:center;width:3.75rem;height:3.75rem;transition:transform .15s cubic-bezier(.4,0,.2,1),border-color .15s cubic-bezier(.4,0,.2,1);display:flex;box-shadow:0 2px 8px #0f172a24,0 1px 3px #0f172a1a}.ov-docx__nav-btn:hover{border-color:#cbd5e1;transform:scale(1.05)}.ov-docx__nav-btn:disabled{cursor:not-allowed;opacity:.25}.ov-docx__nav-btn-icon{color:#1e293b;width:1.5rem;height:1.5rem}.ov-docx__nav-counter{text-align:center;background:#fff;border:1px solid #e2e8f0;border-radius:9999px;min-width:7.5rem;padding:.4375rem 1.25rem;box-shadow:0 1px 3px #0f172a12}.ov-docx__nav-current{color:#334155;font-size:13px;font-weight:600}.ov-docx__nav-total{color:#94a3b8;font-size:13px}.ov-container .docx-wrapper{background:0 0!important;padding:0!important}.ov-container .docx-wrapper>section.docx{box-shadow:var(--ov-shadow-page);background:var(--ov-page);margin-bottom:1.5rem!important}