react-file-preview-engine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ MIT License
2
+
3
+ Copyright (c) Sahil Aggarwal, <aggarwalsahil2004@gmail.com>
4
+ Copyright (c) 2022 Igor Gaponov (gapon2401)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # react-file-preview-engine
2
+
3
+ A renderer driven React file preview engine designed for extensibility, correctness, and long term maintainability.
4
+
5
+ `react-file-preview-engine` lets you preview files by delegating rendering to small, isolated renderers that decide if they can handle a file based on runtime context. It supports common media types out of the box and makes it trivial to add or override renderers for custom formats.
6
+
7
+ ## Why react-file-preview-engine
8
+
9
+ Inspired by [`@codesmith-99/react-file-preview`](https://github.com/AbdulmueezEmiola/React-File-Previewer) and redesigned to address its architectural and maintenance limitations.
10
+
11
+ - Renderer driven architecture instead of hard coded conditional rendering
12
+ - Built in support for images, video, audio, pdf, html, and plain text
13
+ - Automatic MIME type resolution using file name when needed
14
+ - Supports multiple file source types including URL, File, Blob, and ArrayBuffer
15
+ - Stable loading, ready, and error state handling
16
+ - Abortable fetches with proper cleanup for fast file switching
17
+ - First class support for custom renderers
18
+ - Optional, fully typed additional render context
19
+ - Pluggable error renderer
20
+ - Fully typed public API
21
+ - React is not bundled as a direct dependency
22
+ - Actively maintained and designed for long term extensibility
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # npm
28
+ npm install react-file-preview-engine
29
+
30
+ # yarn
31
+ yarn add react-file-preview-engine
32
+
33
+ # pnpm
34
+ pnpm add react-file-preview-engine
35
+
36
+ # bun
37
+ bun add react-file-preview-engine
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ `react-file-preview-engine` exposes a single default export, the `<FilePreviewer />` component.
43
+
44
+ You can pass it a file source and basic file metadata. The source can be a `URL`, `File`, `Blob`, or `ArrayBuffer`. Using the provided MIME type or file name, the engine dynamically resolves the most suitable renderer at runtime.
45
+
46
+ ### Basic Usage
47
+
48
+ This is the minimal setup. The previewer automatically infers the MIME type from `fileName` if not provided.
49
+
50
+ ```tsx
51
+ import React from "react";
52
+ import FilePreviewer from "react-file-preview-engine";
53
+
54
+ export default function App() {
55
+ return <FilePreviewer src="https://example.com/sample.pdf" fileName="sample.pdf" />;
56
+ }
57
+ ```
58
+
59
+ ### Previewing Local Files
60
+
61
+ You can preview files selected by the user without uploading them first. The engine automatically converts File, Blob, and ArrayBuffer sources to object URLs internally.
62
+
63
+ ```tsx
64
+ import React, { useState } from "react";
65
+ import FilePreviewer from "react-file-preview-engine";
66
+
67
+ export default function App() {
68
+ const [file, setFile] = useState<File>();
69
+
70
+ return (
71
+ <>
72
+ <input type="file" onChange={(e) => setFile(e.target.files?.[0])} />
73
+ {file && <FilePreviewer src={file} fileName={file.name} mimeType={file.type} />}
74
+ </>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ### Providing MIME Type Explicitly
80
+
81
+ If you already know the MIME type, you can pass it directly. This ensures the engine uses the correct renderer even when inference is not possible.
82
+
83
+ ```tsx
84
+ import React from "react";
85
+ import FilePreviewer from "react-file-preview-engine";
86
+
87
+ export default function App() {
88
+ return <FilePreviewer src={new Blob(["Hello world"], { type: "text/plain" })} mimeType="text/plain" fileName="hello.txt" />;
89
+ }
90
+ ```
91
+
92
+ ### Handling Load and Error Events
93
+
94
+ You can listen to lifecycle events triggered by the active renderer.
95
+
96
+ ```tsx
97
+ import React from "react";
98
+ import FilePreviewer from "react-file-preview-engine";
99
+
100
+ export default function App() {
101
+ return (
102
+ <FilePreviewer
103
+ src="/document.pdf"
104
+ fileName="document.pdf"
105
+ onLoad={() => {
106
+ console.log("File loaded");
107
+ }}
108
+ onError={() => {
109
+ console.error("Failed to load file");
110
+ }}
111
+ />
112
+ );
113
+ }
114
+ ```
115
+
116
+ ### Auto Play Media Files
117
+
118
+ For audio and video files, you can enable auto play.
119
+
120
+ ```tsx
121
+ import React from "react";
122
+ import FilePreviewer from "react-file-preview-engine";
123
+
124
+ export default function App() {
125
+ return <FilePreviewer src="https://example.com/video.mp4" fileName="video.mp4" autoPlay={true} />;
126
+ }
127
+ ```
128
+
129
+ ### Partial Customization
130
+
131
+ You can customize the loader, container props, and icon props without modifying any renderers.
132
+
133
+ ```tsx
134
+ import React from "react";
135
+ import FilePreviewer from "react-file-preview-engine";
136
+
137
+ export default function App() {
138
+ return (
139
+ <FilePreviewer
140
+ src="/image.jpg"
141
+ fileName="image.jpg"
142
+ loader={<div>Loading preview…</div>}
143
+ containerProps={{ style: { border: "1px solid #e5e7eb", padding: 8 } }}
144
+ iconProps={{ style: { fontSize: 48 } }}
145
+ />
146
+ );
147
+ }
148
+ ```
149
+
150
+ ### Custom Error Renderer
151
+
152
+ When a renderer reports an error, the previewer switches to `errorRenderer`.
153
+
154
+ ```tsx
155
+ import React from "react";
156
+ import FilePreviewer from "react-file-preview-engine";
157
+
158
+ const errorRenderer = {
159
+ Component() {
160
+ return <div>Preview unavailable</div>;
161
+ },
162
+ };
163
+
164
+ export default function App() {
165
+ return <FilePreviewer src="/missing.pdf" fileName="missing.pdf" errorRenderer={errorRenderer} />;
166
+ }
167
+ ```
168
+
169
+ ### Custom Renderers
170
+
171
+ `react-file-preview-engine` resolves previews using renderers. A renderer declares whether it can handle a file using `canRender` and renders the preview inside its `Component`.
172
+
173
+ Renderers receive a render context that includes `src`, `mimeType`, `fileName`, lifecycle callbacks, and any values passed via `additionalContext`. The same context mechanism applies to `customRenderers` and `errorRenderer`.
174
+
175
+ #### Renderer Resolution Order
176
+
177
+ When previewing a file, the engine checks renderers in this order:
178
+
179
+ 1. Your `customRenderers` (from first to last)
180
+ 2. Built-in default renderers
181
+ 3. `fallbackRenderer` (icon fallback)
182
+
183
+ This means you can **override** built-in renderers by providing a custom renderer with the same `canRender` logic.
184
+
185
+ #### Markdown Renderer Example
186
+
187
+ This example adds support for markdown files. By default, it renders markdown as html, but allows switching to raw text using `additionalContext`.
188
+
189
+ ```tsx
190
+ import React, { useEffect, useState } from "react";
191
+ import FilePreviewer from "react-file-preview-engine";
192
+ import type { Renderer } from "react-file-preview-engine/types";
193
+ import remarkGfm from "remark-gfm";
194
+ import rehypeRaw from "rehype-raw";
195
+ import { useRemark } from "react-remarkify";
196
+ import rehypeSanitize from "rehype-sanitize";
197
+
198
+ const markdownRenderer: Renderer<{ renderAsHtml?: boolean }> = {
199
+ name: "markdown",
200
+ canRender({ mimeType }) {
201
+ return mimeType === "text/markdown";
202
+ },
203
+ Component({ src, onLoad, onError, renderAsHtml = true }) {
204
+ const [markdown, setMarkdown] = useState("");
205
+
206
+ useEffect(() => {
207
+ const controller = new AbortController();
208
+
209
+ fetch(src, { signal: controller.signal })
210
+ .then((res) => res.text())
211
+ .then((text) => {
212
+ setMarkdown(text);
213
+ onLoad();
214
+ })
215
+ .catch(onError);
216
+
217
+ return () => controller.abort();
218
+ }, [src]);
219
+
220
+ const reactContent = useRemark({
221
+ markdown,
222
+ remarkPlugins: [remarkGfm],
223
+ rehypePlugins: [rehypeRaw, rehypeSanitize],
224
+ remarkToRehypeOptions: { allowDangerousHtml: true },
225
+ });
226
+
227
+ return renderAsHtml ? <div>{reactContent}</div> : <pre>{markdown}</pre>;
228
+ },
229
+ };
230
+
231
+ export default function App() {
232
+ return <FilePreviewer src="/README.md" fileName="README.md" customRenderers={[markdownRenderer]} />;
233
+ }
234
+ ```
235
+
236
+ #### Passing Additional Context
237
+
238
+ ```tsx
239
+ export default function App() {
240
+ return <FilePreviewer src="/README.md" fileName="README.md" customRenderers={[markdownRenderer]} additionalContext={{ renderAsHtml: false }} />;
241
+ }
242
+ ```
243
+
244
+ #### Default Renderers
245
+
246
+ The library includes built-in renderers for text, pdf, html, images, audio, and video. These handle common file types without any configuration.
247
+
248
+ You can explore the actual renderer definitions in the [`defaultRenderers` constant on GitHub](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/lib/rendererRegistry.tsx).
249
+
250
+ ## API Reference
251
+
252
+ ### FilePreviewer Props
253
+
254
+ | Prop | Type | Required | Default | Description |
255
+ | ------------------- | ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
256
+ | `src` | [`FileSource`](#filesource) | Yes | – | Source URL of the file to preview |
257
+ | `mimeType` | `string` | No | inferred | MIME type of the file |
258
+ | `fileName` | `string` | No | - | Used for MIME inference and accessibility |
259
+ | `autoPlay` | `boolean` | No | `false` | Auto play audio and video |
260
+ | `loader` | `ReactNode` | No | [`<Loader />`](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/components.tsx) | Shown while the file is loading |
261
+ | `customRenderers` | [`Renderer[]`](#renderer) | No | - | Additional or overriding renderers |
262
+ | `additionalContext` | `object` | No | - | Extra data passed to renderers, including errorRenderer |
263
+ | `errorRenderer` | [`Renderer`](#renderer) | No | [`fallbackRenderer`](https://github.com/SahilAggarwal2004/react-file-preview-engine/blob/main/src/lib/rendererRegistry.tsx) | Renderer used on error |
264
+ | `containerProps` | [`DivProps`](#divprops) | No | - | Props applied to preview container |
265
+ | `iconProps` | [`DivProps`](#divprops) | No | - | Props applied to fallback icon |
266
+ | `onLoad` | [`Eventhandler`](#eventhandler) | No | – | Called when preview is ready |
267
+ | `onError` | [`Eventhandler`](#eventhandler) | No | – | Called when preview fails |
268
+
269
+ ## Types
270
+
271
+ ### DivProps
272
+
273
+ ```typescript
274
+ import { DetailedHTMLProps, HTMLAttributes } from "react";
275
+
276
+ type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
277
+ ```
278
+
279
+ ### EventHandler
280
+
281
+ ```ts
282
+ export type EventHandler = () => void;
283
+ ```
284
+
285
+ ### FileSource
286
+
287
+ ```ts
288
+ export type FileSource = string | File | Blob | ArrayBuffer;
289
+ ```
290
+
291
+ ### Renderer
292
+
293
+ ```ts
294
+ type RenderContext<T extends object = {}> = {
295
+ src: string;
296
+ mimeType: string;
297
+ fileName: string;
298
+ autoPlay: boolean;
299
+ iconProps: DivProps;
300
+ onLoad: () => void;
301
+ onError: () => void;
302
+ } & T;
303
+ type Renderer<T extends object = {}> = {
304
+ name?: string;
305
+ canRender?(ctx: RenderContext<T>): boolean;
306
+ Component(ctx: RenderContext<T>): JSX.Element;
307
+ };
308
+ ```
309
+
310
+ ## Author
311
+
312
+ [Sahil Aggarwal](https://www.github.com/SahilAggarwal2004)
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { FilePreviewerProps } from './types.js';
3
+
4
+ declare function FilePreviewer<T extends object = {}>({ src, mimeType, fileName, autoPlay, loader, customRenderers, additionalContext, errorRenderer, containerProps, iconProps, onLoad, onError, }: FilePreviewerProps<T>): React.JSX.Element;
5
+
6
+ export { FilePreviewer as default };
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ import Mime from 'mime/lite';
2
+ import React2, { useMemo, useState, useRef, useEffect } from 'react';
3
+ import { FileIcon, defaultStyles } from 'react-file-icon';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __defProps = Object.defineProperties;
7
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __spreadValues = (a, b) => {
13
+ for (var prop in b || (b = {}))
14
+ if (__hasOwnProp.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ if (__getOwnPropSymbols)
17
+ for (var prop of __getOwnPropSymbols(b)) {
18
+ if (__propIsEnum.call(b, prop))
19
+ __defNormalProp(a, prop, b[prop]);
20
+ }
21
+ return a;
22
+ };
23
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
24
+ var __async = (__this, __arguments, generator) => {
25
+ return new Promise((resolve, reject) => {
26
+ var fulfilled = (value) => {
27
+ try {
28
+ step(generator.next(value));
29
+ } catch (e) {
30
+ reject(e);
31
+ }
32
+ };
33
+ var rejected = (value) => {
34
+ try {
35
+ step(generator.throw(value));
36
+ } catch (e) {
37
+ reject(e);
38
+ }
39
+ };
40
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
41
+ step((generator = generator.apply(__this, __arguments)).next());
42
+ });
43
+ };
44
+ function Loader({ children, text }) {
45
+ return /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader" }, /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader-spinner" }), children != null ? children : /* @__PURE__ */ React2.createElement("div", { className: "rfpe-loader-text" }, text));
46
+ }
47
+
48
+ // src/constants.ts
49
+ var defaults = { additionalContext: {}, customRenderers: [], props: {} };
50
+ function useResolvedSrc(src) {
51
+ const objectUrlRef = useRef(null);
52
+ const revokeObjectURL = () => {
53
+ if (!objectUrlRef.current) return;
54
+ URL.revokeObjectURL(objectUrlRef.current);
55
+ objectUrlRef.current = null;
56
+ };
57
+ const resolvedSrc = useMemo(() => {
58
+ revokeObjectURL();
59
+ if (typeof src === "string") return src;
60
+ const url = src instanceof File || src instanceof Blob ? URL.createObjectURL(src) : src instanceof ArrayBuffer ? URL.createObjectURL(new Blob([src])) : "";
61
+ if (url) objectUrlRef.current = url;
62
+ return url;
63
+ }, [src]);
64
+ useEffect(() => revokeObjectURL, []);
65
+ return resolvedSrc;
66
+ }
67
+
68
+ // src/lib/utils.ts
69
+ var composeClass = (baseClass, props) => `${baseClass}${(props == null ? void 0 : props.className) ? " " + props.className : ""}`;
70
+ var composeProps = (baseClass, props, overrideProps) => {
71
+ const mergedProps = __spreadValues(__spreadValues({}, props), overrideProps);
72
+ return __spreadProps(__spreadValues({}, mergedProps), {
73
+ style: __spreadValues(__spreadValues({}, props == null ? void 0 : props.style), overrideProps == null ? void 0 : overrideProps.style),
74
+ className: composeClass(baseClass, mergedProps)
75
+ });
76
+ };
77
+ function fetchResource(src, type, signal) {
78
+ return __async(this, null, function* () {
79
+ const res = yield fetch(src, { signal });
80
+ if (!res.ok) throw new Error();
81
+ return res[type]();
82
+ });
83
+ }
84
+
85
+ // src/lib/rendererRegistry.tsx
86
+ var audioRenderer = {
87
+ canRender: ({ mimeType }) => mimeType.startsWith("audio/"),
88
+ Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
89
+ return /* @__PURE__ */ React2.createElement("audio", { controls: true, autoPlay, onCanPlay: onLoad, onError, style: { width: "100%" }, "aria-label": fileName || "Audio preview" }, /* @__PURE__ */ React2.createElement("source", { src, type: mimeType }));
90
+ }
91
+ };
92
+ var fallbackRenderer = {
93
+ Component({ mimeType, iconProps, onLoad }) {
94
+ const extension = useMemo(() => {
95
+ var _a;
96
+ return (_a = Mime.getExtension(mimeType)) != null ? _a : "";
97
+ }, [mimeType]);
98
+ useEffect(() => {
99
+ onLoad();
100
+ }, []);
101
+ return /* @__PURE__ */ React2.createElement("div", __spreadValues({}, composeProps("rfpe-icon", iconProps)), /* @__PURE__ */ React2.createElement(FileIcon, __spreadValues({ extension }, defaultStyles[extension])));
102
+ }
103
+ };
104
+ var htmlRenderer = {
105
+ canRender: ({ mimeType }) => mimeType === "text/html",
106
+ Component({ src, onLoad, onError }) {
107
+ const [data, setData] = useState("");
108
+ useEffect(() => {
109
+ const controller = new AbortController();
110
+ fetchResource(src, "text", controller.signal).then((data2) => {
111
+ setData(data2);
112
+ onLoad();
113
+ }).catch(onError);
114
+ return () => controller.abort();
115
+ }, [src]);
116
+ return /* @__PURE__ */ React2.createElement("iframe", { src: `data:text/html; charset=utf-8,${encodeURIComponent(data)}`, sandbox: "", className: "rfpe-iframe" });
117
+ }
118
+ };
119
+ var imageRenderer = {
120
+ canRender: ({ mimeType }) => mimeType.startsWith("image/"),
121
+ Component({ src, fileName, onLoad, onError }) {
122
+ return /* @__PURE__ */ React2.createElement("img", { src, alt: fileName || "Image preview", onLoad, onError, style: { width: "100%", height: "100%" } });
123
+ }
124
+ };
125
+ var pdfRenderer = {
126
+ canRender: ({ mimeType }) => mimeType === "application/pdf",
127
+ Component({ src, onLoad, onError }) {
128
+ const [data, setData] = useState("");
129
+ useEffect(() => {
130
+ const controller = new AbortController();
131
+ let objectUrl;
132
+ fetchResource(src, "arrayBuffer", controller.signal).then((buffer) => {
133
+ const blob = new Blob([buffer], { type: "application/pdf" });
134
+ objectUrl = URL.createObjectURL(blob);
135
+ setData(objectUrl);
136
+ onLoad();
137
+ }).catch(onError);
138
+ return () => {
139
+ controller.abort();
140
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
141
+ };
142
+ }, [src]);
143
+ return /* @__PURE__ */ React2.createElement("iframe", { src: data, className: "rfpe-iframe" });
144
+ }
145
+ };
146
+ var textRenderer = {
147
+ canRender: ({ mimeType }) => mimeType === "text/plain",
148
+ Component({ src, onLoad, onError }) {
149
+ const [data, setData] = useState("");
150
+ useEffect(() => {
151
+ const controller = new AbortController();
152
+ fetchResource(src, "text", controller.signal).then((data2) => {
153
+ setData(data2);
154
+ onLoad();
155
+ }).catch(onError);
156
+ return () => controller.abort();
157
+ }, [src]);
158
+ return /* @__PURE__ */ React2.createElement("div", { style: { width: "100%", height: "100%", overflow: "auto", whiteSpace: "pre-wrap" } }, data);
159
+ }
160
+ };
161
+ var videoRenderer = {
162
+ canRender: ({ mimeType }) => mimeType.startsWith("video/"),
163
+ Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
164
+ return /* @__PURE__ */ React2.createElement("video", { controls: true, autoPlay, onCanPlay: onLoad, onError, style: { width: "100%", height: "100%" }, "aria-label": fileName || "Video preview" }, /* @__PURE__ */ React2.createElement("source", { src, type: mimeType }));
165
+ }
166
+ };
167
+ var defaultRenderers = [textRenderer, pdfRenderer, htmlRenderer, imageRenderer, audioRenderer, videoRenderer];
168
+ function resolveRenderer(customRenderers, ctx) {
169
+ var _a;
170
+ return (_a = customRenderers.concat(defaultRenderers).find((r) => {
171
+ var _a2;
172
+ return (_a2 = r.canRender) == null ? void 0 : _a2.call(r, ctx);
173
+ })) != null ? _a : fallbackRenderer;
174
+ }
175
+
176
+ // src/styles.css
177
+ function injectStyle(css) {
178
+ if (typeof document === "undefined") return;
179
+ const head = document.head || document.getElementsByTagName("head")[0];
180
+ const style = document.createElement("style");
181
+ style.type = "text/css";
182
+ if (head.firstChild) {
183
+ head.insertBefore(style, head.firstChild);
184
+ } else {
185
+ head.appendChild(style);
186
+ }
187
+ if (style.styleSheet) {
188
+ style.styleSheet.cssText = css;
189
+ } else {
190
+ style.appendChild(document.createTextNode(css));
191
+ }
192
+ }
193
+ injectStyle(".rfpe-container {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n max-width: 100lvw;\n}\n.rfpe-icon {\n width: 50px;\n height: 50px;\n}\n.rfpe-iframe {\n width: 100%;\n height: 100%;\n background-color: white;\n}\n.rfpe-loader {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.rfpe-loader-spinner {\n width: 22px;\n height: 22px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-top-color: black;\n border-bottom-color: black;\n animation: rfpe-spin-fast 0.6s ease infinite;\n}\n.rfpe-loader-text {\n font-size: 0.875rem;\n}\n@keyframes rfpe-spin-fast {\n to {\n transform: rotate(360deg);\n }\n}\n");
194
+
195
+ // src/index.tsx
196
+ var { additionalContext: defaultContext, customRenderers: defaultRenderers2, props: defaultProps } = defaults;
197
+ function FilePreviewer({
198
+ src,
199
+ mimeType,
200
+ fileName = "",
201
+ autoPlay = false,
202
+ loader = /* @__PURE__ */ React2.createElement(Loader, null),
203
+ customRenderers = defaultRenderers2,
204
+ additionalContext = defaultContext,
205
+ errorRenderer = fallbackRenderer,
206
+ containerProps = defaultProps,
207
+ iconProps = defaultProps,
208
+ onLoad,
209
+ onError
210
+ }) {
211
+ const resolvedSrc = useResolvedSrc(src);
212
+ const fileType = useMemo(() => {
213
+ var _a;
214
+ return (_a = mimeType != null ? mimeType : Mime.getType(fileName)) != null ? _a : "";
215
+ }, [mimeType, fileName]);
216
+ const fileKey = `${resolvedSrc}|${fileType}|${fileName}`;
217
+ const [state, setState] = useState({ key: fileKey, status: "loading" });
218
+ if (state.key !== fileKey) setState({ key: fileKey, status: "loading" });
219
+ const isLoading = state.status === "loading";
220
+ const handleLoad = () => {
221
+ setState((prev) => {
222
+ if (prev.key !== fileKey || prev.status !== "loading") return prev;
223
+ onLoad == null ? void 0 : onLoad();
224
+ return { key: fileKey, status: "ready" };
225
+ });
226
+ };
227
+ const handleError = () => {
228
+ setState((prev) => {
229
+ if (prev.key !== fileKey || prev.status === "error") return prev;
230
+ onError == null ? void 0 : onError();
231
+ return { key: fileKey, status: "error" };
232
+ });
233
+ };
234
+ const context = __spreadValues({ src: resolvedSrc, mimeType: fileType, fileName, autoPlay, iconProps, onLoad: handleLoad, onError: handleError }, additionalContext);
235
+ const renderer = useMemo(() => resolveRenderer(customRenderers, context), [resolvedSrc, fileType, fileName, autoPlay, additionalContext]);
236
+ const ActiveRenderer = state.status === "error" ? errorRenderer : renderer;
237
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, isLoading && loader, /* @__PURE__ */ React2.createElement("div", __spreadValues({}, composeProps("rfpe-container", containerProps, { style: { visibility: isLoading ? "hidden" : "visible" } })), /* @__PURE__ */ React2.createElement(ActiveRenderer.Component, __spreadValues({ key: fileKey }, context))));
238
+ }
239
+
240
+ export { FilePreviewer as default };
@@ -0,0 +1,47 @@
1
+ import { ReactNode, DetailedHTMLProps, HTMLAttributes, JSX } from 'react';
2
+
3
+ type LoaderProps = {
4
+ children?: ReactNode;
5
+ text?: string;
6
+ };
7
+ type EventHandler = () => void;
8
+ type RenderBehaviour = {
9
+ autoPlay: boolean;
10
+ iconProps: DivProps;
11
+ onLoad: EventHandler;
12
+ onError: EventHandler;
13
+ };
14
+ type RenderContext<T extends object = {}> = {
15
+ src: string;
16
+ mimeType: string;
17
+ fileName: string;
18
+ } & RenderBehaviour & T;
19
+ type Renderer<T extends object = {}> = {
20
+ name?: string;
21
+ canRender?(ctx: RenderContext<T>): boolean;
22
+ Component(ctx: RenderContext<T>): JSX.Element;
23
+ };
24
+ type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
25
+ type FetchType = "text" | "arrayBuffer" | "blob" | "json";
26
+ type FilePreviewerProps<T extends object> = {
27
+ src: FileSource;
28
+ loader?: ReactNode;
29
+ customRenderers?: Renderer<T>[];
30
+ additionalContext?: T;
31
+ errorRenderer?: Renderer<T>;
32
+ containerProps?: DivProps;
33
+ } & MimeTypeSource & RenderBehaviour;
34
+ type FileSource = string | File | Blob | ArrayBuffer;
35
+ type MimeTypeSource = {
36
+ mimeType: string;
37
+ fileName?: string;
38
+ } | {
39
+ fileName: string;
40
+ mimeType?: string;
41
+ };
42
+ type State = {
43
+ key: string;
44
+ status: "loading" | "ready" | "error";
45
+ };
46
+
47
+ export type { DivProps, EventHandler, FetchType, FilePreviewerProps, FileSource, LoaderProps, MimeTypeSource, RenderBehaviour, RenderContext, Renderer, State };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "react-file-preview-engine",
3
+ "version": "0.0.1",
4
+ "description": "A renderer-driven React file preview engine with extensible architecture for images, video, audio, PDF, HTML, and custom formats.",
5
+ "license": "MIT",
6
+ "author": "Sahil Aggarwal <aggarwalsahil2004@gmail.com>",
7
+ "contributors": [],
8
+ "homepage": "https://github.com/SahilAggarwal2004/react-file-preview-engine#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/SahilAggarwal2004/react-file-preview-engine.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SahilAggarwal2004/react-file-preview-engine/issues"
15
+ },
16
+ "type": "module",
17
+ "exports": {
18
+ ".": "./dist/index.js",
19
+ "./types": "./dist/types.js"
20
+ },
21
+ "main": "dist/index.js",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "types": "dist/index.d.ts",
26
+ "dependencies": {
27
+ "mime": "^4.1.0",
28
+ "react-file-icon": "^1.6.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19.0.10",
32
+ "@types/react-file-icon": "^1.0.4",
33
+ "tsup": "^8.4.0",
34
+ "typescript": "^5.8.2"
35
+ },
36
+ "peerDependencies": {
37
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
38
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
39
+ },
40
+ "keywords": [
41
+ "arraybuffer",
42
+ "audio-preview",
43
+ "blob",
44
+ "component",
45
+ "custom-renderer",
46
+ "document-viewer",
47
+ "extensible",
48
+ "file",
49
+ "file-preview",
50
+ "file-viewer",
51
+ "image-preview",
52
+ "mime-type",
53
+ "pdf-viewer",
54
+ "preview",
55
+ "react",
56
+ "react-file-preview-engine",
57
+ "renderer",
58
+ "typescript",
59
+ "video-preview",
60
+ "viewer"
61
+ ],
62
+ "scripts": {
63
+ "build": "pnpm i && pnpm run compile",
64
+ "compile": "tsup",
65
+ "dev": "tsup --watch"
66
+ }
67
+ }