@ibanzajoe/uploader 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/index.cjs +93 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +91 -5
- package/dist/index.js.map +1 -1
- package/dist/styles.css +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -281,6 +281,11 @@ flows through the exact same pipeline as a dropped or browsed file — it lands
|
|
|
281
281
|
the queue with a preview, can be cropped/rotated in the in-picker editor, and is
|
|
282
282
|
then uploaded normally.
|
|
283
283
|
|
|
284
|
+
**Front or rear camera.** On phones and tablets the picker opens the **rear**
|
|
285
|
+
camera; on desktops it opens the front one. Where the device exposes more than
|
|
286
|
+
one camera, a flip control in the corner of the viewfinder switches between them,
|
|
287
|
+
so the default is only the starting side. Set `cameraFacingMode` to pin it.
|
|
288
|
+
|
|
284
289
|
```tsx
|
|
285
290
|
<PickerOverlay
|
|
286
291
|
apikey="pk_…"
|
|
@@ -290,7 +295,8 @@ then uploaded normally.
|
|
|
290
295
|
accept: ['image/*'],
|
|
291
296
|
// Sources offered to the user. Omit to offer everything supported.
|
|
292
297
|
fromSources: ['local_file_system', 'camera'],
|
|
293
|
-
//
|
|
298
|
+
// Omit for the per-device default (rear on mobile, front on desktop).
|
|
299
|
+
// 'user' = front/selfie (mirrored) · 'environment' = rear camera
|
|
294
300
|
cameraFacingMode: 'environment',
|
|
295
301
|
}}
|
|
296
302
|
/>
|
|
@@ -301,8 +307,13 @@ Notes:
|
|
|
301
307
|
permission. Permission/no-camera errors are surfaced inline with a retry.
|
|
302
308
|
- Pass `fromSources: ['local_file_system']` to hide the camera even where it is
|
|
303
309
|
supported; omit `fromSources` to offer it by default.
|
|
310
|
+
- The flip control appears only when `enumerateDevices()` reports two or more
|
|
311
|
+
video inputs (checked after permission is granted, when the list is accurate).
|
|
312
|
+
- The front camera is mirrored in both the preview and the captured still; the
|
|
313
|
+
rear camera is not.
|
|
304
314
|
- The `<CameraCapture>` component and the `isCameraSupported()` /
|
|
305
|
-
`shouldOfferCamera()`
|
|
315
|
+
`shouldOfferCamera()` / `isMobileDevice()` / `defaultCameraFacingMode()`
|
|
316
|
+
helpers are exported for fully custom pickers.
|
|
306
317
|
|
|
307
318
|
## Signed policies
|
|
308
319
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,10 +26,12 @@ __export(src_exports, {
|
|
|
26
26
|
PickerOverlay: () => PickerOverlay,
|
|
27
27
|
UploaderClient: () => UploaderClient,
|
|
28
28
|
crop: () => crop,
|
|
29
|
+
defaultCameraFacingMode: () => defaultCameraFacingMode,
|
|
29
30
|
editImage: () => editImage,
|
|
30
31
|
flip: () => flip,
|
|
31
32
|
flop: () => flop,
|
|
32
33
|
isCameraSupported: () => isCameraSupported,
|
|
34
|
+
isMobileDevice: () => isMobileDevice,
|
|
33
35
|
output: () => output,
|
|
34
36
|
quality: () => quality,
|
|
35
37
|
resize: () => resize,
|
|
@@ -1275,6 +1277,19 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
|
1275
1277
|
function isCameraSupported() {
|
|
1276
1278
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined";
|
|
1277
1279
|
}
|
|
1280
|
+
function isMobileDevice() {
|
|
1281
|
+
if (typeof navigator === "undefined") return false;
|
|
1282
|
+
const uaData = navigator.userAgentData;
|
|
1283
|
+
if (typeof uaData?.mobile === "boolean") return uaData.mobile;
|
|
1284
|
+
const ua = navigator.userAgent ?? "";
|
|
1285
|
+
if (/Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|Mobile/i.test(ua)) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
return /Macintosh/.test(ua) && (navigator.maxTouchPoints ?? 0) > 1;
|
|
1289
|
+
}
|
|
1290
|
+
function defaultCameraFacingMode() {
|
|
1291
|
+
return isMobileDevice() ? "environment" : "user";
|
|
1292
|
+
}
|
|
1278
1293
|
var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
|
|
1279
1294
|
function acceptsImages(accept) {
|
|
1280
1295
|
if (!accept || accept.length === 0) return true;
|
|
@@ -1315,7 +1330,7 @@ function describeCameraError(err) {
|
|
|
1315
1330
|
function CameraCapture({
|
|
1316
1331
|
onCapture,
|
|
1317
1332
|
onCancel,
|
|
1318
|
-
facingMode
|
|
1333
|
+
facingMode,
|
|
1319
1334
|
outputType = "image/jpeg",
|
|
1320
1335
|
quality: quality2 = 0.92,
|
|
1321
1336
|
fileNamePrefix = "camera-photo",
|
|
@@ -1328,12 +1343,32 @@ function CameraCapture({
|
|
|
1328
1343
|
const [status, setStatus] = (0, import_react4.useState)("initializing");
|
|
1329
1344
|
const [error, setError] = (0, import_react4.useState)(null);
|
|
1330
1345
|
const [stillUrl, setStillUrl] = (0, import_react4.useState)("");
|
|
1331
|
-
const
|
|
1346
|
+
const [facing, setFacing] = (0, import_react4.useState)(
|
|
1347
|
+
() => facingMode ?? defaultCameraFacingMode()
|
|
1348
|
+
);
|
|
1349
|
+
const [canFlip, setCanFlip] = (0, import_react4.useState)(false);
|
|
1350
|
+
const mirror = facing === "user";
|
|
1351
|
+
(0, import_react4.useEffect)(() => {
|
|
1352
|
+
if (facingMode) setFacing(facingMode);
|
|
1353
|
+
}, [facingMode]);
|
|
1332
1354
|
const stopStream = (0, import_react4.useCallback)(() => {
|
|
1333
1355
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1334
1356
|
streamRef.current = null;
|
|
1335
1357
|
if (videoRef.current) videoRef.current.srcObject = null;
|
|
1336
1358
|
}, []);
|
|
1359
|
+
const probeCameraCount = (0, import_react4.useCallback)(async () => {
|
|
1360
|
+
const media = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
|
|
1361
|
+
if (!media || typeof media.enumerateDevices !== "function") {
|
|
1362
|
+
setCanFlip(isMobileDevice());
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const devices = await media.enumerateDevices();
|
|
1367
|
+
setCanFlip(devices.filter((d) => d.kind === "videoinput").length > 1);
|
|
1368
|
+
} catch {
|
|
1369
|
+
setCanFlip(isMobileDevice());
|
|
1370
|
+
}
|
|
1371
|
+
}, []);
|
|
1337
1372
|
const startStream = (0, import_react4.useCallback)(async () => {
|
|
1338
1373
|
setError(null);
|
|
1339
1374
|
setStatus("initializing");
|
|
@@ -1342,9 +1377,12 @@ function CameraCapture({
|
|
|
1342
1377
|
setStatus("error");
|
|
1343
1378
|
return;
|
|
1344
1379
|
}
|
|
1380
|
+
stopStream();
|
|
1345
1381
|
try {
|
|
1346
1382
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1347
|
-
|
|
1383
|
+
// Non-exact: a device with no camera on the requested side falls back to
|
|
1384
|
+
// the one it has rather than throwing OverconstrainedError.
|
|
1385
|
+
video: { facingMode: facing },
|
|
1348
1386
|
audio: false
|
|
1349
1387
|
});
|
|
1350
1388
|
if (videoRef.current == null) {
|
|
@@ -1354,11 +1392,12 @@ function CameraCapture({
|
|
|
1354
1392
|
streamRef.current = stream;
|
|
1355
1393
|
videoRef.current.srcObject = stream;
|
|
1356
1394
|
setStatus("live");
|
|
1395
|
+
void probeCameraCount();
|
|
1357
1396
|
} catch (err) {
|
|
1358
1397
|
setError(describeCameraError(err));
|
|
1359
1398
|
setStatus("error");
|
|
1360
1399
|
}
|
|
1361
|
-
}, [
|
|
1400
|
+
}, [facing, probeCameraCount, stopStream]);
|
|
1362
1401
|
(0, import_react4.useEffect)(() => {
|
|
1363
1402
|
void startStream();
|
|
1364
1403
|
return () => {
|
|
@@ -1406,6 +1445,9 @@ function CameraCapture({
|
|
|
1406
1445
|
quality2
|
|
1407
1446
|
);
|
|
1408
1447
|
}, [mirror, outputType, quality2, stopStream]);
|
|
1448
|
+
const flipCamera = (0, import_react4.useCallback)(() => {
|
|
1449
|
+
setFacing((current) => current === "user" ? "environment" : "user");
|
|
1450
|
+
}, []);
|
|
1409
1451
|
const retake = (0, import_react4.useCallback)(() => {
|
|
1410
1452
|
blobRef.current = null;
|
|
1411
1453
|
setStillUrl("");
|
|
@@ -1424,6 +1466,7 @@ function CameraCapture({
|
|
|
1424
1466
|
className: ["uploader-camera", className ?? ""].filter(Boolean).join(" "),
|
|
1425
1467
|
style,
|
|
1426
1468
|
"data-testid": "camera-capture",
|
|
1469
|
+
"data-facing": facing,
|
|
1427
1470
|
children: [
|
|
1428
1471
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-camera-stage", children: [
|
|
1429
1472
|
status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-error", role: "alert", children: error }) : status === "captured" ? (
|
|
@@ -1440,7 +1483,50 @@ function CameraCapture({
|
|
|
1440
1483
|
"data-mirror": mirror ? "true" : void 0
|
|
1441
1484
|
}
|
|
1442
1485
|
),
|
|
1443
|
-
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" })
|
|
1486
|
+
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" }),
|
|
1487
|
+
canFlip && (status === "live" || status === "initializing") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1488
|
+
"button",
|
|
1489
|
+
{
|
|
1490
|
+
type: "button",
|
|
1491
|
+
className: "uploader-camera-flip",
|
|
1492
|
+
"aria-label": facing === "user" ? "Switch to rear camera" : "Switch to front camera",
|
|
1493
|
+
disabled: status !== "live",
|
|
1494
|
+
onClick: flipCamera,
|
|
1495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true", focusable: "false", children: [
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1497
|
+
"path",
|
|
1498
|
+
{
|
|
1499
|
+
d: "M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2l1-1.7a1 1 0 0 1 .87-.5h4.86a1 1 0 0 1 .86.5l1 1.7h1.21A2.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",
|
|
1500
|
+
fill: "none",
|
|
1501
|
+
stroke: "currentColor",
|
|
1502
|
+
strokeWidth: "1.6",
|
|
1503
|
+
strokeLinejoin: "round"
|
|
1504
|
+
}
|
|
1505
|
+
),
|
|
1506
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1507
|
+
"path",
|
|
1508
|
+
{
|
|
1509
|
+
d: "M9.4 12.6a2.6 2.6 0 0 0 4.45 1.6M14.6 11.4a2.6 2.6 0 0 0-4.45-1.6",
|
|
1510
|
+
fill: "none",
|
|
1511
|
+
stroke: "currentColor",
|
|
1512
|
+
strokeWidth: "1.6",
|
|
1513
|
+
strokeLinecap: "round"
|
|
1514
|
+
}
|
|
1515
|
+
),
|
|
1516
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1517
|
+
"path",
|
|
1518
|
+
{
|
|
1519
|
+
d: "m9.4 10.2-.1 2.1 2.1-.6M14.6 13.8l.1-2.1-2.1.6",
|
|
1520
|
+
fill: "none",
|
|
1521
|
+
stroke: "currentColor",
|
|
1522
|
+
strokeWidth: "1.6",
|
|
1523
|
+
strokeLinecap: "round",
|
|
1524
|
+
strokeLinejoin: "round"
|
|
1525
|
+
}
|
|
1526
|
+
)
|
|
1527
|
+
] })
|
|
1528
|
+
}
|
|
1529
|
+
)
|
|
1444
1530
|
] }),
|
|
1445
1531
|
status === "live" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "uploader-camera-hint", children: "Frame your shot, then tap the shutter to capture." }),
|
|
1446
1532
|
/* @__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: [
|
|
@@ -2132,10 +2218,12 @@ function DropPane({
|
|
|
2132
2218
|
PickerOverlay,
|
|
2133
2219
|
UploaderClient,
|
|
2134
2220
|
crop,
|
|
2221
|
+
defaultCameraFacingMode,
|
|
2135
2222
|
editImage,
|
|
2136
2223
|
flip,
|
|
2137
2224
|
flop,
|
|
2138
2225
|
isCameraSupported,
|
|
2226
|
+
isMobileDevice,
|
|
2139
2227
|
output,
|
|
2140
2228
|
quality,
|
|
2141
2229
|
resize,
|