@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/dist/index.d.cts CHANGED
@@ -144,9 +144,14 @@ type PickerOptions = {
144
144
  */
145
145
  fromSources?: Array<'local_file_system' | 'camera'>;
146
146
  /**
147
- * Preferred camera when the 'camera' source is used.
148
- * 'user' — front / selfie camera (mirrored). Default.
147
+ * Which camera opens first when the 'camera' source is used.
148
+ * 'user' — front / selfie camera (mirrored).
149
149
  * 'environment' — rear camera.
150
+ *
151
+ * Omitted (recommended) → chosen per device: **rear on phones and tablets**,
152
+ * front on desktops. The user can flip between them in the camera view
153
+ * whenever the device exposes more than one camera, so this only sets the
154
+ * starting side.
150
155
  */
151
156
  cameraFacingMode?: 'user' | 'environment';
152
157
  };
@@ -302,6 +307,12 @@ declare function DropPane({ apikey, apiUrl, security, deliveryProtection, allowe
302
307
  * unmount, on capture (to release the camera indicator while reviewing), and on
303
308
  * cancel.
304
309
  *
310
+ * Which camera opens first is device-dependent: phones and tablets default to
311
+ * the REAR camera (people photograph the world, not themselves, from an upload
312
+ * form), desktops to the only camera they have. A flip control appears whenever
313
+ * the device actually exposes more than one camera, so the default is a starting
314
+ * point rather than a constraint. `facingMode` overrides the default outright.
315
+ *
305
316
  * All colors resolve from --uploader-* CSS variables — no hard-coded brand
306
317
  * colors. Used by PickerOverlay and DropPane.
307
318
  *
@@ -310,6 +321,24 @@ declare function DropPane({ apikey, apiUrl, security, deliveryProtection, allowe
310
321
 
311
322
  /** True when the current environment can open a camera stream. */
312
323
  declare function isCameraSupported(): boolean;
324
+ /** Which physical camera to open. Mirrors the getUserMedia `facingMode` values. */
325
+ type CameraFacingMode = 'user' | 'environment';
326
+ /**
327
+ * Best-effort "is this a phone or tablet?" check, used only to pick which camera
328
+ * opens first — never to gate a feature, so a wrong answer costs one tap on the
329
+ * flip control.
330
+ *
331
+ * `userAgentData.mobile` is the reliable signal where it exists (Chromium). The
332
+ * UA regex covers everything else, and the `maxTouchPoints` clause catches
333
+ * iPadOS 13+, which reports a desktop Macintosh UA and would otherwise open the
334
+ * front camera on a tablet.
335
+ */
336
+ declare function isMobileDevice(): boolean;
337
+ /**
338
+ * The camera to open when the caller expresses no preference: rear on mobile,
339
+ * front on everything else.
340
+ */
341
+ declare function defaultCameraFacingMode(): CameraFacingMode;
313
342
  /**
314
343
  * Decide whether the picker should offer the camera source, given the picker
315
344
  * options and the current environment. The camera is offered when:
@@ -323,8 +352,12 @@ type CameraCaptureProps = {
323
352
  onCapture: (file: File) => void;
324
353
  /** Called when the user backs out without capturing. */
325
354
  onCancel: () => void;
326
- /** Which camera to prefer. 'user' = front (mirrored), 'environment' = rear. */
327
- facingMode?: 'user' | 'environment';
355
+ /**
356
+ * Which camera to open first. 'user' = front (mirrored), 'environment' = rear.
357
+ * Omitted → `defaultCameraFacingMode()`: rear on mobile, front elsewhere. The
358
+ * user can still flip when the device has more than one camera.
359
+ */
360
+ facingMode?: CameraFacingMode;
328
361
  /** Output MIME type for the still. Default 'image/jpeg'. */
329
362
  outputType?: string;
330
363
  /** Quality 0–1 for lossy output. Default 0.92. */
@@ -401,4 +434,4 @@ type EditImageOptions = {
401
434
  };
402
435
  declare function editImage(file: File, opts?: EditImageOptions): Promise<File>;
403
436
 
404
- export { CameraCapture, type CameraCaptureProps, type CropArea, DeliveryProtection, DropPane, type DropPaneProps, type EditImageOptions, FileResult, type Flip, ImageEditor, type ImageEditorProps, type PickerFile, type PickerFileState, type PickerOptions, PickerOverlay, type PickerOverlayProps, PickerResponse, type SharedPickerProps, UploaderClientOptions, type UploaderTheme, type UsePickerOptions, type UsePickerResult, editImage, isCameraSupported, shouldOfferCamera, themeToVars, usePicker };
437
+ export { CameraCapture, type CameraCaptureProps, type CameraFacingMode, type CropArea, DeliveryProtection, DropPane, type DropPaneProps, type EditImageOptions, FileResult, type Flip, ImageEditor, type ImageEditorProps, type PickerFile, type PickerFileState, type PickerOptions, PickerOverlay, type PickerOverlayProps, PickerResponse, type SharedPickerProps, UploaderClientOptions, type UploaderTheme, type UsePickerOptions, type UsePickerResult, defaultCameraFacingMode, editImage, isCameraSupported, isMobileDevice, shouldOfferCamera, themeToVars, usePicker };
package/dist/index.d.ts CHANGED
@@ -144,9 +144,14 @@ type PickerOptions = {
144
144
  */
145
145
  fromSources?: Array<'local_file_system' | 'camera'>;
146
146
  /**
147
- * Preferred camera when the 'camera' source is used.
148
- * 'user' — front / selfie camera (mirrored). Default.
147
+ * Which camera opens first when the 'camera' source is used.
148
+ * 'user' — front / selfie camera (mirrored).
149
149
  * 'environment' — rear camera.
150
+ *
151
+ * Omitted (recommended) → chosen per device: **rear on phones and tablets**,
152
+ * front on desktops. The user can flip between them in the camera view
153
+ * whenever the device exposes more than one camera, so this only sets the
154
+ * starting side.
150
155
  */
151
156
  cameraFacingMode?: 'user' | 'environment';
152
157
  };
@@ -302,6 +307,12 @@ declare function DropPane({ apikey, apiUrl, security, deliveryProtection, allowe
302
307
  * unmount, on capture (to release the camera indicator while reviewing), and on
303
308
  * cancel.
304
309
  *
310
+ * Which camera opens first is device-dependent: phones and tablets default to
311
+ * the REAR camera (people photograph the world, not themselves, from an upload
312
+ * form), desktops to the only camera they have. A flip control appears whenever
313
+ * the device actually exposes more than one camera, so the default is a starting
314
+ * point rather than a constraint. `facingMode` overrides the default outright.
315
+ *
305
316
  * All colors resolve from --uploader-* CSS variables — no hard-coded brand
306
317
  * colors. Used by PickerOverlay and DropPane.
307
318
  *
@@ -310,6 +321,24 @@ declare function DropPane({ apikey, apiUrl, security, deliveryProtection, allowe
310
321
 
311
322
  /** True when the current environment can open a camera stream. */
312
323
  declare function isCameraSupported(): boolean;
324
+ /** Which physical camera to open. Mirrors the getUserMedia `facingMode` values. */
325
+ type CameraFacingMode = 'user' | 'environment';
326
+ /**
327
+ * Best-effort "is this a phone or tablet?" check, used only to pick which camera
328
+ * opens first — never to gate a feature, so a wrong answer costs one tap on the
329
+ * flip control.
330
+ *
331
+ * `userAgentData.mobile` is the reliable signal where it exists (Chromium). The
332
+ * UA regex covers everything else, and the `maxTouchPoints` clause catches
333
+ * iPadOS 13+, which reports a desktop Macintosh UA and would otherwise open the
334
+ * front camera on a tablet.
335
+ */
336
+ declare function isMobileDevice(): boolean;
337
+ /**
338
+ * The camera to open when the caller expresses no preference: rear on mobile,
339
+ * front on everything else.
340
+ */
341
+ declare function defaultCameraFacingMode(): CameraFacingMode;
313
342
  /**
314
343
  * Decide whether the picker should offer the camera source, given the picker
315
344
  * options and the current environment. The camera is offered when:
@@ -323,8 +352,12 @@ type CameraCaptureProps = {
323
352
  onCapture: (file: File) => void;
324
353
  /** Called when the user backs out without capturing. */
325
354
  onCancel: () => void;
326
- /** Which camera to prefer. 'user' = front (mirrored), 'environment' = rear. */
327
- facingMode?: 'user' | 'environment';
355
+ /**
356
+ * Which camera to open first. 'user' = front (mirrored), 'environment' = rear.
357
+ * Omitted → `defaultCameraFacingMode()`: rear on mobile, front elsewhere. The
358
+ * user can still flip when the device has more than one camera.
359
+ */
360
+ facingMode?: CameraFacingMode;
328
361
  /** Output MIME type for the still. Default 'image/jpeg'. */
329
362
  outputType?: string;
330
363
  /** Quality 0–1 for lossy output. Default 0.92. */
@@ -401,4 +434,4 @@ type EditImageOptions = {
401
434
  };
402
435
  declare function editImage(file: File, opts?: EditImageOptions): Promise<File>;
403
436
 
404
- export { CameraCapture, type CameraCaptureProps, type CropArea, DeliveryProtection, DropPane, type DropPaneProps, type EditImageOptions, FileResult, type Flip, ImageEditor, type ImageEditorProps, type PickerFile, type PickerFileState, type PickerOptions, PickerOverlay, type PickerOverlayProps, PickerResponse, type SharedPickerProps, UploaderClientOptions, type UploaderTheme, type UsePickerOptions, type UsePickerResult, editImage, isCameraSupported, shouldOfferCamera, themeToVars, usePicker };
437
+ export { CameraCapture, type CameraCaptureProps, type CameraFacingMode, type CropArea, DeliveryProtection, DropPane, type DropPaneProps, type EditImageOptions, FileResult, type Flip, ImageEditor, type ImageEditorProps, type PickerFile, type PickerFileState, type PickerOptions, PickerOverlay, type PickerOverlayProps, PickerResponse, type SharedPickerProps, UploaderClientOptions, type UploaderTheme, type UsePickerOptions, type UsePickerResult, defaultCameraFacingMode, editImage, isCameraSupported, isMobileDevice, shouldOfferCamera, themeToVars, usePicker };
package/dist/index.js CHANGED
@@ -782,6 +782,19 @@ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
782
782
  function isCameraSupported() {
783
783
  return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined";
784
784
  }
785
+ function isMobileDevice() {
786
+ if (typeof navigator === "undefined") return false;
787
+ const uaData = navigator.userAgentData;
788
+ if (typeof uaData?.mobile === "boolean") return uaData.mobile;
789
+ const ua = navigator.userAgent ?? "";
790
+ if (/Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|Mobile/i.test(ua)) {
791
+ return true;
792
+ }
793
+ return /Macintosh/.test(ua) && (navigator.maxTouchPoints ?? 0) > 1;
794
+ }
795
+ function defaultCameraFacingMode() {
796
+ return isMobileDevice() ? "environment" : "user";
797
+ }
785
798
  var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
786
799
  function acceptsImages(accept) {
787
800
  if (!accept || accept.length === 0) return true;
@@ -822,7 +835,7 @@ function describeCameraError(err) {
822
835
  function CameraCapture({
823
836
  onCapture,
824
837
  onCancel,
825
- facingMode = "user",
838
+ facingMode,
826
839
  outputType = "image/jpeg",
827
840
  quality: quality2 = 0.92,
828
841
  fileNamePrefix = "camera-photo",
@@ -835,12 +848,32 @@ function CameraCapture({
835
848
  const [status, setStatus] = useState3("initializing");
836
849
  const [error, setError] = useState3(null);
837
850
  const [stillUrl, setStillUrl] = useState3("");
838
- const mirror = facingMode === "user";
851
+ const [facing, setFacing] = useState3(
852
+ () => facingMode ?? defaultCameraFacingMode()
853
+ );
854
+ const [canFlip, setCanFlip] = useState3(false);
855
+ const mirror = facing === "user";
856
+ useEffect2(() => {
857
+ if (facingMode) setFacing(facingMode);
858
+ }, [facingMode]);
839
859
  const stopStream = useCallback4(() => {
840
860
  streamRef.current?.getTracks().forEach((t) => t.stop());
841
861
  streamRef.current = null;
842
862
  if (videoRef.current) videoRef.current.srcObject = null;
843
863
  }, []);
864
+ const probeCameraCount = useCallback4(async () => {
865
+ const media = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
866
+ if (!media || typeof media.enumerateDevices !== "function") {
867
+ setCanFlip(isMobileDevice());
868
+ return;
869
+ }
870
+ try {
871
+ const devices = await media.enumerateDevices();
872
+ setCanFlip(devices.filter((d) => d.kind === "videoinput").length > 1);
873
+ } catch {
874
+ setCanFlip(isMobileDevice());
875
+ }
876
+ }, []);
844
877
  const startStream = useCallback4(async () => {
845
878
  setError(null);
846
879
  setStatus("initializing");
@@ -849,9 +882,12 @@ function CameraCapture({
849
882
  setStatus("error");
850
883
  return;
851
884
  }
885
+ stopStream();
852
886
  try {
853
887
  const stream = await navigator.mediaDevices.getUserMedia({
854
- video: { facingMode },
888
+ // Non-exact: a device with no camera on the requested side falls back to
889
+ // the one it has rather than throwing OverconstrainedError.
890
+ video: { facingMode: facing },
855
891
  audio: false
856
892
  });
857
893
  if (videoRef.current == null) {
@@ -861,11 +897,12 @@ function CameraCapture({
861
897
  streamRef.current = stream;
862
898
  videoRef.current.srcObject = stream;
863
899
  setStatus("live");
900
+ void probeCameraCount();
864
901
  } catch (err) {
865
902
  setError(describeCameraError(err));
866
903
  setStatus("error");
867
904
  }
868
- }, [facingMode]);
905
+ }, [facing, probeCameraCount, stopStream]);
869
906
  useEffect2(() => {
870
907
  void startStream();
871
908
  return () => {
@@ -913,6 +950,9 @@ function CameraCapture({
913
950
  quality2
914
951
  );
915
952
  }, [mirror, outputType, quality2, stopStream]);
953
+ const flipCamera = useCallback4(() => {
954
+ setFacing((current) => current === "user" ? "environment" : "user");
955
+ }, []);
916
956
  const retake = useCallback4(() => {
917
957
  blobRef.current = null;
918
958
  setStillUrl("");
@@ -931,6 +971,7 @@ function CameraCapture({
931
971
  className: ["uploader-camera", className ?? ""].filter(Boolean).join(" "),
932
972
  style,
933
973
  "data-testid": "camera-capture",
974
+ "data-facing": facing,
934
975
  children: [
935
976
  /* @__PURE__ */ jsxs3("div", { className: "uploader-camera-stage", children: [
936
977
  status === "error" ? /* @__PURE__ */ jsx3("div", { className: "uploader-camera-error", role: "alert", children: error }) : status === "captured" ? (
@@ -947,7 +988,50 @@ function CameraCapture({
947
988
  "data-mirror": mirror ? "true" : void 0
948
989
  }
949
990
  ),
950
- status === "initializing" && /* @__PURE__ */ jsx3("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" })
991
+ status === "initializing" && /* @__PURE__ */ jsx3("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" }),
992
+ canFlip && (status === "live" || status === "initializing") && /* @__PURE__ */ jsx3(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ className: "uploader-camera-flip",
997
+ "aria-label": facing === "user" ? "Switch to rear camera" : "Switch to front camera",
998
+ disabled: status !== "live",
999
+ onClick: flipCamera,
1000
+ children: /* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true", focusable: "false", children: [
1001
+ /* @__PURE__ */ jsx3(
1002
+ "path",
1003
+ {
1004
+ 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",
1005
+ fill: "none",
1006
+ stroke: "currentColor",
1007
+ strokeWidth: "1.6",
1008
+ strokeLinejoin: "round"
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsx3(
1012
+ "path",
1013
+ {
1014
+ 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",
1015
+ fill: "none",
1016
+ stroke: "currentColor",
1017
+ strokeWidth: "1.6",
1018
+ strokeLinecap: "round"
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsx3(
1022
+ "path",
1023
+ {
1024
+ d: "m9.4 10.2-.1 2.1 2.1-.6M14.6 13.8l.1-2.1-2.1.6",
1025
+ fill: "none",
1026
+ stroke: "currentColor",
1027
+ strokeWidth: "1.6",
1028
+ strokeLinecap: "round",
1029
+ strokeLinejoin: "round"
1030
+ }
1031
+ )
1032
+ ] })
1033
+ }
1034
+ )
951
1035
  ] }),
952
1036
  status === "live" && /* @__PURE__ */ jsx3("p", { className: "uploader-camera-hint", children: "Frame your shot, then tap the shutter to capture." }),
953
1037
  /* @__PURE__ */ jsx3("div", { className: "uploader-camera-actions", children: status === "captured" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
@@ -1638,10 +1722,12 @@ export {
1638
1722
  PickerOverlay,
1639
1723
  UploaderClient,
1640
1724
  crop,
1725
+ defaultCameraFacingMode,
1641
1726
  editImage,
1642
1727
  flip,
1643
1728
  flop,
1644
1729
  isCameraSupported,
1730
+ isMobileDevice,
1645
1731
  output,
1646
1732
  quality,
1647
1733
  resize,