@incodetech/prizma-core-screens 0.2.7 → 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 (168) 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/DemoScreensLayout.css +16 -0
  8. package/dist/DemoScreensRedirectScreen.d.ts +6 -1
  9. package/dist/DemoScreensRedirectScreen.js +3 -2
  10. package/dist/DemoScreensStrings.d.ts +5 -0
  11. package/dist/DigitalIdUploadErrorScreen.d.ts +31 -0
  12. package/dist/DigitalIdUploadErrorScreen.js +26 -0
  13. package/dist/DigitalIdUploadFileTooLargeScreen.d.ts +27 -0
  14. package/dist/DigitalIdUploadFileTooLargeScreen.js +21 -0
  15. package/dist/DigitalIdUploadReviewScreen.d.ts +57 -0
  16. package/dist/DigitalIdUploadReviewScreen.js +44 -0
  17. package/dist/DigitalIdUploadStrings.d.ts +64 -0
  18. package/dist/DigitalIdUploadStrings.js +19 -0
  19. package/dist/DigitalIdUploadTutorialScreen.d.ts +28 -0
  20. package/dist/DigitalIdUploadTutorialScreen.js +23 -0
  21. package/dist/DocumentCaptureIllustration.d.ts +1 -1
  22. package/dist/EkybCountryScreen.d.ts +6 -6
  23. package/dist/EkybCountryScreen.js +20 -7
  24. package/dist/EkybStrings.d.ts +6 -1
  25. package/dist/ErrorScreensScreen.d.ts +5 -2
  26. package/dist/ErrorScreensScreen.js +15 -4
  27. package/dist/ErrorScreensStrings.d.ts +17 -2
  28. package/dist/ErrorScreensStrings.js +18 -10
  29. package/dist/FaceCaptureCapturingScreen.d.ts +63 -0
  30. package/dist/FaceCaptureCapturingScreen.js +62 -2
  31. package/dist/FaceCaptureLayout.css +69 -4
  32. package/dist/FaceCaptureStrings.d.ts +23 -0
  33. package/dist/FormScreen.css +14 -0
  34. package/dist/FormScreen.d.ts +28 -0
  35. package/dist/FormScreen.js +3 -2
  36. package/dist/IdCaptureLayout.css +165 -2
  37. package/dist/IdCaptureStageScreen.d.ts +47 -0
  38. package/dist/IdCaptureStageScreen.js +39 -3
  39. package/dist/IdCaptureStrings.d.ts +19 -0
  40. package/dist/ListScreen.d.ts +19 -0
  41. package/dist/ListScreen.js +7 -2
  42. package/dist/LoaderScreen.d.ts +7 -5
  43. package/dist/LoaderScreen.js +7 -5
  44. package/dist/LoadersStrings.d.ts +18 -5
  45. package/dist/OcrReviewErrorScreen.d.ts +30 -0
  46. package/dist/OcrReviewErrorScreen.js +24 -0
  47. package/dist/OcrReviewLayout.css +24 -0
  48. package/dist/OcrReviewLoadingScreen.d.ts +25 -0
  49. package/dist/OcrReviewLoadingScreen.js +19 -0
  50. package/dist/OcrReviewScreen.d.ts +76 -0
  51. package/dist/OcrReviewScreen.js +65 -0
  52. package/dist/OcrReviewStrings.d.ts +47 -0
  53. package/dist/OcrReviewStrings.js +17 -0
  54. package/dist/OcrReviewSuccessScreen.d.ts +30 -0
  55. package/dist/OcrReviewSuccessScreen.js +24 -0
  56. package/dist/PermissionsAllowScreen.d.ts +8 -11
  57. package/dist/PermissionsAllowScreen.js +8 -11
  58. package/dist/PermissionsAskScreen.d.ts +41 -0
  59. package/dist/PermissionsAskScreen.js +43 -0
  60. package/dist/PermissionsBrowserStepsScreen.d.ts +9 -2
  61. package/dist/PermissionsBrowserStepsScreen.js +12 -3
  62. package/dist/PermissionsLayout.css +47 -0
  63. package/dist/PermissionsStrings.d.ts +20 -9
  64. package/dist/PermissionsStrings.js +10 -4
  65. package/dist/PhoneInputOtpScreen.d.ts +8 -0
  66. package/dist/PhoneInputOtpScreen.js +26 -19
  67. package/dist/PhoneInputStrings.d.ts +11 -3
  68. package/dist/PhoneInputVerifyScreen.js +21 -5
  69. package/dist/QualifiedSignatureDocumentScreen.d.ts +8 -5
  70. package/dist/QualifiedSignatureDocumentScreen.js +9 -6
  71. package/dist/QualifiedSignatureResultScreen.d.ts +16 -13
  72. package/dist/QualifiedSignatureResultScreen.js +17 -18
  73. package/dist/QualifiedSignatureStrings.d.ts +11 -4
  74. package/dist/ResultScreen.d.ts +8 -0
  75. package/dist/ResultScreen.js +14 -3
  76. package/dist/ReusabilityCredentialSelectionScreen.d.ts +95 -0
  77. package/dist/ReusabilityCredentialSelectionScreen.js +94 -0
  78. package/dist/ReusabilityLayout.css +241 -0
  79. package/dist/ReusabilityPrivacyPolicyScreen.d.ts +55 -0
  80. package/dist/ReusabilityPrivacyPolicyScreen.js +44 -0
  81. package/dist/ReusabilityStrings.d.ts +85 -0
  82. package/dist/ReusabilityStrings.js +37 -0
  83. package/dist/ReviewScreen.d.ts +38 -12
  84. package/dist/ReviewScreen.js +41 -8
  85. package/dist/ScreenLayout.css +382 -6
  86. package/dist/VideoSelfieErrorScreen.js +1 -1
  87. package/dist/VideoSelfieIdCaptureScreen.js +1 -1
  88. package/dist/VideoSelfieLayout.css +304 -2
  89. package/dist/VideoSelfieSelfieCaptureScreen.js +1 -1
  90. package/dist/VideoSelfieSpeakScreen.js +1 -1
  91. package/dist/VideoSelfieStrings.d.ts +4 -1
  92. package/dist/VideoSelfieTurnHeadScreen.js +14 -11
  93. package/dist/VideoSelfieTutorialScreen.d.ts +9 -0
  94. package/dist/VideoSelfieTutorialScreen.js +10 -1
  95. package/dist/VideoSelfieWaveScreen.js +1 -1
  96. package/dist/ViewfinderScreen.css +36 -0
  97. package/dist/flows.d.ts +8 -0
  98. package/dist/flows.js +17 -0
  99. package/dist/index.d.ts +16 -0
  100. package/dist/index.js +12 -0
  101. package/dist/styles.css +3 -0
  102. package/package.json +68 -3
  103. package/samples/video-selfie-camera-wide.jpg +0 -0
  104. package/samples/video-selfie-room-wide.jpg +0 -0
  105. package/src/BiometricConsentScreen.tsx +192 -0
  106. package/src/BiometricConsentStrings.ts +119 -0
  107. package/src/CaptureScreen.tsx +17 -0
  108. package/src/DemoScreensLayout.css +16 -0
  109. package/src/DemoScreensRedirectScreen.tsx +14 -6
  110. package/src/DemoScreensStrings.ts +5 -0
  111. package/src/DigitalIdUploadErrorScreen.tsx +83 -0
  112. package/src/DigitalIdUploadFileTooLargeScreen.tsx +67 -0
  113. package/src/DigitalIdUploadReviewScreen.tsx +119 -0
  114. package/src/DigitalIdUploadStrings.ts +88 -0
  115. package/src/DigitalIdUploadTutorialScreen.tsx +59 -0
  116. package/src/DocumentCaptureIllustration.tsx +1 -1
  117. package/src/EkybCountryScreen.tsx +23 -6
  118. package/src/EkybStrings.ts +6 -1
  119. package/src/ErrorScreensScreen.tsx +15 -4
  120. package/src/ErrorScreensStrings.ts +37 -12
  121. package/src/FaceCaptureCapturingScreen.tsx +102 -13
  122. package/src/FaceCaptureLayout.css +69 -4
  123. package/src/FaceCaptureStrings.ts +24 -0
  124. package/src/FormScreen.css +14 -0
  125. package/src/FormScreen.tsx +34 -0
  126. package/src/IdCaptureLayout.css +165 -2
  127. package/src/IdCaptureStageScreen.tsx +75 -0
  128. package/src/IdCaptureStrings.ts +19 -0
  129. package/src/ListScreen.tsx +48 -9
  130. package/src/LoaderScreen.tsx +7 -5
  131. package/src/LoadersStrings.ts +18 -5
  132. package/src/OcrReviewErrorScreen.tsx +53 -0
  133. package/src/OcrReviewLayout.css +24 -0
  134. package/src/OcrReviewLoadingScreen.tsx +48 -0
  135. package/src/OcrReviewScreen.tsx +190 -0
  136. package/src/OcrReviewStrings.ts +69 -0
  137. package/src/OcrReviewSuccessScreen.tsx +52 -0
  138. package/src/PermissionsAllowScreen.tsx +9 -10
  139. package/src/PermissionsAskScreen.tsx +105 -0
  140. package/src/PermissionsBrowserStepsScreen.tsx +11 -3
  141. package/src/PermissionsLayout.css +47 -0
  142. package/src/PermissionsStrings.ts +42 -12
  143. package/src/PhoneInputOtpScreen.tsx +23 -3
  144. package/src/PhoneInputStrings.ts +11 -3
  145. package/src/PhoneInputVerifyScreen.tsx +24 -4
  146. package/src/QualifiedSignatureDocumentScreen.tsx +9 -7
  147. package/src/QualifiedSignatureResultScreen.tsx +28 -26
  148. package/src/QualifiedSignatureStrings.ts +11 -4
  149. package/src/ResultScreen.tsx +33 -2
  150. package/src/ReusabilityCredentialSelectionScreen.tsx +246 -0
  151. package/src/ReusabilityLayout.css +241 -0
  152. package/src/ReusabilityPrivacyPolicyScreen.tsx +109 -0
  153. package/src/ReusabilityStrings.ts +127 -0
  154. package/src/ReviewScreen.tsx +96 -27
  155. package/src/ScreenLayout.css +382 -6
  156. package/src/VideoSelfieErrorScreen.tsx +1 -1
  157. package/src/VideoSelfieIdCaptureScreen.tsx +9 -1
  158. package/src/VideoSelfieLayout.css +304 -2
  159. package/src/VideoSelfieSelfieCaptureScreen.tsx +9 -1
  160. package/src/VideoSelfieSpeakScreen.tsx +9 -1
  161. package/src/VideoSelfieStrings.ts +4 -2
  162. package/src/VideoSelfieTurnHeadScreen.tsx +25 -14
  163. package/src/VideoSelfieTutorialScreen.tsx +11 -0
  164. package/src/VideoSelfieWaveScreen.tsx +9 -1
  165. package/src/ViewfinderScreen.css +36 -0
  166. package/src/flows.ts +25 -0
  167. package/src/index.ts +74 -3
  168. package/src/styles.css +3 -0
@@ -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
+ }
@@ -23,6 +23,29 @@ export interface FaceCaptureStrings {
23
23
  /** faceCapture.capturing.* — the H2 instruction under the ring, per state.
24
24
  * `initiating` has no instruction in the design; omit it. */
25
25
  capturing: Partial<Record<FaceCaptureCapturingState, string>>;
26
+ /**
27
+ * faceCapture.capturing.extendTime — the accessibility offer that rides any
28
+ * capturing state 20 s before auto capture falls back to manual.
29
+ *
30
+ * It exists for WCAG 2.2.1 Timing Adjustable, satisfied through the *Extend*
31
+ * clause: warn before the limit expires, give the user at least 20 s to
32
+ * extend with a simple action, and let them extend at least ten times. The
33
+ * 20 s is that threshold, not a chosen duration — which is why `description`
34
+ * is a whole string and not a number the screen formats: ten locales write
35
+ * that sentence ten different ways, and the count has to live inside it.
36
+ *
37
+ * Shown only when accessibility mode is ON for the flow (a per-flow
38
+ * dashboard setting). With it off, nothing about today's capture changes.
39
+ * Optional for that reason: a host that never turns it on never writes it.
40
+ */
41
+ extendTime?: {
42
+ /** Bold first line — "Need more time?" */
43
+ title: string;
44
+ /** "Manual capture starts in 20 s" — carries the count, see above. */
45
+ description: string;
46
+ /** The action that keeps the user on auto — "Extend auto-capture". */
47
+ linkLabel: string;
48
+ };
26
49
  /** faceCapture.manualCapture.default */
27
50
  manualCapture: {
28
51
  /** "Align your face within the silhouette and press the button to capture" */
@@ -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:
@@ -50,6 +50,13 @@ export interface PrizmaFormScreenProps extends Omit<PrizmaScreenShellProps, 'dar
50
50
  * body text.
51
51
  */
52
52
  linkLabel?: string;
53
+ /**
54
+ * Renders `linkLabel` in Core's disabled treatment, for a link that exists
55
+ * but cannot be pressed yet — Phone Input's "Resend code in 30s" while the
56
+ * resend timer runs (Figma `phoneNumber.OTP.typing`, Tertiary / Link ·
57
+ * Disabled).
58
+ */
59
+ linkDisabled?: boolean;
53
60
  /**
54
61
  * A composed line below the primary: body copy with inline links, e.g. Phone
55
62
  * Input's "Didn't receive the code? **Resend code** or **Change phone
@@ -86,6 +93,27 @@ export interface PrizmaFormScreenProps extends Omit<PrizmaScreenShellProps, 'dar
86
93
  * eKYB, Data Sharing Consent); it is the archetype's now. DEVIATIONS #DSC-2.
87
94
  */
88
95
  scrollFields?: boolean;
96
+ /**
97
+ * Where the field stack sits in the column when it is SHORTER than the
98
+ * space it has. `start` (the default) hangs it off the title block, which is
99
+ * every form so far. `end` pins it to the CTA band instead, leaving the space
100
+ * between the title and the stack empty — Biometric Consent's section draws
101
+ * exactly that: a `justify-between` middle with the Title at the top and the
102
+ * one consent card flush against Continue, Spacing/16 above it
103
+ * (`biometricConsent.empty`, 2001:16361). Selected here so the module
104
+ * declares no layout of its own; the rule lives once in `FormScreen.css`.
105
+ * DEVIATIONS #BC-1.
106
+ */
107
+ fieldsAlign?: 'start' | 'end';
108
+ /**
109
+ * A shell-level layer painted OVER the whole screen — the same slot archetype
110
+ * A carries for Document Capture's method sheet (#F-32, #DC-30): a scrim plus
111
+ * Core's `PrizmaBottomSheetContent` or `PrizmaModalContent`, presented on top
112
+ * of the dimmed form. It cannot ride in the content column: it is absolutely
113
+ * positioned against the shell. Biometric Consent's quit confirmation is the
114
+ * first Form consumer (DEVIATIONS #BC-8).
115
+ */
116
+ overlay?: ReactNode;
89
117
  /**
90
118
  * Let the field stack change SHAPE on a wide screen: `PrizmaFormRow` pairs
91
119
  * become two lanes and Continue + Back share a row, once the shell's own
@@ -23,11 +23,12 @@ export function PrizmaFormRow({ children, className }) {
23
23
  * (typing / filled / loading) are separate screens sharing this identical
24
24
  * layout, changing only field content.
25
25
  */
26
- export const PrizmaFormScreen = forwardRef(({ progress, title, subtitle, ctaLabel, ctaDisabled, ctaLoading = false, ctaLoadingLabel, linkLabel, linkContent, verifiedLabel, children, fieldsOffset, fieldsGap, scrollFields = false, responsive = false, navLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, className: [shell.className, responsive ? 'prizma-form-screen--responsive' : null]
26
+ export const PrizmaFormScreen = forwardRef(({ progress, title, subtitle, ctaLabel, ctaDisabled, ctaLoading = false, ctaLoadingLabel, linkLabel, linkDisabled, linkContent, verifiedLabel, children, fieldsOffset, fieldsGap, scrollFields = false, fieldsAlign = 'start', overlay, responsive = false, navLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, className: [shell.className, responsive ? 'prizma-form-screen--responsive' : null]
27
27
  .filter(Boolean)
28
28
  .join(' ') || undefined, children: [_jsx(PrizmaScreenNav, { reserved: !showHeader, logo: overrides?.logo, navLabel: navLabel }), _jsxs("div", { className: "prizma-screen-layout__content", children: [progress ? (_jsx("div", { className: "prizma-screen-layout__progress", children: progress })) : null, _jsx(PrizmaTitle, { variant: overrides?.align === 'left' ? 'left' : 'centered', title: title, showSubtitle: Boolean(subtitle), subtitle: subtitle }), _jsx("div", { className: [
29
29
  'prizma-form-screen__fields',
30
30
  scrollFields ? 'prizma-form-screen__fields--scroll' : null,
31
+ fieldsAlign === 'end' ? 'prizma-form-screen__fields--end' : null,
31
32
  ]
32
33
  .filter(Boolean)
33
34
  .join(' '),
@@ -42,6 +43,6 @@ export const PrizmaFormScreen = forwardRef(({ progress, title, subtitle, ctaLabe
42
43
  ? { '--pfs-fields-gap': `var(--prizma-spacing-${fieldsGap})` }
43
44
  : null),
44
45
  }
45
- : undefined, children: children }), ctaLabel || linkLabel || linkContent ? (_jsxs("div", { className: "prizma-screen-layout__cta prizma-form-screen__cta", children: [ctaLabel ? (_jsx(PrizmaButton, { variant: "primary", size: "L", disabled: ctaDisabled, loading: ctaLoading, loadingLabel: ctaLoadingLabel, onClick: () => onAction?.({ type: 'cta', screenId }), children: ctaLabel })) : null, linkLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: linkLabel })) : null, linkContent] })) : null, showFooter ? (_jsx("div", { className: "prizma-screen-layout__footer prizma-screen-layout__footer--pinned", children: _jsx(PrizmaTag, { label: verifiedLabel }) })) : null] })] })));
46
+ : undefined, children: children }), ctaLabel || linkLabel || linkContent ? (_jsxs("div", { className: "prizma-screen-layout__cta prizma-form-screen__cta", children: [ctaLabel ? (_jsx(PrizmaButton, { variant: "primary", size: "L", disabled: ctaDisabled, loading: ctaLoading, loadingLabel: ctaLoadingLabel, onClick: () => onAction?.({ type: 'cta', screenId }), children: ctaLabel })) : null, linkLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", disabled: linkDisabled, onClick: () => onAction?.({ type: 'secondary', screenId }), children: linkLabel })) : null, linkContent] })) : null, showFooter ? (_jsx("div", { className: "prizma-screen-layout__footer prizma-screen-layout__footer--pinned", children: _jsx(PrizmaTag, { label: verifiedLabel }) })) : null] }), overlay] })));
46
47
  PrizmaFormScreen.displayName = 'PrizmaFormScreen';
47
48
  export default PrizmaFormScreen;
@@ -99,12 +99,89 @@
99
99
  flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 420 / 306);
100
100
  }
101
101
 
102
+ /* ── A notice on the stage: the horizontal frame may give up height too ──────
103
+ * The "Need more time?" offer (`.id-capture-stage__notice`, IDC-32) adds ~90px
104
+ * of content to the bottom band. On the 390 × 844 reference the band has the
105
+ * room and nothing moves. On a short panel it does not: measured 2026-10-15,
106
+ * the horizontal ID frame is aspect-locked and does not shrink, so the column
107
+ * overflowed and the notice ran 10px into the help strip at 320 × 693 and 36px
108
+ * at the closed iPhone Duo (466 × 678). The same recipe as the vertical rule
109
+ * above, applied to the horizontal frames ONLY while a notice is on the stage:
110
+ * the frame starts at its full-width height and gives up height when the
111
+ * column is short, the aspect ratio narrows it to match, the title band keeps
112
+ * its content height. Face Capture makes the identical trade at 320 (its
113
+ * selfie frame gives up Spacing/40 for the notice, ScreenLayout.css) and the
114
+ * owner's rule is explicit that the frame may get smaller (2026-08-18). A
115
+ * stage without a notice is untouched. */
116
+ .prizma-viewfinder__content:has(> .id-capture-band > .id-capture-stage__notice) {
117
+ min-height: 0;
118
+ }
119
+ .prizma-viewfinder__content:has(> .id-capture-band > .id-capture-stage__notice) > .id-capture-band--top {
120
+ flex-shrink: 0;
121
+ min-height: min-content;
122
+ }
123
+ .prizma-viewfinder__content:has(> .id-capture-band > .id-capture-stage__notice) > .prizma-id-capture--horizontal:not(.prizma-id-capture--poa) {
124
+ flex: 0 1 auto;
125
+ min-height: 0;
126
+ }
127
+ .prizma-viewfinder__content:has(> .id-capture-band > .id-capture-stage__notice) > .prizma-id-capture--horizontal:not(.prizma-id-capture--poa) .prizma-id-capture__frame {
128
+ flex: 0 1 auto;
129
+ min-height: 0;
130
+ width: auto;
131
+ max-width: 100%;
132
+ flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 220 / 350);
133
+ }
134
+ .prizma-viewfinder__content:has(> .id-capture-band > .id-capture-stage__notice) > .prizma-id-capture--passport.prizma-id-capture--horizontal .prizma-id-capture__frame {
135
+ flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 254 / 350);
136
+ }
137
+
138
+ /* ── Short panels: the tutorial art is sized by its halo, not its canvas ─────
139
+ * `PrizmaIllustrationIdTutorial` is a 342 × 392 canvas with the halo centred
140
+ * at r130 (y 66–326) and the hand-and-document artwork inside it (y 100–313;
141
+ * the passport's hand fades out above). The 66 above and below the halo are
142
+ * air at rest; the bottom band is where the hand travels in from during the
143
+ * loop. The selfie illustration is a 296 canvas with the same r130 halo and 18
144
+ * of air: on the 844 reference both halos are 260 and the two tutorials match.
145
+ *
146
+ * On a short panel the tutorial middle is height-limited (ScreenLayout.css,
147
+ * #F-35) and the two canvases scale by height, so the ID halo comes out 18 %
148
+ * smaller than the selfie's (170 vs 209 on the closed iPhone Duo, measured
149
+ * 2026-10-14; Camila: "ID tutorials have illustration a bit too small, no?
150
+ * Compared to the size on the selfie tutorial one"). Fixed by cropping the
151
+ * canvas to the halo band plus Spacing/16 on each side — y 50 to 342, 292
152
+ * units, the same 89 % the selfie's halo takes of its own box — so the halo
153
+ * fills the slot the way the selfie's does. The wrapper takes the slot and
154
+ * clips; the svg is scaled so 292 of its 392 units fill the wrapper and
155
+ * centres, so the crop is symmetric around the halo and the halo's centre
156
+ * does not move. During the loop the entering hand is clipped 50 units
157
+ * earlier than the canvas clips it, which is invisible against the fade.
158
+ * Outside the query nothing exists: the reference renders the full canvas.
159
+ * DEVIATIONS #IDC-31. */
160
+ @container pcs-screen (max-height: 720px) {
161
+ .prizma-screen-layout__flex > .prizma-illustration-tutorial {
162
+ flex: 1 1 0;
163
+ min-height: 0;
164
+ align-self: stretch;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ overflow: hidden;
169
+ }
170
+
171
+ .prizma-screen-layout__flex > .prizma-illustration-tutorial > svg {
172
+ flex: none;
173
+ height: calc(100% * 392 / 292);
174
+ width: auto;
175
+ max-width: none;
176
+ }
177
+ }
178
+
102
179
  /* ── Camera stage (dark) ───────────────────────────────────────────────────── */
103
180
 
104
181
  /* The stage's nav row carries only the close X (no logo — the camera owns the
105
182
  * screen). The foundation `.prizma-screen-nav` supplies the status-bar reserve,
106
183
  * the 44px flow slot and the un-fixing of Core's PrizmaNavigation; the ink is
107
- * the only thing the camera stage changes: Core's nav icon is the THEMED
184
+ * the only thing the camera stage changes: Core's nav icon is the themed
108
185
  * `Icon/Neutral/500`, which disappears on the always-dark camera surface, so it
109
186
  * takes the camera's own Static ink — the same token PrizmaCaptureManual uses
110
187
  * for the shutter and the "?" help control. DEVIATIONS.md #IDC-21. */
@@ -118,6 +195,44 @@
118
195
  width: 100%;
119
196
  }
120
197
 
198
+ /* ── The "Need more time?" notice ─────────────────────────────────────────────
199
+ * The Snackbar the stage raises 20 s before auto-capture falls back to the
200
+ * shutter (IdCaptureStageScreen `extendTime`, 2026-10-15). It takes the BOTTOM
201
+ * band — the air between the encrypted row and the strip's reserved space —
202
+ * because, unlike Face Capture, this stage's strip is never empty: the "?" help
203
+ * control rides it on every stage (IDC-29), so the shared rail the selfie notice
204
+ * uses (`.prizma-screen-layout__capture-notice`) would land on top of it.
205
+ *
206
+ * The band is `justify-content: flex-end`, so the notice sits just above the
207
+ * strip's Spacing/12 of breathing and the frame above does not move when the
208
+ * offer arrives. The band is also `flex: 1; min-height: 0`, which on a short
209
+ * panel would let it shrink to nothing and spill the notice upward over the
210
+ * encrypted row; with a notice in it the band keeps its content height and the
211
+ * shortfall goes where this stylesheet already sends it — the vertical frame
212
+ * (the short-panel rule above), which the owner has said may get smaller
213
+ * (2026-08-18). DEVIATIONS #IDC-32. */
214
+ .id-capture-band:has(> .id-capture-stage__notice) {
215
+ flex-shrink: 0;
216
+ min-height: min-content;
217
+ padding-top: var(--prizma-spacing-16);
218
+ }
219
+
220
+ .id-capture-stage__notice {
221
+ display: flex;
222
+ /* The HUD column centres its copy; the Snackbar's own text is left-set. */
223
+ text-align: left;
224
+ }
225
+
226
+ /* On a 320 phone the notice drops its status glyph — the same call Face
227
+ * Capture made (Camila, 2026-10-05, ScreenLayout.css): 44 of the 272px column
228
+ * on a decorative mark pushed the body to four lines. The icon is
229
+ * `aria-hidden`; the copy carries the meaning without it. */
230
+ @container pcs-screen (max-width: 340px) {
231
+ .id-capture-stage__notice .prizma-snackbar__icon {
232
+ display: none;
233
+ }
234
+ }
235
+
121
236
  /* The feedback pill replaces the title block on the error stages (Figma
122
237
  * `1-5-5 … 1-5-9-id-errors`): same band, same Spacing/40 above the frame. */
123
238
  .id-capture-stage__feedback {
@@ -125,6 +240,54 @@
125
240
  justify-content: center;
126
241
  }
127
242
 
243
+ /* ── Short or narrow panels: the live cue takes Template B ───────────────────
244
+ * `PrizmaCaptureFeedback` ships two templates on its `showAnimation` axis:
245
+ * A, the 120-ish pill with the illustration and a Headline/H3 title, and B, a
246
+ * 72 pill with no illustration and Headline/H5. The error stages draw A on the
247
+ * 390 × 844 reference. On a closed iPhone Duo (678) A still fits — measured
248
+ * 2026-10-14, the frame lands at 198–473 with the pill above it — but it is
249
+ * the tallest element on a stage that has just lost 166px, and on a 640 phone
250
+ * or a 320 column it does not fit at all. Video Selfie already made this call
251
+ * for its own cues (DEVIATIONS #VS-30, Camila 2026-10-07): under 340 wide or
252
+ * 720 tall the pill is B. The same line here, so the two camera modules agree
253
+ * on when a cue goes compact. Nothing is re-skinned: fill, radius, stroke and
254
+ * shadow are Core's and identical between A and B (capture-feedback.md §4.4);
255
+ * what changes is exactly Template B's four switches, taken from
256
+ * `CaptureFeedback.css`'s own `--compact` modifier.
257
+ *
258
+ * A container query, not the prop: the screen cannot know the panel's height,
259
+ * the shell's `pcs-screen` container can. ONE `or` condition, not two
260
+ * comma-separated queries (lightningcss rejects the latter; see
261
+ * VideoSelfieLayout.css). The 390 reference is unchanged. DEVIATIONS #IDC-30. */
262
+ @container pcs-screen ((max-width: 340px) or (max-height: 720px)) {
263
+ .id-capture-stage__feedback .prizma-capture-feedback__illustration {
264
+ display: none;
265
+ }
266
+
267
+ /* Template B: body padding-bottom 12 → 16, itemSpacing 8 → 4. */
268
+ .id-capture-stage__feedback .prizma-capture-feedback__body {
269
+ padding: var(--prizma-spacing-16) 0;
270
+ gap: var(--prizma-spacing-4);
271
+ }
272
+
273
+ /* Title Headline/H3 → Headline/H5. */
274
+ .id-capture-stage__feedback .prizma-capture-feedback__title {
275
+ font-weight: var(--prizma-headline-h5-font-weight);
276
+ font-size: var(--prizma-headline-h5-font-size);
277
+ line-height: var(--prizma-headline-h5-line-height);
278
+ letter-spacing: var(--prizma-headline-h5-letter-spacing);
279
+ }
280
+
281
+ /* Subtitle Headline/H5 → Body/M Regular. */
282
+ .id-capture-stage__feedback .prizma-capture-feedback__subtitle {
283
+ font-family: var(--prizma-body-family);
284
+ font-weight: var(--prizma-body-m-regular-font-weight);
285
+ font-size: var(--prizma-body-m-regular-font-size);
286
+ line-height: var(--prizma-body-m-regular-line-height);
287
+ letter-spacing: var(--prizma-body-m-regular-letter-spacing);
288
+ }
289
+ }
290
+
128
291
  /* ── Removed: promoted into the foundation ───────────────────────────────────
129
292
  * `.id-capture-glyph*` (the non-status glyph slot) is now
130
293
  * `ScreenLayout.css`'s `__glyph` — DEVIATIONS #DC-29, closed.
@@ -142,7 +305,7 @@
142
305
  *
143
306
  * The plate needs no override — Core's own `mode="overlay"` already makes the
144
307
  * window transparent, which is what the light screens want. The ink does: the
145
- * row is re-inked with the THEMED body token so it also survives dark mode,
308
+ * row is re-inked with the themed body token so it also survives dark mode,
146
309
  * scoped to the light screens only (the camera stage keeps Core's Static white).
147
310
  * Core gap — PrizmaIdCapture has no surface-aware encrypted row. DEVIATIONS.md #IDC-28.
148
311
  */
@@ -32,6 +32,28 @@ export interface IdCaptureStageScreenProps extends Omit<PrizmaScreenShellProps,
32
32
  still?: ReactNode;
33
33
  /** Digit shown by the countdown Timer (`*-countdown` sections 1..3). */
34
34
  countdownValue?: 1 | 2 | 3;
35
+ /**
36
+ * Raise the "Need more time?" offer over the feed — the accessibility
37
+ * notification shown 20 s before auto-capture falls back to the shutter.
38
+ *
39
+ * A MODIFIER of whatever stage state is already showing, not a twelfth
40
+ * state: the 30 s fallback timer runs underneath the hint overlay, so the
41
+ * person can be shown the hand-and-document ghost and offered more time in
42
+ * the same frame. Needs `strings.stage.extendTime`; without it the flag is a
43
+ * no-op, which is what a host that has not localized the copy should get
44
+ * rather than English. Same contract as `FaceCaptureCapturingScreen`.
45
+ */
46
+ extendTime?: boolean;
47
+ /** Tapping "Extend auto-capture". The host resets its own fallback timer. */
48
+ onExtendTime?: () => void;
49
+ /**
50
+ * A notice band of the host's own, in the slot `extendTime` uses. Wins over
51
+ * `extendTime` when both are given. Exists for a host that needs the
52
+ * Snackbar to do something the static one cannot — Core Lab's prototype
53
+ * swaps in a copy whose count actually runs down — without reaching into
54
+ * the screen. The static `extendTime` notice stays the default.
55
+ */
56
+ notice?: ReactNode;
35
57
  screenId?: string;
36
58
  overrides?: PrizmaScreenOverrides;
37
59
  onAction?: PrizmaScreenActionHandler;
@@ -64,6 +86,31 @@ export interface IdCaptureStageScreenProps extends Omit<PrizmaScreenShellProps,
64
86
  * to the column's bottom padding) — no offsets are invented here, and
65
87
  * PrizmaIdCapture's own frame→encrypted-row adjacency is left untouched
66
88
  * (DEVIATIONS.md #12's lesson: never stack an offset a Core component carries).
89
+ *
90
+ * ── The "Need more time?" offer (`extendTime`) ──────────────────────────────
91
+ *
92
+ * Auto-capture gives up after a configured timeout (30 s in the module's own
93
+ * flow frames) and drops the person onto the shutter. Someone who is
94
+ * struggling — a tremor, low vision, a dark room, a card that will not sit
95
+ * flat — gets moved off the EASIER path precisely because they were slow,
96
+ * which is backwards. So 20 s before the switch, the stage offers to stay on
97
+ * auto. A client turns it on per flow from the dashboard; with it off, nothing
98
+ * about today's capture changes. It is the conformance route for WCAG 2.2.1
99
+ * Timing Adjustable via the *Extend* clause (warn before the limit, ≥20 s to
100
+ * act, ≥10 extensions) — the reasoning, the `role="alert"` decision and the
101
+ * `neutral` state are all Face Capture's (2026-10-05) and are not re-argued
102
+ * here: `FaceCaptureCapturingScreen`'s header is the record. One feature, two
103
+ * modules, one Snackbar.
104
+ *
105
+ * WHERE IT SITS is the one thing this module decides differently. Face
106
+ * Capture anchors the notice on the shutter-strip rail because, there, notice
107
+ * and shutter are alternatives — a capturing screen either raises the offer or
108
+ * shows the button. ID Capture's strip is never free: the "?" help control
109
+ * rides it on every stage, auto or manual (IDC-29). So the notice takes the
110
+ * BOTTOM BAND instead — the flexible air between the encrypted row and the
111
+ * strip's reserved space — which is the one slot on this stage nothing else
112
+ * owns. It bottom-anchors there like the band's other content would, so the
113
+ * frame above does not move when the offer arrives. DEVIATIONS.md #IDC-32.
67
114
  */
68
115
  export declare const IdCaptureStageScreen: React.ForwardRefExoticComponent<IdCaptureStageScreenProps & React.RefAttributes<HTMLDivElement>>;
69
116
  export default IdCaptureStageScreen;
@@ -1,10 +1,11 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { PrizmaNavigation } from '@incodetech/prizma-core-web/navigation';
5
5
  import { PrizmaIdCapture, } from '@incodetech/prizma-core-web/id-capture';
6
6
  import { PrizmaCaptureManual } from '@incodetech/prizma-core-web/capture-manual';
7
7
  import { PrizmaCaptureFeedback } from '@incodetech/prizma-core-web/capture-feedback';
8
+ import { PrizmaSnackbar } from '@incodetech/prizma-core-web/snackbar';
8
9
  import { PrizmaTag } from '@incodetech/prizma-core-web/tag';
9
10
  import { PrizmaScreenShell } from './ScreenShell';
10
11
  import './ScreenLayout.css';
@@ -68,8 +69,33 @@ const STAGE_FEEDBACK = {
68
69
  * to the column's bottom padding) — no offsets are invented here, and
69
70
  * PrizmaIdCapture's own frame→encrypted-row adjacency is left untouched
70
71
  * (DEVIATIONS.md #12's lesson: never stack an offset a Core component carries).
72
+ *
73
+ * ── The "Need more time?" offer (`extendTime`) ──────────────────────────────
74
+ *
75
+ * Auto-capture gives up after a configured timeout (30 s in the module's own
76
+ * flow frames) and drops the person onto the shutter. Someone who is
77
+ * struggling — a tremor, low vision, a dark room, a card that will not sit
78
+ * flat — gets moved off the EASIER path precisely because they were slow,
79
+ * which is backwards. So 20 s before the switch, the stage offers to stay on
80
+ * auto. A client turns it on per flow from the dashboard; with it off, nothing
81
+ * about today's capture changes. It is the conformance route for WCAG 2.2.1
82
+ * Timing Adjustable via the *Extend* clause (warn before the limit, ≥20 s to
83
+ * act, ≥10 extensions) — the reasoning, the `role="alert"` decision and the
84
+ * `neutral` state are all Face Capture's (2026-10-05) and are not re-argued
85
+ * here: `FaceCaptureCapturingScreen`'s header is the record. One feature, two
86
+ * modules, one Snackbar.
87
+ *
88
+ * WHERE IT SITS is the one thing this module decides differently. Face
89
+ * Capture anchors the notice on the shutter-strip rail because, there, notice
90
+ * and shutter are alternatives — a capturing screen either raises the offer or
91
+ * shows the button. ID Capture's strip is never free: the "?" help control
92
+ * rides it on every stage, auto or manual (IDC-29). So the notice takes the
93
+ * BOTTOM BAND instead — the flexible air between the encrypted row and the
94
+ * strip's reserved space — which is the one slot on this stage nothing else
95
+ * owns. It bottom-anchors there like the band's other content would, so the
96
+ * frame above does not move when the offer arrives. DEVIATIONS.md #IDC-32.
71
97
  */
72
- export const IdCaptureStageScreen = forwardRef(({ strings, documentType = 'id', side = 'front', vertical = false, state = 'empty', manual = false, showClose = state !== 'empty', feedback, camera, still, countdownValue = 3, screenId = `id-capture-${documentType}-${side}-${state}`, overrides, onAction, className, ...shell }, ref) => {
98
+ export const IdCaptureStageScreen = forwardRef(({ strings, documentType = 'id', side = 'front', vertical = false, state = 'empty', manual = false, showClose = state !== 'empty', feedback, camera, still, countdownValue = 3, extendTime, onExtendTime, notice, screenId = `id-capture-${documentType}-${side}-${state}`, overrides, onAction, className, ...shell }, ref) => {
73
99
  const slice = documentType === 'passport'
74
100
  ? strings.stage.passport
75
101
  : side === 'back'
@@ -77,12 +103,22 @@ export const IdCaptureStageScreen = forwardRef(({ strings, documentType = 'id',
77
103
  : strings.stage.front;
78
104
  const pillKind = feedback === undefined ? STAGE_FEEDBACK[state] : feedback;
79
105
  const pillCopy = pillKind ? strings.feedback[pillKind] : undefined;
106
+ const offer = strings.stage.extendTime;
107
+ // No copy, no notice: a host that has not localized this must not get
108
+ // English, and the flag alone is not permission to invent it.
109
+ const band = notice ??
110
+ (extendTime && offer ? (_jsxs(PrizmaSnackbar, { state: "neutral", isMultiline: true,
111
+ // Assertive on purpose — a polite region can queue behind the
112
+ // stage's own state announcements and be read out after the 20 s
113
+ // has already run out. FaceCaptureCapturingScreen's header has the
114
+ // full reasoning; this instance follows it.
115
+ role: "alert", linkLabel: offer.linkLabel, onLinkClick: onExtendTime, children: [_jsx("strong", { children: offer.title }), _jsx("br", {}), offer.description] })) : undefined);
80
116
  return (_jsxs(PrizmaScreenShell, { ref: ref, dark: true, className: className, ...shell, children: [_jsx("div", { className: "prizma-viewfinder__surface", children: camera }), _jsx("header", { className: "prizma-screen-nav id-capture-stage__nav", children: _jsx(PrizmaNavigation, { showIncodeLogo: false, showRightIcon: showClose, closeLabel: strings.closeLabel, "aria-label": strings.navLabel, onRightIconClick: () => onAction?.({ type: 'secondary', screenId }) }) }), _jsxs("div", { className: [
81
117
  'prizma-viewfinder__content',
82
118
  overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
83
119
  ]
84
120
  .filter(Boolean)
85
- .join(' '), children: [_jsx("div", { className: "id-capture-band id-capture-band--top", children: pillKind && pillCopy ? (_jsx("div", { className: "id-capture-stage__feedback", children: _jsx(PrizmaCaptureFeedback, { feedback: pillKind, title: pillCopy.title, subtitle: pillCopy.subtitle }) })) : (_jsxs(_Fragment, { children: [_jsx("h2", { className: "prizma-viewfinder__title", children: slice.title }), slice.instruction && !manual ? (_jsx("p", { className: "prizma-viewfinder__instruction", children: slice.instruction })) : null] })) }), _jsx(PrizmaIdCapture, { type: documentType, state: CAPTURE_STATE[state], isVertical: vertical, mode: "overlay", scrim: true, countdownValue: countdownValue, encryptedLabel: strings.encryptedLabel, announcement: strings.stage.stateLabels?.[state], still: still }), _jsx("div", { className: "id-capture-band" })] }), _jsx("div", { className: "prizma-screen-layout__capture-strip", children: _jsx(PrizmaCaptureManual, { className: "id-capture-stage__manual", variant: "id-capture", showButton: manual, showTooltip: manual, showHelpIcon: true, tooltipText: manual ? strings.stage.manualTooltip : undefined, ariaLabel: strings.stage.captureLabel, helpLabel: strings.stage.helpLabel, onCapture: () => onAction?.({ type: 'cta', screenId }), onHelp: () => onAction?.({ type: 'help', screenId }) }) }), _jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: strings.verifiedLabel }) })] }));
121
+ .join(' '), children: [_jsx("div", { className: "id-capture-band id-capture-band--top", children: pillKind && pillCopy ? (_jsx("div", { className: "id-capture-stage__feedback", children: _jsx(PrizmaCaptureFeedback, { feedback: pillKind, title: pillCopy.title, subtitle: pillCopy.subtitle }) })) : (_jsxs(_Fragment, { children: [_jsx("h2", { className: "prizma-viewfinder__title", children: slice.title }), slice.instruction && !manual ? (_jsx("p", { className: "prizma-viewfinder__instruction", children: slice.instruction })) : null] })) }), _jsx(PrizmaIdCapture, { type: documentType, state: CAPTURE_STATE[state], isVertical: vertical, mode: "overlay", scrim: true, countdownValue: countdownValue, encryptedLabel: strings.encryptedLabel, announcement: strings.stage.stateLabels?.[state], still: still }), _jsx("div", { className: "id-capture-band", children: band ? _jsx("div", { className: "id-capture-stage__notice", children: band }) : null })] }), _jsx("div", { className: "prizma-screen-layout__capture-strip", children: _jsx(PrizmaCaptureManual, { className: "id-capture-stage__manual", variant: "id-capture", showButton: manual, showTooltip: manual, showHelpIcon: true, tooltipText: manual ? strings.stage.manualTooltip : undefined, ariaLabel: strings.stage.captureLabel, helpLabel: strings.stage.helpLabel, onCapture: () => onAction?.({ type: 'cta', screenId }), onHelp: () => onAction?.({ type: 'help', screenId }) }) }), _jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: strings.verifiedLabel }) })] }));
86
122
  });
87
123
  IdCaptureStageScreen.displayName = 'IdCaptureStageScreen';
88
124
  export default IdCaptureStageScreen;
@@ -102,6 +102,25 @@ export interface IdCaptureStrings {
102
102
  /** Visually-hidden state announcements, per stage state — forwarded to
103
103
  * `PrizmaIdCapture.stateLabels`. */
104
104
  stateLabels?: Partial<Record<IdCaptureStageState, string>>;
105
+ /**
106
+ * stage.extendTime — the accessibility offer that rides the camera stage
107
+ * 20 s before auto-capture gives up and hands over to the shutter
108
+ * ("Need more time? / Manual capture starts in 20 s / Extend auto-capture").
109
+ * The same contract as Face Capture's `extendTime`: one feature, two
110
+ * modules, identical copy, so the locale overlay translates both at once.
111
+ *
112
+ * `description` is one localized sentence that CONTAINS the count, not a
113
+ * template — ten locales put the number in ten places. Optional: a host
114
+ * that has not localized it gets no notice rather than English.
115
+ */
116
+ extendTime?: {
117
+ /** Bold first line — "Need more time?" */
118
+ title: string;
119
+ /** "Manual capture starts in 20 s" — the count lives inside the copy. */
120
+ description: string;
121
+ /** The action that keeps the user on auto — "Extend auto-capture". */
122
+ linkLabel: string;
123
+ };
105
124
  };
106
125
  /** The on-camera feedback pills the error stages carry. */
107
126
  feedback: Partial<Record<IdCaptureFeedbackKind, IdCaptureFeedbackCopy>>;
@@ -61,6 +61,25 @@ export interface PrizmaListScreenProps extends Omit<PrizmaScreenShellProps, 'dar
61
61
  * (common issues) and "Quit process" (permission). Figma draws Core's
62
62
  * `tertiary-link` composite in the band's top slot. */
63
63
  linkLabel?: string;
64
+ /**
65
+ * A SECOND tertiary link, under the first. Reusability's selection draws
66
+ * "What data will be shared?" over "Incode’s Privacy Policy" with no primary
67
+ * between them — two non-primary actions a host has to tell apart, so this
68
+ * one emits `tertiary` where `linkLabel` emits `secondary` (the #PI-10
69
+ * reasoning). Stacked at the band's flat Spacing/16 like everything else in
70
+ * it. Omit it and nothing renders, so every existing consumer is untouched.
71
+ * DEVIATIONS #RU-4.
72
+ */
73
+ secondaryLinkLabel?: string;
74
+ /**
75
+ * A shell-level layer painted OVER the whole screen — the same slot
76
+ * archetypes A and E carry (#F-32, #BC-8): a scrim plus Core's
77
+ * `PrizmaBottomSheetContent` or `PrizmaModalContent`, presented on top of the
78
+ * dimmed list. It cannot ride in the content column: it is absolutely
79
+ * positioned against the shell. Reusability's shared-details sheet is the
80
+ * first List consumer (DEVIATIONS #RU-5).
81
+ */
82
+ overlay?: ReactNode;
64
83
  /** "verified by" attribution copy for the footer tag. */
65
84
  verifiedLabel: string;
66
85
  navLabel?: string;
@@ -31,7 +31,7 @@ import './ScreenLayout.css';
31
31
  * Reach for `PrizmaResultScreen` instead when the middle is a centred verdict
32
32
  * rather than a list.
33
33
  */
34
- export const PrizmaListScreen = forwardRef(({ progress, status = 'none', title, subtitle, glyph, children, listAlign = 'start', scrollList = false, disclaimer, ctaLabel, secondaryCtaLabel, linkLabel, verifiedLabel, navLabel, backLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, children: [_jsx(PrizmaScreenNav, { reserved: !showHeader, logo: overrides?.logo, navLabel: navLabel, backLabel: backLabel, onBack: () => onAction?.({ type: 'back', screenId }) }), progress ? (_jsx("div", { className: "prizma-screen-layout__progress", children: progress })) : null, _jsxs("div", { className: "prizma-screen-layout__content", children: [glyph, _jsx(PrizmaTitle, { variant: overrides?.align === 'left' ? 'left' : 'centered', title: title, showSubtitle: Boolean(subtitle), subtitle: subtitle, icon: status === 'loading'
34
+ export const PrizmaListScreen = forwardRef(({ progress, status = 'none', title, subtitle, glyph, children, listAlign = 'start', scrollList = false, disclaimer, ctaLabel, secondaryCtaLabel, linkLabel, secondaryLinkLabel, overlay, verifiedLabel, navLabel, backLabel, screenId, showHeader = true, showFooter = true, overrides, onAction, ...shell }, ref) => (_jsxs(PrizmaScreenShell, { ref: ref, ...shell, children: [_jsx(PrizmaScreenNav, { reserved: !showHeader, logo: overrides?.logo, navLabel: navLabel, backLabel: backLabel, onBack: () => onAction?.({ type: 'back', screenId }) }), progress ? (_jsx("div", { className: "prizma-screen-layout__progress", children: progress })) : null, _jsxs("div", { className: "prizma-screen-layout__content", children: [glyph, _jsx(PrizmaTitle, { variant: overrides?.align === 'left' ? 'left' : 'centered', title: title, showSubtitle: Boolean(subtitle), subtitle: subtitle, icon: status === 'loading'
35
35
  ? { type: 'spinner' }
36
36
  : status === 'none'
37
37
  ? { type: 'none' }
@@ -41,6 +41,11 @@ export const PrizmaListScreen = forwardRef(({ progress, status = 'none', title,
41
41
  scrollList ? 'prizma-screen-layout__list--scroll' : '',
42
42
  ]
43
43
  .filter(Boolean)
44
- .join(' '), children: children }), ctaLabel || secondaryCtaLabel || linkLabel || disclaimer ? (_jsxs("div", { className: "prizma-screen-layout__band", children: [disclaimer ? _jsx(PrizmaDisclaimer, { text: disclaimer }) : null, linkLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: linkLabel })) : null, ctaLabel ? (_jsx(PrizmaButton, { variant: "primary", size: "L", onClick: () => onAction?.({ type: 'cta', screenId }), children: ctaLabel })) : null, secondaryCtaLabel ? (_jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: secondaryCtaLabel })) : null] })) : null] }), showFooter ? (_jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: verifiedLabel }) })) : null] })));
44
+ .join(' '), children: children }), ctaLabel || secondaryCtaLabel || linkLabel || secondaryLinkLabel || disclaimer ? (_jsxs("div", { className: "prizma-screen-layout__band", children: [disclaimer ? _jsx(PrizmaDisclaimer, { text: disclaimer }) : null, linkLabel || secondaryLinkLabel ? (
45
+ /* The links share ONE band slot, stacked flush: two 56px link
46
+ rows with the band's 16 between them read as two distant
47
+ lines (Camila, 2026-10-13); the row height is already the
48
+ touch target. ScreenLayout.css `__band__links`. */
49
+ _jsxs("div", { className: "prizma-screen-layout__band__links", children: [linkLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: linkLabel })) : null, secondaryLinkLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId }), children: secondaryLinkLabel })) : null] })) : null, ctaLabel ? (_jsx(PrizmaButton, { variant: "primary", size: "L", onClick: () => onAction?.({ type: 'cta', screenId }), children: ctaLabel })) : null, secondaryCtaLabel ? (_jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: secondaryCtaLabel })) : null] })) : null] }), showFooter ? (_jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: verifiedLabel }) })) : null, overlay] })));
45
50
  PrizmaListScreen.displayName = 'PrizmaListScreen';
46
51
  export default PrizmaListScreen;