@pallasoft/ps-file-components 1.4.0 → 1.5.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.
Files changed (113) hide show
  1. package/dist/components/ps-document-scanner-loader.d.ts +1 -1
  2. package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
  3. package/dist/components/ps-document-scanner-sheet.js +69 -21
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -2
  6. package/dist/components/ps-file-picker-actions.d.ts +10 -0
  7. package/dist/components/ps-file-picker-actions.js +21 -0
  8. package/dist/components/ps-file-picker.d.ts +7 -1
  9. package/dist/components/ps-file-picker.js +30 -6
  10. package/dist/components/ps-scan-button.d.ts +13 -0
  11. package/dist/components/ps-scan-button.js +9 -0
  12. package/dist/components/ps-scan-camera-view.d.ts +17 -7
  13. package/dist/components/ps-scan-camera-view.js +23 -16
  14. package/dist/components/ps-scan-crop-editor.d.ts +2 -2
  15. package/dist/components/ps-scan-crop-editor.js +19 -3
  16. package/dist/components/ps-scan-denied-steps.d.ts +6 -0
  17. package/dist/components/ps-scan-denied-steps.js +33 -0
  18. package/dist/components/ps-scan-icon-button.d.ts +4 -3
  19. package/dist/components/ps-scan-icon-button.js +3 -1
  20. package/dist/components/ps-scan-illustration.d.ts +1 -0
  21. package/dist/components/ps-scan-illustration.js +6 -0
  22. package/dist/components/ps-scan-loupe.d.ts +10 -0
  23. package/dist/components/ps-scan-loupe.js +31 -0
  24. package/dist/components/ps-scan-page-list.d.ts +3 -1
  25. package/dist/components/ps-scan-page-list.js +7 -8
  26. package/dist/components/ps-scan-page-stack.d.ts +7 -0
  27. package/dist/components/ps-scan-page-stack.js +6 -0
  28. package/dist/components/ps-scan-permission-view.d.ts +11 -0
  29. package/dist/components/ps-scan-permission-view.js +48 -0
  30. package/dist/components/ps-scan-photo-button.d.ts +8 -0
  31. package/dist/components/ps-scan-photo-button.js +20 -0
  32. package/dist/components/ps-scan-review.d.ts +2 -1
  33. package/dist/components/ps-scan-review.js +21 -4
  34. package/dist/components/ps-scan-shutter.d.ts +7 -0
  35. package/dist/components/ps-scan-shutter.js +6 -0
  36. package/dist/components/ps-scan-viewfinder-overlay.d.ts +8 -0
  37. package/dist/components/ps-scan-viewfinder-overlay.js +28 -0
  38. package/dist/components/ps-upload-row.js +1 -1
  39. package/dist/constants/ps-document-scanner-labels.d.ts +49 -19
  40. package/dist/constants/ps-document-scanner-labels.js +49 -19
  41. package/dist/constants/ps-file-picker-labels.d.ts +5 -3
  42. package/dist/constants/ps-file-picker-labels.js +5 -3
  43. package/dist/constants/ps-scan-camera-status.d.ts +4 -0
  44. package/dist/constants/ps-scan-camera-status.js +4 -0
  45. package/dist/constants/ps-scan-chrome-classes.d.ts +12 -0
  46. package/dist/constants/ps-scan-chrome-classes.js +15 -0
  47. package/dist/constants/ps-scan-config.d.ts +18 -1
  48. package/dist/constants/ps-scan-config.js +18 -1
  49. package/dist/constants/ps-scan-platform.d.ts +6 -0
  50. package/dist/constants/ps-scan-platform.js +8 -0
  51. package/dist/esm/components/ps-document-scanner-sheet.js +71 -23
  52. package/dist/esm/components/ps-document-scanner.js +2 -2
  53. package/dist/esm/components/ps-file-picker-actions.js +17 -0
  54. package/dist/esm/components/ps-file-picker.js +31 -7
  55. package/dist/esm/components/ps-scan-button.js +5 -0
  56. package/dist/esm/components/ps-scan-camera-view.js +26 -19
  57. package/dist/esm/components/ps-scan-crop-editor.js +21 -5
  58. package/dist/esm/components/ps-scan-denied-steps.js +29 -0
  59. package/dist/esm/components/ps-scan-icon-button.js +3 -1
  60. package/dist/esm/components/ps-scan-illustration.js +2 -0
  61. package/dist/esm/components/ps-scan-loupe.js +27 -0
  62. package/dist/esm/components/ps-scan-page-list.js +7 -8
  63. package/dist/esm/components/ps-scan-page-stack.js +2 -0
  64. package/dist/esm/components/ps-scan-permission-view.js +44 -0
  65. package/dist/esm/components/ps-scan-photo-button.js +16 -0
  66. package/dist/esm/components/ps-scan-review.js +21 -4
  67. package/dist/esm/components/ps-scan-shutter.js +2 -0
  68. package/dist/esm/components/ps-scan-viewfinder-overlay.js +24 -0
  69. package/dist/esm/components/ps-upload-row.js +1 -1
  70. package/dist/esm/constants/ps-document-scanner-labels.js +49 -19
  71. package/dist/esm/constants/ps-file-picker-labels.js +5 -3
  72. package/dist/esm/constants/ps-scan-camera-status.js +4 -0
  73. package/dist/esm/constants/ps-scan-chrome-classes.js +12 -0
  74. package/dist/esm/constants/ps-scan-config.js +18 -1
  75. package/dist/esm/constants/ps-scan-platform.js +5 -0
  76. package/dist/esm/hooks/use-ps-scan-camera.js +52 -15
  77. package/dist/esm/hooks/use-ps-scan-live-detection.js +45 -0
  78. package/dist/esm/index.js +1 -0
  79. package/dist/esm/models/ps-file-picker-scan.js +1 -0
  80. package/dist/esm/models/ps-scan-live-detection.js +1 -0
  81. package/dist/esm/models/ps-scan-stability.js +1 -0
  82. package/dist/esm/services/ps-scan-camera-service.js +50 -5
  83. package/dist/esm/services/ps-scan-canvas-service.js +6 -0
  84. package/dist/esm/services/ps-scan-edge-service.js +1 -1
  85. package/dist/esm/services/ps-scan-geometry-service.js +20 -0
  86. package/dist/esm/services/ps-scan-platform-service.js +15 -0
  87. package/dist/esm/services/ps-scan-stability-service.js +41 -0
  88. package/dist/hooks/use-ps-scan-camera.d.ts +3 -1
  89. package/dist/hooks/use-ps-scan-camera.js +52 -15
  90. package/dist/hooks/use-ps-scan-live-detection.d.ts +3 -0
  91. package/dist/hooks/use-ps-scan-live-detection.js +49 -0
  92. package/dist/index.d.ts +1 -0
  93. package/dist/index.js +1 -0
  94. package/dist/models/ps-document-scanner-props.d.ts +1 -0
  95. package/dist/models/ps-document-scanner-sheet-props.d.ts +1 -0
  96. package/dist/models/ps-file-picker-scan.d.ts +4 -0
  97. package/dist/models/ps-file-picker-scan.js +2 -0
  98. package/dist/models/ps-scan-live-detection.d.ts +5 -0
  99. package/dist/models/ps-scan-live-detection.js +2 -0
  100. package/dist/models/ps-scan-stability.d.ts +8 -0
  101. package/dist/models/ps-scan-stability.js +2 -0
  102. package/dist/services/ps-scan-camera-service.d.ts +4 -0
  103. package/dist/services/ps-scan-camera-service.js +50 -5
  104. package/dist/services/ps-scan-canvas-service.d.ts +1 -0
  105. package/dist/services/ps-scan-canvas-service.js +6 -0
  106. package/dist/services/ps-scan-edge-service.js +1 -1
  107. package/dist/services/ps-scan-geometry-service.d.ts +2 -0
  108. package/dist/services/ps-scan-geometry-service.js +20 -0
  109. package/dist/services/ps-scan-platform-service.d.ts +4 -0
  110. package/dist/services/ps-scan-platform-service.js +19 -0
  111. package/dist/services/ps-scan-stability-service.d.ts +9 -0
  112. package/dist/services/ps-scan-stability-service.js +45 -0
  113. package/package.json +3 -3
@@ -1,8 +1,10 @@
1
1
  export declare const PsFilePickerLabels: {
2
- readonly browse: "or click to browse your files";
3
- readonly browseTouch: "or tap to choose from your device";
2
+ readonly choose: "Choose files";
3
+ readonly chooseOne: "Choose a file";
4
+ readonly dragHint: "Drag and drop files here";
5
+ readonly dragHintOne: "Drag and drop a file here";
4
6
  readonly dropActive: "Drop to upload";
5
- readonly formats: "Accepted formats:";
6
7
  readonly invalidType: "is not a supported file type.";
8
+ readonly scan: "Scan with camera";
7
9
  readonly tooLarge: "is too large.";
8
10
  };
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsFilePickerLabels = void 0;
4
4
  exports.PsFilePickerLabels = {
5
- browse: 'or click to browse your files',
6
- browseTouch: 'or tap to choose from your device',
5
+ choose: 'Choose files',
6
+ chooseOne: 'Choose a file',
7
+ dragHint: 'Drag and drop files here',
8
+ dragHintOne: 'Drag and drop a file here',
7
9
  dropActive: 'Drop to upload',
8
- formats: 'Accepted formats:',
9
10
  invalidType: 'is not a supported file type.',
11
+ scan: 'Scan with camera',
10
12
  tooLarge: 'is too large.',
11
13
  };
@@ -1,5 +1,9 @@
1
1
  export declare const PsScanCameraStatus: {
2
+ readonly Busy: "busy";
3
+ readonly Checking: "checking";
2
4
  readonly Denied: "denied";
5
+ readonly Insecure: "insecure";
6
+ readonly Prompt: "prompt";
3
7
  readonly Ready: "ready";
4
8
  readonly Starting: "starting";
5
9
  readonly Unavailable: "unavailable";
@@ -2,7 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsScanCameraStatus = void 0;
4
4
  exports.PsScanCameraStatus = {
5
+ Busy: 'busy',
6
+ Checking: 'checking',
5
7
  Denied: 'denied',
8
+ Insecure: 'insecure',
9
+ Prompt: 'prompt',
6
10
  Ready: 'ready',
7
11
  Starting: 'starting',
8
12
  Unavailable: 'unavailable',
@@ -0,0 +1,12 @@
1
+ export declare const PsScanChromeClasses: {
2
+ readonly button: "ps-focus-ring ps-control ps-control-padding inline-flex max-w-full items-center justify-center gap-2 rounded-ps-control border text-sm font-semibold leading-5 transition disabled:pointer-events-none disabled:opacity-40";
3
+ readonly buttonLarge: "ps-control-large ps-control-padding-large";
4
+ readonly buttonPrimary: "border-primary bg-primary text-[color:var(--ps-color-primary-contrast)] hover:border-primary-dark hover:bg-primary-dark";
5
+ readonly buttonQuiet: "border-white/15 bg-white/10 text-white hover:border-white/25 hover:bg-white/20";
6
+ readonly chip: "ps-focus-ring ps-control inline-flex min-w-0 items-center justify-center rounded-ps-control px-2 text-sm font-semibold transition";
7
+ readonly icon: "ps-focus-ring ps-control ps-control-square inline-flex shrink-0 items-center justify-center rounded-ps-control text-white transition disabled:pointer-events-none disabled:opacity-40";
8
+ readonly iconOff: "bg-black/45 backdrop-blur-md hover:bg-white/20";
9
+ readonly iconOn: "bg-white text-black hover:bg-white/90";
10
+ readonly pill: "pointer-events-none rounded-ps-pill bg-black/55 px-3.5 py-1.5 text-sm font-medium text-white backdrop-blur-md";
11
+ readonly tool: "ps-focus-ring flex min-h-14 min-w-16 flex-col items-center justify-center gap-1 rounded-ps-control px-2 text-xs font-medium text-white/80 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-40";
12
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanChromeClasses = void 0;
4
+ exports.PsScanChromeClasses = {
5
+ button: 'ps-focus-ring ps-control ps-control-padding inline-flex max-w-full items-center justify-center gap-2 rounded-ps-control border text-sm font-semibold leading-5 transition disabled:pointer-events-none disabled:opacity-40',
6
+ buttonLarge: 'ps-control-large ps-control-padding-large',
7
+ buttonPrimary: 'border-primary bg-primary text-[color:var(--ps-color-primary-contrast)] hover:border-primary-dark hover:bg-primary-dark',
8
+ buttonQuiet: 'border-white/15 bg-white/10 text-white hover:border-white/25 hover:bg-white/20',
9
+ chip: 'ps-focus-ring ps-control inline-flex min-w-0 items-center justify-center rounded-ps-control px-2 text-sm font-semibold transition',
10
+ icon: 'ps-focus-ring ps-control ps-control-square inline-flex shrink-0 items-center justify-center rounded-ps-control text-white transition disabled:pointer-events-none disabled:opacity-40',
11
+ iconOff: 'bg-black/45 backdrop-blur-md hover:bg-white/20',
12
+ iconOn: 'bg-white text-black hover:bg-white/90',
13
+ pill: 'pointer-events-none rounded-ps-pill bg-black/55 px-3.5 py-1.5 text-sm font-medium text-white backdrop-blur-md',
14
+ tool: 'ps-focus-ring flex min-h-14 min-w-16 flex-col items-center justify-center gap-1 rounded-ps-control px-2 text-xs font-medium text-white/80 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-40',
15
+ };
@@ -2,6 +2,8 @@ export declare const PsScanConfig: {
2
2
  readonly adaptiveRatio: 0.92;
3
3
  readonly adaptiveWindowDivisor: 16;
4
4
  readonly adaptiveWindowMin: 15;
5
+ readonly autoCaptureMs: 1000;
6
+ readonly cameraPermission: "camera";
5
7
  readonly capturedJpegQuality: 0.92;
6
8
  readonly cornerNudge: 0.005;
7
9
  readonly cornerNudgeLarge: 0.025;
@@ -11,6 +13,7 @@ export declare const PsScanConfig: {
11
13
  readonly defaultMaxDimension: 2000;
12
14
  readonly defaultMaxPages: 30;
13
15
  readonly detectDilations: 2;
16
+ readonly detectInset: 3;
14
17
  readonly detectMaxDimension: 320;
15
18
  readonly detectMinAreaRatio: 0.12;
16
19
  readonly detectMaxAreaRatio: 0.995;
@@ -18,14 +21,28 @@ export declare const PsScanConfig: {
18
21
  readonly detectMinHullFill: 0.85;
19
22
  readonly detectPercentile: 0.98;
20
23
  readonly detectRelativeStrength: 0.3;
24
+ readonly guideArm: 36;
25
+ readonly guideArmRatio: 0.18;
26
+ readonly guideAspect: 0.72;
27
+ readonly guideHeightRatio: 0.58;
28
+ readonly guideLift: 0.03;
29
+ readonly guideWidthRatio: 0.76;
21
30
  readonly hullCandidateLimit: 18;
22
31
  readonly levelHighPercentile: 0.99;
23
32
  readonly levelLowPercentile: 0.01;
33
+ readonly liveDetectIntervalMs: 160;
34
+ readonly liveDetectMaxDimension: 240;
35
+ readonly liveMissLimit: 2;
36
+ readonly liveRearmMovement: 0.08;
37
+ readonly liveSmoothing: 0.5;
38
+ readonly liveStillMovement: 0.02;
39
+ readonly loupeOffset: 72;
40
+ readonly loupeSize: 112;
41
+ readonly loupeZoom: 2.5;
24
42
  readonly minCornerGap: 0.02;
25
43
  readonly pdfDpi: 150;
26
44
  readonly pdfPointsPerInch: 72;
27
45
  readonly previewDebounceMs: 120;
28
46
  readonly previewMaxDimension: 1400;
29
- readonly thumbnailHeight: 56;
30
47
  readonly torchConstraint: "torch";
31
48
  };
@@ -5,6 +5,8 @@ exports.PsScanConfig = {
5
5
  adaptiveRatio: 0.92,
6
6
  adaptiveWindowDivisor: 16,
7
7
  adaptiveWindowMin: 15,
8
+ autoCaptureMs: 1000,
9
+ cameraPermission: 'camera',
8
10
  capturedJpegQuality: 0.92,
9
11
  cornerNudge: 0.005,
10
12
  cornerNudgeLarge: 0.025,
@@ -14,6 +16,7 @@ exports.PsScanConfig = {
14
16
  defaultMaxDimension: 2000,
15
17
  defaultMaxPages: 30,
16
18
  detectDilations: 2,
19
+ detectInset: 3,
17
20
  detectMaxDimension: 320,
18
21
  detectMinAreaRatio: 0.12,
19
22
  detectMaxAreaRatio: 0.995,
@@ -21,14 +24,28 @@ exports.PsScanConfig = {
21
24
  detectMinHullFill: 0.85,
22
25
  detectPercentile: 0.98,
23
26
  detectRelativeStrength: 0.3,
27
+ guideArm: 36,
28
+ guideArmRatio: 0.18,
29
+ guideAspect: 0.72,
30
+ guideHeightRatio: 0.58,
31
+ guideLift: 0.03,
32
+ guideWidthRatio: 0.76,
24
33
  hullCandidateLimit: 18,
25
34
  levelHighPercentile: 0.99,
26
35
  levelLowPercentile: 0.01,
36
+ liveDetectIntervalMs: 160,
37
+ liveDetectMaxDimension: 240,
38
+ liveMissLimit: 2,
39
+ liveRearmMovement: 0.08,
40
+ liveSmoothing: 0.5,
41
+ liveStillMovement: 0.02,
42
+ loupeOffset: 72,
43
+ loupeSize: 112,
44
+ loupeZoom: 2.5,
27
45
  minCornerGap: 0.02,
28
46
  pdfDpi: 150,
29
47
  pdfPointsPerInch: 72,
30
48
  previewDebounceMs: 120,
31
49
  previewMaxDimension: 1400,
32
- thumbnailHeight: 56,
33
50
  torchConstraint: 'torch',
34
51
  };
@@ -0,0 +1,6 @@
1
+ export declare const PsScanPlatform: {
2
+ readonly Android: "android";
3
+ readonly Desktop: "desktop";
4
+ readonly Ios: "ios";
5
+ };
6
+ export type PsScanPlatform = (typeof PsScanPlatform)[keyof typeof PsScanPlatform];
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanPlatform = void 0;
4
+ exports.PsScanPlatform = {
5
+ Android: 'android',
6
+ Desktop: 'desktop',
7
+ Ios: 'ios',
8
+ };
@@ -1,13 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useRef, useState } from 'react';
3
3
  import { PsLayer, usePsLayerZIndex, usePsRestoreFocus, usePsScrollLock, usePsViewport, } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftViewportTier, PallasoftZIndex } from '@pallasoft/ps-design-system';
5
- import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
6
- import { PsModal } from '@pallasoft/ps-popovers';
5
+ import { PsIconKey } from '@pallasoft/ps-icons';
7
6
  import { createPortal } from 'react-dom';
7
+ import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
8
8
  import { PsScanStep } from '../constants/ps-scan-step';
9
9
  import { usePsFocusTrap } from '../hooks/use-ps-focus-trap';
10
10
  import { usePsScanCamera } from '../hooks/use-ps-scan-camera';
11
+ import { usePsScanLiveDetection } from '../hooks/use-ps-scan-live-detection';
11
12
  import { usePsScanPages } from '../hooks/use-ps-scan-pages';
12
13
  import { usePsScanPreviews } from '../hooks/use-ps-scan-previews';
13
14
  import { PsScanOutputService } from '../services/ps-scan-output-service';
@@ -16,27 +17,37 @@ import { PsScanTemplateService } from '../services/ps-scan-template-service';
16
17
  import { PsScanCameraView } from './ps-scan-camera-view';
17
18
  import { PsScanCropEditor } from './ps-scan-crop-editor';
18
19
  import { PsScanIconButton } from './ps-scan-icon-button';
20
+ import { PsScanPermissionView } from './ps-scan-permission-view';
19
21
  import { PsScanReview } from './ps-scan-review';
20
- export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, maxPages, onClose, onComplete, outputFormat, }) => {
22
+ export const PsDocumentScannerSheet = ({ fileName, isAutoCapture, jpegQuality, labels, maxDimension, maxPages, onClose, onComplete, outputFormat, }) => {
21
23
  const tier = usePsViewport();
22
24
  const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
23
25
  const content = useRef(null);
24
26
  const { add, move, pages, remove, replace, setCorners, update } = usePsScanPages();
25
27
  const [step, setStep] = useState(PsScanStep.Camera);
28
+ const [cropReturn, setCropReturn] = useState(PsScanStep.Camera);
26
29
  const [activeId, setActiveId] = useState('');
27
30
  const [retakeId, setRetakeId] = useState('');
28
31
  const [announcement, setAnnouncement] = useState('');
29
32
  const [error, setError] = useState('');
30
33
  const [isBusy, setIsBusy] = useState(false);
34
+ const [isCapturing, setIsCapturing] = useState(false);
35
+ const [isAuto, setIsAuto] = useState(isAutoCapture);
36
+ const [flashKey, setFlashKey] = useState(0);
31
37
  const camera = usePsScanCamera(step === PsScanStep.Camera);
38
+ const isReady = camera.status === PsScanCameraStatus.Ready;
39
+ const isFull = pages.length >= maxPages;
32
40
  const previews = usePsScanPreviews(pages);
33
41
  const page = pages.find((item) => item.id === activeId);
42
+ const lastPage = pages[pages.length - 1];
34
43
  const isDesktop = tier === PallasoftViewportTier.Desktop;
35
- usePsScrollLock(!isDesktop);
44
+ const isCameraStep = step === PsScanStep.Camera;
45
+ const showsCamera = isReady || camera.status === PsScanCameraStatus.Starting;
46
+ usePsScrollLock(true);
36
47
  usePsRestoreFocus(true);
37
48
  usePsFocusTrap(content, true, onClose);
38
49
  const edgeMessage = (isDetected) => isDetected ? labels.edgesFound : labels.edgesNotFound;
39
- const addFiles = async (files) => {
50
+ const addFiles = async (files, after) => {
40
51
  setError('');
41
52
  try {
42
53
  const images = files.filter((file) => file.type === '' || file.type.startsWith('image/'));
@@ -54,21 +65,39 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
54
65
  const added = await add(images.slice(0, room));
55
66
  if (added.length === 0)
56
67
  return;
57
- setActiveId(added[0].page.id);
58
- setStep(added.length === 1 ? PsScanStep.Crop : PsScanStep.Review);
59
- setAnnouncement(added.length === 1
60
- ? edgeMessage(added[0].isDetected)
61
- : PsScanTemplateService.fill(labels.pageAdded, { position: pages.length + added.length }));
68
+ const position = pages.length + added.length;
69
+ setActiveId(added[added.length - 1].page.id);
70
+ setAnnouncement(PsScanTemplateService.fill(labels.pageAdded, { position }));
71
+ if (after === null)
72
+ return;
73
+ if (added.length === 1) {
74
+ setCropReturn(after);
75
+ setStep(PsScanStep.Crop);
76
+ setAnnouncement(edgeMessage(added[0].isDetected));
77
+ }
78
+ else
79
+ setStep(PsScanStep.Review);
62
80
  }
63
81
  catch {
64
82
  setError(labels.processFailed);
65
83
  }
66
84
  };
67
- const capture = async () => {
68
- const blob = await camera.capture();
69
- if (blob !== null)
70
- await addFiles([blob]);
85
+ const capture = async (isAutomatic) => {
86
+ if (isCapturing || isFull)
87
+ return;
88
+ setIsCapturing(true);
89
+ try {
90
+ const blob = await camera.capture();
91
+ if (blob === null)
92
+ return;
93
+ setFlashKey((current) => current + 1);
94
+ await addFiles([blob], isAutomatic && retakeId === '' ? null : PsScanStep.Camera);
95
+ }
96
+ finally {
97
+ setIsCapturing(false);
98
+ }
71
99
  };
100
+ const detection = usePsScanLiveDetection(camera.videoRef, isCameraStep && isReady, isAuto, () => capture(true));
72
101
  const autoDetect = async () => {
73
102
  if (page === undefined)
74
103
  return;
@@ -76,12 +105,32 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
76
105
  setCorners(page.id, prepared.corners);
77
106
  setAnnouncement(edgeMessage(prepared.isDetected));
78
107
  };
108
+ const keep = () => setStep(cropReturn === PsScanStep.Camera && !isFull ? PsScanStep.Camera : PsScanStep.Review);
79
109
  const retake = () => {
80
110
  if (page === undefined)
81
111
  return;
112
+ if (step === PsScanStep.Review)
113
+ setCropReturn(PsScanStep.Review);
82
114
  setRetakeId(page.id);
83
115
  setStep(PsScanStep.Camera);
84
116
  };
117
+ const openCamera = () => {
118
+ setRetakeId('');
119
+ setCropReturn(PsScanStep.Camera);
120
+ setStep(PsScanStep.Camera);
121
+ };
122
+ const openReview = () => {
123
+ if (pages.length === 0)
124
+ return;
125
+ setRetakeId('');
126
+ if (page === undefined)
127
+ setActiveId(lastPage.id);
128
+ setStep(PsScanStep.Review);
129
+ };
130
+ const editCrop = () => {
131
+ setCropReturn(PsScanStep.Review);
132
+ setStep(PsScanStep.Crop);
133
+ };
85
134
  const deletePage = () => {
86
135
  if (page === undefined)
87
136
  return;
@@ -91,7 +140,7 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
91
140
  setAnnouncement(labels.pageDeleted);
92
141
  if (rest.length === 0) {
93
142
  setActiveId('');
94
- setStep(PsScanStep.Camera);
143
+ openCamera();
95
144
  }
96
145
  else
97
146
  setActiveId(rest[Math.min(index, rest.length - 1)].id);
@@ -122,13 +171,12 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
122
171
  position: to + 1,
123
172
  }));
124
173
  }, [move, pages.length, labels]);
125
- const body = (_jsxs("div", { className: 'relative h-full min-h-0 bg-surface text-ink', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), step === PsScanStep.Camera ? (_jsx(PsScanCameraView, { canGoBack: pages.length > 0, canTorch: camera.canTorch, isTorchOn: camera.isTorchOn, labels: labels, onBack: () => {
126
- setRetakeId('');
127
- setStep(PsScanStep.Review);
128
- }, onCapture: capture, onFiles: addFiles, onToggleTorch: camera.toggleTorch, status: camera.status, videoRef: camera.videoRef })) : null, step === PsScanStep.Crop && page !== undefined ? (_jsx(PsScanCropEditor, { labels: labels, onApply: () => setStep(PsScanStep.Review), onAutoDetect: autoDetect, onChange: (corners) => setCorners(page.id, corners), onRetake: retake, page: page })) : null, step === PsScanStep.Review && page !== undefined ? (_jsx(PsScanReview, { canAddPage: pages.length < maxPages, error: error, isBusy: isBusy, labels: labels, onAddPage: () => setStep(PsScanStep.Camera), onDelete: deletePage, onEditCrop: () => setStep(PsScanStep.Crop), onFilter: (filter) => update(page.id, { filter }), onFinish: finish, onMove: moveTo, onRetake: retake, onRotate: () => update(page.id, { rotation: (page.rotation + 90) % 360 }), onSelect: setActiveId, page: page, pages: pages, previews: previews })) : null, step === PsScanStep.Camera && error !== '' ? (_jsx("p", { className: 'absolute inset-x-4 top-3 rounded-ps-control bg-danger px-3 py-2 text-sm text-white', role: 'alert', children: error })) : null] }));
129
- if (isDesktop)
130
- return (_jsx(PsModal, { isOpen: true, onClose: onClose, size: 'xl', title: labels.title, children: _jsx("div", { className: '-m-4 h-[min(70dvh,38rem)] overflow-hidden sm:-m-5', children: body }) }));
174
+ const fromLibrary = (files) => addFiles(files, PsScanStep.Review);
131
175
  if (typeof document === 'undefined')
132
176
  return null;
133
- return createPortal(_jsx("div", { "aria-label": labels.title, "aria-modal": 'true', className: 'fixed inset-0 flex flex-col bg-surface text-ink animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: _jsxs(PsLayer, { zIndex: zIndex, children: [_jsxs("header", { className: 'flex shrink-0 items-center justify-between gap-3 border-b border-line px-4 pt-[max(0.5rem,env(safe-area-inset-top))] pb-2', children: [_jsx("h2", { className: 'truncate text-base font-semibold', children: labels.title }), _jsx(PsScanIconButton, { label: labels.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Close }) })] }), _jsx("div", { className: 'min-h-0 flex-1', children: body })] }) }), document.body);
177
+ return createPortal(_jsx("div", { "aria-label": labels.title, "aria-modal": 'true', className: isDesktop
178
+ ? 'fixed inset-0 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm animate-ps-fade-in'
179
+ : 'fixed inset-0 bg-black animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: _jsx(PsLayer, { zIndex: zIndex, children: _jsxs("div", { className: isDesktop
180
+ ? 'relative h-[min(52rem,calc(100dvh-3rem))] w-[min(76rem,calc(100vw-3rem))] overflow-hidden rounded-2xl bg-black text-white shadow-2xl ring-1 ring-white/10 animate-ps-scale-in'
181
+ : 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? (_jsx(PsScanCameraView, { canTorch: camera.canTorch, detection: detection, flashKey: flashKey, isAuto: isAuto, isBusy: isCapturing, isFull: isFull, isMultiple: maxPages > 1, isReady: isReady, isTorchOn: camera.isTorchOn, labels: labels, lastPreview: lastPage === undefined ? undefined : previews[lastPage.id], onCapture: () => capture(false), onClose: onClose, onDone: openReview, onFiles: fromLibrary, onToggleAuto: () => setIsAuto((current) => !current), onToggleTorch: camera.toggleTorch, pageCount: pages.length, videoRef: camera.videoRef, videoTarget: camera.attachVideo })) : null, isCameraStep && !showsCamera ? (_jsxs(_Fragment, { children: [_jsx(PsScanPermissionView, { isMultiple: maxPages > 1, labels: labels, onAllow: camera.allow, onFiles: fromLibrary, status: camera.status }), _jsx("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }) }), pages.length > 0 ? (_jsx("div", { className: 'absolute right-4 top-[max(1rem,env(safe-area-inset-top))]', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Check, label: PsScanTemplateService.fill(labels.viewPages, { count: pages.length }), onClick: openReview }) })) : null] })) : null, step === PsScanStep.Crop && page !== undefined ? (_jsx(PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: (corners) => setCorners(page.id, corners), onKeep: keep, onRetake: retake, page: page })) : null, step === PsScanStep.Review && page !== undefined ? (_jsx(PsScanReview, { canAddPage: !isFull, error: error, isBusy: isBusy, labels: labels, onAddPage: openCamera, onClose: onClose, onDelete: deletePage, onEditCrop: editCrop, onFilter: (filter) => update(page.id, { filter }), onFinish: finish, onMove: moveTo, onRetake: retake, onRotate: () => update(page.id, { rotation: (page.rotation + 90) % 360 }), onSelect: setActiveId, page: page, pages: pages, previews: previews })) : null, isCameraStep && error !== '' ? (_jsx("p", { className: 'absolute inset-x-4 bottom-36 mx-auto max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })) : null] }) }) }), document.body);
134
182
  };
@@ -5,9 +5,9 @@ import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels
5
5
  import { PsScanConfig } from '../constants/ps-scan-config';
6
6
  import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
7
7
  import { PsDocumentScannerLoader } from './ps-document-scanner-loader';
8
- export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, maxPages = PsScanConfig.defaultMaxPages, onClose, onComplete, outputFormat = PsScanOutputFormat.Pdf, }) => {
8
+ export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isAutoCapture = false, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, maxPages = PsScanConfig.defaultMaxPages, onClose, onComplete, outputFormat = PsScanOutputFormat.Pdf, }) => {
9
9
  const isHydrated = usePsHydrated();
10
10
  if (!isOpen || !isHydrated)
11
11
  return null;
12
- return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, maxPages: maxPages, onClose: onClose, onComplete: onComplete, outputFormat: outputFormat }) }));
12
+ return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, isAutoCapture: isAutoCapture, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, maxPages: maxPages, onClose: onClose, onComplete: onComplete, outputFormat: outputFormat }) }));
13
13
  };
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsButton } from '@pallasoft/ps-core-components';
3
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
+ export const PsFilePickerActions = ({ chooseLabel, className, disabled, isTouch, onChoose, onScan, scanLabel, }) => {
6
+ const hasScan = scanLabel !== undefined;
7
+ const choose = (_jsx(PsButton, { className: isTouch ? 'w-full' : '', disabled: disabled, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Upload }), onClick: (event) => {
8
+ event.stopPropagation();
9
+ onChoose();
10
+ }, size: isTouch ? 'large' : 'default', variant: hasScan && isTouch ? 'surface' : hasScan || isTouch ? 'primary' : 'surface', children: chooseLabel }));
11
+ return (_jsx("div", { className: PallasoftClassNameService.merge(isTouch
12
+ ? 'w-full max-w-xs flex-col gap-2'
13
+ : 'flex-row flex-wrap items-center justify-center gap-2', className), children: hasScan ? (_jsxs(_Fragment, { children: [isTouch ? null : choose, _jsx(PsButton, { className: isTouch ? 'w-full' : '', disabled: disabled, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Camera }), onClick: (event) => {
14
+ event.stopPropagation();
15
+ onScan();
16
+ }, size: isTouch ? 'large' : 'default', variant: isTouch ? 'primary' : 'surface', children: scanLabel }), isTouch ? choose : null] })) : (choose) }));
17
+ };
@@ -1,15 +1,26 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
2
+ import { useMemo, useRef, useState } from 'react';
3
3
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
4
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
5
  import { PsFilePickerLabels } from '../constants/ps-file-picker-labels';
6
+ import { PsScanConfig } from '../constants/ps-scan-config';
7
+ import { PsUploadLabels } from '../constants/ps-upload-labels';
6
8
  import { PsFileSelectionService } from '../services/ps-file-selection-service';
7
- export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false, dropLabel, isLoading = false, labels, maxSizeBytes, maxSizeLabel, multiple = false, onError, onFileLoad, onFilesSelect, touchLabel, }) => {
9
+ import { PsDocumentScanner } from './ps-document-scanner';
10
+ import { PsFilePickerActions } from './ps-file-picker-actions';
11
+ import { PsUploadRow } from './ps-upload-row';
12
+ export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false, dropLabel, isLoading = false, labels, maxSizeBytes, maxSizeLabel, multiple = false, onError, onFileLoad, onFilesSelect, queue, scan, touchLabel, uploadLabels, }) => {
8
13
  const [isDragging, setIsDragging] = useState(false);
14
+ const [scanName, setScanName] = useState(null);
15
+ const input = useRef(null);
9
16
  const acceptAttr = useMemo(() => accept.join(','), [accept]);
10
17
  const text = { ...PsFilePickerLabels, ...labels };
18
+ const rowLabels = { ...PsUploadLabels, ...uploadLabels };
19
+ const details = [acceptLabel, maxSizeLabel].filter((value) => value !== undefined && value !== '');
20
+ const chooseLabel = multiple ? text.choose : text.chooseOne;
21
+ const scanLabel = scan === undefined ? undefined : text.scan;
11
22
  if (isLoading)
12
- return (_jsx("span", { "aria-busy": 'true', className: PallasoftClassNameService.merge('ps-skeleton block min-h-32 w-full rounded-ps-panel', className), role: 'status' }));
23
+ return (_jsx("span", { "aria-busy": 'true', className: PallasoftClassNameService.merge('ps-skeleton block min-h-48 w-full rounded-ps-control', className), role: 'status' }));
13
24
  const select = (list) => {
14
25
  if (list === null || disabled)
15
26
  return;
@@ -28,6 +39,15 @@ export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false,
28
39
  else
29
40
  onFileLoad?.(accepted[0]);
30
41
  };
42
+ const browse = () => {
43
+ if (!disabled)
44
+ input.current?.click();
45
+ };
46
+ const openScan = () => {
47
+ if (disabled || scan === undefined)
48
+ return;
49
+ setScanName(scan.getFileName === undefined ? PsScanConfig.defaultFileName : scan.getFileName());
50
+ };
31
51
  const onChange = (event) => {
32
52
  select(event.target.files);
33
53
  event.target.value = '';
@@ -37,8 +57,12 @@ export const PsFilePicker = ({ accept, acceptLabel, className, disabled = false,
37
57
  if (!disabled)
38
58
  setIsDragging(isOver);
39
59
  };
40
- return (_jsxs("div", { className: PallasoftClassNameService.merge('relative flex h-48 w-full flex-col items-center justify-center rounded-ps-control border-2 border-dashed border-line p-4 transition-all hover:border-primary/50 hover:bg-primary/5 sm:h-64', isDragging ? 'border-primary bg-primary/10' : '', disabled ? 'pointer-events-none opacity-60' : '', className), onDragEnter: (event) => onDrag(event, true), onDragLeave: (event) => onDrag(event, false), onDragOver: (event) => onDrag(event, true), onDrop: (event) => {
41
- onDrag(event, false);
42
- select(event.dataTransfer.files);
43
- }, children: [_jsxs("div", { className: 'pointer-events-none flex flex-col items-center gap-3 text-center', children: [_jsx(PsIcon, { className: 'h-8 w-8 text-primary', iconKey: PsIconKey.Upload }), _jsxs("div", { children: [_jsx("p", { className: 'text-body-2 font-medium text-ink sm:text-body-1', children: isDragging ? (text.dropActive) : (_jsxs(_Fragment, { children: [_jsx("span", { className: touchLabel === undefined ? '' : '[@media(pointer:coarse)]:hidden', children: dropLabel }), touchLabel === undefined ? null : (_jsx("span", { className: 'hidden [@media(pointer:coarse)]:inline', children: touchLabel }))] })) }), _jsxs("p", { className: 'mt-1 text-body-3 text-ink/60 sm:text-body-2', children: [_jsx("span", { className: '[@media(pointer:coarse)]:hidden', children: text.browse }), _jsx("span", { className: 'hidden [@media(pointer:coarse)]:inline', children: text.browseTouch })] }), maxSizeLabel ? _jsx("p", { className: 'mt-3 text-body-3 text-ink/60', children: maxSizeLabel }) : null, _jsxs("p", { className: 'text-body-3 text-ink/60', children: [text.formats, " ", acceptLabel] })] })] }), _jsx("input", { accept: acceptAttr, "aria-disabled": disabled, "aria-label": dropLabel, className: 'absolute inset-0 h-full w-full cursor-pointer opacity-0', disabled: disabled, multiple: multiple, onChange: onChange, type: 'file' })] }));
60
+ return (_jsxs("div", { className: PallasoftClassNameService.merge('flex w-full flex-col gap-3', className), children: [_jsxs("div", { "aria-disabled": disabled || undefined, className: PallasoftClassNameService.merge('flex w-full cursor-pointer flex-col items-center gap-5 rounded-ps-control border-2 border-dashed px-5 py-8 text-center transition-colors duration-200 sm:px-8', isDragging
61
+ ? 'border-primary bg-primary/5'
62
+ : 'border-line bg-surface/40 hover:border-primary/40 hover:bg-primary/[0.03]', disabled ? 'pointer-events-none opacity-60' : ''), onClick: browse, onDragEnter: (event) => onDrag(event, true), onDragLeave: (event) => onDrag(event, false), onDragOver: (event) => onDrag(event, true), onDrop: (event) => {
63
+ onDrag(event, false);
64
+ select(event.dataTransfer.files);
65
+ }, children: [_jsx("span", { "aria-hidden": 'true', className: PallasoftClassNameService.merge('flex size-12 items-center justify-center rounded-full transition-colors duration-200', isDragging
66
+ ? 'bg-primary text-[color:var(--ps-color-primary-contrast)]'
67
+ : 'bg-primary-soft text-primary'), children: _jsx(PsIcon, { className: 'size-6', iconKey: PsIconKey.Upload }) }), _jsxs("div", { className: 'flex flex-col gap-1', children: [_jsx("p", { className: 'text-base font-semibold text-ink', children: isDragging ? (text.dropActive) : (_jsxs(_Fragment, { children: [_jsx("span", { className: touchLabel === undefined ? '' : '[@media(pointer:coarse)]:hidden', children: dropLabel }), touchLabel === undefined ? null : (_jsx("span", { className: 'hidden [@media(pointer:coarse)]:inline', children: touchLabel }))] })) }), _jsx("p", { className: 'text-sm text-ink/60 [@media(pointer:coarse)]:hidden', children: multiple ? text.dragHint : text.dragHintOne })] }), _jsx(PsFilePickerActions, { chooseLabel: chooseLabel, className: 'flex [@media(pointer:coarse)]:hidden', disabled: disabled, isTouch: false, onChoose: browse, onScan: openScan, scanLabel: scanLabel }), _jsx(PsFilePickerActions, { chooseLabel: chooseLabel, className: 'hidden [@media(pointer:coarse)]:flex', disabled: disabled, isTouch: true, onChoose: browse, onScan: openScan, scanLabel: scanLabel }), details.length > 0 ? _jsx("p", { className: 'text-xs text-ink/50', children: details.join(' · ') }) : null, _jsx("input", { accept: acceptAttr, "aria-hidden": 'true', className: 'sr-only', disabled: disabled, multiple: multiple, onChange: onChange, onClick: (event) => event.stopPropagation(), ref: input, tabIndex: -1, type: 'file' })] }), queue === undefined || queue.items.length === 0 ? null : (_jsx("ul", { "aria-label": rowLabels.title, className: 'flex flex-col gap-2', children: queue.items.map((item) => (_jsx(PsUploadRow, { item: item, labels: rowLabels, onCancel: () => queue.cancel(item.id), onRemove: () => queue.remove(item.id), onRetry: () => queue.retry(item.id) }, item.id))) })), scan === undefined ? null : (_jsx(PsDocumentScanner, { ...scan, fileName: scanName === null ? PsScanConfig.defaultFileName : scanName, isOpen: scanName !== null, onClose: () => setScanName(null), onComplete: select }))] }));
44
68
  };
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsSpinner } from '@pallasoft/ps-core-components';
3
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
+ import { PsScanChromeClasses } from '../constants/ps-scan-chrome-classes';
5
+ export const PsScanButton = ({ children, className, disabled = false, isLarge = false, isLoading = false, isPrimary = false, leadingIcon, loadingLabel, onClick, }) => (_jsxs("button", { "aria-busy": isLoading || undefined, className: PallasoftClassNameService.merge(PsScanChromeClasses.button, isLarge ? PsScanChromeClasses.buttonLarge : '', isPrimary ? PsScanChromeClasses.buttonPrimary : PsScanChromeClasses.buttonQuiet, 'relative', className), disabled: disabled || isLoading, onClick: onClick, type: 'button', children: [isLoading ? (_jsx(PsSpinner, { className: 'absolute inset-0 m-auto', color: 'current', label: loadingLabel === undefined ? '' : loadingLabel, size: 'small' })) : null, _jsxs("span", { "aria-hidden": isLoading || undefined, className: `inline-flex min-w-0 items-center gap-2 ${isLoading ? 'invisible' : ''}`, children: [leadingIcon === undefined ? null : _jsx("span", { className: 'shrink-0', children: leadingIcon }), _jsx("span", { className: 'truncate', children: children })] })] }));
@@ -1,23 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef } from 'react';
3
- import { PsButton, PsSpinner } from '@pallasoft/ps-core-components';
4
- import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
- import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
2
+ import { useState } from 'react';
3
+ import { PsSpinner, usePsElementSize } from '@pallasoft/ps-core-components';
4
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
+ import { PsIconKey } from '@pallasoft/ps-icons';
6
+ import { PsScanChromeClasses } from '../constants/ps-scan-chrome-classes';
7
+ import { PsScanTemplateService } from '../services/ps-scan-template-service';
8
+ import { PsScanButton } from './ps-scan-button';
6
9
  import { PsScanIconButton } from './ps-scan-icon-button';
7
- export const PsScanCameraView = ({ canGoBack, canTorch, isTorchOn, labels, onBack, onCapture, onFiles, onToggleTorch, status, videoRef, }) => {
8
- const captureInput = useRef(null);
9
- const libraryInput = useRef(null);
10
- const isReady = status === PsScanCameraStatus.Ready;
11
- const message = status === PsScanCameraStatus.Denied
12
- ? labels.cameraDenied
13
- : status === PsScanCameraStatus.Unavailable
14
- ? labels.cameraUnavailable
15
- : '';
16
- const onChange = (event) => {
17
- const files = Array.from(event.target.files === null ? [] : event.target.files);
18
- event.target.value = '';
19
- if (files.length > 0)
20
- onFiles(files);
10
+ import { PsScanPageStack } from './ps-scan-page-stack';
11
+ import { PsScanPhotoButton } from './ps-scan-photo-button';
12
+ import { PsScanShutter } from './ps-scan-shutter';
13
+ import { PsScanViewfinderOverlay } from './ps-scan-viewfinder-overlay';
14
+ export const PsScanCameraView = ({ canTorch, detection, flashKey, isAuto, isBusy, isFull, isMultiple, isReady, isTorchOn, labels, lastPreview, onCapture, onClose, onDone, onFiles, onToggleAuto, onToggleTorch, pageCount, videoRef, videoTarget, }) => {
15
+ const [areaRef, area] = usePsElementSize();
16
+ const [video, setVideo] = useState({ height: 0, width: 0 });
17
+ const hint = isFull
18
+ ? labels.maxPages
19
+ : detection.quad === null
20
+ ? labels.hintFind
21
+ : isAuto
22
+ ? labels.hintHold
23
+ : labels.hintFound;
24
+ const measure = () => {
25
+ const element = videoRef.current;
26
+ if (element !== null)
27
+ setVideo({ height: element.videoHeight, width: element.videoWidth });
21
28
  };
22
- return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-slate-950 text-white', children: [_jsxs("div", { className: 'relative min-h-0 flex-1', children: [_jsx("video", { "aria-label": labels.title, autoPlay: true, className: `size-full object-contain ${isReady ? '' : 'invisible'}`, muted: true, playsInline: true, ref: videoRef }), status === PsScanCameraStatus.Starting ? (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/80', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })) : null, message === '' ? null : (_jsx("div", { className: 'absolute inset-0 flex items-center justify-center p-6 text-center text-sm text-white/85', role: 'status', children: _jsx("p", { className: 'max-w-sm', children: message }) }))] }), _jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-center gap-3 px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [canGoBack ? (_jsx(PsButton, { onClick: onBack, variant: 'surface', children: labels.backToPages })) : null, _jsx(PsButton, { leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: () => libraryInput.current?.click(), variant: 'surface', children: labels.choosePhotos }), isReady ? (_jsx("button", { "aria-label": labels.capture, className: 'ps-focus-ring flex size-[4.5rem] shrink-0 items-center justify-center rounded-full border-4 border-white bg-white/20 transition active:scale-95', onClick: onCapture, title: labels.capture, type: 'button', children: _jsx("span", { className: 'size-12 rounded-full bg-white' }) })) : (_jsx(PsButton, { onClick: () => captureInput.current?.click(), variant: 'primary', children: labels.takePhoto })), canTorch && isReady ? (_jsx(PsScanIconButton, { isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Spark }) })) : null] }), _jsx("input", { accept: 'image/*', capture: 'environment', className: 'sr-only', onChange: onChange, ref: captureInput, tabIndex: -1, type: 'file' }), _jsx("input", { accept: 'image/*', className: 'sr-only', multiple: true, onChange: onChange, ref: libraryInput, tabIndex: -1, type: 'file' })] }));
29
+ return (_jsxs("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [_jsxs("div", { className: 'absolute inset-0', ref: areaRef, children: [_jsx("video", { "aria-hidden": 'true', autoPlay: true, className: PallasoftClassNameService.merge('size-full object-cover transition-opacity duration-300', isReady ? 'opacity-100' : 'opacity-0'), muted: true, onLoadedMetadata: measure, onResize: measure, playsInline: true, ref: videoTarget }), isReady ? (_jsx(PsScanViewfinderOverlay, { area: area, quad: detection.quad, video: video })) : null] }), isReady ? null : (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })), flashKey > 0 ? (_jsx("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, _jsxs("div", { className: 'absolute inset-x-0 top-0 flex items-start justify-between gap-3 bg-gradient-to-b from-black/60 to-transparent px-4 pb-10 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }), _jsxs("div", { className: 'flex items-center gap-2', children: [canTorch && isReady ? (_jsx(PsScanIconButton, { iconKey: PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null, _jsx("button", { "aria-label": labels.autoCapture, "aria-pressed": isAuto, className: PallasoftClassNameService.merge(PsScanChromeClasses.button, 'border-transparent', isAuto ? PsScanChromeClasses.iconOn : PsScanChromeClasses.iconOff), onClick: onToggleAuto, title: labels.autoCapture, type: 'button', children: labels.auto })] })] }), isReady ? (_jsx("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: _jsx("p", { "aria-live": 'polite', className: PsScanChromeClasses.pill, children: hint }) })) : null, _jsxs("div", { className: 'absolute inset-x-0 bottom-0 grid grid-cols-[1fr_auto_1fr] items-center gap-4 bg-gradient-to-t from-black/80 to-transparent px-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-12', children: [_jsx("div", { className: 'flex justify-start', children: pageCount > 0 ? (_jsx(PsScanPageStack, { count: pageCount, label: PsScanTemplateService.fill(labels.viewPages, { count: pageCount }), onOpen: onDone, preview: lastPreview })) : (_jsx(PsScanPhotoButton, { isIconOnly: true, isMultiple: isMultiple, label: labels.choosePhotos, onFiles: onFiles })) }), _jsx(PsScanShutter, { disabled: !isReady || isFull || isBusy, label: labels.capture, onCapture: onCapture, progress: detection.progress }), _jsx("div", { className: 'flex justify-end', children: pageCount > 0 ? (_jsx(PsScanButton, { isPrimary: true, onClick: onDone, children: labels.done })) : null })] })] }));
23
30
  };