@incodetech/prizma-core-screens 0.2.4 → 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
@@ -28,6 +28,14 @@
28
28
  width: 100%;
29
29
  }
30
30
 
31
+ /* With the opt-in back chevron the nav sits on the content column's 24px
32
+ * gutter, so the chevron lines up with the title and content below it. Core's
33
+ * nav already pads its row by Spacing/2, hence the subtraction. A logo-only nav
34
+ * is centred and needs no gutter, so nothing else moves. */
35
+ .prizma-screen-nav--back {
36
+ padding-inline: calc(var(--prizma-spacing-24) - var(--prizma-spacing-2));
37
+ }
38
+
31
39
  .prizma-screen-nav__custom-logo {
32
40
  display: flex;
33
41
  align-items: center;
@@ -1,4 +1,4 @@
1
- import React, { type ComponentPropsWithoutRef, type ReactNode } from 'react';
1
+ import React, { type ComponentPropsWithoutRef, type MouseEventHandler, type ReactNode } from 'react';
2
2
  import './ScreenNav.css';
3
3
  export interface PrizmaScreenNavProps extends ComponentPropsWithoutRef<'header'> {
4
4
  /** Custom brand logo (e.g. an <img> of the client logo). Rendered centered,
@@ -16,11 +16,24 @@ export interface PrizmaScreenNavProps extends ComponentPropsWithoutRef<'header'>
16
16
  * only the logo disappears, and the row leaves the a11y tree.
17
17
  */
18
18
  reserved?: boolean;
19
+ /**
20
+ * Opt-in back chevron at the nav's LEFT — Core's own
21
+ * (`PrizmaNavigation.showLeftIcon`, IconChevronLeft). Passing the label turns
22
+ * it on; it is the button's accessible name (i18n). Off by default, so every
23
+ * existing screen keeps the logo-only nav. For a screen the user OPENED from
24
+ * another one (Qualified Signature's document viewers, reached from "View"
25
+ * and from the application-form link) — a way back to where they came from,
26
+ * not a dismiss of the flow, which stays out (#PI-9). Owner call 2026-09-29.
27
+ */
28
+ backLabel?: string;
29
+ /** Click handler for the back chevron. */
30
+ onBack?: MouseEventHandler<HTMLButtonElement>;
19
31
  }
20
32
  /**
21
33
  * ScreenNav — the logo header of every SDK screen.
22
34
  * Sits 12px below the top of the shell (the host frame owns the OS status
23
- * bar), logo centered, no back/close buttons (GUIDELINES.md §3).
35
+ * bar), logo centered, no close button (GUIDELINES.md §3). A back chevron is
36
+ * opt-in through `backLabel`, for opened sub-screens only.
24
37
  */
25
38
  export declare const PrizmaScreenNav: React.ForwardRefExoticComponent<PrizmaScreenNavProps & React.RefAttributes<HTMLElement>>;
26
39
  export default PrizmaScreenNav;
package/dist/ScreenNav.js CHANGED
@@ -1,13 +1,20 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { forwardRef } from 'react';
3
+ import { forwardRef, } from 'react';
4
4
  import { PrizmaNavigation } from '@incodetech/prizma-core-web/navigation';
5
5
  import './ScreenNav.css';
6
6
  /**
7
7
  * ScreenNav — the logo header of every SDK screen.
8
8
  * Sits 12px below the top of the shell (the host frame owns the OS status
9
- * bar), logo centered, no back/close buttons (GUIDELINES.md §3).
9
+ * bar), logo centered, no close button (GUIDELINES.md §3). A back chevron is
10
+ * opt-in through `backLabel`, for opened sub-screens only.
10
11
  */
11
- export const PrizmaScreenNav = forwardRef(({ logo, navLabel, reserved = false, className, ...rest }, ref) => (_jsx("header", { ref: ref, "aria-hidden": reserved || undefined, className: ['prizma-screen-nav', className ?? ''].filter(Boolean).join(' '), ...rest, children: reserved ? null : logo ? (_jsx("span", { className: "prizma-screen-nav__custom-logo", children: logo })) : (_jsx(PrizmaNavigation, { showIncodeLogo: true, "aria-label": navLabel })) })));
12
+ export const PrizmaScreenNav = forwardRef(({ logo, navLabel, reserved = false, backLabel, onBack, className, ...rest }, ref) => (_jsx("header", { ref: ref, "aria-hidden": reserved || undefined, className: [
13
+ 'prizma-screen-nav',
14
+ backLabel && !reserved ? 'prizma-screen-nav--back' : '',
15
+ className ?? '',
16
+ ]
17
+ .filter(Boolean)
18
+ .join(' '), ...rest, children: reserved ? null : logo ? (_jsx("span", { className: "prizma-screen-nav__custom-logo", children: logo })) : (_jsx(PrizmaNavigation, { showIncodeLogo: true, "aria-label": navLabel, showLeftIcon: Boolean(backLabel), backLabel: backLabel, onLeftIconClick: onBack })) })));
12
19
  PrizmaScreenNav.displayName = 'PrizmaScreenNav';
13
20
  export default PrizmaScreenNav;
@@ -1,12 +1,13 @@
1
1
  import React, { type ReactNode } from 'react';
2
2
  import { type PrizmaScreenShellProps } from './ScreenShell';
3
3
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
4
- import type { VideoSelfieDocumentType, VideoSelfieIdErrorSeverity, VideoSelfieIdSide, VideoSelfieStrings } from './VideoSelfieStrings';
4
+ import type { VideoSelfieDocumentType, VideoSelfieIdCondition, VideoSelfieIdErrorReason, VideoSelfieIdErrorSeverity, VideoSelfieIdSide, VideoSelfieStrings } from './VideoSelfieStrings';
5
5
  import './ScreenLayout.css';
6
6
  import './ViewfinderScreen.css';
7
7
  import './VideoSelfieLayout.css';
8
- /** Which purple stage section to render (groups 3, 5 and 6). */
9
- export type VideoSelfieIdCaptureState = 'default' | 'shown' | 'processing' | 'allGood' | 'error';
8
+ /** Which purple stage section to render (groups 3, 5 and 6), plus the live
9
+ * coaching beat the full-flow frames draw between them. */
10
+ export type VideoSelfieIdCaptureState = 'default' | 'shown' | 'processing' | 'allGood' | 'error' | 'condition';
10
11
  export interface VideoSelfieIdCaptureScreenProps extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
11
12
  /** Module copy — this screen reads the `idCapture` slice (+ shared labels). */
12
13
  strings: VideoSelfieStrings;
@@ -21,6 +22,12 @@ export interface VideoSelfieIdCaptureScreenProps extends Omit<PrizmaScreenShellP
21
22
  /** Whether the rejection invites a retry (`6.1`/`6.3`) or is terminal
22
23
  * (`6.2`/`6.4`). Read only when `state="error"`. */
23
24
  errorSeverity?: VideoSelfieIdErrorSeverity;
25
+ /** WHY the read was rejected — the `.1`–`.4` suffix of the group 6 section.
26
+ * Read only when `state="error"`. Default `scan`. */
27
+ errorReason?: VideoSelfieIdErrorReason;
28
+ /** Which live condition to coach the user out of. Read only when
29
+ * `state="condition"`. Default `notAligned`. */
30
+ condition?: VideoSelfieIdCondition;
24
31
  /** Live camera feed (<video>) or sample still (<img>) behind the frame. */
25
32
  camera?: ReactNode;
26
33
  /** The captured document still, for the states Core draws one in. */
@@ -2,14 +2,46 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { PrizmaIdCapture, } from '@incodetech/prizma-core-web/id-capture';
5
+ import { PrizmaCaptureFeedback } from '@incodetech/prizma-core-web/capture-feedback';
5
6
  import { PrizmaLoadingSpinner } from '@incodetech/prizma-core-web/loading-spinner';
6
- import { PrizmaSnackbar } from '@incodetech/prizma-core-web/snackbar';
7
7
  import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
8
8
  import { PrizmaTag } from '@incodetech/prizma-core-web/tag';
9
+ import { PrizmaTitle } from '@incodetech/prizma-core-web/title';
9
10
  import { PrizmaScreenShell } from './ScreenShell';
10
11
  import './ScreenLayout.css';
11
12
  import './ViewfinderScreen.css';
12
13
  import './VideoSelfieLayout.css';
14
+ /**
15
+ * Condition → the Core treatment that already exists for it.
16
+ *
17
+ * Four are `PrizmaIdCapture`'s own error states. `blurry` is the plain error
18
+ * border: blur is a DETECTION result, not a framing mistake, and the owner's
19
+ * 2026-07-28 call is that those pair the plain border with the feedback
20
+ * bubble rather than inventing a semantic overlay (`IdCapture.tsx`, the note
21
+ * above `error`). Either way the message is the bubble's, never the frame's.
22
+ */
23
+ const CONDITION_STATE = {
24
+ wrongSide: 'error-wrong-side',
25
+ notAligned: 'error-not-aligned',
26
+ glare: 'error-glare',
27
+ tooDark: 'error-too-dark',
28
+ blurry: 'error',
29
+ };
30
+ /** Condition → the bubble's own variant. All six of Core's are used: the
31
+ * wrong-side bubble names the face the user SHOULD be showing, so it flips
32
+ * with the leg — `show-front` on the front, `show-back` on the back. */
33
+ const CONDITION_FEEDBACK = {
34
+ wrongSide: 'not-aligned', // unused — the wrong-side variant is side-keyed below
35
+ notAligned: 'not-aligned',
36
+ blurry: 'blurry',
37
+ glare: 'glare',
38
+ tooDark: 'too-dark',
39
+ };
40
+ /** The wrong-side bubble, per leg. */
41
+ const WRONG_SIDE_FEEDBACK = {
42
+ front: 'show-front',
43
+ back: 'show-back',
44
+ };
13
45
  /** Purple stage state → Core `PrizmaIdCapture` state. The frame border, the
14
46
  * analyzing sweep, the scrim and the encrypted row are ALL Core's. */
15
47
  const CAPTURE_STATE = {
@@ -18,6 +50,8 @@ const CAPTURE_STATE = {
18
50
  processing: 'analyzing',
19
51
  allGood: 'success',
20
52
  error: 'error',
53
+ // Replaced per-condition below — the map needs an entry for every key.
54
+ condition: 'error',
21
55
  };
22
56
  /** Which beat of the seven-step journey each side occupies — front is step 2
23
57
  * (the microphone grant is step 1), back is step 3. */
@@ -64,23 +98,20 @@ const STEP = { front: 2, back: 3 };
64
98
  * dropped from `VideoSelfieStrings` with it — no other screen in the module
65
99
  * reads one.
66
100
  */
67
- export const VideoSelfieIdCaptureScreen = forwardRef(({ strings, side = 'front', documentType = 'id', state = 'default', errorSeverity = 'tryAgain', camera, still, screenId = `video-selfie-id-capture-${side}-${state}`, overrides, onAction, className, ...shell }, ref) => {
68
- const errorLabel = strings.idCapture.errors[errorSeverity];
101
+ export const VideoSelfieIdCaptureScreen = forwardRef(({ strings, side = 'front', documentType = 'id', state = 'default', errorSeverity = 'tryAgain', errorReason = 'scan', condition = 'notAligned', camera, still, screenId = `video-selfie-id-capture-${side}-${state}`, overrides, onAction, className, ...shell }, ref) => {
102
+ const errorLabel = strings.idCapture.errors[errorReason][errorSeverity];
103
+ /* Only the wrong-side bubble is keyed by side — it is the one that names
104
+ the face to show. The other four describe the shot. */
105
+ const conditionCopy = condition === 'wrongSide'
106
+ ? strings.idCapture.conditions.wrongSide[side]
107
+ : strings.idCapture.conditions[condition];
108
+ const conditionFeedback = condition === 'wrongSide' ? WRONG_SIDE_FEEDBACK[side] : CONDITION_FEEDBACK[condition];
69
109
  return (_jsxs(PrizmaScreenShell, { ref: ref, dark: true, className: className, ...shell, children: [_jsx("div", { className: "prizma-viewfinder__surface", children: camera }), _jsx("div", { className: "video-selfie-stepper", children: _jsx(PrizmaStepper, { steps: 7, currentState: STEP[side], documentType: documentType, completedValueText: strings.stepperCompletedLabel }) }), _jsxs("div", { className: [
70
110
  'prizma-viewfinder__content',
71
111
  overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
72
112
  ]
73
113
  .filter(Boolean)
74
- .join(' '), children: [_jsx("div", { className: "video-selfie-band" }), _jsx(PrizmaIdCapture, { type: documentType, state: CAPTURE_STATE[state], isVertical: true, mode: "overlay", scrim: true, showEncryptedLabel: false, still: still }), state === 'processing' ? (_jsx("div", { className: "video-selfie-stage-status", children: _jsx(PrizmaLoadingSpinner, { showTitle: true, titleText: strings.idCapture.processingLabel, loadingLabel: strings.idCapture.processingLabel }) })) : null, state === 'allGood' || state === 'error' ? (_jsx("div", { className: "video-selfie-stage-status", children: state === 'allGood' ? (_jsx(PrizmaSnackbar, { state: "success", role: "status", children: strings.idCapture.successLabel })) : (
75
- /* NOT `isMultiline`: that modifier switches the row to
76
- `align-items: flex-start` so an icon anchors to the top of
77
- WRAPPED text. The rejection copy renders on ONE line, so it
78
- only pushed the icon 6.9px below the text centre
79
- (owner-reported 2026-08-19: "the text is not even aligned to
80
- the icon"). CORE REQUEST: have the Snackbar follow actual
81
- wrapping rather than a prop the caller has to predict.
82
- DEVIATIONS #VS-9. */
83
- _jsx(PrizmaSnackbar, { state: "error", role: "alert", children: errorLabel })) })) : null] }), _jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: strings.verifiedLabel }) })] }));
114
+ .join(' '), children: [_jsx("div", { className: "video-selfie-band", children: state === 'condition' ? (_jsx("div", { className: "video-selfie-feedback", children: _jsx(PrizmaCaptureFeedback, { feedback: conditionFeedback, title: conditionCopy.title, subtitle: conditionCopy.subtitle }) })) : null }), _jsx(PrizmaIdCapture, { className: "video-selfie-stage", type: documentType, state: state === 'condition' ? CONDITION_STATE[condition] : CAPTURE_STATE[state], isVertical: true, mode: "overlay", scrim: true, showEncryptedLabel: false, still: still }), _jsx("div", { className: "video-selfie-band" }), state === 'processing' ? (_jsx("div", { className: "video-selfie-stage-status", children: _jsx("div", { className: "video-selfie-stage-status__card", children: _jsx(PrizmaLoadingSpinner, { showTitle: true, titleText: strings.idCapture.processingLabel, loadingLabel: strings.idCapture.processingLabel }) }) })) : null, state === 'allGood' || state === 'error' ? (_jsx("div", { className: "video-selfie-stage-status", children: _jsx("div", { className: "video-selfie-stage-status__card", role: state === 'error' ? 'alert' : 'status', children: _jsx(PrizmaTitle, { title: state === 'allGood' ? strings.idCapture.successLabel : errorLabel, icon: { type: 'status', status: state === 'allGood' ? 'success' : 'error' } }) }) })) : null] }), _jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: strings.verifiedLabel }) })] }));
84
115
  });
85
116
  VideoSelfieIdCaptureScreen.displayName = 'VideoSelfieIdCaptureScreen';
86
117
  export default VideoSelfieIdCaptureScreen;
@@ -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,52 +1,69 @@
1
1
  import React, { type ReactNode } from 'react';
2
- import { type PrizmaCaptureScreenProps } from './CaptureScreen';
2
+ import { type PrizmaScreenShellProps } from './ScreenShell';
3
3
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
4
- import type { VideoSelfieFeedbackKind, VideoSelfieStrings } from './VideoSelfieStrings';
4
+ import type { VideoSelfieDocumentType, VideoSelfieFeedbackKind, VideoSelfieStrings } from './VideoSelfieStrings';
5
5
  import './ScreenLayout.css';
6
+ import './ViewfinderScreen.css';
7
+ import './VideoSelfieLayout.css';
6
8
  /** Which purple liveness section to render (group 8). */
7
9
  export type VideoSelfieSelfieState = 'start' | 'processing' | 'success' | 'failed';
8
- export interface VideoSelfieSelfieCaptureScreenProps extends Omit<PrizmaCaptureScreenProps, 'state' | 'feedbackText' | 'statusLabels' | 'verifiedLabel' | 'navLabel'> {
10
+ export interface VideoSelfieSelfieCaptureScreenProps extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
9
11
  /** Module copy — this screen reads the `selfieCapture` slice. */
10
12
  strings: VideoSelfieStrings;
11
13
  /** Which purple section of group 8 to render. Default `start`. */
12
14
  state?: VideoSelfieSelfieState;
13
15
  /**
14
- * Which of group 9's ELEVEN live-feedback lines to show under the ring.
15
- * Only meaningful while the ring is live (`state="start"`): the sections of
16
+ * Which of group 9's live-feedback lines to show under the ring. Only
17
+ * meaningful while the ring is live (`state="start"`): the sections of
16
18
  * group 9 are that same frame with a different instruction. Omit for `8.0`,
17
19
  * which shows the plain "Look at your front camera" line.
18
20
  */
19
21
  feedback?: VideoSelfieFeedbackKind;
20
- /** Live camera feed (<video>) or sample still (<img>). */
22
+ /** Which document the journey collects — the Stepper's own `documentType`. */
23
+ documentType?: VideoSelfieDocumentType;
24
+ /** Live camera feed (<video>) or sample still (<img>), full-bleed behind the
25
+ * ring — NOT inside it. The overlay recipe makes the ring a window. */
21
26
  camera?: ReactNode;
22
27
  screenId?: string;
23
28
  overrides?: PrizmaScreenOverrides;
24
29
  onAction?: PrizmaScreenActionHandler;
25
30
  }
26
31
  /**
27
- * Video Selfie — the liveness ring (Archetype B, GUIDELINES.md §6-B).
28
- * ONE screen for FIFTEEN purple sections: `selfie capture 8.0–8.3` and every
29
- * `selfie capture feedback 9.1–9.11` message. They are `state` × `feedbackText`
30
- * variants of Core's `PrizmaSelfieCapture`, which owns the ring, the
31
- * silhouette, the ring offset (40), the instruction gap (32), the instruction
32
- * typography and every verdict treatment. None of it is re-implemented (§7),
33
- * and this file declares no layout at all.
32
+ * Video Selfie — the liveness ring (Archetype D, GUIDELINES.md §6-D; always
33
+ * dark). ONE screen for every purple section of groups 8 and 9: `selfie
34
+ * capture 8.0–8.3` and every `selfie capture feedback 9.x` message. They are
35
+ * `state` × `feedbackText` variants of Core's `PrizmaSelfieCapture`, which is
36
+ * exactly how the component is axed.
34
37
  *
35
- * **Figma vs Core — Core wins, reported.** These sections draw the camera
36
- * FULL-BLEED behind a scrimmed ring, on a black surface. Core's
37
- * `PrizmaSelfieCapture` puts the feed inside the ring's circular window on a
38
- * light surface, and pins light while the feed is open on purpose — "the
39
- * capture screen stays WHITE in both modes, light is needed for the shot"
40
- * (owner decision 2026-07-14, DEVIATIONS #F-27). Reshaping it here would mean
41
- * re-implementing a Core primitive with a second full-bleed recipe, so the
42
- * screen ships Core's treatment and the divergence is logged, exactly as the
43
- * Face Capture pilot settled it.
38
+ * **Rebuilt 2026-09-30 to follow the frames.** The first pass shipped Core's
39
+ * INLINE recipe — the feed inside a circular window on a WHITE surface, the
40
+ * verdicts dissolving the ring into a centred mark — and logged the gap. On
41
+ * the module page that read as a different product from the rest of the flow:
42
+ * a small ring on white between two full-bleed dark camera screens, with the
43
+ * ring vanishing on the very beats Figma keeps it (owner, 2026-09-30: "they
44
+ * are looking very broken… a broken experience"). Rather than hand-roll the
45
+ * full-bleed composition here, Core's `PrizmaSelfieCapture` grew the
46
+ * `mode="overlay"` + `scrim` axis `PrizmaIdCapture` already carries — it is
47
+ * the Figma library's own `Capture / Selfie overlay` component — and this
48
+ * screen composes it. Nothing about the ring, the scrim, the glow sweeps, the
49
+ * verdict marks or the instruction typography is re-implemented (§7).
50
+ * DEVIATIONS #VS-22.
44
51
  *
45
- * Figma also draws the journey stepper at the top of all fifteen sections.
46
- * Archetype B has no slot for it and this module does not hand-roll one —
47
- * reported. Nothing collapses without it: the four states and the eleven
48
- * messages already render distinctly through Core's own ring treatments and
49
- * instruction copy.
52
+ * The composition, measured off `8.0` (390×844):
53
+ *
54
+ * - the camera runs FULL-BLEED on `.prizma-viewfinder__surface` (`selfie bg
55
+ * image`, 390×844 — it passes under the browser chrome too);
56
+ * - the journey stepper sits in the band the logo nav normally occupies
57
+ * (`top content` y=56, the Stepper 342 wide at x=24), the module-wide
58
+ * treatment `VideoSelfieLayout.css` owns;
59
+ * - the ring is 335 centred in the remaining air (Figma 27,188 → 362,523
60
+ * against a 708-high content area), with the instruction stacked under it.
61
+ * Core's own Spacing/32 gap replaces Figma's 25.5 (the token wins, §4).
62
+ *
63
+ * NO verified tag: `8.x` and `9.x` draw nothing between the instruction and
64
+ * the bottom of the screen (GUIDELINES §1 — follow the module's section, not
65
+ * the default). The document stage keeps its tag because its own sections
66
+ * draw one (#VS-3).
50
67
  */
51
68
  export declare const VideoSelfieSelfieCaptureScreen: React.ForwardRefExoticComponent<VideoSelfieSelfieCaptureScreenProps & React.RefAttributes<HTMLDivElement>>;
52
69
  export default VideoSelfieSelfieCaptureScreen;