@chunkflowjs/upload-component-react 0.0.1-alpha.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/README.md +79 -0
- package/dist/index.d.mts +119 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +683 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @chunkflowjs/upload-component-react
|
|
2
|
+
|
|
3
|
+
Ready-to-use React UI components for ChunkFlow Upload SDK.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @chunkflowjs/upload-component-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
### UploadButton
|
|
14
|
+
|
|
15
|
+
A button component that triggers file selection and validates files before passing them to the onSelect callback.
|
|
16
|
+
|
|
17
|
+
#### Props
|
|
18
|
+
|
|
19
|
+
- `accept?: string` - Accepted file types (e.g., "image/\*", ".pdf,.doc")
|
|
20
|
+
- `multiple?: boolean` - Allow multiple file selection (default: false)
|
|
21
|
+
- `maxSize?: number` - Maximum file size in bytes
|
|
22
|
+
- `onSelect?: (files: File[]) => void` - Callback when files are selected
|
|
23
|
+
- `onError?: (error: FileValidationError) => void` - Callback when file validation fails
|
|
24
|
+
- `children?: ReactNode` - Custom button content
|
|
25
|
+
- `className?: string` - CSS class name for the button
|
|
26
|
+
- `disabled?: boolean` - Disable the button (default: false)
|
|
27
|
+
|
|
28
|
+
#### Example
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { UploadButton } from "@chunkflowjs/upload-component-react";
|
|
32
|
+
|
|
33
|
+
function MyComponent() {
|
|
34
|
+
const handleSelect = (files: File[]) => {
|
|
35
|
+
console.log("Selected files:", files);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleError = (error) => {
|
|
39
|
+
console.error("Validation error:", error.message);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<UploadButton
|
|
44
|
+
accept="image/*"
|
|
45
|
+
maxSize={10 * 1024 * 1024} // 10MB
|
|
46
|
+
multiple
|
|
47
|
+
onSelect={handleSelect}
|
|
48
|
+
onError={handleError}
|
|
49
|
+
>
|
|
50
|
+
Select Images
|
|
51
|
+
</UploadButton>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### File Validation
|
|
57
|
+
|
|
58
|
+
The UploadButton component validates files based on:
|
|
59
|
+
|
|
60
|
+
1. **File Type**: Supports exact MIME types (e.g., "image/jpeg"), wildcard MIME types (e.g., "image/\*"), and file extensions (e.g., ".pdf")
|
|
61
|
+
2. **File Size**: Validates against the `maxSize` prop
|
|
62
|
+
|
|
63
|
+
When validation fails, the `onError` callback is called with a `FileValidationError` that includes:
|
|
64
|
+
|
|
65
|
+
- `message`: Error description
|
|
66
|
+
- `code`: Error code ("FILE_TOO_LARGE" or "INVALID_FILE_TYPE")
|
|
67
|
+
- `file`: The file that failed validation
|
|
68
|
+
|
|
69
|
+
Valid files are passed to the `onSelect` callback.
|
|
70
|
+
|
|
71
|
+
### Other Components (Coming Soon)
|
|
72
|
+
|
|
73
|
+
- `UploadProgress` - Progress indicator
|
|
74
|
+
- `UploadList` - Upload task list
|
|
75
|
+
- `UploadDropzone` - Drag and drop zone
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { UploadTask } from "@chunkflowjs/core";
|
|
4
|
+
|
|
5
|
+
//#region src/UploadButton.d.ts
|
|
6
|
+
interface UploadButtonProps {
|
|
7
|
+
accept?: string;
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
maxSize?: number;
|
|
10
|
+
onSelect?: (files: File[]) => void;
|
|
11
|
+
onError?: (error: FileValidationError) => void;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare class FileValidationError extends Error {
|
|
17
|
+
code: "FILE_TOO_LARGE" | "INVALID_FILE_TYPE";
|
|
18
|
+
file: File;
|
|
19
|
+
constructor(message: string, code: "FILE_TOO_LARGE" | "INVALID_FILE_TYPE", file: File);
|
|
20
|
+
}
|
|
21
|
+
declare function UploadButton({
|
|
22
|
+
accept,
|
|
23
|
+
multiple,
|
|
24
|
+
maxSize,
|
|
25
|
+
onSelect,
|
|
26
|
+
onError,
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
disabled
|
|
30
|
+
}: UploadButtonProps): react_jsx_runtime0.JSX.Element;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/UploadProgress.d.ts
|
|
33
|
+
interface UploadProgressProps {
|
|
34
|
+
task: UploadTask;
|
|
35
|
+
showSpeed?: boolean;
|
|
36
|
+
showRemainingTime?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
style?: CSSProperties;
|
|
39
|
+
progressBarClassName?: string;
|
|
40
|
+
progressFillClassName?: string;
|
|
41
|
+
progressInfoClassName?: string;
|
|
42
|
+
}
|
|
43
|
+
declare function UploadProgress({
|
|
44
|
+
task,
|
|
45
|
+
showSpeed,
|
|
46
|
+
showRemainingTime,
|
|
47
|
+
className,
|
|
48
|
+
style,
|
|
49
|
+
progressBarClassName,
|
|
50
|
+
progressFillClassName,
|
|
51
|
+
progressInfoClassName
|
|
52
|
+
}: UploadProgressProps): react_jsx_runtime0.JSX.Element;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/UploadList.d.ts
|
|
55
|
+
interface UploadListProps {
|
|
56
|
+
className?: string;
|
|
57
|
+
style?: CSSProperties;
|
|
58
|
+
baseURL?: string;
|
|
59
|
+
renderItem?: (task: UploadTask, actions: UploadItemActions) => ReactNode;
|
|
60
|
+
showCompleted?: boolean;
|
|
61
|
+
showFailed?: boolean;
|
|
62
|
+
showCancelled?: boolean;
|
|
63
|
+
maxItems?: number;
|
|
64
|
+
emptyMessage?: string;
|
|
65
|
+
}
|
|
66
|
+
interface UploadItemActions {
|
|
67
|
+
pause: () => void;
|
|
68
|
+
resume: () => void;
|
|
69
|
+
cancel: () => void;
|
|
70
|
+
remove: () => void;
|
|
71
|
+
}
|
|
72
|
+
declare function UploadList({
|
|
73
|
+
className,
|
|
74
|
+
style,
|
|
75
|
+
baseURL,
|
|
76
|
+
renderItem,
|
|
77
|
+
showCompleted,
|
|
78
|
+
showFailed,
|
|
79
|
+
showCancelled,
|
|
80
|
+
maxItems,
|
|
81
|
+
emptyMessage
|
|
82
|
+
}: UploadListProps): react_jsx_runtime0.JSX.Element;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/UploadDropzone.d.ts
|
|
85
|
+
interface UploadDropzoneProps {
|
|
86
|
+
accept?: string;
|
|
87
|
+
maxSize?: number;
|
|
88
|
+
multiple?: boolean;
|
|
89
|
+
onDrop?: (files: File[]) => void;
|
|
90
|
+
onError?: (error: FileValidationError$1) => void;
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
className?: string;
|
|
93
|
+
style?: CSSProperties;
|
|
94
|
+
draggingClassName?: string;
|
|
95
|
+
disabled?: boolean;
|
|
96
|
+
}
|
|
97
|
+
declare class FileValidationError$1 extends Error {
|
|
98
|
+
code: "FILE_TOO_LARGE" | "INVALID_FILE_TYPE";
|
|
99
|
+
file: File;
|
|
100
|
+
constructor(message: string, code: "FILE_TOO_LARGE" | "INVALID_FILE_TYPE", file: File);
|
|
101
|
+
}
|
|
102
|
+
declare function UploadDropzone({
|
|
103
|
+
accept,
|
|
104
|
+
maxSize,
|
|
105
|
+
multiple,
|
|
106
|
+
onDrop,
|
|
107
|
+
onError,
|
|
108
|
+
children,
|
|
109
|
+
className,
|
|
110
|
+
style,
|
|
111
|
+
draggingClassName,
|
|
112
|
+
disabled
|
|
113
|
+
}: UploadDropzoneProps): react_jsx_runtime0.JSX.Element;
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/index.d.ts
|
|
116
|
+
declare const UPLOAD_COMPONENT_REACT_VERSION = "0.0.0";
|
|
117
|
+
//#endregion
|
|
118
|
+
export { FileValidationError, UPLOAD_COMPONENT_REACT_VERSION, UploadButton, type UploadButtonProps, UploadDropzone, type UploadDropzoneProps, type UploadItemActions, UploadList, type UploadListProps, UploadProgress, type UploadProgressProps };
|
|
119
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/UploadButton.tsx","../src/UploadProgress.tsx","../src/UploadList.tsx","../src/UploadDropzone.tsx","../src/index.ts"],"mappings":";;;;;UAKiB,iBAAA;EAIf,MAAA;EAMA,QAAA;EAKA,OAAA;EAKA,QAAA,IAAY,KAAA,EAAO,IAAA;EAKnB,OAAA,IAAW,KAAA,EAAO,mBAAA;EAKlB,QAAA,GAAW,SAAA;EAKX,SAAA;EAMA,QAAA;AAAA;AAAA,cAMW,mBAAA,SAA4B,KAAA;EAG9B,IAAA;EACA,IAAA,EAAM,IAAA;cAFb,OAAA,UACO,IAAA,0CACA,IAAA,EAAM,IAAA;AAAA;AAAA,iBAyBD,YAAA,CAAA;EACd,MAAA;EACA,QAAA;EACA,OAAA;EACA,QAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EACA;AAAA,GACC,iBAAA,GAAiB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCnFH,mBAAA;EAIf,IAAA,EAAM,UAAA;EAMN,SAAA;EAMA,iBAAA;EAKA,SAAA;EAKA,KAAA,GAAQ,aAAA;EAKR,oBAAA;EAKA,qBAAA;EAKA,qBAAA;AAAA;AAAA,iBAgDc,cAAA,CAAA;EACd,IAAA;EACA,SAAA;EACA,iBAAA;EACA,SAAA;EACA,KAAA;EACA,oBAAA;EACA,qBAAA;EACA;AAAA,GACC,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UC/FL,eAAA;EAIf,SAAA;EAKA,KAAA,GAAQ,aAAA;EAMR,OAAA;EAMA,UAAA,IAAc,IAAA,EAAM,UAAA,EAAY,OAAA,EAAS,iBAAA,KAAsB,SAAA;EAM/D,aAAA;EAMA,UAAA;EAMA,aAAA;EAMA,QAAA;EAMA,YAAA;AAAA;AAAA,UAMe,iBAAA;EAEf,KAAA;EAEA,MAAA;EAEA,MAAA;EAEA,MAAA;AAAA;AAAA,iBAiQc,UAAA,CAAA;EACd,SAAA;EACA,KAAA;EACA,OAAA;EACA,UAAA;EACA,aAAA;EACA,UAAA;EACA,aAAA;EACA,QAAA;EACA;AAAA,GACC,eAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCjVD,mBAAA;EAIf,MAAA;EAKA,OAAA;EAMA,QAAA;EAKA,MAAA,IAAU,KAAA,EAAO,IAAA;EAKjB,OAAA,IAAW,KAAA,EAAO,qBAAA;EAKlB,QAAA,GAAW,SAAA;EAKX,SAAA;EAKA,KAAA,GAAQ,aAAA;EAKR,iBAAA;EAMA,QAAA;AAAA;AAAA,cAMW,qBAAA,SAA4B,KAAA;EAG9B,IAAA;EACA,IAAA,EAAM,IAAA;cAFb,OAAA,UACO,IAAA,0CACA,IAAA,EAAM,IAAA;AAAA;AAAA,iBAsED,cAAA,CAAA;EACd,MAAA;EACA,OAAA;EACA,QAAA;EACA,MAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,iBAAA;EACA;AAAA,GACC,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;cCrIT,8BAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { formatFileSize } from "@chunkflowjs/shared";
|
|
4
|
+
import { UploadStatus } from "@chunkflowjs/protocol";
|
|
5
|
+
import { useUploadList } from "@chunkflowjs/upload-client-react";
|
|
6
|
+
|
|
7
|
+
//#region src/UploadButton.tsx
|
|
8
|
+
/**
|
|
9
|
+
* File validation error
|
|
10
|
+
*/
|
|
11
|
+
var FileValidationError = class extends Error {
|
|
12
|
+
constructor(message, code, file) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.file = file;
|
|
16
|
+
this.name = "FileValidationError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* UploadButton component
|
|
21
|
+
*
|
|
22
|
+
* A button component that triggers file selection and validates files
|
|
23
|
+
* before passing them to the onSelect callback.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <UploadButton
|
|
28
|
+
* accept="image/*"
|
|
29
|
+
* maxSize={10 * 1024 * 1024} // 10MB
|
|
30
|
+
* multiple
|
|
31
|
+
* onSelect={(files) => console.log('Selected:', files)}
|
|
32
|
+
* >
|
|
33
|
+
* Select Files
|
|
34
|
+
* </UploadButton>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function UploadButton({ accept, multiple = false, maxSize, onSelect, onError, children, className, disabled = false }) {
|
|
38
|
+
const inputRef = useRef(null);
|
|
39
|
+
const handleClick = () => {
|
|
40
|
+
if (!disabled) inputRef.current?.click();
|
|
41
|
+
};
|
|
42
|
+
const validateFile = (file) => {
|
|
43
|
+
if (maxSize && file.size > maxSize) return new FileValidationError(`File "${file.name}" size ${file.size} bytes exceeds maximum ${maxSize} bytes`, "FILE_TOO_LARGE", file);
|
|
44
|
+
if (accept && !matchAccept$1(file, accept)) return new FileValidationError(`File "${file.name}" type "${file.type}" is not accepted`, "INVALID_FILE_TYPE", file);
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
const handleChange = (e) => {
|
|
48
|
+
const files = Array.from(e.target.files || []);
|
|
49
|
+
if (files.length === 0) return;
|
|
50
|
+
const validFiles = [];
|
|
51
|
+
const errors = [];
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const error = validateFile(file);
|
|
54
|
+
if (error) errors.push(error);
|
|
55
|
+
else validFiles.push(file);
|
|
56
|
+
}
|
|
57
|
+
if (errors.length > 0 && onError) errors.forEach((error) => onError(error));
|
|
58
|
+
if (validFiles.length > 0 && onSelect) onSelect(validFiles);
|
|
59
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
62
|
+
onClick: handleClick,
|
|
63
|
+
className,
|
|
64
|
+
disabled,
|
|
65
|
+
type: "button",
|
|
66
|
+
children: children || "Select Files"
|
|
67
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
68
|
+
ref: inputRef,
|
|
69
|
+
type: "file",
|
|
70
|
+
accept,
|
|
71
|
+
multiple,
|
|
72
|
+
onChange: handleChange,
|
|
73
|
+
style: { display: "none" },
|
|
74
|
+
"aria-hidden": "true"
|
|
75
|
+
})] });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if a file matches the accept pattern
|
|
79
|
+
*
|
|
80
|
+
* @param file - The file to check
|
|
81
|
+
* @param accept - The accept pattern (e.g., "image/star", ".pdf,.doc", "star/star")
|
|
82
|
+
* @returns true if the file matches the pattern
|
|
83
|
+
*/
|
|
84
|
+
function matchAccept$1(file, accept) {
|
|
85
|
+
if (accept === "*/*" || accept === "*") return true;
|
|
86
|
+
const patterns = accept.split(",").map((p) => p.trim());
|
|
87
|
+
for (const pattern of patterns) {
|
|
88
|
+
if (pattern === "*/*" || pattern === "*") return true;
|
|
89
|
+
if (pattern === file.type) return true;
|
|
90
|
+
if (pattern.endsWith("/*")) {
|
|
91
|
+
const prefix = pattern.slice(0, -2);
|
|
92
|
+
if (file.type.startsWith(prefix + "/")) return true;
|
|
93
|
+
}
|
|
94
|
+
if (pattern.startsWith(".")) {
|
|
95
|
+
if (file.name.toLowerCase().endsWith(pattern.toLowerCase())) return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/UploadProgress.tsx
|
|
103
|
+
/**
|
|
104
|
+
* Format time in seconds to human-readable format
|
|
105
|
+
*
|
|
106
|
+
* @param seconds - Time in seconds
|
|
107
|
+
* @returns Formatted string (e.g., "1m 30s", "45s")
|
|
108
|
+
*/
|
|
109
|
+
function formatTime(seconds) {
|
|
110
|
+
if (seconds <= 0 || !isFinite(seconds)) return "0s";
|
|
111
|
+
const hours = Math.floor(seconds / 3600);
|
|
112
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
113
|
+
const secs = Math.floor(seconds % 60);
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
116
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
117
|
+
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
|
|
118
|
+
return parts.join(" ");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* UploadProgress component
|
|
122
|
+
*
|
|
123
|
+
* Displays upload progress with a progress bar, percentage, speed, and remaining time.
|
|
124
|
+
* Automatically updates as the upload progresses.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* <UploadProgress
|
|
129
|
+
* task={uploadTask}
|
|
130
|
+
* showSpeed={true}
|
|
131
|
+
* showRemainingTime={true}
|
|
132
|
+
* />
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function UploadProgress({ task, showSpeed = true, showRemainingTime = true, className, style, progressBarClassName, progressFillClassName, progressInfoClassName }) {
|
|
136
|
+
const [progress, setProgress] = useState(task.getProgress());
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const handleProgress = () => {
|
|
139
|
+
setProgress(task.getProgress());
|
|
140
|
+
};
|
|
141
|
+
task.on("progress", handleProgress);
|
|
142
|
+
task.on("start", handleProgress);
|
|
143
|
+
task.on("success", handleProgress);
|
|
144
|
+
task.on("error", handleProgress);
|
|
145
|
+
task.on("pause", handleProgress);
|
|
146
|
+
task.on("resume", handleProgress);
|
|
147
|
+
handleProgress();
|
|
148
|
+
return () => {
|
|
149
|
+
task.off("progress", handleProgress);
|
|
150
|
+
task.off("start", handleProgress);
|
|
151
|
+
task.off("success", handleProgress);
|
|
152
|
+
task.off("error", handleProgress);
|
|
153
|
+
task.off("pause", handleProgress);
|
|
154
|
+
task.off("resume", handleProgress);
|
|
155
|
+
};
|
|
156
|
+
}, [task]);
|
|
157
|
+
const defaultStyles = {
|
|
158
|
+
container: {
|
|
159
|
+
width: "100%",
|
|
160
|
+
padding: "8px"
|
|
161
|
+
},
|
|
162
|
+
progressBar: {
|
|
163
|
+
width: "100%",
|
|
164
|
+
height: "8px",
|
|
165
|
+
backgroundColor: "#e0e0e0",
|
|
166
|
+
borderRadius: "4px",
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
marginBottom: "8px"
|
|
169
|
+
},
|
|
170
|
+
progressFill: {
|
|
171
|
+
height: "100%",
|
|
172
|
+
backgroundColor: "#4caf50",
|
|
173
|
+
transition: "width 0.3s ease",
|
|
174
|
+
borderRadius: "4px"
|
|
175
|
+
},
|
|
176
|
+
progressInfo: {
|
|
177
|
+
display: "flex",
|
|
178
|
+
justifyContent: "space-between",
|
|
179
|
+
fontSize: "14px",
|
|
180
|
+
color: "#666"
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
184
|
+
className,
|
|
185
|
+
style: style || defaultStyles.container,
|
|
186
|
+
"data-testid": "upload-progress",
|
|
187
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
188
|
+
className: progressBarClassName,
|
|
189
|
+
style: progressBarClassName ? void 0 : defaultStyles.progressBar,
|
|
190
|
+
"data-testid": "progress-bar",
|
|
191
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
192
|
+
className: progressFillClassName,
|
|
193
|
+
style: progressFillClassName ? { width: `${progress.percentage}%` } : {
|
|
194
|
+
...defaultStyles.progressFill,
|
|
195
|
+
width: `${progress.percentage}%`
|
|
196
|
+
},
|
|
197
|
+
"data-testid": "progress-fill"
|
|
198
|
+
})
|
|
199
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: progressInfoClassName,
|
|
201
|
+
style: progressInfoClassName ? void 0 : defaultStyles.progressInfo,
|
|
202
|
+
"data-testid": "progress-info",
|
|
203
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
204
|
+
"data-testid": "progress-percentage",
|
|
205
|
+
children: [progress.percentage.toFixed(1), "%"]
|
|
206
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
207
|
+
style: {
|
|
208
|
+
display: "flex",
|
|
209
|
+
gap: "16px"
|
|
210
|
+
},
|
|
211
|
+
children: [showSpeed && progress.speed > 0 && /* @__PURE__ */ jsxs("span", {
|
|
212
|
+
"data-testid": "progress-speed",
|
|
213
|
+
children: [formatFileSize(progress.speed), "/s"]
|
|
214
|
+
}), showRemainingTime && progress.remainingTime > 0 && /* @__PURE__ */ jsxs("span", {
|
|
215
|
+
"data-testid": "progress-remaining-time",
|
|
216
|
+
children: [formatTime(progress.remainingTime), " remaining"]
|
|
217
|
+
})]
|
|
218
|
+
})]
|
|
219
|
+
})]
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/UploadList.tsx
|
|
225
|
+
/**
|
|
226
|
+
* Default upload item component
|
|
227
|
+
*
|
|
228
|
+
* Displays file info, progress, and action buttons
|
|
229
|
+
*/
|
|
230
|
+
function DefaultUploadItem({ task, onRemove, baseURL }) {
|
|
231
|
+
const status = task.getStatus();
|
|
232
|
+
const [fileUrl, setFileUrl] = useState(null);
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const handleSuccess = ({ fileUrl }) => {
|
|
235
|
+
setFileUrl(baseURL && fileUrl.startsWith("/") ? `${baseURL}${fileUrl}` : fileUrl);
|
|
236
|
+
};
|
|
237
|
+
task.on("success", handleSuccess);
|
|
238
|
+
return () => {
|
|
239
|
+
task.off("success", handleSuccess);
|
|
240
|
+
};
|
|
241
|
+
}, [task, baseURL]);
|
|
242
|
+
const handleClick = () => {
|
|
243
|
+
if (status === UploadStatus.SUCCESS && fileUrl) window.open(fileUrl, "_blank");
|
|
244
|
+
};
|
|
245
|
+
const defaultStyles = {
|
|
246
|
+
container: {
|
|
247
|
+
padding: "16px",
|
|
248
|
+
border: "1px solid var(--border, #333)",
|
|
249
|
+
borderRadius: "8px",
|
|
250
|
+
marginBottom: "8px",
|
|
251
|
+
backgroundColor: "var(--darker-bg, #0f0f0f)",
|
|
252
|
+
cursor: status === UploadStatus.SUCCESS && fileUrl ? "pointer" : "default",
|
|
253
|
+
transition: "all 0.2s"
|
|
254
|
+
},
|
|
255
|
+
fileInfo: {
|
|
256
|
+
display: "flex",
|
|
257
|
+
justifyContent: "space-between",
|
|
258
|
+
alignItems: "center",
|
|
259
|
+
marginBottom: "8px"
|
|
260
|
+
},
|
|
261
|
+
fileName: {
|
|
262
|
+
fontWeight: 500,
|
|
263
|
+
fontSize: "14px",
|
|
264
|
+
color: "var(--text, #e0e0e0)",
|
|
265
|
+
overflow: "hidden",
|
|
266
|
+
textOverflow: "ellipsis",
|
|
267
|
+
whiteSpace: "nowrap",
|
|
268
|
+
flex: 1,
|
|
269
|
+
marginRight: "8px"
|
|
270
|
+
},
|
|
271
|
+
fileSize: {
|
|
272
|
+
fontSize: "12px",
|
|
273
|
+
color: "var(--text-dim, #999)"
|
|
274
|
+
},
|
|
275
|
+
status: {
|
|
276
|
+
fontSize: "12px",
|
|
277
|
+
padding: "2px 8px",
|
|
278
|
+
borderRadius: "4px",
|
|
279
|
+
marginLeft: "8px"
|
|
280
|
+
},
|
|
281
|
+
actions: {
|
|
282
|
+
display: "flex",
|
|
283
|
+
gap: "8px",
|
|
284
|
+
marginTop: "8px"
|
|
285
|
+
},
|
|
286
|
+
button: {
|
|
287
|
+
padding: "4px 12px",
|
|
288
|
+
fontSize: "12px",
|
|
289
|
+
border: "1px solid var(--border, #333)",
|
|
290
|
+
borderRadius: "4px",
|
|
291
|
+
backgroundColor: "var(--card-bg, #1e1e1e)",
|
|
292
|
+
color: "var(--text, #e0e0e0)",
|
|
293
|
+
cursor: "pointer",
|
|
294
|
+
transition: "all 0.2s"
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const getStatusStyle = (status) => {
|
|
298
|
+
const baseStyle = defaultStyles.status;
|
|
299
|
+
switch (status) {
|
|
300
|
+
case UploadStatus.UPLOADING: return {
|
|
301
|
+
...baseStyle,
|
|
302
|
+
backgroundColor: "rgba(33, 150, 243, 0.15)",
|
|
303
|
+
color: "#42a5f5"
|
|
304
|
+
};
|
|
305
|
+
case UploadStatus.SUCCESS: return {
|
|
306
|
+
...baseStyle,
|
|
307
|
+
backgroundColor: "rgba(76, 175, 80, 0.15)",
|
|
308
|
+
color: "#66bb6a"
|
|
309
|
+
};
|
|
310
|
+
case UploadStatus.ERROR: return {
|
|
311
|
+
...baseStyle,
|
|
312
|
+
backgroundColor: "rgba(244, 67, 54, 0.15)",
|
|
313
|
+
color: "#ef5350"
|
|
314
|
+
};
|
|
315
|
+
case UploadStatus.PAUSED: return {
|
|
316
|
+
...baseStyle,
|
|
317
|
+
backgroundColor: "rgba(255, 152, 0, 0.15)",
|
|
318
|
+
color: "#ffa726"
|
|
319
|
+
};
|
|
320
|
+
case UploadStatus.CANCELLED: return {
|
|
321
|
+
...baseStyle,
|
|
322
|
+
backgroundColor: "rgba(158, 158, 158, 0.15)",
|
|
323
|
+
color: "#bdbdbd"
|
|
324
|
+
};
|
|
325
|
+
default: return {
|
|
326
|
+
...baseStyle,
|
|
327
|
+
backgroundColor: "rgba(158, 158, 158, 0.15)",
|
|
328
|
+
color: "#bdbdbd"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const getStatusText = (status) => {
|
|
333
|
+
switch (status) {
|
|
334
|
+
case UploadStatus.IDLE: return "Idle";
|
|
335
|
+
case UploadStatus.HASHING: return "Hashing";
|
|
336
|
+
case UploadStatus.UPLOADING: return "Uploading";
|
|
337
|
+
case UploadStatus.PAUSED: return "Paused";
|
|
338
|
+
case UploadStatus.SUCCESS: return "Success";
|
|
339
|
+
case UploadStatus.ERROR: return "Error";
|
|
340
|
+
case UploadStatus.CANCELLED: return "Cancelled";
|
|
341
|
+
default: return "Unknown";
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
345
|
+
style: defaultStyles.container,
|
|
346
|
+
"data-testid": "upload-item",
|
|
347
|
+
onClick: handleClick,
|
|
348
|
+
onMouseEnter: (e) => {
|
|
349
|
+
if (status === UploadStatus.SUCCESS && fileUrl) e.currentTarget.style.backgroundColor = "var(--card-bg, #1e1e1e)";
|
|
350
|
+
},
|
|
351
|
+
onMouseLeave: (e) => {
|
|
352
|
+
e.currentTarget.style.backgroundColor = "var(--darker-bg, #0f0f0f)";
|
|
353
|
+
},
|
|
354
|
+
title: status === UploadStatus.SUCCESS && fileUrl ? "Click to open file" : "",
|
|
355
|
+
children: [
|
|
356
|
+
/* @__PURE__ */ jsxs("div", {
|
|
357
|
+
style: defaultStyles.fileInfo,
|
|
358
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
359
|
+
style: {
|
|
360
|
+
display: "flex",
|
|
361
|
+
alignItems: "center",
|
|
362
|
+
flex: 1,
|
|
363
|
+
minWidth: 0
|
|
364
|
+
},
|
|
365
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
366
|
+
style: defaultStyles.fileName,
|
|
367
|
+
title: task.file.name,
|
|
368
|
+
children: task.file.name
|
|
369
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
370
|
+
style: defaultStyles.fileSize,
|
|
371
|
+
children: formatFileSize(task.file.size)
|
|
372
|
+
})]
|
|
373
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
374
|
+
style: getStatusStyle(status),
|
|
375
|
+
"data-testid": "upload-status",
|
|
376
|
+
children: getStatusText(status)
|
|
377
|
+
})]
|
|
378
|
+
}),
|
|
379
|
+
(status === UploadStatus.UPLOADING || status === UploadStatus.PAUSED || status === UploadStatus.HASHING) && /* @__PURE__ */ jsx(UploadProgress, { task }),
|
|
380
|
+
/* @__PURE__ */ jsxs("div", {
|
|
381
|
+
style: defaultStyles.actions,
|
|
382
|
+
onClick: (e) => e.stopPropagation(),
|
|
383
|
+
children: [
|
|
384
|
+
status === UploadStatus.UPLOADING && /* @__PURE__ */ jsx("button", {
|
|
385
|
+
onClick: () => task.pause(),
|
|
386
|
+
style: defaultStyles.button,
|
|
387
|
+
"data-testid": "pause-button",
|
|
388
|
+
type: "button",
|
|
389
|
+
children: "Pause"
|
|
390
|
+
}),
|
|
391
|
+
status === UploadStatus.PAUSED && /* @__PURE__ */ jsx("button", {
|
|
392
|
+
onClick: () => task.resume(),
|
|
393
|
+
style: defaultStyles.button,
|
|
394
|
+
"data-testid": "resume-button",
|
|
395
|
+
type: "button",
|
|
396
|
+
children: "Resume"
|
|
397
|
+
}),
|
|
398
|
+
(status === UploadStatus.UPLOADING || status === UploadStatus.PAUSED) && /* @__PURE__ */ jsx("button", {
|
|
399
|
+
onClick: () => task.cancel(),
|
|
400
|
+
style: defaultStyles.button,
|
|
401
|
+
"data-testid": "cancel-button",
|
|
402
|
+
type: "button",
|
|
403
|
+
children: "Cancel"
|
|
404
|
+
}),
|
|
405
|
+
status === UploadStatus.SUCCESS && fileUrl && /* @__PURE__ */ jsx("button", {
|
|
406
|
+
onClick: handleClick,
|
|
407
|
+
style: defaultStyles.button,
|
|
408
|
+
"data-testid": "open-button",
|
|
409
|
+
type: "button",
|
|
410
|
+
children: "Open"
|
|
411
|
+
}),
|
|
412
|
+
/* @__PURE__ */ jsx("button", {
|
|
413
|
+
onClick: onRemove,
|
|
414
|
+
style: defaultStyles.button,
|
|
415
|
+
"data-testid": "remove-button",
|
|
416
|
+
type: "button",
|
|
417
|
+
children: "Remove"
|
|
418
|
+
})
|
|
419
|
+
]
|
|
420
|
+
})
|
|
421
|
+
]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* UploadList component
|
|
426
|
+
*
|
|
427
|
+
* Displays a list of upload tasks with progress bars and action buttons.
|
|
428
|
+
* Integrates with useUploadList hook to automatically sync with the upload manager.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```tsx
|
|
432
|
+
* // Basic usage
|
|
433
|
+
* <UploadList />
|
|
434
|
+
*
|
|
435
|
+
* // With custom styling
|
|
436
|
+
* <UploadList
|
|
437
|
+
* className="my-upload-list"
|
|
438
|
+
* showCompleted={false}
|
|
439
|
+
* maxItems={10}
|
|
440
|
+
* />
|
|
441
|
+
*
|
|
442
|
+
* // With custom item renderer
|
|
443
|
+
* <UploadList
|
|
444
|
+
* renderItem={(task, actions) => (
|
|
445
|
+
* <div>
|
|
446
|
+
* <h3>{task.file.name}</h3>
|
|
447
|
+
* <button onClick={actions.pause}>Pause</button>
|
|
448
|
+
* <button onClick={actions.remove}>Remove</button>
|
|
449
|
+
* </div>
|
|
450
|
+
* )}
|
|
451
|
+
* />
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
function UploadList({ className, style, baseURL, renderItem, showCompleted = true, showFailed = true, showCancelled = true, maxItems, emptyMessage = "No uploads" }) {
|
|
455
|
+
const { tasks, removeTask } = useUploadList();
|
|
456
|
+
const filteredTasks = tasks.filter((task) => {
|
|
457
|
+
const status = task.getStatus();
|
|
458
|
+
if (!showCompleted && status === UploadStatus.SUCCESS) return false;
|
|
459
|
+
if (!showFailed && status === UploadStatus.ERROR) return false;
|
|
460
|
+
if (!showCancelled && status === UploadStatus.CANCELLED) return false;
|
|
461
|
+
return true;
|
|
462
|
+
});
|
|
463
|
+
const displayTasks = maxItems ? filteredTasks.slice(0, maxItems) : filteredTasks;
|
|
464
|
+
const defaultStyles = {
|
|
465
|
+
container: { width: "100%" },
|
|
466
|
+
empty: {
|
|
467
|
+
padding: "32px",
|
|
468
|
+
textAlign: "center",
|
|
469
|
+
color: "var(--text-dim, #999)",
|
|
470
|
+
fontSize: "14px"
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
if (displayTasks.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
474
|
+
className,
|
|
475
|
+
style: style || defaultStyles.container,
|
|
476
|
+
"data-testid": "upload-list-empty",
|
|
477
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
478
|
+
style: defaultStyles.empty,
|
|
479
|
+
children: emptyMessage
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
return /* @__PURE__ */ jsx("div", {
|
|
483
|
+
className,
|
|
484
|
+
style: style || defaultStyles.container,
|
|
485
|
+
"data-testid": "upload-list",
|
|
486
|
+
children: displayTasks.map((task) => {
|
|
487
|
+
const actions = {
|
|
488
|
+
pause: () => task.pause(),
|
|
489
|
+
resume: () => task.resume(),
|
|
490
|
+
cancel: () => task.cancel(),
|
|
491
|
+
remove: () => removeTask(task.id)
|
|
492
|
+
};
|
|
493
|
+
return /* @__PURE__ */ jsx("div", {
|
|
494
|
+
"data-testid": "upload-list-item",
|
|
495
|
+
children: renderItem ? renderItem(task, actions) : /* @__PURE__ */ jsx(DefaultUploadItem, {
|
|
496
|
+
task,
|
|
497
|
+
onRemove: actions.remove,
|
|
498
|
+
baseURL
|
|
499
|
+
})
|
|
500
|
+
}, task.id);
|
|
501
|
+
})
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/UploadDropzone.tsx
|
|
507
|
+
/**
|
|
508
|
+
* File validation error
|
|
509
|
+
*/
|
|
510
|
+
var FileValidationError$1 = class extends Error {
|
|
511
|
+
constructor(message, code, file) {
|
|
512
|
+
super(message);
|
|
513
|
+
this.code = code;
|
|
514
|
+
this.file = file;
|
|
515
|
+
this.name = "FileValidationError";
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Check if a file matches the accept pattern
|
|
520
|
+
*
|
|
521
|
+
* @param file - The file to check
|
|
522
|
+
* @param accept - The accept pattern (e.g., "image/star", ".pdf,.doc", "star/star")
|
|
523
|
+
* @returns true if the file matches the pattern
|
|
524
|
+
*/
|
|
525
|
+
function matchAccept(file, accept) {
|
|
526
|
+
if (accept === "*/*" || accept === "*") return true;
|
|
527
|
+
const patterns = accept.split(",").map((p) => p.trim());
|
|
528
|
+
for (const pattern of patterns) {
|
|
529
|
+
if (pattern === "*/*" || pattern === "*") return true;
|
|
530
|
+
if (pattern === file.type) return true;
|
|
531
|
+
if (pattern.endsWith("/*")) {
|
|
532
|
+
const prefix = pattern.slice(0, -2);
|
|
533
|
+
if (file.type.startsWith(prefix + "/")) return true;
|
|
534
|
+
}
|
|
535
|
+
if (pattern.startsWith(".")) {
|
|
536
|
+
if (file.name.toLowerCase().endsWith(pattern.toLowerCase())) return true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* UploadDropzone component
|
|
543
|
+
*
|
|
544
|
+
* A drag-and-drop zone for file uploads with visual feedback.
|
|
545
|
+
* Validates files based on type and size before passing them to the onDrop callback.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```tsx
|
|
549
|
+
* <UploadDropzone
|
|
550
|
+
* accept="image/*"
|
|
551
|
+
* maxSize={10 * 1024 * 1024} // 10MB
|
|
552
|
+
* multiple
|
|
553
|
+
* onDrop={(files) => console.log('Dropped:', files)}
|
|
554
|
+
* >
|
|
555
|
+
* <p>Drag and drop files here, or click to select</p>
|
|
556
|
+
* </UploadDropzone>
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
function UploadDropzone({ accept, maxSize, multiple = true, onDrop, onError, children, className, style, draggingClassName, disabled = false }) {
|
|
560
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
561
|
+
const inputRef = useRef(null);
|
|
562
|
+
const dragCounterRef = useRef(0);
|
|
563
|
+
const validateFile = (file) => {
|
|
564
|
+
if (maxSize && file.size > maxSize) return new FileValidationError$1(`File "${file.name}" size ${file.size} bytes exceeds maximum ${maxSize} bytes`, "FILE_TOO_LARGE", file);
|
|
565
|
+
if (accept && !matchAccept(file, accept)) return new FileValidationError$1(`File "${file.name}" type "${file.type}" is not accepted`, "INVALID_FILE_TYPE", file);
|
|
566
|
+
return null;
|
|
567
|
+
};
|
|
568
|
+
const processFiles = (files) => {
|
|
569
|
+
if (files.length === 0) return;
|
|
570
|
+
const validFiles = [];
|
|
571
|
+
const errors = [];
|
|
572
|
+
for (const file of files) {
|
|
573
|
+
const error = validateFile(file);
|
|
574
|
+
if (error) errors.push(error);
|
|
575
|
+
else validFiles.push(file);
|
|
576
|
+
}
|
|
577
|
+
if (errors.length > 0 && onError) errors.forEach((error) => onError(error));
|
|
578
|
+
if (validFiles.length > 0 && onDrop) onDrop(validFiles);
|
|
579
|
+
};
|
|
580
|
+
const handleDragEnter = (e) => {
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
e.stopPropagation();
|
|
583
|
+
if (disabled) return;
|
|
584
|
+
dragCounterRef.current++;
|
|
585
|
+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) setIsDragging(true);
|
|
586
|
+
};
|
|
587
|
+
const handleDragLeave = (e) => {
|
|
588
|
+
e.preventDefault();
|
|
589
|
+
e.stopPropagation();
|
|
590
|
+
if (disabled) return;
|
|
591
|
+
dragCounterRef.current--;
|
|
592
|
+
if (dragCounterRef.current === 0) setIsDragging(false);
|
|
593
|
+
};
|
|
594
|
+
const handleDragOver = (e) => {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
e.stopPropagation();
|
|
597
|
+
if (disabled) return;
|
|
598
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
599
|
+
};
|
|
600
|
+
const handleDrop = (e) => {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
if (disabled) return;
|
|
604
|
+
setIsDragging(false);
|
|
605
|
+
dragCounterRef.current = 0;
|
|
606
|
+
const files = Array.from(e.dataTransfer?.files || []);
|
|
607
|
+
processFiles(multiple ? files : files.slice(0, 1));
|
|
608
|
+
};
|
|
609
|
+
const handleClick = () => {
|
|
610
|
+
if (!disabled) inputRef.current?.click();
|
|
611
|
+
};
|
|
612
|
+
const handleInputChange = (e) => {
|
|
613
|
+
processFiles(Array.from(e.target.files || []));
|
|
614
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
615
|
+
};
|
|
616
|
+
const defaultStyles = {
|
|
617
|
+
container: {
|
|
618
|
+
border: "2px dashed #ccc",
|
|
619
|
+
borderRadius: "8px",
|
|
620
|
+
padding: "32px",
|
|
621
|
+
textAlign: "center",
|
|
622
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
623
|
+
transition: "all 0.3s ease",
|
|
624
|
+
backgroundColor: disabled ? "#f5f5f5" : "#fafafa",
|
|
625
|
+
opacity: disabled ? .6 : 1
|
|
626
|
+
},
|
|
627
|
+
dragging: {
|
|
628
|
+
borderColor: "#4caf50",
|
|
629
|
+
backgroundColor: "#e8f5e9"
|
|
630
|
+
},
|
|
631
|
+
content: {
|
|
632
|
+
color: "#666",
|
|
633
|
+
fontSize: "14px"
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const containerStyle = {
|
|
637
|
+
...defaultStyles.container,
|
|
638
|
+
...isDragging && !disabled ? defaultStyles.dragging : {},
|
|
639
|
+
...style
|
|
640
|
+
};
|
|
641
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
642
|
+
className: [className, isDragging && !disabled ? draggingClassName : ""].filter(Boolean).join(" ") || void 0,
|
|
643
|
+
style: containerStyle,
|
|
644
|
+
onDragEnter: handleDragEnter,
|
|
645
|
+
onDragLeave: handleDragLeave,
|
|
646
|
+
onDragOver: handleDragOver,
|
|
647
|
+
onDrop: handleDrop,
|
|
648
|
+
onClick: handleClick,
|
|
649
|
+
"data-testid": "upload-dropzone",
|
|
650
|
+
"data-dragging": isDragging,
|
|
651
|
+
role: "button",
|
|
652
|
+
tabIndex: disabled ? -1 : 0,
|
|
653
|
+
"aria-disabled": disabled,
|
|
654
|
+
onKeyDown: (e) => {
|
|
655
|
+
if (!disabled && (e.key === "Enter" || e.key === " ")) {
|
|
656
|
+
e.preventDefault();
|
|
657
|
+
handleClick();
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
children: children || /* @__PURE__ */ jsxs("div", {
|
|
661
|
+
style: defaultStyles.content,
|
|
662
|
+
"data-testid": "dropzone-default-content",
|
|
663
|
+
children: [/* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("strong", { children: "Drag and drop files here" }) }), /* @__PURE__ */ jsx("p", { children: "or click to select files" })]
|
|
664
|
+
})
|
|
665
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
666
|
+
ref: inputRef,
|
|
667
|
+
type: "file",
|
|
668
|
+
accept,
|
|
669
|
+
multiple,
|
|
670
|
+
onChange: handleInputChange,
|
|
671
|
+
style: { display: "none" },
|
|
672
|
+
"aria-hidden": "true",
|
|
673
|
+
disabled
|
|
674
|
+
})] });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/index.ts
|
|
679
|
+
const UPLOAD_COMPONENT_REACT_VERSION = "0.0.0";
|
|
680
|
+
|
|
681
|
+
//#endregion
|
|
682
|
+
export { FileValidationError, UPLOAD_COMPONENT_REACT_VERSION, UploadButton, UploadDropzone, UploadList, UploadProgress };
|
|
683
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["matchAccept","FileValidationError"],"sources":["../src/UploadButton.tsx","../src/UploadProgress.tsx","../src/UploadList.tsx","../src/UploadDropzone.tsx","../src/index.ts"],"sourcesContent":["import { useRef, ChangeEvent, ReactNode } from \"react\";\n\n/**\n * Props for the UploadButton component\n */\nexport interface UploadButtonProps {\n /**\n * Accepted file types (e.g., \"image/*\", \".pdf,.doc\")\n */\n accept?: string;\n\n /**\n * Allow multiple file selection\n * @default false\n */\n multiple?: boolean;\n\n /**\n * Maximum file size in bytes\n */\n maxSize?: number;\n\n /**\n * Callback when files are selected\n */\n onSelect?: (files: File[]) => void;\n\n /**\n * Callback when file validation fails\n */\n onError?: (error: FileValidationError) => void;\n\n /**\n * Custom button content\n */\n children?: ReactNode;\n\n /**\n * CSS class name for the button\n */\n className?: string;\n\n /**\n * Disable the button\n * @default false\n */\n disabled?: boolean;\n}\n\n/**\n * File validation error\n */\nexport class FileValidationError extends Error {\n constructor(\n message: string,\n public code: \"FILE_TOO_LARGE\" | \"INVALID_FILE_TYPE\",\n public file: File,\n ) {\n super(message);\n this.name = \"FileValidationError\";\n }\n}\n\n/**\n * UploadButton component\n *\n * A button component that triggers file selection and validates files\n * before passing them to the onSelect callback.\n *\n * @example\n * ```tsx\n * <UploadButton\n * accept=\"image/*\"\n * maxSize={10 * 1024 * 1024} // 10MB\n * multiple\n * onSelect={(files) => console.log('Selected:', files)}\n * >\n * Select Files\n * </UploadButton>\n * ```\n */\nexport function UploadButton({\n accept,\n multiple = false,\n maxSize,\n onSelect,\n onError,\n children,\n className,\n disabled = false,\n}: UploadButtonProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n const handleClick = () => {\n if (!disabled) {\n inputRef.current?.click();\n }\n };\n\n const validateFile = (file: File): FileValidationError | null => {\n // Validate file size\n if (maxSize && file.size > maxSize) {\n return new FileValidationError(\n `File \"${file.name}\" size ${file.size} bytes exceeds maximum ${maxSize} bytes`,\n \"FILE_TOO_LARGE\",\n file,\n );\n }\n\n // Validate file type\n if (accept && !matchAccept(file, accept)) {\n return new FileValidationError(\n `File \"${file.name}\" type \"${file.type}\" is not accepted`,\n \"INVALID_FILE_TYPE\",\n file,\n );\n }\n\n return null;\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n const files = Array.from(e.target.files || []);\n\n if (files.length === 0) {\n return;\n }\n\n // Validate all files\n const validFiles: File[] = [];\n const errors: FileValidationError[] = [];\n\n for (const file of files) {\n const error = validateFile(file);\n if (error) {\n errors.push(error);\n } else {\n validFiles.push(file);\n }\n }\n\n // Report errors\n if (errors.length > 0 && onError) {\n errors.forEach((error) => onError(error));\n }\n\n // Call onSelect with valid files\n if (validFiles.length > 0 && onSelect) {\n onSelect(validFiles);\n }\n\n // Reset input value to allow selecting the same file again\n if (inputRef.current) {\n inputRef.current.value = \"\";\n }\n };\n\n return (\n <>\n <button onClick={handleClick} className={className} disabled={disabled} type=\"button\">\n {children || \"Select Files\"}\n </button>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n onChange={handleChange}\n style={{ display: \"none\" }}\n aria-hidden=\"true\"\n />\n </>\n );\n}\n\n/**\n * Check if a file matches the accept pattern\n *\n * @param file - The file to check\n * @param accept - The accept pattern (e.g., \"image/star\", \".pdf,.doc\", \"star/star\")\n * @returns true if the file matches the pattern\n */\nfunction matchAccept(file: File, accept: string): boolean {\n // Accept all files if pattern is \"*/*\" or \"*\"\n if (accept === \"*/*\" || accept === \"*\") {\n return true;\n }\n\n const patterns = accept.split(\",\").map((p) => p.trim());\n\n for (const pattern of patterns) {\n // Accept all files\n if (pattern === \"*/*\" || pattern === \"*\") {\n return true;\n }\n\n // Exact MIME type match (e.g., \"image/jpeg\")\n if (pattern === file.type) {\n return true;\n }\n\n // Wildcard MIME type match (e.g., \"image/*\")\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -2);\n if (file.type.startsWith(prefix + \"/\")) {\n return true;\n }\n }\n\n // File extension match (e.g., \".pdf\")\n if (pattern.startsWith(\".\")) {\n if (file.name.toLowerCase().endsWith(pattern.toLowerCase())) {\n return true;\n }\n }\n }\n\n return false;\n}\n","import { useEffect, useState, CSSProperties } from \"react\";\nimport type { UploadTask } from \"@chunkflowjs/core\";\nimport { formatFileSize } from \"@chunkflowjs/shared\";\n\n/**\n * Props for the UploadProgress component\n */\nexport interface UploadProgressProps {\n /**\n * The upload task to display progress for\n */\n task: UploadTask;\n\n /**\n * Whether to show upload speed\n * @default true\n */\n showSpeed?: boolean;\n\n /**\n * Whether to show remaining time\n * @default true\n */\n showRemainingTime?: boolean;\n\n /**\n * CSS class name for the container\n */\n className?: string;\n\n /**\n * Custom styles for the container\n */\n style?: CSSProperties;\n\n /**\n * CSS class name for the progress bar\n */\n progressBarClassName?: string;\n\n /**\n * CSS class name for the progress fill\n */\n progressFillClassName?: string;\n\n /**\n * CSS class name for the progress info\n */\n progressInfoClassName?: string;\n}\n\n/**\n * Format time in seconds to human-readable format\n *\n * @param seconds - Time in seconds\n * @returns Formatted string (e.g., \"1m 30s\", \"45s\")\n */\nfunction formatTime(seconds: number): string {\n if (seconds <= 0 || !isFinite(seconds)) {\n return \"0s\";\n }\n\n const hours = Math.floor(seconds / 3600);\n const minutes = Math.floor((seconds % 3600) / 60);\n const secs = Math.floor(seconds % 60);\n\n const parts: string[] = [];\n\n if (hours > 0) {\n parts.push(`${hours}h`);\n }\n if (minutes > 0) {\n parts.push(`${minutes}m`);\n }\n if (secs > 0 || parts.length === 0) {\n parts.push(`${secs}s`);\n }\n\n return parts.join(\" \");\n}\n\n/**\n * UploadProgress component\n *\n * Displays upload progress with a progress bar, percentage, speed, and remaining time.\n * Automatically updates as the upload progresses.\n *\n * @example\n * ```tsx\n * <UploadProgress\n * task={uploadTask}\n * showSpeed={true}\n * showRemainingTime={true}\n * />\n * ```\n */\nexport function UploadProgress({\n task,\n showSpeed = true,\n showRemainingTime = true,\n className,\n style,\n progressBarClassName,\n progressFillClassName,\n progressInfoClassName,\n}: UploadProgressProps) {\n const [progress, setProgress] = useState(task.getProgress());\n\n useEffect(() => {\n // Update progress when the task emits progress events\n const handleProgress = () => {\n setProgress(task.getProgress());\n };\n\n // Subscribe to progress events\n task.on(\"progress\", handleProgress);\n\n // Also update on other state changes that might affect progress\n task.on(\"start\", handleProgress);\n task.on(\"success\", handleProgress);\n task.on(\"error\", handleProgress);\n task.on(\"pause\", handleProgress);\n task.on(\"resume\", handleProgress);\n\n // Initial update\n handleProgress();\n\n // Cleanup: unsubscribe from events\n return () => {\n task.off(\"progress\", handleProgress);\n task.off(\"start\", handleProgress);\n task.off(\"success\", handleProgress);\n task.off(\"error\", handleProgress);\n task.off(\"pause\", handleProgress);\n task.off(\"resume\", handleProgress);\n };\n }, [task]);\n\n const defaultStyles = {\n container: {\n width: \"100%\",\n padding: \"8px\",\n } as CSSProperties,\n progressBar: {\n width: \"100%\",\n height: \"8px\",\n backgroundColor: \"#e0e0e0\",\n borderRadius: \"4px\",\n overflow: \"hidden\",\n marginBottom: \"8px\",\n } as CSSProperties,\n progressFill: {\n height: \"100%\",\n backgroundColor: \"#4caf50\",\n transition: \"width 0.3s ease\",\n borderRadius: \"4px\",\n } as CSSProperties,\n progressInfo: {\n display: \"flex\",\n justifyContent: \"space-between\",\n fontSize: \"14px\",\n color: \"#666\",\n } as CSSProperties,\n };\n\n return (\n <div\n className={className}\n style={style || defaultStyles.container}\n data-testid=\"upload-progress\"\n >\n {/* Progress bar */}\n <div\n className={progressBarClassName}\n style={progressBarClassName ? undefined : defaultStyles.progressBar}\n data-testid=\"progress-bar\"\n >\n <div\n className={progressFillClassName}\n style={\n progressFillClassName\n ? { width: `${progress.percentage}%` }\n : { ...defaultStyles.progressFill, width: `${progress.percentage}%` }\n }\n data-testid=\"progress-fill\"\n />\n </div>\n\n {/* Progress info */}\n <div\n className={progressInfoClassName}\n style={progressInfoClassName ? undefined : defaultStyles.progressInfo}\n data-testid=\"progress-info\"\n >\n <span data-testid=\"progress-percentage\">{progress.percentage.toFixed(1)}%</span>\n\n <div style={{ display: \"flex\", gap: \"16px\" }}>\n {showSpeed && progress.speed > 0 && (\n <span data-testid=\"progress-speed\">{formatFileSize(progress.speed)}/s</span>\n )}\n\n {showRemainingTime && progress.remainingTime > 0 && (\n <span data-testid=\"progress-remaining-time\">\n {formatTime(progress.remainingTime)} remaining\n </span>\n )}\n </div>\n </div>\n </div>\n );\n}\n","import { ReactNode, CSSProperties, useState, useEffect } from \"react\";\nimport type { UploadTask } from \"@chunkflowjs/core\";\nimport { UploadStatus } from \"@chunkflowjs/protocol\";\nimport { formatFileSize } from \"@chunkflowjs/shared\";\nimport { useUploadList } from \"@chunkflowjs/upload-client-react\";\nimport { UploadProgress } from \"./UploadProgress\";\n\n/**\n * Props for the UploadList component\n */\nexport interface UploadListProps {\n /**\n * CSS class name for the container\n */\n className?: string;\n\n /**\n * Custom styles for the container\n */\n style?: CSSProperties;\n\n /**\n * Base URL for file downloads (e.g., \"http://localhost:3001\")\n * If provided, relative file URLs will be converted to absolute URLs\n */\n baseURL?: string;\n\n /**\n * Custom render function for each upload item\n * If provided, this will be used instead of the default item renderer\n */\n renderItem?: (task: UploadTask, actions: UploadItemActions) => ReactNode;\n\n /**\n * Whether to show completed uploads\n * @default true\n */\n showCompleted?: boolean;\n\n /**\n * Whether to show failed uploads\n * @default true\n */\n showFailed?: boolean;\n\n /**\n * Whether to show cancelled uploads\n * @default true\n */\n showCancelled?: boolean;\n\n /**\n * Maximum number of items to display\n * If not set, all items will be displayed\n */\n maxItems?: number;\n\n /**\n * Message to display when there are no uploads\n * @default \"No uploads\"\n */\n emptyMessage?: string;\n}\n\n/**\n * Actions available for each upload item\n */\nexport interface UploadItemActions {\n /** Pause the upload */\n pause: () => void;\n /** Resume the upload */\n resume: () => void;\n /** Cancel the upload */\n cancel: () => void;\n /** Remove the upload from the list */\n remove: () => void;\n}\n\n/**\n * Props for the default upload item component\n */\ninterface DefaultUploadItemProps {\n task: UploadTask;\n onRemove: () => void;\n baseURL?: string;\n}\n\n/**\n * Default upload item component\n *\n * Displays file info, progress, and action buttons\n */\nfunction DefaultUploadItem({ task, onRemove, baseURL }: DefaultUploadItemProps) {\n const status = task.getStatus();\n const [fileUrl, setFileUrl] = useState<string | null>(null);\n\n // Listen to success event to get fileUrl\n useEffect(() => {\n const handleSuccess = ({ fileUrl }: { fileUrl: string }) => {\n // Convert relative URL to absolute URL if baseURL is provided\n const absoluteUrl = baseURL && fileUrl.startsWith(\"/\") ? `${baseURL}${fileUrl}` : fileUrl;\n setFileUrl(absoluteUrl);\n };\n\n task.on(\"success\", handleSuccess);\n\n return () => {\n task.off(\"success\", handleSuccess);\n };\n }, [task, baseURL]);\n\n const handleClick = () => {\n // Only open file if upload is successful and we have a URL\n if (status === UploadStatus.SUCCESS && fileUrl) {\n window.open(fileUrl, \"_blank\");\n }\n };\n\n const defaultStyles = {\n container: {\n padding: \"16px\",\n border: \"1px solid var(--border, #333)\",\n borderRadius: \"8px\",\n marginBottom: \"8px\",\n backgroundColor: \"var(--darker-bg, #0f0f0f)\",\n cursor: status === UploadStatus.SUCCESS && fileUrl ? \"pointer\" : \"default\",\n transition: \"all 0.2s\",\n } as CSSProperties,\n fileInfo: {\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"8px\",\n } as CSSProperties,\n fileName: {\n fontWeight: 500,\n fontSize: \"14px\",\n color: \"var(--text, #e0e0e0)\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\" as const,\n flex: 1,\n marginRight: \"8px\",\n } as CSSProperties,\n fileSize: {\n fontSize: \"12px\",\n color: \"var(--text-dim, #999)\",\n } as CSSProperties,\n status: {\n fontSize: \"12px\",\n padding: \"2px 8px\",\n borderRadius: \"4px\",\n marginLeft: \"8px\",\n } as CSSProperties,\n actions: {\n display: \"flex\",\n gap: \"8px\",\n marginTop: \"8px\",\n } as CSSProperties,\n button: {\n padding: \"4px 12px\",\n fontSize: \"12px\",\n border: \"1px solid var(--border, #333)\",\n borderRadius: \"4px\",\n backgroundColor: \"var(--card-bg, #1e1e1e)\",\n color: \"var(--text, #e0e0e0)\",\n cursor: \"pointer\",\n transition: \"all 0.2s\",\n } as CSSProperties,\n };\n\n const getStatusStyle = (status: UploadStatus): CSSProperties => {\n const baseStyle = defaultStyles.status;\n switch (status) {\n case UploadStatus.UPLOADING:\n return { ...baseStyle, backgroundColor: \"rgba(33, 150, 243, 0.15)\", color: \"#42a5f5\" };\n case UploadStatus.SUCCESS:\n return { ...baseStyle, backgroundColor: \"rgba(76, 175, 80, 0.15)\", color: \"#66bb6a\" };\n case UploadStatus.ERROR:\n return { ...baseStyle, backgroundColor: \"rgba(244, 67, 54, 0.15)\", color: \"#ef5350\" };\n case UploadStatus.PAUSED:\n return { ...baseStyle, backgroundColor: \"rgba(255, 152, 0, 0.15)\", color: \"#ffa726\" };\n case UploadStatus.CANCELLED:\n return { ...baseStyle, backgroundColor: \"rgba(158, 158, 158, 0.15)\", color: \"#bdbdbd\" };\n default:\n return { ...baseStyle, backgroundColor: \"rgba(158, 158, 158, 0.15)\", color: \"#bdbdbd\" };\n }\n };\n\n const getStatusText = (status: UploadStatus): string => {\n switch (status) {\n case UploadStatus.IDLE:\n return \"Idle\";\n case UploadStatus.HASHING:\n return \"Hashing\";\n case UploadStatus.UPLOADING:\n return \"Uploading\";\n case UploadStatus.PAUSED:\n return \"Paused\";\n case UploadStatus.SUCCESS:\n return \"Success\";\n case UploadStatus.ERROR:\n return \"Error\";\n case UploadStatus.CANCELLED:\n return \"Cancelled\";\n default:\n return \"Unknown\";\n }\n };\n\n return (\n <div\n style={defaultStyles.container}\n data-testid=\"upload-item\"\n onClick={handleClick}\n onMouseEnter={(e) => {\n if (status === UploadStatus.SUCCESS && fileUrl) {\n e.currentTarget.style.backgroundColor = \"var(--card-bg, #1e1e1e)\";\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = \"var(--darker-bg, #0f0f0f)\";\n }}\n title={status === UploadStatus.SUCCESS && fileUrl ? \"Click to open file\" : \"\"}\n >\n {/* File info */}\n <div style={defaultStyles.fileInfo}>\n <div style={{ display: \"flex\", alignItems: \"center\", flex: 1, minWidth: 0 }}>\n <span style={defaultStyles.fileName} title={task.file.name}>\n {task.file.name}\n </span>\n <span style={defaultStyles.fileSize}>{formatFileSize(task.file.size)}</span>\n </div>\n <span style={getStatusStyle(status)} data-testid=\"upload-status\">\n {getStatusText(status)}\n </span>\n </div>\n\n {/* Progress bar (only show for active uploads) */}\n {(status === UploadStatus.UPLOADING ||\n status === UploadStatus.PAUSED ||\n status === UploadStatus.HASHING) && <UploadProgress task={task} />}\n\n {/* Actions */}\n <div style={defaultStyles.actions} onClick={(e) => e.stopPropagation()}>\n {status === UploadStatus.UPLOADING && (\n <button\n onClick={() => task.pause()}\n style={defaultStyles.button}\n data-testid=\"pause-button\"\n type=\"button\"\n >\n Pause\n </button>\n )}\n\n {status === UploadStatus.PAUSED && (\n <button\n onClick={() => task.resume()}\n style={defaultStyles.button}\n data-testid=\"resume-button\"\n type=\"button\"\n >\n Resume\n </button>\n )}\n\n {(status === UploadStatus.UPLOADING || status === UploadStatus.PAUSED) && (\n <button\n onClick={() => task.cancel()}\n style={defaultStyles.button}\n data-testid=\"cancel-button\"\n type=\"button\"\n >\n Cancel\n </button>\n )}\n\n {status === UploadStatus.SUCCESS && fileUrl && (\n <button\n onClick={handleClick}\n style={defaultStyles.button}\n data-testid=\"open-button\"\n type=\"button\"\n >\n Open\n </button>\n )}\n\n <button\n onClick={onRemove}\n style={defaultStyles.button}\n data-testid=\"remove-button\"\n type=\"button\"\n >\n Remove\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * UploadList component\n *\n * Displays a list of upload tasks with progress bars and action buttons.\n * Integrates with useUploadList hook to automatically sync with the upload manager.\n *\n * @example\n * ```tsx\n * // Basic usage\n * <UploadList />\n *\n * // With custom styling\n * <UploadList\n * className=\"my-upload-list\"\n * showCompleted={false}\n * maxItems={10}\n * />\n *\n * // With custom item renderer\n * <UploadList\n * renderItem={(task, actions) => (\n * <div>\n * <h3>{task.file.name}</h3>\n * <button onClick={actions.pause}>Pause</button>\n * <button onClick={actions.remove}>Remove</button>\n * </div>\n * )}\n * />\n * ```\n */\nexport function UploadList({\n className,\n style,\n baseURL,\n renderItem,\n showCompleted = true,\n showFailed = true,\n showCancelled = true,\n maxItems,\n emptyMessage = \"No uploads\",\n}: UploadListProps) {\n const { tasks, removeTask } = useUploadList();\n\n // Filter tasks based on props\n const filteredTasks = tasks.filter((task) => {\n const status = task.getStatus();\n\n if (!showCompleted && status === UploadStatus.SUCCESS) {\n return false;\n }\n\n if (!showFailed && status === UploadStatus.ERROR) {\n return false;\n }\n\n if (!showCancelled && status === UploadStatus.CANCELLED) {\n return false;\n }\n\n return true;\n });\n\n // Limit number of items if maxItems is set\n const displayTasks = maxItems ? filteredTasks.slice(0, maxItems) : filteredTasks;\n\n const defaultStyles = {\n container: {\n width: \"100%\",\n } as CSSProperties,\n empty: {\n padding: \"32px\",\n textAlign: \"center\" as const,\n color: \"var(--text-dim, #999)\",\n fontSize: \"14px\",\n } as CSSProperties,\n };\n\n // Show empty message if no tasks\n if (displayTasks.length === 0) {\n return (\n <div\n className={className}\n style={style || defaultStyles.container}\n data-testid=\"upload-list-empty\"\n >\n <div style={defaultStyles.empty}>{emptyMessage}</div>\n </div>\n );\n }\n\n return (\n <div className={className} style={style || defaultStyles.container} data-testid=\"upload-list\">\n {displayTasks.map((task) => {\n const actions: UploadItemActions = {\n pause: () => task.pause(),\n resume: () => task.resume(),\n cancel: () => task.cancel(),\n remove: () => removeTask(task.id),\n };\n\n return (\n <div key={task.id} data-testid=\"upload-list-item\">\n {renderItem ? (\n renderItem(task, actions)\n ) : (\n <DefaultUploadItem task={task} onRemove={actions.remove} baseURL={baseURL} />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n","import { useState, useRef, DragEvent, ReactNode, CSSProperties } from \"react\";\n\n/**\n * Props for the UploadDropzone component\n */\nexport interface UploadDropzoneProps {\n /**\n * Accepted file types (e.g., \"image/*\", \".pdf,.doc\")\n */\n accept?: string;\n\n /**\n * Maximum file size in bytes\n */\n maxSize?: number;\n\n /**\n * Allow multiple file selection\n * @default true\n */\n multiple?: boolean;\n\n /**\n * Callback when files are dropped\n */\n onDrop?: (files: File[]) => void;\n\n /**\n * Callback when file validation fails\n */\n onError?: (error: FileValidationError) => void;\n\n /**\n * Custom content to display in the dropzone\n */\n children?: ReactNode;\n\n /**\n * CSS class name for the container\n */\n className?: string;\n\n /**\n * Custom styles for the container\n */\n style?: CSSProperties;\n\n /**\n * CSS class name when dragging over\n */\n draggingClassName?: string;\n\n /**\n * Disable the dropzone\n * @default false\n */\n disabled?: boolean;\n}\n\n/**\n * File validation error\n */\nexport class FileValidationError extends Error {\n constructor(\n message: string,\n public code: \"FILE_TOO_LARGE\" | \"INVALID_FILE_TYPE\",\n public file: File,\n ) {\n super(message);\n this.name = \"FileValidationError\";\n }\n}\n\n/**\n * Check if a file matches the accept pattern\n *\n * @param file - The file to check\n * @param accept - The accept pattern (e.g., \"image/star\", \".pdf,.doc\", \"star/star\")\n * @returns true if the file matches the pattern\n */\nfunction matchAccept(file: File, accept: string): boolean {\n // Accept all files if pattern is \"*/*\" or \"*\"\n if (accept === \"*/*\" || accept === \"*\") {\n return true;\n }\n\n const patterns = accept.split(\",\").map((p) => p.trim());\n\n for (const pattern of patterns) {\n // Accept all files\n if (pattern === \"*/*\" || pattern === \"*\") {\n return true;\n }\n\n // Exact MIME type match (e.g., \"image/jpeg\")\n if (pattern === file.type) {\n return true;\n }\n\n // Wildcard MIME type match (e.g., \"image/*\")\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -2);\n if (file.type.startsWith(prefix + \"/\")) {\n return true;\n }\n }\n\n // File extension match (e.g., \".pdf\")\n if (pattern.startsWith(\".\")) {\n if (file.name.toLowerCase().endsWith(pattern.toLowerCase())) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * UploadDropzone component\n *\n * A drag-and-drop zone for file uploads with visual feedback.\n * Validates files based on type and size before passing them to the onDrop callback.\n *\n * @example\n * ```tsx\n * <UploadDropzone\n * accept=\"image/*\"\n * maxSize={10 * 1024 * 1024} // 10MB\n * multiple\n * onDrop={(files) => console.log('Dropped:', files)}\n * >\n * <p>Drag and drop files here, or click to select</p>\n * </UploadDropzone>\n * ```\n */\nexport function UploadDropzone({\n accept,\n maxSize,\n multiple = true,\n onDrop,\n onError,\n children,\n className,\n style,\n draggingClassName,\n disabled = false,\n}: UploadDropzoneProps) {\n const [isDragging, setIsDragging] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const dragCounterRef = useRef(0);\n\n const validateFile = (file: File): FileValidationError | null => {\n // Validate file size\n if (maxSize && file.size > maxSize) {\n return new FileValidationError(\n `File \"${file.name}\" size ${file.size} bytes exceeds maximum ${maxSize} bytes`,\n \"FILE_TOO_LARGE\",\n file,\n );\n }\n\n // Validate file type\n if (accept && !matchAccept(file, accept)) {\n return new FileValidationError(\n `File \"${file.name}\" type \"${file.type}\" is not accepted`,\n \"INVALID_FILE_TYPE\",\n file,\n );\n }\n\n return null;\n };\n\n const processFiles = (files: File[]) => {\n if (files.length === 0) {\n return;\n }\n\n // Validate all files\n const validFiles: File[] = [];\n const errors: FileValidationError[] = [];\n\n for (const file of files) {\n const error = validateFile(file);\n if (error) {\n errors.push(error);\n } else {\n validFiles.push(file);\n }\n }\n\n // Report errors\n if (errors.length > 0 && onError) {\n errors.forEach((error) => onError(error));\n }\n\n // Call onDrop with valid files\n if (validFiles.length > 0 && onDrop) {\n onDrop(validFiles);\n }\n };\n\n const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (disabled) return;\n\n dragCounterRef.current++;\n if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {\n setIsDragging(true);\n }\n };\n\n const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (disabled) return;\n\n dragCounterRef.current--;\n if (dragCounterRef.current === 0) {\n setIsDragging(false);\n }\n };\n\n const handleDragOver = (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (disabled) return;\n\n // Set dropEffect to indicate this is a copy operation\n if (e.dataTransfer) {\n e.dataTransfer.dropEffect = \"copy\";\n }\n };\n\n const handleDrop = (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (disabled) return;\n\n setIsDragging(false);\n dragCounterRef.current = 0;\n\n const files = Array.from(e.dataTransfer?.files || []);\n\n // Limit to single file if multiple is false\n const filesToProcess = multiple ? files : files.slice(0, 1);\n\n processFiles(filesToProcess);\n };\n\n const handleClick = () => {\n if (!disabled) {\n inputRef.current?.click();\n }\n };\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = Array.from(e.target.files || []);\n processFiles(files);\n\n // Reset input value to allow selecting the same file again\n if (inputRef.current) {\n inputRef.current.value = \"\";\n }\n };\n\n const defaultStyles = {\n container: {\n border: \"2px dashed #ccc\",\n borderRadius: \"8px\",\n padding: \"32px\",\n textAlign: \"center\" as const,\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n transition: \"all 0.3s ease\",\n backgroundColor: disabled ? \"#f5f5f5\" : \"#fafafa\",\n opacity: disabled ? 0.6 : 1,\n } as CSSProperties,\n dragging: {\n borderColor: \"#4caf50\",\n backgroundColor: \"#e8f5e9\",\n } as CSSProperties,\n content: {\n color: \"#666\",\n fontSize: \"14px\",\n } as CSSProperties,\n };\n\n const containerStyle = {\n ...defaultStyles.container,\n ...(isDragging && !disabled ? defaultStyles.dragging : {}),\n ...style,\n };\n\n const containerClassName = [className, isDragging && !disabled ? draggingClassName : \"\"]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <>\n <div\n className={containerClassName || undefined}\n style={containerStyle}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n onClick={handleClick}\n data-testid=\"upload-dropzone\"\n data-dragging={isDragging}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled}\n onKeyDown={(e) => {\n if (!disabled && (e.key === \"Enter\" || e.key === \" \")) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {children || (\n <div style={defaultStyles.content} data-testid=\"dropzone-default-content\">\n <p>\n <strong>Drag and drop files here</strong>\n </p>\n <p>or click to select files</p>\n </div>\n )}\n </div>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n onChange={handleInputChange}\n style={{ display: \"none\" }}\n aria-hidden=\"true\"\n disabled={disabled}\n />\n </>\n );\n}\n","// React components exports\nexport { UploadButton, FileValidationError } from \"./UploadButton\";\nexport type { UploadButtonProps } from \"./UploadButton\";\n\nexport { UploadProgress } from \"./UploadProgress\";\nexport type { UploadProgressProps } from \"./UploadProgress\";\n\nexport { UploadList } from \"./UploadList\";\nexport type { UploadListProps, UploadItemActions } from \"./UploadList\";\n\nexport { UploadDropzone } from \"./UploadDropzone\";\nexport type { UploadDropzoneProps } from \"./UploadDropzone\";\n\n// Placeholder export to allow build to succeed\nexport const UPLOAD_COMPONENT_REACT_VERSION = \"0.0.0\";\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chunkflowjs/upload-component-react",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "React UI components for ChunkFlow Upload SDK",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"components",
|
|
7
|
+
"react",
|
|
8
|
+
"typescript",
|
|
9
|
+
"ui",
|
|
10
|
+
"upload"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.mjs",
|
|
19
|
+
"module": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"import": "./dist/index.mjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown src/index.ts --format esm --dts",
|
|
29
|
+
"dev": "tsdown src/index.ts --format esm --dts --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"clean": "rm -rf dist"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@chunkflowjs/core": "workspace:*",
|
|
37
|
+
"@chunkflowjs/shared": "workspace:*",
|
|
38
|
+
"@chunkflowjs/upload-client-react": "workspace:*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
42
|
+
"@testing-library/react": "^16.3.2",
|
|
43
|
+
"@testing-library/user-event": "^14.6.1",
|
|
44
|
+
"@types/react": "^19.2.10",
|
|
45
|
+
"@types/react-dom": "^19.2.3",
|
|
46
|
+
"react": "^19.2.4",
|
|
47
|
+
"react-dom": "^19.2.4",
|
|
48
|
+
"tsdown": "^0.20.1",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vitest": "^4.0.18"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": "^18.0.0",
|
|
54
|
+
"react-dom": "^18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|