@incodetech/prizma-core-screens 0.2.3 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CurpEnterScreen.d.ts +7 -0
- package/dist/CurpEnterScreen.js +17 -3
- package/dist/CurpGenerateScreen.d.ts +8 -0
- package/dist/CurpGenerateScreen.js +28 -13
- package/dist/CurpLayout.css +8 -34
- package/dist/CurpStrings.d.ts +16 -2
- package/dist/EkybAddressDetailsScreen.d.ts +7 -0
- package/dist/EkybAddressDetailsScreen.js +25 -7
- package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
- package/dist/EkybBusinessDetailsScreen.js +23 -6
- package/dist/EkybCountryScreen.d.ts +13 -5
- package/dist/EkybCountryScreen.js +27 -9
- package/dist/EkybOwnersScreen.d.ts +8 -0
- package/dist/EkybOwnersScreen.js +26 -3
- package/dist/EkybStrings.d.ts +27 -10
- package/dist/EkycCountryCatalog.d.ts +4 -1
- package/dist/EkycCountryCatalog.js +5 -3
- package/dist/EkycCountryFormScreen.d.ts +12 -3
- package/dist/EkycCountryFormScreen.js +28 -10
- package/dist/EkycDetailsScreen.d.ts +7 -0
- package/dist/EkycDetailsScreen.js +21 -4
- package/dist/EkycStrings.d.ts +14 -2
- package/dist/ElectronicSignatureLayout.css +6 -0
- package/dist/EmailInputStrings.d.ts +11 -1
- package/dist/EmailInputVerifyScreen.d.ts +7 -0
- package/dist/EmailInputVerifyScreen.js +17 -3
- package/dist/FaceMatchComparisonScreen.d.ts +3 -3
- package/dist/FaceMatchLayout.css +21 -12
- package/dist/FormScreen.css +16 -0
- package/dist/FormsLongFormScreen.d.ts +8 -0
- package/dist/FormsLongFormScreen.js +34 -13
- package/dist/FormsStrings.d.ts +11 -1
- package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
- package/dist/GeolocationBrowserStepsScreen.js +7 -3
- package/dist/GeolocationStrings.d.ts +3 -0
- package/dist/IdCaptureLayout.css +40 -0
- package/dist/ListScreen.d.ts +2 -0
- package/dist/ListScreen.js +1 -1
- package/dist/PhoneInputLayout.css +10 -9
- package/dist/PhoneInputStrings.d.ts +11 -1
- package/dist/PhoneInputVerifyScreen.d.ts +8 -0
- package/dist/PhoneInputVerifyScreen.js +19 -6
- package/dist/QualifiedSignatureAcceptScreen.js +6 -1
- package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
- package/dist/QualifiedSignatureDocumentScreen.js +18 -16
- package/dist/QualifiedSignatureLayout.css +76 -0
- package/dist/QualifiedSignatureStrings.d.ts +10 -0
- package/dist/ScreenNav.css +8 -0
- package/dist/ScreenNav.d.ts +15 -2
- package/dist/ScreenNav.js +10 -3
- package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
- package/dist/VideoSelfieIdCaptureScreen.js +44 -13
- package/dist/VideoSelfieLayout.css +164 -33
- package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
- package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
- package/dist/VideoSelfieSpeakScreen.js +18 -1
- package/dist/VideoSelfieStrings.d.ts +78 -12
- package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
- package/dist/VideoSelfieTurnHeadScreen.js +63 -0
- package/dist/VideoSelfieTutorialScreen.js +13 -5
- package/dist/VideoSelfieWaveScreen.d.ts +53 -0
- package/dist/VideoSelfieWaveScreen.js +62 -0
- package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
- package/dist/WatchlistBusinessFormScreen.js +27 -13
- package/dist/WatchlistBusinessStrings.d.ts +21 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +3 -1
- package/package.json +14 -3
- package/samples/video-selfie-room.png +0 -0
- package/src/CurpEnterScreen.tsx +21 -4
- package/src/CurpGenerateScreen.tsx +46 -22
- package/src/CurpLayout.css +8 -34
- package/src/CurpStrings.ts +22 -2
- package/src/EkybAddressDetailsScreen.tsx +32 -4
- package/src/EkybBusinessDetailsScreen.tsx +23 -5
- package/src/EkybCountryScreen.tsx +28 -7
- package/src/EkybOwnersScreen.tsx +34 -3
- package/src/EkybStrings.ts +29 -10
- package/src/EkycCountryCatalog.ts +9 -4
- package/src/EkycCountryFormScreen.tsx +51 -12
- package/src/EkycDetailsScreen.tsx +22 -1
- package/src/EkycStrings.ts +14 -2
- package/src/ElectronicSignatureLayout.css +6 -0
- package/src/EmailInputStrings.ts +12 -1
- package/src/EmailInputVerifyScreen.tsx +21 -4
- package/src/FaceMatchComparisonScreen.tsx +3 -3
- package/src/FaceMatchLayout.css +21 -12
- package/src/FormScreen.css +16 -0
- package/src/FormsLongFormScreen.tsx +41 -13
- package/src/FormsStrings.ts +12 -1
- package/src/GeolocationBrowserStepsScreen.tsx +6 -3
- package/src/GeolocationStrings.ts +3 -0
- package/src/IdCaptureLayout.css +40 -0
- package/src/ListScreen.tsx +10 -1
- package/src/PhoneInputLayout.css +10 -9
- package/src/PhoneInputStrings.ts +12 -1
- package/src/PhoneInputVerifyScreen.tsx +21 -5
- package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
- package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
- package/src/QualifiedSignatureLayout.css +76 -0
- package/src/QualifiedSignatureStrings.ts +10 -0
- package/src/ScreenNav.css +8 -0
- package/src/ScreenNav.tsx +35 -5
- package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
- package/src/VideoSelfieLayout.css +164 -33
- package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
- package/src/VideoSelfieSpeakScreen.tsx +25 -14
- package/src/VideoSelfieStrings.ts +85 -12
- package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
- package/src/VideoSelfieTutorialScreen.tsx +34 -23
- package/src/VideoSelfieWaveScreen.tsx +135 -0
- package/src/WatchlistBusinessFormScreen.tsx +35 -16
- package/src/WatchlistBusinessStrings.ts +21 -7
- package/src/index.ts +14 -0
- package/src/types.ts +3 -1
|
@@ -48,6 +48,13 @@ export interface EmailInputVerifyScreenProps
|
|
|
48
48
|
* paints the DISABLED surface (owner definition 2026-07-12), which would grey
|
|
49
49
|
* the four resting states Figma draws white. The field is uncontrolled instead
|
|
50
50
|
* (DEVIATIONS #PI-6).
|
|
51
|
+
*
|
|
52
|
+
* `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
|
|
53
|
+
* GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
|
|
54
|
+
* typed. The field renders empty with no placeholder, in Core's error state
|
|
55
|
+
* with `requiredError`, and the primary CTA is disabled, following the drawn
|
|
56
|
+
* precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
|
|
57
|
+
* so the user can reach this state.
|
|
51
58
|
*/
|
|
52
59
|
export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerifyScreenProps>(
|
|
53
60
|
(
|
|
@@ -64,6 +71,9 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
64
71
|
) => {
|
|
65
72
|
const busy = state === 'loading';
|
|
66
73
|
const invalid = state === 'error';
|
|
74
|
+
// Empty submit: the field drops its placeholder and takes Core's error
|
|
75
|
+
// state with the shared "required" row; the CTA greys.
|
|
76
|
+
const emptyError = state === 'emptyError';
|
|
67
77
|
const focused = state === 'typing' || state === 'filled';
|
|
68
78
|
|
|
69
79
|
return (
|
|
@@ -74,6 +84,7 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
74
84
|
ctaLabel={strings.verify.ctaLabel}
|
|
75
85
|
/* Figma's `loading` section swaps the CTA label for Core's spinner. */
|
|
76
86
|
ctaLoading={busy}
|
|
87
|
+
ctaDisabled={emptyError}
|
|
77
88
|
ctaLoadingLabel={strings.verify.ctaLoadingLabel}
|
|
78
89
|
verifiedLabel={strings.verifiedLabel}
|
|
79
90
|
navLabel={strings.navLabel}
|
|
@@ -97,13 +108,19 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
97
108
|
/* Figma's field has no visible label row — the placeholder carries
|
|
98
109
|
the meaning — so the copy rides `aria-label` (DEVIATIONS #PI-4). */
|
|
99
110
|
aria-label={strings.verify.fieldLabel}
|
|
100
|
-
placeholder={strings.verify.placeholder}
|
|
111
|
+
placeholder={emptyError ? undefined : strings.verify.placeholder}
|
|
101
112
|
/* Uncontrolled: a screen is one static spec state, so no `onChange`
|
|
102
113
|
is needed and no warning is raised. */
|
|
103
|
-
defaultValue={value ?? ''}
|
|
114
|
+
defaultValue={emptyError ? '' : value ?? ''}
|
|
104
115
|
disabled={busy}
|
|
105
|
-
error={invalid}
|
|
106
|
-
errorText={
|
|
116
|
+
error={invalid || emptyError}
|
|
117
|
+
errorText={
|
|
118
|
+
invalid
|
|
119
|
+
? strings.verify.errorText
|
|
120
|
+
: emptyError
|
|
121
|
+
? strings.requiredError
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
107
124
|
/* The clear ✕ is drawn on the `error` section only. */
|
|
108
125
|
clearable={invalid}
|
|
109
126
|
clearLabel={strings.verify.clearLabel}
|
|
@@ -17,9 +17,9 @@ import './FaceMatchLayout.css';
|
|
|
17
17
|
export type FaceMatchPhotoRole = 'selfie' | 'id' | 'nfc' | 'secondId';
|
|
18
18
|
|
|
19
19
|
/** Host-supplied portraits, one per role the chosen variant asks for. A role
|
|
20
|
-
* left out renders the empty state
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* left out renders the empty state (capture missing or not retrievable): a
|
|
21
|
+
* grey Surface/Neutral/100 disc under its usual label tooltip, marked with
|
|
22
|
+
* `.face-match-cluster__photo--empty`. See FaceMatchLayout.css. */
|
|
23
23
|
export type FaceMatchComparisonPhotos = Partial<Record<FaceMatchPhotoRole, ReactNode>>;
|
|
24
24
|
|
|
25
25
|
/**
|
package/src/FaceMatchLayout.css
CHANGED
|
@@ -57,8 +57,14 @@
|
|
|
57
57
|
|
|
58
58
|
/* The disc's diameter, named once. `--matching`'s rosette and `--animated`'s
|
|
59
59
|
* convergence both have to know it, and a second literal is a second thing to
|
|
60
|
-
* forget. Still a design literal, still DEVIATIONS #FM-22 — just not repeated.
|
|
61
|
-
|
|
60
|
+
* forget. Still a design literal, still DEVIATIONS #FM-22 — just not repeated.
|
|
61
|
+
*
|
|
62
|
+
* 160 at most, and smaller when two discs and their gap do not fit the
|
|
63
|
+
* content column: on a 320 phone the column is 272, so each disc takes
|
|
64
|
+
* (272 − 16) / 2 = 128 (Camila, 2026-09-28). `100cqi` is the screen shell's
|
|
65
|
+
* width (ScreenShell.css, `container-type: size`), less its two Spacing/24
|
|
66
|
+
* gutters. At 390 and up this resolves to 160, unchanged. */
|
|
67
|
+
--face-match-photo: min(160px, calc((100cqi - 2 * var(--prizma-spacing-24) - var(--prizma-spacing-16)) / 2));
|
|
62
68
|
/* Half the distance between two neighbouring discs: what one of them has to
|
|
63
69
|
* travel to reach the pair's centre line. */
|
|
64
70
|
--face-match-half-step: calc((var(--face-match-photo, 160px) + var(--prizma-spacing-16)) / 2);
|
|
@@ -118,16 +124,16 @@
|
|
|
118
124
|
background: var(--prizma-surface-neutral-100);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
/* No photo
|
|
127
|
+
/* No photo for this role: the capture was never taken, or could not be
|
|
128
|
+
* retrieved.
|
|
122
129
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* the
|
|
127
|
-
*
|
|
128
|
-
* so swapping it in is a one-rule change and nothing downstream moves. */
|
|
130
|
+
* Owner call 2026-09-28 (replaces the 2026-09-25 Brand/50 interim): a plain
|
|
131
|
+
* grey disc, Surface/Neutral/100, the same neutral Core puts behind an image
|
|
132
|
+
* that is not there yet (CaptureDigitalId.css, list.css). The callout above it
|
|
133
|
+
* still renders, so the tooltip names which capture is missing. The modifier
|
|
134
|
+
* stays a named state so a richer treatment is still a one-rule change. */
|
|
129
135
|
.face-match-cluster__photo--empty {
|
|
130
|
-
background: var(--prizma-surface-
|
|
136
|
+
background: var(--prizma-surface-neutral-100);
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
.face-match-cluster__photo > img,
|
|
@@ -153,11 +159,14 @@
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
.face-match-cluster--matching .face-match-cluster__face + .face-match-cluster__face {
|
|
156
|
-
|
|
162
|
+
/* Figma converged overlap, 44 on the 160 disc; no token (verified absent).
|
|
163
|
+
* Kept as that ratio so the rosette holds its shape on a smaller disc. */
|
|
164
|
+
margin-left: calc(var(--face-match-photo, 160px) * -44 / 160);
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
.face-match-cluster--matching .face-match-cluster__row + .face-match-cluster__row {
|
|
160
|
-
|
|
168
|
+
/* Half a disc: 80 on the 160 disc (`2 × Spacing/40`), proportional below it. */
|
|
169
|
+
margin-top: calc(var(--face-match-photo, 160px) / -2);
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/* ── The comparison, moving (`phase` on FaceMatchComparisonScreen) ───────────
|
package/src/FormScreen.css
CHANGED
|
@@ -129,6 +129,22 @@
|
|
|
129
129
|
flex: none;
|
|
130
130
|
width: calc(50% - var(--prizma-spacing-16) / 2);
|
|
131
131
|
min-width: 0;
|
|
132
|
+
position: relative;
|
|
133
|
+
}
|
|
134
|
+
/* An error or helper line under ONE field of a pair made that field taller,
|
|
135
|
+
* and the bottom alignment above lifted its input above its neighbour's
|
|
136
|
+
* (Elena, 2026-10-02: "estos inputs tienen que estar alineados"). The line
|
|
137
|
+
* hangs below its input instead, out of the alignment, and the row reserves
|
|
138
|
+
* its height so the next row does not ride over it. */
|
|
139
|
+
.prizma-form-screen--responsive .prizma-form-screen__row > * > [class$="__error"],
|
|
140
|
+
.prizma-form-screen--responsive .prizma-form-screen__row > * > [class$="__helper"] {
|
|
141
|
+
position: absolute;
|
|
142
|
+
top: 100%;
|
|
143
|
+
left: 0;
|
|
144
|
+
margin-top: var(--prizma-spacing-8);
|
|
145
|
+
}
|
|
146
|
+
.prizma-form-screen--responsive .prizma-form-screen__row:has(> * > [class$="__error"], > * > [class$="__helper"]) {
|
|
147
|
+
padding-bottom: calc(var(--prizma-spacing-8) + var(--prizma-body-m-regular-line-height));
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
/* Continue and Back share a row: Back on the left, Continue on the right, as
|
|
@@ -51,7 +51,9 @@ const PRESET_ROWS: Record<FormsLongFormPreset, FormsLongFormRow[]> = {
|
|
|
51
51
|
{ key: 'idNumber', kind: 'text', requirement: 'required' },
|
|
52
52
|
{ key: 'email', kind: 'email', requirement: 'required' },
|
|
53
53
|
{ key: 'residence', kind: 'country', requirement: 'required' },
|
|
54
|
-
|
|
54
|
+
// Date of birth carries NO asterisk (owner Elena, 2026-09-30), so the
|
|
55
|
+
// `emptyError` state flags the other three rows and leaves it at rest.
|
|
56
|
+
{ key: 'dob', kind: 'date', requirement: 'none' },
|
|
55
57
|
],
|
|
56
58
|
};
|
|
57
59
|
|
|
@@ -131,6 +133,14 @@ export interface FormsLongFormScreenProps
|
|
|
131
133
|
* into the CTA band — the half a module stylesheet kept losing. It is a no-op
|
|
132
134
|
* on the short `name` preset, whose rows never overflow. DEVIATIONS #DSC-2,
|
|
133
135
|
* restating #CURP-1.
|
|
136
|
+
*
|
|
137
|
+
* `emptyError` is the GUIDELINES §6-E empty-submit rule (Elena 2026-09-30):
|
|
138
|
+
* the user pressed the CTA with the form still empty. Every REQUIRED row
|
|
139
|
+
* renders empty, with no placeholder, in Core's own error state carrying
|
|
140
|
+
* `strings.requiredError`; optional and unmarked rows stay on their resting
|
|
141
|
+
* surface with no error. The CTA greys there and ONLY there: the resting
|
|
142
|
+
* `default` keeps it enabled, so the user can press it to reach the error —
|
|
143
|
+
* the `FormsFieldScreen` `emptyError` precedent applied to a stack.
|
|
134
144
|
*/
|
|
135
145
|
export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScreenProps>(
|
|
136
146
|
(
|
|
@@ -152,6 +162,7 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
152
162
|
) => {
|
|
153
163
|
const copy = strings.longForm[preset];
|
|
154
164
|
const busy = state === 'loading';
|
|
165
|
+
const emptyError = state === 'emptyError';
|
|
155
166
|
const countryList = countries ?? [];
|
|
156
167
|
|
|
157
168
|
// The Optional / Required pair documents the eKYC form specifically; no
|
|
@@ -174,8 +185,15 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
174
185
|
const fieldCopy = copy.fields[row.key];
|
|
175
186
|
const label = labelFor(row);
|
|
176
187
|
const required = row.requirement === 'required';
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
// `emptyError` is an EMPTY submit: every row renders empty, whatever
|
|
189
|
+
// values the host passed.
|
|
190
|
+
const value = emptyError ? '' : (values?.[row.key] ?? '');
|
|
191
|
+
const focused = !busy && !emptyError && focusedField === row.key;
|
|
192
|
+
// Only REQUIRED rows flag the empty submit. Their placeholder goes, as
|
|
193
|
+
// on the drawn `forms.IDnumber.error`; optional rows stay at rest.
|
|
194
|
+
const invalid = emptyError && required;
|
|
195
|
+
const errorText = invalid ? strings.requiredError : undefined;
|
|
196
|
+
const placeholder = invalid ? undefined : fieldCopy?.placeholder;
|
|
179
197
|
|
|
180
198
|
switch (row.kind) {
|
|
181
199
|
case 'country':
|
|
@@ -185,12 +203,14 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
185
203
|
label={label}
|
|
186
204
|
required={required}
|
|
187
205
|
requiredHint={strings.requiredHint}
|
|
188
|
-
placeholder={
|
|
206
|
+
placeholder={placeholder}
|
|
189
207
|
options={countryList.map((c) => ({ value: c.code, label: c.name }))}
|
|
190
208
|
value={value || null}
|
|
191
209
|
searchable
|
|
192
210
|
searchPlaceholder={copy.searchPlaceholder}
|
|
193
211
|
disabled={busy}
|
|
212
|
+
error={invalid}
|
|
213
|
+
errorText={errorText}
|
|
194
214
|
forceState={focused ? 'focused' : undefined}
|
|
195
215
|
renderLeading={(option) => {
|
|
196
216
|
const match = countryList.find((c) => c.code === option.value);
|
|
@@ -205,10 +225,12 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
205
225
|
label={label}
|
|
206
226
|
required={required}
|
|
207
227
|
requiredHint={strings.requiredHint}
|
|
208
|
-
placeholder={
|
|
228
|
+
placeholder={placeholder}
|
|
209
229
|
options={options?.[row.key] ?? []}
|
|
210
230
|
value={value || null}
|
|
211
231
|
disabled={busy}
|
|
232
|
+
error={invalid}
|
|
233
|
+
errorText={errorText}
|
|
212
234
|
forceState={focused ? 'focused' : undefined}
|
|
213
235
|
/>
|
|
214
236
|
);
|
|
@@ -220,9 +242,13 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
220
242
|
label={label}
|
|
221
243
|
required={required}
|
|
222
244
|
requiredHint={strings.requiredHint}
|
|
223
|
-
|
|
245
|
+
/* Core falls back to its DD/MM/YYYY mask on an undefined
|
|
246
|
+
placeholder, so the error row passes an explicit empty one. */
|
|
247
|
+
placeholder={invalid ? '' : fieldCopy?.placeholder}
|
|
224
248
|
defaultValue={value}
|
|
225
249
|
disabled={busy}
|
|
250
|
+
error={invalid}
|
|
251
|
+
errorText={errorText}
|
|
226
252
|
calendarLabel={copy.calendarLabel}
|
|
227
253
|
/>
|
|
228
254
|
);
|
|
@@ -234,7 +260,7 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
234
260
|
label={label}
|
|
235
261
|
required={required}
|
|
236
262
|
requiredHint={strings.requiredHint}
|
|
237
|
-
placeholder={
|
|
263
|
+
placeholder={placeholder}
|
|
238
264
|
defaultValue={value}
|
|
239
265
|
type={row.kind === 'email' ? 'email' : 'text'}
|
|
240
266
|
inputMode={row.kind === 'email' ? 'email' : undefined}
|
|
@@ -242,6 +268,8 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
242
268
|
autoCapitalize={row.kind === 'email' ? 'none' : undefined}
|
|
243
269
|
spellCheck={false}
|
|
244
270
|
disabled={busy}
|
|
271
|
+
error={invalid}
|
|
272
|
+
errorText={errorText}
|
|
245
273
|
/* Figma draws the clear ✕ only on the focused row of
|
|
246
274
|
`longform.focused`; every other row draws none. */
|
|
247
275
|
clearable={focused}
|
|
@@ -266,12 +294,12 @@ export const FormsLongFormScreen = forwardRef<HTMLDivElement, FormsLongFormScree
|
|
|
266
294
|
title={copy.title}
|
|
267
295
|
subtitle={copy.subtitle}
|
|
268
296
|
ctaLabel={copy.ctaLabel}
|
|
269
|
-
/*
|
|
270
|
-
the
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
ctaDisabled={
|
|
297
|
+
/* GUIDELINES §6-E empty-submit rule (Elena 2026-09-30): the CTA stays
|
|
298
|
+
ENABLED on the resting empty form, every preset alike, so the user
|
|
299
|
+
can press it; it greys only on `emptyError`, the state that press
|
|
300
|
+
leads to. This supersedes the per-section greying #FDE-3 followed
|
|
301
|
+
on `longform.default` and `longform.customizable`. */
|
|
302
|
+
ctaDisabled={emptyError}
|
|
275
303
|
ctaLoading={busy}
|
|
276
304
|
ctaLoadingLabel={copy.ctaLoadingLabel}
|
|
277
305
|
/* The four- and five-row stacks are taller than the 844 frame; the
|
package/src/FormsStrings.ts
CHANGED
|
@@ -127,13 +127,18 @@ export type FormsLongFormPreset = 'name' | 'profile' | 'eKyc';
|
|
|
127
127
|
* - `loading` — `longform.loading`: the whole form greyed, spinner in the CTA
|
|
128
128
|
* - `optional` — the "Optional / Required" documentation pair: the same form
|
|
129
129
|
* with mixed required and optional markers
|
|
130
|
+
* - `emptyError` — the user pressed the CTA with the form still empty: every
|
|
131
|
+
* REQUIRED row renders empty in Core's error state with
|
|
132
|
+
* `requiredError`, optional and unmarked rows stay clean, CTA
|
|
133
|
+
* greyed (GUIDELINES §6-E empty-submit rule, Elena 2026-09-30)
|
|
130
134
|
*/
|
|
131
135
|
export type FormsLongFormState =
|
|
132
136
|
| 'default'
|
|
133
137
|
| 'focused'
|
|
134
138
|
| 'filled'
|
|
135
139
|
| 'loading'
|
|
136
|
-
| 'optional'
|
|
140
|
+
| 'optional'
|
|
141
|
+
| 'emptyError';
|
|
137
142
|
|
|
138
143
|
/** Copy for one single-field section. */
|
|
139
144
|
export interface FormsFieldCopy {
|
|
@@ -218,6 +223,12 @@ export interface FormsStrings {
|
|
|
218
223
|
* the label string by the screen. See DEVIATIONS #FDE-4.
|
|
219
224
|
*/
|
|
220
225
|
optionalHint: string;
|
|
226
|
+
/**
|
|
227
|
+
* The error row every REQUIRED field shows on the `emptyError` long-form
|
|
228
|
+
* state ("This field is required"). Maps to the SDK's
|
|
229
|
+
* `dynamicForms.fieldRequired` key. GUIDELINES §6-E empty-submit rule.
|
|
230
|
+
*/
|
|
231
|
+
requiredError: string;
|
|
221
232
|
|
|
222
233
|
/** The single-field sections. */
|
|
223
234
|
fields: Record<FormsFieldKey, FormsFieldCopy>;
|
|
@@ -97,9 +97,13 @@ export interface GeolocationBrowserStepsScreenProps
|
|
|
97
97
|
* The rows are Core's `PrizmaCardListCommonIssues` verbatim; only the leading
|
|
98
98
|
* glyph is chosen here, from the canonical bank (see `STEP_ICONS`).
|
|
99
99
|
*
|
|
100
|
+
* No subtitle: the browser name (`label`) is NOT drawn under the title (owner
|
|
101
|
+
* call, Elena 2026-09-30). The label stays in the strings as the section's
|
|
102
|
+
* identifier for hosts and catalogs; the screen itself shows title + steps.
|
|
103
|
+
*
|
|
100
104
|
* The title block is LEFT-aligned for this whole family. Every
|
|
101
|
-
* `Geolocation.allowLocation.<browser>` section starts the two-line headline
|
|
102
|
-
*
|
|
105
|
+
* `Geolocation.allowLocation.<browser>` section starts the two-line headline
|
|
106
|
+
* flush at the gutter, over left-led step
|
|
103
107
|
* rows; a centred headline above a left-aligned list reads as a mistake. The
|
|
104
108
|
* screen therefore DEFAULTS the archetype's own `align` override to `left`
|
|
105
109
|
* rather than declaring any alignment of its own, and a host that passes
|
|
@@ -147,7 +151,6 @@ export const GeolocationBrowserStepsScreen = forwardRef<
|
|
|
147
151
|
<PrizmaListScreen
|
|
148
152
|
ref={ref}
|
|
149
153
|
title={strings.browserSteps.title}
|
|
150
|
-
subtitle={slice?.label}
|
|
151
154
|
ctaLabel={strings.browserSteps.ctaLabel}
|
|
152
155
|
secondaryCtaLabel={showSkip ? strings.browserSteps.skipLabel : undefined}
|
|
153
156
|
verifiedLabel={strings.verifiedLabel}
|
|
@@ -123,6 +123,9 @@ export interface GeolocationBrowserSteps {
|
|
|
123
123
|
* rows and would render as one screen three times. Every browser therefore
|
|
124
124
|
* SHOULD carry a label, extending the precedent Figma itself sets on Safari.
|
|
125
125
|
* Still optional so a host that ships its own logo artwork can drop it.
|
|
126
|
+
*
|
|
127
|
+
* 2026-09-30 (Elena): the screen no longer RENDERS it as a subtitle. It is
|
|
128
|
+
* kept as the section's name for catalogs and hosts, not as on-screen copy.
|
|
126
129
|
*/
|
|
127
130
|
label?: string;
|
|
128
131
|
/** The steps, in order. */
|
package/src/IdCaptureLayout.css
CHANGED
|
@@ -59,6 +59,46 @@
|
|
|
59
59
|
gap: var(--prizma-spacing-8); /* Title → instruction, GUIDELINES §5 */
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/* ── Short screens: the vertical frame gives up height, never the title ──────
|
|
63
|
+
* The vertical ID (288:456) and passport (306:420) frames are tall. On a
|
|
64
|
+
* 320 × 693 phone the column is 272 wide, so the frame alone is ~431 tall and
|
|
65
|
+
* the stage ran 168px past the bottom, under the shutter strip (Camila,
|
|
66
|
+
* 2026-09-28). Here the frame is the ONE part of the column that shrinks: it
|
|
67
|
+
* starts at its full-width height (the column less its two Spacing/24
|
|
68
|
+
* gutters, over the aspect), and when the screen is too short the flex column
|
|
69
|
+
* takes the shortfall out of its height; the aspect ratio then narrows its
|
|
70
|
+
* width to match, so it stays an ID-shaped frame, centred. The title band
|
|
71
|
+
* keeps its content height. With room to spare (390 × 844) the frame never
|
|
72
|
+
* needs to shrink, so it sits exactly where it did.
|
|
73
|
+
*
|
|
74
|
+
* Scoped to ID + passport with `:not(--poa)`: Document Capture renders the
|
|
75
|
+
* same stage as `type="poa" isVertical`, so it carries `--vertical` too, but
|
|
76
|
+
* the PoA frame has NO aspect ratio (it fills the host's height). `width:
|
|
77
|
+
* auto` there collapsed it to a 0-wide sliver, a grey vertical bar down the
|
|
78
|
+
* camera screen (Elena, 2026-09-30). Only showed where this file and Document
|
|
79
|
+
* Capture's CSS ship in the same bundle, i.e. the production build. */
|
|
80
|
+
.prizma-viewfinder__content:has(> .prizma-id-capture--vertical:not(.prizma-id-capture--poa)) {
|
|
81
|
+
min-height: 0;
|
|
82
|
+
}
|
|
83
|
+
.prizma-viewfinder__content:has(> .prizma-id-capture--vertical:not(.prizma-id-capture--poa)) > .id-capture-band--top {
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
min-height: min-content;
|
|
86
|
+
}
|
|
87
|
+
.prizma-viewfinder__content > .prizma-id-capture--vertical:not(.prizma-id-capture--poa) {
|
|
88
|
+
flex: 0 1 auto;
|
|
89
|
+
min-height: 0;
|
|
90
|
+
}
|
|
91
|
+
.prizma-viewfinder__content > .prizma-id-capture--vertical:not(.prizma-id-capture--poa) .prizma-id-capture__frame {
|
|
92
|
+
flex: 0 1 auto;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
width: auto;
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 456 / 288);
|
|
97
|
+
}
|
|
98
|
+
.prizma-viewfinder__content > .prizma-id-capture--passport.prizma-id-capture--vertical .prizma-id-capture__frame {
|
|
99
|
+
flex-basis: calc((100cqw - 2 * var(--prizma-spacing-24)) * 420 / 306);
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
/* ── Camera stage (dark) ───────────────────────────────────────────────────── */
|
|
63
103
|
|
|
64
104
|
/* The stage's nav row carries only the close X (no logo — the camera owns the
|
package/src/ListScreen.tsx
CHANGED
|
@@ -77,6 +77,8 @@ export interface PrizmaListScreenProps
|
|
|
77
77
|
/** "verified by" attribution copy for the footer tag. */
|
|
78
78
|
verifiedLabel: string;
|
|
79
79
|
navLabel?: string;
|
|
80
|
+
/** Opt-in back chevron in the nav (its accessible name). Emits `back`. */
|
|
81
|
+
backLabel?: string;
|
|
80
82
|
screenId?: string;
|
|
81
83
|
overrides?: PrizmaScreenOverrides;
|
|
82
84
|
onAction?: PrizmaScreenActionHandler;
|
|
@@ -122,6 +124,7 @@ export const PrizmaListScreen = forwardRef<HTMLDivElement, PrizmaListScreenProps
|
|
|
122
124
|
linkLabel,
|
|
123
125
|
verifiedLabel,
|
|
124
126
|
navLabel,
|
|
127
|
+
backLabel,
|
|
125
128
|
screenId,
|
|
126
129
|
showHeader = true,
|
|
127
130
|
showFooter = true,
|
|
@@ -132,7 +135,13 @@ export const PrizmaListScreen = forwardRef<HTMLDivElement, PrizmaListScreenProps
|
|
|
132
135
|
ref,
|
|
133
136
|
) => (
|
|
134
137
|
<PrizmaScreenShell ref={ref} {...shell}>
|
|
135
|
-
<PrizmaScreenNav
|
|
138
|
+
<PrizmaScreenNav
|
|
139
|
+
reserved={!showHeader}
|
|
140
|
+
logo={overrides?.logo}
|
|
141
|
+
navLabel={navLabel}
|
|
142
|
+
backLabel={backLabel}
|
|
143
|
+
onBack={() => onAction?.({ type: 'back', screenId })}
|
|
144
|
+
/>
|
|
136
145
|
{progress ? (
|
|
137
146
|
<div className="prizma-screen-layout__progress">{progress}</div>
|
|
138
147
|
) : null}
|
package/src/PhoneInputLayout.css
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
align-items: flex-start;
|
|
22
22
|
gap: var(--prizma-spacing-8);
|
|
23
23
|
width: 100%;
|
|
24
|
+
position: relative; /* the country menu's containing block — see below */
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/* The dial-code dropdown hugs its content ("🇷🇸 +381"), as Figma draws it.
|
|
@@ -47,15 +48,15 @@
|
|
|
47
48
|
min-width: 0;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/* The open menu
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
/* The open menu spans the WHOLE row (dial code + number field), as Figma's
|
|
52
|
+
* `Input / Combined` draws it: Core anchors the menu overlay to the trigger's
|
|
53
|
+
* `__anchor` (`position: relative`, Dropdown.css), so once the trigger hugs
|
|
54
|
+
* "🇷🇸 +381" the menu came out as narrow as the dial code. Releasing the
|
|
55
|
+
* anchor's positioning hands the overlay's `left: 0; right: 0` to the row
|
|
56
|
+
* above (`position: relative`), so the menu matches the row's width exactly.
|
|
57
|
+
* Core's own overlay offset (`top: 100% + Spacing/8`) is left untouched.
|
|
55
58
|
* DEVIATIONS #PI-1 (same root cause as the hug).
|
|
56
59
|
*/
|
|
57
|
-
.phone-input-combined__country .prizma-
|
|
58
|
-
|
|
59
|
-
min-width: 100%;
|
|
60
|
-
max-width: 342px;
|
|
60
|
+
.phone-input-combined__country .prizma-dropdown__anchor {
|
|
61
|
+
position: static;
|
|
61
62
|
}
|
package/src/PhoneInputStrings.ts
CHANGED
|
@@ -20,13 +20,18 @@
|
|
|
20
20
|
* - `typing` — partial value, focused field
|
|
21
21
|
* - `filled` — complete value
|
|
22
22
|
* - `loading` — CTA in its loading treatment while the code is requested
|
|
23
|
+
* - `emptyError` — "Continue" pressed with no number typed: the phone field
|
|
24
|
+
* stays empty (no placeholder) in Core's error state with `requiredError`,
|
|
25
|
+
* and the CTA is disabled. The dial code always carries a selection, so it
|
|
26
|
+
* takes no error. GUIDELINES §6-E empty-submit rule (Elena, 2026-09-30).
|
|
23
27
|
*/
|
|
24
28
|
export type PhoneInputVerifyState =
|
|
25
29
|
| 'default'
|
|
26
30
|
| 'countryCode'
|
|
27
31
|
| 'typing'
|
|
28
32
|
| 'filled'
|
|
29
|
-
| 'loading'
|
|
33
|
+
| 'loading'
|
|
34
|
+
| 'emptyError';
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* The `phoneNumber.OTP.*` sections — same layout, different field state:
|
|
@@ -63,6 +68,12 @@ export interface PhoneInputStrings {
|
|
|
63
68
|
verifiedLabel: string;
|
|
64
69
|
/** Accessible name for the logo nav row. */
|
|
65
70
|
navLabel?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Error row under the required field when the primary CTA is pressed with it
|
|
73
|
+
* still empty (the verify screen's `emptyError` state): "This field is
|
|
74
|
+
* required". SDK key `dynamicForms.fieldRequired`.
|
|
75
|
+
*/
|
|
76
|
+
requiredError: string;
|
|
66
77
|
|
|
67
78
|
/** phoneNumber.verify.* */
|
|
68
79
|
verify: {
|
|
@@ -55,6 +55,14 @@ const DEFAULT_COUNTRIES: PhoneInputCountry[] = [
|
|
|
55
55
|
* primitives (`PrizmaDropdown`, `PrizmaTextField`) at the designed 56px min
|
|
56
56
|
* height and `Border/Radius/small`, so the module layer contributes only the
|
|
57
57
|
* row that pairs them (`PhoneInputLayout.css`) — no primitive is re-implemented.
|
|
58
|
+
*
|
|
59
|
+
* `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
|
|
60
|
+
* GUIDELINES §6-E empty-submit rule): "Continue" was pressed with no number
|
|
61
|
+
* typed. The phone field renders empty with no placeholder, in Core's error
|
|
62
|
+
* state with `requiredError`, and the primary CTA is disabled, following the
|
|
63
|
+
* drawn precedent in `FormsFieldScreen`. The dial code always carries a
|
|
64
|
+
* selection, so it takes no error. The resting `default` keeps the CTA enabled
|
|
65
|
+
* so the user can reach this state.
|
|
58
66
|
*/
|
|
59
67
|
export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerifyScreenProps>(
|
|
60
68
|
(
|
|
@@ -74,9 +82,11 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
|
|
|
74
82
|
const selected = country ?? countries[0]?.value;
|
|
75
83
|
const options = countries.map((c) => ({
|
|
76
84
|
value: c.value,
|
|
77
|
-
// The
|
|
78
|
-
// ("🇷🇸 +381"); the
|
|
79
|
-
|
|
85
|
+
// The open menu row reads flag + dial code + country name
|
|
86
|
+
// ("🇷🇸 +381 Serbia"); the closed field keeps only flag + dial code
|
|
87
|
+
// ("🇷🇸 +381") through Core's `fieldLabel`, exactly as Figma draws it.
|
|
88
|
+
label: [c.flag, c.dialCode, c.label].filter(Boolean).join(' '),
|
|
89
|
+
fieldLabel: [c.flag, c.dialCode].filter(Boolean).join(' '),
|
|
80
90
|
}));
|
|
81
91
|
|
|
82
92
|
// Figma treats the states as three distinct field treatments, not one:
|
|
@@ -88,6 +98,9 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
|
|
|
88
98
|
// for none of them — it would grey the resting states too (DEVIATIONS #PI-6).
|
|
89
99
|
const busy = state === 'loading';
|
|
90
100
|
const focused = state === 'typing';
|
|
101
|
+
// Empty submit: the number field drops its placeholder and takes Core's
|
|
102
|
+
// error state with the shared "required" row; the CTA greys.
|
|
103
|
+
const emptyError = state === 'emptyError';
|
|
91
104
|
|
|
92
105
|
return (
|
|
93
106
|
<PrizmaFormScreen
|
|
@@ -97,6 +110,7 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
|
|
|
97
110
|
ctaLabel={strings.verify.ctaLabel}
|
|
98
111
|
/* Figma's `loading` section swaps the CTA label for Core's spinner. */
|
|
99
112
|
ctaLoading={busy}
|
|
113
|
+
ctaDisabled={emptyError}
|
|
100
114
|
ctaLoadingLabel={strings.verify.ctaLoadingLabel}
|
|
101
115
|
verifiedLabel={strings.verifiedLabel}
|
|
102
116
|
navLabel={strings.navLabel}
|
|
@@ -134,12 +148,14 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
|
|
|
134
148
|
type="tel"
|
|
135
149
|
inputMode="tel"
|
|
136
150
|
aria-label={strings.verify.fieldLabel}
|
|
137
|
-
placeholder={strings.verify.placeholder}
|
|
151
|
+
placeholder={emptyError ? undefined : strings.verify.placeholder}
|
|
138
152
|
/* Uncontrolled: a screen is one static spec state, and `value`
|
|
139
153
|
without `onChange` would either warn or force `readOnly`, whose
|
|
140
154
|
disabled surface is exactly the grey Figma does NOT draw here. */
|
|
141
|
-
defaultValue={value ?? ''}
|
|
155
|
+
defaultValue={emptyError ? '' : value ?? ''}
|
|
142
156
|
disabled={busy}
|
|
157
|
+
error={emptyError}
|
|
158
|
+
errorText={emptyError ? strings.requiredError : undefined}
|
|
143
159
|
/* Figma draws the clear ✕ only while the field is focused. */
|
|
144
160
|
clearable={focused}
|
|
145
161
|
/>
|
|
@@ -118,6 +118,7 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
|
|
|
118
118
|
// `loading` makes the whole form inert.
|
|
119
119
|
const busy = state === 'loading';
|
|
120
120
|
const ticked = state !== 'default';
|
|
121
|
+
const requiredHint = strings.accept.requiredHint;
|
|
121
122
|
|
|
122
123
|
return (
|
|
123
124
|
<PrizmaFormScreen
|
|
@@ -177,6 +178,12 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
|
|
|
177
178
|
role="group"
|
|
178
179
|
aria-label={strings.accept.termsLabel}
|
|
179
180
|
>
|
|
181
|
+
{/* The legend LEADS the group (Elena, 2026-09-30, picked from four
|
|
182
|
+
placements): it sits where the markers are and, on the five-clause
|
|
183
|
+
reading that scrolls, is still on screen on arrival. */}
|
|
184
|
+
{requiredHint && strings.accept.requiredLegend ? (
|
|
185
|
+
<p className="qualified-signature-terms__legend">{strings.accept.requiredLegend}</p>
|
|
186
|
+
) : null}
|
|
180
187
|
{terms.map((term) => (
|
|
181
188
|
<PrizmaCheckbox
|
|
182
189
|
key={term.id}
|
|
@@ -185,7 +192,11 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
|
|
|
185
192
|
value={term.id}
|
|
186
193
|
checked={ticked}
|
|
187
194
|
disabled={busy}
|
|
188
|
-
|
|
195
|
+
required={Boolean(requiredHint)}
|
|
196
|
+
/* The `*` rides the END of the clause copy, the Data Sharing
|
|
197
|
+
Consent idiom — the L card paints no marker of its own beside
|
|
198
|
+
the box (owner, 2026-09-02). */
|
|
199
|
+
segments={requiredHint ? [...term.segments, ` ${requiredHint}`] : term.segments}
|
|
189
200
|
/>
|
|
190
201
|
))}
|
|
191
202
|
</div>
|