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