@incodetech/prizma-core-screens 0.2.14 → 0.2.20

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 (59) hide show
  1. package/dist/DigitalIdProvidersDocumentSelectionScreen.d.ts +3 -3
  2. package/dist/DigitalIdProvidersDocumentSelectionScreen.js +4 -3
  3. package/dist/DigitalIdProvidersLayout.css +0 -12
  4. package/dist/DigitalIdProvidersRedirectScreen.d.ts +2 -3
  5. package/dist/DigitalIdProvidersRedirectScreen.js +3 -4
  6. package/dist/DigitalIdProvidersStrings.d.ts +4 -0
  7. package/dist/FaceCaptureLayout.css +150 -0
  8. package/dist/FormScreen.css +35 -0
  9. package/dist/IdCaptureLayout.css +63 -0
  10. package/dist/RotateToPortraitScreen.css +162 -0
  11. package/dist/RotateToPortraitScreen.d.ts +64 -0
  12. package/dist/RotateToPortraitScreen.js +68 -0
  13. package/dist/RotateToPortraitStrings.d.ts +8 -0
  14. package/dist/RotateToPortraitStrings.js +20 -0
  15. package/dist/ScreenLayout.css +200 -0
  16. package/dist/ScreenShell.css +13 -0
  17. package/dist/TutorialScreen.js +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +1 -0
  20. package/dist/styles.css +1 -0
  21. package/package.json +9 -3
  22. package/samples/digital-id-providers/audkenni.png +0 -0
  23. package/samples/digital-id-providers/bankid-cz.png +0 -0
  24. package/samples/digital-id-providers/bankid-se.png +0 -0
  25. package/samples/digital-id-providers/ca-dmv-wallet.png +0 -0
  26. package/samples/digital-id-providers/digidentity.png +0 -0
  27. package/samples/digital-id-providers/diia.png +0 -0
  28. package/samples/digital-id-providers/edo.png +0 -0
  29. package/samples/digital-id-providers/eparaksts.png +0 -0
  30. package/samples/digital-id-providers/evrotrust.png +0 -0
  31. package/samples/digital-id-providers/freja.png +0 -0
  32. package/samples/digital-id-providers/ftn.png +0 -0
  33. package/samples/digital-id-providers/handy-signatur.png +0 -0
  34. package/samples/digital-id-providers/idin.png +0 -0
  35. package/samples/digital-id-providers/itsme.png +0 -0
  36. package/samples/digital-id-providers/mitid.png +0 -0
  37. package/samples/digital-id-providers/mobile-id.png +0 -0
  38. package/samples/digital-id-providers/mojeid.png +0 -0
  39. package/samples/digital-id-providers/oneid.png +0 -0
  40. package/samples/digital-id-providers/smart-id.png +0 -0
  41. package/samples/digital-id-providers/spid.png +0 -0
  42. package/samples/digital-id-providers/swissid.png +0 -0
  43. package/samples/digital-id-providers/uae-pass.png +0 -0
  44. package/samples/digital-id-providers/verimi.png +0 -0
  45. package/src/DigitalIdProvidersDocumentSelectionScreen.tsx +4 -3
  46. package/src/DigitalIdProvidersLayout.css +0 -12
  47. package/src/DigitalIdProvidersRedirectScreen.tsx +2 -9
  48. package/src/DigitalIdProvidersStrings.ts +4 -0
  49. package/src/FaceCaptureLayout.css +150 -0
  50. package/src/FormScreen.css +35 -0
  51. package/src/IdCaptureLayout.css +63 -0
  52. package/src/RotateToPortraitScreen.css +162 -0
  53. package/src/RotateToPortraitScreen.tsx +162 -0
  54. package/src/RotateToPortraitStrings.ts +28 -0
  55. package/src/ScreenLayout.css +200 -0
  56. package/src/ScreenShell.css +13 -0
  57. package/src/TutorialScreen.tsx +4 -1
  58. package/src/index.ts +11 -0
  59. package/src/styles.css +1 -0
@@ -22,9 +22,9 @@ export interface DigitalIdProvidersDocumentSelectionScreenProps extends Omit<Pri
22
22
  * Figma: `Document Chooser` (27753:8099) and the full list (27703:4895).
23
23
  *
24
24
  * Two of Core's card lists, each under a section label, after a "Viewing
25
- * options for" country line with Core's flag. Every row is Core's; the
26
- * providers carry Core's generic `digital-id-web` card icon until their logos
27
- * arrive from the backend catalog (DEVIATIONS #DIP-1), and the Identity Card
25
+ * options for" country line with Core's flag. Every row is Core's; a
26
+ * provider leads with its logo from the backend catalog, or Core's generic
27
+ * `digital-id-web` card icon when it has none (DEVIATIONS #DIP-1), and the Identity Card
28
28
  * row has no second line, which Core's row cannot draw (#DIP-3).
29
29
  */
30
30
  export declare const DigitalIdProvidersDocumentSelectionScreen: React.ForwardRefExoticComponent<DigitalIdProvidersDocumentSelectionScreenProps & React.RefAttributes<HTMLDivElement>>;
@@ -12,9 +12,9 @@ import './DigitalIdProvidersLayout.css';
12
12
  * Figma: `Document Chooser` (27753:8099) and the full list (27703:4895).
13
13
  *
14
14
  * Two of Core's card lists, each under a section label, after a "Viewing
15
- * options for" country line with Core's flag. Every row is Core's; the
16
- * providers carry Core's generic `digital-id-web` card icon until their logos
17
- * arrive from the backend catalog (DEVIATIONS #DIP-1), and the Identity Card
15
+ * options for" country line with Core's flag. Every row is Core's; a
16
+ * provider leads with its logo from the backend catalog, or Core's generic
17
+ * `digital-id-web` card icon when it has none (DEVIATIONS #DIP-1), and the Identity Card
18
18
  * row has no second line, which Core's row cannot draw (#DIP-3).
19
19
  */
20
20
  export const DigitalIdProvidersDocumentSelectionScreen = forwardRef(({ strings, providerCount, onOptionSelect, screenId = 'digital-id-providers-document-selection', overrides, onAction, ...shell }, ref) => {
@@ -29,6 +29,7 @@ export const DigitalIdProvidersDocumentSelectionScreen = forwardRef(({ strings,
29
29
  { icon: 'passport', title: copy.passportLabel, onClick: () => choose({ kind: 'passport' }) },
30
30
  ] })] }), _jsxs("section", { className: "digital-id-providers-section", children: [_jsx("p", { className: "digital-id-providers-section__label", children: copy.providersSectionLabel }), _jsx(PrizmaCardListVerificationOptions, { items: providers.map(provider => ({
31
31
  icon: 'digital-id-web',
32
+ logo: provider.logo,
32
33
  title: provider.label,
33
34
  onClick: () => choose({ kind: 'provider', providerId: provider.id }),
34
35
  })) })] })] }) }));
@@ -45,15 +45,3 @@
45
45
  line-height: var(--prizma-body-m-regular-line-height);
46
46
  letter-spacing: var(--prizma-body-m-regular-letter-spacing);
47
47
  }
48
-
49
- /* Redirecting — the spinner and its copy sit at the middle of the screen.
50
- * With the header hidden, archetype C still reserves the nav row above the
51
- * content and centres the column in what is left, so the block lands ~63px
52
- * low. Mirroring that reserve below — status bar, the nav's gap and height,
53
- * the column's own top padding (ScreenNav.css, ScreenLayout.css) — balances
54
- * it on the shell's midline. */
55
- .digital-id-providers-redirect .prizma-screen-layout__content--centered {
56
- padding-bottom: calc(
57
- var(--pcs-status-bar, 47px) + var(--prizma-spacing-12) + 44px + var(--prizma-spacing-24)
58
- );
59
- }
@@ -3,7 +3,6 @@ import { type PrizmaProcessingScreenProps } from './ProcessingScreen';
3
3
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
4
4
  import type { DigitalIdProvidersStrings } from './DigitalIdProvidersStrings';
5
5
  import './ScreenLayout.css';
6
- import './DigitalIdProvidersLayout.css';
7
6
  export interface DigitalIdProvidersRedirectScreenProps extends Omit<PrizmaProcessingScreenProps, 'status' | 'title' | 'subtitle' | 'verifiedLabel' | 'navLabel'> {
8
7
  /** Module copy — this screen reads the `redirecting` slice. */
9
8
  strings: DigitalIdProvidersStrings;
@@ -17,8 +16,8 @@ export interface DigitalIdProvidersRedirectScreenProps extends Omit<PrizmaProces
17
16
  * archetype (GUIDELINES.md §6-C). Figma: the `1-18-1-id-hold-on` instance in
18
17
  * the web full flow (27707:9281), with the module's own copy. The Web SDK's
19
18
  * `digitalIdRedirect` phases `loading` and `redirecting` both look like this.
20
- * The block is centred on the whole screen, not below the hidden nav's
21
- * reserved row (DigitalIdProvidersLayout.css).
19
+ * Built like Core's other processing screens: the archetype as-is, with the
20
+ * logo nav and the footer tag (DEVIATIONS #DIP-9).
22
21
  */
23
22
  export declare const DigitalIdProvidersRedirectScreen: React.ForwardRefExoticComponent<DigitalIdProvidersRedirectScreenProps & React.RefAttributes<HTMLDivElement>>;
24
23
  export default DigitalIdProvidersRedirectScreen;
@@ -3,16 +3,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { PrizmaProcessingScreen } from './ProcessingScreen';
5
5
  import './ScreenLayout.css';
6
- import './DigitalIdProvidersLayout.css';
7
6
  /**
8
7
  * Digital ID Providers — "Redirecting for verification…", the wait before the
9
8
  * browser leaves for the provider's site, on the `PrizmaProcessingScreen`
10
9
  * archetype (GUIDELINES.md §6-C). Figma: the `1-18-1-id-hold-on` instance in
11
10
  * the web full flow (27707:9281), with the module's own copy. The Web SDK's
12
11
  * `digitalIdRedirect` phases `loading` and `redirecting` both look like this.
13
- * The block is centred on the whole screen, not below the hidden nav's
14
- * reserved row (DigitalIdProvidersLayout.css).
12
+ * Built like Core's other processing screens: the archetype as-is, with the
13
+ * logo nav and the footer tag (DEVIATIONS #DIP-9).
15
14
  */
16
- export const DigitalIdProvidersRedirectScreen = forwardRef(({ strings, screenId = 'digital-id-providers-redirecting', showHeader = false, showFooter = false, overrides, onAction, className, ...shell }, ref) => (_jsx(PrizmaProcessingScreen, { ref: ref, status: "loading", title: strings.redirecting.title, subtitle: strings.redirecting.subtitle, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, showHeader: showHeader, showFooter: showFooter, screenId: screenId, overrides: overrides, onAction: onAction, className: ['digital-id-providers-redirect', className].filter(Boolean).join(' '), ...shell })));
15
+ export const DigitalIdProvidersRedirectScreen = forwardRef(({ strings, screenId = 'digital-id-providers-redirecting', overrides, onAction, ...shell }, ref) => (_jsx(PrizmaProcessingScreen, { ref: ref, status: "loading", title: strings.redirecting.title, subtitle: strings.redirecting.subtitle, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell })));
17
16
  DigitalIdProvidersRedirectScreen.displayName = 'DigitalIdProvidersRedirectScreen';
18
17
  export default DigitalIdProvidersRedirectScreen;
@@ -5,6 +5,10 @@ export interface DigitalIdProvidersOption {
5
5
  id: string;
6
6
  /** "iDIN", "itsme", "CA DMV Wallet"… */
7
7
  label: string;
8
+ /** The provider's logo tile (an image URL), from the backend catalog.
9
+ * Absent → Core's generic `digital-id-web` card icon. Samples ship under
10
+ * `samples/digital-id-providers/`. */
11
+ logo?: string;
8
12
  }
9
13
  /** What the person picked: a physical document, or a provider by its id. */
10
14
  export type DigitalIdProvidersSelection = {
@@ -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
+ }
@@ -0,0 +1,162 @@
1
+ /* Rotate to portrait — the prompt a capture screen shows on a phone held
2
+ * sideways (RotateToPortraitScreen.tsx, DEVIATIONS #F-42).
3
+ *
4
+ * ONLY what the foundation does not already own. The column, its centring,
5
+ * the glyph slot and its ink, the Title's own 16 / 12 rhythm and 420 cap all
6
+ * come from `ScreenLayout.css` and Core's Title. What is new here is the
7
+ * glyph's motion and the landscape seat of the column. Every value is a token
8
+ * except the bank's stroke weight, which is stated once and said so.
9
+ */
10
+
11
+ /* ── The live region wrapper ─────────────────────────────────────────────────
12
+ * Full width so Core's Title centres inside it exactly as it would as a direct
13
+ * child of the centred column (the Title is `width: 100%` capped at its own
14
+ * Title/Max-width and centres with `margin-inline: auto`). */
15
+ .prizma-rotate-to-portrait__status {
16
+ width: 100%;
17
+ }
18
+
19
+ /* ── The mark: Title/Icon/Size, the status mark's box ────────────────────────
20
+ * Sits inside the foundation's `__glyph` row, which centres it and sets the
21
+ * ink (`__glyph--neutral` → Icon/Neutral/800). Two layers share the box: the
22
+ * turn (variant A) and the static icon (variant C), one shown at a time. */
23
+ .prizma-rotate-to-portrait__mark {
24
+ position: relative;
25
+ display: block;
26
+ width: var(--prizma-title-icon-size);
27
+ height: var(--prizma-title-icon-size);
28
+ }
29
+
30
+ .prizma-rotate-to-portrait__turn {
31
+ position: absolute;
32
+ inset: 0;
33
+ display: block;
34
+ }
35
+
36
+ /* The arc layer and Core's `mobile` icon fill the box; the arc runs past the
37
+ * 24 grid on two sides (its radius is the phone's half diagonal plus the
38
+ * stroke), so the layer must not clip. The bank icon ships `width="24"`
39
+ * attributes; CSS sizes it to the box, as `__glyph > svg` does for the
40
+ * Wi-Fi mark. */
41
+ .prizma-rotate-to-portrait__turn > svg {
42
+ position: absolute;
43
+ inset: 0;
44
+ display: block;
45
+ width: 100%;
46
+ height: 100%;
47
+ overflow: visible;
48
+ }
49
+
50
+ /* The phone turns about its own centre (the icon's 12,12). At rest it is the
51
+ * way the phone is held: top edge to the left, −90. */
52
+ .prizma-rotate-to-portrait__phone {
53
+ transform-origin: 50% 50%;
54
+ transform: rotate(-90deg);
55
+ animation: prizma-rotate-to-portrait-turn var(--prizma-motion-duration-loop) linear infinite;
56
+ }
57
+
58
+ /* The arc the top edge travels, and the arrowhead where it lands. Drawn at
59
+ * the bank's own outline weight on the bank's 24 grid, so at Title/Icon/Size
60
+ * it reads as one stroke with the phone. Always the icon's own colour, never
61
+ * faded in or out (Camila, 2026-11-03: a colour that fades while the phone
62
+ * turns, and an arrowhead whose ink arrives late, is not the design). The arc
63
+ * is REVEALED by its dash, not by opacity; what is not drawn yet is hidden
64
+ * with `visibility`, which steps and never fades, and also hides the round
65
+ * cap a fully offset dash would otherwise show as a dot at the path's end. */
66
+ .prizma-rotate-to-portrait__arc-path,
67
+ .prizma-rotate-to-portrait__arc-head {
68
+ stroke: currentColor;
69
+ stroke-width: 1.8; /* hardcoded — the icon bank's outline stroke on its 24 grid (core/icons/bank); not a token */
70
+ stroke-linecap: round;
71
+ stroke-linejoin: round;
72
+ visibility: hidden;
73
+ }
74
+
75
+ .prizma-rotate-to-portrait__arc-path {
76
+ stroke-dasharray: 100;
77
+ stroke-dashoffset: 100;
78
+ animation: prizma-rotate-to-portrait-arc var(--prizma-motion-duration-loop) linear infinite;
79
+ }
80
+
81
+ .prizma-rotate-to-portrait__arc-head {
82
+ animation: prizma-rotate-to-portrait-head var(--prizma-motion-duration-loop) linear infinite;
83
+ }
84
+
85
+ /* Held the other way (top edge to the right, +90): the phone starts there and
86
+ * the arc mirrors about the box's centre, so the glyph turns the way the
87
+ * person has to. */
88
+ .prizma-rotate-to-portrait__mark--top-right .prizma-rotate-to-portrait__phone {
89
+ transform: rotate(90deg);
90
+ animation-name: prizma-rotate-to-portrait-turn-from-right;
91
+ }
92
+
93
+ .prizma-rotate-to-portrait__mark--top-right .prizma-rotate-to-portrait__arc {
94
+ transform: scaleX(-1);
95
+ }
96
+
97
+ /* One cycle is Motion/Duration/Loop (3200, the ID tutorial illustration's
98
+ * loop): hold at the held angle, turn on Easing/Standard while the arc draws
99
+ * under it, hold upright with the arrowhead on, then start again on a cut.
100
+ * Nothing fades: the phone keeps its ink the whole way, the arc appears as it
101
+ * is drawn and the arrowhead the moment the arc reaches it (Camila,
102
+ * 2026-11-03). The turn's percentages are the playground's, measured as the
103
+ * read Camila picked (round 2, 2026-10-29); the fade-out that used to mask
104
+ * the loop's reset is gone with the rest of the fades. */
105
+ @keyframes prizma-rotate-to-portrait-turn {
106
+ 0%, 15% { transform: rotate(-90deg); animation-timing-function: var(--prizma-motion-easing-standard); }
107
+ 55%, 100% { transform: rotate(0deg); }
108
+ }
109
+
110
+ @keyframes prizma-rotate-to-portrait-turn-from-right {
111
+ 0%, 15% { transform: rotate(90deg); animation-timing-function: var(--prizma-motion-easing-standard); }
112
+ 55%, 100% { transform: rotate(0deg); }
113
+ }
114
+
115
+ @keyframes prizma-rotate-to-portrait-arc {
116
+ 0%, 14.9% { stroke-dashoffset: 100; visibility: hidden; }
117
+ 15% { stroke-dashoffset: 100; visibility: visible; animation-timing-function: var(--prizma-motion-easing-standard); }
118
+ 55%, 100% { stroke-dashoffset: 0; visibility: visible; }
119
+ }
120
+
121
+ @keyframes prizma-rotate-to-portrait-head {
122
+ 0%, 54.9% { visibility: hidden; }
123
+ 55%, 100% { visibility: visible; }
124
+ }
125
+
126
+ /* ── Reduced motion: variant C ───────────────────────────────────────────────
127
+ * Core's static `rotate-phone` bank icon says the same thing without moving.
128
+ * Same media query every Core component with motion uses (Button, Capture
129
+ * manual, the ID feedback illustration). The static icon is a direct child of
130
+ * the mark, sized to the box like the turn. */
131
+ .prizma-rotate-to-portrait__static {
132
+ display: none;
133
+ width: 100%;
134
+ height: 100%;
135
+ }
136
+
137
+ @media (prefers-reduced-motion: reduce) {
138
+ .prizma-rotate-to-portrait__turn {
139
+ display: none;
140
+ }
141
+
142
+ .prizma-rotate-to-portrait__static {
143
+ display: block;
144
+ }
145
+ }
146
+
147
+ /* ── Landscape phones: the column's seat ─────────────────────────────────────
148
+ * The same line as every landscape rule (#F-39, LANDSCAPE.md). In portrait the
149
+ * column wears the foundation's `--no-nav` reserve (status bar + Spacing/24)
150
+ * and nothing is declared here. Sideways, the column takes the seat the
151
+ * result and tutorial columns take: Spacing/8 under the status-bar reserve
152
+ * (47 by default, the host's value otherwise) and Spacing/16 off the bottom,
153
+ * the playground's measured values. The Title then centres in what is left:
154
+ * glyph 40, Spacing/16, H2 28, Spacing/12, two lines of H5 37 = 133 in the
155
+ * 375 SE's 304. (0,2,0) so it outranks `--no-nav`'s own (0,1,0) regardless of
156
+ * import order. Outside the query not one declaration exists. */
157
+ @container pcs-screen ((orientation: landscape) and (max-height: 500px)) {
158
+ .prizma-screen-shell .prizma-rotate-to-portrait__content {
159
+ padding-top: calc(var(--pcs-status-bar, 47px) + var(--prizma-spacing-8));
160
+ padding-bottom: var(--prizma-spacing-16);
161
+ }
162
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { type PrizmaScreenShellProps } from './ScreenShell';
3
+ import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
4
+ import type { RotateToPortraitStrings } from './RotateToPortraitStrings';
5
+ import './ScreenLayout.css';
6
+ import './RotateToPortraitScreen.css';
7
+ /**
8
+ * Where the phone's TOP edge points while it is held sideways. The SDK reads
9
+ * the device angle (−90 or +90) and the glyph turns the phone back upright
10
+ * from that side, so it always turns the way the person has to; the arc
11
+ * beside it mirrors with it (playground DECISION.md, round 2).
12
+ */
13
+ export type RotateToPortraitTopEdge = 'left' | 'right';
14
+ export interface RotateToPortraitScreenProps extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
15
+ /** The prompt's copy. Shared by every capture module, see the contract. */
16
+ strings: RotateToPortraitStrings;
17
+ /** Which side the phone's top edge is on. Default `left`. */
18
+ topEdge?: RotateToPortraitTopEdge;
19
+ screenId?: string;
20
+ overrides?: PrizmaScreenOverrides;
21
+ /** Accepted for parity with every other screen; this one has no action.
22
+ * Turning the phone is the action, and the camera reopens on its own. */
23
+ onAction?: PrizmaScreenActionHandler;
24
+ }
25
+ /**
26
+ * Rotate to portrait — the prompt a capture screen shows on a phone held
27
+ * sideways. Shared by every capture module; Face Capture is the first consumer.
28
+ *
29
+ * Capture screens ALWAYS stay portrait (Camila, 2026-11-03). While the person
30
+ * is on one (every `capturing-*` state, the extend-time offer, manual) and the
31
+ * phone is held sideways, the SDK closes the camera and shows this screen in
32
+ * its place; the camera reopens on its own when the phone is upright again.
33
+ * This is the "essential" exception of WCAG 2.1 SC 1.3.4 Orientation the
34
+ * accessibility auditors agreed to on 2026-10-26 (precedent: Bicycle Health
35
+ * accepted portrait-only capture). The sideways capture layout in
36
+ * `FaceCaptureLayout.css` (#F-39) stays as the longer-term full-compliance
37
+ * track and is never reached while this screen is shown.
38
+ *
39
+ * Anatomy: archetype C (Processing, GUIDELINES.md §6-C) under the landscape
40
+ * rules of `LANDSCAPE.md`: no logo nav, no verified tag, the status-bar
41
+ * reserve kept, the Title centred in what is left of the height, Core's own
42
+ * 420 max. Camera closed, so it is an ordinary screen on the app theme, like
43
+ * uploading (the 2026-08-18 rule): no theme pin. The screen is only ever shown
44
+ * in a landscape shell, but it renders the same centred stack in the portrait
45
+ * reference frame, which is how Latest designs shows every screen in its grid,
46
+ * so nothing here hides outside the landscape query.
47
+ *
48
+ * The glyph is variant A of the playground
49
+ * (`artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt`): Core's
50
+ * `mobile` bank icon turning about its centre from the way the phone is held
51
+ * to upright, with the arc its top edge travels drawn beside it and the
52
+ * arrowhead landing where the top ends up, at Title/Icon/Size (40, the status
53
+ * mark's box), Icon/Neutral/800, on Core's loop duration and standard easing.
54
+ * It rides the foundation's `__glyph` slot above the Title, the same seat the
55
+ * Wi-Fi mark takes on every noConnection screen (DEVIATIONS #10), because
56
+ * Core's `PrizmaTitle.icon` allowlist is `status | spinner | none`. Under
57
+ * `prefers-reduced-motion: reduce` it is Core's static `rotate-phone` icon
58
+ * (variant C). The glyph is decorative; the Title is a polite live region so
59
+ * a screen reader hears the headline when the prompt appears.
60
+ *
61
+ * No button: turning the phone is the action. DEVIATIONS #F-42.
62
+ */
63
+ export declare const RotateToPortraitScreen: React.ForwardRefExoticComponent<RotateToPortraitScreenProps & React.RefAttributes<HTMLDivElement>>;
64
+ export default RotateToPortraitScreen;