@ibanzajoe/uploader 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.cjs +709 -289
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -8
- package/dist/index.d.ts +90 -8
- package/dist/index.js +708 -291
- package/dist/index.js.map +1 -1
- package/dist/styles.css +561 -129
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
CameraCapture: () => CameraCapture,
|
|
23
24
|
DropPane: () => DropPane,
|
|
24
25
|
ImageEditor: () => ImageEditor,
|
|
25
26
|
PickerOverlay: () => PickerOverlay,
|
|
@@ -28,10 +29,12 @@ __export(src_exports, {
|
|
|
28
29
|
editImage: () => editImage,
|
|
29
30
|
flip: () => flip,
|
|
30
31
|
flop: () => flop,
|
|
32
|
+
isCameraSupported: () => isCameraSupported,
|
|
31
33
|
output: () => output,
|
|
32
34
|
quality: () => quality,
|
|
33
35
|
resize: () => resize,
|
|
34
36
|
rotate: () => rotate,
|
|
37
|
+
shouldOfferCamera: () => shouldOfferCamera,
|
|
35
38
|
transformUrl: () => transformUrl,
|
|
36
39
|
usePicker: () => usePicker
|
|
37
40
|
});
|
|
@@ -337,7 +340,7 @@ function transformUrl({ handle, ops, apiUrl = "" }) {
|
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
// src/react/PickerOverlay.tsx
|
|
340
|
-
var
|
|
343
|
+
var import_react6 = require("react");
|
|
341
344
|
|
|
342
345
|
// src/react/usePicker.ts
|
|
343
346
|
var import_react = require("react");
|
|
@@ -960,8 +963,458 @@ function ImageEditor({
|
|
|
960
963
|
);
|
|
961
964
|
}
|
|
962
965
|
|
|
963
|
-
// src/react/
|
|
966
|
+
// src/react/Dropzone.tsx
|
|
967
|
+
var import_react3 = require("react");
|
|
964
968
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
969
|
+
function Dropzone({
|
|
970
|
+
onFiles,
|
|
971
|
+
isDragOver,
|
|
972
|
+
onDragEnter,
|
|
973
|
+
onDragLeave,
|
|
974
|
+
onDragOver,
|
|
975
|
+
onDrop,
|
|
976
|
+
accept,
|
|
977
|
+
multiple = true,
|
|
978
|
+
onCamera,
|
|
979
|
+
hint,
|
|
980
|
+
className,
|
|
981
|
+
style
|
|
982
|
+
}) {
|
|
983
|
+
const inputRef = (0, import_react3.useRef)(null);
|
|
984
|
+
const openFileDialog = (0, import_react3.useCallback)(() => inputRef.current?.click(), []);
|
|
985
|
+
const handleInputChange = (0, import_react3.useCallback)(
|
|
986
|
+
(e) => {
|
|
987
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
988
|
+
onFiles(e.target.files);
|
|
989
|
+
e.target.value = "";
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
[onFiles]
|
|
993
|
+
);
|
|
994
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
995
|
+
"div",
|
|
996
|
+
{
|
|
997
|
+
className: ["uploader-dropzone", className ?? ""].filter(Boolean).join(" "),
|
|
998
|
+
"data-drag-over": isDragOver ? "true" : void 0,
|
|
999
|
+
role: "button",
|
|
1000
|
+
tabIndex: 0,
|
|
1001
|
+
"aria-label": "Drop files here or click to browse",
|
|
1002
|
+
style,
|
|
1003
|
+
onDragEnter,
|
|
1004
|
+
onDragLeave,
|
|
1005
|
+
onDragOver,
|
|
1006
|
+
onDrop,
|
|
1007
|
+
onClick: openFileDialog,
|
|
1008
|
+
onKeyDown: (e) => {
|
|
1009
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1010
|
+
e.preventDefault();
|
|
1011
|
+
openFileDialog();
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
children: [
|
|
1015
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "uploader-dropzone-icon-wrap", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "uploader-dropzone-icon" }) }),
|
|
1016
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "uploader-dropzone-label", children: "Drag & drop files here" }),
|
|
1017
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "uploader-dropzone-sublabel", children: "or" }),
|
|
1018
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "uploader-dropzone-actions", children: [
|
|
1019
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1020
|
+
"button",
|
|
1021
|
+
{
|
|
1022
|
+
type: "button",
|
|
1023
|
+
className: "uploader-btn-browse",
|
|
1024
|
+
onClick: (e) => {
|
|
1025
|
+
e.stopPropagation();
|
|
1026
|
+
openFileDialog();
|
|
1027
|
+
},
|
|
1028
|
+
children: "Browse files"
|
|
1029
|
+
}
|
|
1030
|
+
),
|
|
1031
|
+
onCamera && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1032
|
+
"button",
|
|
1033
|
+
{
|
|
1034
|
+
type: "button",
|
|
1035
|
+
className: "uploader-btn-camera",
|
|
1036
|
+
onClick: (e) => {
|
|
1037
|
+
e.stopPropagation();
|
|
1038
|
+
onCamera();
|
|
1039
|
+
},
|
|
1040
|
+
children: [
|
|
1041
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1042
|
+
"svg",
|
|
1043
|
+
{
|
|
1044
|
+
className: "uploader-btn-camera-icon",
|
|
1045
|
+
width: "16",
|
|
1046
|
+
height: "16",
|
|
1047
|
+
viewBox: "0 0 24 24",
|
|
1048
|
+
fill: "none",
|
|
1049
|
+
"aria-hidden": "true",
|
|
1050
|
+
children: [
|
|
1051
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1052
|
+
"path",
|
|
1053
|
+
{
|
|
1054
|
+
d: "M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2a1 1 0 0 0 .83-.45l.74-1.1A1 1 0 0 1 10.1 4h3.8a1 1 0 0 1 .83.45l.74 1.1a1 1 0 0 0 .83.45h1.2A2.5 2.5 0 0 1 20 8.5v8A2.5 2.5 0 0 1 17.5 19h-11A2.5 2.5 0 0 1 4 16.5v-8Z",
|
|
1055
|
+
stroke: "currentColor",
|
|
1056
|
+
strokeWidth: "1.6",
|
|
1057
|
+
strokeLinejoin: "round"
|
|
1058
|
+
}
|
|
1059
|
+
),
|
|
1060
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12.5", r: "3.2", stroke: "currentColor", strokeWidth: "1.6" })
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
),
|
|
1064
|
+
"Take photo"
|
|
1065
|
+
]
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
] }),
|
|
1069
|
+
hint && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "uploader-dropzone-hint", children: hint }),
|
|
1070
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1071
|
+
"input",
|
|
1072
|
+
{
|
|
1073
|
+
ref: inputRef,
|
|
1074
|
+
type: "file",
|
|
1075
|
+
multiple,
|
|
1076
|
+
accept,
|
|
1077
|
+
className: "uploader-file-input",
|
|
1078
|
+
"aria-hidden": "true",
|
|
1079
|
+
tabIndex: -1,
|
|
1080
|
+
onChange: handleInputChange
|
|
1081
|
+
}
|
|
1082
|
+
)
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/react/CameraCapture.tsx
|
|
1089
|
+
var import_react4 = require("react");
|
|
1090
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1091
|
+
function isCameraSupported() {
|
|
1092
|
+
return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined";
|
|
1093
|
+
}
|
|
1094
|
+
var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
|
|
1095
|
+
function acceptsImages(accept) {
|
|
1096
|
+
if (!accept || accept.length === 0) return true;
|
|
1097
|
+
return accept.some((a) => a.startsWith("image/") || a === "image/*" || IMAGE_EXT.test(a.trim()));
|
|
1098
|
+
}
|
|
1099
|
+
function shouldOfferCamera(options) {
|
|
1100
|
+
if (!isCameraSupported()) return false;
|
|
1101
|
+
if (!acceptsImages(options?.accept)) return false;
|
|
1102
|
+
const sources = options?.fromSources;
|
|
1103
|
+
if (sources && !sources.includes("camera")) return false;
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
function extForType(mime) {
|
|
1107
|
+
switch (mime) {
|
|
1108
|
+
case "image/png":
|
|
1109
|
+
return "png";
|
|
1110
|
+
case "image/webp":
|
|
1111
|
+
return "webp";
|
|
1112
|
+
default:
|
|
1113
|
+
return "jpg";
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function describeCameraError(err) {
|
|
1117
|
+
const name = err instanceof DOMException ? err.name : "";
|
|
1118
|
+
switch (name) {
|
|
1119
|
+
case "NotAllowedError":
|
|
1120
|
+
case "SecurityError":
|
|
1121
|
+
return "Camera access was blocked. Allow camera permission and try again.";
|
|
1122
|
+
case "NotFoundError":
|
|
1123
|
+
case "OverconstrainedError":
|
|
1124
|
+
return "No camera was found on this device.";
|
|
1125
|
+
case "NotReadableError":
|
|
1126
|
+
return "The camera is already in use by another application.";
|
|
1127
|
+
default:
|
|
1128
|
+
return "Could not start the camera.";
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function CameraCapture({
|
|
1132
|
+
onCapture,
|
|
1133
|
+
onCancel,
|
|
1134
|
+
facingMode = "user",
|
|
1135
|
+
outputType = "image/jpeg",
|
|
1136
|
+
quality: quality2 = 0.92,
|
|
1137
|
+
fileNamePrefix = "camera-photo",
|
|
1138
|
+
className,
|
|
1139
|
+
style
|
|
1140
|
+
}) {
|
|
1141
|
+
const videoRef = (0, import_react4.useRef)(null);
|
|
1142
|
+
const streamRef = (0, import_react4.useRef)(null);
|
|
1143
|
+
const blobRef = (0, import_react4.useRef)(null);
|
|
1144
|
+
const [status, setStatus] = (0, import_react4.useState)("initializing");
|
|
1145
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
1146
|
+
const [stillUrl, setStillUrl] = (0, import_react4.useState)("");
|
|
1147
|
+
const mirror = facingMode === "user";
|
|
1148
|
+
const stopStream = (0, import_react4.useCallback)(() => {
|
|
1149
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1150
|
+
streamRef.current = null;
|
|
1151
|
+
if (videoRef.current) videoRef.current.srcObject = null;
|
|
1152
|
+
}, []);
|
|
1153
|
+
const startStream = (0, import_react4.useCallback)(async () => {
|
|
1154
|
+
setError(null);
|
|
1155
|
+
setStatus("initializing");
|
|
1156
|
+
if (!isCameraSupported()) {
|
|
1157
|
+
setError("Camera capture is not supported in this browser.");
|
|
1158
|
+
setStatus("error");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1163
|
+
video: { facingMode },
|
|
1164
|
+
audio: false
|
|
1165
|
+
});
|
|
1166
|
+
if (videoRef.current == null) {
|
|
1167
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
streamRef.current = stream;
|
|
1171
|
+
videoRef.current.srcObject = stream;
|
|
1172
|
+
setStatus("live");
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
setError(describeCameraError(err));
|
|
1175
|
+
setStatus("error");
|
|
1176
|
+
}
|
|
1177
|
+
}, [facingMode]);
|
|
1178
|
+
(0, import_react4.useEffect)(() => {
|
|
1179
|
+
void startStream();
|
|
1180
|
+
return () => {
|
|
1181
|
+
stopStream();
|
|
1182
|
+
};
|
|
1183
|
+
}, [startStream, stopStream]);
|
|
1184
|
+
(0, import_react4.useEffect)(() => {
|
|
1185
|
+
if (!stillUrl) return;
|
|
1186
|
+
return () => URL.revokeObjectURL(stillUrl);
|
|
1187
|
+
}, [stillUrl]);
|
|
1188
|
+
const capture = (0, import_react4.useCallback)(() => {
|
|
1189
|
+
const video = videoRef.current;
|
|
1190
|
+
if (!video) return;
|
|
1191
|
+
const w = video.videoWidth;
|
|
1192
|
+
const h = video.videoHeight;
|
|
1193
|
+
if (!w || !h) {
|
|
1194
|
+
setError("The camera is still warming up \u2014 try again in a moment.");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const canvas = document.createElement("canvas");
|
|
1198
|
+
canvas.width = w;
|
|
1199
|
+
canvas.height = h;
|
|
1200
|
+
const ctx = canvas.getContext("2d");
|
|
1201
|
+
if (!ctx) {
|
|
1202
|
+
setError("Canvas is unavailable for capturing.");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (mirror) {
|
|
1206
|
+
ctx.translate(w, 0);
|
|
1207
|
+
ctx.scale(-1, 1);
|
|
1208
|
+
}
|
|
1209
|
+
ctx.drawImage(video, 0, 0, w, h);
|
|
1210
|
+
canvas.toBlob(
|
|
1211
|
+
(blob) => {
|
|
1212
|
+
if (!blob) {
|
|
1213
|
+
setError("Failed to capture the photo.");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
blobRef.current = blob;
|
|
1217
|
+
setStillUrl(URL.createObjectURL(blob));
|
|
1218
|
+
setStatus("captured");
|
|
1219
|
+
stopStream();
|
|
1220
|
+
},
|
|
1221
|
+
outputType,
|
|
1222
|
+
quality2
|
|
1223
|
+
);
|
|
1224
|
+
}, [mirror, outputType, quality2, stopStream]);
|
|
1225
|
+
const retake = (0, import_react4.useCallback)(() => {
|
|
1226
|
+
blobRef.current = null;
|
|
1227
|
+
setStillUrl("");
|
|
1228
|
+
void startStream();
|
|
1229
|
+
}, [startStream]);
|
|
1230
|
+
const confirm = (0, import_react4.useCallback)(() => {
|
|
1231
|
+
const blob = blobRef.current;
|
|
1232
|
+
if (!blob) return;
|
|
1233
|
+
const name = `${fileNamePrefix}-${Date.now()}.${extForType(outputType)}`;
|
|
1234
|
+
const file = new File([blob], name, { type: outputType, lastModified: Date.now() });
|
|
1235
|
+
onCapture(file);
|
|
1236
|
+
}, [fileNamePrefix, outputType, onCapture]);
|
|
1237
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1238
|
+
"div",
|
|
1239
|
+
{
|
|
1240
|
+
className: ["uploader-camera", className ?? ""].filter(Boolean).join(" "),
|
|
1241
|
+
style,
|
|
1242
|
+
"data-testid": "camera-capture",
|
|
1243
|
+
children: [
|
|
1244
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-camera-stage", children: [
|
|
1245
|
+
status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-error", role: "alert", children: error }) : status === "captured" ? (
|
|
1246
|
+
/* eslint-disable-next-line @next/next/no-img-element */
|
|
1247
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: stillUrl, alt: "Captured preview", className: "uploader-camera-still" })
|
|
1248
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1249
|
+
"video",
|
|
1250
|
+
{
|
|
1251
|
+
ref: videoRef,
|
|
1252
|
+
className: "uploader-camera-video",
|
|
1253
|
+
autoPlay: true,
|
|
1254
|
+
playsInline: true,
|
|
1255
|
+
muted: true,
|
|
1256
|
+
"data-mirror": mirror ? "true" : void 0
|
|
1257
|
+
}
|
|
1258
|
+
),
|
|
1259
|
+
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" })
|
|
1260
|
+
] }),
|
|
1261
|
+
status === "live" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "uploader-camera-hint", children: "Frame your shot, then tap the shutter to capture." }),
|
|
1262
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-actions", children: status === "captured" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1263
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "uploader-btn-secondary", onClick: retake, children: "Retake" }),
|
|
1264
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "uploader-btn-primary", onClick: confirm, children: "Use photo" })
|
|
1265
|
+
] }) : status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1266
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "uploader-btn-secondary", onClick: onCancel, children: "Back" }),
|
|
1267
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "uploader-btn-primary", onClick: () => void startStream(), children: "Try again" })
|
|
1268
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1269
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "uploader-btn-secondary", onClick: onCancel, children: "Back" }),
|
|
1270
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1271
|
+
"button",
|
|
1272
|
+
{
|
|
1273
|
+
type: "button",
|
|
1274
|
+
className: "uploader-camera-shutter",
|
|
1275
|
+
"aria-label": "Capture photo",
|
|
1276
|
+
disabled: status !== "live",
|
|
1277
|
+
onClick: capture,
|
|
1278
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-camera-shutter-ring", "aria-hidden": "true" })
|
|
1279
|
+
}
|
|
1280
|
+
),
|
|
1281
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-actions-spacer", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-btn-secondary", children: "Back" }) })
|
|
1282
|
+
] }) })
|
|
1283
|
+
]
|
|
1284
|
+
}
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/react/ProgressBar.tsx
|
|
1289
|
+
var import_react5 = require("react");
|
|
1290
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1291
|
+
function ProgressBar({ value, label, size = "sm", className, style }) {
|
|
1292
|
+
const clamped = Math.max(0, Math.min(100, value));
|
|
1293
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1294
|
+
"div",
|
|
1295
|
+
{
|
|
1296
|
+
role: "progressbar",
|
|
1297
|
+
"aria-valuenow": clamped,
|
|
1298
|
+
"aria-valuemin": 0,
|
|
1299
|
+
"aria-valuemax": 100,
|
|
1300
|
+
"aria-label": label,
|
|
1301
|
+
"data-size": size,
|
|
1302
|
+
className: ["uploader-progress-bar", className ?? ""].filter(Boolean).join(" "),
|
|
1303
|
+
style,
|
|
1304
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1305
|
+
"div",
|
|
1306
|
+
{
|
|
1307
|
+
className: "uploader-progress-bar-fill",
|
|
1308
|
+
style: { width: `${clamped}%` }
|
|
1309
|
+
}
|
|
1310
|
+
)
|
|
1311
|
+
}
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/react/FileQueueRow.tsx
|
|
1316
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1317
|
+
function statusLabel(entry) {
|
|
1318
|
+
switch (entry.state) {
|
|
1319
|
+
case "uploading":
|
|
1320
|
+
return entry.progress != null ? `${entry.progress}%` : "Uploading\u2026";
|
|
1321
|
+
case "done":
|
|
1322
|
+
return "Done";
|
|
1323
|
+
case "error":
|
|
1324
|
+
return entry.error ?? "Failed";
|
|
1325
|
+
default:
|
|
1326
|
+
return "Ready";
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function FileQueueRow({ entry, onRemove, onRetry, onEdit }) {
|
|
1330
|
+
const { id, file, state, progress, previewUrl } = entry;
|
|
1331
|
+
const isUploading = state === "uploading";
|
|
1332
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("li", { className: `uploader-file-item uploader-file-${state}`, children: [
|
|
1333
|
+
previewUrl ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1334
|
+
"img",
|
|
1335
|
+
{
|
|
1336
|
+
src: previewUrl,
|
|
1337
|
+
alt: "",
|
|
1338
|
+
"aria-hidden": "true",
|
|
1339
|
+
className: "uploader-file-thumb"
|
|
1340
|
+
}
|
|
1341
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uploader-file-thumb-placeholder", "aria-hidden": "true" }),
|
|
1342
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uploader-file-info", children: [
|
|
1343
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uploader-file-info-row", children: [
|
|
1344
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uploader-file-name", children: file.name }),
|
|
1345
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1346
|
+
"span",
|
|
1347
|
+
{
|
|
1348
|
+
className: "uploader-file-status",
|
|
1349
|
+
"data-state": state,
|
|
1350
|
+
role: state === "error" ? "alert" : void 0,
|
|
1351
|
+
children: statusLabel(entry)
|
|
1352
|
+
}
|
|
1353
|
+
)
|
|
1354
|
+
] }),
|
|
1355
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1356
|
+
ProgressBar,
|
|
1357
|
+
{
|
|
1358
|
+
value: isUploading && progress != null ? progress : state === "done" ? 100 : 0,
|
|
1359
|
+
label: `${file.name} upload progress`
|
|
1360
|
+
}
|
|
1361
|
+
)
|
|
1362
|
+
] }),
|
|
1363
|
+
state === "done" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "uploader-file-done-badge", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "9", height: "7", viewBox: "0 0 9 7", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1364
|
+
"path",
|
|
1365
|
+
{
|
|
1366
|
+
d: "M1 3.5L3.5 6L8 1",
|
|
1367
|
+
stroke: "currentColor",
|
|
1368
|
+
strokeWidth: "1.8",
|
|
1369
|
+
strokeLinecap: "round",
|
|
1370
|
+
strokeLinejoin: "round"
|
|
1371
|
+
}
|
|
1372
|
+
) }) }),
|
|
1373
|
+
!isUploading && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "uploader-file-actions", children: [
|
|
1374
|
+
onEdit && previewUrl && (state === "pending" || state === "error") && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1375
|
+
"button",
|
|
1376
|
+
{
|
|
1377
|
+
type: "button",
|
|
1378
|
+
className: "uploader-btn-edit",
|
|
1379
|
+
"aria-label": `Edit ${file.name}`,
|
|
1380
|
+
onClick: () => onEdit(id),
|
|
1381
|
+
children: "Edit"
|
|
1382
|
+
}
|
|
1383
|
+
),
|
|
1384
|
+
state === "error" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1385
|
+
"button",
|
|
1386
|
+
{
|
|
1387
|
+
type: "button",
|
|
1388
|
+
className: "uploader-btn-retry",
|
|
1389
|
+
"aria-label": `Retry uploading ${file.name}`,
|
|
1390
|
+
onClick: () => onRetry(id),
|
|
1391
|
+
children: "Retry"
|
|
1392
|
+
}
|
|
1393
|
+
),
|
|
1394
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1395
|
+
"button",
|
|
1396
|
+
{
|
|
1397
|
+
type: "button",
|
|
1398
|
+
className: "uploader-btn-remove",
|
|
1399
|
+
"aria-label": `Remove ${file.name}`,
|
|
1400
|
+
onClick: () => onRemove(id),
|
|
1401
|
+
children: "\xD7"
|
|
1402
|
+
}
|
|
1403
|
+
)
|
|
1404
|
+
] })
|
|
1405
|
+
] });
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/react/PickerOverlay.tsx
|
|
1409
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1410
|
+
function dialogTitle(isUploading, isDone, totalFiles, doneCount, errorCount, editingId, cameraOpen) {
|
|
1411
|
+
if (editingId) return "Edit Image";
|
|
1412
|
+
if (cameraOpen) return "Take a photo";
|
|
1413
|
+
if (isDone && errorCount === 0) return "All files uploaded";
|
|
1414
|
+
if (isDone && errorCount > 0) return `${doneCount} of ${totalFiles} uploaded`;
|
|
1415
|
+
if (isUploading) return `Uploading ${totalFiles} file${totalFiles !== 1 ? "s" : ""}`;
|
|
1416
|
+
return "Upload files";
|
|
1417
|
+
}
|
|
965
1418
|
function PickerOverlay({
|
|
966
1419
|
open,
|
|
967
1420
|
onClose,
|
|
@@ -976,11 +1429,11 @@ function PickerOverlay({
|
|
|
976
1429
|
className,
|
|
977
1430
|
style
|
|
978
1431
|
}) {
|
|
979
|
-
const titleId = (0,
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
const [
|
|
983
|
-
const [
|
|
1432
|
+
const titleId = (0, import_react6.useId)();
|
|
1433
|
+
const panelRef = (0, import_react6.useRef)(null);
|
|
1434
|
+
const [isDragOver, setIsDragOver] = (0, import_react6.useState)(false);
|
|
1435
|
+
const [editingId, setEditingId] = (0, import_react6.useState)(null);
|
|
1436
|
+
const [cameraOpen, setCameraOpen] = (0, import_react6.useState)(false);
|
|
984
1437
|
const { files, addFiles, removeFile, editFile, retryFile, upload, progress, isUploading, isDone } = usePicker({
|
|
985
1438
|
apikey,
|
|
986
1439
|
apiUrl,
|
|
@@ -990,7 +1443,7 @@ function PickerOverlay({
|
|
|
990
1443
|
onFileUploadFinished,
|
|
991
1444
|
onFileUploadFailed
|
|
992
1445
|
});
|
|
993
|
-
(0,
|
|
1446
|
+
(0, import_react6.useEffect)(() => {
|
|
994
1447
|
if (!open) return;
|
|
995
1448
|
const handler = (e) => {
|
|
996
1449
|
if (e.key === "Escape") {
|
|
@@ -1001,7 +1454,7 @@ function PickerOverlay({
|
|
|
1001
1454
|
document.addEventListener("keydown", handler);
|
|
1002
1455
|
return () => document.removeEventListener("keydown", handler);
|
|
1003
1456
|
}, [open, onClose, onCancel]);
|
|
1004
|
-
(0,
|
|
1457
|
+
(0, import_react6.useEffect)(() => {
|
|
1005
1458
|
if (!open || !panelRef.current) return;
|
|
1006
1459
|
const panel = panelRef.current;
|
|
1007
1460
|
const focusable = panel.querySelectorAll(
|
|
@@ -1027,8 +1480,8 @@ function PickerOverlay({
|
|
|
1027
1480
|
};
|
|
1028
1481
|
document.addEventListener("keydown", trap);
|
|
1029
1482
|
return () => document.removeEventListener("keydown", trap);
|
|
1030
|
-
}, [open, files, editingId]);
|
|
1031
|
-
(0,
|
|
1483
|
+
}, [open, files, editingId, cameraOpen]);
|
|
1484
|
+
(0, import_react6.useEffect)(() => {
|
|
1032
1485
|
if (!open) return;
|
|
1033
1486
|
const prev = document.body.style.overflow;
|
|
1034
1487
|
document.body.style.overflow = "hidden";
|
|
@@ -1036,18 +1489,18 @@ function PickerOverlay({
|
|
|
1036
1489
|
document.body.style.overflow = prev;
|
|
1037
1490
|
};
|
|
1038
1491
|
}, [open]);
|
|
1039
|
-
const handleDragEnter = (0,
|
|
1492
|
+
const handleDragEnter = (0, import_react6.useCallback)((e) => {
|
|
1040
1493
|
e.preventDefault();
|
|
1041
1494
|
setIsDragOver(true);
|
|
1042
1495
|
}, []);
|
|
1043
|
-
const handleDragLeave = (0,
|
|
1496
|
+
const handleDragLeave = (0, import_react6.useCallback)((e) => {
|
|
1044
1497
|
e.preventDefault();
|
|
1045
1498
|
setIsDragOver(false);
|
|
1046
1499
|
}, []);
|
|
1047
|
-
const handleDragOver = (0,
|
|
1500
|
+
const handleDragOver = (0, import_react6.useCallback)((e) => {
|
|
1048
1501
|
e.preventDefault();
|
|
1049
1502
|
}, []);
|
|
1050
|
-
const handleDrop = (0,
|
|
1503
|
+
const handleDrop = (0, import_react6.useCallback)(
|
|
1051
1504
|
(e) => {
|
|
1052
1505
|
e.preventDefault();
|
|
1053
1506
|
setIsDragOver(false);
|
|
@@ -1057,58 +1510,63 @@ function PickerOverlay({
|
|
|
1057
1510
|
},
|
|
1058
1511
|
[addFiles]
|
|
1059
1512
|
);
|
|
1060
|
-
const
|
|
1061
|
-
const handleInputChange = (0, import_react3.useCallback)(
|
|
1062
|
-
(e) => {
|
|
1063
|
-
if (e.target.files && e.target.files.length > 0) {
|
|
1064
|
-
addFiles(e.target.files);
|
|
1065
|
-
e.target.value = "";
|
|
1066
|
-
}
|
|
1067
|
-
},
|
|
1068
|
-
[addFiles]
|
|
1069
|
-
);
|
|
1070
|
-
const handleClose = (0, import_react3.useCallback)(() => {
|
|
1513
|
+
const handleClose = (0, import_react6.useCallback)(() => {
|
|
1071
1514
|
onCancel?.();
|
|
1072
1515
|
onClose();
|
|
1073
1516
|
}, [onCancel, onClose]);
|
|
1074
1517
|
const hasPending = files.some((f) => f.state === "pending");
|
|
1518
|
+
const pendingCount = files.filter((f) => f.state === "pending").length;
|
|
1519
|
+
const doneCount = files.filter((f) => f.state === "done").length;
|
|
1520
|
+
const errorCount = files.filter((f) => f.state === "error").length;
|
|
1075
1521
|
const accept = pickerOptions?.accept?.join(",");
|
|
1522
|
+
const multiple = !pickerOptions?.maxFiles || pickerOptions.maxFiles > 1;
|
|
1076
1523
|
const editingEntry = files.find((f) => f.id === editingId) ?? null;
|
|
1524
|
+
const offerCamera = shouldOfferCamera(pickerOptions);
|
|
1525
|
+
const title = dialogTitle(
|
|
1526
|
+
isUploading,
|
|
1527
|
+
isDone,
|
|
1528
|
+
files.length,
|
|
1529
|
+
doneCount,
|
|
1530
|
+
errorCount,
|
|
1531
|
+
editingId,
|
|
1532
|
+
cameraOpen
|
|
1533
|
+
);
|
|
1077
1534
|
if (!open) return null;
|
|
1078
1535
|
return (
|
|
1079
1536
|
/* Backdrop — clicking outside the panel dismisses the modal */
|
|
1080
|
-
/* @__PURE__ */ (0,
|
|
1537
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1081
1538
|
"div",
|
|
1082
1539
|
{
|
|
1083
1540
|
className: "uploader-backdrop",
|
|
1084
1541
|
"data-testid": "picker-backdrop",
|
|
1085
1542
|
onClick: handleClose,
|
|
1086
|
-
children: /* @__PURE__ */ (0,
|
|
1543
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1087
1544
|
"div",
|
|
1088
1545
|
{
|
|
1089
1546
|
ref: panelRef,
|
|
1090
1547
|
role: "dialog",
|
|
1091
1548
|
"aria-modal": "true",
|
|
1092
1549
|
"aria-labelledby": titleId,
|
|
1550
|
+
"data-drag-over": isDragOver && !editingEntry && files.length === 0 ? "true" : void 0,
|
|
1093
1551
|
className: ["uploader-picker-panel", className ?? ""].filter(Boolean).join(" "),
|
|
1094
1552
|
style,
|
|
1095
1553
|
"data-testid": "picker-panel",
|
|
1096
1554
|
onClick: (e) => e.stopPropagation(),
|
|
1097
1555
|
children: [
|
|
1098
|
-
/* @__PURE__ */ (0,
|
|
1099
|
-
/* @__PURE__ */ (0,
|
|
1100
|
-
/* @__PURE__ */ (0,
|
|
1556
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "uploader-picker-header", children: [
|
|
1557
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { id: titleId, className: "uploader-picker-title", children: title }),
|
|
1558
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1101
1559
|
"button",
|
|
1102
1560
|
{
|
|
1103
1561
|
type: "button",
|
|
1104
1562
|
className: "uploader-btn-close",
|
|
1105
1563
|
"aria-label": "Close picker",
|
|
1106
1564
|
onClick: handleClose,
|
|
1107
|
-
children: "\
|
|
1565
|
+
children: "\u2715"
|
|
1108
1566
|
}
|
|
1109
1567
|
)
|
|
1110
1568
|
] }),
|
|
1111
|
-
editingEntry ? /* @__PURE__ */ (0,
|
|
1569
|
+
editingEntry ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1112
1570
|
ImageEditor,
|
|
1113
1571
|
{
|
|
1114
1572
|
file: editingEntry.file,
|
|
@@ -1118,160 +1576,134 @@ function PickerOverlay({
|
|
|
1118
1576
|
setEditingId(null);
|
|
1119
1577
|
}
|
|
1120
1578
|
}
|
|
1121
|
-
) :
|
|
1122
|
-
/*
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
className: [
|
|
1126
|
-
"uploader-dropzone",
|
|
1127
|
-
isDragOver ? "uploader-drag-over" : ""
|
|
1128
|
-
].filter(Boolean).join(" "),
|
|
1129
|
-
role: "button",
|
|
1130
|
-
tabIndex: 0,
|
|
1131
|
-
"aria-label": "Drop files here or click to browse",
|
|
1132
|
-
onDragEnter: handleDragEnter,
|
|
1133
|
-
onDragLeave: handleDragLeave,
|
|
1134
|
-
onDragOver: handleDragOver,
|
|
1135
|
-
onDrop: handleDrop,
|
|
1136
|
-
onClick: openFileDialog,
|
|
1137
|
-
onKeyDown: (e) => {
|
|
1138
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1139
|
-
e.preventDefault();
|
|
1140
|
-
openFileDialog();
|
|
1141
|
-
}
|
|
1142
|
-
},
|
|
1143
|
-
children: [
|
|
1144
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "uploader-dropzone-icon", "aria-hidden": "true", children: "\u2191" }),
|
|
1145
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "uploader-dropzone-label", children: "Drag & drop files here" }),
|
|
1146
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1147
|
-
"button",
|
|
1148
|
-
{
|
|
1149
|
-
type: "button",
|
|
1150
|
-
className: "uploader-btn-browse",
|
|
1151
|
-
onClick: (e) => {
|
|
1152
|
-
e.stopPropagation();
|
|
1153
|
-
openFileDialog();
|
|
1154
|
-
},
|
|
1155
|
-
children: "Browse files"
|
|
1156
|
-
}
|
|
1157
|
-
)
|
|
1158
|
-
]
|
|
1159
|
-
}
|
|
1160
|
-
),
|
|
1161
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1162
|
-
"input",
|
|
1579
|
+
) : cameraOpen ? (
|
|
1580
|
+
/* ── Camera capture view ───────────────────────────────────────── */
|
|
1581
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1582
|
+
CameraCapture,
|
|
1163
1583
|
{
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
"aria-hidden": "true",
|
|
1170
|
-
tabIndex: -1,
|
|
1171
|
-
onChange: handleInputChange
|
|
1172
|
-
}
|
|
1173
|
-
),
|
|
1174
|
-
files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "uploader-file-list", role: "list", "aria-label": "Selected files", children: files.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: `uploader-file-item uploader-file-${entry.state}`, children: [
|
|
1175
|
-
entry.previewUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1176
|
-
"img",
|
|
1177
|
-
{
|
|
1178
|
-
src: entry.previewUrl,
|
|
1179
|
-
alt: "",
|
|
1180
|
-
"aria-hidden": "true",
|
|
1181
|
-
className: "uploader-file-thumb"
|
|
1584
|
+
facingMode: pickerOptions?.cameraFacingMode,
|
|
1585
|
+
onCancel: () => setCameraOpen(false),
|
|
1586
|
+
onCapture: (file) => {
|
|
1587
|
+
addFiles([file]);
|
|
1588
|
+
setCameraOpen(false);
|
|
1182
1589
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1590
|
+
}
|
|
1591
|
+
)
|
|
1592
|
+
) : isDone ? (
|
|
1593
|
+
/* ── Done state ─────────────────────────────────────────────────── */
|
|
1594
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "uploader-done-body", children: [
|
|
1595
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "uploader-done-subtitle", children: [
|
|
1596
|
+
files.length,
|
|
1597
|
+
" file",
|
|
1598
|
+
files.length !== 1 ? "s" : "",
|
|
1599
|
+
" delivered to your bucket"
|
|
1188
1600
|
] }),
|
|
1189
|
-
|
|
1190
|
-
|
|
1601
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "uploader-file-list uploader-done-list", role: "list", "aria-label": "Uploaded files", children: files.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1602
|
+
FileQueueRow,
|
|
1191
1603
|
{
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
),
|
|
1198
|
-
/* @__PURE__ */ (0,
|
|
1199
|
-
entry.previewUrl && (entry.state === "pending" || entry.state === "error") && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1200
|
-
"button",
|
|
1201
|
-
{
|
|
1202
|
-
type: "button",
|
|
1203
|
-
className: "uploader-btn-edit",
|
|
1204
|
-
"aria-label": `Edit ${entry.file.name}`,
|
|
1205
|
-
onClick: () => setEditingId(entry.id),
|
|
1206
|
-
children: "Edit"
|
|
1207
|
-
}
|
|
1208
|
-
),
|
|
1209
|
-
entry.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1210
|
-
"button",
|
|
1211
|
-
{
|
|
1212
|
-
type: "button",
|
|
1213
|
-
className: "uploader-btn-retry",
|
|
1214
|
-
"aria-label": `Retry uploading ${entry.file.name}`,
|
|
1215
|
-
onClick: () => retryFile(entry.id),
|
|
1216
|
-
children: "Retry"
|
|
1217
|
-
}
|
|
1218
|
-
),
|
|
1219
|
-
entry.state !== "uploading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1220
|
-
"button",
|
|
1221
|
-
{
|
|
1222
|
-
type: "button",
|
|
1223
|
-
className: "uploader-btn-remove",
|
|
1224
|
-
"aria-label": `Remove ${entry.file.name}`,
|
|
1225
|
-
onClick: () => removeFile(entry.id),
|
|
1226
|
-
children: "\xD7"
|
|
1227
|
-
}
|
|
1228
|
-
)
|
|
1229
|
-
] })
|
|
1230
|
-
] }, entry.id)) }),
|
|
1231
|
-
isUploading && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "uploader-aggregate-progress", role: "status", "aria-live": "polite", children: [
|
|
1232
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("progress", { value: progress, max: 100, "aria-label": "Overall upload progress" }),
|
|
1233
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { "aria-live": "polite", children: [
|
|
1234
|
-
progress,
|
|
1235
|
-
"%"
|
|
1236
|
-
] })
|
|
1237
|
-
] }),
|
|
1238
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "uploader-picker-footer", children: [
|
|
1239
|
-
hasPending && !isUploading && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1604
|
+
entry,
|
|
1605
|
+
onRemove: removeFile,
|
|
1606
|
+
onRetry: retryFile
|
|
1607
|
+
},
|
|
1608
|
+
entry.id
|
|
1609
|
+
)) }),
|
|
1610
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1240
1611
|
"button",
|
|
1241
1612
|
{
|
|
1242
1613
|
type: "button",
|
|
1243
|
-
className: "uploader-btn-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1614
|
+
className: "uploader-btn-primary uploader-btn-done-full",
|
|
1615
|
+
onClick: onClose,
|
|
1616
|
+
children: "Done"
|
|
1617
|
+
}
|
|
1618
|
+
)
|
|
1619
|
+
] })
|
|
1620
|
+
) : (
|
|
1621
|
+
/* ── Normal (idle / uploading / error) view ─────────────────────── */
|
|
1622
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1623
|
+
files.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1624
|
+
Dropzone,
|
|
1625
|
+
{
|
|
1626
|
+
onFiles: addFiles,
|
|
1627
|
+
isDragOver,
|
|
1628
|
+
onDragEnter: handleDragEnter,
|
|
1629
|
+
onDragLeave: handleDragLeave,
|
|
1630
|
+
onDragOver: handleDragOver,
|
|
1631
|
+
onDrop: handleDrop,
|
|
1632
|
+
accept,
|
|
1633
|
+
multiple,
|
|
1634
|
+
onCamera: offerCamera ? () => setCameraOpen(true) : void 0,
|
|
1635
|
+
hint: "PNG, JPG, PDF, ZIP \xB7 up to 50 MB"
|
|
1252
1636
|
}
|
|
1253
1637
|
),
|
|
1254
|
-
|
|
1255
|
-
"
|
|
1638
|
+
files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1639
|
+
"ul",
|
|
1256
1640
|
{
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1641
|
+
className: "uploader-file-list",
|
|
1642
|
+
role: "list",
|
|
1643
|
+
"aria-label": "Selected files",
|
|
1644
|
+
"aria-live": "polite",
|
|
1645
|
+
children: files.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1646
|
+
FileQueueRow,
|
|
1647
|
+
{
|
|
1648
|
+
entry,
|
|
1649
|
+
onRemove: removeFile,
|
|
1650
|
+
onRetry: retryFile,
|
|
1651
|
+
onEdit: setEditingId
|
|
1652
|
+
},
|
|
1653
|
+
entry.id
|
|
1654
|
+
))
|
|
1261
1655
|
}
|
|
1262
1656
|
),
|
|
1263
|
-
|
|
1264
|
-
"
|
|
1657
|
+
isUploading && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1658
|
+
"div",
|
|
1265
1659
|
{
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1660
|
+
className: "uploader-aggregate-progress",
|
|
1661
|
+
role: "status",
|
|
1662
|
+
"aria-live": "polite",
|
|
1663
|
+
children: [
|
|
1664
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "uploader-aggregate-progress-label", children: "Total progress" }),
|
|
1665
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1666
|
+
ProgressBar,
|
|
1667
|
+
{
|
|
1668
|
+
value: progress,
|
|
1669
|
+
label: "Overall upload progress",
|
|
1670
|
+
size: "lg"
|
|
1671
|
+
}
|
|
1672
|
+
),
|
|
1673
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "uploader-aggregate-progress-pct", children: [
|
|
1674
|
+
progress,
|
|
1675
|
+
"%"
|
|
1676
|
+
] })
|
|
1677
|
+
]
|
|
1271
1678
|
}
|
|
1272
|
-
)
|
|
1679
|
+
),
|
|
1680
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "uploader-picker-footer", children: [
|
|
1681
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "uploader-picker-footer-esc", children: "Esc to close" }),
|
|
1682
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "uploader-picker-footer-actions", children: [
|
|
1683
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1684
|
+
"button",
|
|
1685
|
+
{
|
|
1686
|
+
type: "button",
|
|
1687
|
+
className: "uploader-btn-secondary uploader-btn-cancel",
|
|
1688
|
+
disabled: isUploading,
|
|
1689
|
+
onClick: handleClose,
|
|
1690
|
+
children: "Cancel"
|
|
1691
|
+
}
|
|
1692
|
+
),
|
|
1693
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1694
|
+
"button",
|
|
1695
|
+
{
|
|
1696
|
+
type: "button",
|
|
1697
|
+
className: "uploader-btn-primary uploader-btn-upload",
|
|
1698
|
+
disabled: isUploading || !hasPending,
|
|
1699
|
+
onClick: () => void upload(),
|
|
1700
|
+
children: isUploading ? "Uploading\u2026" : `Upload${pendingCount > 0 ? ` ${pendingCount} file${pendingCount !== 1 ? "s" : ""}` : ""}`
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
] })
|
|
1704
|
+
] })
|
|
1273
1705
|
] })
|
|
1274
|
-
|
|
1706
|
+
)
|
|
1275
1707
|
]
|
|
1276
1708
|
}
|
|
1277
1709
|
)
|
|
@@ -1281,8 +1713,8 @@ function PickerOverlay({
|
|
|
1281
1713
|
}
|
|
1282
1714
|
|
|
1283
1715
|
// src/react/DropPane.tsx
|
|
1284
|
-
var
|
|
1285
|
-
var
|
|
1716
|
+
var import_react7 = require("react");
|
|
1717
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1286
1718
|
function DropPane({
|
|
1287
1719
|
apikey,
|
|
1288
1720
|
apiUrl,
|
|
@@ -1292,12 +1724,12 @@ function DropPane({
|
|
|
1292
1724
|
onFileUploadFinished,
|
|
1293
1725
|
onFileUploadFailed,
|
|
1294
1726
|
onCancel,
|
|
1727
|
+
hint = "or click to browse \xB7 max 50 MB",
|
|
1295
1728
|
className,
|
|
1296
1729
|
style
|
|
1297
1730
|
}) {
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
const [isDragOver, setIsDragOver] = (0, import_react4.useState)(false);
|
|
1731
|
+
const [isDragOver, setIsDragOver] = (0, import_react7.useState)(false);
|
|
1732
|
+
const [cameraOpen, setCameraOpen] = (0, import_react7.useState)(false);
|
|
1301
1733
|
const { files, addFiles, removeFile, retryFile, upload, progress, isUploading, isDone } = usePicker({
|
|
1302
1734
|
apikey,
|
|
1303
1735
|
apiUrl,
|
|
@@ -1307,21 +1739,21 @@ function DropPane({
|
|
|
1307
1739
|
onFileUploadFinished,
|
|
1308
1740
|
onFileUploadFailed
|
|
1309
1741
|
});
|
|
1310
|
-
const handleDragEnter = (0,
|
|
1742
|
+
const handleDragEnter = (0, import_react7.useCallback)((e) => {
|
|
1311
1743
|
e.preventDefault();
|
|
1312
1744
|
e.stopPropagation();
|
|
1313
1745
|
setIsDragOver(true);
|
|
1314
1746
|
}, []);
|
|
1315
|
-
const handleDragLeave = (0,
|
|
1747
|
+
const handleDragLeave = (0, import_react7.useCallback)((e) => {
|
|
1316
1748
|
e.preventDefault();
|
|
1317
1749
|
e.stopPropagation();
|
|
1318
1750
|
setIsDragOver(false);
|
|
1319
1751
|
}, []);
|
|
1320
|
-
const handleDragOver = (0,
|
|
1752
|
+
const handleDragOver = (0, import_react7.useCallback)((e) => {
|
|
1321
1753
|
e.preventDefault();
|
|
1322
1754
|
e.stopPropagation();
|
|
1323
1755
|
}, []);
|
|
1324
|
-
const handleDrop = (0,
|
|
1756
|
+
const handleDrop = (0, import_react7.useCallback)(
|
|
1325
1757
|
(e) => {
|
|
1326
1758
|
e.preventDefault();
|
|
1327
1759
|
e.stopPropagation();
|
|
@@ -1332,141 +1764,126 @@ function DropPane({
|
|
|
1332
1764
|
},
|
|
1333
1765
|
[addFiles]
|
|
1334
1766
|
);
|
|
1335
|
-
const handleInputChange = (0, import_react4.useCallback)(
|
|
1336
|
-
(e) => {
|
|
1337
|
-
if (e.target.files && e.target.files.length > 0) {
|
|
1338
|
-
addFiles(e.target.files);
|
|
1339
|
-
e.target.value = "";
|
|
1340
|
-
}
|
|
1341
|
-
},
|
|
1342
|
-
[addFiles]
|
|
1343
|
-
);
|
|
1344
|
-
const openFileDialog = (0, import_react4.useCallback)(() => {
|
|
1345
|
-
inputRef.current?.click();
|
|
1346
|
-
}, []);
|
|
1347
1767
|
const hasPending = files.some((f) => f.state === "pending");
|
|
1768
|
+
const pendingCount = files.filter((f) => f.state === "pending").length;
|
|
1348
1769
|
const accept = pickerOptions?.accept?.join(",");
|
|
1349
|
-
|
|
1770
|
+
const multiple = !pickerOptions?.maxFiles || pickerOptions.maxFiles > 1;
|
|
1771
|
+
const offerCamera = shouldOfferCamera(pickerOptions);
|
|
1772
|
+
if (cameraOpen) {
|
|
1773
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1774
|
+
"div",
|
|
1775
|
+
{
|
|
1776
|
+
className: ["uploader-drop-pane", className ?? ""].filter(Boolean).join(" "),
|
|
1777
|
+
"data-testid": "drop-pane",
|
|
1778
|
+
style,
|
|
1779
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1780
|
+
CameraCapture,
|
|
1781
|
+
{
|
|
1782
|
+
facingMode: pickerOptions?.cameraFacingMode,
|
|
1783
|
+
onCancel: () => setCameraOpen(false),
|
|
1784
|
+
onCapture: (file) => {
|
|
1785
|
+
addFiles([file]);
|
|
1786
|
+
setCameraOpen(false);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
)
|
|
1790
|
+
}
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1350
1794
|
"div",
|
|
1351
1795
|
{
|
|
1352
|
-
className: ["uploader-drop-pane",
|
|
1353
|
-
|
|
1796
|
+
className: ["uploader-drop-pane", className ?? ""].filter(Boolean).join(" "),
|
|
1797
|
+
"data-drag-over": isDragOver ? "true" : void 0,
|
|
1354
1798
|
"data-testid": "drop-pane",
|
|
1799
|
+
style,
|
|
1355
1800
|
children: [
|
|
1356
|
-
/* @__PURE__ */ (0,
|
|
1357
|
-
|
|
1801
|
+
files.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1802
|
+
Dropzone,
|
|
1358
1803
|
{
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
tabIndex: 0,
|
|
1362
|
-
"aria-label": "Drop files here or click to browse",
|
|
1804
|
+
onFiles: addFiles,
|
|
1805
|
+
isDragOver,
|
|
1363
1806
|
onDragEnter: handleDragEnter,
|
|
1364
1807
|
onDragLeave: handleDragLeave,
|
|
1365
1808
|
onDragOver: handleDragOver,
|
|
1366
1809
|
onDrop: handleDrop,
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
openFileDialog();
|
|
1372
|
-
}
|
|
1373
|
-
},
|
|
1374
|
-
children: [
|
|
1375
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-dropzone-icon", "aria-hidden": "true", children: "\u2191" }),
|
|
1376
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "uploader-dropzone-label", children: [
|
|
1377
|
-
"Drag & drop files here or",
|
|
1378
|
-
" ",
|
|
1379
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: inputId, className: "uploader-browse-link", children: "browse" })
|
|
1380
|
-
] })
|
|
1381
|
-
]
|
|
1810
|
+
accept,
|
|
1811
|
+
multiple,
|
|
1812
|
+
onCamera: offerCamera ? () => setCameraOpen(true) : void 0,
|
|
1813
|
+
hint
|
|
1382
1814
|
}
|
|
1383
1815
|
),
|
|
1384
|
-
/* @__PURE__ */ (0,
|
|
1385
|
-
"
|
|
1816
|
+
files.length > 0 && isDragOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1817
|
+
"div",
|
|
1386
1818
|
{
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
className: "uploader-file-input",
|
|
1819
|
+
className: "uploader-drop-pane-drag-overlay",
|
|
1820
|
+
onDragEnter: handleDragEnter,
|
|
1821
|
+
onDragLeave: handleDragLeave,
|
|
1822
|
+
onDragOver: handleDragOver,
|
|
1823
|
+
onDrop: handleDrop,
|
|
1393
1824
|
"aria-hidden": "true",
|
|
1394
|
-
|
|
1395
|
-
onChange: handleInputChange
|
|
1825
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "uploader-drop-pane-drag-label", children: "Drop to add files" })
|
|
1396
1826
|
}
|
|
1397
1827
|
),
|
|
1398
|
-
files.length > 0 && /* @__PURE__ */ (0,
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
),
|
|
1408
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-file-info", children: [
|
|
1409
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-file-name", children: entry.file.name }),
|
|
1410
|
-
entry.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-file-error", role: "alert", children: entry.error ?? "Upload failed" }),
|
|
1411
|
-
entry.state === "done" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "uploader-file-done", children: "Uploaded" })
|
|
1412
|
-
] }),
|
|
1413
|
-
entry.state === "uploading" && entry.progress != null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1414
|
-
"progress",
|
|
1415
|
-
{
|
|
1416
|
-
className: "uploader-file-progress",
|
|
1417
|
-
value: entry.progress,
|
|
1418
|
-
max: 100,
|
|
1419
|
-
"aria-label": `${entry.file.name} upload progress`
|
|
1420
|
-
}
|
|
1421
|
-
),
|
|
1422
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-file-actions", children: [
|
|
1423
|
-
entry.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1424
|
-
"button",
|
|
1425
|
-
{
|
|
1426
|
-
type: "button",
|
|
1427
|
-
className: "uploader-btn-retry",
|
|
1428
|
-
"aria-label": `Retry uploading ${entry.file.name}`,
|
|
1429
|
-
onClick: () => retryFile(entry.id),
|
|
1430
|
-
children: "Retry"
|
|
1431
|
-
}
|
|
1432
|
-
),
|
|
1433
|
-
entry.state !== "uploading" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1434
|
-
"button",
|
|
1828
|
+
files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1829
|
+
"ul",
|
|
1830
|
+
{
|
|
1831
|
+
className: "uploader-file-list uploader-drop-pane-file-list",
|
|
1832
|
+
role: "list",
|
|
1833
|
+
"aria-label": "Selected files",
|
|
1834
|
+
"aria-live": "polite",
|
|
1835
|
+
children: files.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1836
|
+
FileQueueRow,
|
|
1435
1837
|
{
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
"
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1838
|
+
entry,
|
|
1839
|
+
onRemove: removeFile,
|
|
1840
|
+
onRetry: retryFile
|
|
1841
|
+
},
|
|
1842
|
+
entry.id
|
|
1843
|
+
))
|
|
1844
|
+
}
|
|
1845
|
+
),
|
|
1846
|
+
isUploading && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1847
|
+
"div",
|
|
1848
|
+
{
|
|
1849
|
+
className: "uploader-aggregate-progress",
|
|
1850
|
+
role: "status",
|
|
1851
|
+
"aria-live": "polite",
|
|
1852
|
+
children: [
|
|
1853
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1854
|
+
ProgressBar,
|
|
1855
|
+
{
|
|
1856
|
+
value: progress,
|
|
1857
|
+
label: "Overall upload progress",
|
|
1858
|
+
size: "lg"
|
|
1859
|
+
}
|
|
1860
|
+
),
|
|
1861
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "uploader-aggregate-progress-pct", children: [
|
|
1862
|
+
progress,
|
|
1863
|
+
"%"
|
|
1864
|
+
] })
|
|
1865
|
+
]
|
|
1866
|
+
}
|
|
1867
|
+
),
|
|
1868
|
+
files.length > 0 && !isDone && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "uploader-drop-pane-actions", children: [
|
|
1869
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1454
1870
|
"button",
|
|
1455
1871
|
{
|
|
1456
1872
|
type: "button",
|
|
1457
|
-
className: "uploader-btn-
|
|
1873
|
+
className: "uploader-btn-secondary uploader-btn-cancel",
|
|
1458
1874
|
disabled: isUploading,
|
|
1459
|
-
onClick:
|
|
1460
|
-
children: "
|
|
1875
|
+
onClick: onCancel,
|
|
1876
|
+
children: "Cancel"
|
|
1461
1877
|
}
|
|
1462
1878
|
),
|
|
1463
|
-
|
|
1879
|
+
hasPending && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1464
1880
|
"button",
|
|
1465
1881
|
{
|
|
1466
1882
|
type: "button",
|
|
1467
|
-
className: "uploader-btn-
|
|
1468
|
-
|
|
1469
|
-
|
|
1883
|
+
className: "uploader-btn-primary uploader-btn-upload",
|
|
1884
|
+
disabled: isUploading,
|
|
1885
|
+
onClick: () => void upload(),
|
|
1886
|
+
children: isUploading ? "Uploading\u2026" : `Upload${pendingCount > 0 ? ` ${pendingCount} file${pendingCount !== 1 ? "s" : ""}` : ""}`
|
|
1470
1887
|
}
|
|
1471
1888
|
)
|
|
1472
1889
|
] })
|
|
@@ -1476,6 +1893,7 @@ function DropPane({
|
|
|
1476
1893
|
}
|
|
1477
1894
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1478
1895
|
0 && (module.exports = {
|
|
1896
|
+
CameraCapture,
|
|
1479
1897
|
DropPane,
|
|
1480
1898
|
ImageEditor,
|
|
1481
1899
|
PickerOverlay,
|
|
@@ -1484,10 +1902,12 @@ function DropPane({
|
|
|
1484
1902
|
editImage,
|
|
1485
1903
|
flip,
|
|
1486
1904
|
flop,
|
|
1905
|
+
isCameraSupported,
|
|
1487
1906
|
output,
|
|
1488
1907
|
quality,
|
|
1489
1908
|
resize,
|
|
1490
1909
|
rotate,
|
|
1910
|
+
shouldOfferCamera,
|
|
1491
1911
|
transformUrl,
|
|
1492
1912
|
usePicker
|
|
1493
1913
|
});
|