@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
@@ -12,7 +12,72 @@
12
12
  * inherit all three as correct defaults.
13
13
  */
14
14
 
15
- /* Nothing module-specific is left: the non-status glyph slot this file used to
16
- * own (`.face-capture-glyph`) was the third byte-identical copy of one pattern,
17
- * so it moved to `ScreenLayout.css` as `__glyph` — DEVIATIONS #DC-29, closed.
18
- * The file is kept as the module's landing place for future additions. */
15
+ /* The non-status glyph slot this file used to own (`.face-capture-glyph`) was
16
+ * the third byte-identical copy of one pattern, so it moved to
17
+ * `ScreenLayout.css` as `__glyph` — DEVIATIONS #DC-29, closed. */
18
+
19
+ /* ── Short panels: the capture stack moves up, the ring never changes size ───
20
+ * `PrizmaSelfieCapture` is a fixed stack by design: Spacing/40, the 335 stage,
21
+ * Spacing/32, the instruction, and in manual mode another Spacing/32 and the 64
22
+ * shutter, then Spacing/40 — 583 tall on the manual screen, every number
23
+ * measured off the module Figma (DEVIATIONS #12). Under the nav and above the
24
+ * footer's reserve a closed iPhone Duo leaves 513 (678 − 103 − 62, measured
25
+ * 2026-10-14), so the manual stack ran 70 past its column and the shutter
26
+ * landed on the verified tag (shutter 582–646, tag 606–636). The auto-capture
27
+ * screens are 94 shorter and fit the Duo as authored.
28
+ *
29
+ * The RING is not what gives ground. Camila, 2026-10-14: the face frame is the
30
+ * same size on every screen, because the user has to see their face; if it
31
+ * ever had to shrink it would shrink on every capture screen together, and we
32
+ * avoid that. So the stack moves UP instead: the root's Spacing/40 above and
33
+ * below go (the ring sits flush under the nav row, 40 higher than on the
34
+ * reference) and the two Spacing/32 gaps become Spacing/16, which on the
35
+ * manual screen is 112 back — 103 + 334 + 16 + 41 + 16 + 64 = 574, 32 of air
36
+ * above the tag. Instruction and shutter keep their height, the ring its 334.
37
+ *
38
+ * EVERY capture screen takes it, not only the manual one that needed it
39
+ * (Camila, 2026-10-14: "put all face frames at the height that the manual
40
+ * capture one is, so we don't need a jump there"). The ring not moving between
41
+ * screens is the rule Video Selfie's #VS-24 was written for: a capture window
42
+ * that jumps as the step advances reads as a glitch. So on a short panel the
43
+ * whole module's ring is at 103, auto, manual, loading and verdict alike.
44
+ * Outside the query not one declaration exists, so the reference is
45
+ * byte-identical. DEVIATIONS #20. */
46
+ @container pcs-screen (max-height: 720px) {
47
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture {
48
+ padding-block: 0;
49
+ gap: var(--prizma-spacing-16);
50
+ }
51
+ }
52
+
53
+ /* ── Big panels: the ring grows to the column and the stack moves down ───────
54
+ * The mirror of the block above, and the capture counterpart of the tutorial's
55
+ * #F-36. On the 768 × 1024 iPad the column widens to 468 but the stack kept
56
+ * its 335 ring flush under the nav, so the ring read small (335 in a 420
57
+ * content column, 85px of air either side) and high (its centre at 30% of the
58
+ * panel, where the 390 phone has it at 37%), with 300px of empty column under
59
+ * the instruction (Camila, 2026-10-19: "the face frame too small and way up
60
+ * the screen", on the tablet flow, grid and screen view alike).
61
+ *
62
+ * Two owner decisions, 2026-10-20. The RING GROWS: the 2026-10-14 rule that
63
+ * the ring never changes size was written so it would not SHRINK on a short
64
+ * panel, and on a panel this big the person's face is still what the screen is
65
+ * about, so the stage takes the content column's full measure (420 inside the
66
+ * 468 wide column), the way #F-36 lets the tutorial art grow. Every state
67
+ * grows together (auto, manual, loading, verdict) so the ring still never
68
+ * jumps between steps. The STACK MOVES DOWN BY A FIXED OFFSET, not to the
69
+ * centre: the stack under the ring is a different height on every screen
70
+ * (the manual one carries the shutter), so centring THAT would move the ring
71
+ * between steps, the bug #VS-24 exists for; the same reason Video Selfie's
72
+ * #VS-32 took an offset. Spacing/40 + Spacing/32 above the ring puts its
73
+ * centre at 102 + 72 + 210 = 384, 37.5% of 1024, the reference's own
74
+ * proportion (measured 2026-10-20); the manual screen's shutter ends at 765
75
+ * with the verified tag at 978. Both axes gate it for #F-36's reasons: `min-height: 845px` keeps the
76
+ * 844 reference and every short panel byte-identical, `min-width: 468px` keeps
77
+ * it off phones and foldables where the column stays 390. DEVIATIONS #21. */
78
+ @container pcs-screen ((min-height: 845px) and (min-width: 468px)) {
79
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture {
80
+ max-width: 100%;
81
+ padding-top: calc(var(--prizma-spacing-40) + var(--prizma-spacing-32));
82
+ }
83
+ }
@@ -56,6 +56,30 @@ export interface FaceCaptureStrings {
56
56
  * `initiating` has no instruction in the design; omit it. */
57
57
  capturing: Partial<Record<FaceCaptureCapturingState, string>>;
58
58
 
59
+ /**
60
+ * faceCapture.capturing.extendTime — the accessibility offer that rides any
61
+ * capturing state 20 s before auto capture falls back to manual.
62
+ *
63
+ * It exists for WCAG 2.2.1 Timing Adjustable, satisfied through the *Extend*
64
+ * clause: warn before the limit expires, give the user at least 20 s to
65
+ * extend with a simple action, and let them extend at least ten times. The
66
+ * 20 s is that threshold, not a chosen duration — which is why `description`
67
+ * is a whole string and not a number the screen formats: ten locales write
68
+ * that sentence ten different ways, and the count has to live inside it.
69
+ *
70
+ * Shown only when accessibility mode is ON for the flow (a per-flow
71
+ * dashboard setting). With it off, nothing about today's capture changes.
72
+ * Optional for that reason: a host that never turns it on never writes it.
73
+ */
74
+ extendTime?: {
75
+ /** Bold first line — "Need more time?" */
76
+ title: string;
77
+ /** "Manual capture starts in 20 s" — carries the count, see above. */
78
+ description: string;
79
+ /** The action that keeps the user on auto — "Extend auto-capture". */
80
+ linkLabel: string;
81
+ };
82
+
59
83
  /** faceCapture.manualCapture.default */
60
84
  manualCapture: {
61
85
  /** "Align your face within the silhouette and press the button to capture" */
@@ -17,9 +17,9 @@ import './FaceMatchLayout.css';
17
17
  export type FaceMatchPhotoRole = 'selfie' | 'id' | 'nfc' | 'secondId';
18
18
 
19
19
  /** Host-supplied portraits, one per role the chosen variant asks for. A role
20
- * left out renders the empty state: a Brand/50 disc, marked with
21
- * `.face-match-cluster__photo--empty`. Interim treatment (2026-09-25) while
22
- * the designed no-photo state is drawn; see FaceMatchLayout.css. */
20
+ * left out renders the empty state (capture missing or not retrievable): a
21
+ * grey Surface/Neutral/100 disc under its usual label tooltip, marked with
22
+ * `.face-match-cluster__photo--empty`. See FaceMatchLayout.css. */
23
23
  export type FaceMatchComparisonPhotos = Partial<Record<FaceMatchPhotoRole, ReactNode>>;
24
24
 
25
25
  /**
@@ -57,8 +57,14 @@
57
57
 
58
58
  /* The disc's diameter, named once. `--matching`'s rosette and `--animated`'s
59
59
  * convergence both have to know it, and a second literal is a second thing to
60
- * forget. Still a design literal, still DEVIATIONS #FM-22 — just not repeated. */
61
- --face-match-photo: 160px;
60
+ * forget. Still a design literal, still DEVIATIONS #FM-22 — just not repeated.
61
+ *
62
+ * 160 at most, and smaller when two discs and their gap do not fit the
63
+ * content column: on a 320 phone the column is 272, so each disc takes
64
+ * (272 − 16) / 2 = 128 (Camila, 2026-09-28). `100cqi` is the screen shell's
65
+ * width (ScreenShell.css, `container-type: size`), less its two Spacing/24
66
+ * gutters. At 390 and up this resolves to 160, unchanged. */
67
+ --face-match-photo: min(160px, calc((100cqi - 2 * var(--prizma-spacing-24) - var(--prizma-spacing-16)) / 2));
62
68
  /* Half the distance between two neighbouring discs: what one of them has to
63
69
  * travel to reach the pair's centre line. */
64
70
  --face-match-half-step: calc((var(--face-match-photo, 160px) + var(--prizma-spacing-16)) / 2);
@@ -118,16 +124,16 @@
118
124
  background: var(--prizma-surface-neutral-100);
119
125
  }
120
126
 
121
- /* No photo supplied for this role.
127
+ /* No photo for this role: the capture was never taken, or could not be
128
+ * retrieved.
122
129
  *
123
- * INTERIM (2026-09-25, owner call): colour is the whole state for now, so the
124
- * slot reads as a portrait that belongs here rather than an undesigned hole.
125
- * Brand/50 is the same disc PrizmaIllustrationAvatar sits on, which is where
126
- * the designed state is most likely to land. The real treatment (bust,
127
- * outline, or copy) replaces this rule; the modifier and its class name stay,
128
- * so swapping it in is a one-rule change and nothing downstream moves. */
130
+ * Owner call 2026-09-28 (replaces the 2026-09-25 Brand/50 interim): a plain
131
+ * grey disc, Surface/Neutral/100, the same neutral Core puts behind an image
132
+ * that is not there yet (CaptureDigitalId.css, list.css). The callout above it
133
+ * still renders, so the tooltip names which capture is missing. The modifier
134
+ * stays a named state so a richer treatment is still a one-rule change. */
129
135
  .face-match-cluster__photo--empty {
130
- background: var(--prizma-surface-brand-50);
136
+ background: var(--prizma-surface-neutral-100);
131
137
  }
132
138
 
133
139
  .face-match-cluster__photo > img,
@@ -153,11 +159,14 @@
153
159
  }
154
160
 
155
161
  .face-match-cluster--matching .face-match-cluster__face + .face-match-cluster__face {
156
- margin-left: -44px; /* hardcoded — Figma converged overlap; no token (verified absent) */
162
+ /* Figma converged overlap, 44 on the 160 disc; no token (verified absent).
163
+ * Kept as that ratio so the rosette holds its shape on a smaller disc. */
164
+ margin-left: calc(var(--face-match-photo, 160px) * -44 / 160);
157
165
  }
158
166
 
159
167
  .face-match-cluster--matching .face-match-cluster__row + .face-match-cluster__row {
160
- margin-top: calc(-2 * var(--prizma-spacing-40));
168
+ /* Half a disc: 80 on the 160 disc (`2 × Spacing/40`), proportional below it. */
169
+ margin-top: calc(var(--face-match-photo, 160px) / -2);
161
170
  }
162
171
 
163
172
  /* ── The comparison, moving (`phase` on FaceMatchComparisonScreen) ───────────
@@ -29,6 +29,20 @@
29
29
  padding-top: var(--pfs-fields-offset, var(--prizma-spacing-32));
30
30
  }
31
31
 
32
+ /* ── Field stack pinned to the CTA band ──────────────────────────────────────
33
+ * `fieldsAlign="end"` on `PrizmaFormScreen`. The stack keeps its `flex: 1`
34
+ * column and simply packs its rows at the bottom, so the space between the
35
+ * title block and the first row is what is left over rather than a value
36
+ * anybody declares. The `margin-bottom` is the frame's own Spacing/16 between
37
+ * the card and the Button group (`biometricConsent.empty`, 2001:16361 — the
38
+ * CONTENT column's gap), the same value the scrolling stack uses for the same
39
+ * seam, so the two shapes agree on how far a row sits from Continue. Biometric
40
+ * Consent is the first consumer. DEVIATIONS #BC-1. */
41
+ .prizma-form-screen__fields--end {
42
+ justify-content: flex-end;
43
+ margin-bottom: var(--prizma-spacing-16);
44
+ }
45
+
32
46
  /* ── Scrolling field stack ───────────────────────────────────────────────────
33
47
  * Screens are authored at 390×844 and never reflow (DEVIATIONS #17), but some
34
48
  * Form sections draw a middle column taller than the frame and let it pan:
@@ -129,6 +143,22 @@
129
143
  flex: none;
130
144
  width: calc(50% - var(--prizma-spacing-16) / 2);
131
145
  min-width: 0;
146
+ position: relative;
147
+ }
148
+ /* An error or helper line under ONE field of a pair made that field taller,
149
+ * and the bottom alignment above lifted its input above its neighbour's
150
+ * (Elena, 2026-10-02: "estos inputs tienen que estar alineados"). The line
151
+ * hangs below its input instead, out of the alignment, and the row reserves
152
+ * its height so the next row does not ride over it. */
153
+ .prizma-form-screen--responsive .prizma-form-screen__row > * > [class$="__error"],
154
+ .prizma-form-screen--responsive .prizma-form-screen__row > * > [class$="__helper"] {
155
+ position: absolute;
156
+ top: 100%;
157
+ left: 0;
158
+ margin-top: var(--prizma-spacing-8);
159
+ }
160
+ .prizma-form-screen--responsive .prizma-form-screen__row:has(> * > [class$="__error"], > * > [class$="__helper"]) {
161
+ padding-bottom: calc(var(--prizma-spacing-8) + var(--prizma-body-m-regular-line-height));
132
162
  }
133
163
 
134
164
  /* Continue and Back share a row: Back on the left, Continue on the right, as
@@ -62,6 +62,13 @@ export interface PrizmaFormScreenProps
62
62
  * body text.
63
63
  */
64
64
  linkLabel?: string;
65
+ /**
66
+ * Renders `linkLabel` in Core's disabled treatment, for a link that exists
67
+ * but cannot be pressed yet — Phone Input's "Resend code in 30s" while the
68
+ * resend timer runs (Figma `phoneNumber.OTP.typing`, Tertiary / Link ·
69
+ * Disabled).
70
+ */
71
+ linkDisabled?: boolean;
65
72
  /**
66
73
  * A composed line below the primary: body copy with inline links, e.g. Phone
67
74
  * Input's "Didn't receive the code? **Resend code** or **Change phone
@@ -98,6 +105,27 @@ export interface PrizmaFormScreenProps
98
105
  * eKYB, Data Sharing Consent); it is the archetype's now. DEVIATIONS #DSC-2.
99
106
  */
100
107
  scrollFields?: boolean;
108
+ /**
109
+ * Where the field stack sits in the column when it is SHORTER than the
110
+ * space it has. `start` (the default) hangs it off the title block, which is
111
+ * every form so far. `end` pins it to the CTA band instead, leaving the space
112
+ * between the title and the stack empty — Biometric Consent's section draws
113
+ * exactly that: a `justify-between` middle with the Title at the top and the
114
+ * one consent card flush against Continue, Spacing/16 above it
115
+ * (`biometricConsent.empty`, 2001:16361). Selected here so the module
116
+ * declares no layout of its own; the rule lives once in `FormScreen.css`.
117
+ * DEVIATIONS #BC-1.
118
+ */
119
+ fieldsAlign?: 'start' | 'end';
120
+ /**
121
+ * A shell-level layer painted OVER the whole screen — the same slot archetype
122
+ * A carries for Document Capture's method sheet (#F-32, #DC-30): a scrim plus
123
+ * Core's `PrizmaBottomSheetContent` or `PrizmaModalContent`, presented on top
124
+ * of the dimmed form. It cannot ride in the content column: it is absolutely
125
+ * positioned against the shell. Biometric Consent's quit confirmation is the
126
+ * first Form consumer (DEVIATIONS #BC-8).
127
+ */
128
+ overlay?: ReactNode;
101
129
  /**
102
130
  * Let the field stack change SHAPE on a wide screen: `PrizmaFormRow` pairs
103
131
  * become two lanes and Continue + Back share a row, once the shell's own
@@ -140,12 +168,15 @@ export const PrizmaFormScreen = forwardRef<HTMLDivElement, PrizmaFormScreenProps
140
168
  ctaLoading = false,
141
169
  ctaLoadingLabel,
142
170
  linkLabel,
171
+ linkDisabled,
143
172
  linkContent,
144
173
  verifiedLabel,
145
174
  children,
146
175
  fieldsOffset,
147
176
  fieldsGap,
148
177
  scrollFields = false,
178
+ fieldsAlign = 'start',
179
+ overlay,
149
180
  responsive = false,
150
181
  navLabel,
151
182
  screenId,
@@ -179,6 +210,7 @@ export const PrizmaFormScreen = forwardRef<HTMLDivElement, PrizmaFormScreenProps
179
210
  className={[
180
211
  'prizma-form-screen__fields',
181
212
  scrollFields ? 'prizma-form-screen__fields--scroll' : null,
213
+ fieldsAlign === 'end' ? 'prizma-form-screen__fields--end' : null,
182
214
  ]
183
215
  .filter(Boolean)
184
216
  .join(' ')}
@@ -217,6 +249,7 @@ export const PrizmaFormScreen = forwardRef<HTMLDivElement, PrizmaFormScreenProps
217
249
  <PrizmaButton
218
250
  variant="tertiary-link"
219
251
  size="L"
252
+ disabled={linkDisabled}
220
253
  onClick={() => onAction?.({ type: 'secondary', screenId })}
221
254
  >
222
255
  {linkLabel}
@@ -231,6 +264,7 @@ export const PrizmaFormScreen = forwardRef<HTMLDivElement, PrizmaFormScreenProps
231
264
  </div>
232
265
  ) : null}
233
266
  </div>
267
+ {overlay}
234
268
  </PrizmaScreenShell>
235
269
  ),
236
270
  );
@@ -51,7 +51,9 @@ const PRESET_ROWS: Record<FormsLongFormPreset, FormsLongFormRow[]> = {
51
51
  { key: 'idNumber', kind: 'text', requirement: 'required' },
52
52
  { key: 'email', kind: 'email', requirement: 'required' },
53
53
  { key: 'residence', kind: 'country', requirement: 'required' },
54
- { key: 'dob', kind: 'date', requirement: 'required' },
54
+ // Date of birth carries NO asterisk (owner Elena, 2026-09-30), so the
55
+ // `emptyError` state flags the other three rows and leaves it at rest.
56
+ { key: 'dob', kind: 'date', requirement: 'none' },
55
57
  ],
56
58
  };
57
59
 
@@ -131,6 +133,14 @@ export interface FormsLongFormScreenProps
131
133
  * into the CTA band — the half a module stylesheet kept losing. It is a no-op
132
134
  * on the short `name` preset, whose rows never overflow. DEVIATIONS #DSC-2,
133
135
  * restating #CURP-1.
136
+ *
137
+ * `emptyError` is the GUIDELINES §6-E empty-submit rule (Elena 2026-09-30):
138
+ * the user pressed the CTA with the form still empty. Every REQUIRED row
139
+ * renders empty, with no placeholder, in Core's own error state carrying
140
+ * `strings.requiredError`; optional and unmarked rows stay on their resting
141
+ * surface with no error. The CTA greys there and ONLY there: the resting
142
+ * `default` keeps it enabled, so the user can press it to reach the error —
143
+ * the `FormsFieldScreen` `emptyError` precedent applied to a stack.
134
144
  */
135
145
  export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScreenProps>(
136
146
  (
@@ -152,6 +162,7 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
152
162
  ) => {
153
163
  const copy = strings.longForm[preset];
154
164
  const busy = state === 'loading';
165
+ const emptyError = state === 'emptyError';
155
166
  const countryList = countries ?? [];
156
167
 
157
168
  // The Optional / Required pair documents the eKYC form specifically; no
@@ -174,8 +185,15 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
174
185
  const fieldCopy = copy.fields[row.key];
175
186
  const label = labelFor(row);
176
187
  const required = row.requirement === 'required';
177
- const value = values?.[row.key] ?? '';
178
- const focused = !busy && focusedField === row.key;
188
+ // `emptyError` is an EMPTY submit: every row renders empty, whatever
189
+ // values the host passed.
190
+ const value = emptyError ? '' : (values?.[row.key] ?? '');
191
+ const focused = !busy && !emptyError && focusedField === row.key;
192
+ // Only REQUIRED rows flag the empty submit. Their placeholder goes, as
193
+ // on the drawn `forms.IDnumber.error`; optional rows stay at rest.
194
+ const invalid = emptyError && required;
195
+ const errorText = invalid ? strings.requiredError : undefined;
196
+ const placeholder = invalid ? undefined : fieldCopy?.placeholder;
179
197
 
180
198
  switch (row.kind) {
181
199
  case 'country':
@@ -185,12 +203,14 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
185
203
  label={label}
186
204
  required={required}
187
205
  requiredHint={strings.requiredHint}
188
- placeholder={fieldCopy?.placeholder}
206
+ placeholder={placeholder}
189
207
  options={countryList.map((c) => ({ value: c.code, label: c.name }))}
190
208
  value={value || null}
191
209
  searchable
192
210
  searchPlaceholder={copy.searchPlaceholder}
193
211
  disabled={busy}
212
+ error={invalid}
213
+ errorText={errorText}
194
214
  forceState={focused ? 'focused' : undefined}
195
215
  renderLeading={(option) => {
196
216
  const match = countryList.find((c) => c.code === option.value);
@@ -205,10 +225,12 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
205
225
  label={label}
206
226
  required={required}
207
227
  requiredHint={strings.requiredHint}
208
- placeholder={fieldCopy?.placeholder}
228
+ placeholder={placeholder}
209
229
  options={options?.[row.key] ?? []}
210
230
  value={value || null}
211
231
  disabled={busy}
232
+ error={invalid}
233
+ errorText={errorText}
212
234
  forceState={focused ? 'focused' : undefined}
213
235
  />
214
236
  );
@@ -220,9 +242,13 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
220
242
  label={label}
221
243
  required={required}
222
244
  requiredHint={strings.requiredHint}
223
- placeholder={fieldCopy?.placeholder}
245
+ /* Core falls back to its DD/MM/YYYY mask on an undefined
246
+ placeholder, so the error row passes an explicit empty one. */
247
+ placeholder={invalid ? '' : fieldCopy?.placeholder}
224
248
  defaultValue={value}
225
249
  disabled={busy}
250
+ error={invalid}
251
+ errorText={errorText}
226
252
  calendarLabel={copy.calendarLabel}
227
253
  />
228
254
  );
@@ -234,7 +260,7 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
234
260
  label={label}
235
261
  required={required}
236
262
  requiredHint={strings.requiredHint}
237
- placeholder={fieldCopy?.placeholder}
263
+ placeholder={placeholder}
238
264
  defaultValue={value}
239
265
  type={row.kind === 'email' ? 'email' : 'text'}
240
266
  inputMode={row.kind === 'email' ? 'email' : undefined}
@@ -242,6 +268,8 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
242
268
  autoCapitalize={row.kind === 'email' ? 'none' : undefined}
243
269
  spellCheck={false}
244
270
  disabled={busy}
271
+ error={invalid}
272
+ errorText={errorText}
245
273
  /* Figma draws the clear ✕ only on the focused row of
246
274
  `longform.focused`; every other row draws none. */
247
275
  clearable={focused}
@@ -266,12 +294,12 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
266
294
  title={copy.title}
267
295
  subtitle={copy.subtitle}
268
296
  ctaLabel={copy.ctaLabel}
269
- /* `longform.default` and `longform.customizable`'s default section grey
270
- the CTA until something is entered; `forms.name` draws it ENABLED on
271
- the same empty form. Figma contradicts its own "optimistic rule"
272
- sticky here, so each section is followed literally — DEVIATIONS
273
- #FDE-3. */
274
- ctaDisabled={state === 'default' && preset !== 'name'}
297
+ /* GUIDELINES §6-E empty-submit rule (Elena 2026-09-30): the CTA stays
298
+ ENABLED on the resting empty form, every preset alike, so the user
299
+ can press it; it greys only on `emptyError`, the state that press
300
+ leads to. This supersedes the per-section greying #FDE-3 followed
301
+ on `longform.default` and `longform.customizable`. */
302
+ ctaDisabled={emptyError}
275
303
  ctaLoading={busy}
276
304
  ctaLoadingLabel={copy.ctaLoadingLabel}
277
305
  /* The four- and five-row stacks are taller than the 844 frame; the
@@ -127,13 +127,18 @@ export type FormsLongFormPreset = 'name' | 'profile' | 'eKyc';
127
127
  * - `loading` — `longform.loading`: the whole form greyed, spinner in the CTA
128
128
  * - `optional` — the "Optional / Required" documentation pair: the same form
129
129
  * with mixed required and optional markers
130
+ * - `emptyError` — the user pressed the CTA with the form still empty: every
131
+ * REQUIRED row renders empty in Core's error state with
132
+ * `requiredError`, optional and unmarked rows stay clean, CTA
133
+ * greyed (GUIDELINES §6-E empty-submit rule, Elena 2026-09-30)
130
134
  */
131
135
  export type FormsLongFormState =
132
136
  | 'default'
133
137
  | 'focused'
134
138
  | 'filled'
135
139
  | 'loading'
136
- | 'optional';
140
+ | 'optional'
141
+ | 'emptyError';
137
142
 
138
143
  /** Copy for one single-field section. */
139
144
  export interface FormsFieldCopy {
@@ -218,6 +223,12 @@ export interface FormsStrings {
218
223
  * the label string by the screen. See DEVIATIONS #FDE-4.
219
224
  */
220
225
  optionalHint: string;
226
+ /**
227
+ * The error row every REQUIRED field shows on the `emptyError` long-form
228
+ * state ("This field is required"). Maps to the SDK's
229
+ * `dynamicForms.fieldRequired` key. GUIDELINES §6-E empty-submit rule.
230
+ */
231
+ requiredError: string;
221
232
 
222
233
  /** The single-field sections. */
223
234
  fields: Record<FormsFieldKey, FormsFieldCopy>;
@@ -97,9 +97,13 @@ export interface GeolocationBrowserStepsScreenProps
97
97
  * The rows are Core's `PrizmaCardListCommonIssues` verbatim; only the leading
98
98
  * glyph is chosen here, from the canonical bank (see `STEP_ICONS`).
99
99
  *
100
+ * No subtitle: the browser name (`label`) is NOT drawn under the title (owner
101
+ * call, Elena 2026-09-30). The label stays in the strings as the section's
102
+ * identifier for hosts and catalogs; the screen itself shows title + steps.
103
+ *
100
104
  * The title block is LEFT-aligned for this whole family. Every
101
- * `Geolocation.allowLocation.<browser>` section starts the two-line headline —
102
- * and the browser label under it — flush at the gutter, over left-led step
105
+ * `Geolocation.allowLocation.<browser>` section starts the two-line headline
106
+ * flush at the gutter, over left-led step
103
107
  * rows; a centred headline above a left-aligned list reads as a mistake. The
104
108
  * screen therefore DEFAULTS the archetype's own `align` override to `left`
105
109
  * rather than declaring any alignment of its own, and a host that passes
@@ -147,7 +151,6 @@ export const GeolocationBrowserStepsScreen = forwardRef<
147
151
  <PrizmaListScreen
148
152
  ref={ref}
149
153
  title={strings.browserSteps.title}
150
- subtitle={slice?.label}
151
154
  ctaLabel={strings.browserSteps.ctaLabel}
152
155
  secondaryCtaLabel={showSkip ? strings.browserSteps.skipLabel : undefined}
153
156
  verifiedLabel={strings.verifiedLabel}
@@ -123,6 +123,9 @@ export interface GeolocationBrowserSteps {
123
123
  * rows and would render as one screen three times. Every browser therefore
124
124
  * SHOULD carry a label, extending the precedent Figma itself sets on Safari.
125
125
  * Still optional so a host that ships its own logo artwork can drop it.
126
+ *
127
+ * 2026-09-30 (Elena): the screen no longer RENDERS it as a subtitle. It is
128
+ * kept as the section's name for catalogs and hosts, not as on-screen copy.
126
129
  */
127
130
  label?: string;
128
131
  /** The steps, in order. */