@pallasoft/ps-file-components 1.4.0 → 2.0.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 (194) 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 +64 -86
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -3
  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 +11 -8
  13. package/dist/components/ps-scan-camera-view.js +14 -16
  14. package/dist/components/ps-scan-crop-editor.d.ts +4 -4
  15. package/dist/components/ps-scan-crop-editor.js +24 -8
  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-permission-view.d.ts +10 -0
  25. package/dist/components/ps-scan-permission-view.js +48 -0
  26. package/dist/components/ps-scan-photo-button.d.ts +7 -0
  27. package/dist/components/ps-scan-photo-button.js +20 -0
  28. package/dist/components/ps-scan-preview.d.ts +12 -0
  29. package/dist/components/ps-scan-preview.js +10 -0
  30. package/dist/components/ps-scan-shutter.d.ts +6 -0
  31. package/dist/components/ps-scan-shutter.js +6 -0
  32. package/dist/components/ps-scan-viewfinder-overlay.d.ts +8 -0
  33. package/dist/components/ps-scan-viewfinder-overlay.js +28 -0
  34. package/dist/components/ps-upload-row.js +1 -1
  35. package/dist/constants/ps-document-scanner-labels.d.ts +39 -38
  36. package/dist/constants/ps-document-scanner-labels.js +39 -38
  37. package/dist/constants/ps-file-picker-labels.d.ts +5 -3
  38. package/dist/constants/ps-file-picker-labels.js +5 -3
  39. package/dist/constants/ps-scan-camera-status.d.ts +4 -0
  40. package/dist/constants/ps-scan-camera-status.js +4 -0
  41. package/dist/constants/ps-scan-chrome-classes.d.ts +10 -0
  42. package/dist/constants/ps-scan-chrome-classes.js +13 -0
  43. package/dist/constants/ps-scan-config.d.ts +34 -24
  44. package/dist/constants/ps-scan-config.js +34 -24
  45. package/dist/constants/ps-scan-platform.d.ts +6 -0
  46. package/dist/constants/ps-scan-platform.js +8 -0
  47. package/dist/constants/ps-scan-step.d.ts +1 -1
  48. package/dist/constants/ps-scan-step.js +1 -1
  49. package/dist/esm/components/ps-document-scanner-sheet.js +67 -89
  50. package/dist/esm/components/ps-document-scanner.js +2 -3
  51. package/dist/esm/components/ps-file-picker-actions.js +17 -0
  52. package/dist/esm/components/ps-file-picker.js +31 -7
  53. package/dist/esm/components/ps-scan-button.js +5 -0
  54. package/dist/esm/components/ps-scan-camera-view.js +17 -19
  55. package/dist/esm/components/ps-scan-crop-editor.js +26 -10
  56. package/dist/esm/components/ps-scan-denied-steps.js +29 -0
  57. package/dist/esm/components/ps-scan-icon-button.js +3 -1
  58. package/dist/esm/components/ps-scan-illustration.js +2 -0
  59. package/dist/esm/components/ps-scan-loupe.js +27 -0
  60. package/dist/esm/components/ps-scan-permission-view.js +44 -0
  61. package/dist/esm/components/ps-scan-photo-button.js +16 -0
  62. package/dist/esm/components/ps-scan-preview.js +6 -0
  63. package/dist/esm/components/ps-scan-shutter.js +2 -0
  64. package/dist/esm/components/ps-scan-viewfinder-overlay.js +24 -0
  65. package/dist/esm/components/ps-upload-row.js +1 -1
  66. package/dist/esm/constants/ps-document-scanner-labels.js +39 -38
  67. package/dist/esm/constants/ps-file-picker-labels.js +5 -3
  68. package/dist/esm/constants/ps-scan-camera-status.js +4 -0
  69. package/dist/esm/constants/ps-scan-chrome-classes.js +10 -0
  70. package/dist/esm/constants/ps-scan-config.js +34 -24
  71. package/dist/esm/constants/ps-scan-platform.js +5 -0
  72. package/dist/esm/constants/ps-scan-step.js +1 -1
  73. package/dist/esm/hooks/use-ps-scan-camera.js +52 -15
  74. package/dist/esm/hooks/use-ps-scan-live-detection.js +41 -0
  75. package/dist/esm/hooks/use-ps-scan-photo.js +42 -0
  76. package/dist/esm/index.js +1 -2
  77. package/dist/esm/models/ps-scan-region.js +1 -0
  78. package/dist/esm/models/ps-scan-result.js +1 -0
  79. package/dist/esm/models/ps-scan-stability.js +1 -0
  80. package/dist/esm/services/ps-scan-camera-service.js +52 -7
  81. package/dist/esm/services/ps-scan-canny-service.js +93 -0
  82. package/dist/esm/services/ps-scan-canvas-service.js +13 -1
  83. package/dist/esm/services/ps-scan-edge-service.js +73 -137
  84. package/dist/esm/services/ps-scan-file-name-service.js +2 -11
  85. package/dist/esm/services/ps-scan-geometry-service.js +56 -0
  86. package/dist/esm/services/ps-scan-hull-service.js +0 -39
  87. package/dist/esm/services/ps-scan-image-filter-service.js +0 -21
  88. package/dist/esm/services/ps-scan-morphology-service.js +44 -0
  89. package/dist/esm/services/ps-scan-pipeline-service.js +9 -19
  90. package/dist/esm/services/ps-scan-platform-service.js +15 -0
  91. package/dist/esm/services/ps-scan-polygon-service.js +75 -0
  92. package/dist/esm/services/ps-scan-region-service.js +52 -0
  93. package/dist/esm/services/ps-scan-stability-service.js +28 -0
  94. package/dist/esm/services/ps-scan-warp-service.js +8 -2
  95. package/dist/esm/services/ps-scan-yield-service.js +5 -0
  96. package/dist/hooks/use-ps-scan-camera.d.ts +3 -1
  97. package/dist/hooks/use-ps-scan-camera.js +52 -15
  98. package/dist/hooks/use-ps-scan-live-detection.d.ts +3 -0
  99. package/dist/hooks/use-ps-scan-live-detection.js +45 -0
  100. package/dist/hooks/use-ps-scan-photo.d.ts +9 -0
  101. package/dist/hooks/use-ps-scan-photo.js +46 -0
  102. package/dist/index.d.ts +1 -2
  103. package/dist/index.js +1 -2
  104. package/dist/models/ps-document-scanner-props.d.ts +0 -3
  105. package/dist/models/ps-document-scanner-sheet-props.d.ts +0 -3
  106. package/dist/models/ps-file-picker-scan.d.ts +4 -0
  107. package/dist/models/ps-scan-gradient.d.ts +4 -0
  108. package/dist/models/ps-scan-live-detection.d.ts +4 -0
  109. package/dist/models/ps-scan-located.d.ts +5 -0
  110. package/dist/models/ps-scan-photo.d.ts +9 -0
  111. package/dist/models/ps-scan-region.d.ts +5 -0
  112. package/dist/models/ps-scan-region.js +2 -0
  113. package/dist/models/ps-scan-result.d.ts +6 -0
  114. package/dist/models/ps-scan-result.js +2 -0
  115. package/dist/models/ps-scan-stability.d.ts +5 -0
  116. package/dist/models/ps-scan-stability.js +2 -0
  117. package/dist/services/ps-scan-camera-service.d.ts +4 -0
  118. package/dist/services/ps-scan-camera-service.js +52 -7
  119. package/dist/services/ps-scan-canny-service.d.ts +7 -0
  120. package/dist/services/ps-scan-canny-service.js +97 -0
  121. package/dist/services/ps-scan-canvas-service.d.ts +1 -0
  122. package/dist/services/ps-scan-canvas-service.js +13 -1
  123. package/dist/services/ps-scan-edge-service.d.ts +5 -5
  124. package/dist/services/ps-scan-edge-service.js +73 -137
  125. package/dist/services/ps-scan-file-name-service.d.ts +1 -3
  126. package/dist/services/ps-scan-file-name-service.js +2 -11
  127. package/dist/services/ps-scan-geometry-service.d.ts +4 -0
  128. package/dist/services/ps-scan-geometry-service.js +56 -0
  129. package/dist/services/ps-scan-hull-service.d.ts +0 -2
  130. package/dist/services/ps-scan-hull-service.js +0 -39
  131. package/dist/services/ps-scan-image-filter-service.d.ts +0 -1
  132. package/dist/services/ps-scan-image-filter-service.js +0 -21
  133. package/dist/services/ps-scan-morphology-service.d.ts +7 -0
  134. package/dist/services/ps-scan-morphology-service.js +48 -0
  135. package/dist/services/ps-scan-pipeline-service.d.ts +2 -5
  136. package/dist/services/ps-scan-pipeline-service.js +9 -19
  137. package/dist/services/ps-scan-platform-service.d.ts +4 -0
  138. package/dist/services/ps-scan-platform-service.js +19 -0
  139. package/dist/services/ps-scan-polygon-service.d.ts +8 -0
  140. package/dist/services/ps-scan-polygon-service.js +79 -0
  141. package/dist/services/ps-scan-region-service.d.ts +4 -0
  142. package/dist/services/ps-scan-region-service.js +56 -0
  143. package/dist/services/ps-scan-stability-service.d.ts +7 -0
  144. package/dist/services/ps-scan-stability-service.js +32 -0
  145. package/dist/services/ps-scan-warp-service.d.ts +1 -1
  146. package/dist/services/ps-scan-warp-service.js +8 -2
  147. package/dist/services/ps-scan-yield-service.d.ts +3 -0
  148. package/dist/services/ps-scan-yield-service.js +9 -0
  149. package/package.json +3 -3
  150. package/dist/components/ps-scan-page-list.d.ts +0 -12
  151. package/dist/components/ps-scan-page-list.js +0 -23
  152. package/dist/components/ps-scan-review.d.ts +0 -23
  153. package/dist/components/ps-scan-review.js +0 -20
  154. package/dist/constants/ps-scan-filter.d.ts +0 -7
  155. package/dist/constants/ps-scan-filter.js +0 -9
  156. package/dist/constants/ps-scan-output-format.d.ts +0 -5
  157. package/dist/constants/ps-scan-output-format.js +0 -7
  158. package/dist/esm/components/ps-scan-page-list.js +0 -19
  159. package/dist/esm/components/ps-scan-review.js +0 -16
  160. package/dist/esm/constants/ps-scan-filter.js +0 -6
  161. package/dist/esm/constants/ps-scan-output-format.js +0 -4
  162. package/dist/esm/hooks/use-ps-scan-pages.js +0 -62
  163. package/dist/esm/hooks/use-ps-scan-previews.js +0 -57
  164. package/dist/esm/services/ps-scan-enhance-service.js +0 -97
  165. package/dist/esm/services/ps-scan-order-service.js +0 -13
  166. package/dist/esm/services/ps-scan-output-service.js +0 -24
  167. package/dist/esm/services/ps-scan-pdf-service.js +0 -56
  168. package/dist/hooks/use-ps-scan-pages.d.ts +0 -12
  169. package/dist/hooks/use-ps-scan-pages.js +0 -66
  170. package/dist/hooks/use-ps-scan-previews.d.ts +0 -2
  171. package/dist/hooks/use-ps-scan-previews.js +0 -61
  172. package/dist/models/ps-scan-added-page.d.ts +0 -5
  173. package/dist/models/ps-scan-levels.d.ts +0 -4
  174. package/dist/models/ps-scan-output-options.d.ts +0 -7
  175. package/dist/models/ps-scan-page.d.ts +0 -12
  176. package/dist/models/ps-scan-pdf-page.d.ts +0 -5
  177. package/dist/services/ps-scan-enhance-service.d.ts +0 -9
  178. package/dist/services/ps-scan-enhance-service.js +0 -101
  179. package/dist/services/ps-scan-order-service.d.ts +0 -4
  180. package/dist/services/ps-scan-order-service.js +0 -17
  181. package/dist/services/ps-scan-output-service.d.ts +0 -5
  182. package/dist/services/ps-scan-output-service.js +0 -28
  183. package/dist/services/ps-scan-pdf-service.d.ts +0 -5
  184. package/dist/services/ps-scan-pdf-service.js +0 -60
  185. /package/dist/esm/models/{ps-scan-added-page.js → ps-file-picker-scan.js} +0 -0
  186. /package/dist/esm/models/{ps-scan-levels.js → ps-scan-gradient.js} +0 -0
  187. /package/dist/esm/models/{ps-scan-output-options.js → ps-scan-live-detection.js} +0 -0
  188. /package/dist/esm/models/{ps-scan-page.js → ps-scan-located.js} +0 -0
  189. /package/dist/esm/models/{ps-scan-pdf-page.js → ps-scan-photo.js} +0 -0
  190. /package/dist/models/{ps-scan-added-page.js → ps-file-picker-scan.js} +0 -0
  191. /package/dist/models/{ps-scan-levels.js → ps-scan-gradient.js} +0 -0
  192. /package/dist/models/{ps-scan-output-options.js → ps-scan-live-detection.js} +0 -0
  193. /package/dist/models/{ps-scan-page.js → ps-scan-located.js} +0 -0
  194. /package/dist/models/{ps-scan-pdf-page.js → ps-scan-photo.js} +0 -0
@@ -1,20 +1,29 @@
1
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';
2
+ import { useRef, useState } from 'react';
3
+ import { usePsElementSize } from '@pallasoft/ps-core-components';
4
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
+ import { PsIconKey } from '@pallasoft/ps-icons';
4
6
  import { PsScanConfig } from '../constants/ps-scan-config';
5
7
  import { PsScanCornerOrder } from '../constants/ps-scan-corner-order';
6
8
  import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
7
- export const PsScanCropEditor = ({ labels, onApply, onAutoDetect, onChange, onRetake, page, }) => {
9
+ import { PsScanButton } from './ps-scan-button';
10
+ import { PsScanIconButton } from './ps-scan-icon-button';
11
+ import { PsScanLoupe } from './ps-scan-loupe';
12
+ export const PsScanCropEditor = ({ labels, onAutoDetect, onChange, onContinue, onRetake, photo, }) => {
8
13
  const [areaRef, area] = usePsElementSize();
9
14
  const stageRef = useRef(null);
15
+ const [activeIndex, setActiveIndex] = useState(null);
10
16
  const hintId = 'ps-scan-crop-hint';
11
- const size = PsScanGeometryService.fit(area, { height: page.height, width: page.width });
17
+ const size = PsScanGeometryService.fit(area, { height: photo.height, width: photo.width });
12
18
  const cornerLabels = [
13
19
  labels.cornerTopLeft,
14
20
  labels.cornerTopRight,
15
21
  labels.cornerBottomRight,
16
22
  labels.cornerBottomLeft,
17
23
  ];
24
+ const outline = photo.corners
25
+ .map((point) => `${point.x * size.width},${point.y * size.height}`)
26
+ .join(' L');
18
27
  const move = (index, event) => {
19
28
  const stage = stageRef.current;
20
29
  if (stage === null || !event.currentTarget.hasPointerCapture(event.pointerId))
@@ -24,10 +33,14 @@ export const PsScanCropEditor = ({ labels, onApply, onAutoDetect, onChange, onRe
24
33
  x: (event.clientX - rect.left) / rect.width,
25
34
  y: (event.clientY - rect.top) / rect.height,
26
35
  });
27
- const next = page.corners.map((corner, at) => (at === index ? point : corner));
36
+ const next = photo.corners.map((corner, at) => (at === index ? point : corner));
28
37
  if (PsScanGeometryService.isUsable(next))
29
38
  onChange(next);
30
39
  };
40
+ const release = (event) => {
41
+ event.currentTarget.releasePointerCapture(event.pointerId);
42
+ setActiveIndex(null);
43
+ };
31
44
  const nudge = (index, event) => {
32
45
  const step = event.shiftKey ? PsScanConfig.cornerNudgeLarge : PsScanConfig.cornerNudge;
33
46
  const deltas = {
@@ -40,10 +53,13 @@ export const PsScanCropEditor = ({ labels, onApply, onAutoDetect, onChange, onRe
40
53
  if (delta === undefined)
41
54
  return;
42
55
  event.preventDefault();
43
- onChange(PsScanGeometryService.nudge(page.corners, index, delta[0], delta[1]));
56
+ onChange(PsScanGeometryService.nudge(photo.corners, index, delta[0], delta[1]));
44
57
  };
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 })] })] }));
58
+ return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx("span", { "aria-hidden": 'true' }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.cropTitle }), _jsx(PsScanIconButton, { iconKey: PsIconKey.Spark, label: labels.autoDetect, onClick: onAutoDetect })] }), _jsxs("p", { className: 'shrink-0 px-6 text-center text-sm text-white/65', id: hintId, children: [photo.isDetected ? labels.edgesFound : labels.edgesNotFound, " ", labels.cropHint] }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-8 py-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 rounded-sm', draggable: false, src: photo.sourceUrl }), _jsxs("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full overflow-visible', viewBox: `0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`, children: [_jsx("path", { className: 'fill-black/55', d: `M0 0H${size.width}V${size.height}H0Z M${outline}Z`, fillRule: 'evenodd' }), _jsx("path", { className: 'fill-none stroke-primary', d: `M${outline}Z`, strokeLinejoin: 'round', strokeWidth: '2.5' })] }), 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: release, onPointerDown: (event) => {
59
+ event.currentTarget.setPointerCapture(event.pointerId);
60
+ setActiveIndex(index);
61
+ }, onPointerMove: (event) => move(index, event), onPointerUp: release, style: {
62
+ left: `${photo.corners[index].x * 100}%`,
63
+ top: `${photo.corners[index].y * 100}%`,
64
+ }, type: 'button', children: _jsx("span", { className: PallasoftClassNameService.merge('size-6 rounded-full border-[3px] border-white bg-primary shadow-lg transition-transform duration-150', activeIndex === index ? 'scale-125' : '') }) }, key))), activeIndex === null ? null : (_jsx(PsScanLoupe, { corners: photo.corners, point: photo.corners[activeIndex], source: photo.sourceUrl, stage: size }))] }) }), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [_jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { isLarge: true, isPrimary: true, onClick: onContinue, children: labels.continue })] })] }));
49
65
  };
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
+ import { PsScanPlatform } from '../constants/ps-scan-platform';
5
+ import { PsScanPlatformService } from '../services/ps-scan-platform-service';
6
+ export const PsScanDeniedSteps = ({ labels }) => {
7
+ const [platform, setPlatform] = useState(() => PsScanPlatformService.detect());
8
+ const platforms = [
9
+ {
10
+ key: PsScanPlatform.Ios,
11
+ label: labels.platformIos,
12
+ steps: [labels.deniedIos1, labels.deniedIos2, labels.deniedIos3],
13
+ },
14
+ {
15
+ key: PsScanPlatform.Android,
16
+ label: labels.platformAndroid,
17
+ steps: [labels.deniedAndroid1, labels.deniedAndroid2, labels.deniedAndroid3],
18
+ },
19
+ {
20
+ key: PsScanPlatform.Desktop,
21
+ label: labels.platformDesktop,
22
+ steps: [labels.deniedDesktop1, labels.deniedDesktop2, labels.deniedDesktop3],
23
+ },
24
+ ];
25
+ const active = platforms.find((item) => item.key === platform);
26
+ return (_jsxs("div", { className: 'flex w-full flex-col gap-4 rounded-ps-control border border-white/10 bg-white/5 p-4 text-left', children: [_jsx("div", { className: 'grid grid-cols-3 gap-1 rounded-ps-control bg-black/40 p-1', role: 'group', children: platforms.map((item) => (_jsx("button", { "aria-pressed": item.key === platform, className: PallasoftClassNameService.merge('ps-focus-ring ps-control min-w-0 truncate rounded-ps-control px-2 text-xs font-semibold transition', item.key === platform
27
+ ? 'bg-white text-black'
28
+ : 'text-white/70 hover:bg-white/10 hover:text-white'), onClick: () => setPlatform(item.key), type: 'button', children: item.label }, item.key))) }), _jsx("ol", { className: 'flex flex-col gap-3', children: (active === undefined ? [] : active.steps).map((step, index) => (_jsxs("li", { className: 'flex items-start gap-3 text-sm leading-5 text-white/85', children: [_jsx("span", { className: 'flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-[color:var(--ps-color-primary-contrast)]', children: index + 1 }), _jsx("span", { className: 'pt-0.5', children: step })] }, step))) })] }));
29
+ };
@@ -1,3 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
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 }));
3
+ import { PsIcon } from '@pallasoft/ps-icons';
4
+ import { PsScanChromeClasses } from '../constants/ps-scan-chrome-classes';
5
+ export const PsScanIconButton = ({ className, disabled, iconClassName, iconKey, isPressed, label, onClick, }) => (_jsx("button", { "aria-label": label, "aria-pressed": isPressed, className: PallasoftClassNameService.merge(PsScanChromeClasses.icon, isPressed ? PsScanChromeClasses.iconOn : PsScanChromeClasses.iconOff, className), disabled: disabled, onClick: onClick, title: label, type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('size-ps-control-icon-standalone', iconClassName), iconKey: iconKey }) }));
@@ -0,0 +1,2 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const PsScanIllustration = () => (_jsxs("svg", { "aria-hidden": 'true', className: 'h-auto w-40', fill: 'none', viewBox: '0 0 160 160', children: [_jsx("rect", { className: 'fill-white/5 stroke-white/15', height: '148', rx: '28', width: '148', x: '6', y: '6' }), _jsx("path", { className: 'fill-white', d: 'M52 34h56a4 4 0 0 1 4 4v84l-8-5-8 5-8-5-8 5-8-5-8 5-8-5-8 5V38a4 4 0 0 1 4-4Z' }), _jsx("path", { className: 'stroke-black/25', d: 'M62 52h36M62 64h28M62 76h36M62 88h20M88 100h10', strokeLinecap: 'round', strokeWidth: '4' }), _jsx("path", { className: 'stroke-primary', d: 'M38 46V30a6 6 0 0 1 6-6h16M100 24h16a6 6 0 0 1 6 6v16M122 114v16a6 6 0 0 1-6 6h-16M60 136H44a6 6 0 0 1-6-6v-16', strokeLinecap: 'round', strokeWidth: '5' }), _jsx("rect", { className: 'fill-primary motion-safe:animate-pulse', height: '4', rx: '2', width: '96', x: '32', y: '78' })] }));
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsScanConfig } from '../constants/ps-scan-config';
3
+ export const PsScanLoupe = ({ corners, point, source, stage }) => {
4
+ const size = PsScanConfig.loupeSize;
5
+ const zoom = PsScanConfig.loupeZoom;
6
+ const centreX = point.x * stage.width * zoom;
7
+ const centreY = point.y * stage.height * zoom;
8
+ const pointX = point.x * stage.width;
9
+ const pointY = point.y * stage.height;
10
+ const isBelow = pointY - PsScanConfig.loupeOffset - size < 0;
11
+ const left = Math.min(Math.max(pointX - size / 2, -size / 4), stage.width - (size * 3) / 4);
12
+ const top = isBelow
13
+ ? pointY + PsScanConfig.loupeOffset
14
+ : pointY - PsScanConfig.loupeOffset - size;
15
+ return (_jsx("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute z-10 overflow-hidden rounded-full border-4 border-white bg-black shadow-2xl animate-ps-scale-in', style: {
16
+ backgroundImage: `url("${source}")`,
17
+ backgroundPosition: `${size / 2 - centreX}px ${size / 2 - centreY}px`,
18
+ backgroundRepeat: 'no-repeat',
19
+ backgroundSize: `${stage.width * zoom}px ${stage.height * zoom}px`,
20
+ height: size,
21
+ left,
22
+ top,
23
+ width: size,
24
+ }, children: _jsxs("svg", { className: 'absolute inset-0 size-full', viewBox: `0 0 ${size} ${size}`, children: [_jsx("polygon", { className: 'fill-none stroke-primary', points: corners
25
+ .map((corner) => `${corner.x * stage.width * zoom - centreX + size / 2},${corner.y * stage.height * zoom - centreY + size / 2}`)
26
+ .join(' '), strokeWidth: '2' }), _jsx("path", { className: 'stroke-white', d: `M${size / 2 - 10} ${size / 2}H${size / 2 + 10}M${size / 2} ${size / 2 - 10}V${size / 2 + 10}`, strokeLinecap: 'round', strokeWidth: '2' })] }) }));
27
+ };
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsSpinner } from '@pallasoft/ps-core-components';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
5
+ import { PsScanButton } from './ps-scan-button';
6
+ import { PsScanDeniedSteps } from './ps-scan-denied-steps';
7
+ import { PsScanIllustration } from './ps-scan-illustration';
8
+ import { PsScanPhotoButton } from './ps-scan-photo-button';
9
+ export const PsScanPermissionView = ({ labels, onAllow, onFile, status, }) => {
10
+ if (status === PsScanCameraStatus.Checking || status === PsScanCameraStatus.Starting)
11
+ return (_jsxs("div", { className: 'flex h-full 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 })] }));
12
+ const screens = {
13
+ [PsScanCameraStatus.Busy]: {
14
+ body: labels.busyBody,
15
+ iconKey: PsIconKey.Camera,
16
+ title: labels.busyTitle,
17
+ },
18
+ [PsScanCameraStatus.Denied]: {
19
+ body: labels.deniedBody,
20
+ iconKey: PsIconKey.CameraOff,
21
+ title: labels.deniedTitle,
22
+ },
23
+ [PsScanCameraStatus.Insecure]: {
24
+ body: labels.insecureBody,
25
+ iconKey: PsIconKey.Shield,
26
+ title: labels.insecureTitle,
27
+ },
28
+ [PsScanCameraStatus.Prompt]: {
29
+ body: labels.primerBody,
30
+ iconKey: PsIconKey.Camera,
31
+ title: labels.primerTitle,
32
+ },
33
+ [PsScanCameraStatus.Ready]: { body: '', iconKey: PsIconKey.Camera, title: '' },
34
+ [PsScanCameraStatus.Unavailable]: {
35
+ body: labels.unavailableBody,
36
+ iconKey: PsIconKey.CameraOff,
37
+ title: labels.unavailableTitle,
38
+ },
39
+ };
40
+ const screen = screens[status];
41
+ const isPrimer = status === PsScanCameraStatus.Prompt;
42
+ const canRetry = status === PsScanCameraStatus.Denied || status === PsScanCameraStatus.Busy;
43
+ return (_jsx("div", { className: 'flex h-full min-h-0 flex-col overflow-y-auto', children: _jsxs("div", { className: 'm-auto flex w-full max-w-sm flex-col items-center gap-6 px-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-20 text-center animate-ps-slide-up', children: [isPrimer ? (_jsx(PsScanIllustration, {})) : (_jsx("span", { className: 'flex size-20 items-center justify-center rounded-full bg-white/10 text-white', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-9', iconKey: screen.iconKey }) })), _jsxs("div", { className: 'flex flex-col gap-2', children: [_jsx("h2", { className: 'text-xl font-semibold leading-tight text-white sm:text-2xl', children: screen.title }), _jsx("p", { className: 'text-sm leading-6 text-white/70', children: screen.body })] }), status === PsScanCameraStatus.Denied ? _jsx(PsScanDeniedSteps, { labels: labels }) : null, _jsxs("div", { className: 'flex w-full flex-col gap-3', children: [isPrimer || canRetry ? (_jsx(PsScanButton, { className: 'w-full', isLarge: true, isPrimary: true, leadingIcon: isPrimer ? (_jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Camera })) : undefined, onClick: onAllow, children: isPrimer ? labels.allowCamera : labels.tryAgain })) : null, _jsx(PsScanPhotoButton, { className: 'w-full', label: labels.choosePhoto, onFile: onFile })] })] }) }));
44
+ };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanButton } from './ps-scan-button';
5
+ import { PsScanIconButton } from './ps-scan-icon-button';
6
+ export const PsScanPhotoButton = ({ className, isIconOnly = false, label, onFile, }) => {
7
+ const input = useRef(null);
8
+ const open = () => input.current?.click();
9
+ const onChange = (event) => {
10
+ const file = event.target.files === null ? undefined : event.target.files[0];
11
+ event.target.value = '';
12
+ if (file !== undefined)
13
+ onFile(file);
14
+ };
15
+ return (_jsxs(_Fragment, { children: [isIconOnly ? (_jsx(PsScanIconButton, { className: className, iconKey: PsIconKey.Image, label: label, onClick: open })) : (_jsx(PsScanButton, { className: className, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: open, children: label })), _jsx("input", { accept: 'image/*', "aria-hidden": 'true', className: 'sr-only', onChange: onChange, ref: input, tabIndex: -1, type: 'file' })] }));
16
+ };
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsSpinner } from '@pallasoft/ps-core-components';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanButton } from './ps-scan-button';
5
+ import { PsScanIconButton } from './ps-scan-icon-button';
6
+ export const PsScanPreview = ({ error, labels, onAdjust, onRetake, onUse, result, }) => (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Crop, label: labels.cropTitle, onClick: onAdjust }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.previewTitle }), _jsx("span", { "aria-hidden": 'true' })] }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-6 py-4', children: result === null ? (_jsxs("div", { className: 'flex flex-col items-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.preparing, size: 'small' }), _jsx("span", { children: labels.preparing })] })) : (_jsx("img", { alt: labels.previewAlt, className: 'max-h-full max-w-full rounded-sm bg-white object-contain shadow-[0_24px_60px_-20px_rgb(0_0_0/0.9)] animate-ps-fade-in', src: result.url })) }), error === '' ? null : (_jsx("p", { className: 'mx-auto mb-2 max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [_jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { disabled: result === null, isLarge: true, isPrimary: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Check }), onClick: onUse, children: labels.useScan })] })] }));
@@ -0,0 +1,2 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const PsScanShutter = ({ disabled, label, onCapture }) => (_jsxs("button", { "aria-label": label, className: 'ps-focus-ring group relative flex size-[4.75rem] shrink-0 items-center justify-center rounded-full disabled:opacity-40', disabled: disabled, onClick: onCapture, title: label, type: 'button', children: [_jsx("svg", { "aria-hidden": 'true', className: 'absolute inset-0 size-full -rotate-90', viewBox: '0 0 76 76', children: _jsx("circle", { className: 'stroke-white', cx: '38', cy: '38', fill: 'none', r: '35', strokeWidth: '4' }) }), _jsx("span", { className: 'size-[3.75rem] rounded-full bg-white transition-transform duration-150 group-active:scale-90' })] }));
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PsScanConfig } from '../constants/ps-scan-config';
3
+ import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
4
+ export const PsScanViewfinderOverlay = ({ area, quad, video }) => {
5
+ if (area.width <= 0 || area.height <= 0)
6
+ return null;
7
+ const box = PsScanGeometryService.cover(area, video);
8
+ const left = (area.width - box.width) / 2;
9
+ const top = (area.height - box.height) / 2;
10
+ const frameHeight = Math.min(area.height * PsScanConfig.guideHeightRatio, (area.width * PsScanConfig.guideWidthRatio) / PsScanConfig.guideAspect);
11
+ const frameWidth = frameHeight * PsScanConfig.guideAspect;
12
+ const frameLeft = (area.width - frameWidth) / 2;
13
+ const frameTop = (area.height - frameHeight) / 2 - area.height * PsScanConfig.guideLift;
14
+ const arm = Math.min(PsScanConfig.guideArm, frameWidth * PsScanConfig.guideArmRatio);
15
+ const corners = [
16
+ `M${frameLeft} ${frameTop + arm}V${frameTop}H${frameLeft + arm}`,
17
+ `M${frameLeft + frameWidth - arm} ${frameTop}H${frameLeft + frameWidth}V${frameTop + arm}`,
18
+ `M${frameLeft + frameWidth} ${frameTop + frameHeight - arm}V${frameTop + frameHeight}H${frameLeft + frameWidth - arm}`,
19
+ `M${frameLeft + arm} ${frameTop + frameHeight}H${frameLeft}V${frameTop + frameHeight - arm}`,
20
+ ].join('');
21
+ return (_jsx("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', viewBox: `0 0 ${area.width} ${area.height}`, children: quad === null || box.width === 0 ? (_jsx("path", { className: 'stroke-white/75 animate-ps-fade-in', d: corners, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: '4' })) : (_jsx("polygon", { className: 'fill-primary/10 stroke-primary', points: quad
22
+ .map((point) => `${left + point.x * box.width},${top + point.y * box.height}`)
23
+ .join(' '), strokeLinejoin: 'round', strokeWidth: '3' })) }));
24
+ };
@@ -16,5 +16,5 @@ export const PsUploadRow = ({ item, labels, onCancel, onRemove, onRetry }) => {
16
16
  : item.status === PsUploadStatus.failed && item.error
17
17
  ? item.error
18
18
  : labels[item.status];
19
- return (_jsxs("li", { className: 'flex items-center gap-3 rounded-ps-control border border-line px-3 py-2.5', children: [_jsx(PsFileKindIcon, { contentType: item.file.type, name: item.file.name }), _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-1.5', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: isProblem ? 1 : item.progress }), _jsxs("p", { className: `truncate text-xs ${isProblem ? 'text-danger' : item.status === PsUploadStatus.done ? 'text-success' : 'text-ink/55'}`, role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [size, " \u00B7 ", status] })] }), _jsxs("div", { className: 'flex w-16 shrink-0 items-center justify-end gap-0.5', children: [isProblem ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Refresh, label: `${labels.retry} ${item.file.name}`, onClick: onRetry })) : null, isActive ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.cancel} ${item.file.name}`, onClick: onCancel, tone: 'danger' })) : item.status === PsUploadStatus.done ? (_jsx("span", { className: 'ps-control-square ps-control-compact inline-flex items-center justify-center text-success', children: _jsx(PsIcon, { "aria-label": labels.done, className: 'h-4 w-4', iconKey: PsIconKey.Check }) })) : (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.remove} ${item.file.name}`, onClick: onRemove }))] })] }));
19
+ return (_jsxs("li", { className: 'flex items-center gap-3 rounded-ps-control border border-line px-3 py-2.5', children: [_jsx(PsFileKindIcon, { contentType: item.file.type, name: item.file.name }), _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-1.5', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: isProblem ? 1 : item.progress }), _jsxs("p", { className: `truncate text-xs ${isProblem ? 'text-danger' : item.status === PsUploadStatus.done ? 'text-success' : 'text-ink/55'}`, role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [size, " \u00B7 ", status] })] }), _jsxs("div", { className: 'flex w-20 shrink-0 items-center justify-end gap-1', children: [isProblem ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Refresh, label: `${labels.retry} ${item.file.name}`, onClick: onRetry })) : null, isActive ? (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.cancel} ${item.file.name}`, onClick: onCancel, tone: 'danger' })) : item.status === PsUploadStatus.done ? (_jsx("span", { className: 'ps-control-square ps-control-compact inline-flex items-center justify-center text-success', children: _jsx(PsIcon, { "aria-label": labels.done, className: 'h-4 w-4', iconKey: PsIconKey.Check }) })) : (_jsx(PsFileIconButton, { iconKey: PsIconKey.Close, label: `${labels.remove} ${item.file.name}`, onClick: onRemove }))] })] }));
20
20
  };
@@ -1,50 +1,51 @@
1
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.',
2
+ allowCamera: 'Allow camera',
3
+ autoDetect: 'Detect edges',
4
+ busyBody: 'Another app is using the camera. Close it, then try again.',
5
+ busyTitle: 'Your camera is busy',
5
6
  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',
7
+ capture: 'Take photo',
8
+ choosePhoto: 'Choose a photo instead',
10
9
  close: 'Close scanner',
10
+ continue: 'Continue',
11
11
  cornerBottomLeft: 'Bottom left corner',
12
12
  cornerBottomRight: 'Bottom right corner',
13
13
  cornerTopLeft: 'Top left corner',
14
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',
15
+ cropHint: 'Drag the corners to the edges of the page.',
16
+ cropTitle: 'Adjust corners',
17
+ deniedAndroid1: 'Tap the icon to the left of the address bar.',
18
+ deniedAndroid2: 'Open Permissions, then Camera.',
19
+ deniedAndroid3: 'Choose Allow, then come back and tap Try again.',
20
+ deniedBody: 'You can turn it back on in a few taps.',
21
+ deniedDesktop1: 'Click the camera or site icon in the address bar.',
22
+ deniedDesktop2: 'Set Camera to Allow for this site.',
23
+ deniedDesktop3: 'Close the menu, then click Try again.',
24
+ deniedIos1: 'Tap aA in the address bar, then Website Settings.',
25
+ deniedIos2: 'Set Camera to Allow.',
26
+ deniedIos3: 'Come back and tap Try again.',
27
+ deniedTitle: 'Camera access is off',
21
28
  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',
29
+ edgesNotFound: 'Edges not found. Drag the corners to the page.',
30
+ hintFind: 'Point your camera at a document',
31
+ hintFound: 'Document found',
32
+ insecureBody: 'The camera only works on a secure (https) page. You can still choose a photo.',
33
+ insecureTitle: 'Camera not available here',
34
+ platformAndroid: 'Android',
35
+ platformDesktop: 'Computer',
36
+ platformIos: 'iPhone',
37
+ preparing: 'Preparing your scan',
38
+ previewAlt: 'Your flattened scan',
39
+ previewTitle: 'Your scan',
40
+ primerBody: 'The camera is only used while the scanner is open. Nothing is saved until you upload.',
41
+ primerTitle: 'Allow camera access to scan your documents',
38
42
  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
43
  retake: 'Retake',
45
- rotate: 'Rotate',
46
- takePhoto: 'Take photo',
47
44
  title: 'Scan document',
48
- torchOff: 'Turn light off',
49
- torchOn: 'Turn light on',
45
+ torchOff: 'Turn flash off',
46
+ torchOn: 'Turn flash on',
47
+ tryAgain: 'Try again',
48
+ unavailableBody: 'This device has no camera the browser can use. Choose a photo of the page instead.',
49
+ unavailableTitle: 'No camera found',
50
+ useScan: 'Use scan',
50
51
  };
@@ -1,8 +1,10 @@
1
1
  export const PsFilePickerLabels = {
2
- browse: 'or click to browse your files',
3
- browseTouch: 'or tap to choose from your device',
2
+ choose: 'Choose files',
3
+ chooseOne: 'Choose a file',
4
+ dragHint: 'Drag and drop files here',
5
+ dragHintOne: 'Drag and drop a file here',
4
6
  dropActive: 'Drop to upload',
5
- formats: 'Accepted formats:',
6
7
  invalidType: 'is not a supported file type.',
8
+ scan: 'Scan with camera',
7
9
  tooLarge: 'is too large.',
8
10
  };
@@ -1,5 +1,9 @@
1
1
  export const PsScanCameraStatus = {
2
+ Busy: 'busy',
3
+ Checking: 'checking',
2
4
  Denied: 'denied',
5
+ Insecure: 'insecure',
6
+ Prompt: 'prompt',
3
7
  Ready: 'ready',
4
8
  Starting: 'starting',
5
9
  Unavailable: 'unavailable',
@@ -0,0 +1,10 @@
1
+ export const PsScanChromeClasses = {
2
+ 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
+ buttonLarge: 'ps-control-large ps-control-padding-large',
4
+ buttonPrimary: 'border-primary bg-primary text-[color:var(--ps-color-primary-contrast)] hover:border-primary-dark hover:bg-primary-dark',
5
+ buttonQuiet: 'border-white/15 bg-white/10 text-white hover:border-white/25 hover:bg-white/20',
6
+ 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',
7
+ iconOff: 'bg-black/45 backdrop-blur-md hover:bg-white/20',
8
+ iconOn: 'bg-white text-black hover:bg-white/90',
9
+ 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',
10
+ };
@@ -1,31 +1,41 @@
1
1
  export const PsScanConfig = {
2
- adaptiveRatio: 0.92,
3
- adaptiveWindowDivisor: 16,
4
- adaptiveWindowMin: 15,
5
- capturedJpegQuality: 0.92,
2
+ cameraPermission: 'camera',
3
+ cannyHighPercentile: 0.9,
4
+ cannyLowRatio: 0.45,
5
+ cannyMinHigh: 12,
6
+ capturedJpegQuality: 0.97,
7
+ closeRadii: [1, 2, 4],
6
8
  cornerNudge: 0.005,
7
9
  cornerNudgeLarge: 0.025,
8
- decodeMaxDimension: 3000,
10
+ decodeMaxPixels: 16000000,
9
11
  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,
12
+ defaultJpegQuality: 0.92,
13
+ defaultMaxDimension: 4000,
14
+ detectBorderMargin: 3,
15
+ detectExpand: 0.3,
16
+ detectMaxAngle: 140,
17
+ detectMaxAreaRatio: 0.95,
18
+ detectMaxDimension: 480,
19
+ detectMinAngle: 40,
20
+ detectMinAreaRatio: 0.1,
21
+ detectMinQuadFill: 0.88,
22
+ detectMinSideRatio: 0.06,
23
+ guideArm: 36,
24
+ guideArmRatio: 0.18,
25
+ guideAspect: 0.72,
26
+ guideHeightRatio: 0.58,
27
+ guideLift: 0.03,
28
+ guideWidthRatio: 0.76,
29
+ liveDeadzone: 0.006,
30
+ liveDetectIntervalMs: 200,
31
+ liveDetectMaxDimension: 280,
32
+ liveMissLimit: 3,
33
+ liveSmoothing: 0.45,
34
+ loupeOffset: 72,
35
+ loupeSize: 112,
36
+ loupeZoom: 2.5,
24
37
  minCornerGap: 0.02,
25
- pdfDpi: 150,
26
- pdfPointsPerInch: 72,
27
- previewDebounceMs: 120,
28
- previewMaxDimension: 1400,
29
- thumbnailHeight: 56,
38
+ polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
30
39
  torchConstraint: 'torch',
40
+ warpChunkPixels: 400000,
31
41
  };
@@ -0,0 +1,5 @@
1
+ export const PsScanPlatform = {
2
+ Android: 'android',
3
+ Desktop: 'desktop',
4
+ Ios: 'ios',
5
+ };
@@ -1,5 +1,5 @@
1
1
  export const PsScanStep = {
2
2
  Camera: 'camera',
3
3
  Crop: 'crop',
4
- Review: 'review',
4
+ Preview: 'preview',
5
5
  };
@@ -6,18 +6,40 @@ import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
6
6
  export const usePsScanCamera = (isActive) => {
7
7
  const videoRef = useRef(null);
8
8
  const sessionRef = useRef(null);
9
- const [status, setStatus] = useState(PsScanCameraStatus.Starting);
9
+ const [status, setStatus] = useState(PsScanCameraStatus.Checking);
10
+ const [request, setRequest] = useState(0);
10
11
  const [canTorch, setCanTorch] = useState(false);
11
12
  const [isTorchOn, setIsTorchOn] = useState(false);
12
13
  useEffect(() => {
13
- if (!isActive)
14
+ let isCancelled = false;
15
+ let permission = null;
16
+ const onChange = () => {
17
+ if (permission !== null && permission.state === 'granted')
18
+ setRequest((current) => current + 1);
19
+ };
20
+ PsScanCameraService.check().then(async (next) => {
21
+ if (isCancelled)
22
+ return;
23
+ if (next === PsScanCameraStatus.Starting)
24
+ setRequest(1);
25
+ else
26
+ setStatus(next);
27
+ permission = await PsScanCameraService.permission();
28
+ if (isCancelled || permission === null)
29
+ return;
30
+ permission.addEventListener('change', onChange);
31
+ });
32
+ return () => {
33
+ isCancelled = true;
34
+ if (permission !== null)
35
+ permission.removeEventListener('change', onChange);
36
+ };
37
+ }, []);
38
+ useEffect(() => {
39
+ if (!isActive || request === 0)
14
40
  return;
15
41
  let isCancelled = false;
16
42
  setIsTorchOn(false);
17
- if (!PsScanCameraService.isSupported()) {
18
- setStatus(PsScanCameraStatus.Unavailable);
19
- return;
20
- }
21
43
  setStatus(PsScanCameraStatus.Starting);
22
44
  PsScanCameraService.open()
23
45
  .then(async (session) => {
@@ -27,12 +49,10 @@ export const usePsScanCamera = (isActive) => {
27
49
  }
28
50
  sessionRef.current = session;
29
51
  setCanTorch(session.canTorch);
30
- const video = videoRef.current;
31
- if (video !== null) {
32
- video.srcObject = session.stream;
33
- await video.play().catch(() => undefined);
34
- }
35
- setStatus(PsScanCameraStatus.Ready);
52
+ if (videoRef.current !== null)
53
+ await PsScanCameraService.attach(videoRef.current, session);
54
+ if (!isCancelled)
55
+ setStatus(PsScanCameraStatus.Ready);
36
56
  })
37
57
  .catch((error) => {
38
58
  if (!isCancelled)
@@ -45,7 +65,13 @@ export const usePsScanCamera = (isActive) => {
45
65
  if (videoRef.current !== null)
46
66
  videoRef.current.srcObject = null;
47
67
  };
48
- }, [isActive]);
68
+ }, [isActive, request]);
69
+ const attachVideo = useCallback((element) => {
70
+ videoRef.current = element;
71
+ if (element !== null && sessionRef.current !== null)
72
+ void PsScanCameraService.attach(element, sessionRef.current);
73
+ }, []);
74
+ const allow = useCallback(() => setRequest((current) => current + 1), []);
49
75
  const toggleTorch = useCallback(async () => {
50
76
  const session = sessionRef.current;
51
77
  if (session === null)
@@ -54,9 +80,20 @@ export const usePsScanCamera = (isActive) => {
54
80
  }, [isTorchOn]);
55
81
  const capture = useCallback(async () => {
56
82
  const video = videoRef.current;
57
- if (video === null || video.videoWidth === 0)
83
+ if (video === null ||
84
+ video.videoWidth === 0 ||
85
+ video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)
58
86
  return null;
59
87
  return await PsScanCanvasService.fromVideo(video, PsScanConfig.capturedJpegQuality);
60
88
  }, []);
61
- return { canTorch, capture, isTorchOn, status, toggleTorch, videoRef };
89
+ return {
90
+ allow,
91
+ attachVideo,
92
+ canTorch,
93
+ capture,
94
+ isTorchOn,
95
+ status,
96
+ toggleTorch,
97
+ videoRef,
98
+ };
62
99
  };