@fluxfiles/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.
- package/dist/index.d.mts +202 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +305 -0
- package/dist/index.mjs +276 -0
- package/package.json +58 -0
- package/src/FluxFiles.tsx +91 -0
- package/src/FluxFilesModal.tsx +134 -0
- package/src/index.ts +17 -0
- package/src/types.ts +124 -0
- package/src/useFluxFiles.ts +138 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/FluxFiles.tsx
|
|
2
|
+
import { forwardRef, useImperativeHandle } from "react";
|
|
3
|
+
|
|
4
|
+
// src/useFluxFiles.ts
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
var SOURCE = "fluxfiles";
|
|
7
|
+
var VERSION = 1;
|
|
8
|
+
function uid() {
|
|
9
|
+
return "ff-" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
10
|
+
}
|
|
11
|
+
function useFluxFiles(options) {
|
|
12
|
+
const iframeElRef = useRef(null);
|
|
13
|
+
const [ready, setReady] = useState(false);
|
|
14
|
+
const optionsRef = useRef(options);
|
|
15
|
+
optionsRef.current = options;
|
|
16
|
+
const endpoint = (options.endpoint || "").replace(/\/+$/, "");
|
|
17
|
+
const iframeSrc = endpoint + "/public/index.html";
|
|
18
|
+
const post = useCallback((type, payload = {}) => {
|
|
19
|
+
const el = iframeElRef.current;
|
|
20
|
+
if (!el?.contentWindow) return;
|
|
21
|
+
el.contentWindow.postMessage(
|
|
22
|
+
{ source: SOURCE, type, v: VERSION, id: uid(), payload },
|
|
23
|
+
"*"
|
|
24
|
+
);
|
|
25
|
+
}, []);
|
|
26
|
+
const sendConfig = useCallback(() => {
|
|
27
|
+
const opts = optionsRef.current;
|
|
28
|
+
post("FM_CONFIG", {
|
|
29
|
+
disk: opts.disk || "local",
|
|
30
|
+
token: opts.token || "",
|
|
31
|
+
mode: opts.mode || "picker",
|
|
32
|
+
allowedTypes: opts.allowedTypes || null,
|
|
33
|
+
maxSize: opts.maxSize || null,
|
|
34
|
+
endpoint: opts.endpoint || "",
|
|
35
|
+
locale: opts.locale || null
|
|
36
|
+
});
|
|
37
|
+
}, [post]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
function onMessage(e) {
|
|
40
|
+
const msg = e.data;
|
|
41
|
+
if (!msg || msg.source !== SOURCE) return;
|
|
42
|
+
const opts = optionsRef.current;
|
|
43
|
+
switch (msg.type) {
|
|
44
|
+
case "FM_READY":
|
|
45
|
+
setReady(true);
|
|
46
|
+
sendConfig();
|
|
47
|
+
opts.onReady?.();
|
|
48
|
+
break;
|
|
49
|
+
case "FM_SELECT":
|
|
50
|
+
opts.onSelect?.(msg.payload);
|
|
51
|
+
break;
|
|
52
|
+
case "FM_EVENT":
|
|
53
|
+
opts.onEvent?.(msg.payload);
|
|
54
|
+
break;
|
|
55
|
+
case "FM_CLOSE":
|
|
56
|
+
opts.onClose?.();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
window.addEventListener("message", onMessage);
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener("message", onMessage);
|
|
63
|
+
};
|
|
64
|
+
}, [sendConfig]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (ready) {
|
|
67
|
+
sendConfig();
|
|
68
|
+
}
|
|
69
|
+
}, [options.token, options.disk, options.mode, options.locale, ready, sendConfig]);
|
|
70
|
+
const command = useCallback(
|
|
71
|
+
(action, data = {}) => {
|
|
72
|
+
post("FM_COMMAND", { action, ...data });
|
|
73
|
+
},
|
|
74
|
+
[post]
|
|
75
|
+
);
|
|
76
|
+
const navigate = useCallback((path) => command("navigate", { path }), [command]);
|
|
77
|
+
const setDisk = useCallback((disk) => command("setDisk", { disk }), [command]);
|
|
78
|
+
const refresh = useCallback(() => command("refresh"), [command]);
|
|
79
|
+
const search = useCallback((q) => command("search", { q }), [command]);
|
|
80
|
+
const crossCopy = useCallback((dstDisk, dstPath) => command("crossCopy", { dst_disk: dstDisk, dst_path: dstPath || "" }), [command]);
|
|
81
|
+
const crossMove = useCallback((dstDisk, dstPath) => command("crossMove", { dst_disk: dstDisk, dst_path: dstPath || "" }), [command]);
|
|
82
|
+
const crop = useCallback((x, y, width, height, savePath) => command("crop", { x, y, width, height, save_path: savePath || "" }), [command]);
|
|
83
|
+
const aiTag = useCallback(() => command("aiTag"), [command]);
|
|
84
|
+
const iframeRef = useCallback((el) => {
|
|
85
|
+
iframeElRef.current = el;
|
|
86
|
+
if (!el) {
|
|
87
|
+
setReady(false);
|
|
88
|
+
}
|
|
89
|
+
}, []);
|
|
90
|
+
return {
|
|
91
|
+
iframeRef,
|
|
92
|
+
iframeSrc,
|
|
93
|
+
ready,
|
|
94
|
+
command,
|
|
95
|
+
navigate,
|
|
96
|
+
setDisk,
|
|
97
|
+
refresh,
|
|
98
|
+
search,
|
|
99
|
+
crossCopy,
|
|
100
|
+
crossMove,
|
|
101
|
+
crop,
|
|
102
|
+
aiTag
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/FluxFiles.tsx
|
|
107
|
+
import { jsx } from "react/jsx-runtime";
|
|
108
|
+
var FluxFiles = forwardRef(
|
|
109
|
+
function FluxFiles2(props, ref) {
|
|
110
|
+
const {
|
|
111
|
+
endpoint,
|
|
112
|
+
token,
|
|
113
|
+
disk,
|
|
114
|
+
mode,
|
|
115
|
+
allowedTypes,
|
|
116
|
+
maxSize,
|
|
117
|
+
width = "100%",
|
|
118
|
+
height = "600px",
|
|
119
|
+
className,
|
|
120
|
+
style,
|
|
121
|
+
onSelect,
|
|
122
|
+
onClose,
|
|
123
|
+
onReady,
|
|
124
|
+
onEvent
|
|
125
|
+
} = props;
|
|
126
|
+
const handle = useFluxFiles({
|
|
127
|
+
endpoint,
|
|
128
|
+
token,
|
|
129
|
+
disk,
|
|
130
|
+
mode,
|
|
131
|
+
allowedTypes,
|
|
132
|
+
maxSize,
|
|
133
|
+
onSelect,
|
|
134
|
+
onClose,
|
|
135
|
+
onReady,
|
|
136
|
+
onEvent
|
|
137
|
+
});
|
|
138
|
+
useImperativeHandle(ref, () => ({
|
|
139
|
+
command: handle.command,
|
|
140
|
+
navigate: handle.navigate,
|
|
141
|
+
setDisk: handle.setDisk,
|
|
142
|
+
refresh: handle.refresh,
|
|
143
|
+
search: handle.search,
|
|
144
|
+
crossCopy: handle.crossCopy,
|
|
145
|
+
crossMove: handle.crossMove,
|
|
146
|
+
crop: handle.crop,
|
|
147
|
+
aiTag: handle.aiTag,
|
|
148
|
+
ready: handle.ready
|
|
149
|
+
}), [handle]);
|
|
150
|
+
const containerStyle = {
|
|
151
|
+
width: typeof width === "number" ? `${width}px` : width,
|
|
152
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
153
|
+
...style
|
|
154
|
+
};
|
|
155
|
+
return /* @__PURE__ */ jsx("div", { className, style: containerStyle, children: /* @__PURE__ */ jsx(
|
|
156
|
+
"iframe",
|
|
157
|
+
{
|
|
158
|
+
ref: handle.iframeRef,
|
|
159
|
+
src: handle.iframeSrc,
|
|
160
|
+
style: { width: "100%", height: "100%", border: "none" },
|
|
161
|
+
allow: "clipboard-write",
|
|
162
|
+
title: "FluxFiles File Manager"
|
|
163
|
+
}
|
|
164
|
+
) });
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// src/FluxFilesModal.tsx
|
|
169
|
+
import { useCallback as useCallback2, useEffect as useEffect2 } from "react";
|
|
170
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
171
|
+
var defaultOverlayStyle = {
|
|
172
|
+
position: "fixed",
|
|
173
|
+
inset: 0,
|
|
174
|
+
background: "rgba(0, 0, 0, 0.5)",
|
|
175
|
+
zIndex: 99999,
|
|
176
|
+
display: "flex",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "center"
|
|
179
|
+
};
|
|
180
|
+
var defaultModalStyle = {
|
|
181
|
+
width: "90vw",
|
|
182
|
+
maxWidth: "1200px",
|
|
183
|
+
height: "85vh",
|
|
184
|
+
background: "#fff",
|
|
185
|
+
borderRadius: "8px",
|
|
186
|
+
overflow: "hidden",
|
|
187
|
+
boxShadow: "0 25px 50px rgba(0, 0, 0, 0.25)"
|
|
188
|
+
};
|
|
189
|
+
function FluxFilesModal({
|
|
190
|
+
open,
|
|
191
|
+
endpoint,
|
|
192
|
+
token,
|
|
193
|
+
disk,
|
|
194
|
+
mode = "picker",
|
|
195
|
+
allowedTypes,
|
|
196
|
+
maxSize,
|
|
197
|
+
onSelect,
|
|
198
|
+
onClose,
|
|
199
|
+
onReady,
|
|
200
|
+
onEvent,
|
|
201
|
+
overlayClassName,
|
|
202
|
+
modalClassName
|
|
203
|
+
}) {
|
|
204
|
+
const handle = useFluxFiles({
|
|
205
|
+
endpoint,
|
|
206
|
+
token,
|
|
207
|
+
disk,
|
|
208
|
+
mode,
|
|
209
|
+
allowedTypes,
|
|
210
|
+
maxSize,
|
|
211
|
+
onSelect,
|
|
212
|
+
onClose,
|
|
213
|
+
onReady,
|
|
214
|
+
onEvent
|
|
215
|
+
});
|
|
216
|
+
useEffect2(() => {
|
|
217
|
+
if (!open) return;
|
|
218
|
+
function onKeyDown(e) {
|
|
219
|
+
if (e.key === "Escape") {
|
|
220
|
+
onClose?.();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
document.addEventListener("keydown", onKeyDown);
|
|
224
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
225
|
+
}, [open, onClose]);
|
|
226
|
+
useEffect2(() => {
|
|
227
|
+
if (!open) return;
|
|
228
|
+
const prev = document.body.style.overflow;
|
|
229
|
+
document.body.style.overflow = "hidden";
|
|
230
|
+
return () => {
|
|
231
|
+
document.body.style.overflow = prev;
|
|
232
|
+
};
|
|
233
|
+
}, [open]);
|
|
234
|
+
const handleOverlayClick = useCallback2(
|
|
235
|
+
(e) => {
|
|
236
|
+
if (e.target === e.currentTarget) {
|
|
237
|
+
onClose?.();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[onClose]
|
|
241
|
+
);
|
|
242
|
+
if (!open) return null;
|
|
243
|
+
return /* @__PURE__ */ jsx2(
|
|
244
|
+
"div",
|
|
245
|
+
{
|
|
246
|
+
className: overlayClassName,
|
|
247
|
+
style: overlayClassName ? void 0 : defaultOverlayStyle,
|
|
248
|
+
onClick: handleOverlayClick,
|
|
249
|
+
role: "dialog",
|
|
250
|
+
"aria-modal": "true",
|
|
251
|
+
"aria-label": "FluxFiles File Manager",
|
|
252
|
+
children: /* @__PURE__ */ jsx2(
|
|
253
|
+
"div",
|
|
254
|
+
{
|
|
255
|
+
className: modalClassName,
|
|
256
|
+
style: modalClassName ? void 0 : defaultModalStyle,
|
|
257
|
+
children: /* @__PURE__ */ jsx2(
|
|
258
|
+
"iframe",
|
|
259
|
+
{
|
|
260
|
+
ref: handle.iframeRef,
|
|
261
|
+
src: handle.iframeSrc,
|
|
262
|
+
style: { width: "100%", height: "100%", border: "none" },
|
|
263
|
+
allow: "clipboard-write",
|
|
264
|
+
title: "FluxFiles File Manager"
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
export {
|
|
273
|
+
FluxFiles,
|
|
274
|
+
FluxFilesModal,
|
|
275
|
+
useFluxFiles
|
|
276
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxfiles/react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React components and hooks for FluxFiles file manager",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
22
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
27
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^18.0.0",
|
|
31
|
+
"@types/react-dom": "^18.0.0",
|
|
32
|
+
"react": "^18.0.0",
|
|
33
|
+
"react-dom": "^18.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"author": "thai-pc",
|
|
38
|
+
"homepage": "https://github.com/thai-pc/fluxfiles#react",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/thai-pc/fluxfiles.git",
|
|
42
|
+
"directory": "adapters/react"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/thai-pc/fluxfiles/issues"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"fluxfiles",
|
|
52
|
+
"file-manager",
|
|
53
|
+
"react",
|
|
54
|
+
"s3",
|
|
55
|
+
"r2",
|
|
56
|
+
"upload"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import type { FluxFilesProps, FluxFilesHandle } from './types';
|
|
3
|
+
import { useFluxFiles } from './useFluxFiles';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Embedded FluxFiles file manager component.
|
|
7
|
+
*
|
|
8
|
+
* Renders an iframe inside a container div. Use `ref` to access command methods.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const ref = useRef<FluxFilesHandle>(null);
|
|
13
|
+
*
|
|
14
|
+
* <FluxFiles
|
|
15
|
+
* ref={ref}
|
|
16
|
+
* endpoint="https://files.example.com"
|
|
17
|
+
* token={jwt}
|
|
18
|
+
* disk="local"
|
|
19
|
+
* onSelect={(file) => console.log(file)}
|
|
20
|
+
* height="600px"
|
|
21
|
+
* />
|
|
22
|
+
*
|
|
23
|
+
* // Programmatic control:
|
|
24
|
+
* ref.current?.navigate('/uploads');
|
|
25
|
+
* ref.current?.refresh();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const FluxFiles = forwardRef<FluxFilesHandle, FluxFilesProps>(
|
|
29
|
+
function FluxFiles(props, ref) {
|
|
30
|
+
const {
|
|
31
|
+
endpoint,
|
|
32
|
+
token,
|
|
33
|
+
disk,
|
|
34
|
+
mode,
|
|
35
|
+
allowedTypes,
|
|
36
|
+
maxSize,
|
|
37
|
+
width = '100%',
|
|
38
|
+
height = '600px',
|
|
39
|
+
className,
|
|
40
|
+
style,
|
|
41
|
+
onSelect,
|
|
42
|
+
onClose,
|
|
43
|
+
onReady,
|
|
44
|
+
onEvent,
|
|
45
|
+
} = props;
|
|
46
|
+
|
|
47
|
+
const handle = useFluxFiles({
|
|
48
|
+
endpoint,
|
|
49
|
+
token,
|
|
50
|
+
disk,
|
|
51
|
+
mode,
|
|
52
|
+
allowedTypes,
|
|
53
|
+
maxSize,
|
|
54
|
+
onSelect,
|
|
55
|
+
onClose,
|
|
56
|
+
onReady,
|
|
57
|
+
onEvent,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
useImperativeHandle(ref, () => ({
|
|
61
|
+
command: handle.command,
|
|
62
|
+
navigate: handle.navigate,
|
|
63
|
+
setDisk: handle.setDisk,
|
|
64
|
+
refresh: handle.refresh,
|
|
65
|
+
search: handle.search,
|
|
66
|
+
crossCopy: handle.crossCopy,
|
|
67
|
+
crossMove: handle.crossMove,
|
|
68
|
+
crop: handle.crop,
|
|
69
|
+
aiTag: handle.aiTag,
|
|
70
|
+
ready: handle.ready,
|
|
71
|
+
}), [handle]);
|
|
72
|
+
|
|
73
|
+
const containerStyle: React.CSSProperties = {
|
|
74
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
75
|
+
height: typeof height === 'number' ? `${height}px` : height,
|
|
76
|
+
...style,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={className} style={containerStyle}>
|
|
81
|
+
<iframe
|
|
82
|
+
ref={handle.iframeRef}
|
|
83
|
+
src={handle.iframeSrc}
|
|
84
|
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
|
85
|
+
allow="clipboard-write"
|
|
86
|
+
title="FluxFiles File Manager"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from 'react';
|
|
2
|
+
import type { FluxFilesModalProps } from './types';
|
|
3
|
+
import { useFluxFiles } from './useFluxFiles';
|
|
4
|
+
|
|
5
|
+
const defaultOverlayStyle: React.CSSProperties = {
|
|
6
|
+
position: 'fixed',
|
|
7
|
+
inset: 0,
|
|
8
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
9
|
+
zIndex: 99999,
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const defaultModalStyle: React.CSSProperties = {
|
|
16
|
+
width: '90vw',
|
|
17
|
+
maxWidth: '1200px',
|
|
18
|
+
height: '85vh',
|
|
19
|
+
background: '#fff',
|
|
20
|
+
borderRadius: '8px',
|
|
21
|
+
overflow: 'hidden',
|
|
22
|
+
boxShadow: '0 25px 50px rgba(0, 0, 0, 0.25)',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Modal wrapper for FluxFiles.
|
|
27
|
+
*
|
|
28
|
+
* Renders a fullscreen overlay with the file manager when `open` is true.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const [open, setOpen] = useState(false);
|
|
33
|
+
*
|
|
34
|
+
* <button onClick={() => setOpen(true)}>Pick file</button>
|
|
35
|
+
*
|
|
36
|
+
* <FluxFilesModal
|
|
37
|
+
* open={open}
|
|
38
|
+
* endpoint="https://files.example.com"
|
|
39
|
+
* token={jwt}
|
|
40
|
+
* onSelect={(file) => {
|
|
41
|
+
* console.log(file);
|
|
42
|
+
* setOpen(false);
|
|
43
|
+
* }}
|
|
44
|
+
* onClose={() => setOpen(false)}
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function FluxFilesModal({
|
|
49
|
+
open,
|
|
50
|
+
endpoint,
|
|
51
|
+
token,
|
|
52
|
+
disk,
|
|
53
|
+
mode = 'picker',
|
|
54
|
+
allowedTypes,
|
|
55
|
+
maxSize,
|
|
56
|
+
onSelect,
|
|
57
|
+
onClose,
|
|
58
|
+
onReady,
|
|
59
|
+
onEvent,
|
|
60
|
+
overlayClassName,
|
|
61
|
+
modalClassName,
|
|
62
|
+
}: FluxFilesModalProps) {
|
|
63
|
+
const handle = useFluxFiles({
|
|
64
|
+
endpoint,
|
|
65
|
+
token,
|
|
66
|
+
disk,
|
|
67
|
+
mode,
|
|
68
|
+
allowedTypes,
|
|
69
|
+
maxSize,
|
|
70
|
+
onSelect,
|
|
71
|
+
onClose,
|
|
72
|
+
onReady,
|
|
73
|
+
onEvent,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Close on escape
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open) return;
|
|
79
|
+
|
|
80
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
81
|
+
if (e.key === 'Escape') {
|
|
82
|
+
onClose?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
document.addEventListener('keydown', onKeyDown);
|
|
87
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
88
|
+
}, [open, onClose]);
|
|
89
|
+
|
|
90
|
+
// Prevent body scroll when open
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open) return;
|
|
93
|
+
const prev = document.body.style.overflow;
|
|
94
|
+
document.body.style.overflow = 'hidden';
|
|
95
|
+
return () => {
|
|
96
|
+
document.body.style.overflow = prev;
|
|
97
|
+
};
|
|
98
|
+
}, [open]);
|
|
99
|
+
|
|
100
|
+
const handleOverlayClick = useCallback(
|
|
101
|
+
(e: React.MouseEvent) => {
|
|
102
|
+
if (e.target === e.currentTarget) {
|
|
103
|
+
onClose?.();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[onClose]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!open) return null;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={overlayClassName}
|
|
114
|
+
style={overlayClassName ? undefined : defaultOverlayStyle}
|
|
115
|
+
onClick={handleOverlayClick}
|
|
116
|
+
role="dialog"
|
|
117
|
+
aria-modal="true"
|
|
118
|
+
aria-label="FluxFiles File Manager"
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
className={modalClassName}
|
|
122
|
+
style={modalClassName ? undefined : defaultModalStyle}
|
|
123
|
+
>
|
|
124
|
+
<iframe
|
|
125
|
+
ref={handle.iframeRef}
|
|
126
|
+
src={handle.iframeSrc}
|
|
127
|
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
|
128
|
+
allow="clipboard-write"
|
|
129
|
+
title="FluxFiles File Manager"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { FluxFiles } from './FluxFiles';
|
|
3
|
+
export { FluxFilesModal } from './FluxFilesModal';
|
|
4
|
+
|
|
5
|
+
// Hook
|
|
6
|
+
export { useFluxFiles } from './useFluxFiles';
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type {
|
|
10
|
+
FluxFile,
|
|
11
|
+
FluxEvent,
|
|
12
|
+
FluxFilesConfig,
|
|
13
|
+
FluxFilesProps,
|
|
14
|
+
FluxFilesModalProps,
|
|
15
|
+
FluxFilesHandle,
|
|
16
|
+
FluxCommand,
|
|
17
|
+
} from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** A file or directory entry returned by FluxFiles. */
|
|
2
|
+
export interface FluxFile {
|
|
3
|
+
path: string;
|
|
4
|
+
basename: string;
|
|
5
|
+
type: 'file' | 'dir';
|
|
6
|
+
size?: number;
|
|
7
|
+
mime?: string;
|
|
8
|
+
modified?: number;
|
|
9
|
+
url?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
alt_text?: string;
|
|
12
|
+
caption?: string;
|
|
13
|
+
hash?: string;
|
|
14
|
+
variants?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Event payload dispatched by the file manager iframe. */
|
|
18
|
+
export interface FluxEvent {
|
|
19
|
+
action: string;
|
|
20
|
+
disk?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
file?: FluxFile;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Configuration for the FluxFiles component. */
|
|
27
|
+
export interface FluxFilesConfig {
|
|
28
|
+
/** Base URL of the FluxFiles API. */
|
|
29
|
+
endpoint: string;
|
|
30
|
+
/** JWT token for authentication. */
|
|
31
|
+
token: string;
|
|
32
|
+
/** Storage disk to use. */
|
|
33
|
+
disk?: string;
|
|
34
|
+
/** Display mode: "picker" selects a file, "browser" is free-browse. */
|
|
35
|
+
mode?: 'picker' | 'browser';
|
|
36
|
+
/** Filter displayed file types (e.g. ["image/*", ".pdf"]). */
|
|
37
|
+
allowedTypes?: string[] | null;
|
|
38
|
+
/** Max file size filter in bytes. */
|
|
39
|
+
maxSize?: number | null;
|
|
40
|
+
/** Locale code (e.g. "en", "vi", "ar"). */
|
|
41
|
+
locale?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Props for the <FluxFiles /> embedded component. */
|
|
45
|
+
export interface FluxFilesProps extends FluxFilesConfig {
|
|
46
|
+
/** Container width. */
|
|
47
|
+
width?: string | number;
|
|
48
|
+
/** Container height. */
|
|
49
|
+
height?: string | number;
|
|
50
|
+
/** CSS class for the wrapper div. */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Inline styles for the wrapper div. */
|
|
53
|
+
style?: React.CSSProperties;
|
|
54
|
+
/** Fired when a file is selected (picker mode). */
|
|
55
|
+
onSelect?: (file: FluxFile) => void;
|
|
56
|
+
/** Fired when the file manager signals a close. */
|
|
57
|
+
onClose?: () => void;
|
|
58
|
+
/** Fired when the iframe is ready. */
|
|
59
|
+
onReady?: () => void;
|
|
60
|
+
/** Fired on file operations (upload, delete, move, etc.). */
|
|
61
|
+
onEvent?: (event: FluxEvent) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Props for the <FluxFilesModal /> component. */
|
|
65
|
+
export interface FluxFilesModalProps extends FluxFilesConfig {
|
|
66
|
+
/** Whether the modal is open. */
|
|
67
|
+
open: boolean;
|
|
68
|
+
/** Fired when a file is selected. */
|
|
69
|
+
onSelect?: (file: FluxFile) => void;
|
|
70
|
+
/** Fired when the modal should close (overlay click, escape, or FM_CLOSE). */
|
|
71
|
+
onClose?: () => void;
|
|
72
|
+
/** Fired when the iframe is ready. */
|
|
73
|
+
onReady?: () => void;
|
|
74
|
+
/** Fired on file operations. */
|
|
75
|
+
onEvent?: (event: FluxEvent) => void;
|
|
76
|
+
/** CSS class for the overlay. */
|
|
77
|
+
overlayClassName?: string;
|
|
78
|
+
/** CSS class for the modal content. */
|
|
79
|
+
modalClassName?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Commands that can be sent to the file manager. */
|
|
83
|
+
export type FluxCommand =
|
|
84
|
+
| { action: 'navigate'; path: string }
|
|
85
|
+
| { action: 'setDisk'; disk: string }
|
|
86
|
+
| { action: 'refresh' }
|
|
87
|
+
| { action: 'search'; q: string }
|
|
88
|
+
| { action: 'crossCopy'; dst_disk: string; dst_path?: string }
|
|
89
|
+
| { action: 'crossMove'; dst_disk: string; dst_path?: string }
|
|
90
|
+
| { action: 'crop'; x: number; y: number; width: number; height: number; save_path?: string }
|
|
91
|
+
| { action: 'aiTag' };
|
|
92
|
+
|
|
93
|
+
/** Return type of useFluxFiles hook. */
|
|
94
|
+
export interface FluxFilesHandle {
|
|
95
|
+
/** Send a command to the iframe. */
|
|
96
|
+
command: (action: string, data?: Record<string, unknown>) => void;
|
|
97
|
+
/** Navigate to a path. */
|
|
98
|
+
navigate: (path: string) => void;
|
|
99
|
+
/** Switch disk. */
|
|
100
|
+
setDisk: (disk: string) => void;
|
|
101
|
+
/** Refresh the file list. */
|
|
102
|
+
refresh: () => void;
|
|
103
|
+
/** Search files. */
|
|
104
|
+
search: (q: string) => void;
|
|
105
|
+
/** Copy selected files to another disk. */
|
|
106
|
+
crossCopy: (dstDisk: string, dstPath?: string) => void;
|
|
107
|
+
/** Move selected files to another disk. */
|
|
108
|
+
crossMove: (dstDisk: string, dstPath?: string) => void;
|
|
109
|
+
/** Crop the currently selected image. */
|
|
110
|
+
crop: (x: number, y: number, width: number, height: number, savePath?: string) => void;
|
|
111
|
+
/** Trigger AI tagging on the currently selected image. */
|
|
112
|
+
aiTag: () => void;
|
|
113
|
+
/** Whether the iframe has reported ready. */
|
|
114
|
+
ready: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Internal postMessage protocol types. */
|
|
118
|
+
export interface FluxMessage {
|
|
119
|
+
source: 'fluxfiles';
|
|
120
|
+
type: 'FM_READY' | 'FM_SELECT' | 'FM_EVENT' | 'FM_CLOSE' | 'FM_CONFIG' | 'FM_COMMAND';
|
|
121
|
+
v: number;
|
|
122
|
+
id: string;
|
|
123
|
+
payload: Record<string, unknown>;
|
|
124
|
+
}
|