@incodetech/prizma-core-screens 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/CurpEnterScreen.d.ts +7 -0
  2. package/dist/CurpEnterScreen.js +17 -3
  3. package/dist/CurpGenerateScreen.d.ts +8 -0
  4. package/dist/CurpGenerateScreen.js +28 -13
  5. package/dist/CurpLayout.css +8 -34
  6. package/dist/CurpStrings.d.ts +16 -2
  7. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  8. package/dist/EkybAddressDetailsScreen.js +25 -7
  9. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  10. package/dist/EkybBusinessDetailsScreen.js +23 -6
  11. package/dist/EkybCountryScreen.d.ts +13 -5
  12. package/dist/EkybCountryScreen.js +27 -9
  13. package/dist/EkybOwnersScreen.d.ts +8 -0
  14. package/dist/EkybOwnersScreen.js +26 -3
  15. package/dist/EkybStrings.d.ts +27 -10
  16. package/dist/EkycCountryCatalog.d.ts +4 -1
  17. package/dist/EkycCountryCatalog.js +5 -3
  18. package/dist/EkycCountryFormScreen.d.ts +12 -3
  19. package/dist/EkycCountryFormScreen.js +28 -10
  20. package/dist/EkycDetailsScreen.d.ts +7 -0
  21. package/dist/EkycDetailsScreen.js +21 -4
  22. package/dist/EkycStrings.d.ts +14 -2
  23. package/dist/ElectronicSignatureLayout.css +6 -0
  24. package/dist/EmailInputStrings.d.ts +11 -1
  25. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  26. package/dist/EmailInputVerifyScreen.js +17 -3
  27. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  28. package/dist/FaceMatchLayout.css +21 -12
  29. package/dist/FormScreen.css +16 -0
  30. package/dist/FormsLongFormScreen.d.ts +8 -0
  31. package/dist/FormsLongFormScreen.js +34 -13
  32. package/dist/FormsStrings.d.ts +11 -1
  33. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  34. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  35. package/dist/GeolocationStrings.d.ts +3 -0
  36. package/dist/IdCaptureLayout.css +40 -0
  37. package/dist/ListScreen.d.ts +2 -0
  38. package/dist/ListScreen.js +1 -1
  39. package/dist/PhoneInputLayout.css +10 -9
  40. package/dist/PhoneInputStrings.d.ts +11 -1
  41. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  42. package/dist/PhoneInputVerifyScreen.js +19 -6
  43. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  44. package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
  45. package/dist/QualifiedSignatureDocumentScreen.js +18 -16
  46. package/dist/QualifiedSignatureLayout.css +76 -0
  47. package/dist/QualifiedSignatureStrings.d.ts +10 -0
  48. package/dist/ScreenNav.css +8 -0
  49. package/dist/ScreenNav.d.ts +15 -2
  50. package/dist/ScreenNav.js +10 -3
  51. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  52. package/dist/VideoSelfieIdCaptureScreen.js +44 -13
  53. package/dist/VideoSelfieLayout.css +164 -33
  54. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  55. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  56. package/dist/VideoSelfieSpeakScreen.js +18 -1
  57. package/dist/VideoSelfieStrings.d.ts +78 -12
  58. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  59. package/dist/VideoSelfieTurnHeadScreen.js +63 -0
  60. package/dist/VideoSelfieTutorialScreen.js +13 -5
  61. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  62. package/dist/VideoSelfieWaveScreen.js +62 -0
  63. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  64. package/dist/WatchlistBusinessFormScreen.js +27 -13
  65. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +2 -0
  68. package/dist/types.d.ts +3 -1
  69. package/package.json +14 -3
  70. package/samples/video-selfie-room.png +0 -0
  71. package/src/CurpEnterScreen.tsx +21 -4
  72. package/src/CurpGenerateScreen.tsx +46 -22
  73. package/src/CurpLayout.css +8 -34
  74. package/src/CurpStrings.ts +22 -2
  75. package/src/EkybAddressDetailsScreen.tsx +32 -4
  76. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  77. package/src/EkybCountryScreen.tsx +28 -7
  78. package/src/EkybOwnersScreen.tsx +34 -3
  79. package/src/EkybStrings.ts +29 -10
  80. package/src/EkycCountryCatalog.ts +9 -4
  81. package/src/EkycCountryFormScreen.tsx +51 -12
  82. package/src/EkycDetailsScreen.tsx +22 -1
  83. package/src/EkycStrings.ts +14 -2
  84. package/src/ElectronicSignatureLayout.css +6 -0
  85. package/src/EmailInputStrings.ts +12 -1
  86. package/src/EmailInputVerifyScreen.tsx +21 -4
  87. package/src/FaceMatchComparisonScreen.tsx +3 -3
  88. package/src/FaceMatchLayout.css +21 -12
  89. package/src/FormScreen.css +16 -0
  90. package/src/FormsLongFormScreen.tsx +41 -13
  91. package/src/FormsStrings.ts +12 -1
  92. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  93. package/src/GeolocationStrings.ts +3 -0
  94. package/src/IdCaptureLayout.css +40 -0
  95. package/src/ListScreen.tsx +10 -1
  96. package/src/PhoneInputLayout.css +10 -9
  97. package/src/PhoneInputStrings.ts +12 -1
  98. package/src/PhoneInputVerifyScreen.tsx +21 -5
  99. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  100. package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
  101. package/src/QualifiedSignatureLayout.css +76 -0
  102. package/src/QualifiedSignatureStrings.ts +10 -0
  103. package/src/ScreenNav.css +8 -0
  104. package/src/ScreenNav.tsx +35 -5
  105. package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
  106. package/src/VideoSelfieLayout.css +164 -33
  107. package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
  108. package/src/VideoSelfieSpeakScreen.tsx +25 -14
  109. package/src/VideoSelfieStrings.ts +85 -12
  110. package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
  111. package/src/VideoSelfieTutorialScreen.tsx +34 -23
  112. package/src/VideoSelfieWaveScreen.tsx +135 -0
  113. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  114. package/src/WatchlistBusinessStrings.ts +21 -7
  115. package/src/index.ts +14 -0
  116. package/src/types.ts +3 -1
@@ -11,8 +11,9 @@ import './ScreenLayout.css';
11
11
  * (GUIDELINES.md §6-E, `PrizmaFormScreen`).
12
12
  *
13
13
  * ONE screen for both purple form sections (`watchlist.default` and
14
- * `watchlist.filled`): measured against the 390 frames they share an identical
15
- * skeleton — nav → Title (H2, no subtitle) → Spacing/32 → Country dropdown,
14
+ * `watchlist.filled`), plus the `emptyError` state the GUIDELINES §6-E
15
+ * empty-submit rule adds (Elena 2026-09-30, see note 3): measured against the 390 frames they share an identical
16
+ * skeleton — nav → Title (H2 + the "fields marked with *" H5) → Spacing/32 → Country dropdown,
16
17
  * Business Name and Tax ID at Spacing/16 → primary CTA → tag — and differ ONLY
17
18
  * in field content and whether the CTA is live.
18
19
  *
@@ -27,11 +28,14 @@ import './ScreenLayout.css';
27
28
  * and Core's `readOnly`, the other way to make a field inert, paints the
28
29
  * DISABLED grey surface on states Figma draws white (DEVIATIONS #PI-6).
29
30
  * Neither section greys anything, so `disabled` is set nowhere.
30
- * 3. The CTA state comes from the SECTION, not from the values: Figma greys
31
- * "Continue" on `default` and lights it on `filled` — and `filled` leaves Tax
32
- * ID empty, which is how we know the tax identifier does not gate the lookup
33
- * (DEVIATIONS #WB-2). Deriving `ctaDisabled` from the values would have
34
- * disagreed with the design.
31
+ * 3. The CTA state comes from the SECTION, not from the values. Under the
32
+ * GUIDELINES §6-E empty-submit rule (Elena 2026-09-30) "Continue" is live
33
+ * on the resting `default` form, so the user can press it, and greys ONLY
34
+ * on `emptyError`, the state that press leads to: Country and Business
35
+ * Name render empty (no placeholder) in Core's error state with
36
+ * `requiredError`. Tax ID never errors — `filled` leaves it empty with the
37
+ * CTA live, which is how we know it does not gate the lookup (DEVIATIONS
38
+ * #WB-2).
35
39
  * 4. The country flag is Core's canonical raster (24×18, radius 2) in the
36
40
  * dropdown's LEADING slot — `PrizmaCountryFlag` composed into
37
41
  * `PrizmaDropdown.renderLeading`, which exists for exactly this case and
@@ -49,21 +53,29 @@ import './ScreenLayout.css';
49
53
  */
50
54
  export const WatchlistBusinessFormScreen = forwardRef(({ strings, state = 'default', countries = [], values, screenId = `watchlist-business-form-${state}`, overrides, onAction, ...shell }, ref) => {
51
55
  const copy = strings.form;
52
- // `default` draws the CTA greyed, `filled` draws it live. The section is the
53
- // spec, not the field contents (see note 3 above).
54
- const empty = state === 'default';
56
+ // The CTA greys only after an empty submit; the resting form keeps it live
57
+ // so that submit can happen. The section is the spec, not the field
58
+ // contents (see note 3 above).
59
+ const emptyError = state === 'emptyError';
60
+ const errorText = emptyError ? strings.requiredError : undefined;
55
61
  // The label is the country NAME only. The flag is not part of it — it rides
56
62
  // the dropdown's leading slot below, where Core paints the raster.
57
63
  const options = countries.map((c) => ({
58
64
  value: c.value,
59
65
  label: c.label,
60
66
  }));
61
- return (_jsxs(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, ctaDisabled: empty, ctaLoadingLabel: copy.ctaLoadingLabel,
67
+ return (_jsxs(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, ctaDisabled: emptyError, ctaLoadingLabel: copy.ctaLoadingLabel,
62
68
  /* Figma draws no verified tag on either form section, but GUIDELINES §1
63
69
  and the standing owner rule (#F-30, #PI-8) keep header AND footer on
64
70
  every non-capture screen — so nothing shifts between the form, the
65
71
  spinner and the verdict. DEVIATIONS #WB-1. */
66
- verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: [_jsx(PrizmaDropdown, { label: copy.country.label, placeholder: copy.country.placeholder, options: options, defaultValue: values?.country ?? null,
72
+ verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: [_jsx(PrizmaDropdown, { label: copy.country.label,
73
+ /* Country and Business Name are required and marked (Elena,
74
+ 2026-09-30); the H5 explains the asterisk. */
75
+ required: true, requiredHint: strings.requiredHint,
76
+ /* The empty-submit error draws the field with nothing in it, as the
77
+ `FormsFieldScreen` `emptyError` precedent does. */
78
+ placeholder: emptyError ? undefined : copy.country.placeholder, options: options, defaultValue: emptyError ? null : (values?.country ?? null), error: emptyError, errorText: errorText,
67
79
  /* A country catalogue is long enough that Core's own search bar is
68
80
  the right behaviour; no purple section draws the menu open, so
69
81
  nothing about its appearance is being invented here. */
@@ -79,7 +91,9 @@ export const WatchlistBusinessFormScreen = forwardRef(({ strings, state = 'defau
79
91
  dialCode: '',
80
92
  flag: match.flag ?? '',
81
93
  } })) : null;
82
- } }), _jsx(PrizmaTextField, { label: copy.businessName.label, placeholder: copy.businessName.placeholder, autoComplete: "organization", defaultValue: values?.businessName ?? '' }), _jsx(PrizmaTextField, { label: copy.taxId.label, placeholder: copy.taxId.placeholder, autoComplete: "off", autoCapitalize: "characters", spellCheck: false, defaultValue: values?.taxId ?? '' })] }));
94
+ } }), _jsx(PrizmaTextField, { label: copy.businessName.label, required: true, requiredHint: strings.requiredHint, placeholder: emptyError ? undefined : copy.businessName.placeholder, autoComplete: "organization", defaultValue: emptyError ? '' : (values?.businessName ?? ''), error: emptyError, errorText: errorText }), _jsx(PrizmaTextField, { label: copy.taxId.label, placeholder: copy.taxId.placeholder, autoComplete: "off", autoCapitalize: "characters", spellCheck: false,
95
+ /* Optional (#WB-2): an empty submit leaves it clean. */
96
+ defaultValue: emptyError ? '' : (values?.taxId ?? '') })] }));
83
97
  });
84
98
  WatchlistBusinessFormScreen.displayName = 'WatchlistBusinessFormScreen';
85
99
  export default WatchlistBusinessFormScreen;
@@ -4,15 +4,18 @@
4
4
  * in field content and whether the CTA is live, which is exactly what
5
5
  * GUIDELINES §6-E means by "states are separate screens sharing the identical
6
6
  * layout":
7
- * - `default` — every field empty (the country dropdown on its placeholder),
8
- * CTA greyed
9
- * - `filled` — country selected and business name typed, CTA live
7
+ * - `default` — every field empty (the country dropdown on its placeholder),
8
+ * CTA live so the user can press it (GUIDELINES §6-E empty-submit rule)
9
+ * - `filled` — country selected and business name typed, CTA live
10
+ * - `emptyError` — the user pressed the CTA with the form empty: Country and
11
+ * Business Name render empty in Core's error state with `requiredError`,
12
+ * Tax ID stays clean, CTA greyed (Elena 2026-09-30)
10
13
  *
11
14
  * Note what `filled` does NOT do: it leaves **Tax ID empty** and still enables
12
15
  * the CTA, so the tax identifier is optional and does not gate the lookup
13
- * (DEVIATIONS #WB-2).
16
+ * (DEVIATIONS #WB-2). That is also why `emptyError` leaves Tax ID alone.
14
17
  */
15
- export type WatchlistBusinessFormState = 'default' | 'filled';
18
+ export type WatchlistBusinessFormState = 'default' | 'filled' | 'emptyError';
16
19
  /**
17
20
  * One selectable country of registration. Runtime data, never copy — the host
18
21
  * owns the catalogue (a full ISO list is not a string table), exactly as Phone
@@ -58,11 +61,22 @@ export interface WatchlistBusinessStrings {
58
61
  verifiedLabel: string;
59
62
  /** Accessible name for the logo nav row. */
60
63
  navLabel?: string;
61
- /** watchlist.{default,filled} */
64
+ /**
65
+ * The error row the REQUIRED fields (Country, Business Name) show on the
66
+ * `emptyError` form state ("This field is required"). Maps to the SDK's
67
+ * `dynamicForms.fieldRequired` key. GUIDELINES §6-E empty-submit rule.
68
+ */
69
+ requiredError: string;
70
+ /**
71
+ * Required-field marker ("*"), passed to Core's own `requiredHint` prop on
72
+ * Country and Business Name (Elena, 2026-09-30).
73
+ */
74
+ requiredHint?: string;
75
+ /** watchlist.{default,filled,emptyError} */
62
76
  form: {
63
77
  /** H2 ("Watchlist for business"). */
64
78
  title: string;
65
- /** H5 — the purple sections draw none, kept for hosts that want one. */
79
+ /** H5 ("All fields marked with * are required."), explains the asterisk. */
66
80
  subtitle?: string;
67
81
  country: WatchlistBusinessFieldStrings & {
68
82
  /** Search placeholder inside the open menu. */
package/dist/index.d.ts CHANGED
@@ -109,7 +109,7 @@ export { type WatchlistBusinessFormState, type WatchlistBusinessCountry, type Wa
109
109
  export { WatchlistBusinessFormScreen, type WatchlistBusinessFormScreenProps } from './WatchlistBusinessFormScreen';
110
110
  export { WatchlistBusinessProcessingScreen, type WatchlistBusinessProcessingScreenProps } from './WatchlistBusinessProcessingScreen';
111
111
  export { WatchlistBusinessSuccessScreen, type WatchlistBusinessSuccessScreenProps } from './WatchlistBusinessSuccessScreen';
112
- export { type VideoSelfieDocumentType, type VideoSelfieTutorialRow, type VideoSelfieTutorialRowKind, type VideoSelfieFeedbackKind, type VideoSelfieIdSide, type VideoSelfieIdErrorSeverity, type VideoSelfieSpeakFamily, type VideoSelfieStepsOs, type VideoSelfieStepsStrings, type VideoSelfieSpeakStrings, type VideoSelfieStrings, } from './VideoSelfieStrings';
112
+ export { type VideoSelfieDocumentType, type VideoSelfieTutorialRow, type VideoSelfieTutorialRowKind, type VideoSelfieFeedbackKind, type VideoSelfieIdSide, type VideoSelfieConditionCopy, type VideoSelfieIdCondition, type VideoSelfieIdErrorReason, type VideoSelfieIdErrorSeverity, type VideoSelfieTurnDirection, type VideoSelfieSpeakFamily, type VideoSelfieStepsOs, type VideoSelfieStepsStrings, type VideoSelfieSpeakStrings, type VideoSelfieStrings, } from './VideoSelfieStrings';
113
113
  export { VideoSelfieCameraStepsScreen, type VideoSelfieCameraStepsScreenProps } from './VideoSelfieCameraStepsScreen';
114
114
  export { VideoSelfieErrorScreen, type VideoSelfieErrorVariant, type VideoSelfieErrorScreenProps, } from './VideoSelfieErrorScreen';
115
115
  export { VideoSelfieFlipCameraScreen, type VideoSelfieFlipCameraScreenProps } from './VideoSelfieFlipCameraScreen';
@@ -123,6 +123,8 @@ export { VideoSelfieSelfieCaptureScreen, type VideoSelfieSelfieState, type Video
123
123
  export { VideoSelfieSpeakScreen, type VideoSelfieSpeakStage, type VideoSelfieSpeakScreenProps, } from './VideoSelfieSpeakScreen';
124
124
  export { VideoSelfieSuccessScreen, type VideoSelfieSuccessScreenProps } from './VideoSelfieSuccessScreen';
125
125
  export { VideoSelfieTutorialScreen, type VideoSelfieTutorialScreenProps } from './VideoSelfieTutorialScreen';
126
+ export { VideoSelfieTurnHeadScreen, type VideoSelfieTurnStage, type VideoSelfieTurnHeadScreenProps, } from './VideoSelfieTurnHeadScreen';
127
+ export { VideoSelfieWaveScreen, type VideoSelfieWaveStage, type VideoSelfieWaveScreenProps, } from './VideoSelfieWaveScreen';
126
128
  export { type NfcDocumentType, type NfcIssueKind, type NfcIssueStrings, type NfcHeadingStrings, type NfcStrings, } from './NfcStrings';
127
129
  export { NfcCommonIssuesScreen, type NfcCommonIssuesScreenProps } from './NfcCommonIssuesScreen';
128
130
  export { NfcErrorScreen, type NfcErrorVariant, type NfcErrorScreenProps, } from './NfcErrorScreen';
package/dist/index.js CHANGED
@@ -112,6 +112,8 @@ export { VideoSelfieSelfieCaptureScreen, } from './VideoSelfieSelfieCaptureScree
112
112
  export { VideoSelfieSpeakScreen, } from './VideoSelfieSpeakScreen';
113
113
  export { VideoSelfieSuccessScreen } from './VideoSelfieSuccessScreen';
114
114
  export { VideoSelfieTutorialScreen } from './VideoSelfieTutorialScreen';
115
+ export { VideoSelfieTurnHeadScreen, } from './VideoSelfieTurnHeadScreen';
116
+ export { VideoSelfieWaveScreen, } from './VideoSelfieWaveScreen';
115
117
  export { NfcCommonIssuesScreen } from './NfcCommonIssuesScreen';
116
118
  export { NfcErrorScreen, } from './NfcErrorScreen';
117
119
  export { NfcProcessingScreen } from './NfcProcessingScreen';
package/dist/types.d.ts CHANGED
@@ -10,7 +10,9 @@ import type { ReactNode } from 'react';
10
10
  * they collapsed into one indistinguishable event. DEVIATIONS #PI-10.
11
11
  */
12
12
  export interface PrizmaScreenAction {
13
- type: 'cta' | 'secondary' | 'tertiary' | 'auto' | 'help';
13
+ /** `back` — the nav's back chevron, on the screens that opt into one
14
+ * (`PrizmaScreenNav.backLabel`). */
15
+ type: 'cta' | 'secondary' | 'tertiary' | 'auto' | 'help' | 'back';
14
16
  /** Stable per-screen identifier, e.g. `face-capture-tutorial`. */
15
17
  screenId?: string;
16
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incodetech/prizma-core-screens",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Prizma Design System — SDK module screens: composed, responsive, brand-themeable React screens built from @incodetech/prizma-core-web.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -655,11 +655,21 @@
655
655
  "types": "./dist/VideoSelfieSuccessScreen.d.ts",
656
656
  "import": "./dist/VideoSelfieSuccessScreen.js"
657
657
  },
658
+ "./video-selfie-turn-head-screen": {
659
+ "source": "./src/VideoSelfieTurnHeadScreen.tsx",
660
+ "types": "./dist/VideoSelfieTurnHeadScreen.d.ts",
661
+ "import": "./dist/VideoSelfieTurnHeadScreen.js"
662
+ },
658
663
  "./video-selfie-tutorial-screen": {
659
664
  "source": "./src/VideoSelfieTutorialScreen.tsx",
660
665
  "types": "./dist/VideoSelfieTutorialScreen.d.ts",
661
666
  "import": "./dist/VideoSelfieTutorialScreen.js"
662
667
  },
668
+ "./video-selfie-wave-screen": {
669
+ "source": "./src/VideoSelfieWaveScreen.tsx",
670
+ "types": "./dist/VideoSelfieWaveScreen.d.ts",
671
+ "import": "./dist/VideoSelfieWaveScreen.js"
672
+ },
663
673
  "./viewfinder-screen": {
664
674
  "source": "./src/ViewfinderScreen.tsx",
665
675
  "types": "./dist/ViewfinderScreen.d.ts",
@@ -721,6 +731,7 @@
721
731
  "./samples/selfie-mask.png": "./samples/selfie-mask.png",
722
732
  "./samples/selfie.png": "./samples/selfie.png",
723
733
  "./samples/video-selfie-camera.png": "./samples/video-selfie-camera.png",
734
+ "./samples/video-selfie-room.png": "./samples/video-selfie-room.png",
724
735
  "./package.json": "./package.json"
725
736
  },
726
737
  "files": [
@@ -739,8 +750,8 @@
739
750
  },
740
751
  "dependencies": {
741
752
  "@incodetech/prizma-core-icons": "^0.4.0",
742
- "@incodetech/prizma-core-tokens": "^0.12.0",
743
- "@incodetech/prizma-core-web": "^0.12.0"
753
+ "@incodetech/prizma-core-tokens": "^0.13.0",
754
+ "@incodetech/prizma-core-web": "^0.13.0"
744
755
  },
745
756
  "devDependencies": {
746
757
  "@types/react": "^18.3.12",
Binary file
@@ -54,6 +54,13 @@ export interface CurpEnterScreenProps
54
54
  * `linkContent` slot: no primitive is invented, no layout is declared, and the
55
55
  * two actions stay distinguishable (`cta` vs `secondary`, the #PI-10 rule).
56
56
  * The gap is reported for promotion — see the module's DEVIATIONS rows.
57
+ *
58
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
59
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
60
+ * typed. The field renders empty with no placeholder, in Core's error state
61
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
62
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
63
+ * so the user can reach this state.
57
64
  */
58
65
  export const CurpEnterScreen = forwardRef<HTMLDivElement, CurpEnterScreenProps>(
59
66
  (
@@ -77,6 +84,9 @@ export const CurpEnterScreen = forwardRef<HTMLDivElement, CurpEnterScreenProps>(
77
84
  // surface.
78
85
  const busy = state === 'loading';
79
86
  const invalid = state === 'error';
87
+ // Empty submit: the field stays empty, drops its placeholder and takes
88
+ // Core's error state with the shared "required" row; the CTA greys.
89
+ const emptyError = state === 'emptyError';
80
90
  const focused = state === 'focused' || state === 'filled';
81
91
 
82
92
  return (
@@ -100,6 +110,7 @@ export const CurpEnterScreen = forwardRef<HTMLDivElement, CurpEnterScreenProps>(
100
110
  ctaLabel={strings.enter.ctaLabel}
101
111
  /* The `loading` section swaps the CTA label for Core's spinner. */
102
112
  ctaLoading={busy}
113
+ ctaDisabled={emptyError}
103
114
  ctaLoadingLabel={strings.enter.ctaLoadingLabel}
104
115
  linkContent={
105
116
  <PrizmaButton
@@ -125,15 +136,21 @@ export const CurpEnterScreen = forwardRef<HTMLDivElement, CurpEnterScreenProps>(
125
136
  carry the meaning — so the copy rides `aria-label` rather than
126
137
  Core's visible `label` slot (DEVIATIONS #PI-4). */
127
138
  aria-label={strings.enter.fieldLabel}
128
- placeholder={strings.enter.placeholder}
129
- defaultValue={value ?? ''}
139
+ placeholder={emptyError ? undefined : strings.enter.placeholder}
140
+ defaultValue={emptyError ? '' : value ?? ''}
130
141
  /* A CURP is an 18-character uppercase key. */
131
142
  autoCapitalize="characters"
132
143
  autoComplete="off"
133
144
  spellCheck={false}
134
145
  disabled={busy}
135
- error={invalid}
136
- errorText={invalid ? strings.enter.errorText : undefined}
146
+ error={invalid || emptyError}
147
+ errorText={
148
+ invalid
149
+ ? strings.enter.errorText
150
+ : emptyError
151
+ ? strings.requiredError
152
+ : undefined
153
+ }
137
154
  /* The clear ✕ is drawn only on the error section (76:36434); the
138
155
  `filled` section (79:1553) draws none. Core still owns WHEN it
139
156
  shows — its `--show` state needs a value in the field. */
@@ -56,6 +56,14 @@ export interface CurpGenerateScreenProps
56
56
  * The `dropdown` section IS the open-menu spec state, which Core exposes as the
57
57
  * documentation-only `forceOpen` (a screen is one static state, never a live
58
58
  * interaction) — the same mechanism Phone Input's `countryCode` section uses.
59
+ *
60
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
61
+ * GUIDELINES §6-E empty-submit rule): "Generate" was pressed with the form
62
+ * empty. All six fields carry the required `*`, so all six render empty with no
63
+ * placeholder, in Core's error state with `requiredError`; no menu is open and
64
+ * the primary CTA is disabled, following the drawn precedent in
65
+ * `FormsFieldScreen`. The resting `default` keeps the CTA enabled so the user
66
+ * can reach this state.
59
67
  */
60
68
  export const CurpGenerateScreen = forwardRef<HTMLDivElement, CurpGenerateScreenProps>(
61
69
  (
@@ -75,21 +83,30 @@ export const CurpGenerateScreen = forwardRef<HTMLDivElement, CurpGenerateScreenP
75
83
  ) => {
76
84
  const copy = strings.generate;
77
85
 
86
+ // Empty submit: every required field drops its value and placeholder and
87
+ // takes Core's error state with the shared "required" row; the CTA greys.
88
+ const emptyError = state === 'emptyError';
89
+ const ph = (placeholder?: string) => (emptyError ? undefined : placeholder);
90
+ const err = {
91
+ error: emptyError,
92
+ errorText: emptyError ? strings.requiredError : undefined,
93
+ };
94
+
78
95
  return (
79
96
  <PrizmaFormScreen
80
97
  ref={ref}
81
- /* Scopes the two module rules in CurpLayout.css (see the foundation and
82
- Core requests recorded there):
83
- - `--scroll` lets the six-field stack scroll inside the 844 frame
84
- instead of overflowing its box and painting over the CTA. EVERY
85
- section gets it, `dropdown` included — Figma draws "Generate"
86
- clear and unobstructed under the open menu.
87
- - `--menu-open` flips Core's absolutely-positioned menu above its
88
- trigger on the one section that forces it open, so the three
89
- gender rows stay inside the scrolling column. */
98
+ /* The six-field stack scrolls inside the 844 frame through the
99
+ foundation's `scrollFields` (overflow + the Spacing/16 that keeps the
100
+ cut clear of "Generate"). EVERY section gets it, `dropdown` included:
101
+ Figma draws "Generate" clear and unobstructed under the open menu.
102
+ It replaces the module's old `--scroll` rule, which scrolled but
103
+ carried no spacing, so the last field ran flush into the button.
104
+ `--menu-open` (CurpLayout.css) flips Core's absolutely-positioned
105
+ menu above its trigger on the one section that forces it open, so
106
+ the three gender rows stay inside the scrolling column. */
107
+ scrollFields
90
108
  className={[
91
109
  'curp-generate',
92
- 'curp-generate--scroll',
93
110
  state === 'dropdown' ? 'curp-generate--menu-open' : '',
94
111
  className,
95
112
  ]
@@ -98,6 +115,7 @@ export const CurpGenerateScreen = forwardRef<HTMLDivElement, CurpGenerateScreenP
98
115
  title={copy.title}
99
116
  subtitle={copy.subtitle}
100
117
  ctaLabel={copy.ctaLabel}
118
+ ctaDisabled={emptyError}
101
119
  ctaLoadingLabel={copy.ctaLoadingLabel}
102
120
  verifiedLabel={strings.verifiedLabel}
103
121
  navLabel={strings.navLabel}
@@ -112,54 +130,60 @@ export const CurpGenerateScreen = forwardRef<HTMLDivElement, CurpGenerateScreenP
112
130
  (DEVIATIONS #PI-6). */}
113
131
  <PrizmaTextField
114
132
  label={copy.firstName.label}
115
- placeholder={copy.firstName.placeholder}
133
+ placeholder={ph(copy.firstName.placeholder)}
116
134
  required
117
135
  requiredHint={strings.requiredHint}
118
136
  autoComplete="off"
119
- defaultValue={values?.firstName ?? ''}
137
+ defaultValue={emptyError ? '' : values?.firstName ?? ''}
138
+ {...err}
120
139
  />
121
140
  <PrizmaTextField
122
141
  label={copy.firstLastName.label}
123
- placeholder={copy.firstLastName.placeholder}
142
+ placeholder={ph(copy.firstLastName.placeholder)}
124
143
  required
125
144
  requiredHint={strings.requiredHint}
126
145
  autoComplete="off"
127
- defaultValue={values?.firstLastName ?? ''}
146
+ defaultValue={emptyError ? '' : values?.firstLastName ?? ''}
147
+ {...err}
128
148
  />
129
149
  <PrizmaTextField
130
150
  label={copy.secondLastName.label}
131
- placeholder={copy.secondLastName.placeholder}
151
+ placeholder={ph(copy.secondLastName.placeholder)}
132
152
  required
133
153
  requiredHint={strings.requiredHint}
134
154
  autoComplete="off"
135
- defaultValue={values?.secondLastName ?? ''}
155
+ defaultValue={emptyError ? '' : values?.secondLastName ?? ''}
156
+ {...err}
136
157
  />
137
158
  <PrizmaDropdown
138
159
  label={copy.gender.label}
139
- placeholder={copy.gender.placeholder}
160
+ placeholder={ph(copy.gender.placeholder)}
140
161
  required
141
162
  requiredHint={strings.requiredHint}
142
163
  options={genders}
143
- defaultValue={values?.gender ?? null}
164
+ defaultValue={emptyError ? null : values?.gender ?? null}
165
+ {...err}
144
166
  searchPlaceholder={copy.gender.searchPlaceholder}
145
167
  /* The `dropdown` section is the open Gender menu. */
146
168
  forceOpen={state === 'dropdown'}
147
169
  />
148
170
  <PrizmaDateField
149
171
  label={copy.dateOfBirth.label}
150
- placeholder={copy.dateOfBirth.placeholder}
172
+ placeholder={ph(copy.dateOfBirth.placeholder)}
151
173
  required
152
174
  requiredHint={strings.requiredHint}
153
175
  calendarLabel={copy.dateOfBirth.calendarLabel}
154
- defaultValue={values?.dateOfBirth ?? ''}
176
+ defaultValue={emptyError ? '' : values?.dateOfBirth ?? ''}
177
+ {...err}
155
178
  />
156
179
  <PrizmaDropdown
157
180
  label={copy.birthState.label}
158
- placeholder={copy.birthState.placeholder}
181
+ placeholder={ph(copy.birthState.placeholder)}
159
182
  required
160
183
  requiredHint={strings.requiredHint}
161
184
  options={states}
162
- defaultValue={values?.birthState ?? null}
185
+ defaultValue={emptyError ? null : values?.birthState ?? null}
186
+ {...err}
163
187
  searchable
164
188
  searchPlaceholder={copy.birthState.searchPlaceholder}
165
189
  />
@@ -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
@@ -27,16 +27,30 @@
27
27
  * - `error` — negative field + clear ✕ + the "Invalid CURP" row
28
28
  * - `loading` — both actions inert while the lookup runs; the spinner rides the
29
29
  * CTA, never the field (the Phone Input lesson, DEVIATIONS #PI-5)
30
+ * - `emptyError` — "Continue" pressed with the field still empty: the field
31
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
32
+ * and the CTA is disabled. GUIDELINES §6-E empty-submit rule (Elena,
33
+ * 2026-09-30).
30
34
  */
31
- export type CurpEnterState = 'default' | 'focused' | 'filled' | 'error' | 'loading';
35
+ export type CurpEnterState =
36
+ | 'default'
37
+ | 'focused'
38
+ | 'filled'
39
+ | 'error'
40
+ | 'loading'
41
+ | 'emptyError';
32
42
 
33
43
  /**
34
44
  * The `CURP.Gene.*` form sections — the six-field generator:
35
45
  * - `default` — every field empty on its placeholder
36
46
  * - `dropdown` — the Gender menu open over the form (Core's `forceOpen`)
37
47
  * - `filled` — every field carrying a value
48
+ * - `emptyError` — "Generate" pressed with the form still empty: every required
49
+ * field stays empty (no placeholder) in Core's error state with
50
+ * `requiredError`, no menu is open, and the CTA is disabled. GUIDELINES §6-E
51
+ * empty-submit rule (Elena, 2026-09-30).
38
52
  */
39
- export type CurpGenerateState = 'default' | 'dropdown' | 'filled';
53
+ export type CurpGenerateState = 'default' | 'dropdown' | 'filled' | 'emptyError';
40
54
 
41
55
  /**
42
56
  * The two spinner sections. `verifying` is `CURP.EnterCURP.veryfing` (the
@@ -96,6 +110,12 @@ export interface CurpStrings {
96
110
  * the marker is localizable and never hardcoded in a screen.
97
111
  */
98
112
  requiredHint?: string;
113
+ /**
114
+ * Error row under every required field left empty when the primary CTA is
115
+ * pressed (the `emptyError` state of the entry and generator forms):
116
+ * "This field is required". SDK key `dynamicForms.fieldRequired`.
117
+ */
118
+ requiredError: string;
99
119
 
100
120
  /** CURP.EnterCURP.{default,focused,filled,error,loading} */
101
121
  enter: {
@@ -82,6 +82,13 @@ export interface EkybAddressDetailsScreenProps
82
82
  * segment 2 solid, segment 3 grey) is Core's `PrizmaStepper` at
83
83
  * `currentState={2}`, composed into the archetype's `progress` slot. This is
84
84
  * step 2 of 3.
85
+ *
86
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
87
+ * 2026-09-30, GUIDELINES §6-E empty-submit rule; precedent `FormsFieldScreen`'s
88
+ * `emptyError`): every field renders empty, the five REQUIRED ones (State
89
+ * included) in Core's `error` state with `requiredError` and no placeholder,
90
+ * Address line 2 unflagged (optional, DEVIATIONS #EKC-6's rule), and Continue
91
+ * disabled. `empty` keeps Continue enabled so the press can happen.
85
92
  */
86
93
  export const EkybAddressDetailsScreen = forwardRef<
87
94
  HTMLDivElement,
@@ -91,9 +98,10 @@ export const EkybAddressDetailsScreen = forwardRef<
91
98
  {
92
99
  strings,
93
100
  state = 'empty',
94
- values,
101
+ values: valuesProp,
95
102
  states = [],
96
103
  screenId = `ekyb-address-${state}`,
104
+ ctaDisabled,
97
105
  className,
98
106
  overrides,
99
107
  onAction,
@@ -102,6 +110,14 @@ export const EkybAddressDetailsScreen = forwardRef<
102
110
  ref,
103
111
  ) => {
104
112
  const copy = strings.address;
113
+ const emptyError = state === 'emptyError';
114
+ const values = emptyError ? undefined : valuesProp;
115
+ // Core's own error treatment, spread AFTER each required field's own
116
+ // placeholder so the error state shows none (the `FormsFieldScreen`
117
+ // precedent). Address line 2 never receives it.
118
+ const missing = emptyError
119
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
120
+ : {};
105
121
 
106
122
  return (
107
123
  <PrizmaFormScreen
@@ -122,6 +138,8 @@ export const EkybAddressDetailsScreen = forwardRef<
122
138
  title={copy.title}
123
139
  subtitle={copy.subtitle}
124
140
  ctaLabel={copy.ctaLabel}
141
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
142
+ ctaDisabled={emptyError || ctaDisabled}
125
143
  linkContent={
126
144
  <PrizmaButton
127
145
  variant="secondary"
@@ -136,17 +154,20 @@ export const EkybAddressDetailsScreen = forwardRef<
136
154
  screenId={screenId}
137
155
  overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
138
156
  onAction={onAction}
139
- /* Exploration ekyb-desktop: on a wide frame only State + Postal code
140
- share a row (Figma 6407:43637); the four text fields above span the
141
- column. On the phone nothing changes. */
157
+ /* Exploration ekyb-desktop: on a wide frame the fields pair up in
158
+ three rows, Street + House number, Address line 2 + City (Elena,
159
+ 2026-09-28), and State + Postal code (Figma 6407:43637). On the
160
+ phone nothing changes. */
142
161
  responsive
143
162
  {...shell}
144
163
  >
164
+ <PrizmaFormRow>
145
165
  <PrizmaTextField
146
166
  label={copy.street.label}
147
167
  placeholder={copy.street.placeholder}
148
168
  required
149
169
  requiredHint={strings.requiredHint}
170
+ {...missing}
150
171
  autoComplete="off"
151
172
  defaultValue={values?.street ?? ''}
152
173
  />
@@ -155,9 +176,12 @@ export const EkybAddressDetailsScreen = forwardRef<
155
176
  placeholder={copy.houseNumber.placeholder}
156
177
  required
157
178
  requiredHint={strings.requiredHint}
179
+ {...missing}
158
180
  autoComplete="off"
159
181
  defaultValue={values?.houseNumber ?? ''}
160
182
  />
183
+ </PrizmaFormRow>
184
+ <PrizmaFormRow>
161
185
  {/* The one field 6340:50731 draws without an asterisk. */}
162
186
  <PrizmaTextField
163
187
  label={copy.addressLine2.label}
@@ -170,15 +194,18 @@ export const EkybAddressDetailsScreen = forwardRef<
170
194
  placeholder={copy.city.placeholder}
171
195
  required
172
196
  requiredHint={strings.requiredHint}
197
+ {...missing}
173
198
  autoComplete="off"
174
199
  defaultValue={values?.city ?? ''}
175
200
  />
201
+ </PrizmaFormRow>
176
202
  <PrizmaFormRow>
177
203
  <PrizmaDropdown
178
204
  label={copy.state.label}
179
205
  placeholder={copy.state.placeholder}
180
206
  required
181
207
  requiredHint={strings.requiredHint}
208
+ {...missing}
182
209
  options={states}
183
210
  /* Both sections draw the placeholder unless the host supplies a
184
211
  value — see the docstring note on 6340:38197. */
@@ -193,6 +220,7 @@ export const EkybAddressDetailsScreen = forwardRef<
193
220
  placeholder={copy.postalCode.placeholder}
194
221
  required
195
222
  requiredHint={strings.requiredHint}
223
+ {...missing}
196
224
  autoComplete="off"
197
225
  defaultValue={values?.postalCode ?? ''}
198
226
  />