@incodetech/prizma-core-screens 0.2.3 → 0.2.7

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 (116) hide show
  1. package/dist/CurpEnterScreen.d.ts +7 -0
  2. package/dist/CurpEnterScreen.js +17 -3
  3. package/dist/CurpGenerateScreen.d.ts +8 -0
  4. package/dist/CurpGenerateScreen.js +28 -13
  5. package/dist/CurpLayout.css +8 -34
  6. package/dist/CurpStrings.d.ts +16 -2
  7. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  8. package/dist/EkybAddressDetailsScreen.js +25 -7
  9. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  10. package/dist/EkybBusinessDetailsScreen.js +23 -6
  11. package/dist/EkybCountryScreen.d.ts +13 -5
  12. package/dist/EkybCountryScreen.js +27 -9
  13. package/dist/EkybOwnersScreen.d.ts +8 -0
  14. package/dist/EkybOwnersScreen.js +26 -3
  15. package/dist/EkybStrings.d.ts +27 -10
  16. package/dist/EkycCountryCatalog.d.ts +4 -1
  17. package/dist/EkycCountryCatalog.js +5 -3
  18. package/dist/EkycCountryFormScreen.d.ts +12 -3
  19. package/dist/EkycCountryFormScreen.js +28 -10
  20. package/dist/EkycDetailsScreen.d.ts +7 -0
  21. package/dist/EkycDetailsScreen.js +21 -4
  22. package/dist/EkycStrings.d.ts +14 -2
  23. package/dist/ElectronicSignatureLayout.css +6 -0
  24. package/dist/EmailInputStrings.d.ts +11 -1
  25. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  26. package/dist/EmailInputVerifyScreen.js +17 -3
  27. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  28. package/dist/FaceMatchLayout.css +21 -12
  29. package/dist/FormScreen.css +16 -0
  30. package/dist/FormsLongFormScreen.d.ts +8 -0
  31. package/dist/FormsLongFormScreen.js +34 -13
  32. package/dist/FormsStrings.d.ts +11 -1
  33. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  34. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  35. package/dist/GeolocationStrings.d.ts +3 -0
  36. package/dist/IdCaptureLayout.css +40 -0
  37. package/dist/ListScreen.d.ts +2 -0
  38. package/dist/ListScreen.js +1 -1
  39. package/dist/PhoneInputLayout.css +10 -9
  40. package/dist/PhoneInputStrings.d.ts +11 -1
  41. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  42. package/dist/PhoneInputVerifyScreen.js +19 -6
  43. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  44. package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
  45. package/dist/QualifiedSignatureDocumentScreen.js +18 -16
  46. package/dist/QualifiedSignatureLayout.css +76 -0
  47. package/dist/QualifiedSignatureStrings.d.ts +10 -0
  48. package/dist/ScreenNav.css +8 -0
  49. package/dist/ScreenNav.d.ts +15 -2
  50. package/dist/ScreenNav.js +10 -3
  51. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  52. package/dist/VideoSelfieIdCaptureScreen.js +44 -13
  53. package/dist/VideoSelfieLayout.css +164 -33
  54. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  55. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  56. package/dist/VideoSelfieSpeakScreen.js +18 -1
  57. package/dist/VideoSelfieStrings.d.ts +78 -12
  58. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  59. package/dist/VideoSelfieTurnHeadScreen.js +63 -0
  60. package/dist/VideoSelfieTutorialScreen.js +13 -5
  61. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  62. package/dist/VideoSelfieWaveScreen.js +62 -0
  63. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  64. package/dist/WatchlistBusinessFormScreen.js +27 -13
  65. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +2 -0
  68. package/dist/types.d.ts +3 -1
  69. package/package.json +14 -3
  70. package/samples/video-selfie-room.png +0 -0
  71. package/src/CurpEnterScreen.tsx +21 -4
  72. package/src/CurpGenerateScreen.tsx +46 -22
  73. package/src/CurpLayout.css +8 -34
  74. package/src/CurpStrings.ts +22 -2
  75. package/src/EkybAddressDetailsScreen.tsx +32 -4
  76. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  77. package/src/EkybCountryScreen.tsx +28 -7
  78. package/src/EkybOwnersScreen.tsx +34 -3
  79. package/src/EkybStrings.ts +29 -10
  80. package/src/EkycCountryCatalog.ts +9 -4
  81. package/src/EkycCountryFormScreen.tsx +51 -12
  82. package/src/EkycDetailsScreen.tsx +22 -1
  83. package/src/EkycStrings.ts +14 -2
  84. package/src/ElectronicSignatureLayout.css +6 -0
  85. package/src/EmailInputStrings.ts +12 -1
  86. package/src/EmailInputVerifyScreen.tsx +21 -4
  87. package/src/FaceMatchComparisonScreen.tsx +3 -3
  88. package/src/FaceMatchLayout.css +21 -12
  89. package/src/FormScreen.css +16 -0
  90. package/src/FormsLongFormScreen.tsx +41 -13
  91. package/src/FormsStrings.ts +12 -1
  92. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  93. package/src/GeolocationStrings.ts +3 -0
  94. package/src/IdCaptureLayout.css +40 -0
  95. package/src/ListScreen.tsx +10 -1
  96. package/src/PhoneInputLayout.css +10 -9
  97. package/src/PhoneInputStrings.ts +12 -1
  98. package/src/PhoneInputVerifyScreen.tsx +21 -5
  99. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  100. package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
  101. package/src/QualifiedSignatureLayout.css +76 -0
  102. package/src/QualifiedSignatureStrings.ts +10 -0
  103. package/src/ScreenNav.css +8 -0
  104. package/src/ScreenNav.tsx +35 -5
  105. package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
  106. package/src/VideoSelfieLayout.css +164 -33
  107. package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
  108. package/src/VideoSelfieSpeakScreen.tsx +25 -14
  109. package/src/VideoSelfieStrings.ts +85 -12
  110. package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
  111. package/src/VideoSelfieTutorialScreen.tsx +34 -23
  112. package/src/VideoSelfieWaveScreen.tsx +135 -0
  113. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  114. package/src/WatchlistBusinessStrings.ts +21 -7
  115. package/src/index.ts +14 -0
  116. package/src/types.ts +3 -1
@@ -63,6 +63,27 @@
63
63
  z-index: 1;
64
64
  }
65
65
 
66
+ /* ── The face ring sits in the SAME place on every screen ────────────────────
67
+ * Figma puts `ring-uploading` at (27, 188) on every frame that draws it —
68
+ * `8.x`, `9.x`, the four head turns and the wave — and never moves it. Left
69
+ * to the flex band the ring drifted instead, because what sits UNDER it
70
+ * changes from screen to screen: an instruction on `8.0`, a spinner and a
71
+ * label on `8.1`, an arrow chip plus a line on the turns, nothing at all
72
+ * behind the wave's card. Centring a taller stack pushes the ring up
73
+ * (measured 150 / 165 / 185 across three screens where the frames say 188),
74
+ * and a capture window that moves between steps reads as a glitch.
75
+ *
76
+ * So the ring band is TOP-aligned and the offset is named once. 188 is
77
+ * Figma's; 84 is where the content column starts here (the status-bar reserve
78
+ * plus the 37 stepper band); 40 is Core's own padding above the stage inside
79
+ * `PrizmaSelfieCapture`. What is left is the air this rule owns. Everything
80
+ * under the ring then flows below it, which is what the frames draw.
81
+ * DEVIATIONS #VS-24. */
82
+ .video-selfie-band--ring {
83
+ justify-content: flex-start;
84
+ padding-top: calc(188px - var(--pcs-status-bar, 47px) - 37px - var(--prizma-spacing-40));
85
+ }
86
+
66
87
  /* ── Recording indicator (10.1 / 11.1 / 11.3 / 11.5) ─────────────────────────
67
88
  * A sound-wave glyph over the "Recording…" HUD line. The glyph ink is Core's
68
89
  * own Static camera ink — the token `PrizmaCaptureManual` uses for the shutter
@@ -106,6 +127,33 @@
106
127
  align-items: center;
107
128
  }
108
129
 
130
+ /* The two verdict cards (`10.2` processing, `10.3` success) carry no buttons,
131
+ * so their content stack is two rows where the prompt's is three — left alone
132
+ * they came out ~70px shorter and the sheet jumped as the step advanced. The
133
+ * owner's call (2026-09-30) is that every card in the step is the same size,
134
+ * so the card takes the prompt's measure and centres its content in it.
135
+ *
136
+ * 184 is Figma's own `modal-light` height on `10.0` / `10.1`, which is what
137
+ * the prompt renders at; it is the module's geometry, not a token, and it is
138
+ * a MINIMUM — the card still grows if a translation needs more room.
139
+ * DEVIATIONS #VS-25. */
140
+ /* ⚠ The card, not the wrapper, has to do the centring. `height: 100%` on the
141
+ * wrapper resolved to `auto` — Core's `.prizma-modal` is not a flex container
142
+ * and its height is content-driven, so a percentage height had nothing to
143
+ * measure against and the block stayed pinned to the top (owner, 2026-09-30:
144
+ * "these are aligned to the top instead of centered"). Making the CARD the
145
+ * flex column is what actually distributes the extra height. */
146
+ .video-selfie-sheet-panel--status .prizma-modal {
147
+ min-height: 184px;
148
+ display: flex;
149
+ flex-direction: column;
150
+ justify-content: center;
151
+ }
152
+
153
+ .video-selfie-sheet-panel--status .prizma-modal__title {
154
+ text-align: center;
155
+ }
156
+
109
157
  /* The card's verdict mark (10.3 / 11.x success). The glyph itself is the
110
158
  * canonical bank icon Core's own `PrizmaTitle` uses for `status: success`
111
159
  * (`IconStatusPositive` at Title/Icon/Size); only the INK is set here, because
@@ -155,10 +203,49 @@
155
203
  color: var(--prizma-text-body-500-secondary);
156
204
  }
157
205
 
206
+ /* ── The document frame's own measure ────────────────────────────────────────
207
+ * `3.x` / `5.x` / `6.x` all draw `ID Capture Frame` at **286 × 455**, centred
208
+ * in the 390 column (52 either side) — and they keep that measure on every
209
+ * device section in the file: 320, 368, 390, 430 and the 892-wide Pixel Fold
210
+ * all show the same 286-wide frame, only the air around it changes.
211
+ *
212
+ * Core's `PrizmaIdCapture` is `width: 100%` + `aspect-ratio: 288/456`, so left
213
+ * alone it takes the whole content column: 342 × 541 at 390, which ran the
214
+ * frame down into the verified tag (owner-reported in the 320 sweep,
215
+ * 2026-09-28, and the reason the stage read broken at every width, 2026-09-30).
216
+ *
217
+ * So the MODULE caps it, exactly as the frames do. `min()` keeps the cap from
218
+ * turning into a floor: on a column narrower than the frame it gives ground
219
+ * instead of spilling, which is what the 320 sections show. The aspect ratio
220
+ * stays Core's. DEVIATIONS #VS-18. */
221
+ .video-selfie-stage {
222
+ /* Figma geometry, not a token: `ID Capture Frame` is 286 wide on every
223
+ device section of this module. */
224
+ width: min(286px, 100%);
225
+ /* The viewfinder column stretches its children, so a child that names its
226
+ own width lands flush LEFT — which put the frame 16px off the phone's
227
+ centre line while the status card floating on it stayed centred. */
228
+ margin-inline: auto;
229
+ }
230
+
231
+ /* `IdCaptureLayout.css` gives every vertical stage inside a viewfinder a
232
+ * `flex-basis` of "the full column over the aspect", so the frame can give up
233
+ * height on a short screen and the aspect then narrows it back (Camila,
234
+ * 2026-09-28). That machinery is right and stays — it just has to start from
235
+ * THIS module's capped width, not the whole column: fed 342 it computed a
236
+ * 541-tall frame and then clamped the width back to 286, which is how the
237
+ * stage ended up 286 × 541 instead of 286 × 455 and grew into the verified
238
+ * tag. Same formula, capped input. */
239
+ .prizma-viewfinder__content
240
+ > .video-selfie-stage.prizma-id-capture--vertical:not(.prizma-id-capture--poa)
241
+ .prizma-id-capture__frame {
242
+ flex-basis: calc(min(286px, 100cqw - 2 * var(--prizma-spacing-24)) * 456 / 288);
243
+ }
244
+
158
245
  /* ── The captured document inside the vertical frame ─────────────────────────
159
246
  * Groups 3 and 5 draw the ID UPRIGHT in a portrait frame: the card is rotated a
160
- * quarter turn so its long edge runs down the screen, exactly as `3.2` and
161
- * `5.2` show it (the licence's text reads bottom-to-top).
247
+ * quarter turn so its long edge runs down the screen, exactly as `3.1`/`3.2`
248
+ * and `5.1`/`5.2` show it (the licence's text reads bottom-to-top).
162
249
  *
163
250
  * The sample scans are landscape (328×202). Dropped into a portrait frame with
164
251
  * Core's `object-fit: cover` they scaled 4.2× to fill the height and lost their
@@ -166,40 +253,41 @@
166
253
  * zoomed in to fit height, when it should be 90 degrees to fit the frame").
167
254
  * Rotating instead of cropping keeps the whole document legible.
168
255
  *
169
- * `width`/`height` are swapped BEFORE the rotation because a rotated element
170
- * still lays out on its pre-transform box: the image is sized to the frame's
171
- * height × width, then turned, so it lands exactly frame-sized. No literal —
172
- * both are percentages of whatever box Core gives it. DEVIATIONS #VS-6.
173
- */
174
- /* The captured document is landscape (a card scan); the VERTICAL frame is
175
- * portrait. `object-fit: cover` cropped its sides away and blew it up 4.2× —
176
- * the band of static the owner reported. Figma turns the card a quarter turn
177
- * instead, so the whole document stays legible with its text running up the
178
- * side, exactly as `3.2` / `5.2` draw it.
179
- *
180
- * The image is sized to the frame's height × width BEFORE the turn, because a
256
+ * The image is sized to the frame's HEIGHT × WIDTH before the turn, because a
181
257
  * rotated element still lays out on its pre-transform box — so after the 90°
182
258
  * it lands exactly frame-sized. Both values are percentages of whatever box
183
- * Core gives it, so nothing is hardcoded. DEVIATIONS #VS-6. */
259
+ * Core gives it, so nothing is hardcoded.
260
+ *
261
+ * ⚠ The first pass centred that oversized box with `display: grid;
262
+ * place-items: center`, which does NOT centre it: a grid item WIDER than its
263
+ * area is clamped to the start edge (the browsers' overflow-safety rule). The
264
+ * card therefore laid out at the frame's top-left, and turning it around ITS
265
+ * centre threw it 75px right and 75px up — hanging off two sides of the frame
266
+ * and across the camera. That is the "ID shown in a very weird way" report
267
+ * (owner, 2026-09-30). Absolute centring has no such clamp, so the turn
268
+ * happens around the frame's real middle. DEVIATIONS #VS-6. */
184
269
  .prizma-id-capture--vertical .prizma-id-capture__still {
185
270
  /* A size container, so the image can be measured against the frame's OWN
186
271
  box — `cqh`/`cqw` below are the only way to swap width and height without
187
272
  writing either number down. The frame has definite dimensions, which is
188
273
  what `container-type: size` requires. */
189
274
  container-type: size;
190
- display: grid;
191
- place-items: center;
192
275
  overflow: hidden;
193
276
  }
194
277
 
195
278
  .prizma-id-capture--vertical .prizma-id-capture__still > img {
196
279
  /* Swapped on purpose: the image is laid out at the frame's HEIGHT × WIDTH,
197
- then turned a quarter, so it lands exactly frame-sized. */
280
+ then turned, so it lands exactly frame-sized. */
281
+ position: absolute;
282
+ inset: auto;
283
+ top: 50%;
284
+ left: 50%;
198
285
  width: 100cqh;
199
286
  height: 100cqw;
200
287
  max-width: none;
201
288
  object-fit: cover;
202
- transform: rotate(90deg);
289
+ /* Centre first, THEN turn — around the frame's own middle. */
290
+ transform: translate(-50%, -50%) rotate(90deg);
203
291
  transform-origin: center;
204
292
  }
205
293
 
@@ -222,26 +310,69 @@
222
310
  pointer-events: none;
223
311
  }
224
312
 
225
- .video-selfie-stage-status > * {
226
- padding: var(--prizma-spacing-16) var(--prizma-spacing-24);
313
+ /* The card itself: hugs its content, never wider than the document frame it
314
+ * floats on (Figma's widest, `6.1.3`'s three-line rejection, is 260 inside the
315
+ * 286 frame), and always dark — the camera stage never mode-flips. The mark
316
+ * stacks above the label because the content is Core's `PrizmaTitle`; the card
317
+ * only supplies the surface and the air around it. */
318
+ .video-selfie-stage-status__card {
319
+ box-sizing: border-box;
320
+ max-width: min(260px, 100%);
321
+ padding: var(--prizma-spacing-24);
227
322
  border-radius: var(--prizma-border-radius-medium);
228
- background: var(--prizma-surface-neutral-900-80-static);
323
+ background: var(--prizma-surface-neutral-900-static);
229
324
  }
230
325
 
231
- /* ── Tutorial step rows ──────────────────────────────────────────────────────
232
- * The <ul> Core's `PrizmaList` would normally supply. Composed here only
233
- * because the wrapper's icon variants cap at six rows and the tutorial draws
234
- * seven (#VS-8); the gap is the variant's OWN Spacing/12 — nothing new.
235
- */
236
- .video-selfie-tutorial-list {
326
+ /* Both pieces inside the card sit ON that dark surface, so their ink is the
327
+ * camera HUD's own Static white rather than the theme's primary — the same
328
+ * re-seat `SelfieCapture.css` does for its overlay status band. */
329
+ .video-selfie-stage-status__card .prizma-title__heading,
330
+ .video-selfie-stage-status__card .prizma-loading-spinner__title {
331
+ color: var(--prizma-text-body-0-static);
332
+ }
333
+
334
+ /* ── The coaching bubble above the document frame ────────────────────────────
335
+ * The five condition frames put `Capture feedback` at the TOP of the content,
336
+ * over the camera and above the frame. The band it sits in is already the
337
+ * flexible one; this only pins it to the band's top edge and lifts it over
338
+ * the stage's scrim (the trap `ViewfinderScreen.css` documents on `__intro`:
339
+ * the scrim is an absolutely positioned sibling with an escaping box-shadow,
340
+ * so unlifted content renders under an 80% veil). The bubble's own width,
341
+ * radius, ink and illustration are Core's. DEVIATIONS #VS-28. */
342
+ .video-selfie-feedback {
343
+ position: relative;
344
+ z-index: 1;
345
+ width: 100%;
346
+ /* Top of the band, which is directly under the stepper — where the five
347
+ condition frames seat it (bubble top y≈88 of 844). */
348
+ margin-block: 0 auto;
349
+ }
350
+
351
+ /* ── The head-turn mark and its instruction ──────────────────────────────────
352
+ * The `🙂‍↔️ Turn Head` band puts a 48 circled arrow between the ring and the
353
+ * line that says which way to turn (`element container` 342×48, the glyph
354
+ * centred, then 16 to the text). The gap is Core's own Spacing/16; the copy is
355
+ * the foundation's `.prizma-viewfinder__title`, unchanged. Lifted over the
356
+ * stage's scrim for the reason `ViewfinderScreen.css` documents on `__intro`.
357
+ * DEVIATIONS #VS-20. */
358
+ .video-selfie-turn {
359
+ position: relative;
360
+ z-index: 1;
237
361
  display: flex;
238
362
  flex-direction: column;
239
- align-items: flex-start;
363
+ align-items: center;
364
+ gap: var(--prizma-spacing-16);
240
365
  width: 100%;
241
- margin: 0;
242
- padding: 0;
243
- list-style: none;
244
- gap: var(--prizma-spacing-12);
366
+ }
367
+
368
+ /* The mark is ONE bank glyph at 2xl — a filled disc with the arrow knocked
369
+ * out — so this only inks it. The first pass wrapped a 24 bare arrow in a
370
+ * hand-made disc and read as a thin chevron on the camera. */
371
+ .video-selfie-turn__mark {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+ color: var(--prizma-icon-neutral-50-static);
245
376
  }
246
377
 
247
378
  /* Step rows — the <ol> `PrizmaList` would supply, composed here only because
@@ -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,42 @@ 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
100
- 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}
115
- {...shell}
116
- />
129
+ <PrizmaScreenShell ref={ref} dark className={className} {...shell}>
130
+ <div className="prizma-viewfinder__surface">{camera}</div>
131
+ <div className="video-selfie-stepper">
132
+ <PrizmaStepper
133
+ steps={7}
134
+ currentState={STEP}
135
+ documentType={documentType}
136
+ completedValueText={strings.stepperCompletedLabel}
137
+ />
138
+ </div>
139
+ <div
140
+ className={[
141
+ 'prizma-viewfinder__content',
142
+ overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
143
+ ]
144
+ .filter(Boolean)
145
+ .join(' ')}
146
+ >
147
+ <div className="video-selfie-band video-selfie-band--ring">
148
+ <PrizmaSelfieCapture
149
+ mode="overlay"
150
+ scrim
151
+ state={CAPTURE_STATE[state]}
152
+ feedbackText={feedbackText}
153
+ statusLabels={{
154
+ uploading: slice.processingLabel,
155
+ success: slice.successLabel,
156
+ error: slice.failedLabel,
157
+ }}
158
+ />
159
+ </div>
160
+ </div>
161
+ </PrizmaScreenShell>
117
162
  );
118
163
  },
119
164
  );
@@ -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
  />
@@ -214,6 +222,9 @@ export const VideoSelfieSpeakScreen = forwardRef<
214
222
  className={[
215
223
  'video-selfie-sheet-panel',
216
224
  isConfirm ? 'video-selfie-sheet-panel--centered' : '',
225
+ stage === 'processing' || stage === 'success'
226
+ ? 'video-selfie-sheet-panel--status'
227
+ : '',
217
228
  ]
218
229
  .filter(Boolean)
219
230
  .join(' ')}