@pallasoft/ps-file-components 1.3.0 → 1.4.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 (154) hide show
  1. package/dist/components/ps-document-scanner-loader.d.ts +1 -0
  2. package/dist/components/ps-document-scanner-loader.js +40 -0
  3. package/dist/components/ps-document-scanner-sheet.d.ts +2 -0
  4. package/dist/components/ps-document-scanner-sheet.js +138 -0
  5. package/dist/components/ps-document-scanner.d.ts +2 -0
  6. package/dist/components/ps-document-scanner.js +17 -0
  7. package/dist/components/ps-file-viewer-pane.d.ts +2 -0
  8. package/dist/components/ps-file-viewer-pane.js +39 -0
  9. package/dist/components/ps-scan-camera-view.d.ts +17 -0
  10. package/dist/components/ps-scan-camera-view.js +27 -0
  11. package/dist/components/ps-scan-crop-editor.d.ts +13 -0
  12. package/dist/components/ps-scan-crop-editor.js +53 -0
  13. package/dist/components/ps-scan-icon-button.d.ts +10 -0
  14. package/dist/components/ps-scan-icon-button.js +7 -0
  15. package/dist/components/ps-scan-page-list.d.ts +12 -0
  16. package/dist/components/ps-scan-page-list.js +23 -0
  17. package/dist/components/ps-scan-review.d.ts +23 -0
  18. package/dist/components/ps-scan-review.js +20 -0
  19. package/dist/constants/ps-document-scanner-labels.d.ts +50 -0
  20. package/dist/constants/ps-document-scanner-labels.js +53 -0
  21. package/dist/constants/ps-scan-camera-status.d.ts +7 -0
  22. package/dist/constants/ps-scan-camera-status.js +9 -0
  23. package/dist/constants/ps-scan-config.d.ts +31 -0
  24. package/dist/constants/ps-scan-config.js +34 -0
  25. package/dist/constants/ps-scan-corner-order.d.ts +1 -0
  26. package/dist/constants/ps-scan-corner-order.js +4 -0
  27. package/dist/constants/ps-scan-filter.d.ts +7 -0
  28. package/dist/constants/ps-scan-filter.js +9 -0
  29. package/dist/constants/ps-scan-output-format.d.ts +5 -0
  30. package/dist/constants/ps-scan-output-format.js +7 -0
  31. package/dist/constants/ps-scan-step.d.ts +6 -0
  32. package/dist/constants/ps-scan-step.js +8 -0
  33. package/dist/esm/components/ps-document-scanner-loader.js +4 -0
  34. package/dist/esm/components/ps-document-scanner-sheet.js +134 -0
  35. package/dist/esm/components/ps-document-scanner.js +13 -0
  36. package/dist/esm/components/ps-file-viewer-pane.js +35 -0
  37. package/dist/esm/components/ps-scan-camera-view.js +23 -0
  38. package/dist/esm/components/ps-scan-crop-editor.js +49 -0
  39. package/dist/esm/components/ps-scan-icon-button.js +3 -0
  40. package/dist/esm/components/ps-scan-page-list.js +19 -0
  41. package/dist/esm/components/ps-scan-review.js +16 -0
  42. package/dist/esm/constants/ps-document-scanner-labels.js +50 -0
  43. package/dist/esm/constants/ps-scan-camera-status.js +6 -0
  44. package/dist/esm/constants/ps-scan-config.js +31 -0
  45. package/dist/esm/constants/ps-scan-corner-order.js +1 -0
  46. package/dist/esm/constants/ps-scan-filter.js +6 -0
  47. package/dist/esm/constants/ps-scan-output-format.js +4 -0
  48. package/dist/esm/constants/ps-scan-step.js +5 -0
  49. package/dist/esm/hooks/use-ps-focus-trap.js +45 -0
  50. package/dist/esm/hooks/use-ps-scan-camera.js +62 -0
  51. package/dist/esm/hooks/use-ps-scan-pages.js +62 -0
  52. package/dist/esm/hooks/use-ps-scan-previews.js +57 -0
  53. package/dist/esm/index.js +7 -0
  54. package/dist/esm/models/ps-document-scanner-props.js +1 -0
  55. package/dist/esm/models/ps-document-scanner-sheet-props.js +1 -0
  56. package/dist/esm/models/ps-file-viewer-pane-props.js +1 -0
  57. package/dist/esm/models/ps-scan-added-page.js +1 -0
  58. package/dist/esm/models/ps-scan-camera-session.js +1 -0
  59. package/dist/esm/models/ps-scan-decoded-image.js +1 -0
  60. package/dist/esm/models/ps-scan-levels.js +1 -0
  61. package/dist/esm/models/ps-scan-output-options.js +1 -0
  62. package/dist/esm/models/ps-scan-page.js +1 -0
  63. package/dist/esm/models/ps-scan-pdf-page.js +1 -0
  64. package/dist/esm/models/ps-scan-prepared.js +1 -0
  65. package/dist/esm/models/ps-scan-quad.js +1 -0
  66. package/dist/esm/models/ps-scan-raster.js +1 -0
  67. package/dist/esm/models/ps-scan-rendered-page.js +1 -0
  68. package/dist/esm/services/ps-scan-camera-service.js +50 -0
  69. package/dist/esm/services/ps-scan-canvas-service.js +46 -0
  70. package/dist/esm/services/ps-scan-edge-service.js +156 -0
  71. package/dist/esm/services/ps-scan-enhance-service.js +97 -0
  72. package/dist/esm/services/ps-scan-file-name-service.js +25 -0
  73. package/dist/esm/services/ps-scan-geometry-service.js +96 -0
  74. package/dist/esm/services/ps-scan-homography-service.js +46 -0
  75. package/dist/esm/services/ps-scan-hull-service.js +59 -0
  76. package/dist/esm/services/ps-scan-image-filter-service.js +52 -0
  77. package/dist/esm/services/ps-scan-order-service.js +13 -0
  78. package/dist/esm/services/ps-scan-output-service.js +24 -0
  79. package/dist/esm/services/ps-scan-pdf-service.js +56 -0
  80. package/dist/esm/services/ps-scan-pipeline-service.js +40 -0
  81. package/dist/esm/services/ps-scan-raster-service.js +67 -0
  82. package/dist/esm/services/ps-scan-template-service.js +5 -0
  83. package/dist/esm/services/ps-scan-warp-service.js +35 -0
  84. package/dist/hooks/use-ps-focus-trap.d.ts +2 -0
  85. package/dist/hooks/use-ps-focus-trap.js +49 -0
  86. package/dist/hooks/use-ps-scan-camera.d.ts +9 -0
  87. package/dist/hooks/use-ps-scan-camera.js +66 -0
  88. package/dist/hooks/use-ps-scan-pages.d.ts +12 -0
  89. package/dist/hooks/use-ps-scan-pages.js +66 -0
  90. package/dist/hooks/use-ps-scan-previews.d.ts +2 -0
  91. package/dist/hooks/use-ps-scan-previews.js +61 -0
  92. package/dist/index.d.ts +7 -0
  93. package/dist/index.js +7 -0
  94. package/dist/models/ps-document-scanner-props.d.ts +15 -0
  95. package/dist/models/ps-document-scanner-props.js +2 -0
  96. package/dist/models/ps-document-scanner-sheet-props.d.ts +13 -0
  97. package/dist/models/ps-document-scanner-sheet-props.js +2 -0
  98. package/dist/models/ps-file-viewer-pane-props.d.ts +12 -0
  99. package/dist/models/ps-file-viewer-pane-props.js +2 -0
  100. package/dist/models/ps-scan-added-page.d.ts +5 -0
  101. package/dist/models/ps-scan-added-page.js +2 -0
  102. package/dist/models/ps-scan-camera-session.d.ts +5 -0
  103. package/dist/models/ps-scan-camera-session.js +2 -0
  104. package/dist/models/ps-scan-decoded-image.d.ts +6 -0
  105. package/dist/models/ps-scan-decoded-image.js +2 -0
  106. package/dist/models/ps-scan-levels.d.ts +4 -0
  107. package/dist/models/ps-scan-levels.js +2 -0
  108. package/dist/models/ps-scan-output-options.d.ts +7 -0
  109. package/dist/models/ps-scan-output-options.js +2 -0
  110. package/dist/models/ps-scan-page.d.ts +12 -0
  111. package/dist/models/ps-scan-page.js +2 -0
  112. package/dist/models/ps-scan-pdf-page.d.ts +5 -0
  113. package/dist/models/ps-scan-pdf-page.js +2 -0
  114. package/dist/models/ps-scan-prepared.d.ts +7 -0
  115. package/dist/models/ps-scan-prepared.js +2 -0
  116. package/dist/models/ps-scan-quad.d.ts +2 -0
  117. package/dist/models/ps-scan-quad.js +2 -0
  118. package/dist/models/ps-scan-raster.d.ts +5 -0
  119. package/dist/models/ps-scan-raster.js +2 -0
  120. package/dist/models/ps-scan-rendered-page.d.ts +5 -0
  121. package/dist/models/ps-scan-rendered-page.js +2 -0
  122. package/dist/services/ps-scan-camera-service.d.ts +10 -0
  123. package/dist/services/ps-scan-camera-service.js +54 -0
  124. package/dist/services/ps-scan-canvas-service.d.ts +8 -0
  125. package/dist/services/ps-scan-canvas-service.js +50 -0
  126. package/dist/services/ps-scan-edge-service.d.ts +9 -0
  127. package/dist/services/ps-scan-edge-service.js +160 -0
  128. package/dist/services/ps-scan-enhance-service.d.ts +9 -0
  129. package/dist/services/ps-scan-enhance-service.js +101 -0
  130. package/dist/services/ps-scan-file-name-service.d.ts +6 -0
  131. package/dist/services/ps-scan-file-name-service.js +29 -0
  132. package/dist/services/ps-scan-geometry-service.d.ts +18 -0
  133. package/dist/services/ps-scan-geometry-service.js +100 -0
  134. package/dist/services/ps-scan-homography-service.d.ts +7 -0
  135. package/dist/services/ps-scan-homography-service.js +50 -0
  136. package/dist/services/ps-scan-hull-service.d.ts +6 -0
  137. package/dist/services/ps-scan-hull-service.js +63 -0
  138. package/dist/services/ps-scan-image-filter-service.d.ts +6 -0
  139. package/dist/services/ps-scan-image-filter-service.js +56 -0
  140. package/dist/services/ps-scan-order-service.d.ts +4 -0
  141. package/dist/services/ps-scan-order-service.js +17 -0
  142. package/dist/services/ps-scan-output-service.d.ts +5 -0
  143. package/dist/services/ps-scan-output-service.js +28 -0
  144. package/dist/services/ps-scan-pdf-service.d.ts +5 -0
  145. package/dist/services/ps-scan-pdf-service.js +60 -0
  146. package/dist/services/ps-scan-pipeline-service.d.ts +10 -0
  147. package/dist/services/ps-scan-pipeline-service.js +44 -0
  148. package/dist/services/ps-scan-raster-service.d.ts +6 -0
  149. package/dist/services/ps-scan-raster-service.js +71 -0
  150. package/dist/services/ps-scan-template-service.d.ts +3 -0
  151. package/dist/services/ps-scan-template-service.js +9 -0
  152. package/dist/services/ps-scan-warp-service.d.ts +6 -0
  153. package/dist/services/ps-scan-warp-service.js +39 -0
  154. package/package.json +9 -6
@@ -0,0 +1,31 @@
1
+ export declare const PsScanConfig: {
2
+ readonly adaptiveRatio: 0.92;
3
+ readonly adaptiveWindowDivisor: 16;
4
+ readonly adaptiveWindowMin: 15;
5
+ readonly capturedJpegQuality: 0.92;
6
+ readonly cornerNudge: 0.005;
7
+ readonly cornerNudgeLarge: 0.025;
8
+ readonly decodeMaxDimension: 3000;
9
+ readonly defaultFileName: "scan";
10
+ readonly defaultJpegQuality: 0.82;
11
+ readonly defaultMaxDimension: 2000;
12
+ readonly defaultMaxPages: 30;
13
+ readonly detectDilations: 2;
14
+ readonly detectMaxDimension: 320;
15
+ readonly detectMinAreaRatio: 0.12;
16
+ readonly detectMaxAreaRatio: 0.995;
17
+ readonly detectMinEdgeStrength: 24;
18
+ readonly detectMinHullFill: 0.85;
19
+ readonly detectPercentile: 0.98;
20
+ readonly detectRelativeStrength: 0.3;
21
+ readonly hullCandidateLimit: 18;
22
+ readonly levelHighPercentile: 0.99;
23
+ readonly levelLowPercentile: 0.01;
24
+ readonly minCornerGap: 0.02;
25
+ readonly pdfDpi: 150;
26
+ readonly pdfPointsPerInch: 72;
27
+ readonly previewDebounceMs: 120;
28
+ readonly previewMaxDimension: 1400;
29
+ readonly thumbnailHeight: 56;
30
+ readonly torchConstraint: "torch";
31
+ };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanConfig = void 0;
4
+ exports.PsScanConfig = {
5
+ adaptiveRatio: 0.92,
6
+ adaptiveWindowDivisor: 16,
7
+ adaptiveWindowMin: 15,
8
+ capturedJpegQuality: 0.92,
9
+ cornerNudge: 0.005,
10
+ cornerNudgeLarge: 0.025,
11
+ decodeMaxDimension: 3000,
12
+ defaultFileName: 'scan',
13
+ defaultJpegQuality: 0.82,
14
+ defaultMaxDimension: 2000,
15
+ defaultMaxPages: 30,
16
+ detectDilations: 2,
17
+ detectMaxDimension: 320,
18
+ detectMinAreaRatio: 0.12,
19
+ detectMaxAreaRatio: 0.995,
20
+ detectMinEdgeStrength: 24,
21
+ detectMinHullFill: 0.85,
22
+ detectPercentile: 0.98,
23
+ detectRelativeStrength: 0.3,
24
+ hullCandidateLimit: 18,
25
+ levelHighPercentile: 0.99,
26
+ levelLowPercentile: 0.01,
27
+ minCornerGap: 0.02,
28
+ pdfDpi: 150,
29
+ pdfPointsPerInch: 72,
30
+ previewDebounceMs: 120,
31
+ previewMaxDimension: 1400,
32
+ thumbnailHeight: 56,
33
+ torchConstraint: 'torch',
34
+ };
@@ -0,0 +1 @@
1
+ export declare const PsScanCornerOrder: readonly ["topLeft", "topRight", "bottomRight", "bottomLeft"];
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanCornerOrder = void 0;
4
+ exports.PsScanCornerOrder = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'];
@@ -0,0 +1,7 @@
1
+ export declare const PsScanFilter: {
2
+ readonly Colour: "colour";
3
+ readonly Document: "document";
4
+ readonly Greyscale: "greyscale";
5
+ readonly Original: "original";
6
+ };
7
+ export type PsScanFilter = (typeof PsScanFilter)[keyof typeof PsScanFilter];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanFilter = void 0;
4
+ exports.PsScanFilter = {
5
+ Colour: 'colour',
6
+ Document: 'document',
7
+ Greyscale: 'greyscale',
8
+ Original: 'original',
9
+ };
@@ -0,0 +1,5 @@
1
+ export declare const PsScanOutputFormat: {
2
+ readonly Jpeg: "jpeg";
3
+ readonly Pdf: "pdf";
4
+ };
5
+ export type PsScanOutputFormat = (typeof PsScanOutputFormat)[keyof typeof PsScanOutputFormat];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanOutputFormat = void 0;
4
+ exports.PsScanOutputFormat = {
5
+ Jpeg: 'jpeg',
6
+ Pdf: 'pdf',
7
+ };
@@ -0,0 +1,6 @@
1
+ export declare const PsScanStep: {
2
+ readonly Camera: "camera";
3
+ readonly Crop: "crop";
4
+ readonly Review: "review";
5
+ };
6
+ export type PsScanStep = (typeof PsScanStep)[keyof typeof PsScanStep];
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanStep = void 0;
4
+ exports.PsScanStep = {
5
+ Camera: 'camera',
6
+ Crop: 'crop',
7
+ Review: 'review',
8
+ };
@@ -0,0 +1,4 @@
1
+ import { lazy } from 'react';
2
+ export const PsDocumentScannerLoader = lazy(() => import('./ps-document-scanner-sheet').then((module) => ({
3
+ default: module.PsDocumentScannerSheet,
4
+ })));
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useRef, useState } from 'react';
3
+ import { PsLayer, usePsLayerZIndex, usePsRestoreFocus, usePsScrollLock, usePsViewport, } from '@pallasoft/ps-core-components';
4
+ import { PallasoftViewportTier, PallasoftZIndex } from '@pallasoft/ps-design-system';
5
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
6
+ import { PsModal } from '@pallasoft/ps-popovers';
7
+ import { createPortal } from 'react-dom';
8
+ import { PsScanStep } from '../constants/ps-scan-step';
9
+ import { usePsFocusTrap } from '../hooks/use-ps-focus-trap';
10
+ import { usePsScanCamera } from '../hooks/use-ps-scan-camera';
11
+ import { usePsScanPages } from '../hooks/use-ps-scan-pages';
12
+ import { usePsScanPreviews } from '../hooks/use-ps-scan-previews';
13
+ import { PsScanOutputService } from '../services/ps-scan-output-service';
14
+ import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
15
+ import { PsScanTemplateService } from '../services/ps-scan-template-service';
16
+ import { PsScanCameraView } from './ps-scan-camera-view';
17
+ import { PsScanCropEditor } from './ps-scan-crop-editor';
18
+ import { PsScanIconButton } from './ps-scan-icon-button';
19
+ import { PsScanReview } from './ps-scan-review';
20
+ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, maxPages, onClose, onComplete, outputFormat, }) => {
21
+ const tier = usePsViewport();
22
+ const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
23
+ const content = useRef(null);
24
+ const { add, move, pages, remove, replace, setCorners, update } = usePsScanPages();
25
+ const [step, setStep] = useState(PsScanStep.Camera);
26
+ const [activeId, setActiveId] = useState('');
27
+ const [retakeId, setRetakeId] = useState('');
28
+ const [announcement, setAnnouncement] = useState('');
29
+ const [error, setError] = useState('');
30
+ const [isBusy, setIsBusy] = useState(false);
31
+ const camera = usePsScanCamera(step === PsScanStep.Camera);
32
+ const previews = usePsScanPreviews(pages);
33
+ const page = pages.find((item) => item.id === activeId);
34
+ const isDesktop = tier === PallasoftViewportTier.Desktop;
35
+ usePsScrollLock(!isDesktop);
36
+ usePsRestoreFocus(true);
37
+ usePsFocusTrap(content, true, onClose);
38
+ const edgeMessage = (isDetected) => isDetected ? labels.edgesFound : labels.edgesNotFound;
39
+ const addFiles = async (files) => {
40
+ setError('');
41
+ try {
42
+ const images = files.filter((file) => file.type === '' || file.type.startsWith('image/'));
43
+ if (retakeId !== '' && images.length > 0) {
44
+ const added = await replace(retakeId, images[0]);
45
+ setActiveId(retakeId);
46
+ setRetakeId('');
47
+ setStep(PsScanStep.Crop);
48
+ setAnnouncement(edgeMessage(added.isDetected));
49
+ return;
50
+ }
51
+ const room = Math.max(0, maxPages - pages.length);
52
+ if (images.length > room)
53
+ setError(labels.maxPages);
54
+ const added = await add(images.slice(0, room));
55
+ if (added.length === 0)
56
+ 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 }));
62
+ }
63
+ catch {
64
+ setError(labels.processFailed);
65
+ }
66
+ };
67
+ const capture = async () => {
68
+ const blob = await camera.capture();
69
+ if (blob !== null)
70
+ await addFiles([blob]);
71
+ };
72
+ const autoDetect = async () => {
73
+ if (page === undefined)
74
+ return;
75
+ const prepared = await PsScanPipelineService.prepare(page.source);
76
+ setCorners(page.id, prepared.corners);
77
+ setAnnouncement(edgeMessage(prepared.isDetected));
78
+ };
79
+ const retake = () => {
80
+ if (page === undefined)
81
+ return;
82
+ setRetakeId(page.id);
83
+ setStep(PsScanStep.Camera);
84
+ };
85
+ const deletePage = () => {
86
+ if (page === undefined)
87
+ return;
88
+ const index = pages.findIndex((item) => item.id === page.id);
89
+ const rest = pages.filter((item) => item.id !== page.id);
90
+ remove(page.id);
91
+ setAnnouncement(labels.pageDeleted);
92
+ if (rest.length === 0) {
93
+ setActiveId('');
94
+ setStep(PsScanStep.Camera);
95
+ }
96
+ else
97
+ setActiveId(rest[Math.min(index, rest.length - 1)].id);
98
+ };
99
+ const finish = async () => {
100
+ setIsBusy(true);
101
+ setError('');
102
+ try {
103
+ const files = await PsScanOutputService.build(pages, {
104
+ fileName,
105
+ format: outputFormat,
106
+ jpegQuality,
107
+ maxDimension,
108
+ });
109
+ onComplete(files);
110
+ onClose();
111
+ }
112
+ catch {
113
+ setError(labels.processFailed);
114
+ setIsBusy(false);
115
+ }
116
+ };
117
+ const moveTo = useCallback((from, to) => {
118
+ move(from, to);
119
+ setAnnouncement(PsScanTemplateService.fill(labels.pageMoved, {
120
+ count: pages.length,
121
+ page: `${labels.page} ${from + 1}`,
122
+ position: to + 1,
123
+ }));
124
+ }, [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 }) }));
131
+ if (typeof document === 'undefined')
132
+ 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);
134
+ };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense } from 'react';
3
+ import { usePsHydrated } from '@pallasoft/ps-core-components';
4
+ import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
5
+ import { PsScanConfig } from '../constants/ps-scan-config';
6
+ import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
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, }) => {
9
+ const isHydrated = usePsHydrated();
10
+ if (!isOpen || !isHydrated)
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 }) }));
13
+ };
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
5
+ import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
6
+ import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
7
+ import { PsFileTypeService } from '../services/ps-file-type-service';
8
+ import { PsFileViewerButton } from './ps-file-viewer-button';
9
+ import { PsFileViewerContent } from './ps-file-viewer-content';
10
+ export const PsFileViewerPane = ({ contentType, headerActions, id, labels = PsFileViewerLabels, name, pdfWorkerSrc, url, }) => {
11
+ const [zoom, setZoom] = useState(1);
12
+ const [rotation, setRotation] = useState(0);
13
+ const kind = PsFileTypeService.kindOf(contentType, name);
14
+ const canZoom = PsFileViewerKinds.zoomable.includes(kind);
15
+ const canRotate = PsFileViewerKinds.rotatable.includes(kind);
16
+ const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
17
+ const fileKey = id === undefined ? url : id;
18
+ const rotate = () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360);
19
+ const onKeyDown = (event) => {
20
+ if (event.metaKey || event.ctrlKey || event.altKey)
21
+ return;
22
+ if (canZoom && (event.key === '+' || event.key === '='))
23
+ changeZoom(PsFileViewerConfig.zoomStep);
24
+ else if (canZoom && (event.key === '-' || event.key === '_'))
25
+ changeZoom(-PsFileViewerConfig.zoomStep);
26
+ else if (canZoom && event.key === '0')
27
+ setZoom(1);
28
+ else if (canRotate && event.key.toLowerCase() === 'r')
29
+ rotate();
30
+ else
31
+ return;
32
+ event.preventDefault();
33
+ };
34
+ return (_jsxs("div", { className: 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden rounded-ps-panel bg-black/85', children: [_jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-between gap-x-2 border-b border-white/10 px-2 py-1 text-white', children: [_jsx("p", { className: 'min-w-0 flex-1 basis-32 truncate px-1 text-xs text-white/70', title: name, children: name }), _jsxs("div", { className: 'flex items-center gap-0.5', role: 'toolbar', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { disabled: zoom <= PsFileViewerConfig.minZoom, label: labels.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: labels.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { disabled: zoom >= PsFileViewerConfig.maxZoom, label: labels.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: labels.rotate, onClick: rotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Rotate }) })) : null, headerActions] })] }), _jsx("div", { className: 'ps-focus-ring flex min-h-0 flex-1 p-2', onKeyDown: onKeyDown, tabIndex: 0, children: _jsx(PsFileViewerContent, { activeMatch: 0, file: { contentType, id: fileKey, name, url }, labels: labels, onMatchCount: () => undefined, pdfWorkerSrc: pdfWorkerSrc, query: '', rotation: rotation, zoom: zoom }, fileKey) })] }));
35
+ };
@@ -0,0 +1,23 @@
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';
6
+ 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);
21
+ };
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' })] }));
23
+ };
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { PsButton, usePsElementSize } from '@pallasoft/ps-core-components';
4
+ import { PsScanConfig } from '../constants/ps-scan-config';
5
+ import { PsScanCornerOrder } from '../constants/ps-scan-corner-order';
6
+ import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
7
+ export const PsScanCropEditor = ({ labels, onApply, onAutoDetect, onChange, onRetake, page, }) => {
8
+ const [areaRef, area] = usePsElementSize();
9
+ const stageRef = useRef(null);
10
+ const hintId = 'ps-scan-crop-hint';
11
+ const size = PsScanGeometryService.fit(area, { height: page.height, width: page.width });
12
+ const cornerLabels = [
13
+ labels.cornerTopLeft,
14
+ labels.cornerTopRight,
15
+ labels.cornerBottomRight,
16
+ labels.cornerBottomLeft,
17
+ ];
18
+ const move = (index, event) => {
19
+ const stage = stageRef.current;
20
+ if (stage === null || !event.currentTarget.hasPointerCapture(event.pointerId))
21
+ return;
22
+ const rect = stage.getBoundingClientRect();
23
+ const point = PsScanGeometryService.clamp({
24
+ x: (event.clientX - rect.left) / rect.width,
25
+ y: (event.clientY - rect.top) / rect.height,
26
+ });
27
+ const next = page.corners.map((corner, at) => (at === index ? point : corner));
28
+ if (PsScanGeometryService.isUsable(next))
29
+ onChange(next);
30
+ };
31
+ const nudge = (index, event) => {
32
+ const step = event.shiftKey ? PsScanConfig.cornerNudgeLarge : PsScanConfig.cornerNudge;
33
+ const deltas = {
34
+ ArrowDown: [0, step],
35
+ ArrowLeft: [-step, 0],
36
+ ArrowRight: [step, 0],
37
+ ArrowUp: [0, -step],
38
+ };
39
+ const delta = deltas[event.key];
40
+ if (delta === undefined)
41
+ return;
42
+ event.preventDefault();
43
+ onChange(PsScanGeometryService.nudge(page.corners, index, delta[0], delta[1]));
44
+ };
45
+ return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-slate-950 text-white', children: [_jsx("p", { className: 'shrink-0 px-4 pt-3 text-center text-sm text-white/80', id: hintId, children: labels.cropHint }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center p-6', ref: areaRef, children: _jsxs("div", { className: 'relative touch-none select-none', ref: stageRef, style: { height: size.height, width: size.width }, children: [_jsx("img", { alt: '', className: 'absolute inset-0 size-full', draggable: false, src: page.sourceUrl }), _jsx("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', preserveAspectRatio: 'none', viewBox: '0 0 1 1', children: _jsx("polygon", { fill: 'rgba(255,255,255,0.12)', points: page.corners.map((point) => `${point.x},${point.y}`).join(' '), stroke: 'white', strokeWidth: 2, vectorEffect: 'non-scaling-stroke' }) }), PsScanCornerOrder.map((key, index) => (_jsx("button", { "aria-describedby": hintId, "aria-label": cornerLabels[index], className: 'ps-focus-ring absolute flex size-11 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center rounded-full active:cursor-grabbing', onKeyDown: (event) => nudge(index, event), onPointerCancel: (event) => event.currentTarget.releasePointerCapture(event.pointerId), onPointerDown: (event) => event.currentTarget.setPointerCapture(event.pointerId), onPointerMove: (event) => move(index, event), onPointerUp: (event) => event.currentTarget.releasePointerCapture(event.pointerId), style: {
46
+ left: `${page.corners[index].x * 100}%`,
47
+ top: `${page.corners[index].y * 100}%`,
48
+ }, type: 'button', children: _jsx("span", { className: 'size-5 rounded-full border-2 border-white bg-primary shadow' }) }, key)))] }) }), _jsxs("div", { className: 'flex shrink-0 flex-wrap items-center justify-center gap-3 px-4 pt-2 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [_jsx(PsButton, { onClick: onRetake, variant: 'surface', children: labels.retake }), _jsx(PsButton, { onClick: onAutoDetect, variant: 'surface', children: labels.autoDetect }), _jsx(PsButton, { onClick: onApply, variant: 'primary', children: labels.cropApply })] })] }));
49
+ };
@@ -0,0 +1,3 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ export const PsScanIconButton = ({ children, className, disabled, isPressed, label, onClick, }) => (_jsx("button", { "aria-label": label, "aria-pressed": isPressed, className: PallasoftClassNameService.merge('ps-focus-ring inline-flex size-11 min-h-11 min-w-11 shrink-0 items-center justify-center rounded-ps-control border border-line bg-surface text-ink transition hover:border-primary/40 hover:text-primary disabled:pointer-events-none disabled:opacity-40', isPressed ? 'border-primary bg-primary-soft text-primary' : '', className), disabled: disabled, onClick: onClick, title: label, type: 'button', children: children }));
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsSortableList } from '@pallasoft/ps-core-components';
3
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
+ import { PsScanTemplateService } from '../services/ps-scan-template-service';
6
+ import { PsScanIconButton } from './ps-scan-icon-button';
7
+ export const PsScanPageList = ({ activeId, labels, onMove, onSelect, pages, previews, }) => {
8
+ const items = pages.map((page, index) => ({
9
+ id: page.id,
10
+ label: `${labels.page} ${index + 1}`,
11
+ }));
12
+ const say = (template, label, position) => PsScanTemplateService.fill(template, { count: pages.length, page: label, position });
13
+ return (_jsx(PsSortableList, { announcements: {
14
+ cancelled: (label) => say(labels.reorderCancelled, label, 0),
15
+ dropped: (label, position) => say(labels.reorderDropped, label, position),
16
+ lifted: (label, position) => say(labels.reorderLifted, label, position),
17
+ moved: (label, position) => say(labels.reorderMoved, label, position),
18
+ }, className: 'flex flex-col gap-2', getHandleLabel: (label) => PsScanTemplateService.fill(labels.dragPage, { page: label }), instructions: labels.reorderInstructions, items: items, onMove: (_id, from, to) => onMove(from, to), renderItem: ({ handle, index, isDragging, item }) => (_jsxs("div", { className: PallasoftClassNameService.merge('flex items-center gap-2 rounded-ps-control border bg-surface p-1.5', item.id === activeId ? 'border-primary' : 'border-line', isDragging ? 'opacity-80 shadow-lg' : ''), children: [handle, _jsxs("button", { "aria-current": item.id === activeId, "aria-label": item.label, className: 'ps-focus-ring flex min-h-11 min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onSelect(item.id), type: 'button', children: [previews[item.id] === undefined ? (_jsx("span", { className: 'ps-skeleton h-14 w-10 shrink-0 rounded' })) : (_jsx("img", { alt: '', className: 'h-14 w-10 shrink-0 rounded border border-line bg-white object-cover object-top', src: previews[item.id] })), _jsx("span", { className: 'truncate text-sm font-medium text-ink', children: item.label })] }), _jsx(PsScanIconButton, { disabled: index === 0, label: PsScanTemplateService.fill(labels.moveUp, { page: item.label }), onClick: () => onMove(index, index - 1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5 -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsScanIconButton, { disabled: index === pages.length - 1, label: PsScanTemplateService.fill(labels.moveDown, { page: item.label }), onClick: () => onMove(index, index + 1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5 rotate-90', iconKey: PsIconKey.ChevronRight }) })] })), role: 'list' }));
19
+ };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsButton } from '@pallasoft/ps-core-components';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanFilter } from '../constants/ps-scan-filter';
5
+ import { PsScanIconButton } from './ps-scan-icon-button';
6
+ import { PsScanPageList } from './ps-scan-page-list';
7
+ export const PsScanReview = ({ canAddPage, error, isBusy, labels, onAddPage, onDelete, onEditCrop, onFilter, onFinish, onMove, onRetake, onRotate, onSelect, page, pages, previews, }) => {
8
+ const filters = [
9
+ { key: PsScanFilter.Original, label: labels.filterOriginal },
10
+ { key: PsScanFilter.Colour, label: labels.filterColour },
11
+ { key: PsScanFilter.Greyscale, label: labels.filterGreyscale },
12
+ { key: PsScanFilter.Document, label: labels.filterDocument },
13
+ ];
14
+ const preview = previews[page.id];
15
+ return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col lg:flex-row', children: [_jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center bg-slate-950 p-3', children: preview === undefined ? (_jsx("span", { "aria-hidden": 'true', className: 'ps-skeleton h-full w-2/3 rounded' })) : (_jsx("img", { alt: labels.page, className: 'max-h-full max-w-full object-contain', src: preview })) }), _jsxs("div", { className: 'flex max-h-[55%] min-h-0 shrink-0 flex-col gap-3 overflow-y-auto bg-surface px-4 pt-3 lg:max-h-none lg:w-80 lg:border-l lg:border-line', children: [_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [_jsx(PsScanIconButton, { label: labels.rotate, onClick: onRotate, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Rotate }) }), _jsx(PsScanIconButton, { label: labels.editCrop, onClick: onEditCrop, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Pencil }) }), _jsx(PsScanIconButton, { label: labels.retake, onClick: onRetake, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Refresh }) }), _jsx(PsScanIconButton, { label: labels.deletePage, onClick: onDelete, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-5', iconKey: PsIconKey.Trash }) })] }), _jsx("div", { "aria-label": labels.filters, className: 'flex flex-wrap gap-2', role: 'group', children: filters.map((filter) => (_jsx("button", { "aria-pressed": page.filter === filter.key, className: `ps-focus-ring min-h-11 rounded-ps-control border px-3 text-sm font-medium transition ${page.filter === filter.key ? 'border-primary bg-primary-soft text-primary' : 'border-line bg-surface text-ink hover:border-primary/40'}`, onClick: () => onFilter(filter.key), type: 'button', children: filter.label }, filter.key))) }), _jsxs("section", { "aria-label": labels.pages, className: 'min-h-0', children: [_jsxs("h3", { className: 'mb-2 text-sm font-semibold text-ink', children: [labels.pages, " (", pages.length, ")"] }), _jsx(PsScanPageList, { activeId: page.id, labels: labels, onMove: onMove, onSelect: onSelect, pages: pages, previews: previews })] }), error === '' ? null : (_jsx("p", { className: 'text-sm text-danger', role: 'alert', children: error })), _jsxs("div", { className: 'sticky bottom-0 mt-auto flex flex-wrap gap-3 bg-surface pt-2 pb-[max(0.75rem,env(safe-area-inset-bottom))]', children: [_jsx(PsButton, { className: 'flex-1', disabled: !canAddPage || isBusy, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Plus }), onClick: onAddPage, variant: 'surface', children: labels.addPage }), _jsx(PsButton, { className: 'flex-1', isLoading: isBusy, loadingLabel: labels.finishing, onClick: onFinish, variant: 'primary', children: labels.finish })] })] })] }));
16
+ };
@@ -0,0 +1,50 @@
1
+ export const PsDocumentScannerLabels = {
2
+ addPage: 'Add page',
3
+ autoDetect: 'Auto-detect edges',
4
+ cameraDenied: 'Camera access was blocked. Allow it in your browser settings, or take a photo or choose existing photos instead.',
5
+ cameraStarting: 'Starting camera',
6
+ cameraUnavailable: 'No camera is available here. Take a photo or choose existing photos instead.',
7
+ backToPages: 'Back to pages',
8
+ capture: 'Capture page',
9
+ choosePhotos: 'Choose photos',
10
+ close: 'Close scanner',
11
+ cornerBottomLeft: 'Bottom left corner',
12
+ cornerBottomRight: 'Bottom right corner',
13
+ cornerTopLeft: 'Top left corner',
14
+ cornerTopRight: 'Top right corner',
15
+ cropApply: 'Use crop',
16
+ cropHint: 'Drag the corners to fit the page, or use the arrow keys on a focused corner.',
17
+ cropTitle: 'Adjust edges',
18
+ deletePage: 'Delete page',
19
+ dragPage: 'Drag {page} to reorder',
20
+ editCrop: 'Edit edges',
21
+ edgesFound: 'Page edges found.',
22
+ edgesNotFound: 'Edges not found. Adjust the corners.',
23
+ filterColour: 'Colour',
24
+ filterDocument: 'Black and white',
25
+ filterGreyscale: 'Greyscale',
26
+ filterOriginal: 'Original',
27
+ filters: 'Enhance',
28
+ finish: 'Use scan',
29
+ finishing: 'Preparing your scan',
30
+ maxPages: 'You reached the page limit.',
31
+ moveDown: 'Move {page} down',
32
+ moveUp: 'Move {page} up',
33
+ page: 'Page',
34
+ pageAdded: 'Page {position} added.',
35
+ pageDeleted: 'Page deleted.',
36
+ pageMoved: '{page} moved to position {position} of {count}.',
37
+ pages: 'Pages',
38
+ processFailed: 'The scan could not be prepared. Try again.',
39
+ reorderCancelled: '{page} stays where it was.',
40
+ reorderDropped: '{page} dropped at {position} of {count}.',
41
+ reorderInstructions: 'Press space to lift a page, the arrow keys to move it and space to drop it.',
42
+ reorderLifted: '{page} lifted from {position} of {count}.',
43
+ reorderMoved: '{page} is now {position} of {count}.',
44
+ retake: 'Retake',
45
+ rotate: 'Rotate',
46
+ takePhoto: 'Take photo',
47
+ title: 'Scan document',
48
+ torchOff: 'Turn light off',
49
+ torchOn: 'Turn light on',
50
+ };
@@ -0,0 +1,6 @@
1
+ export const PsScanCameraStatus = {
2
+ Denied: 'denied',
3
+ Ready: 'ready',
4
+ Starting: 'starting',
5
+ Unavailable: 'unavailable',
6
+ };
@@ -0,0 +1,31 @@
1
+ export const PsScanConfig = {
2
+ adaptiveRatio: 0.92,
3
+ adaptiveWindowDivisor: 16,
4
+ adaptiveWindowMin: 15,
5
+ capturedJpegQuality: 0.92,
6
+ cornerNudge: 0.005,
7
+ cornerNudgeLarge: 0.025,
8
+ decodeMaxDimension: 3000,
9
+ defaultFileName: 'scan',
10
+ defaultJpegQuality: 0.82,
11
+ defaultMaxDimension: 2000,
12
+ defaultMaxPages: 30,
13
+ detectDilations: 2,
14
+ detectMaxDimension: 320,
15
+ detectMinAreaRatio: 0.12,
16
+ detectMaxAreaRatio: 0.995,
17
+ detectMinEdgeStrength: 24,
18
+ detectMinHullFill: 0.85,
19
+ detectPercentile: 0.98,
20
+ detectRelativeStrength: 0.3,
21
+ hullCandidateLimit: 18,
22
+ levelHighPercentile: 0.99,
23
+ levelLowPercentile: 0.01,
24
+ minCornerGap: 0.02,
25
+ pdfDpi: 150,
26
+ pdfPointsPerInch: 72,
27
+ previewDebounceMs: 120,
28
+ previewMaxDimension: 1400,
29
+ thumbnailHeight: 56,
30
+ torchConstraint: 'torch',
31
+ };
@@ -0,0 +1 @@
1
+ export const PsScanCornerOrder = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'];
@@ -0,0 +1,6 @@
1
+ export const PsScanFilter = {
2
+ Colour: 'colour',
3
+ Document: 'document',
4
+ Greyscale: 'greyscale',
5
+ Original: 'original',
6
+ };
@@ -0,0 +1,4 @@
1
+ export const PsScanOutputFormat = {
2
+ Jpeg: 'jpeg',
3
+ Pdf: 'pdf',
4
+ };
@@ -0,0 +1,5 @@
1
+ export const PsScanStep = {
2
+ Camera: 'camera',
3
+ Crop: 'crop',
4
+ Review: 'review',
5
+ };
@@ -0,0 +1,45 @@
1
+ import { useEffect } from 'react';
2
+ export const usePsFocusTrap = (ref, isActive, onEscape) => {
3
+ useEffect(() => {
4
+ if (!isActive || ref.current === null)
5
+ return;
6
+ const dialog = ref.current.closest('[role="dialog"]');
7
+ const root = dialog === null ? ref.current : dialog;
8
+ const focusable = () => Array.from(root.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((element) => element.offsetParent !== null || element === document.activeElement);
9
+ if (!root.contains(document.activeElement)) {
10
+ root.setAttribute('tabindex', '-1');
11
+ root.focus({ preventScroll: true });
12
+ }
13
+ const onKeyDown = (event) => {
14
+ if (event.key === 'Escape') {
15
+ event.stopPropagation();
16
+ onEscape();
17
+ return;
18
+ }
19
+ if (event.key !== 'Tab')
20
+ return;
21
+ const items = focusable();
22
+ if (items.length === 0) {
23
+ event.preventDefault();
24
+ return;
25
+ }
26
+ const first = items[0];
27
+ const last = items[items.length - 1];
28
+ const current = document.activeElement;
29
+ if (!root.contains(current)) {
30
+ event.preventDefault();
31
+ first.focus();
32
+ }
33
+ else if (event.shiftKey && (current === first || current === root)) {
34
+ event.preventDefault();
35
+ last.focus();
36
+ }
37
+ else if (!event.shiftKey && current === last) {
38
+ event.preventDefault();
39
+ first.focus();
40
+ }
41
+ };
42
+ document.addEventListener('keydown', onKeyDown, true);
43
+ return () => document.removeEventListener('keydown', onKeyDown, true);
44
+ }, [isActive, onEscape]);
45
+ };