@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/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 import_react3 = require("react");
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/PickerOverlay.tsx
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, import_react3.useId)();
980
- const inputRef = (0, import_react3.useRef)(null);
981
- const panelRef = (0, import_react3.useRef)(null);
982
- const [isDragOver, setIsDragOver] = (0, import_react3.useState)(false);
983
- const [editingId, setEditingId] = (0, import_react3.useState)(null);
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, import_react3.useEffect)(() => {
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, import_react3.useEffect)(() => {
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, import_react3.useEffect)(() => {
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, import_react3.useCallback)((e) => {
1492
+ const handleDragEnter = (0, import_react6.useCallback)((e) => {
1040
1493
  e.preventDefault();
1041
1494
  setIsDragOver(true);
1042
1495
  }, []);
1043
- const handleDragLeave = (0, import_react3.useCallback)((e) => {
1496
+ const handleDragLeave = (0, import_react6.useCallback)((e) => {
1044
1497
  e.preventDefault();
1045
1498
  setIsDragOver(false);
1046
1499
  }, []);
1047
- const handleDragOver = (0, import_react3.useCallback)((e) => {
1500
+ const handleDragOver = (0, import_react6.useCallback)((e) => {
1048
1501
  e.preventDefault();
1049
1502
  }, []);
1050
- const handleDrop = (0, import_react3.useCallback)(
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 openFileDialog = (0, import_react3.useCallback)(() => inputRef.current?.click(), []);
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, import_jsx_runtime2.jsx)(
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, import_jsx_runtime2.jsxs)(
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, import_jsx_runtime2.jsxs)("div", { className: "uploader-picker-header", children: [
1099
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { id: titleId, className: "uploader-picker-title", children: editingEntry ? "Edit Image" : "Upload Files" }),
1100
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
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: "\xD7"
1565
+ children: "\u2715"
1108
1566
  }
1109
1567
  )
1110
1568
  ] }),
1111
- editingEntry ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
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
- ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1122
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1123
- "div",
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
- ref: inputRef,
1165
- type: "file",
1166
- multiple: !pickerOptions?.maxFiles || pickerOptions.maxFiles > 1,
1167
- accept,
1168
- className: "uploader-file-input",
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
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "uploader-file-info", children: [
1185
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "uploader-file-name", children: entry.file.name }),
1186
- entry.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "uploader-file-error", role: "alert", children: entry.error ?? "Upload failed" }),
1187
- entry.state === "done" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "uploader-file-done", children: "Uploaded" })
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
- entry.state === "uploading" && entry.progress != null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1190
- "progress",
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
- className: "uploader-file-progress",
1193
- value: entry.progress,
1194
- max: 100,
1195
- "aria-label": `${entry.file.name} upload progress`
1196
- }
1197
- ),
1198
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "uploader-file-actions", children: [
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-upload",
1244
- disabled: isUploading || !hasPending,
1245
- onClick: () => void upload(),
1246
- children: [
1247
- "Upload ",
1248
- files.filter((f) => f.state === "pending").length,
1249
- " file",
1250
- files.filter((f) => f.state === "pending").length !== 1 ? "s" : ""
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
- isDone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1255
- "button",
1638
+ files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1639
+ "ul",
1256
1640
  {
1257
- type: "button",
1258
- className: "uploader-btn-done",
1259
- onClick: onClose,
1260
- children: "Done"
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
- !isDone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1264
- "button",
1657
+ isUploading && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1658
+ "div",
1265
1659
  {
1266
- type: "button",
1267
- className: "uploader-btn-cancel",
1268
- disabled: isUploading,
1269
- onClick: handleClose,
1270
- children: "Cancel"
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 import_react4 = require("react");
1285
- var import_jsx_runtime3 = require("react/jsx-runtime");
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 inputId = (0, import_react4.useId)();
1299
- const inputRef = (0, import_react4.useRef)(null);
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, import_react4.useCallback)((e) => {
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, import_react4.useCallback)((e) => {
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, import_react4.useCallback)((e) => {
1752
+ const handleDragOver = (0, import_react7.useCallback)((e) => {
1321
1753
  e.preventDefault();
1322
1754
  e.stopPropagation();
1323
1755
  }, []);
1324
- const handleDrop = (0, import_react4.useCallback)(
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
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
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", isDragOver ? "uploader-drag-over" : "", className ?? ""].filter(Boolean).join(" "),
1353
- style,
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, import_jsx_runtime3.jsxs)(
1357
- "div",
1801
+ files.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1802
+ Dropzone,
1358
1803
  {
1359
- className: "uploader-dropzone",
1360
- role: "button",
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
- onClick: openFileDialog,
1368
- onKeyDown: (e) => {
1369
- if (e.key === "Enter" || e.key === " ") {
1370
- e.preventDefault();
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, import_jsx_runtime3.jsx)(
1385
- "input",
1816
+ files.length > 0 && isDragOver && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1817
+ "div",
1386
1818
  {
1387
- id: inputId,
1388
- ref: inputRef,
1389
- type: "file",
1390
- multiple: !pickerOptions?.maxFiles || pickerOptions.maxFiles > 1,
1391
- accept,
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
- tabIndex: -1,
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, import_jsx_runtime3.jsx)("ul", { className: "uploader-file-list", role: "list", "aria-label": "Selected files", children: files.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: `uploader-file-item uploader-file-${entry.state}`, children: [
1399
- entry.previewUrl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1400
- "img",
1401
- {
1402
- src: entry.previewUrl,
1403
- alt: "",
1404
- "aria-hidden": "true",
1405
- className: "uploader-file-thumb"
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
- type: "button",
1437
- className: "uploader-btn-remove",
1438
- "aria-label": `Remove ${entry.file.name}`,
1439
- onClick: () => removeFile(entry.id),
1440
- children: "\xD7"
1441
- }
1442
- )
1443
- ] })
1444
- ] }, entry.id)) }),
1445
- isUploading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-aggregate-progress", role: "status", "aria-live": "polite", children: [
1446
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("progress", { value: progress, max: 100, "aria-label": "Overall upload progress" }),
1447
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1448
- progress,
1449
- "%"
1450
- ] })
1451
- ] }),
1452
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-actions", children: [
1453
- hasPending && !isUploading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
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-upload",
1873
+ className: "uploader-btn-secondary uploader-btn-cancel",
1458
1874
  disabled: isUploading,
1459
- onClick: () => void upload(),
1460
- children: "Upload"
1875
+ onClick: onCancel,
1876
+ children: "Cancel"
1461
1877
  }
1462
1878
  ),
1463
- !isDone && !isUploading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1879
+ hasPending && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1464
1880
  "button",
1465
1881
  {
1466
1882
  type: "button",
1467
- className: "uploader-btn-cancel",
1468
- onClick: onCancel,
1469
- children: "Cancel"
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
  });