react-file-preview-engine 0.1.8 → 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.
package/README.md CHANGED
@@ -50,6 +50,7 @@ This is the minimal setup. The previewer automatically infers the MIME type from
50
50
  ```tsx
51
51
  import React from "react";
52
52
  import FilePreviewer from "react-file-preview-engine";
53
+ import "react-file-preview-engine/style.css";
53
54
 
54
55
  export default function App() {
55
56
  return <FilePreviewer src="https://example.com/sample.pdf" fileName="sample.pdf" />;
@@ -63,6 +64,7 @@ You can preview files selected by the user without uploading them first. The eng
63
64
  ```tsx
64
65
  import React, { useState } from "react";
65
66
  import FilePreviewer from "react-file-preview-engine";
67
+ import "react-file-preview-engine/style.css";
66
68
 
67
69
  export default function App() {
68
70
  const [file, setFile] = useState<File>();
@@ -83,6 +85,7 @@ If you already know the MIME type, you can pass it directly. This ensures the en
83
85
  ```tsx
84
86
  import React from "react";
85
87
  import FilePreviewer from "react-file-preview-engine";
88
+ import "react-file-preview-engine/style.css";
86
89
 
87
90
  export default function App() {
88
91
  return <FilePreviewer src={new Blob(["Hello world"], { type: "text/plain" })} mimeType="text/plain" fileName="hello.txt" />;
@@ -96,6 +99,7 @@ You can listen to lifecycle events triggered by the active renderer.
96
99
  ```tsx
97
100
  import React from "react";
98
101
  import FilePreviewer from "react-file-preview-engine";
102
+ import "react-file-preview-engine/style.css";
99
103
 
100
104
  export default function App() {
101
105
  return (
@@ -120,6 +124,7 @@ For audio and video files, you can enable auto play.
120
124
  ```tsx
121
125
  import React from "react";
122
126
  import FilePreviewer from "react-file-preview-engine";
127
+ import "react-file-preview-engine/style.css";
123
128
 
124
129
  export default function App() {
125
130
  return <FilePreviewer src="https://example.com/video.mp4" fileName="video.mp4" autoPlay={true} />;
@@ -133,6 +138,7 @@ You can customize the loader, container props, and icon props without modifying
133
138
  ```tsx
134
139
  import React from "react";
135
140
  import FilePreviewer from "react-file-preview-engine";
141
+ import "react-file-preview-engine/style.css";
136
142
 
137
143
  export default function App() {
138
144
  return (
@@ -154,6 +160,7 @@ When a renderer reports an error, the previewer switches to `errorRenderer`.
154
160
  ```tsx
155
161
  import React from "react";
156
162
  import FilePreviewer from "react-file-preview-engine";
163
+ import "react-file-preview-engine/style.css";
157
164
 
158
165
  const errorRenderer = {
159
166
  Component() {
@@ -189,6 +196,7 @@ This example adds support for markdown files.
189
196
  ```tsx
190
197
  import React, { useEffect, useState } from "react";
191
198
  import FilePreviewer from "react-file-preview-engine";
199
+ import "react-file-preview-engine/style.css";
192
200
  import type { Renderer } from "react-file-preview-engine/types";
193
201
  import remarkGfm from "remark-gfm";
194
202
  import rehypeRaw from "rehype-raw";
@@ -242,6 +250,7 @@ You can extend renderers with custom configuration using `additionalContext`. Th
242
250
  ```tsx
243
251
  import React, { useEffect, useMemo, useState } from "react";
244
252
  import FilePreviewer from "react-file-preview-engine";
253
+ import "react-file-preview-engine/style.css";
245
254
  import type { Renderer } from "react-file-preview-engine/types";
246
255
  import remarkGfm from "remark-gfm";
247
256
  import rehypeRaw from "rehype-raw";
@@ -0,0 +1,20 @@
1
+ import { i as FilePreviewerProps } from "./types-DHt-OXXj.mjs";
2
+ import React from "react";
3
+
4
+ //#region src/components.d.ts
5
+ declare function FilePreviewer<T extends object = {}>({
6
+ src,
7
+ mimeType,
8
+ fileName,
9
+ autoPlay,
10
+ loader,
11
+ customRenderers,
12
+ additionalContext,
13
+ errorRenderer,
14
+ containerProps,
15
+ iconProps,
16
+ onLoad,
17
+ onError
18
+ }: FilePreviewerProps<T>): React.JSX.Element;
19
+ //#endregion
20
+ export { FilePreviewer as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,237 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+ import { FileIcon, defaultStyles } from "react-file-icon";
3
+ import Mime from "mime/lite";
4
+ //#region src/constants.ts
5
+ const defaults = {
6
+ additionalContext: {},
7
+ customRenderers: [],
8
+ props: {}
9
+ };
10
+ const fileExtensionRegex = /\.([^.]+)$/;
11
+ //#endregion
12
+ //#region src/hooks.ts
13
+ function useResolvedSrc(src) {
14
+ const objectUrlRef = useRef(null);
15
+ const revokeObjectURL = () => {
16
+ if (!objectUrlRef.current) return;
17
+ URL.revokeObjectURL(objectUrlRef.current);
18
+ objectUrlRef.current = null;
19
+ };
20
+ const resolvedSrc = useMemo(() => {
21
+ revokeObjectURL();
22
+ if (typeof src === "string") return src;
23
+ const url = src instanceof File || src instanceof Blob ? URL.createObjectURL(src) : src instanceof ArrayBuffer ? URL.createObjectURL(new Blob([src])) : "";
24
+ if (url) objectUrlRef.current = url;
25
+ return url;
26
+ }, [src]);
27
+ useEffect(() => revokeObjectURL, []);
28
+ return resolvedSrc;
29
+ }
30
+ //#endregion
31
+ //#region src/lib/utils.ts
32
+ const composeClass = (baseClass, props) => `${baseClass}${props?.className ? " " + props.className : ""}`;
33
+ const composeProps = (baseClass, props, overrideProps) => {
34
+ const mergedProps = {
35
+ ...props,
36
+ ...overrideProps
37
+ };
38
+ return {
39
+ ...mergedProps,
40
+ style: {
41
+ ...props?.style,
42
+ ...overrideProps?.style
43
+ },
44
+ className: composeClass(baseClass, mergedProps)
45
+ };
46
+ };
47
+ async function fetchResource(src, type, signal) {
48
+ const res = await fetch(src, { signal });
49
+ if (!res.ok) throw new Error();
50
+ return res[type]();
51
+ }
52
+ const getFileExtension = (mimeType, fileName) => Mime.getExtension(mimeType) || fileName?.match(fileExtensionRegex)?.[1] || "";
53
+ const getFileType = (mimeType, fileName) => mimeType || fileName && Mime.getType(fileName) || "";
54
+ //#endregion
55
+ //#region src/lib/rendererRegistry.tsx
56
+ const audioRenderer = {
57
+ canRender: ({ mimeType }) => mimeType.startsWith("audio/"),
58
+ Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
59
+ return /* @__PURE__ */ React.createElement("audio", {
60
+ className: "rfpe-audio",
61
+ controls: true,
62
+ autoPlay,
63
+ onCanPlay: onLoad,
64
+ onError,
65
+ "aria-label": fileName || "Audio preview"
66
+ }, /* @__PURE__ */ React.createElement("source", {
67
+ src,
68
+ type: mimeType
69
+ }));
70
+ }
71
+ };
72
+ const fallbackRenderer = { Component({ mimeType, fileName, iconProps, onLoad }) {
73
+ const extension = useMemo(() => getFileExtension(mimeType, fileName), [mimeType, fileName]);
74
+ useEffect(() => {
75
+ onLoad();
76
+ }, []);
77
+ return /* @__PURE__ */ React.createElement("div", composeProps("rfpe-icon", iconProps), /* @__PURE__ */ React.createElement(FileIcon, {
78
+ extension,
79
+ ...defaultStyles[extension]
80
+ }));
81
+ } };
82
+ const defaultRenderers$1 = [
83
+ {
84
+ canRender: ({ mimeType }) => mimeType === "text/plain",
85
+ Component({ src, onLoad, onError }) {
86
+ const [data, setData] = useState("");
87
+ useEffect(() => {
88
+ const controller = new AbortController();
89
+ fetchResource(src, "text", controller.signal).then((data) => {
90
+ setData(data);
91
+ onLoad();
92
+ }).catch(onError);
93
+ return () => controller.abort();
94
+ }, [src]);
95
+ return /* @__PURE__ */ React.createElement("div", { className: "rfpe-text" }, data);
96
+ }
97
+ },
98
+ {
99
+ canRender: ({ mimeType }) => mimeType === "application/pdf",
100
+ Component({ src, onLoad, onError }) {
101
+ const [data, setData] = useState("");
102
+ useEffect(() => {
103
+ const controller = new AbortController();
104
+ let objectUrl;
105
+ fetchResource(src, "arrayBuffer", controller.signal).then((buffer) => {
106
+ const blob = new Blob([buffer], { type: "application/pdf" });
107
+ objectUrl = URL.createObjectURL(blob);
108
+ setData(objectUrl);
109
+ onLoad();
110
+ }).catch(onError);
111
+ return () => {
112
+ controller.abort();
113
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
114
+ };
115
+ }, [src]);
116
+ return /* @__PURE__ */ React.createElement("iframe", {
117
+ className: "rfpe-iframe",
118
+ src: data
119
+ });
120
+ }
121
+ },
122
+ {
123
+ canRender: ({ mimeType }) => mimeType === "text/html",
124
+ Component({ src, onLoad, onError }) {
125
+ const [data, setData] = useState("");
126
+ useEffect(() => {
127
+ const controller = new AbortController();
128
+ fetchResource(src, "text", controller.signal).then((data) => {
129
+ setData(data);
130
+ onLoad();
131
+ }).catch(onError);
132
+ return () => controller.abort();
133
+ }, [src]);
134
+ return /* @__PURE__ */ React.createElement("iframe", {
135
+ className: "rfpe-iframe",
136
+ src: `data:text/html; charset=utf-8,${encodeURIComponent(data)}`,
137
+ sandbox: ""
138
+ });
139
+ }
140
+ },
141
+ {
142
+ canRender: ({ mimeType }) => mimeType.startsWith("image/"),
143
+ Component({ src, fileName, onLoad, onError }) {
144
+ return /* @__PURE__ */ React.createElement("img", {
145
+ className: "rfpe-image",
146
+ src,
147
+ alt: fileName || "Image preview",
148
+ onLoad,
149
+ onError
150
+ });
151
+ }
152
+ },
153
+ audioRenderer,
154
+ {
155
+ canRender: ({ mimeType }) => mimeType.startsWith("video/"),
156
+ Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
157
+ return /* @__PURE__ */ React.createElement("video", {
158
+ className: "rfpe-video",
159
+ controls: true,
160
+ autoPlay,
161
+ onCanPlay: onLoad,
162
+ onError,
163
+ "aria-label": fileName || "Video preview"
164
+ }, /* @__PURE__ */ React.createElement("source", {
165
+ src,
166
+ type: mimeType
167
+ }));
168
+ }
169
+ }
170
+ ];
171
+ function resolveRenderer(customRenderers, ctx) {
172
+ return customRenderers.concat(defaultRenderers$1).find((r) => r.canRender?.(ctx)) ?? fallbackRenderer;
173
+ }
174
+ //#endregion
175
+ //#region src/components.tsx
176
+ const { additionalContext: defaultContext, customRenderers: defaultRenderers, props: defaultProps } = defaults;
177
+ function FilePreviewer({ src, mimeType, fileName = "", autoPlay = false, loader = /* @__PURE__ */ React.createElement(Loader, null), customRenderers = defaultRenderers, additionalContext = defaultContext, errorRenderer = fallbackRenderer, containerProps = defaultProps, iconProps = defaultProps, onLoad, onError }) {
178
+ const resolvedSrc = useResolvedSrc(src);
179
+ const fileType = useMemo(() => getFileType(mimeType, fileName), [mimeType, fileName]);
180
+ const fileKey = `${resolvedSrc}|${fileType}|${fileName}`;
181
+ const [state, setState] = useState({
182
+ key: fileKey,
183
+ status: "loading"
184
+ });
185
+ if (state.key !== fileKey) setState({
186
+ key: fileKey,
187
+ status: "loading"
188
+ });
189
+ const isLoading = state.status === "loading";
190
+ const handleLoad = () => {
191
+ setState((prev) => {
192
+ if (prev.key !== fileKey || prev.status !== "loading") return prev;
193
+ onLoad?.();
194
+ return {
195
+ key: fileKey,
196
+ status: "ready"
197
+ };
198
+ });
199
+ };
200
+ const handleError = () => {
201
+ setState((prev) => {
202
+ if (prev.key !== fileKey || prev.status === "error") return prev;
203
+ onError?.();
204
+ return {
205
+ key: fileKey,
206
+ status: "error"
207
+ };
208
+ });
209
+ };
210
+ const context = {
211
+ src: resolvedSrc,
212
+ mimeType: fileType,
213
+ fileName,
214
+ autoPlay,
215
+ iconProps,
216
+ onLoad: handleLoad,
217
+ onError: handleError,
218
+ ...additionalContext
219
+ };
220
+ const renderer = useMemo(() => resolveRenderer(customRenderers, context), [
221
+ resolvedSrc,
222
+ fileType,
223
+ fileName,
224
+ autoPlay,
225
+ additionalContext
226
+ ]);
227
+ const ActiveRenderer = state.status === "error" ? errorRenderer : renderer;
228
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, isLoading && loader, /* @__PURE__ */ React.createElement("div", composeProps("rfpe-container", containerProps, { style: { visibility: isLoading ? "hidden" : "visible" } }), /* @__PURE__ */ React.createElement(ActiveRenderer.Component, {
229
+ key: fileKey,
230
+ ...context
231
+ })));
232
+ }
233
+ function Loader({ children, text }) {
234
+ return /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader" }, /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader-spinner" }), children ?? /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader-text" }, text));
235
+ }
236
+ //#endregion
237
+ export { FilePreviewer as default };
package/dist/style.css ADDED
@@ -0,0 +1,63 @@
1
+ @layer rfpe {
2
+ :where(.rfpe-container) {
3
+ justify-content: center;
4
+ align-items: center;
5
+ max-width: 100vw;
6
+ height: 100%;
7
+ display: flex;
8
+ }
9
+
10
+ :where(.rfpe-icon) {
11
+ width: 50px;
12
+ }
13
+ }
14
+
15
+ .rfpe-audio {
16
+ width: 100%;
17
+ }
18
+
19
+ .rfpe-iframe {
20
+ background-color: #fff;
21
+ }
22
+
23
+ .rfpe-iframe, .rfpe-image, .rfpe-video {
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
28
+ .rfpe-image, .rfpe-video {
29
+ object-fit: contain;
30
+ }
31
+
32
+ .rfpe-text {
33
+ white-space: pre-wrap;
34
+ width: 100%;
35
+ height: 100%;
36
+ overflow: auto;
37
+ }
38
+
39
+ .rfpe-loader {
40
+ align-items: center;
41
+ gap: .5rem;
42
+ display: inline-flex;
43
+ }
44
+
45
+ .rfpe-loader-spinner {
46
+ box-sizing: border-box;
47
+ aspect-ratio: 1;
48
+ border: 2px solid #000;
49
+ border-color: #000 #0000;
50
+ border-radius: 50%;
51
+ width: 22px;
52
+ animation: .6s infinite rfpe-spin-fast;
53
+ }
54
+
55
+ .rfpe-loader-text {
56
+ font-size: .875rem;
57
+ }
58
+
59
+ @keyframes rfpe-spin-fast {
60
+ to {
61
+ transform: rotate(360deg);
62
+ }
63
+ }
@@ -0,0 +1,48 @@
1
+ import { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
2
+
3
+ //#region src/types.d.ts
4
+ type LoaderProps = {
5
+ children?: ReactNode;
6
+ text?: string;
7
+ };
8
+ type EventHandler = () => void;
9
+ type RenderBehaviour = {
10
+ autoPlay: boolean;
11
+ iconProps: DivProps;
12
+ onLoad: EventHandler;
13
+ onError: EventHandler;
14
+ };
15
+ type RenderContext<T extends object = {}> = {
16
+ src: string;
17
+ mimeType: string;
18
+ fileName: string;
19
+ } & RenderBehaviour & T;
20
+ type Renderer<T extends object = {}> = {
21
+ name?: string;
22
+ canRender?(ctx: RenderContext<T>): boolean;
23
+ Component(ctx: RenderContext<T>): ReactNode;
24
+ };
25
+ type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
26
+ type FetchType = "text" | "arrayBuffer" | "blob" | "json";
27
+ type FilePreviewerProps<T extends object> = {
28
+ src: FileSource;
29
+ loader?: ReactNode;
30
+ customRenderers?: Renderer<T>[];
31
+ additionalContext?: T;
32
+ errorRenderer?: Renderer<T>;
33
+ containerProps?: DivProps;
34
+ } & MimeTypeSource & Partial<RenderBehaviour>;
35
+ type FileSource = string | File | Blob | ArrayBuffer;
36
+ type MimeTypeSource = {
37
+ mimeType: string;
38
+ fileName?: string;
39
+ } | {
40
+ fileName: string;
41
+ mimeType?: string;
42
+ };
43
+ type State = {
44
+ key: string;
45
+ status: "loading" | "ready" | "error";
46
+ };
47
+ //#endregion
48
+ export { FileSource as a, RenderBehaviour as c, State as d, FilePreviewerProps as i, RenderContext as l, EventHandler as n, LoaderProps as o, FetchType as r, MimeTypeSource as s, DivProps as t, Renderer as u };
@@ -0,0 +1,2 @@
1
+ import { a as FileSource, c as RenderBehaviour, d as State, i as FilePreviewerProps, l as RenderContext, n as EventHandler, o as LoaderProps, r as FetchType, s as MimeTypeSource, t as DivProps, u as Renderer } from "./types-DHt-OXXj.mjs";
2
+ export { DivProps, EventHandler, FetchType, FilePreviewerProps, FileSource, LoaderProps, MimeTypeSource, RenderBehaviour, RenderContext, Renderer, State };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-file-preview-engine",
3
- "version": "0.1.8",
3
+ "version": "1.0.0",
4
4
  "description": "A renderer-driven React file preview engine with extensible architecture for images, video, audio, pdf, html, and custom formats.",
5
5
  "license": "MIT",
6
6
  "author": "Sahil Aggarwal <aggarwalsahil2004@gmail.com>",
@@ -15,17 +15,17 @@
15
15
  },
16
16
  "type": "module",
17
17
  "exports": {
18
- ".": "./dist/index.js",
19
- "./types": "./dist/types.js"
18
+ ".": "./dist/index.mjs",
19
+ "./style.css": "./dist/style.css",
20
+ "./types": "./dist/types.mjs"
20
21
  },
21
- "main": "dist/index.js",
22
22
  "files": [
23
23
  "dist"
24
24
  ],
25
25
  "sideEffects": [
26
26
  "**/*.css"
27
27
  ],
28
- "types": "dist/index.d.ts",
28
+ "types": "./dist/index.d.mts",
29
29
  "dependencies": {
30
30
  "mime": "^4.1.0",
31
31
  "react-file-icon": "^1.6.0"
@@ -35,12 +35,12 @@
35
35
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@release-it/conventional-changelog": "^10.0.4",
39
- "@types/react": "^19.2.7",
38
+ "@release-it/conventional-changelog": "^10.0.6",
39
+ "@types/react": "^19.2.14",
40
40
  "@types/react-file-icon": "^1.0.4",
41
41
  "prettier-package-json": "^2.8.0",
42
- "release-it": "^19.2.2",
43
- "tsup": "^8.5.1",
42
+ "release-it": "^19.2.4",
43
+ "tsdown": "^0.21.2",
44
44
  "typescript": "^5.9.3"
45
45
  },
46
46
  "keywords": [
@@ -67,8 +67,8 @@
67
67
  ],
68
68
  "scripts": {
69
69
  "build": "pnpm i && pnpm run compile",
70
- "compile": "tsup",
71
- "dev": "tsup --watch",
70
+ "compile": "tsdown",
71
+ "dev": "tsdown --watch",
72
72
  "dry-release": "release-it --ci --dry-run",
73
73
  "prettier": "prettier-package-json --write package.json",
74
74
  "pub": "pnpm login && pnpm publish",
@@ -1,238 +0,0 @@
1
- import React, { useMemo, useState, useRef, useEffect } from 'react';
2
- import { FileIcon, defaultStyles } from 'react-file-icon';
3
- import Mime from 'mime/lite';
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
-
45
- // src/constants.ts
46
- var defaults = { additionalContext: {}, customRenderers: [], props: {} };
47
- var fileExtensionRegex = /\.([^.]+)$/;
48
- function useResolvedSrc(src) {
49
- const objectUrlRef = useRef(null);
50
- const revokeObjectURL = () => {
51
- if (!objectUrlRef.current) return;
52
- URL.revokeObjectURL(objectUrlRef.current);
53
- objectUrlRef.current = null;
54
- };
55
- const resolvedSrc = useMemo(() => {
56
- revokeObjectURL();
57
- if (typeof src === "string") return src;
58
- const url = src instanceof File || src instanceof Blob ? URL.createObjectURL(src) : src instanceof ArrayBuffer ? URL.createObjectURL(new Blob([src])) : "";
59
- if (url) objectUrlRef.current = url;
60
- return url;
61
- }, [src]);
62
- useEffect(() => revokeObjectURL, []);
63
- return resolvedSrc;
64
- }
65
- var composeClass = (baseClass, props) => `${baseClass}${(props == null ? void 0 : props.className) ? " " + props.className : ""}`;
66
- var composeProps = (baseClass, props, overrideProps) => {
67
- const mergedProps = __spreadValues(__spreadValues({}, props), overrideProps);
68
- return __spreadProps(__spreadValues({}, mergedProps), {
69
- style: __spreadValues(__spreadValues({}, props == null ? void 0 : props.style), overrideProps == null ? void 0 : overrideProps.style),
70
- className: composeClass(baseClass, mergedProps)
71
- });
72
- };
73
- function fetchResource(src, type, signal) {
74
- return __async(this, null, function* () {
75
- const res = yield fetch(src, { signal });
76
- if (!res.ok) throw new Error();
77
- return res[type]();
78
- });
79
- }
80
- var getFileExtension = (mimeType, fileName) => {
81
- var _a;
82
- return Mime.getExtension(mimeType) || ((_a = fileName == null ? void 0 : fileName.match(fileExtensionRegex)) == null ? void 0 : _a[1]) || "";
83
- };
84
- var getFileType = (mimeType, fileName) => mimeType || fileName && Mime.getType(fileName) || "";
85
-
86
- // src/lib/rendererRegistry.tsx
87
- var audioRenderer = {
88
- canRender: ({ mimeType }) => mimeType.startsWith("audio/"),
89
- Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
90
- return /* @__PURE__ */ React.createElement("audio", { className: "rfpe-audio", controls: true, autoPlay, onCanPlay: onLoad, onError, "aria-label": fileName || "Audio preview" }, /* @__PURE__ */ React.createElement("source", { src, type: mimeType }));
91
- }
92
- };
93
- var fallbackRenderer = {
94
- Component({ mimeType, fileName, iconProps, onLoad }) {
95
- const extension = useMemo(() => getFileExtension(mimeType, fileName), [mimeType, fileName]);
96
- useEffect(() => {
97
- onLoad();
98
- }, []);
99
- return /* @__PURE__ */ React.createElement("div", __spreadValues({}, composeProps("rfpe-icon", iconProps)), /* @__PURE__ */ React.createElement(FileIcon, __spreadValues({ extension }, defaultStyles[extension])));
100
- }
101
- };
102
- var htmlRenderer = {
103
- canRender: ({ mimeType }) => mimeType === "text/html",
104
- Component({ src, onLoad, onError }) {
105
- const [data, setData] = useState("");
106
- useEffect(() => {
107
- const controller = new AbortController();
108
- fetchResource(src, "text", controller.signal).then((data2) => {
109
- setData(data2);
110
- onLoad();
111
- }).catch(onError);
112
- return () => controller.abort();
113
- }, [src]);
114
- return /* @__PURE__ */ React.createElement("iframe", { className: "rfpe-iframe", src: `data:text/html; charset=utf-8,${encodeURIComponent(data)}`, sandbox: "" });
115
- }
116
- };
117
- var imageRenderer = {
118
- canRender: ({ mimeType }) => mimeType.startsWith("image/"),
119
- Component({ src, fileName, onLoad, onError }) {
120
- return /* @__PURE__ */ React.createElement("img", { className: "rfpe-image", src, alt: fileName || "Image preview", onLoad, onError });
121
- }
122
- };
123
- var pdfRenderer = {
124
- canRender: ({ mimeType }) => mimeType === "application/pdf",
125
- Component({ src, onLoad, onError }) {
126
- const [data, setData] = useState("");
127
- useEffect(() => {
128
- const controller = new AbortController();
129
- let objectUrl;
130
- fetchResource(src, "arrayBuffer", controller.signal).then((buffer) => {
131
- const blob = new Blob([buffer], { type: "application/pdf" });
132
- objectUrl = URL.createObjectURL(blob);
133
- setData(objectUrl);
134
- onLoad();
135
- }).catch(onError);
136
- return () => {
137
- controller.abort();
138
- if (objectUrl) URL.revokeObjectURL(objectUrl);
139
- };
140
- }, [src]);
141
- return /* @__PURE__ */ React.createElement("iframe", { className: "rfpe-iframe", src: data });
142
- }
143
- };
144
- var textRenderer = {
145
- canRender: ({ mimeType }) => mimeType === "text/plain",
146
- Component({ src, onLoad, onError }) {
147
- const [data, setData] = useState("");
148
- useEffect(() => {
149
- const controller = new AbortController();
150
- fetchResource(src, "text", controller.signal).then((data2) => {
151
- setData(data2);
152
- onLoad();
153
- }).catch(onError);
154
- return () => controller.abort();
155
- }, [src]);
156
- return /* @__PURE__ */ React.createElement("div", { className: "rfpe-text" }, data);
157
- }
158
- };
159
- var videoRenderer = {
160
- canRender: ({ mimeType }) => mimeType.startsWith("video/"),
161
- Component({ src, mimeType, fileName, autoPlay, onLoad, onError }) {
162
- return /* @__PURE__ */ React.createElement("video", { className: "rfpe-video", controls: true, autoPlay, onCanPlay: onLoad, onError, "aria-label": fileName || "Video preview" }, /* @__PURE__ */ React.createElement("source", { src, type: mimeType }));
163
- }
164
- };
165
- var defaultRenderers = [textRenderer, pdfRenderer, htmlRenderer, imageRenderer, audioRenderer, videoRenderer];
166
- function resolveRenderer(customRenderers, ctx) {
167
- var _a;
168
- return (_a = customRenderers.concat(defaultRenderers).find((r) => {
169
- var _a2;
170
- return (_a2 = r.canRender) == null ? void 0 : _a2.call(r, ctx);
171
- })) != null ? _a : fallbackRenderer;
172
- }
173
-
174
- // src/styles.css
175
- function injectStyle(css) {
176
- if (typeof document === "undefined") return;
177
- const head = document.head || document.getElementsByTagName("head")[0];
178
- const style = document.createElement("style");
179
- style.type = "text/css";
180
- if (head.firstChild) {
181
- head.insertBefore(style, head.firstChild);
182
- } else {
183
- head.appendChild(style);
184
- }
185
- if (style.styleSheet) {
186
- style.styleSheet.cssText = css;
187
- } else {
188
- style.appendChild(document.createTextNode(css));
189
- }
190
- }
191
- injectStyle("@layer rfpe {\n :where(.rfpe-container) {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n max-width: 100lvw;\n max-width: 100vw;\n }\n :where(.rfpe-icon) {\n width: 50px;\n }\n}\n.rfpe-audio {\n width: 100%;\n}\n.rfpe-iframe {\n background-color: white;\n}\n.rfpe-iframe,\n.rfpe-image,\n.rfpe-video {\n width: 100%;\n height: 100%;\n}\n.rfpe-image,\n.rfpe-video {\n object-fit: contain;\n}\n.rfpe-text {\n width: 100%;\n height: 100%;\n overflow: auto;\n white-space: pre-wrap;\n}\n.rfpe-loader {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.rfpe-loader-spinner {\n box-sizing: border-box;\n width: 22px;\n aspect-ratio: 1;\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");
192
-
193
- // src/components.tsx
194
- var { additionalContext: defaultContext, customRenderers: defaultRenderers2, props: defaultProps } = defaults;
195
- function FilePreviewer({
196
- src,
197
- mimeType,
198
- fileName = "",
199
- autoPlay = false,
200
- loader = /* @__PURE__ */ React.createElement(Loader, null),
201
- customRenderers = defaultRenderers2,
202
- additionalContext = defaultContext,
203
- errorRenderer = fallbackRenderer,
204
- containerProps = defaultProps,
205
- iconProps = defaultProps,
206
- onLoad,
207
- onError
208
- }) {
209
- const resolvedSrc = useResolvedSrc(src);
210
- const fileType = useMemo(() => getFileType(mimeType, fileName), [mimeType, fileName]);
211
- const fileKey = `${resolvedSrc}|${fileType}|${fileName}`;
212
- const [state, setState] = useState({ key: fileKey, status: "loading" });
213
- if (state.key !== fileKey) setState({ key: fileKey, status: "loading" });
214
- const isLoading = state.status === "loading";
215
- const handleLoad = () => {
216
- setState((prev) => {
217
- if (prev.key !== fileKey || prev.status !== "loading") return prev;
218
- onLoad == null ? void 0 : onLoad();
219
- return { key: fileKey, status: "ready" };
220
- });
221
- };
222
- const handleError = () => {
223
- setState((prev) => {
224
- if (prev.key !== fileKey || prev.status === "error") return prev;
225
- onError == null ? void 0 : onError();
226
- return { key: fileKey, status: "error" };
227
- });
228
- };
229
- const context = __spreadValues({ src: resolvedSrc, mimeType: fileType, fileName, autoPlay, iconProps, onLoad: handleLoad, onError: handleError }, additionalContext);
230
- const renderer = useMemo(() => resolveRenderer(customRenderers, context), [resolvedSrc, fileType, fileName, autoPlay, additionalContext]);
231
- const ActiveRenderer = state.status === "error" ? errorRenderer : renderer;
232
- return /* @__PURE__ */ React.createElement(React.Fragment, null, isLoading && loader, /* @__PURE__ */ React.createElement("div", __spreadValues({}, composeProps("rfpe-container", containerProps, { style: { visibility: isLoading ? "hidden" : "visible" } })), /* @__PURE__ */ React.createElement(ActiveRenderer.Component, __spreadValues({ key: fileKey }, context))));
233
- }
234
- function Loader({ children, text }) {
235
- return /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader" }, /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader-spinner" }), children != null ? children : /* @__PURE__ */ React.createElement("div", { className: "rfpe-loader-text" }, text));
236
- }
237
-
238
- export { FilePreviewer };
@@ -1,6 +0,0 @@
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 };
@@ -1 +0,0 @@
1
- export { FilePreviewer as default } from './chunks/chunk-YEB7RW6X.js';
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export { default } from './components.js';
2
- import 'react';
3
- import './types.js';
package/dist/index.js DELETED
@@ -1 +0,0 @@
1
- export { FilePreviewer as default } from './chunks/chunk-YEB7RW6X.js';
package/dist/types.d.ts DELETED
@@ -1,47 +0,0 @@
1
- import { ReactNode, DetailedHTMLProps, HTMLAttributes } 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>): ReactNode;
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 & Partial<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 DELETED
@@ -1 +0,0 @@
1
-