@incodetech/prizma-core-screens 0.2.3 โ†’ 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/CurpEnterScreen.d.ts +7 -0
  2. package/dist/CurpEnterScreen.js +17 -3
  3. package/dist/CurpGenerateScreen.d.ts +8 -0
  4. package/dist/CurpGenerateScreen.js +28 -13
  5. package/dist/CurpLayout.css +8 -34
  6. package/dist/CurpStrings.d.ts +16 -2
  7. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  8. package/dist/EkybAddressDetailsScreen.js +25 -7
  9. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  10. package/dist/EkybBusinessDetailsScreen.js +23 -6
  11. package/dist/EkybCountryScreen.d.ts +13 -5
  12. package/dist/EkybCountryScreen.js +27 -9
  13. package/dist/EkybOwnersScreen.d.ts +8 -0
  14. package/dist/EkybOwnersScreen.js +26 -3
  15. package/dist/EkybStrings.d.ts +27 -10
  16. package/dist/EkycCountryCatalog.d.ts +4 -1
  17. package/dist/EkycCountryCatalog.js +5 -3
  18. package/dist/EkycCountryFormScreen.d.ts +12 -3
  19. package/dist/EkycCountryFormScreen.js +28 -10
  20. package/dist/EkycDetailsScreen.d.ts +7 -0
  21. package/dist/EkycDetailsScreen.js +21 -4
  22. package/dist/EkycStrings.d.ts +14 -2
  23. package/dist/ElectronicSignatureLayout.css +6 -0
  24. package/dist/EmailInputStrings.d.ts +11 -1
  25. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  26. package/dist/EmailInputVerifyScreen.js +17 -3
  27. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  28. package/dist/FaceMatchLayout.css +21 -12
  29. package/dist/FormScreen.css +16 -0
  30. package/dist/FormsLongFormScreen.d.ts +8 -0
  31. package/dist/FormsLongFormScreen.js +34 -13
  32. package/dist/FormsStrings.d.ts +11 -1
  33. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  34. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  35. package/dist/GeolocationStrings.d.ts +3 -0
  36. package/dist/IdCaptureLayout.css +40 -0
  37. package/dist/ListScreen.d.ts +2 -0
  38. package/dist/ListScreen.js +1 -1
  39. package/dist/PhoneInputLayout.css +10 -9
  40. package/dist/PhoneInputStrings.d.ts +11 -1
  41. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  42. package/dist/PhoneInputVerifyScreen.js +19 -6
  43. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  44. package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
  45. package/dist/QualifiedSignatureDocumentScreen.js +18 -16
  46. package/dist/QualifiedSignatureLayout.css +76 -0
  47. package/dist/QualifiedSignatureStrings.d.ts +10 -0
  48. package/dist/ScreenNav.css +8 -0
  49. package/dist/ScreenNav.d.ts +15 -2
  50. package/dist/ScreenNav.js +10 -3
  51. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  52. package/dist/VideoSelfieIdCaptureScreen.js +44 -13
  53. package/dist/VideoSelfieLayout.css +164 -33
  54. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  55. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  56. package/dist/VideoSelfieSpeakScreen.js +18 -1
  57. package/dist/VideoSelfieStrings.d.ts +78 -12
  58. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  59. package/dist/VideoSelfieTurnHeadScreen.js +63 -0
  60. package/dist/VideoSelfieTutorialScreen.js +13 -5
  61. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  62. package/dist/VideoSelfieWaveScreen.js +62 -0
  63. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  64. package/dist/WatchlistBusinessFormScreen.js +27 -13
  65. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +2 -0
  68. package/dist/types.d.ts +3 -1
  69. package/package.json +14 -3
  70. package/samples/video-selfie-room.png +0 -0
  71. package/src/CurpEnterScreen.tsx +21 -4
  72. package/src/CurpGenerateScreen.tsx +46 -22
  73. package/src/CurpLayout.css +8 -34
  74. package/src/CurpStrings.ts +22 -2
  75. package/src/EkybAddressDetailsScreen.tsx +32 -4
  76. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  77. package/src/EkybCountryScreen.tsx +28 -7
  78. package/src/EkybOwnersScreen.tsx +34 -3
  79. package/src/EkybStrings.ts +29 -10
  80. package/src/EkycCountryCatalog.ts +9 -4
  81. package/src/EkycCountryFormScreen.tsx +51 -12
  82. package/src/EkycDetailsScreen.tsx +22 -1
  83. package/src/EkycStrings.ts +14 -2
  84. package/src/ElectronicSignatureLayout.css +6 -0
  85. package/src/EmailInputStrings.ts +12 -1
  86. package/src/EmailInputVerifyScreen.tsx +21 -4
  87. package/src/FaceMatchComparisonScreen.tsx +3 -3
  88. package/src/FaceMatchLayout.css +21 -12
  89. package/src/FormScreen.css +16 -0
  90. package/src/FormsLongFormScreen.tsx +41 -13
  91. package/src/FormsStrings.ts +12 -1
  92. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  93. package/src/GeolocationStrings.ts +3 -0
  94. package/src/IdCaptureLayout.css +40 -0
  95. package/src/ListScreen.tsx +10 -1
  96. package/src/PhoneInputLayout.css +10 -9
  97. package/src/PhoneInputStrings.ts +12 -1
  98. package/src/PhoneInputVerifyScreen.tsx +21 -5
  99. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  100. package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
  101. package/src/QualifiedSignatureLayout.css +76 -0
  102. package/src/QualifiedSignatureStrings.ts +10 -0
  103. package/src/ScreenNav.css +8 -0
  104. package/src/ScreenNav.tsx +35 -5
  105. package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
  106. package/src/VideoSelfieLayout.css +164 -33
  107. package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
  108. package/src/VideoSelfieSpeakScreen.tsx +25 -14
  109. package/src/VideoSelfieStrings.ts +85 -12
  110. package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
  111. package/src/VideoSelfieTutorialScreen.tsx +34 -23
  112. package/src/VideoSelfieWaveScreen.tsx +135 -0
  113. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  114. package/src/WatchlistBusinessStrings.ts +21 -7
  115. package/src/index.ts +14 -0
  116. package/src/types.ts +3 -1
@@ -46,6 +46,13 @@ export interface CurpEnterScreenProps extends Omit<PrizmaFormScreenProps, 'title
46
46
  * `linkContent` slot: no primitive is invented, no layout is declared, and the
47
47
  * two actions stay distinguishable (`cta` vs `secondary`, the #PI-10 rule).
48
48
  * The gap is reported for promotion โ€” see the module's DEVIATIONS rows.
49
+ *
50
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
51
+ * GUIDELINES ยง6-E empty-submit rule): "Continue" was pressed with nothing
52
+ * typed. The field renders empty with no placeholder, in Core's error state
53
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
54
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
55
+ * so the user can reach this state.
49
56
  */
50
57
  export declare const CurpEnterScreen: React.ForwardRefExoticComponent<CurpEnterScreenProps & React.RefAttributes<HTMLDivElement>>;
51
58
  export default CurpEnterScreen;
@@ -37,6 +37,13 @@ import './CurpLayout.css';
37
37
  * `linkContent` slot: no primitive is invented, no layout is declared, and the
38
38
  * two actions stay distinguishable (`cta` vs `secondary`, the #PI-10 rule).
39
39
  * The gap is reported for promotion โ€” see the module's DEVIATIONS rows.
40
+ *
41
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
42
+ * GUIDELINES ยง6-E empty-submit rule): "Continue" was pressed with nothing
43
+ * typed. The field renders empty with no placeholder, in Core's error state
44
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
45
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
46
+ * so the user can reach this state.
40
47
  */
41
48
  export const CurpEnterScreen = forwardRef(({ strings, state = 'default', value, screenId = `curp-enter-${state}`, className, overrides, onAction, ...shell }, ref) => {
42
49
  // Figma treats the five sections as four distinct field treatments:
@@ -47,6 +54,9 @@ export const CurpEnterScreen = forwardRef(({ strings, state = 'default', value,
47
54
  // surface.
48
55
  const busy = state === 'loading';
49
56
  const invalid = state === 'error';
57
+ // Empty submit: the field stays empty, drops its placeholder and takes
58
+ // Core's error state with the shared "required" row; the CTA greys.
59
+ const emptyError = state === 'emptyError';
50
60
  const focused = state === 'focused' || state === 'filled';
51
61
  return (_jsx(PrizmaFormScreen, { ref: ref,
52
62
  /* Scopes the one module rule that lets the `error` section keep the
@@ -63,11 +73,15 @@ export const CurpEnterScreen = forwardRef(({ strings, state = 'default', value,
63
73
  .filter(Boolean)
64
74
  .join(' '), title: strings.enter.title, subtitle: strings.enter.subtitle, ctaLabel: strings.enter.ctaLabel,
65
75
  /* The `loading` section swaps the CTA label for Core's spinner. */
66
- ctaLoading: busy, ctaLoadingLabel: strings.enter.ctaLoadingLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L",
76
+ ctaLoading: busy, ctaDisabled: emptyError, ctaLoadingLabel: strings.enter.ctaLoadingLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L",
67
77
  /* Figma greys this action too while the lookup runs. */
68
- disabled: busy, onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.enter.noCurpLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, "aria-label": strings.enter.fieldLabel, placeholder: strings.enter.placeholder, defaultValue: value ?? '',
78
+ disabled: busy, onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.enter.noCurpLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, "aria-label": strings.enter.fieldLabel, placeholder: emptyError ? undefined : strings.enter.placeholder, defaultValue: emptyError ? '' : value ?? '',
69
79
  /* A CURP is an 18-character uppercase key. */
70
- autoCapitalize: "characters", autoComplete: "off", spellCheck: false, disabled: busy, error: invalid, errorText: invalid ? strings.enter.errorText : undefined,
80
+ autoCapitalize: "characters", autoComplete: "off", spellCheck: false, disabled: busy, error: invalid || emptyError, errorText: invalid
81
+ ? strings.enter.errorText
82
+ : emptyError
83
+ ? strings.requiredError
84
+ : undefined,
71
85
  /* The clear โœ• is drawn only on the error section (76:36434); the
72
86
  `filled` section (79:1553) draws none. Core still owns WHEN it
73
87
  shows โ€” its `--show` state needs a value in the field. */
@@ -42,6 +42,14 @@ export interface CurpGenerateScreenProps extends Omit<PrizmaFormScreenProps, 'ti
42
42
  * The `dropdown` section IS the open-menu spec state, which Core exposes as the
43
43
  * documentation-only `forceOpen` (a screen is one static state, never a live
44
44
  * interaction) โ€” the same mechanism Phone Input's `countryCode` section uses.
45
+ *
46
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
47
+ * GUIDELINES ยง6-E empty-submit rule): "Generate" was pressed with the form
48
+ * empty. All six fields carry the required `*`, so all six render empty with no
49
+ * placeholder, in Core's error state with `requiredError`; no menu is open and
50
+ * the primary CTA is disabled, following the drawn precedent in
51
+ * `FormsFieldScreen`. The resting `default` keeps the CTA enabled so the user
52
+ * can reach this state.
45
53
  */
46
54
  export declare const CurpGenerateScreen: React.ForwardRefExoticComponent<CurpGenerateScreenProps & React.RefAttributes<HTMLDivElement>>;
47
55
  export default CurpGenerateScreen;
@@ -29,29 +29,44 @@ import './CurpLayout.css';
29
29
  * The `dropdown` section IS the open-menu spec state, which Core exposes as the
30
30
  * documentation-only `forceOpen` (a screen is one static state, never a live
31
31
  * interaction) โ€” the same mechanism Phone Input's `countryCode` section uses.
32
+ *
33
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
34
+ * GUIDELINES ยง6-E empty-submit rule): "Generate" was pressed with the form
35
+ * empty. All six fields carry the required `*`, so all six render empty with no
36
+ * placeholder, in Core's error state with `requiredError`; no menu is open and
37
+ * the primary CTA is disabled, following the drawn precedent in
38
+ * `FormsFieldScreen`. The resting `default` keeps the CTA enabled so the user
39
+ * can reach this state.
32
40
  */
33
41
  export const CurpGenerateScreen = forwardRef(({ strings, state = 'default', values, genders = [], states = [], screenId = `curp-generate-${state}`, className, overrides, onAction, ...shell }, ref) => {
34
42
  const copy = strings.generate;
43
+ // Empty submit: every required field drops its value and placeholder and
44
+ // takes Core's error state with the shared "required" row; the CTA greys.
45
+ const emptyError = state === 'emptyError';
46
+ const ph = (placeholder) => (emptyError ? undefined : placeholder);
47
+ const err = {
48
+ error: emptyError,
49
+ errorText: emptyError ? strings.requiredError : undefined,
50
+ };
35
51
  return (_jsxs(PrizmaFormScreen, { ref: ref,
36
- /* Scopes the two module rules in CurpLayout.css (see the foundation and
37
- Core requests recorded there):
38
- - `--scroll` lets the six-field stack scroll inside the 844 frame
39
- instead of overflowing its box and painting over the CTA. EVERY
40
- section gets it, `dropdown` included โ€” Figma draws "Generate"
41
- clear and unobstructed under the open menu.
42
- - `--menu-open` flips Core's absolutely-positioned menu above its
43
- trigger on the one section that forces it open, so the three
44
- gender rows stay inside the scrolling column. */
45
- className: [
52
+ /* The six-field stack scrolls inside the 844 frame through the
53
+ foundation's `scrollFields` (overflow + the Spacing/16 that keeps the
54
+ cut clear of "Generate"). EVERY section gets it, `dropdown` included:
55
+ Figma draws "Generate" clear and unobstructed under the open menu.
56
+ It replaces the module's old `--scroll` rule, which scrolled but
57
+ carried no spacing, so the last field ran flush into the button.
58
+ `--menu-open` (CurpLayout.css) flips Core's absolutely-positioned
59
+ menu above its trigger on the one section that forces it open, so
60
+ the three gender rows stay inside the scrolling column. */
61
+ scrollFields: true, className: [
46
62
  'curp-generate',
47
- 'curp-generate--scroll',
48
63
  state === 'dropdown' ? 'curp-generate--menu-open' : '',
49
64
  className,
50
65
  ]
51
66
  .filter(Boolean)
52
- .join(' '), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, ctaLoadingLabel: copy.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: [_jsx(PrizmaTextField, { label: copy.firstName.label, placeholder: copy.firstName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.firstName ?? '' }), _jsx(PrizmaTextField, { label: copy.firstLastName.label, placeholder: copy.firstLastName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.firstLastName ?? '' }), _jsx(PrizmaTextField, { label: copy.secondLastName.label, placeholder: copy.secondLastName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondLastName ?? '' }), _jsx(PrizmaDropdown, { label: copy.gender.label, placeholder: copy.gender.placeholder, required: true, requiredHint: strings.requiredHint, options: genders, defaultValue: values?.gender ?? null, searchPlaceholder: copy.gender.searchPlaceholder,
67
+ .join(' '), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, ctaDisabled: emptyError, ctaLoadingLabel: copy.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: [_jsx(PrizmaTextField, { label: copy.firstName.label, placeholder: ph(copy.firstName.placeholder), required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: emptyError ? '' : values?.firstName ?? '', ...err }), _jsx(PrizmaTextField, { label: copy.firstLastName.label, placeholder: ph(copy.firstLastName.placeholder), required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: emptyError ? '' : values?.firstLastName ?? '', ...err }), _jsx(PrizmaTextField, { label: copy.secondLastName.label, placeholder: ph(copy.secondLastName.placeholder), required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: emptyError ? '' : values?.secondLastName ?? '', ...err }), _jsx(PrizmaDropdown, { label: copy.gender.label, placeholder: ph(copy.gender.placeholder), required: true, requiredHint: strings.requiredHint, options: genders, defaultValue: emptyError ? null : values?.gender ?? null, ...err, searchPlaceholder: copy.gender.searchPlaceholder,
53
68
  /* The `dropdown` section is the open Gender menu. */
54
- forceOpen: state === 'dropdown' }), _jsx(PrizmaDateField, { label: copy.dateOfBirth.label, placeholder: copy.dateOfBirth.placeholder, required: true, requiredHint: strings.requiredHint, calendarLabel: copy.dateOfBirth.calendarLabel, defaultValue: values?.dateOfBirth ?? '' }), _jsx(PrizmaDropdown, { label: copy.birthState.label, placeholder: copy.birthState.placeholder, required: true, requiredHint: strings.requiredHint, options: states, defaultValue: values?.birthState ?? null, searchable: true, searchPlaceholder: copy.birthState.searchPlaceholder })] }));
69
+ forceOpen: state === 'dropdown' }), _jsx(PrizmaDateField, { label: copy.dateOfBirth.label, placeholder: ph(copy.dateOfBirth.placeholder), required: true, requiredHint: strings.requiredHint, calendarLabel: copy.dateOfBirth.calendarLabel, defaultValue: emptyError ? '' : values?.dateOfBirth ?? '', ...err }), _jsx(PrizmaDropdown, { label: copy.birthState.label, placeholder: ph(copy.birthState.placeholder), required: true, requiredHint: strings.requiredHint, options: states, defaultValue: emptyError ? null : values?.birthState ?? null, ...err, searchable: true, searchPlaceholder: copy.birthState.searchPlaceholder })] }));
55
70
  });
56
71
  CurpGenerateScreen.displayName = 'CurpGenerateScreen';
57
72
  export default CurpGenerateScreen;
@@ -1,6 +1,6 @@
1
1
  /* CURP โ€” module layout additions.
2
2
  *
3
- * THREE rules, and none of them declares spacing: the content column, the 24px
3
+ * TWO rules, and none of them declares spacing: the content column, the 24px
4
4
  * gutters, the title โ†’ fields Spacing/32, the field gaps, the CTA column and
5
5
  * the anchored footer all stay the foundation's (`ScreenLayout.css`,
6
6
  * `FormScreen.css`). Nothing here re-declares an archetype value, invents a
@@ -10,40 +10,14 @@
10
10
  */
11
11
 
12
12
  /* โ”€โ”€ The generator's field stack is taller than the screen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
- * `CURP.Gene.default` is a 1051-tall frame: its `input-list` alone is 548 tall
14
- * (six 78px labelled fields at Spacing/16) and the CONTENT frame grows to 861
15
- * instead of the usual 654. At the 390ร—844 reference the column has ~470 for
16
- * it, so the last dropdown and the CTA fall past the fold.
17
- *
18
- * Archetype E's `.prizma-form-screen__fields` is `flex: 1; min-height: 0` with
19
- * no overflow behaviour โ€” it was measured against Phone Input, whose forms are
20
- * one or two fields โ€” so the surplus is silently clipped by `ScreenShell`'s
21
- * `overflow: hidden`, and (because the fields cannot shrink below
22
- * `--prizma-input-size-field-height`) it first PAINTS OVER the sibling CTA.
23
- * Screens are authored at 390ร—844 and never reflow (README / DEVIATIONS #17),
24
- * so growing the frame is not an option either.
25
- *
26
- * The stack is therefore allowed to scroll INSIDE the column: the title, the
27
- * CTA and the anchored tag stay put and the six fields pan, which is the
28
- * closest a fixed 844 frame gets to Figma's taller page. It adds NO padding, NO
29
- * gap and NO size of its own.
30
- *
31
- * Applied to EVERY `CURP.Gene.*` section including `dropdown`. The earlier
32
- * build withheld it from `dropdown` to keep Core's absolutely-positioned menu
33
- * from being clipped, and that traded one clipped element for a worse one: with
34
- * `overflow: visible` the 548-tall stack overflowed its box and painted the
35
- * open menu straight across the "Generate" button, which Figma (2058:16373)
36
- * draws clear and unobstructed under the menu. The CTA is never allowed to be
37
- * overlapped; the menu is handled by the next rule instead.
38
- *
39
- * FOUNDATION REQUEST: promote this to `FormScreen.css` as a
40
- * `.prizma-form-screen__fields--scroll` variant (or make overflow the default),
41
- * driven by a `scrollFields` prop on `PrizmaFormScreen`. Every long-form module
42
- * still to come in Wave 3 โ€” eKYC, eKYB, address entry โ€” will hit the same wall.
13
+ * `CURP.Gene.default` is a 1051-tall frame (six 78px labelled fields at
14
+ * Spacing/16), so at 390ร—844 the stack scrolls inside the column. That is the
15
+ * foundation's `scrollFields` on `PrizmaFormScreen` now (FormScreen.css
16
+ * `__fields--scroll`), which also carries the Spacing/16 between the cut and
17
+ * the CTA. The module used to hand-roll `overflow-y: auto` here with no
18
+ * spacing, so the last field ran flush into "Generate" (Elena, 2026-09-30).
19
+ * Removed in favour of the foundation rule.
43
20
  */
44
- .curp-generate--scroll .prizma-form-screen__fields {
45
- overflow-y: auto;
46
- }
47
21
 
48
22
  /* โ”€โ”€ The open Gender menu must stay inside the scrolling column โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
49
23
  * `CURP.Gene.dropdown` exists to show the three gender rows, so they may not be
@@ -11,15 +11,23 @@
11
11
  * - `error` โ€” negative field + clear โœ• + the "Invalid CURP" row
12
12
  * - `loading` โ€” both actions inert while the lookup runs; the spinner rides the
13
13
  * CTA, never the field (the Phone Input lesson, DEVIATIONS #PI-5)
14
+ * - `emptyError` โ€” "Continue" pressed with the field still empty: the field
15
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
16
+ * and the CTA is disabled. GUIDELINES ยง6-E empty-submit rule (Elena,
17
+ * 2026-09-30).
14
18
  */
15
- export type CurpEnterState = 'default' | 'focused' | 'filled' | 'error' | 'loading';
19
+ export type CurpEnterState = 'default' | 'focused' | 'filled' | 'error' | 'loading' | 'emptyError';
16
20
  /**
17
21
  * The `CURP.Gene.*` form sections โ€” the six-field generator:
18
22
  * - `default` โ€” every field empty on its placeholder
19
23
  * - `dropdown` โ€” the Gender menu open over the form (Core's `forceOpen`)
20
24
  * - `filled` โ€” every field carrying a value
25
+ * - `emptyError` โ€” "Generate" pressed with the form still empty: every required
26
+ * field stays empty (no placeholder) in Core's error state with
27
+ * `requiredError`, no menu is open, and the CTA is disabled. GUIDELINES ยง6-E
28
+ * empty-submit rule (Elena, 2026-09-30).
21
29
  */
22
- export type CurpGenerateState = 'default' | 'dropdown' | 'filled';
30
+ export type CurpGenerateState = 'default' | 'dropdown' | 'filled' | 'emptyError';
23
31
  /**
24
32
  * The two spinner sections. `verifying` is `CURP.EnterCURP.veryfing` (the
25
33
  * Figma layer name carries the typo); `generating` is `CURP.Gene.generating`.
@@ -73,6 +81,12 @@ export interface CurpStrings {
73
81
  * the marker is localizable and never hardcoded in a screen.
74
82
  */
75
83
  requiredHint?: string;
84
+ /**
85
+ * Error row under every required field left empty when the primary CTA is
86
+ * pressed (the `emptyError` state of the entry and generator forms):
87
+ * "This field is required". SDK key `dynamicForms.fieldRequired`.
88
+ */
89
+ requiredError: string;
76
90
  /** CURP.EnterCURP.{default,focused,filled,error,loading} */
77
91
  enter: {
78
92
  /** H2 ("Enter your CURP"). */
@@ -56,6 +56,13 @@ export interface EkybAddressDetailsScreenProps extends Omit<PrizmaFormScreenProp
56
56
  * segment 2 solid, segment 3 grey) is Core's `PrizmaStepper` at
57
57
  * `currentState={2}`, composed into the archetype's `progress` slot. This is
58
58
  * step 2 of 3.
59
+ *
60
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
61
+ * 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
62
+ * `emptyError`): every field renders empty, the five REQUIRED ones (State
63
+ * included) in Core's `error` state with `requiredError` and no placeholder,
64
+ * Address line 2 unflagged (optional, DEVIATIONS #EKC-6's rule), and Continue
65
+ * disabled. `empty` keeps Continue enabled so the press can happen.
59
66
  */
60
67
  export declare const EkybAddressDetailsScreen: React.ForwardRefExoticComponent<EkybAddressDetailsScreenProps & React.RefAttributes<HTMLDivElement>>;
61
68
  export default EkybAddressDetailsScreen;
@@ -46,22 +46,40 @@ import './ScreenLayout.css';
46
46
  * segment 2 solid, segment 3 grey) is Core's `PrizmaStepper` at
47
47
  * `currentState={2}`, composed into the archetype's `progress` slot. This is
48
48
  * step 2 of 3.
49
+ *
50
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
51
+ * 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
52
+ * `emptyError`): every field renders empty, the five REQUIRED ones (State
53
+ * included) in Core's `error` state with `requiredError` and no placeholder,
54
+ * Address line 2 unflagged (optional, DEVIATIONS #EKC-6's rule), and Continue
55
+ * disabled. `empty` keeps Continue enabled so the press can happen.
49
56
  */
50
- export const EkybAddressDetailsScreen = forwardRef(({ strings, state = 'empty', values, states = [], screenId = `ekyb-address-${state}`, className, overrides, onAction, ...shell }, ref) => {
57
+ export const EkybAddressDetailsScreen = forwardRef(({ strings, state = 'empty', values: valuesProp, states = [], screenId = `ekyb-address-${state}`, ctaDisabled, className, overrides, onAction, ...shell }, ref) => {
51
58
  const copy = strings.address;
59
+ const emptyError = state === 'emptyError';
60
+ const values = emptyError ? undefined : valuesProp;
61
+ // Core's own error treatment, spread AFTER each required field's own
62
+ // placeholder so the error state shows none (the `FormsFieldScreen`
63
+ // precedent). Address line 2 never receives it.
64
+ const missing = emptyError
65
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
66
+ : {};
52
67
  return (_jsxs(PrizmaFormScreen, { ref: ref, className: ['ekyb-address', className].filter(Boolean).join(' '),
53
68
  /* The archetype's own scrolling field stack โ€” the six-field column is
54
69
  548 tall against a middle that cannot be. No module CSS. */
55
70
  scrollFields: true,
56
71
  /* Step 2 of 3. `showIcons={false}`: the eKYB bar is a bare 5px rail. */
57
- progress: _jsx(PrizmaStepper, { steps: 3, currentState: 2, showIcons: false, "aria-label": strings.stepperLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
58
- /* Exploration ekyb-desktop: on a wide frame only State + Postal code
59
- share a row (Figma 6407:43637); the four text fields above span the
60
- column. On the phone nothing changes. */
61
- responsive: true, ...shell, children: [_jsx(PrizmaTextField, { label: copy.street.label, placeholder: copy.street.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.street ?? '' }), _jsx(PrizmaTextField, { label: copy.houseNumber.label, placeholder: copy.houseNumber.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.houseNumber ?? '' }), _jsx(PrizmaTextField, { label: copy.addressLine2.label, placeholder: copy.addressLine2.placeholder, autoComplete: "off", defaultValue: values?.addressLine2 ?? '' }), _jsx(PrizmaTextField, { label: copy.city.label, placeholder: copy.city.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.city ?? '' }), _jsxs(PrizmaFormRow, { children: [_jsx(PrizmaDropdown, { label: copy.state.label, placeholder: copy.state.placeholder, required: true, requiredHint: strings.requiredHint, options: states,
72
+ progress: _jsx(PrizmaStepper, { steps: 3, currentState: 2, showIcons: false, "aria-label": strings.stepperLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
73
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
74
+ ctaDisabled: emptyError || ctaDisabled, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
75
+ /* Exploration ekyb-desktop: on a wide frame the fields pair up in
76
+ three rows, Street + House number, Address line 2 + City (Elena,
77
+ 2026-09-28), and State + Postal code (Figma 6407:43637). On the
78
+ phone nothing changes. */
79
+ responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.street.label, placeholder: copy.street.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.street ?? '' }), _jsx(PrizmaTextField, { label: copy.houseNumber.label, placeholder: copy.houseNumber.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.houseNumber ?? '' })] }), _jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.addressLine2.label, placeholder: copy.addressLine2.placeholder, autoComplete: "off", defaultValue: values?.addressLine2 ?? '' }), _jsx(PrizmaTextField, { label: copy.city.label, placeholder: copy.city.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.city ?? '' })] }), _jsxs(PrizmaFormRow, { children: [_jsx(PrizmaDropdown, { label: copy.state.label, placeholder: copy.state.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, options: states,
62
80
  /* Both sections draw the placeholder unless the host supplies a
63
81
  value โ€” see the docstring note on 6340:38197. */
64
- defaultValue: values?.state ?? null }), _jsx(PrizmaTextField, { label: copy.postalCode.label, placeholder: copy.postalCode.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.postalCode ?? '' })] })] }));
82
+ defaultValue: values?.state ?? null }), _jsx(PrizmaTextField, { label: copy.postalCode.label, placeholder: copy.postalCode.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.postalCode ?? '' })] })] }));
65
83
  });
66
84
  EkybAddressDetailsScreen.displayName = 'EkybAddressDetailsScreen';
67
85
  export default EkybAddressDetailsScreen;
@@ -50,6 +50,12 @@ export interface EkybBusinessDetailsScreenProps extends Omit<PrizmaFormScreenPro
50
50
  * bar is at `currentState="Start"`: segment 1 filled, segments 2 and 3 grey,
51
51
  * exactly what 5919:68599 renders. Both offsets around it live in
52
52
  * `ScreenLayout.css`, never here.
53
+ *
54
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
55
+ * 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
56
+ * `emptyError`): both REQUIRED fields render empty in Core's `error` state with
57
+ * `requiredError` under them, and Continue is disabled. `empty` keeps Continue
58
+ * enabled so the press can happen.
53
59
  */
54
60
  export declare const EkybBusinessDetailsScreen: React.ForwardRefExoticComponent<EkybBusinessDetailsScreenProps & React.RefAttributes<HTMLDivElement>>;
55
61
  export default EkybBusinessDetailsScreen;
@@ -4,7 +4,7 @@ import { forwardRef } from 'react';
4
4
  import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
5
5
  import { PrizmaButton } from '@incodetech/prizma-core-web/button';
6
6
  import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
7
- import { PrizmaFormScreen, PrizmaFormRow } from './FormScreen';
7
+ import { PrizmaFormScreen } from './FormScreen';
8
8
  import './ScreenLayout.css';
9
9
  /**
10
10
  * eKYB โ€” step 1 of the wizard, the company's identity, built on the Form
@@ -42,19 +42,36 @@ import './ScreenLayout.css';
42
42
  * bar is at `currentState="Start"`: segment 1 filled, segments 2 and 3 grey,
43
43
  * exactly what 5919:68599 renders. Both offsets around it live in
44
44
  * `ScreenLayout.css`, never here.
45
+ *
46
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
47
+ * 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
48
+ * `emptyError`): both REQUIRED fields render empty in Core's `error` state with
49
+ * `requiredError` under them, and Continue is disabled. `empty` keeps Continue
50
+ * enabled so the press can happen.
45
51
  */
46
- export const EkybBusinessDetailsScreen = forwardRef(({ strings, state = 'empty', values, screenId = `ekyb-business-${state}`, overrides, onAction, ...shell }, ref) => {
52
+ export const EkybBusinessDetailsScreen = forwardRef(({ strings, state = 'empty', values: valuesProp, screenId = `ekyb-business-${state}`, ctaDisabled, overrides, onAction, ...shell }, ref) => {
47
53
  const copy = strings.business;
48
- return (_jsx(PrizmaFormScreen, { ref: ref,
54
+ const emptyError = state === 'emptyError';
55
+ const values = emptyError ? undefined : valuesProp;
56
+ // Core's own error treatment, spread onto the REQUIRED fields only; the
57
+ // error state shows no placeholder (the `FormsFieldScreen` precedent).
58
+ const missing = emptyError
59
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
60
+ : {};
61
+ return (_jsxs(PrizmaFormScreen, { ref: ref,
49
62
  /* Step 1 of 3. `showIcons={false}` because the eKYB frames draw the bar
50
63
  as a bare 5px rail with no glyph row. */
51
- progress: _jsx(PrizmaStepper, { steps: 3, currentState: "Start", showIcons: false, "aria-label": strings.stepperLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
64
+ progress: _jsx(PrizmaStepper, { steps: 3, currentState: "Start", showIcons: false, "aria-label": strings.stepperLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
65
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
66
+ ctaDisabled: emptyError || ctaDisabled, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
52
67
  /* Left-aligned title block, the alignment every eKYB frame draws; a host
53
68
  `align` still wins (#GEO-2). */
54
69
  overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
55
70
  /* Exploration ekyb-desktop: on a tablet or desktop frame the two
56
- fields share a row; on the phone nothing changes. */
57
- responsive: true, ...shell, children: _jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.businessName.label, placeholder: copy.businessName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.businessName ?? '' }), _jsx(PrizmaTextField, { label: copy.taxId.label, placeholder: copy.taxId.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.taxId ?? '' })] }) }));
71
+ fields stay on two rows, each spanning the column (Elena,
72
+ 2026-09-28); `responsive` still pairs Continue and Back. On the
73
+ phone nothing changes. */
74
+ responsive: true, ...shell, children: [_jsx(PrizmaTextField, { label: copy.businessName.label, placeholder: copy.businessName.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.businessName ?? '' }), _jsx(PrizmaTextField, { label: copy.taxId.label, placeholder: copy.taxId.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.taxId ?? '' })] }));
58
75
  });
59
76
  EkybBusinessDetailsScreen.displayName = 'EkybBusinessDetailsScreen';
60
77
  export default EkybBusinessDetailsScreen;
@@ -39,14 +39,22 @@ export interface EkybCountryScreenProps extends Omit<PrizmaFormScreenProps, 'tit
39
39
  * live interaction) โ€” the same mechanism Phone Input's `countryCode` and
40
40
  * CURP's `Gene.dropdown` sections use. The search bar Figma draws inside the
41
41
  * menu is Core's own `searchable`.
42
- * 3. The closed field and the menu row are DIFFERENT faces of one option:
43
- * 5707:54174 draws "๐Ÿ‡บ๐Ÿ‡ธ United States" in the field while the rows read
44
- * "๐Ÿ‡บ๐Ÿ‡ธ +1 United States". That is Core's `PrizmaDropdownOption.fieldLabel`,
45
- * so nothing is hand-composed. The flag travels inside the label text
46
- * because Core still ships no country-flag primitive (#PI-3).
42
+ * 3. Menu rows and the closed field read the same "๐Ÿ‡บ๐Ÿ‡ธ United States": flag
43
+ * and country name, no dial code. Figma 5707:54174 draws the rows with
44
+ * Phone Input's "+1" prefixes, dropped by the owner (Elena, 2026-09-28)
45
+ * because a country of registration has no phone prefix. The flag travels
46
+ * inside the label text because Core still ships no country-flag primitive
47
+ * (#PI-3).
47
48
  *
48
49
  * This page is the only eKYB form page with NO "Back" secondary and NO Stepper โ€”
49
50
  * it is step 0, before the three-step wizard starts.
51
+ *
52
+ * `emptyError` is Continue pressed with no country chosen (Elena, 2026-09-30,
53
+ * GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
54
+ * `emptyError`): the dropdown renders empty, with no placeholder, in Core's
55
+ * `error` state with `requiredError` under it, and Continue is disabled. The
56
+ * field draws no `*`, but it is the page's only input and the flow cannot go on
57
+ * without it, so it is treated as required. `empty` keeps Continue enabled.
50
58
  */
51
59
  export declare const EkybCountryScreen: React.ForwardRefExoticComponent<EkybCountryScreenProps & React.RefAttributes<HTMLDivElement>>;
52
60
  export default EkybCountryScreen;
@@ -26,26 +26,44 @@ import './ScreenLayout.css';
26
26
  * live interaction) โ€” the same mechanism Phone Input's `countryCode` and
27
27
  * CURP's `Gene.dropdown` sections use. The search bar Figma draws inside the
28
28
  * menu is Core's own `searchable`.
29
- * 3. The closed field and the menu row are DIFFERENT faces of one option:
30
- * 5707:54174 draws "๐Ÿ‡บ๐Ÿ‡ธ United States" in the field while the rows read
31
- * "๐Ÿ‡บ๐Ÿ‡ธ +1 United States". That is Core's `PrizmaDropdownOption.fieldLabel`,
32
- * so nothing is hand-composed. The flag travels inside the label text
33
- * because Core still ships no country-flag primitive (#PI-3).
29
+ * 3. Menu rows and the closed field read the same "๐Ÿ‡บ๐Ÿ‡ธ United States": flag
30
+ * and country name, no dial code. Figma 5707:54174 draws the rows with
31
+ * Phone Input's "+1" prefixes, dropped by the owner (Elena, 2026-09-28)
32
+ * because a country of registration has no phone prefix. The flag travels
33
+ * inside the label text because Core still ships no country-flag primitive
34
+ * (#PI-3).
34
35
  *
35
36
  * This page is the only eKYB form page with NO "Back" secondary and NO Stepper โ€”
36
37
  * it is step 0, before the three-step wizard starts.
38
+ *
39
+ * `emptyError` is Continue pressed with no country chosen (Elena, 2026-09-30,
40
+ * GUIDELINES ยง6-E empty-submit rule; precedent `FormsFieldScreen`'s
41
+ * `emptyError`): the dropdown renders empty, with no placeholder, in Core's
42
+ * `error` state with `requiredError` under it, and Continue is disabled. The
43
+ * field draws no `*`, but it is the page's only input and the flow cannot go on
44
+ * without it, so it is treated as required. `empty` keeps Continue enabled.
37
45
  */
38
- export const EkybCountryScreen = forwardRef(({ strings, state = 'empty', countries = [], value, screenId = `ekyb-country-${state}`, overrides, onAction, ...shell }, ref) => {
46
+ export const EkybCountryScreen = forwardRef(({ strings, state = 'empty', countries = [], value, screenId = `ekyb-country-${state}`, ctaDisabled, overrides, onAction, ...shell }, ref) => {
39
47
  const copy = strings.country;
40
48
  // `empty` is the only section drawn on the placeholder; both other sections
41
49
  // draw a chosen country in the closed field.
42
- const selected = state === 'empty' ? null : (value ?? countries[0]?.value ?? null);
43
- return (_jsx(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
50
+ const emptyError = state === 'emptyError';
51
+ const selected = state === 'empty' || emptyError ? null : (value ?? countries[0]?.value ?? null);
52
+ return (_jsx(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
53
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
54
+ ctaDisabled: emptyError || ctaDisabled, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
44
55
  /* Every eKYB form frame LEFT-aligns its title block, while the archetype
45
56
  defaults to centred. The module supplies its designed alignment
46
57
  through the sanctioned override rather than declaring any layout of
47
58
  its own, and a host that passes `align` still wins (#GEO-2). */
48
- overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction, ...shell, children: _jsx(PrizmaDropdown, { label: copy.field.label, placeholder: copy.field.placeholder, options: countries,
59
+ overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction, ...shell, children: _jsx(PrizmaDropdown, { label: copy.field.label,
60
+ /* Required, but UNMARKED: a single-input screen shows no asterisk
61
+ and no legend (Core pattern "Optimistic submission", single-input
62
+ screens; Elena 2026-09-30). The requirement surfaces on submit,
63
+ through the empty-submit error. */
64
+ /* The error state draws the field with nothing in it, not even the
65
+ placeholder (the `FormsFieldScreen` precedent). */
66
+ placeholder: emptyError ? undefined : copy.field.placeholder, error: emptyError, errorText: emptyError ? strings.requiredError : undefined, options: countries,
49
67
  /* Uncontrolled: a screen is one static spec state, so no `onChange` is
50
68
  needed. Core's `readOnly`/`disabled` are NOT used to freeze it โ€”
51
69
  they paint the DISABLED grey surface on a section Figma draws white
@@ -38,6 +38,14 @@ export interface EkybOwnersScreenProps extends Omit<PrizmaFormScreenProps, 'titl
38
38
  * - `loading` 5663:94390 โ€” `directorFilled` with the submission in
39
39
  * flight: every field, both "Add another" links and "Back" go inert, and the
40
40
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
41
+ * - `uboEmptyError` / `directorEmptyError` (no frame) โ€” `uboEmpty` /
42
+ * `directorEmpty` after Continue was pressed with the fields still empty
43
+ * (Elena, 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent
44
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the two
45
+ * REQUIRED UBO fields in Core's `error` state with `requiredError` and no
46
+ * placeholder, the optional Director fields unflagged, and Continue
47
+ * disabled. The `*Empty` states keep Continue enabled so the press can
48
+ * happen.
41
49
  *
42
50
  * Four things are deliberate:
43
51
  *
@@ -30,6 +30,14 @@ import './ScreenLayout.css';
30
30
  * - `loading` 5663:94390 โ€” `directorFilled` with the submission in
31
31
  * flight: every field, both "Add another" links and "Back" go inert, and the
32
32
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
33
+ * - `uboEmptyError` / `directorEmptyError` (no frame) โ€” `uboEmpty` /
34
+ * `directorEmpty` after Continue was pressed with the fields still empty
35
+ * (Elena, 2026-09-30, GUIDELINES ยง6-E empty-submit rule; precedent
36
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the two
37
+ * REQUIRED UBO fields in Core's `error` state with `requiredError` and no
38
+ * placeholder, the optional Director fields unflagged, and Continue
39
+ * disabled. The `*Empty` states keep Continue enabled so the press can
40
+ * happen.
33
41
  *
34
42
  * Four things are deliberate:
35
43
  *
@@ -65,24 +73,39 @@ import './ScreenLayout.css';
65
73
  * distinguishes `loading` from `directorFilled` above the fold, so it is not
66
74
  * optional chrome.
67
75
  */
68
- export const EkybOwnersScreen = forwardRef(({ strings, state = 'uboEmpty', values, screenId = `ekyb-owners-${state}`, className, overrides, onAction, ...shell }, ref) => {
76
+ export const EkybOwnersScreen = forwardRef(({ strings, state = 'uboEmpty', values: valuesProp, screenId = `ekyb-owners-${state}`, ctaDisabled, className, overrides, onAction, ...shell }, ref) => {
69
77
  const copy = strings.owners;
70
78
  const busy = state === 'loading';
71
79
  const secondUbo = state === 'uboAdded';
72
- const showDirector = state === 'directorEmpty' || state === 'directorFilled' || secondUbo || busy;
80
+ const emptyError = state === 'uboEmptyError' || state === 'directorEmptyError';
81
+ const values = emptyError ? undefined : valuesProp;
82
+ const showDirector = state === 'directorEmpty' ||
83
+ state === 'directorFilled' ||
84
+ state === 'directorEmptyError' ||
85
+ secondUbo ||
86
+ busy;
87
+ // Core's own error treatment, spread AFTER each required UBO field's own
88
+ // placeholder so the error state shows none (the `FormsFieldScreen`
89
+ // precedent). The Director fields are optional and never receive it.
90
+ const missing = emptyError
91
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
92
+ : {};
73
93
  return (_jsxs(PrizmaFormScreen, { ref: ref, className: ['ekyb-owners', className].filter(Boolean).join(' '),
74
94
  /* The archetype's own scrolling field stack โ€” two titled blocks, four
75
95
  fields and two links do not fit a 844 frame. No module CSS. */
76
96
  scrollFields: true,
77
97
  /* Step 3 of 3, and GREEN across all three once the submission runs. */
78
98
  progress: _jsx(PrizmaStepper, { steps: 3, currentState: busy ? 'Completed' : 3, showIcons: false, "aria-label": strings.stepperLabel, completedValueText: strings.stepperCompletedLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
99
+ /* Disabled only after the empty submit; the `*Empty` states keep it
100
+ enabled. */
101
+ ctaDisabled: emptyError || ctaDisabled,
79
102
  /* `loading` swaps the CTA label for Core's designed spinner. */
80
103
  ctaLoading: busy, ctaLoadingLabel: copy.ctaLoadingLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L",
81
104
  /* 5663:94390 greys "Back" while the submission runs. */
82
105
  disabled: busy, onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
83
106
  /* Exploration ekyb-desktop: name + surname share a row per block on a
84
107
  wide frame; on the phone nothing changes. */
85
- responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.uboName ?? '', disabled: busy }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, autoComplete: "off", defaultValue: values?.uboSurname ?? '', disabled: busy })] }), secondUbo ? (_jsxs(_Fragment, { children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboName ?? '' }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, autoComplete: "off", defaultValue: values?.secondUboSurname ?? '' })] }), copy.removeUboLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId: `${screenId}-remove-ubo` }), children: copy.removeUboLabel })) : null] })) : null, _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", disabled: busy,
108
+ responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.uboName ?? '', disabled: busy }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.uboSurname ?? '', disabled: busy })] }), secondUbo ? (_jsxs(_Fragment, { children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboName ?? '' }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboSurname ?? '' })] }), copy.removeUboLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId: `${screenId}-remove-ubo` }), children: copy.removeUboLabel })) : null] })) : null, _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", disabled: busy,
86
109
  /* Action-qualified id: the two "Add anotherโ€ฆ" links are different
87
110
  flows and must stay distinguishable (#PI-10), but the action
88
111
  contract has no per-action discriminator yet. */