@incodetech/prizma-core-screens 0.2.12 → 0.2.18

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 (64) hide show
  1. package/dist/DigitalIdDeviceWalletsDocumentSelectionScreen.d.ts +34 -0
  2. package/dist/DigitalIdDeviceWalletsDocumentSelectionScreen.js +47 -0
  3. package/dist/DigitalIdDeviceWalletsResultScreen.d.ts +28 -0
  4. package/dist/DigitalIdDeviceWalletsResultScreen.js +23 -0
  5. package/dist/DigitalIdDeviceWalletsStrings.d.ts +55 -0
  6. package/dist/DigitalIdDeviceWalletsStrings.js +19 -0
  7. package/dist/DigitalIdProvidersDocumentSelectionScreen.d.ts +31 -0
  8. package/dist/DigitalIdProvidersDocumentSelectionScreen.js +38 -0
  9. package/dist/DigitalIdProvidersErrorScreen.d.ts +33 -0
  10. package/dist/DigitalIdProvidersErrorScreen.js +33 -0
  11. package/dist/DigitalIdProvidersLayout.css +47 -0
  12. package/dist/DigitalIdProvidersRedirectScreen.d.ts +23 -0
  13. package/dist/DigitalIdProvidersRedirectScreen.js +17 -0
  14. package/dist/DigitalIdProvidersStrings.d.ts +76 -0
  15. package/dist/DigitalIdProvidersStrings.js +15 -0
  16. package/dist/FaceCaptureLayout.css +150 -0
  17. package/dist/FormScreen.css +35 -0
  18. package/dist/IdCaptureLayout.css +63 -0
  19. package/dist/ScreenLayout.css +191 -0
  20. package/dist/ScreenShell.css +13 -0
  21. package/dist/TutorialScreen.js +1 -1
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.js +5 -0
  24. package/dist/styles.css +1 -0
  25. package/package.json +29 -3
  26. package/samples/digital-id-providers/audkenni.png +0 -0
  27. package/samples/digital-id-providers/bankid-cz.png +0 -0
  28. package/samples/digital-id-providers/bankid-se.png +0 -0
  29. package/samples/digital-id-providers/ca-dmv-wallet.png +0 -0
  30. package/samples/digital-id-providers/digidentity.png +0 -0
  31. package/samples/digital-id-providers/diia.png +0 -0
  32. package/samples/digital-id-providers/edo.png +0 -0
  33. package/samples/digital-id-providers/eparaksts.png +0 -0
  34. package/samples/digital-id-providers/evrotrust.png +0 -0
  35. package/samples/digital-id-providers/freja.png +0 -0
  36. package/samples/digital-id-providers/ftn.png +0 -0
  37. package/samples/digital-id-providers/handy-signatur.png +0 -0
  38. package/samples/digital-id-providers/idin.png +0 -0
  39. package/samples/digital-id-providers/itsme.png +0 -0
  40. package/samples/digital-id-providers/mitid.png +0 -0
  41. package/samples/digital-id-providers/mobile-id.png +0 -0
  42. package/samples/digital-id-providers/mojeid.png +0 -0
  43. package/samples/digital-id-providers/oneid.png +0 -0
  44. package/samples/digital-id-providers/smart-id.png +0 -0
  45. package/samples/digital-id-providers/spid.png +0 -0
  46. package/samples/digital-id-providers/swissid.png +0 -0
  47. package/samples/digital-id-providers/uae-pass.png +0 -0
  48. package/samples/digital-id-providers/verimi.png +0 -0
  49. package/src/DigitalIdDeviceWalletsDocumentSelectionScreen.tsx +108 -0
  50. package/src/DigitalIdDeviceWalletsResultScreen.tsx +76 -0
  51. package/src/DigitalIdDeviceWalletsStrings.ts +80 -0
  52. package/src/DigitalIdProvidersDocumentSelectionScreen.tsx +113 -0
  53. package/src/DigitalIdProvidersErrorScreen.tsx +104 -0
  54. package/src/DigitalIdProvidersLayout.css +47 -0
  55. package/src/DigitalIdProvidersRedirectScreen.tsx +58 -0
  56. package/src/DigitalIdProvidersStrings.ts +102 -0
  57. package/src/FaceCaptureLayout.css +150 -0
  58. package/src/FormScreen.css +35 -0
  59. package/src/IdCaptureLayout.css +63 -0
  60. package/src/ScreenLayout.css +191 -0
  61. package/src/ScreenShell.css +13 -0
  62. package/src/TutorialScreen.tsx +4 -1
  63. package/src/index.ts +42 -0
  64. package/src/styles.css +1 -0
@@ -0,0 +1,102 @@
1
+ // Digital ID Providers — copy contract.
2
+ //
3
+ // Every Digital ID Providers screen receives the SAME `strings:
4
+ // DigitalIdProvidersStrings` object (rule: all copy via props, no hardcoded
5
+ // English — GUIDELINES.md §9).
6
+ //
7
+ // Authoritative screens: `Digital ID Providers - Latest Designs` Figma file
8
+ // (q0wiyQkLXxGQ2jkPEutxR8) — the chooser `Document Chooser` (27753:8099) and its
9
+ // full provider list (27703:4895), and the section `🪪 Digital ID Verification
10
+ // Flow - 🌐 Web - Full flow` (27707:9237): Redirecting, the provider's own pages
11
+ // (OS chrome, not built), success and five errors. Web only: the providers
12
+ // redirect out of the browser to the provider's site and back (Web SDK,
13
+ // `digitalIdRedirect`). These screens sit in the Digital IDs module beside the
14
+ // device wallets (`DigitalIdDeviceWallets*`), and share its success screen.
15
+
16
+ /** The five ways a provider run can fail (`27707:9338`). */
17
+ export type DigitalIdProvidersErrorState =
18
+ | 'unavailable'
19
+ | 'consent-denied'
20
+ | 'timed-out'
21
+ | 'not-accepted'
22
+ | 'expired';
23
+
24
+ /** One provider row on the chooser. `id` is what `onOptionSelect` reports. */
25
+ export interface DigitalIdProvidersOption {
26
+ id: string;
27
+ /** "iDIN", "itsme", "CA DMV Wallet"… */
28
+ label: string;
29
+ /** The provider's logo tile (an image URL), from the backend catalog.
30
+ * Absent → Core's generic `digital-id-web` card icon. Samples ship under
31
+ * `samples/digital-id-providers/`. */
32
+ logo?: string;
33
+ }
34
+
35
+ /** What the person picked: a physical document, or a provider by its id. */
36
+ export type DigitalIdProvidersSelection =
37
+ | { kind: 'id' }
38
+ | { kind: 'passport' }
39
+ | { kind: 'provider'; providerId: string };
40
+
41
+ export interface DigitalIdProvidersStrings {
42
+ /** Accessible name for the logo nav region (i18n — no baked-in English). */
43
+ navLabel?: string;
44
+ /** "verified by" attribution copy for the footer tag. */
45
+ verifiedLabel: string;
46
+
47
+ /** `Document Chooser` (27753:8099). */
48
+ documentSelection: {
49
+ /** "Select how to verify" — the module's one chooser title (owner call,
50
+ * 2026-10-28; the frame says "Verify your identity"). */
51
+ title: string;
52
+ /** "Viewing options for" — before the country. */
53
+ countryLabel: string;
54
+ /** ISO code of the country the options are for, e.g. "US" (the flag). */
55
+ countryCode: string;
56
+ /** The country as shown, e.g. "US". */
57
+ countryName: string;
58
+ /** "Use your ID" */
59
+ idSectionLabel: string;
60
+ /** "Identity Card" */
61
+ idLabel: string;
62
+ /** "Passport" */
63
+ passportLabel: string;
64
+ /** "Digital ID verification" */
65
+ providersSectionLabel: string;
66
+ /** The providers the flow offers, in order. */
67
+ providers: DigitalIdProvidersOption[];
68
+ };
69
+
70
+ /** `Redirecting` (1-18-1-id-hold-on, 27707:9281). */
71
+ redirecting: {
72
+ /** "Redirecting for verification…" */
73
+ title: string;
74
+ /** "You'll be redirected to complete verification" */
75
+ subtitle?: string;
76
+ };
77
+
78
+ /** `Errors` (27707:9338), one slice per state. */
79
+ errors: Record<
80
+ DigitalIdProvidersErrorState,
81
+ {
82
+ title: string;
83
+ subtitle?: string;
84
+ /** "Try again", or "Scan ID" when the provider is unavailable. */
85
+ ctaLabel: string;
86
+ /** "Choose another method" — one wording on every error (owner call,
87
+ * 2026-10-28; the frame says "Change verification method" on two). */
88
+ secondaryCtaLabel: string;
89
+ }
90
+ >;
91
+
92
+ /** The accepted-documents card on `not-accepted` and `expired`. */
93
+ acceptedDocuments: {
94
+ /** ISO code for the flag, e.g. "IN". */
95
+ countryCode: string;
96
+ /** "India" */
97
+ country: string;
98
+ /** Emoji fallback when the flag is outside Core's raster bank. */
99
+ flag?: string;
100
+ documents: string[];
101
+ };
102
+ }
@@ -50,6 +50,156 @@
50
50
  }
51
51
  }
52
52
 
53
+ /* ── Landscape phones: the ring holds its 335, the stack changes axis ────────
54
+ * The capture half of ScreenLayout.css's landscape block (#F-39). A phone
55
+ * held sideways is 375 to 440 tall and the portrait stack needs ~557 (nav,
56
+ * Spacing/40, the 335 ring, Spacing/32, instruction, footer): on the 844 x
57
+ * 390 frame the ring ran 48 past the bottom of the panel, the instruction and
58
+ * the shutter were below the frame entirely and the bottom-anchored tag drew
59
+ * across the ring (measured on the Core Lab canvas, 2026-10-20). The ring is
60
+ * not what gives ground (#20, Camila 2026-10-14) and nothing stacked under it
61
+ * can fit, so the component's column becomes two ZONES side by side: the ring
62
+ * at the left, vertically centred, the instruction beside it filling the rest,
63
+ * and on the manual screen the shutter at the right edge. 375 − 335 leaves 20 of air
64
+ * above and below the ring on the shortest phone, so it fits at full size.
65
+ *
66
+ * The instruction stays INSIDE `PrizmaSelfieCapture` (#12/#14): the root
67
+ * changes axis, the way #20 already re-seats its padding and gap from this
68
+ * file, and no copy leaves the component. Three decisions:
69
+ *
70
+ * · The ring is pinned to the LEFT edge, not centred with its neighbours, so
71
+ * it is in the same place on every live state (auto has an instruction
72
+ * beside it, manual an instruction and a shutter); once it has dissolved
73
+ * into the spinner or the verdict mark there is nothing beside it and the
74
+ * stage centres: a ring that moves as the
75
+ * step advances is the glitch #VS-24 exists for. The uploading / processing
76
+ * spinner therefore dissolves in the ring's own seat at the left, with the
77
+ * right zone empty for the second it lasts.
78
+ * · The instruction takes whatever width is left and centres in it. On the
79
+ * 844 panel that is 437; on the 667 SE it is 172 beside a shutter, which
80
+ * wraps the manual line to five lines of H4 (104 tall) inside a 375 panel.
81
+ * · The SHUTTER sits at the right edge, where the thumb is on a phone held
82
+ * sideways: the same seat every native camera app gives it in landscape.
83
+ * Beside the ring would put it under the user's palm.
84
+ *
85
+ * The content column stops reserving the footer under the ring
86
+ * (ScreenLayout.css's block): with the reserve the ring had 328 to sit in;
87
+ * the tag itself goes, see below. The "Need more time" notice sits on the
88
+ * right zone's rail. Outside the query not one declaration
89
+ * exists. DEVIATIONS #F-39. */
90
+ @container pcs-screen ((orientation: landscape) and (max-height: 500px)) {
91
+ /* The root becomes the frame the zones are anchored in. Anchored, not a
92
+ * flex row: Core Lab's scene engine centres the IN-FLOW children of a row
93
+ * as a group and draws the stage from its own pinned box, which put the
94
+ * instruction across the ring on the canvas while the browser drew it
95
+ * right. Anchored boxes are the one layout the engine reproduces exactly
96
+ * (the tag, the capture strip and the notice already rely on it), and the
97
+ * browser lays them out the same way, so the canvas IS the SDK. */
98
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture {
99
+ position: relative;
100
+ display: block;
101
+ width: 100%;
102
+ max-width: none;
103
+ height: 100%;
104
+ padding: 0;
105
+ /* The ring's measure, stated once: Core's own Capture / Selfie frame width
106
+ * (SelfieCapture.css caps the ROOT at 335 and the stage takes 100% of it;
107
+ * with the root now the full width the cap moves to the stage, same
108
+ * number, same source). A host that must fit the ring between its own
109
+ * bars overrides this one property (Core Lab does, `[data-pcs-safe]`). */
110
+ --pcs-ring: 335px; /* hardcoded — Core's Capture / Selfie frame width; not a token */
111
+ /* The instruction's box beside the ring: 320 at most (the width a two-line
112
+ * H4 instruction takes), less when the column has less, and on manual
113
+ * capture less the shutter's 112 rail at the right edge. */
114
+ --pcs-instruction: min(320px, calc(100% - var(--pcs-ring) - var(--prizma-spacing-24)));
115
+ /* Ring, Spacing/24, instruction: ONE pair, centred on the column. Round 2
116
+ * pinned the ring at the left edge and let the instruction float in the
117
+ * middle of whatever was left, which at 844 put the words 230 from the
118
+ * ring (Camila, 2026-10-23: "the face frame looks too separate from the
119
+ * feedback"). Still anchored boxes, not a flex row, for the canvas. */
120
+ --pcs-pair: calc(var(--pcs-ring) + var(--prizma-spacing-24) + var(--pcs-instruction));
121
+ --pcs-pair-left: calc((100% - var(--pcs-pair)) / 2);
122
+ }
123
+
124
+ /* Manual capture: the pair centres in what is left of the shutter's rail. */
125
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture:has(> .prizma-selfie-capture__shutter) {
126
+ --pcs-instruction: min(320px, calc(100% - var(--pcs-ring) - var(--prizma-spacing-24) - var(--prizma-icon-size-3xl) - 2 * var(--prizma-spacing-24)));
127
+ --pcs-pair-left: calc((100% - var(--prizma-icon-size-3xl) - 2 * var(--prizma-spacing-24) - var(--pcs-pair)) / 2);
128
+ }
129
+
130
+ /* The ring: the pair's left edge, centred on the panel's height. */
131
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture > .prizma-selfie-capture__stage {
132
+ position: absolute;
133
+ left: var(--pcs-pair-left);
134
+ top: calc(50% - var(--pcs-ring) / 2);
135
+ width: var(--pcs-ring);
136
+ height: var(--pcs-ring); /* the same measure, stated outright so an engine without aspect-ratio draws the square */
137
+ max-width: none;
138
+ }
139
+
140
+ /* A stage with nothing beside it centres: uploading, processing and the
141
+ * success verdict have dissolved the ring into a spinner or a mark and
142
+ * carry no instruction, no shutter, so there is no second zone to hold
143
+ * (Camila, 2026-10-21: "it has space to be in the centre, so it should").
144
+ * The live capture states keep the ring at the left so it never moves
145
+ * between them; the dissolve is the one step where the stage changes
146
+ * anyway. */
147
+ .prizma-screen-layout__content--capture
148
+ > .prizma-selfie-capture:not(
149
+ :has(> .prizma-selfie-capture__feedback, > .prizma-selfie-capture__shutter, > .prizma-selfie-capture__status)
150
+ )
151
+ > .prizma-selfie-capture__stage {
152
+ left: calc(50% - var(--pcs-ring) / 2);
153
+ }
154
+
155
+ /* The instruction (and overlay mode's status band): Spacing/24 right of the
156
+ * ring, in its own box, vertically centred on the ring's axis, the words
157
+ * starting at the box's left edge so they read as the ring's caption rather
158
+ * than as a second column. */
159
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture > .prizma-selfie-capture__status,
160
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture > .prizma-selfie-capture__feedback {
161
+ position: absolute;
162
+ top: 0;
163
+ bottom: 0;
164
+ left: calc(var(--pcs-pair-left) + var(--pcs-ring) + var(--prizma-spacing-24));
165
+ width: var(--pcs-instruction);
166
+ right: auto;
167
+ margin: 0;
168
+ max-width: none;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: flex-start;
172
+ text-align: start;
173
+ }
174
+
175
+ /* The shutter: right edge, centred on the panel's height. 64 is the strip's
176
+ * own measure (`--pcs-capture-chrome-space` reserves the same token). */
177
+ .prizma-screen-layout__content--capture > .prizma-selfie-capture > .prizma-selfie-capture__shutter {
178
+ position: absolute;
179
+ right: 0;
180
+ top: calc(50% - var(--prizma-icon-size-3xl) / 2);
181
+ width: var(--prizma-icon-size-3xl);
182
+ max-width: none;
183
+ }
184
+
185
+ /* The verified tag goes on landscape capture screens (Camila, 2026-10-21:
186
+ * "somewhere at the bottom or not at all"). Bottom centre, its one seat, is
187
+ * where the ring's lower edge is: on the SE the ring spans 20 to 355 and the
188
+ * tag 329 to 359. Moving the ring off centre to clear it would put the ring
189
+ * in a different place on this screen than on the uploading one, and a
190
+ * feed-open capture screen already drops the client logo for the same
191
+ * reason (dense, 2026-08-18): the tag comes back on the very next screen. */
192
+ .prizma-screen-shell:has(> .prizma-screen-layout__content--capture) .prizma-screen-layout__footer {
193
+ display: none;
194
+ }
195
+
196
+ /* The notice keeps the rail the tag used to reserve, in the right zone. */
197
+ .prizma-screen-shell .prizma-screen-layout__capture-notice {
198
+ left: 50%;
199
+ bottom: var(--prizma-spacing-16);
200
+ }
201
+ }
202
+
53
203
  /* ── Big panels: the ring grows to the column and the stack moves down ───────
54
204
  * The mirror of the block above, and the capture counterpart of the tutorial's
55
205
  * #F-36. On the 768 × 1024 iPad the column widens to 468 but the stack kept
@@ -91,6 +91,41 @@
91
91
  flex-shrink: 0;
92
92
  }
93
93
 
94
+ /* ── Short panels: a field stack that does not fit pans ──────────────────────
95
+ * The same decision archetype G's list took in #F-38, for archetype E. The
96
+ * stack is `flex: 1; min-height: 0`, so on a short panel it shrinks to what
97
+ * the title block and the CTA leave, and its rows, which keep their height,
98
+ * run straight on under the anchored CTA: on the closed iPhone Duo (678,
99
+ * measured 2026-10-29, the day the Duo presets opened to every module) eKYC's
100
+ * "Enter your details" drew Continue across its Date of Birth field and the
101
+ * empty-fields error painted its "This field is required" over the verified
102
+ * tag; Advanced Signature's accept screen lost its third required checkbox
103
+ * entirely on the open panel (626) while "Finish signing" showed enabled.
104
+ * The archetype already owns the answer as `scrollFields` (`--scroll`), opted
105
+ * into by the screens that know they are long (CURP, the consents, eKYB,
106
+ * the long forms, OCR review); a short panel is the same situation decided
107
+ * by the panel rather than the screen, so under 720 every stack takes the
108
+ * variant's declarations: the rows pan, Spacing/16 of air stays above the
109
+ * CTA, no scrollbar is drawn, and the title, the CTA and the tag hold still.
110
+ * Rows keep their intrinsic height for the same reason the variant's do
111
+ * (#QS-1). `--end` keeps hanging its rows off the bottom when they fit.
112
+ * Outside the query not one declaration exists. DEVIATIONS #F-40. */
113
+ @container pcs-screen (max-height: 720px) {
114
+ .prizma-form-screen__fields {
115
+ overflow-y: auto;
116
+ margin-bottom: var(--prizma-spacing-16);
117
+ scrollbar-width: none;
118
+ }
119
+
120
+ .prizma-form-screen__fields::-webkit-scrollbar {
121
+ display: none;
122
+ }
123
+
124
+ .prizma-form-screen__fields > * {
125
+ flex-shrink: 0;
126
+ }
127
+ }
128
+
94
129
  /* CTA column: primary, then the optional tertiary link below it. Core's
95
130
  * `PrizmaButtonGroup` owns this primary → tertiary rhythm at Spacing/12, so the
96
131
  * same value is used here rather than a new one (the archetype composes two
@@ -312,3 +312,66 @@
312
312
  .id-capture-on-surface .prizma-id-capture__encrypted {
313
313
  color: var(--prizma-text-body-800-primary);
314
314
  }
315
+
316
+ /* ── Short panels: the verdict still shrinks, the manual stage keeps its caption
317
+ * Two more rows for the 720 line (SHORT-PANELS.md), found the day the iPhone
318
+ * Duo presets opened to every module (2026-10-29), both on the open panel
319
+ * (626) and neither at 678 or above.
320
+ *
321
+ * (a) The result screens with a captured still (every error verdict, the
322
+ * success verdicts, Classification) run on archetype F, whose column pans
323
+ * under 720 (#F-37). The still is Core's `PrizmaIdCapture` in its filled
324
+ * state: 342 wide, its height from the variant's aspect, the encrypted row
325
+ * under it. At 626 the stack is 21 to 39px too tall, so the column panned and
326
+ * the still lost its bottom border and its caption, or the caption was
327
+ * squashed to a sliver (the row is a flex child with no floor). Step 1 of the
328
+ * order of concessions, the same mechanism as the vertical stage frame above:
329
+ * the still is the one part of the column that gives up height, its aspect
330
+ * ratio narrows the width to match, every other child keeps its height, and
331
+ * the caption stays a full row. With room to spare the still never shrinks.
332
+ * DEVIATIONS #IDC-33.
333
+ *
334
+ * (b) On the manual stages the strip carries the numbered tooltip above the
335
+ * shutter (`PrizmaCaptureManual`, absolute, `bottom: 100%` of the strip).
336
+ * The viewfinder column reserves the strip's footprint
337
+ * (`--pcs-capture-chrome-space`, ScreenLayout.css) but not the tooltip's, so
338
+ * on a panel short enough for the frame to sit flush on that reserve the
339
+ * tooltip landed on the "All data is encrypted" row under the frame. When the
340
+ * strip has a tooltip, the column reserves it too: the multiline Tooltip's
341
+ * two lines plus its Spacing/16 padding, plus the zone's Spacing/8 gap. The
342
+ * shortfall goes where this stylesheet already sends it, the frame. At 844
343
+ * the bottom band has that much slack and the frame would not move, but the
344
+ * rule is gated with the rest so the reference stays byte-identical.
345
+ * DEVIATIONS #IDC-34. */
346
+ @container pcs-screen (max-height: 720px) {
347
+ .id-capture-on-surface .prizma-screen-layout__content--centered > :not(.prizma-id-capture) {
348
+ flex-shrink: 0;
349
+ }
350
+ .id-capture-on-surface .prizma-screen-layout__content--centered > .prizma-id-capture {
351
+ flex: 0 1 auto;
352
+ min-height: 0;
353
+ }
354
+ .id-capture-on-surface .prizma-screen-layout__content--centered > .prizma-id-capture > :not(.prizma-id-capture__frame) {
355
+ flex-shrink: 0;
356
+ }
357
+ .id-capture-on-surface .prizma-screen-layout__content--centered > .prizma-id-capture > .prizma-id-capture__frame {
358
+ flex: 0 1 auto;
359
+ min-height: 0;
360
+ width: auto;
361
+ max-width: 100%;
362
+ /* The still's natural height at the column's width (the column is capped
363
+ * at 390 on any panel, less its two Spacing/24 gutters), so with room to
364
+ * spare nothing changes and under pressure it is the first to give. */
365
+ flex-basis: calc((min(100cqw, 390px) - 2 * var(--prizma-spacing-24)) * 220 / 350);
366
+ }
367
+ .id-capture-on-surface .prizma-screen-layout__content--centered > .prizma-id-capture--passport > .prizma-id-capture__frame {
368
+ flex-basis: calc((min(100cqw, 390px) - 2 * var(--prizma-spacing-24)) * 254 / 350);
369
+ }
370
+
371
+ .prizma-screen-shell:has(.prizma-screen-layout__capture-strip .prizma-capture-manual__tooltip-zone) .prizma-viewfinder__content {
372
+ padding-bottom: calc(
373
+ var(--pcs-capture-chrome-space) + var(--prizma-spacing-8) +
374
+ 2 * var(--prizma-tooltip-m-line-height) + 2 * var(--prizma-spacing-16)
375
+ );
376
+ }
377
+ }
@@ -829,6 +829,197 @@
829
829
  }
830
830
  }
831
831
 
832
+ /* ── Landscape phones: the stack keeps its order, the ring changes axis ──────
833
+ * WCAG 2.1 SC 1.3.4 Orientation (AA) asks every screen to work held sideways,
834
+ * and a phone held sideways is 375 to 440 tall (the phone catalog rotated:
835
+ * iPhone SE 667 x 375, the 844 x 390 reference, Large Android 915 x 412,
836
+ * Pro Max 956 x 440). Measured against the stacks the short-panel rules fit
837
+ * into 626: a capture screen needs ~557, a tutorial ~700. So two things give
838
+ * ground that no short panel asked for: the tutorial loses its illustration,
839
+ * and the capture screen turns its ring and its instruction side by side
840
+ * (FaceCaptureLayout.css). Everything else keeps the portrait order, title
841
+ * over copy over button, compacted (Camila, 2026-10-21: top-down, not side by
842
+ * side; the tag always centred; the button closer to the bottom).
843
+ *
844
+ * THE LINE is the shell's own shape, never the browser viewport (a 390 shell
845
+ * in a 1400 window is portrait): `orientation: landscape` with a 500 height
846
+ * cap. The cap keeps the rules off tablets, desktop windows and the iPhone
847
+ * Duo open full screen (626), which already have their own answers (#21,
848
+ * #VS-32, #F-37) and are wider than they are tall. 500 has slack above the
849
+ * 440 ceiling and under the 626 floor. Every rule below lives inside the one
850
+ * query, so outside it not one declaration exists and the portrait render is
851
+ * byte-identical. Exploration `face-capture-landscape` (Camila, 2026-10-20);
852
+ * the developer-facing list is LANDSCAPE.md. DEVIATIONS #F-39.
853
+ *
854
+ * What never changes, in either orientation: the H2, the CTA, the live
855
+ * instruction and where it lives (#12/#14), the status and verdict copy, and
856
+ * the tag's seat: bottom centre, Spacing/16 up, on every screen that has one. */
857
+ @container pcs-screen ((orientation: landscape) and (max-height: 500px)) {
858
+ /* The status-bar reserve STAYS (`--pcs-status-bar`, 47 by default, the
859
+ * host's own value otherwise). Round 2 had zeroed it on the argument that an
860
+ * iPhone draws no status bar in landscape; Camila's QA of the canvas
861
+ * (2026-10-21) found the result screens' status mark sitting in the system
862
+ * bar's row (the Lab's pills at 25 to 35, Elena's landscape reference keeps
863
+ * a 54 bar), which no device would allow. The reserve is the one line every
864
+ * screen already respects in portrait, so it is kept rather than replaced
865
+ * by a landscape-only offset. The capture screens are unaffected: their nav
866
+ * is hidden below and the ring is placed on the panel's height. */
867
+
868
+ /* Capture screens drop the nav row entirely. It is already reserved and
869
+ * empty on a feed-open capture screen (the 2026-08-18 header rule), and a
870
+ * 335 ring plus the 56 row does not fit in 375. A client logo opt-in goes
871
+ * with it in landscape: there is no row to hold it. */
872
+ .prizma-screen-shell:has(> .prizma-screen-layout__content--capture) > .prizma-screen-nav {
873
+ display: none;
874
+ }
875
+
876
+ /* …and the capture column stops reserving the footer under the ring: with
877
+ * the reserve the ring had 328 to sit in. (0,3,0), the reserve's own
878
+ * specificity, later in the file. FaceCaptureLayout.css says what happens
879
+ * to the tag there. */
880
+ .prizma-screen-shell .prizma-screen-layout__content.prizma-screen-layout__content--capture {
881
+ padding-bottom: 0;
882
+ }
883
+
884
+ /* ARCHETYPE A (tutorial). The illustration goes: it is the one element that
885
+ * carries no task information (step 1 of the order), and in landscape it is
886
+ * also the only one that cannot fit above a title block and a CTA at any
887
+ * size that still reads (Camila, 2026-10-21: "remove it to have more space,
888
+ * it's not adding essential information"). Its slot stays as the flexible
889
+ * middle, so the band keeps sitting at the bottom of the column, above the
890
+ * tag, where the portrait has it; the title block stays under the logo nav.
891
+ * Measured on the SE (375): nav 56, title block 77, disclaimer and CTA 105,
892
+ * the tag's reserve 62, Spacing/16 twice: 316, so the disclaimer stays too.
893
+ * Addressed by the archetype's own `--tutorial` class (TutorialScreen.tsx),
894
+ * not by `:has()` on the illustration slot: Core Lab's converter drops
895
+ * hidden subtrees before the scene is laid out, so a rule that needed the
896
+ * hidden art to match would vanish on the canvas. */
897
+ .prizma-screen-shell .prizma-screen-layout__content--tutorial {
898
+ /* The logo nav is gone in landscape (below), so the column reserves the
899
+ * status bar itself, Spacing/8 under it like the result screens. */
900
+ padding-top: calc(var(--pcs-status-bar, 47px) + var(--prizma-spacing-8));
901
+ /* The column centres its children (the band below is the one that is
902
+ * narrower than the column). `align-items` on the parent rather than
903
+ * `align-self` or `margin-inline: auto` on the band: Core Lab's converter
904
+ * reads a flex container's alignment and ignores per-child margins and
905
+ * `align-self`, so the first take drew the 420 band at the left edge on
906
+ * the canvas while the browser centred it (Camila, 2026-10-21). */
907
+ align-items: center;
908
+ }
909
+
910
+ /* Every other child keeps the full column: `align-items: center` would
911
+ * otherwise shrink the title block to its content. */
912
+ .prizma-screen-layout__content--tutorial > :not(.prizma-screen-layout__band) {
913
+ width: 100%;
914
+ }
915
+
916
+ .prizma-screen-layout__content--tutorial > .prizma-screen-layout__flex > [class*='prizma-illustration-'] {
917
+ display: none;
918
+ }
919
+
920
+ /* The slot that held the art keeps only its flexible height, not its
921
+ * Spacing/16 padding above and below: with the illustration gone those 32
922
+ * are empty, and on the SE they were what pushed the CTA onto the tag once
923
+ * the status bar came back (QA 2026-10-21). SE budget: nav 91, Spacing/16,
924
+ * title block 76, band 109, the tag's reserve 62: 354 of 375. */
925
+ .prizma-screen-shell .prizma-screen-layout__content--tutorial > .prizma-screen-layout__flex {
926
+ padding-block: 0;
927
+ }
928
+
929
+ /* The actions take Elena's landscape column: 420, centred (Core Lab
930
+ * recipes, landscape, read off the Document Capture reference 6701:8042).
931
+ * A CTA stretched across 796 reads as a bar, not a button; 420 is the
932
+ * Title's own max width, so the axis is one. The band's tag is anchored to
933
+ * the shell and already centres. */
934
+ /* (0,3,0) on purpose: Core Lab's device CSS caps `.prizma-screen-layout__band`
935
+ * at the panel's column with a (0,2,0) rule declared later, and at 844 that
936
+ * read as "no cap" on the canvas while the browser kept 420. */
937
+ .prizma-screen-shell .prizma-screen-layout__content--tutorial > .prizma-screen-layout__band {
938
+ width: 100%;
939
+ max-width: 420px; /* hardcoded — the actions column of Elena's landscape reference, equal to Core's Title max width */
940
+ gap: var(--prizma-spacing-8); /* Elena's first concession, the same rhythm as the result band below */
941
+ }
942
+
943
+ /* ARCHETYPE F (result with a CTA band). The logo nav goes instead of the
944
+ * status mark: the mark is the one thing that tells a warning from a
945
+ * failure, the logo is the client's presence and comes back on the next
946
+ * screen (Camila, 2026-10-21: remove the logo or the icon, keep the text,
947
+ * bring the button down). The title block centres in what is left above the
948
+ * band; the band keeps its portrait seat at the bottom, above the tag, with
949
+ * #F-37's compact rhythm already in force under 720. Measured on the SE:
950
+ * title block with its mark 153, band 101, the tag's reserve 62, Spacing/16:
951
+ * 332 in 375. The genericErrors family has no nav to begin with. */
952
+ .prizma-screen-shell:has(> .prizma-screen-layout__band--result) > .prizma-screen-nav {
953
+ display: none;
954
+ }
955
+
956
+ /* The title block sits at a FIXED offset under the status bar and the band
957
+ * at the bottom: Spacing/8 under the reserve clears the bar on both frames
958
+ * (mark at 55 with 47; Elena's bar ends at 54). Not `safe center`: the
959
+ * portrait column centres the title block in the free height, which on the
960
+ * SE left the mark 2.9 px inside the bar and 12.1 when the subtitle wrapped,
961
+ * and Core Lab's converter does not centre a column's children at all, so
962
+ * the canvas and the browser disagreed about where the mark was. A fixed
963
+ * seat is the same on both. SE budget, worst case (two-line subtitle):
964
+ * 55 + 132 + 16 + 109 + 62 = 374 of 375, with the band's gap conceded
965
+ * below. */
966
+ .prizma-screen-shell:has(> .prizma-screen-layout__band--result) > .prizma-screen-layout__content {
967
+ padding-top: calc(var(--pcs-status-bar, 47px) + var(--prizma-spacing-8));
968
+ justify-content: flex-start;
969
+ }
970
+
971
+ /* Same 420 column for the result band, centred. The band stays full width
972
+ * and centres its own rows (disclaimer, CTAs, extra content), each capped
973
+ * at 420: `align-items` on a flex column is what Core Lab's converter reads,
974
+ * while `margin-inline: auto` on the band, or `align-items` on the shell
975
+ * (whose anchored footer and tag it lays out by hand), centred in the
976
+ * browser and pinned the band left on the canvas (Camila, 2026-10-21).
977
+ * (0,3,0) for the same reason as the tutorial band above. */
978
+ .prizma-screen-shell:has(> .prizma-screen-layout__band--result) > .prizma-screen-layout__band--result {
979
+ width: 100%;
980
+ align-items: center;
981
+ box-sizing: border-box;
982
+ /* Elena's first concession (Core Lab recipes, landscape): gaps 16 to 8
983
+ * before anything else goes. It is what keeps the two-line result stack
984
+ * inside the 375 SE under the restored status bar. */
985
+ gap: var(--prizma-spacing-8);
986
+ }
987
+
988
+ .prizma-screen-shell:has(> .prizma-screen-layout__band--result) > .prizma-screen-layout__band--result > * {
989
+ width: 100%;
990
+ max-width: 420px; /* hardcoded — Elena's actions column, equal to Core's Title max width */
991
+ }
992
+
993
+ /* NO LOGO, NO TAG on any landscape screen (Camila, 2026-10-23: "they take
994
+ * up space that could be used by the content"). The nav row (the client
995
+ * logo's seat) and the verified tag go on every family; the capture screens
996
+ * had already dropped both. The columns that reserved the tag's 62 at the
997
+ * bottom keep Spacing/16 instead, and the result band sits Spacing/16 off
998
+ * the bottom; the capture column keeps its 0 so the ring's centre does not
999
+ * move. The status bar stays reserved by the columns themselves. */
1000
+ .prizma-screen-shell > .prizma-screen-nav,
1001
+ .prizma-screen-shell .prizma-screen-layout__footer,
1002
+ .prizma-screen-shell .prizma-screen-layout__band__tag {
1003
+ display: none;
1004
+ }
1005
+
1006
+ .prizma-screen-shell:has(.prizma-screen-layout__band__tag) .prizma-screen-layout__content:not(.prizma-screen-layout__content--capture) {
1007
+ padding-bottom: var(--prizma-spacing-16);
1008
+ }
1009
+
1010
+ .prizma-screen-shell:has(> .prizma-screen-layout__band--result) > .prizma-screen-layout__band--result {
1011
+ margin-bottom: var(--prizma-spacing-16);
1012
+ }
1013
+
1014
+ /* The band's disclaimer ("2 attempts remaining", "Stay still…") stays on
1015
+ * one line: in the 420 column every disclaimer the module ships fits, and
1016
+ * Core Lab's converter wrapped it to its first word otherwise (Camila,
1017
+ * 2026-10-23: "remaining attempts should always be in one line"). */
1018
+ .prizma-screen-layout__band .prizma-disclaimer__text {
1019
+ white-space: nowrap;
1020
+ }
1021
+ }
1022
+
832
1023
  /* ── Anchored-chrome layering ─────────────────────────────────────────────────
833
1024
  * The camera HUD bands lift themselves to `z-index: 1` so their copy paints
834
1025
  * above `PrizmaIdCapture`'s scrim (an absolutely positioned box-shadow that
@@ -25,6 +25,19 @@
25
25
  * layout paint` does both without forcing a composited layer the way
26
26
  * `transform` would. */
27
27
  contain: layout paint;
28
+ /* And it is the WINDOW Core's presenters size their overlays to. The Modal
29
+ * and Bottom sheet presenters draw their scrim `position: fixed` with
30
+ * `height: var(--prizma-overlay-height, 100dvh)` and pin the panel to the
31
+ * scrim's bottom, so a host whose fixed children land inside a box smaller
32
+ * than the viewport has to say so. Unsaid, the scrim inside this shell was
33
+ * the browser's height and the sheet sat at the browser's bottom edge, below
34
+ * the frame: on the closed iPhone Duo (678) Qualified Signature's certificate
35
+ * popups showed only the sheet's title peeking in at the bottom, on the open
36
+ * panel (626) nothing but scrim (measured 2026-10-29). 100% of the shell is
37
+ * the window, whatever the panel; on a real device the shell IS the viewport
38
+ * and the value reads the same. DEVIATIONS #F-41. */
39
+ --prizma-overlay-top: 0px;
40
+ --prizma-overlay-height: 100%;
28
41
  }
29
42
 
30
43
  /* Camera viewfinder screens — always a dark stage regardless of theme.
@@ -109,7 +109,10 @@ export const PrizmaTutorialScreen = forwardRef<HTMLDivElement, PrizmaTutorialScr
109
109
  {progress ? (
110
110
  <div className="prizma-screen-layout__progress">{progress}</div>
111
111
  ) : null}
112
- <div className="prizma-screen-layout__content">
112
+ {/* `--tutorial` names the archetype on the column so the landscape rule
113
+ in ScreenLayout.css (#F-39) can address it without `:has()` on the
114
+ illustration slot, which that rule hides. Portrait styles none of it. */}
115
+ <div className="prizma-screen-layout__content prizma-screen-layout__content--tutorial">
113
116
  <PrizmaTitle
114
117
  variant={overrides?.align === 'left' ? 'left' : 'centered'}
115
118
  title={title}
package/src/index.ts CHANGED
@@ -181,6 +181,48 @@ export {
181
181
  DigitalIdUploadFileTooLargeScreen,
182
182
  type DigitalIdUploadFileTooLargeScreenProps,
183
183
  } from './DigitalIdUploadFileTooLargeScreen';
184
+ // ── Digital ID Device Wallets ───────────────────────────────────────────────
185
+ // Digital IDs - Latest Design Figma file (u7N5Zq4bfOwiVbpKHKmSPE), section
186
+ // `iOS Apple Wallet` (node 26103:3689): the chooser with the wallet row and the
187
+ // generic error. The Apple Wallet sheet is OS chrome and not built — see
188
+ // DigitalIdDeviceWalletsStrings.ts header.
189
+ export {
190
+ type DigitalIdDeviceWalletsStrings,
191
+ type DigitalIdDeviceWalletsPlatformWallet,
192
+ type DigitalIdDeviceWalletsResultState,
193
+ type DigitalIdDeviceWalletsOption,
194
+ } from './DigitalIdDeviceWalletsStrings';
195
+ export {
196
+ DigitalIdDeviceWalletsDocumentSelectionScreen,
197
+ type DigitalIdDeviceWalletsDocumentSelectionScreenProps,
198
+ } from './DigitalIdDeviceWalletsDocumentSelectionScreen';
199
+ export {
200
+ DigitalIdDeviceWalletsResultScreen,
201
+ type DigitalIdDeviceWalletsResultScreenProps,
202
+ } from './DigitalIdDeviceWalletsResultScreen';
203
+ // ── Digital ID Providers ────────────────────────────────────────────────────
204
+ // Digital ID Providers - Latest Designs Figma file (q0wiyQkLXxGQ2jkPEutxR8):
205
+ // the chooser (27753:8099) and the web full flow (27707:9237) — Redirecting
206
+ // and five errors. The provider's own pages are OS chrome and not built; see
207
+ // DigitalIdProvidersStrings.ts header.
208
+ export {
209
+ type DigitalIdProvidersStrings,
210
+ type DigitalIdProvidersErrorState,
211
+ type DigitalIdProvidersOption,
212
+ type DigitalIdProvidersSelection,
213
+ } from './DigitalIdProvidersStrings';
214
+ export {
215
+ DigitalIdProvidersDocumentSelectionScreen,
216
+ type DigitalIdProvidersDocumentSelectionScreenProps,
217
+ } from './DigitalIdProvidersDocumentSelectionScreen';
218
+ export {
219
+ DigitalIdProvidersRedirectScreen,
220
+ type DigitalIdProvidersRedirectScreenProps,
221
+ } from './DigitalIdProvidersRedirectScreen';
222
+ export {
223
+ DigitalIdProvidersErrorScreen,
224
+ type DigitalIdProvidersErrorScreenProps,
225
+ } from './DigitalIdProvidersErrorScreen';
184
226
  // ── ID Capture ──────────────────────────────────────────────────────────────
185
227
  // Purple sections of the ID Capture Figma file (U1EOfl9RQ1rOlOIuQw3rhL, page
186
228
  // 16285:41282) — 100 spec sections across 13 parent groups, collapsed onto 12
package/src/styles.css CHANGED
@@ -16,6 +16,7 @@
16
16
  @import './CurpLayout.css';
17
17
  @import './DataSharingConsentLayout.css';
18
18
  @import './DemoScreensLayout.css';
19
+ @import './DigitalIdProvidersLayout.css';
19
20
  @import './DocumentCaptureLayout.css';
20
21
  @import './EkycLayout.css';
21
22
  @import './ElectronicSignatureLayout.css';