@incodetech/prizma-core-screens 0.2.4 → 0.2.12

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 (243) hide show
  1. package/dist/BiometricConsentScreen.d.ts +95 -0
  2. package/dist/BiometricConsentScreen.js +91 -0
  3. package/dist/BiometricConsentStrings.d.ts +84 -0
  4. package/dist/BiometricConsentStrings.js +31 -0
  5. package/dist/CaptureScreen.d.ts +13 -0
  6. package/dist/CaptureScreen.js +2 -2
  7. package/dist/CurpEnterScreen.d.ts +7 -0
  8. package/dist/CurpEnterScreen.js +17 -3
  9. package/dist/CurpGenerateScreen.d.ts +8 -0
  10. package/dist/CurpGenerateScreen.js +28 -13
  11. package/dist/CurpLayout.css +8 -34
  12. package/dist/CurpStrings.d.ts +16 -2
  13. package/dist/DemoScreensLayout.css +16 -0
  14. package/dist/DemoScreensRedirectScreen.d.ts +6 -1
  15. package/dist/DemoScreensRedirectScreen.js +3 -2
  16. package/dist/DemoScreensStrings.d.ts +5 -0
  17. package/dist/DigitalIdUploadErrorScreen.d.ts +31 -0
  18. package/dist/DigitalIdUploadErrorScreen.js +26 -0
  19. package/dist/DigitalIdUploadFileTooLargeScreen.d.ts +27 -0
  20. package/dist/DigitalIdUploadFileTooLargeScreen.js +21 -0
  21. package/dist/DigitalIdUploadReviewScreen.d.ts +57 -0
  22. package/dist/DigitalIdUploadReviewScreen.js +44 -0
  23. package/dist/DigitalIdUploadStrings.d.ts +64 -0
  24. package/dist/DigitalIdUploadStrings.js +19 -0
  25. package/dist/DigitalIdUploadTutorialScreen.d.ts +28 -0
  26. package/dist/DigitalIdUploadTutorialScreen.js +23 -0
  27. package/dist/DocumentCaptureIllustration.d.ts +1 -1
  28. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  29. package/dist/EkybAddressDetailsScreen.js +25 -7
  30. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  31. package/dist/EkybBusinessDetailsScreen.js +23 -6
  32. package/dist/EkybCountryScreen.d.ts +13 -5
  33. package/dist/EkybCountryScreen.js +41 -10
  34. package/dist/EkybOwnersScreen.d.ts +8 -0
  35. package/dist/EkybOwnersScreen.js +26 -3
  36. package/dist/EkybStrings.d.ts +32 -10
  37. package/dist/EkycCountryCatalog.d.ts +4 -1
  38. package/dist/EkycCountryCatalog.js +5 -3
  39. package/dist/EkycCountryFormScreen.d.ts +12 -3
  40. package/dist/EkycCountryFormScreen.js +28 -10
  41. package/dist/EkycDetailsScreen.d.ts +7 -0
  42. package/dist/EkycDetailsScreen.js +21 -4
  43. package/dist/EkycStrings.d.ts +14 -2
  44. package/dist/ElectronicSignatureLayout.css +6 -0
  45. package/dist/EmailInputStrings.d.ts +11 -1
  46. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  47. package/dist/EmailInputVerifyScreen.js +17 -3
  48. package/dist/ErrorScreensScreen.d.ts +5 -2
  49. package/dist/ErrorScreensScreen.js +15 -4
  50. package/dist/ErrorScreensStrings.d.ts +17 -2
  51. package/dist/ErrorScreensStrings.js +18 -10
  52. package/dist/FaceCaptureCapturingScreen.d.ts +63 -0
  53. package/dist/FaceCaptureCapturingScreen.js +62 -2
  54. package/dist/FaceCaptureLayout.css +69 -4
  55. package/dist/FaceCaptureStrings.d.ts +23 -0
  56. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  57. package/dist/FaceMatchLayout.css +21 -12
  58. package/dist/FormScreen.css +30 -0
  59. package/dist/FormScreen.d.ts +28 -0
  60. package/dist/FormScreen.js +3 -2
  61. package/dist/FormsLongFormScreen.d.ts +8 -0
  62. package/dist/FormsLongFormScreen.js +34 -13
  63. package/dist/FormsStrings.d.ts +11 -1
  64. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  65. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  66. package/dist/GeolocationStrings.d.ts +3 -0
  67. package/dist/IdCaptureLayout.css +205 -2
  68. package/dist/IdCaptureStageScreen.d.ts +47 -0
  69. package/dist/IdCaptureStageScreen.js +39 -3
  70. package/dist/IdCaptureStrings.d.ts +19 -0
  71. package/dist/ListScreen.d.ts +21 -0
  72. package/dist/ListScreen.js +7 -2
  73. package/dist/LoaderScreen.d.ts +7 -5
  74. package/dist/LoaderScreen.js +7 -5
  75. package/dist/LoadersStrings.d.ts +18 -5
  76. package/dist/OcrReviewErrorScreen.d.ts +30 -0
  77. package/dist/OcrReviewErrorScreen.js +24 -0
  78. package/dist/OcrReviewLayout.css +24 -0
  79. package/dist/OcrReviewLoadingScreen.d.ts +25 -0
  80. package/dist/OcrReviewLoadingScreen.js +19 -0
  81. package/dist/OcrReviewScreen.d.ts +76 -0
  82. package/dist/OcrReviewScreen.js +65 -0
  83. package/dist/OcrReviewStrings.d.ts +47 -0
  84. package/dist/OcrReviewStrings.js +17 -0
  85. package/dist/OcrReviewSuccessScreen.d.ts +30 -0
  86. package/dist/OcrReviewSuccessScreen.js +24 -0
  87. package/dist/PermissionsAllowScreen.d.ts +8 -11
  88. package/dist/PermissionsAllowScreen.js +8 -11
  89. package/dist/PermissionsAskScreen.d.ts +41 -0
  90. package/dist/PermissionsAskScreen.js +43 -0
  91. package/dist/PermissionsBrowserStepsScreen.d.ts +9 -2
  92. package/dist/PermissionsBrowserStepsScreen.js +12 -3
  93. package/dist/PermissionsLayout.css +47 -0
  94. package/dist/PermissionsStrings.d.ts +20 -9
  95. package/dist/PermissionsStrings.js +10 -4
  96. package/dist/PhoneInputLayout.css +10 -9
  97. package/dist/PhoneInputOtpScreen.d.ts +8 -0
  98. package/dist/PhoneInputOtpScreen.js +26 -19
  99. package/dist/PhoneInputStrings.d.ts +22 -4
  100. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  101. package/dist/PhoneInputVerifyScreen.js +36 -7
  102. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  103. package/dist/QualifiedSignatureDocumentScreen.d.ts +17 -10
  104. package/dist/QualifiedSignatureDocumentScreen.js +22 -17
  105. package/dist/QualifiedSignatureLayout.css +76 -0
  106. package/dist/QualifiedSignatureResultScreen.d.ts +16 -13
  107. package/dist/QualifiedSignatureResultScreen.js +17 -18
  108. package/dist/QualifiedSignatureStrings.d.ts +19 -2
  109. package/dist/ResultScreen.d.ts +8 -0
  110. package/dist/ResultScreen.js +14 -3
  111. package/dist/ReusabilityCredentialSelectionScreen.d.ts +95 -0
  112. package/dist/ReusabilityCredentialSelectionScreen.js +94 -0
  113. package/dist/ReusabilityLayout.css +241 -0
  114. package/dist/ReusabilityPrivacyPolicyScreen.d.ts +55 -0
  115. package/dist/ReusabilityPrivacyPolicyScreen.js +44 -0
  116. package/dist/ReusabilityStrings.d.ts +85 -0
  117. package/dist/ReusabilityStrings.js +37 -0
  118. package/dist/ReviewScreen.d.ts +38 -12
  119. package/dist/ReviewScreen.js +41 -8
  120. package/dist/ScreenLayout.css +382 -6
  121. package/dist/ScreenNav.css +8 -0
  122. package/dist/ScreenNav.d.ts +15 -2
  123. package/dist/ScreenNav.js +10 -3
  124. package/dist/VideoSelfieErrorScreen.js +1 -1
  125. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  126. package/dist/VideoSelfieIdCaptureScreen.js +45 -14
  127. package/dist/VideoSelfieLayout.css +467 -34
  128. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  129. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  130. package/dist/VideoSelfieSpeakScreen.js +19 -2
  131. package/dist/VideoSelfieStrings.d.ts +82 -13
  132. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  133. package/dist/VideoSelfieTurnHeadScreen.js +66 -0
  134. package/dist/VideoSelfieTutorialScreen.d.ts +9 -0
  135. package/dist/VideoSelfieTutorialScreen.js +22 -5
  136. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  137. package/dist/VideoSelfieWaveScreen.js +62 -0
  138. package/dist/ViewfinderScreen.css +36 -0
  139. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  140. package/dist/WatchlistBusinessFormScreen.js +27 -13
  141. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  142. package/dist/flows.d.ts +8 -0
  143. package/dist/flows.js +17 -0
  144. package/dist/index.d.ts +19 -1
  145. package/dist/index.js +14 -0
  146. package/dist/styles.css +3 -0
  147. package/dist/types.d.ts +3 -1
  148. package/package.json +79 -3
  149. package/samples/video-selfie-camera-wide.jpg +0 -0
  150. package/samples/video-selfie-room-wide.jpg +0 -0
  151. package/samples/video-selfie-room.png +0 -0
  152. package/src/BiometricConsentScreen.tsx +192 -0
  153. package/src/BiometricConsentStrings.ts +119 -0
  154. package/src/CaptureScreen.tsx +17 -0
  155. package/src/CurpEnterScreen.tsx +21 -4
  156. package/src/CurpGenerateScreen.tsx +46 -22
  157. package/src/CurpLayout.css +8 -34
  158. package/src/CurpStrings.ts +22 -2
  159. package/src/DemoScreensLayout.css +16 -0
  160. package/src/DemoScreensRedirectScreen.tsx +14 -6
  161. package/src/DemoScreensStrings.ts +5 -0
  162. package/src/DigitalIdUploadErrorScreen.tsx +83 -0
  163. package/src/DigitalIdUploadFileTooLargeScreen.tsx +67 -0
  164. package/src/DigitalIdUploadReviewScreen.tsx +119 -0
  165. package/src/DigitalIdUploadStrings.ts +88 -0
  166. package/src/DigitalIdUploadTutorialScreen.tsx +59 -0
  167. package/src/DocumentCaptureIllustration.tsx +1 -1
  168. package/src/EkybAddressDetailsScreen.tsx +32 -4
  169. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  170. package/src/EkybCountryScreen.tsx +45 -7
  171. package/src/EkybOwnersScreen.tsx +34 -3
  172. package/src/EkybStrings.ts +34 -10
  173. package/src/EkycCountryCatalog.ts +9 -4
  174. package/src/EkycCountryFormScreen.tsx +51 -12
  175. package/src/EkycDetailsScreen.tsx +22 -1
  176. package/src/EkycStrings.ts +14 -2
  177. package/src/ElectronicSignatureLayout.css +6 -0
  178. package/src/EmailInputStrings.ts +12 -1
  179. package/src/EmailInputVerifyScreen.tsx +21 -4
  180. package/src/ErrorScreensScreen.tsx +15 -4
  181. package/src/ErrorScreensStrings.ts +37 -12
  182. package/src/FaceCaptureCapturingScreen.tsx +102 -13
  183. package/src/FaceCaptureLayout.css +69 -4
  184. package/src/FaceCaptureStrings.ts +24 -0
  185. package/src/FaceMatchComparisonScreen.tsx +3 -3
  186. package/src/FaceMatchLayout.css +21 -12
  187. package/src/FormScreen.css +30 -0
  188. package/src/FormScreen.tsx +34 -0
  189. package/src/FormsLongFormScreen.tsx +41 -13
  190. package/src/FormsStrings.ts +12 -1
  191. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  192. package/src/GeolocationStrings.ts +3 -0
  193. package/src/IdCaptureLayout.css +205 -2
  194. package/src/IdCaptureStageScreen.tsx +75 -0
  195. package/src/IdCaptureStrings.ts +19 -0
  196. package/src/ListScreen.tsx +58 -10
  197. package/src/LoaderScreen.tsx +7 -5
  198. package/src/LoadersStrings.ts +18 -5
  199. package/src/OcrReviewErrorScreen.tsx +53 -0
  200. package/src/OcrReviewLayout.css +24 -0
  201. package/src/OcrReviewLoadingScreen.tsx +48 -0
  202. package/src/OcrReviewScreen.tsx +190 -0
  203. package/src/OcrReviewStrings.ts +69 -0
  204. package/src/OcrReviewSuccessScreen.tsx +52 -0
  205. package/src/PermissionsAllowScreen.tsx +9 -10
  206. package/src/PermissionsAskScreen.tsx +105 -0
  207. package/src/PermissionsBrowserStepsScreen.tsx +11 -3
  208. package/src/PermissionsLayout.css +47 -0
  209. package/src/PermissionsStrings.ts +42 -12
  210. package/src/PhoneInputLayout.css +10 -9
  211. package/src/PhoneInputOtpScreen.tsx +23 -3
  212. package/src/PhoneInputStrings.ts +23 -4
  213. package/src/PhoneInputVerifyScreen.tsx +41 -5
  214. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  215. package/src/QualifiedSignatureDocumentScreen.tsx +31 -19
  216. package/src/QualifiedSignatureLayout.css +76 -0
  217. package/src/QualifiedSignatureResultScreen.tsx +28 -26
  218. package/src/QualifiedSignatureStrings.ts +19 -2
  219. package/src/ResultScreen.tsx +33 -2
  220. package/src/ReusabilityCredentialSelectionScreen.tsx +246 -0
  221. package/src/ReusabilityLayout.css +241 -0
  222. package/src/ReusabilityPrivacyPolicyScreen.tsx +109 -0
  223. package/src/ReusabilityStrings.ts +127 -0
  224. package/src/ReviewScreen.tsx +96 -27
  225. package/src/ScreenLayout.css +382 -6
  226. package/src/ScreenNav.css +8 -0
  227. package/src/ScreenNav.tsx +35 -5
  228. package/src/VideoSelfieErrorScreen.tsx +1 -1
  229. package/src/VideoSelfieIdCaptureScreen.tsx +125 -31
  230. package/src/VideoSelfieLayout.css +467 -34
  231. package/src/VideoSelfieSelfieCaptureScreen.tsx +110 -57
  232. package/src/VideoSelfieSpeakScreen.tsx +34 -15
  233. package/src/VideoSelfieStrings.ts +89 -14
  234. package/src/VideoSelfieTurnHeadScreen.tsx +162 -0
  235. package/src/VideoSelfieTutorialScreen.tsx +45 -23
  236. package/src/VideoSelfieWaveScreen.tsx +143 -0
  237. package/src/ViewfinderScreen.css +36 -0
  238. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  239. package/src/WatchlistBusinessStrings.ts +21 -7
  240. package/src/flows.ts +25 -0
  241. package/src/index.ts +88 -3
  242. package/src/styles.css +3 -0
  243. package/src/types.ts +3 -1
@@ -1,41 +1,61 @@
1
1
  import React, { forwardRef, type ReactNode } from 'react';
2
- import type { PrizmaSelfieCaptureState } from '@incodetech/prizma-core-web/selfie-capture';
3
- import { PrizmaCaptureScreen, type PrizmaCaptureScreenProps } from './CaptureScreen';
2
+ import {
3
+ PrizmaSelfieCapture,
4
+ type PrizmaSelfieCaptureState,
5
+ } from '@incodetech/prizma-core-web/selfie-capture';
6
+ import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
7
+ import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
4
8
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
5
- import type { VideoSelfieFeedbackKind, VideoSelfieStrings } from './VideoSelfieStrings';
9
+ import type {
10
+ VideoSelfieDocumentType,
11
+ VideoSelfieFeedbackKind,
12
+ VideoSelfieStrings,
13
+ } from './VideoSelfieStrings';
6
14
  import './ScreenLayout.css';
15
+ import './ViewfinderScreen.css';
16
+ import './VideoSelfieLayout.css';
7
17
 
8
18
  /** Which purple liveness section to render (group 8). */
9
19
  export type VideoSelfieSelfieState = 'start' | 'processing' | 'success' | 'failed';
10
20
 
11
- /** Purple state → Core `PrizmaSelfieCapture` state. Core models the in-flight
12
- * and verdict beats as capture STATES — the ring dissolves into the spinner or
13
- * the status mark in its own slot (DEVIATIONS #11) — so all four are one
14
- * archetype call, not a separate processing screen. */
21
+ /**
22
+ * Purple state → Core `PrizmaSelfieCapture` state, in the OVERLAY recipe.
23
+ *
24
+ * `start` is `filled`, not `empty`: `empty` is Core's *searching* beat and
25
+ * draws the dashed silhouette guide, which `8.0` and every `9.x` message
26
+ * deliberately hide (`silhouette hidden=true` on the frames — the guide
27
+ * belongs to the head-turn steps, which do draw it). `filled` is Core's
28
+ * "window open, neutral ring, no guide" beat, and in overlay mode there is no
29
+ * still to fill it with: the host's feed shows straight through the ring.
30
+ */
15
31
  const CAPTURE_STATE: Record<VideoSelfieSelfieState, PrizmaSelfieCaptureState> = {
16
- start: 'ready',
32
+ start: 'filled',
17
33
  processing: 'uploading',
18
34
  success: 'success',
19
35
  failed: 'error',
20
36
  };
21
37
 
38
+ /** Which beat of the seven-step journey the liveness ring occupies — the
39
+ * microphone grant is 1, the two document sides 2 and 3. */
40
+ const STEP = 4;
41
+
22
42
  export interface VideoSelfieSelfieCaptureScreenProps
23
- extends Omit<
24
- PrizmaCaptureScreenProps,
25
- 'state' | 'feedbackText' | 'statusLabels' | 'verifiedLabel' | 'navLabel'
26
- > {
43
+ extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
27
44
  /** Module copy — this screen reads the `selfieCapture` slice. */
28
45
  strings: VideoSelfieStrings;
29
46
  /** Which purple section of group 8 to render. Default `start`. */
30
47
  state?: VideoSelfieSelfieState;
31
48
  /**
32
- * Which of group 9's ELEVEN live-feedback lines to show under the ring.
33
- * Only meaningful while the ring is live (`state="start"`): the sections of
49
+ * Which of group 9's live-feedback lines to show under the ring. Only
50
+ * meaningful while the ring is live (`state="start"`): the sections of
34
51
  * group 9 are that same frame with a different instruction. Omit for `8.0`,
35
52
  * which shows the plain "Look at your front camera" line.
36
53
  */
37
54
  feedback?: VideoSelfieFeedbackKind;
38
- /** Live camera feed (<video>) or sample still (<img>). */
55
+ /** Which document the journey collects — the Stepper's own `documentType`. */
56
+ documentType?: VideoSelfieDocumentType;
57
+ /** Live camera feed (<video>) or sample still (<img>), full-bleed behind the
58
+ * ring — NOT inside it. The overlay recipe makes the ring a window. */
39
59
  camera?: ReactNode;
40
60
  screenId?: string;
41
61
  overrides?: PrizmaScreenOverrides;
@@ -43,29 +63,41 @@ export interface VideoSelfieSelfieCaptureScreenProps
43
63
  }
44
64
 
45
65
  /**
46
- * Video Selfie — the liveness ring (Archetype B, GUIDELINES.md §6-B).
47
- * ONE screen for FIFTEEN purple sections: `selfie capture 8.0–8.3` and every
48
- * `selfie capture feedback 9.1–9.11` message. They are `state` × `feedbackText`
49
- * variants of Core's `PrizmaSelfieCapture`, which owns the ring, the
50
- * silhouette, the ring offset (40), the instruction gap (32), the instruction
51
- * typography and every verdict treatment. None of it is re-implemented (§7),
52
- * and this file declares no layout at all.
66
+ * Video Selfie — the liveness ring (Archetype D, GUIDELINES.md §6-D; always
67
+ * dark). ONE screen for every purple section of groups 8 and 9: `selfie
68
+ * capture 8.0–8.3` and every `selfie capture feedback 9.x` message. They are
69
+ * `state` × `feedbackText` variants of Core's `PrizmaSelfieCapture`, which is
70
+ * exactly how the component is axed.
71
+ *
72
+ * **Rebuilt 2026-09-30 to follow the frames.** The first pass shipped Core's
73
+ * INLINE recipe — the feed inside a circular window on a WHITE surface, the
74
+ * verdicts dissolving the ring into a centred mark — and logged the gap. On
75
+ * the module page that read as a different product from the rest of the flow:
76
+ * a small ring on white between two full-bleed dark camera screens, with the
77
+ * ring vanishing on the very beats Figma keeps it (owner, 2026-09-30: "they
78
+ * are looking very broken… a broken experience"). Rather than hand-roll the
79
+ * full-bleed composition here, Core's `PrizmaSelfieCapture` grew the
80
+ * `mode="overlay"` + `scrim` axis `PrizmaIdCapture` already carries — it is
81
+ * the Figma library's own `Capture / Selfie overlay` component — and this
82
+ * screen composes it. Nothing about the ring, the scrim, the glow sweeps, the
83
+ * verdict marks or the instruction typography is re-implemented (§7).
84
+ * DEVIATIONS #VS-22.
85
+ *
86
+ * The composition, measured off `8.0` (390×844):
53
87
  *
54
- * **Figma vs Core — Core wins, reported.** These sections draw the camera
55
- * FULL-BLEED behind a scrimmed ring, on a black surface. Core's
56
- * `PrizmaSelfieCapture` puts the feed inside the ring's circular window on a
57
- * light surface, and pins light while the feed is open on purpose — "the
58
- * capture screen stays WHITE in both modes, light is needed for the shot"
59
- * (owner decision 2026-07-14, DEVIATIONS #F-27). Reshaping it here would mean
60
- * re-implementing a Core primitive with a second full-bleed recipe, so the
61
- * screen ships Core's treatment and the divergence is logged, exactly as the
62
- * Face Capture pilot settled it.
88
+ * - the camera runs FULL-BLEED on `.prizma-viewfinder__surface` (`selfie bg
89
+ * image`, 390×844 — it passes under the browser chrome too);
90
+ * - the journey stepper sits in the band the logo nav normally occupies
91
+ * (`top content` y=56, the Stepper 342 wide at x=24), the module-wide
92
+ * treatment `VideoSelfieLayout.css` owns;
93
+ * - the ring is 335 centred in the remaining air (Figma 27,188 → 362,523
94
+ * against a 708-high content area), with the instruction stacked under it.
95
+ * Core's own Spacing/32 gap replaces Figma's 25.5 (the token wins, §4).
63
96
  *
64
- * Figma also draws the journey stepper at the top of all fifteen sections.
65
- * Archetype B has no slot for it and this module does not hand-roll one —
66
- * reported. Nothing collapses without it: the four states and the eleven
67
- * messages already render distinctly through Core's own ring treatments and
68
- * instruction copy.
97
+ * NO verified tag: `8.x` and `9.x` draw nothing between the instruction and
98
+ * the bottom of the screen (GUIDELINES §1 — follow the module's section, not
99
+ * the default). The document stage keeps its tag because its own sections
100
+ * draw one (#VS-3).
69
101
  */
70
102
  export const VideoSelfieSelfieCaptureScreen = forwardRef<
71
103
  HTMLDivElement,
@@ -76,10 +108,12 @@ export const VideoSelfieSelfieCaptureScreen = forwardRef<
76
108
  strings,
77
109
  state = 'start',
78
110
  feedback,
111
+ documentType = 'id',
79
112
  camera,
80
113
  screenId = `video-selfie-selfie-capture-${feedback ?? state}`,
81
114
  overrides,
82
115
  onAction,
116
+ className,
83
117
  ...shell
84
118
  },
85
119
  ref,
@@ -89,31 +123,50 @@ export const VideoSelfieSelfieCaptureScreen = forwardRef<
89
123
  carry Core's own status copy instead (its `statusLabels` slot), which is
90
124
  why the feedback line is scoped to the live state. */
91
125
  const feedbackText =
92
- CAPTURE_STATE[state] === 'ready'
93
- ? feedback
94
- ? slice.feedback[feedback]
95
- : slice.startLabel
96
- : undefined;
126
+ state === 'start' ? (feedback ? slice.feedback[feedback] : slice.startLabel) : undefined;
97
127
 
98
128
  return (
99
- <PrizmaCaptureScreen
129
+ <PrizmaScreenShell
100
130
  ref={ref}
101
- state={CAPTURE_STATE[state]}
102
- cameraOpen={state === 'start'}
103
- camera={camera}
104
- feedbackText={feedbackText}
105
- statusLabels={{
106
- uploading: slice.processingLabel,
107
- success: slice.successLabel,
108
- error: slice.failedLabel,
109
- }}
110
- verifiedLabel={strings.verifiedLabel}
111
- navLabel={strings.navLabel}
112
- screenId={screenId}
113
- overrides={overrides}
114
- onAction={onAction}
131
+ dark
132
+ /* The camera owns the whole window, never a column of it — see the
133
+ contract note in `VideoSelfieLayout.css`. */
134
+ data-pcs-overlay="window"
135
+ className={className}
115
136
  {...shell}
116
- />
137
+ >
138
+ <div className="prizma-viewfinder__surface">{camera}</div>
139
+ <div className="video-selfie-stepper">
140
+ <PrizmaStepper
141
+ steps={7}
142
+ currentState={STEP}
143
+ documentType={documentType}
144
+ completedValueText={strings.stepperCompletedLabel}
145
+ />
146
+ </div>
147
+ <div
148
+ className={[
149
+ 'prizma-viewfinder__content',
150
+ overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
151
+ ]
152
+ .filter(Boolean)
153
+ .join(' ')}
154
+ >
155
+ <div className="video-selfie-band video-selfie-band--ring">
156
+ <PrizmaSelfieCapture
157
+ mode="overlay"
158
+ scrim
159
+ state={CAPTURE_STATE[state]}
160
+ feedbackText={feedbackText}
161
+ statusLabels={{
162
+ uploading: slice.processingLabel,
163
+ success: slice.successLabel,
164
+ error: slice.failedLabel,
165
+ }}
166
+ />
167
+ </div>
168
+ </div>
169
+ </PrizmaScreenShell>
117
170
  );
118
171
  },
119
172
  );
@@ -120,23 +120,31 @@ export const VideoSelfieSpeakScreen = forwardRef<
120
120
  onSecondaryClick: () => onAction?.({ type: 'secondary', screenId }),
121
121
  }}
122
122
  />
123
- ) : stage === 'processing' ? (
124
- <PrizmaModalContent
125
- title={slice.processingLabel}
126
- showButtonGroup={false}
127
- closeLabel={slice.closeLabel}
128
- onClose={() => onAction?.({ type: 'tertiary', screenId })}
129
- body={<PrizmaLoadingSpinner loadingLabel={slice.processingLabel} />}
130
- />
131
- ) : stage === 'success' ? (
123
+ ) : stage === 'processing' || stage === 'success' ? (
124
+ /* The two verdict cards. They carry no buttons, so left alone they came
125
+ out shorter than the prompt they replace and the sheet jumped between
126
+ steps; and Core's Modal renders `body` AFTER the heading, which put
127
+ the spinner and the check UNDER their own label. Both are fixed in
128
+ the module: `--status` gives the card the prompt's measure and centres
129
+ its content, and the mark is passed as the LEADING slot so it stacks
130
+ above the copy the way `10.2` / `10.3` draw it.
131
+
132
+ No ✕ either. Figma draws one, but it is the same call #VS-5 already
133
+ made on the prompt: closing mid-step strands the user with no way
134
+ back, and a dismiss that appears on two of five cards and not the
135
+ other three is the inconsistency, not the fix (owner, 2026-09-30).
136
+ DEVIATIONS #VS-25. */
132
137
  <PrizmaModalContent
133
- title={slice.successLabel}
138
+ title={stage === 'processing' ? slice.processingLabel : slice.successLabel}
139
+ showCloseIcon={false}
134
140
  showButtonGroup={false}
135
- closeLabel={slice.closeLabel}
136
- onClose={() => onAction?.({ type: 'tertiary', screenId })}
137
- body={
141
+ leading={
138
142
  <span className="video-selfie-sheet-status">
139
- <IconStatusPositive size="xl" />
143
+ {stage === 'processing' ? (
144
+ <PrizmaLoadingSpinner loadingLabel={slice.processingLabel} />
145
+ ) : (
146
+ <IconStatusPositive size="xl" />
147
+ )}
140
148
  </span>
141
149
  }
142
150
  />
@@ -170,7 +178,15 @@ export const VideoSelfieSpeakScreen = forwardRef<
170
178
  );
171
179
 
172
180
  return (
173
- <PrizmaScreenShell ref={ref} dark className={className} {...shell}>
181
+ <PrizmaScreenShell
182
+ ref={ref}
183
+ dark
184
+ /* The camera owns the whole window, never a column of it — see the
185
+ contract note in `VideoSelfieLayout.css`. */
186
+ data-pcs-overlay="window"
187
+ className={className}
188
+ {...shell}
189
+ >
174
190
  <div className="prizma-viewfinder__surface">{camera}</div>
175
191
  <div className="video-selfie-stepper">
176
192
  <PrizmaStepper
@@ -214,6 +230,9 @@ export const VideoSelfieSpeakScreen = forwardRef<
214
230
  className={[
215
231
  'video-selfie-sheet-panel',
216
232
  isConfirm ? 'video-selfie-sheet-panel--centered' : '',
233
+ stage === 'processing' || stage === 'success'
234
+ ? 'video-selfie-sheet-panel--status'
235
+ : '',
217
236
  ]
218
237
  .filter(Boolean)
219
238
  .join(' ')}
@@ -38,7 +38,10 @@ export type VideoSelfieTutorialRowKind =
38
38
  | 'questions'
39
39
  | 'speak';
40
40
 
41
- /** The eleven live-feedback messages of group 9, in Figma order. */
41
+ /** The live-feedback messages of group 9, in Figma order. Fourteen since
42
+ * 2026-09-30: `9.12`–`9.14` (head cover · closed eyes · face occlusion) were
43
+ * added to the frames after the module's first build and are on the Web, iOS
44
+ * and Android full-flow maps. */
42
45
  export type VideoSelfieFeedbackKind =
43
46
  | 'dark'
44
47
  | 'blurry'
@@ -50,19 +53,67 @@ export type VideoSelfieFeedbackKind =
50
53
  | 'turnLeft'
51
54
  | 'alignFace'
52
55
  | 'moveCloser'
53
- | 'moveAway';
56
+ | 'moveAway'
57
+ | 'headCover'
58
+ | 'closedEyes'
59
+ | 'faceOcclusion';
60
+
61
+ /** Which head-turn the liveness step is asking for (the `🙂‍↔️ Turn Head` band
62
+ * of the full-flow frames: Left · Right · Upward · Downward, each drawn as a
63
+ * guide beat and a captured beat). */
64
+ export type VideoSelfieTurnDirection = 'left' | 'right' | 'up' | 'down';
54
65
 
55
66
  /** Which face of the document the ID stage is on (groups 3 and 5). */
56
67
  export type VideoSelfieIdSide = 'front' | 'back';
57
68
 
58
- /** Why an ID read was rejected (group 6, the `.1`–`.4` variants). */
59
69
  /**
60
- * REMOVED (2026-08-19). A rejection-REASON axis (`scanFailed | name | photo |
61
- * typeMismatch`) was invented: group 6 draws exactly FOUR sections —
62
- * `6.1.1`/`6.2.1` (front try-again / failed) and `6.3.1`/`6.4.1` (back) — with
63
- * no reason axis at all, so the four kinds multiplied the gallery to 16 error
64
- * cards where the spec has 4. Severity is the only real axis. DEVIATIONS #VS-1.
70
+ * Why an ID read was rejected — the `.1`–`.4` suffix of every group 6 section.
71
+ *
72
+ * RESTORED 2026-09-30. It was removed on 2026-08-19 on the reading that "group
73
+ * 6 draws exactly four sections", because the `📐 Screens specs & behavior`
74
+ * page documents ONE representative per group (`6.1.1`, `6.2.1`, `6.3.1`,
75
+ * `6.4.1`) and the rest are component variants inside it. The `🔸 Full flow
76
+ * map` draws them all, and they carry DIFFERENT copy, not a translation of
77
+ * one line: `6.2.2 - name failed` reads "Name verification failed",
78
+ * `6.2.3 - photo failed` reads "ID photo verification failed",
79
+ * `6.1.4 - type mismatch try again` reads "ID type mismatch, show the same ID".
80
+ * Collapsing them to one line lost three rejections a user can actually hit.
81
+ * DEVIATIONS #VS-1.
65
82
  */
83
+ export type VideoSelfieIdErrorReason = 'scan' | 'name' | 'photo' | 'typeMismatch';
84
+
85
+ /**
86
+ * A live CONDITION the document stage can coach the user out of, as opposed
87
+ * to a rejection it has to report.
88
+ *
89
+ * BOTH legs run five of these before the identity checks — the frames draw
90
+ * them on the back only, and the owner extended them to the front on
91
+ * 2026-09-30 ("I think it would be nice to add them for the front as well").
92
+ * Each one draws its OWN screen: the frame in its
93
+ * matching red treatment with a `Capture feedback` bubble above it saying
94
+ * what to do. They were all wired to the single "ID scan failed, try again"
95
+ * card until 2026-09-30 — the five screens are `frame` nodes rather than
96
+ * component instances in the flow map, so the first trace walked straight
97
+ * past them (owner: "we do have those states in figma").
98
+ *
99
+ * All five map 1:1 onto states Core already ships: four onto
100
+ * `PrizmaIdCapture`'s own `error-*` treatments, and `blurry` onto the plain
101
+ * error border plus the bubble, which is the recipe the owner set for
102
+ * detection results on 2026-07-28 (the border is the state, the message is
103
+ * the bubble).
104
+ */
105
+ /** One coaching bubble's copy: what is wrong, and what to do about it. */
106
+ export interface VideoSelfieConditionCopy {
107
+ title: string;
108
+ subtitle: string;
109
+ }
110
+
111
+ export type VideoSelfieIdCondition =
112
+ | 'wrongSide'
113
+ | 'notAligned'
114
+ | 'blurry'
115
+ | 'glare'
116
+ | 'tooDark';
66
117
 
67
118
  /** Whether the rejection invites a retry (`6.1` / `6.3`) or is terminal
68
119
  * (`6.2` / `6.4`). */
@@ -172,9 +223,26 @@ export interface VideoSelfieStrings {
172
223
  processingLabel: string;
173
224
  /** Accepted label (`3.3` / `5.3`). */
174
225
  successLabel: string;
175
- /** Rejection copy, keyed reason → severity (group 6). */
176
- /** Rejection copy per severity (group 6: try-again vs failed). */
177
- errors: Record<VideoSelfieIdErrorSeverity, string>;
226
+ /** Rejection copy, keyed reason → severity (group 6). The two axes are
227
+ * independent: `6.1.x` / `6.3.x` are the try-again lines, `6.2.x` /
228
+ * `6.4.x` the terminal ones, and the `.1`–`.4` suffix is the reason. The
229
+ * two sides share the copy — only the stepper beat differs. */
230
+ errors: Record<VideoSelfieIdErrorReason, Record<VideoSelfieIdErrorSeverity, string>>;
231
+ /**
232
+ * The five coaching bubbles — title + what to do about it.
233
+ *
234
+ * Only `wrongSide` is keyed by side, because it is the only one that
235
+ * NAMES the face the user should be showing; the other four describe the
236
+ * shot, which is the same problem whichever face is up. Keying all five by
237
+ * side would duplicate four strings per language for nothing.
238
+ */
239
+ conditions: {
240
+ wrongSide: Record<VideoSelfieIdSide, VideoSelfieConditionCopy>;
241
+ notAligned: VideoSelfieConditionCopy;
242
+ blurry: VideoSelfieConditionCopy;
243
+ glare: VideoSelfieConditionCopy;
244
+ tooDark: VideoSelfieConditionCopy;
245
+ };
178
246
  };
179
247
 
180
248
  /** Group 4 — the flip interstitial. */
@@ -193,8 +261,13 @@ export interface VideoSelfieStrings {
193
261
  successLabel: string;
194
262
  /** `8.3` rejected. */
195
263
  failedLabel: string;
196
- /** Group 9 — the eleven live-feedback lines. */
264
+ /** Group 9 — the fourteen live-feedback lines. */
197
265
  feedback: Record<VideoSelfieFeedbackKind, string>;
266
+ /** The head-turn step's own instruction, one per direction (the
267
+ * `🙂‍↔️ Turn Head` band of the full-flow frames). */
268
+ turn: Record<VideoSelfieTurnDirection, string>;
269
+ /** The wave step's instruction and its three checkpoints (`👋 Wave`). */
270
+ wave: { instruction: string; steps: string[]; doneLabel: string };
198
271
  };
199
272
 
200
273
  /** Groups 10 · 11 — the spoken steps. */
@@ -216,7 +289,9 @@ export interface VideoSelfieStrings {
216
289
  stepError: { title: string; ctaLabel: string };
217
290
  /** `14.3` — the whole module blew up. */
218
291
  moduleError: { title: string; ctaLabel: string; quitLabel: string };
219
- /** `14.4` — connectivity. */
220
- noConnection: { title: string; ctaLabel: string; quitLabel: string };
292
+ /** `14.4` — connectivity. The supporting line is the generic family's
293
+ * (`ErrorScreensStrings.noConnection.subtitle`), so this screen reads
294
+ * like every other module's connectivity screen (#F-34, #PI-8). */
295
+ noConnection: { title: string; subtitle?: string; ctaLabel: string; quitLabel: string };
221
296
  };
222
297
  }
@@ -0,0 +1,162 @@
1
+ import React, { forwardRef, type ReactNode } from 'react';
2
+ import {
3
+ IconChevronDown,
4
+ IconChevronLeft,
5
+ IconChevronRight,
6
+ IconChevronUp,
7
+ } from '@incodetech/prizma-core-icons';
8
+ import { PrizmaSelfieCapture } from '@incodetech/prizma-core-web/selfie-capture';
9
+ import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
10
+ import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
11
+ import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
12
+ import type {
13
+ VideoSelfieDocumentType,
14
+ VideoSelfieStrings,
15
+ VideoSelfieTurnDirection,
16
+ } from './VideoSelfieStrings';
17
+ import './ScreenLayout.css';
18
+ import './ViewfinderScreen.css';
19
+ import './VideoSelfieLayout.css';
20
+
21
+ /** The two beats each direction is drawn in: the stage ASKING (dashed guide
22
+ * ring) and the stage having SEEN it (the positive ring). */
23
+ export type VideoSelfieTurnStage = 'ask' | 'done';
24
+
25
+ /**
26
+ * Direction → the mark under the ring, from the canonical bank at 2xl (48) —
27
+ * the size the frames draw it (`element container` 342×48, the glyph centred).
28
+ *
29
+ * Since the Core and Enterprise icon unification (Sofia, 2026-10-06) the
30
+ * mark is the library chevron at 2xl: the four solid circle arrows left the
31
+ * bank (their old names now alias the chevrons). Nothing is drawn here (§10).
32
+ *
33
+ * The mark never mirrors in RTL (Sofia, 2026-10-07): a head turn is a
34
+ * physical direction, so the chevrons keep their LTR orientation in Hebrew.
35
+ * The opt-out is one scoped rule in `VideoSelfieLayout.css`; the chevrons
36
+ * still mirror everywhere else.
37
+ */
38
+ const TURN_ICON: Record<VideoSelfieTurnDirection, ReactNode> = {
39
+ left: <IconChevronLeft size="2xl" />,
40
+ right: <IconChevronRight size="2xl" />,
41
+ up: <IconChevronUp size="2xl" />,
42
+ down: <IconChevronDown size="2xl" />,
43
+ };
44
+
45
+ export interface VideoSelfieTurnHeadScreenProps
46
+ extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
47
+ /** Module copy — this screen reads `selfieCapture.turn`. */
48
+ strings: VideoSelfieStrings;
49
+ /** Which way the user is asked to turn. Default `left`. */
50
+ direction?: VideoSelfieTurnDirection;
51
+ /** Asking, or seen. Default `ask`. */
52
+ stage?: VideoSelfieTurnStage;
53
+ /** Which document the journey collects — the Stepper's own `documentType`. */
54
+ documentType?: VideoSelfieDocumentType;
55
+ /** Live camera feed (<video>) or sample still (<img>), full-bleed behind
56
+ * the ring. */
57
+ camera?: ReactNode;
58
+ screenId?: string;
59
+ overrides?: PrizmaScreenOverrides;
60
+ onAction?: PrizmaScreenActionHandler;
61
+ }
62
+
63
+ /**
64
+ * Video Selfie — the head-turn beats of the liveness sequence (Archetype D,
65
+ * GUIDELINES.md §6-D; always dark). NEW 2026-09-30: the `🙂‍↔️ Turn Head` band
66
+ * of the Web / iOS / Android full-flow frames draws four directions, each in
67
+ * two beats, and none of them existed in the screens package — the module
68
+ * jumped straight from the liveness ring to the spoken steps.
69
+ *
70
+ * The frames are the liveness stage with three changes, and nothing else:
71
+ *
72
+ * - the ring is Core's GUIDE stroke (`ring="dashed"`) while the stage waits
73
+ * for the turn, and the positive ring once it has seen it;
74
+ * - the silhouette guide is ON (Core's `empty`), because the user is being
75
+ * asked to move their face rather than hold it still;
76
+ * - a small arrow chip sits between the ring and the instruction, saying
77
+ * which way.
78
+ *
79
+ * The mark under the ring is a canonical bank glyph at 2xl — the disc is part
80
+ * of the drawing, not something this screen paints. `PrizmaTitle.icon` takes
81
+ * `status | spinner | none` only (DEVIATIONS #10), so the block below the ring
82
+ * is composed here; it declares a gap and nothing else. CORE REQUEST stands: a
83
+ * `glyph` slot on the capture stage, so this and Face Capture's future hint
84
+ * marks share one component. DEVIATIONS #VS-20.
85
+ */
86
+ export const VideoSelfieTurnHeadScreen = forwardRef<
87
+ HTMLDivElement,
88
+ VideoSelfieTurnHeadScreenProps
89
+ >(
90
+ (
91
+ {
92
+ strings,
93
+ direction = 'left',
94
+ stage = 'ask',
95
+ documentType = 'id',
96
+ camera,
97
+ screenId = `video-selfie-turn-${direction}-${stage}`,
98
+ overrides,
99
+ onAction,
100
+ className,
101
+ ...shell
102
+ },
103
+ ref,
104
+ ) => (
105
+ <PrizmaScreenShell
106
+ ref={ref}
107
+ dark
108
+ /* The camera owns the whole window, never a column of it — see the
109
+ contract note in `VideoSelfieLayout.css`. */
110
+ data-pcs-overlay="window"
111
+ className={className}
112
+ {...shell}
113
+ >
114
+ <div className="prizma-viewfinder__surface">{camera}</div>
115
+ <div className="video-selfie-stepper">
116
+ <PrizmaStepper
117
+ steps={7}
118
+ currentState={4}
119
+ documentType={documentType}
120
+ completedValueText={strings.stepperCompletedLabel}
121
+ />
122
+ </div>
123
+ <div
124
+ className={[
125
+ 'prizma-viewfinder__content',
126
+ overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
127
+ ]
128
+ .filter(Boolean)
129
+ .join(' ')}
130
+ >
131
+ <div className="video-selfie-band video-selfie-band--ring">
132
+ <PrizmaSelfieCapture
133
+ mode="overlay"
134
+ scrim
135
+ /* The ring says WHICH WAY: the half on the side being asked for
136
+ is dashed while the stage waits and positive once it has seen
137
+ the turn, and the other half stays the neutral track. The state
138
+ is `filled` on both beats — the verdict lives in the arc, not in
139
+ the whole ring, so nothing else about the stage changes. */
140
+ guide={{ side: direction, state: stage === 'ask' ? 'waiting' : 'seen' }}
141
+ state="filled"
142
+ showFeedback={false}
143
+ />
144
+ {/* Both beats carry the SAME chip and the same instruction — the
145
+ frames change only the ring. Nothing is announced on the turn
146
+ itself, which is why the stage is handed no `statusLabels`: in
147
+ overlay mode a verdict with no copy is a ring colour, not a
148
+ message. */}
149
+ <div className="video-selfie-turn">
150
+ <span className="video-selfie-turn__mark">{TURN_ICON[direction]}</span>
151
+ <p className="prizma-viewfinder__title">
152
+ {strings.selfieCapture.turn[direction]}
153
+ </p>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </PrizmaScreenShell>
158
+ ),
159
+ );
160
+ VideoSelfieTurnHeadScreen.displayName = 'VideoSelfieTurnHeadScreen';
161
+
162
+ export default VideoSelfieTurnHeadScreen;