@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.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
+ }