@incodetech/prizma-core-screens 0.2.18 → 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.
@@ -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;
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef } from 'react';
4
+ import { IconMobile, IconRotatePhone } from '@incodetech/prizma-core-icons';
5
+ import { PrizmaTitle } from '@incodetech/prizma-core-web/title';
6
+ import { PrizmaScreenShell } from './ScreenShell';
7
+ import './ScreenLayout.css';
8
+ import './RotateToPortraitScreen.css';
9
+ /**
10
+ * Rotate to portrait — the prompt a capture screen shows on a phone held
11
+ * sideways. Shared by every capture module; Face Capture is the first consumer.
12
+ *
13
+ * Capture screens ALWAYS stay portrait (Camila, 2026-11-03). While the person
14
+ * is on one (every `capturing-*` state, the extend-time offer, manual) and the
15
+ * phone is held sideways, the SDK closes the camera and shows this screen in
16
+ * its place; the camera reopens on its own when the phone is upright again.
17
+ * This is the "essential" exception of WCAG 2.1 SC 1.3.4 Orientation the
18
+ * accessibility auditors agreed to on 2026-10-26 (precedent: Bicycle Health
19
+ * accepted portrait-only capture). The sideways capture layout in
20
+ * `FaceCaptureLayout.css` (#F-39) stays as the longer-term full-compliance
21
+ * track and is never reached while this screen is shown.
22
+ *
23
+ * Anatomy: archetype C (Processing, GUIDELINES.md §6-C) under the landscape
24
+ * rules of `LANDSCAPE.md`: no logo nav, no verified tag, the status-bar
25
+ * reserve kept, the Title centred in what is left of the height, Core's own
26
+ * 420 max. Camera closed, so it is an ordinary screen on the app theme, like
27
+ * uploading (the 2026-08-18 rule): no theme pin. The screen is only ever shown
28
+ * in a landscape shell, but it renders the same centred stack in the portrait
29
+ * reference frame, which is how Latest designs shows every screen in its grid,
30
+ * so nothing here hides outside the landscape query.
31
+ *
32
+ * The glyph is variant A of the playground
33
+ * (`artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt`): Core's
34
+ * `mobile` bank icon turning about its centre from the way the phone is held
35
+ * to upright, with the arc its top edge travels drawn beside it and the
36
+ * arrowhead landing where the top ends up, at Title/Icon/Size (40, the status
37
+ * mark's box), Icon/Neutral/800, on Core's loop duration and standard easing.
38
+ * It rides the foundation's `__glyph` slot above the Title, the same seat the
39
+ * Wi-Fi mark takes on every noConnection screen (DEVIATIONS #10), because
40
+ * Core's `PrizmaTitle.icon` allowlist is `status | spinner | none`. Under
41
+ * `prefers-reduced-motion: reduce` it is Core's static `rotate-phone` icon
42
+ * (variant C). The glyph is decorative; the Title is a polite live region so
43
+ * a screen reader hears the headline when the prompt appears.
44
+ *
45
+ * No button: turning the phone is the action. DEVIATIONS #F-42.
46
+ */
47
+ export const RotateToPortraitScreen = forwardRef(({ strings, topEdge = 'left', screenId = 'rotate-to-portrait', overrides,
48
+ // No CTA here, so nothing ever fires it; destructured so it stays off
49
+ // the shell's DOM node, like `screenId`.
50
+ onAction, className, ...shell }, ref) => {
51
+ void screenId;
52
+ void onAction;
53
+ const left = overrides?.align === 'left';
54
+ return (_jsx(PrizmaScreenShell, { ref: ref, className: ['prizma-rotate-to-portrait', className].filter(Boolean).join(' '), ...shell, children: _jsxs("div", { className: "prizma-screen-layout__content prizma-screen-layout__content--centered prizma-screen-layout__content--no-nav prizma-rotate-to-portrait__content", children: [_jsx("span", { className: [
55
+ 'prizma-screen-layout__glyph',
56
+ 'prizma-screen-layout__glyph--neutral',
57
+ left ? 'prizma-screen-layout__glyph--left' : '',
58
+ ]
59
+ .filter(Boolean)
60
+ .join(' '), "aria-hidden": "true", children: _jsxs("span", { className: [
61
+ 'prizma-rotate-to-portrait__mark',
62
+ topEdge === 'right' ? 'prizma-rotate-to-portrait__mark--top-right' : '',
63
+ ]
64
+ .filter(Boolean)
65
+ .join(' '), children: [_jsxs("span", { className: "prizma-rotate-to-portrait__turn", children: [_jsxs("svg", { className: "prizma-rotate-to-portrait__arc", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { className: "prizma-rotate-to-portrait__arc-path", pathLength: 100, d: "M -2.5 12 A 14.5 14.5 0 0 1 12 -2.5" }), _jsx("path", { className: "prizma-rotate-to-portrait__arc-head", d: "M 9.7 -4.9 L 12 -2.5 L 9.7 -0.1" })] }), _jsx(IconMobile, { className: "prizma-rotate-to-portrait__phone" })] }), _jsx(IconRotatePhone, { className: "prizma-rotate-to-portrait__static" })] }) }), _jsx("div", { className: "prizma-rotate-to-portrait__status", role: "status", "aria-live": "polite", children: _jsx(PrizmaTitle, { variant: left ? 'left' : 'centered', title: strings.title, showSubtitle: Boolean(strings.subtitle), subtitle: strings.subtitle, icon: { type: 'none' } }) })] }) }));
66
+ });
67
+ RotateToPortraitScreen.displayName = 'RotateToPortraitScreen';
68
+ export default RotateToPortraitScreen;
@@ -0,0 +1,8 @@
1
+ export interface RotateToPortraitStrings {
2
+ /** H2 — "Turn your phone upright". */
3
+ title: string;
4
+ /** H5 — "Hold it vertically to continue with your verification."
5
+ * Optional: a host that only wants the headline omits it and the Title
6
+ * renders alone, Core's own `showSubtitle` toggle. */
7
+ subtitle?: string;
8
+ }
@@ -0,0 +1,20 @@
1
+ // Rotate to portrait — copy contract.
2
+ //
3
+ // NOT a module. The one prompt every capture module shows while the phone is
4
+ // held sideways (Face Capture today; ID Capture, Document Capture and Video
5
+ // Selfie as they adopt it). Capture screens stay portrait: this screen is one
6
+ // step before them, shown while the phone is sideways, and the camera opens
7
+ // once the phone is upright, so there is no button and no action in the contract
8
+ // (Camila, 2026-11-03; the auditors' WCAG 2.1 SC 1.3.4 reading of 2026-10-26,
9
+ // "essential" exception for the capture screens).
10
+ //
11
+ // The copy is module-agnostic on purpose: one sentence pair serves every
12
+ // capture module, so a host writes it once. Same rule as every other contract
13
+ // here: all copy via props, no hardcoded English (GUIDELINES.md §9).
14
+ //
15
+ // ⚠️ The English below the type is PROPOSED copy from the playground
16
+ // (artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt). Before this
17
+ // screen ships in the SDK the two strings must come from the SDK's own
18
+ // localization tables; nothing here is hand-translated, and no language is
19
+ // added to this file.
20
+ export {};
@@ -889,7 +889,10 @@
889
889
  * middle, so the band keeps sitting at the bottom of the column, above the
890
890
  * tag, where the portrait has it; the title block stays under the logo nav.
891
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.
892
+ * the tag's reserve 62, Spacing/16 twice: 316. The disclaimer fit, and
893
+ * stayed until 2026-11-03, when Camila took it off the landscape tutorial
894
+ * as well (below): with the art gone the line between the subtitle and the
895
+ * CTA read as a second subtitle, and the task is in the title block.
893
896
  * Addressed by the archetype's own `--tutorial` class (TutorialScreen.tsx),
894
897
  * not by `:has()` on the illustration slot: Core Lab's converter drops
895
898
  * hidden subtrees before the scene is laid out, so a rule that needed the
@@ -917,6 +920,12 @@
917
920
  display: none;
918
921
  }
919
922
 
923
+ /* The disclaimer goes with the art (Camila, 2026-11-03): title, subtitle,
924
+ * CTA. Portrait keeps it; outside the query this rule does not exist. */
925
+ .prizma-screen-layout__content--tutorial > .prizma-screen-layout__band > .prizma-disclaimer {
926
+ display: none;
927
+ }
928
+
920
929
  /* The slot that held the art keeps only its flexible height, not its
921
930
  * Spacing/16 padding above and below: with the illustration gone those 32
922
931
  * are empty, and on the SE they were what pushed the CTA onto the tag once
package/dist/index.d.ts CHANGED
@@ -166,6 +166,8 @@ export { type LoadersStrings, type LoadersVariant, type LoadersCopy } from './Lo
166
166
  export { LoaderScreen, type LoaderScreenProps } from './LoaderScreen';
167
167
  export { type ErrorScreensStrings, type ErrorScreensVariant, type ErrorScreensCopy, } from './ErrorScreensStrings';
168
168
  export { ErrorScreensScreen, type ErrorScreensScreenProps } from './ErrorScreensScreen';
169
+ export { type RotateToPortraitStrings } from './RotateToPortraitStrings';
170
+ export { RotateToPortraitScreen, type RotateToPortraitScreenProps, type RotateToPortraitTopEdge, } from './RotateToPortraitScreen';
169
171
  export { type DemoScreensStrings, type DemoScreensLanguage, type DemoScreensOption, } from './DemoScreensStrings';
170
172
  export { DemoScreensConfigScreen, type DemoScreensConfigScreenProps, type DemoScreensConfigState, } from './DemoScreensConfigScreen';
171
173
  export { DemoScreensTermsScreen, type DemoScreensTermsScreenProps, } from './DemoScreensTermsScreen';
package/dist/index.js CHANGED
@@ -145,6 +145,7 @@ export { PermissionsAllowScreen, } from './PermissionsAllowScreen';
145
145
  export { PermissionsBrowserStepsScreen, } from './PermissionsBrowserStepsScreen';
146
146
  export { LoaderScreen } from './LoaderScreen';
147
147
  export { ErrorScreensScreen } from './ErrorScreensScreen';
148
+ export { RotateToPortraitScreen, } from './RotateToPortraitScreen';
148
149
  export { DemoScreensConfigScreen, } from './DemoScreensConfigScreen';
149
150
  export { DemoScreensTermsScreen, } from './DemoScreensTermsScreen';
150
151
  export { DemoScreensLaunchScreen, } from './DemoScreensLaunchScreen';
package/dist/styles.css CHANGED
@@ -31,6 +31,7 @@
31
31
  @import './PhoneInputLayout.css';
32
32
  @import './QualifiedSignatureLayout.css';
33
33
  @import './ReusabilityLayout.css';
34
+ @import './RotateToPortraitScreen.css';
34
35
  @import './ScreenLayout.css';
35
36
  @import './ScreenNav.css';
36
37
  @import './ScreenShell.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incodetech/prizma-core-screens",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
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",
@@ -665,6 +665,11 @@
665
665
  "types": "./dist/ReviewScreen.d.ts",
666
666
  "import": "./dist/ReviewScreen.js"
667
667
  },
668
+ "./rotate-to-portrait-screen": {
669
+ "source": "./src/RotateToPortraitScreen.tsx",
670
+ "types": "./dist/RotateToPortraitScreen.d.ts",
671
+ "import": "./dist/RotateToPortraitScreen.js"
672
+ },
668
673
  "./screen-nav": {
669
674
  "source": "./src/ScreenNav.tsx",
670
675
  "types": "./dist/ScreenNav.d.ts",
@@ -795,6 +800,7 @@
795
800
  "./phone-input-layout.css": "./dist/PhoneInputLayout.css",
796
801
  "./qualified-signature-layout.css": "./dist/QualifiedSignatureLayout.css",
797
802
  "./reusability-layout.css": "./dist/ReusabilityLayout.css",
803
+ "./rotate-to-portrait-screen.css": "./dist/RotateToPortraitScreen.css",
798
804
  "./screen-layout.css": "./dist/ScreenLayout.css",
799
805
  "./screen-nav.css": "./dist/ScreenNav.css",
800
806
  "./screen-shell.css": "./dist/ScreenShell.css",
@@ -841,8 +847,8 @@
841
847
  },
842
848
  "dependencies": {
843
849
  "@incodetech/prizma-core-icons": "^0.4.0",
844
- "@incodetech/prizma-core-tokens": "^0.17.0",
845
- "@incodetech/prizma-core-web": "^0.17.0"
850
+ "@incodetech/prizma-core-tokens": "^0.18.0",
851
+ "@incodetech/prizma-core-web": "^0.18.0"
846
852
  },
847
853
  "devDependencies": {
848
854
  "@types/react": "^18.3.12",
@@ -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,162 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { IconMobile, IconRotatePhone } from '@incodetech/prizma-core-icons';
3
+ import { PrizmaTitle } from '@incodetech/prizma-core-web/title';
4
+ import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
5
+ import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
6
+ import type { RotateToPortraitStrings } from './RotateToPortraitStrings';
7
+ import './ScreenLayout.css';
8
+ import './RotateToPortraitScreen.css';
9
+
10
+ /**
11
+ * Where the phone's TOP edge points while it is held sideways. The SDK reads
12
+ * the device angle (−90 or +90) and the glyph turns the phone back upright
13
+ * from that side, so it always turns the way the person has to; the arc
14
+ * beside it mirrors with it (playground DECISION.md, round 2).
15
+ */
16
+ export type RotateToPortraitTopEdge = 'left' | 'right';
17
+
18
+ export interface RotateToPortraitScreenProps
19
+ extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
20
+ /** The prompt's copy. Shared by every capture module, see the contract. */
21
+ strings: RotateToPortraitStrings;
22
+ /** Which side the phone's top edge is on. Default `left`. */
23
+ topEdge?: RotateToPortraitTopEdge;
24
+ screenId?: string;
25
+ overrides?: PrizmaScreenOverrides;
26
+ /** Accepted for parity with every other screen; this one has no action.
27
+ * Turning the phone is the action, and the camera reopens on its own. */
28
+ onAction?: PrizmaScreenActionHandler;
29
+ }
30
+
31
+ /**
32
+ * Rotate to portrait — the prompt a capture screen shows on a phone held
33
+ * sideways. Shared by every capture module; Face Capture is the first consumer.
34
+ *
35
+ * Capture screens ALWAYS stay portrait (Camila, 2026-11-03). While the person
36
+ * is on one (every `capturing-*` state, the extend-time offer, manual) and the
37
+ * phone is held sideways, the SDK closes the camera and shows this screen in
38
+ * its place; the camera reopens on its own when the phone is upright again.
39
+ * This is the "essential" exception of WCAG 2.1 SC 1.3.4 Orientation the
40
+ * accessibility auditors agreed to on 2026-10-26 (precedent: Bicycle Health
41
+ * accepted portrait-only capture). The sideways capture layout in
42
+ * `FaceCaptureLayout.css` (#F-39) stays as the longer-term full-compliance
43
+ * track and is never reached while this screen is shown.
44
+ *
45
+ * Anatomy: archetype C (Processing, GUIDELINES.md §6-C) under the landscape
46
+ * rules of `LANDSCAPE.md`: no logo nav, no verified tag, the status-bar
47
+ * reserve kept, the Title centred in what is left of the height, Core's own
48
+ * 420 max. Camera closed, so it is an ordinary screen on the app theme, like
49
+ * uploading (the 2026-08-18 rule): no theme pin. The screen is only ever shown
50
+ * in a landscape shell, but it renders the same centred stack in the portrait
51
+ * reference frame, which is how Latest designs shows every screen in its grid,
52
+ * so nothing here hides outside the landscape query.
53
+ *
54
+ * The glyph is variant A of the playground
55
+ * (`artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt`): Core's
56
+ * `mobile` bank icon turning about its centre from the way the phone is held
57
+ * to upright, with the arc its top edge travels drawn beside it and the
58
+ * arrowhead landing where the top ends up, at Title/Icon/Size (40, the status
59
+ * mark's box), Icon/Neutral/800, on Core's loop duration and standard easing.
60
+ * It rides the foundation's `__glyph` slot above the Title, the same seat the
61
+ * Wi-Fi mark takes on every noConnection screen (DEVIATIONS #10), because
62
+ * Core's `PrizmaTitle.icon` allowlist is `status | spinner | none`. Under
63
+ * `prefers-reduced-motion: reduce` it is Core's static `rotate-phone` icon
64
+ * (variant C). The glyph is decorative; the Title is a polite live region so
65
+ * a screen reader hears the headline when the prompt appears.
66
+ *
67
+ * No button: turning the phone is the action. DEVIATIONS #F-42.
68
+ */
69
+ export const RotateToPortraitScreen = forwardRef<HTMLDivElement, RotateToPortraitScreenProps>(
70
+ (
71
+ {
72
+ strings,
73
+ topEdge = 'left',
74
+ screenId = 'rotate-to-portrait',
75
+ overrides,
76
+ // No CTA here, so nothing ever fires it; destructured so it stays off
77
+ // the shell's DOM node, like `screenId`.
78
+ onAction,
79
+ className,
80
+ ...shell
81
+ },
82
+ ref,
83
+ ) => {
84
+ void screenId;
85
+ void onAction;
86
+ const left = overrides?.align === 'left';
87
+ return (
88
+ <PrizmaScreenShell
89
+ ref={ref}
90
+ className={['prizma-rotate-to-portrait', className].filter(Boolean).join(' ')}
91
+ {...shell}
92
+ >
93
+ {/* No nav row at all: the prompt never shows a client logo (the
94
+ landscape rule of 2026-10-23), so the column carries the status-bar
95
+ reserve itself through the foundation's `--no-nav` class, the way
96
+ the genericErrors family does (DEVIATIONS #9). */}
97
+ <div className="prizma-screen-layout__content prizma-screen-layout__content--centered prizma-screen-layout__content--no-nav prizma-rotate-to-portrait__content">
98
+ <span
99
+ className={[
100
+ 'prizma-screen-layout__glyph',
101
+ 'prizma-screen-layout__glyph--neutral',
102
+ left ? 'prizma-screen-layout__glyph--left' : '',
103
+ ]
104
+ .filter(Boolean)
105
+ .join(' ')}
106
+ aria-hidden="true"
107
+ >
108
+ <span
109
+ className={[
110
+ 'prizma-rotate-to-portrait__mark',
111
+ topEdge === 'right' ? 'prizma-rotate-to-portrait__mark--top-right' : '',
112
+ ]
113
+ .filter(Boolean)
114
+ .join(' ')}
115
+ >
116
+ {/* Variant A: the turn. The arc is the one drawn addition, a
117
+ stroke at the bank's weight on the bank's 24 grid; the phone
118
+ is Core's `mobile` icon untouched, rotated by CSS. */}
119
+ <span className="prizma-rotate-to-portrait__turn">
120
+ <svg
121
+ className="prizma-rotate-to-portrait__arc"
122
+ viewBox="0 0 24 24"
123
+ fill="none"
124
+ xmlns="http://www.w3.org/2000/svg"
125
+ >
126
+ <path
127
+ className="prizma-rotate-to-portrait__arc-path"
128
+ pathLength={100}
129
+ d="M -2.5 12 A 14.5 14.5 0 0 1 12 -2.5"
130
+ />
131
+ <path
132
+ className="prizma-rotate-to-portrait__arc-head"
133
+ d="M 9.7 -4.9 L 12 -2.5 L 9.7 -0.1"
134
+ />
135
+ </svg>
136
+ <IconMobile className="prizma-rotate-to-portrait__phone" />
137
+ </span>
138
+ {/* Variant C: what A degrades to under reduced motion. */}
139
+ <IconRotatePhone className="prizma-rotate-to-portrait__static" />
140
+ </span>
141
+ </span>
142
+ {/* Core's Title sets `role="status"` itself only for the spinner
143
+ shape, and its own `role` attribute lands after the spread, so a
144
+ wrapper carries the live region here. Full width so the Title
145
+ centres in it exactly as it would in the column. */}
146
+ <div className="prizma-rotate-to-portrait__status" role="status" aria-live="polite">
147
+ <PrizmaTitle
148
+ variant={left ? 'left' : 'centered'}
149
+ title={strings.title}
150
+ showSubtitle={Boolean(strings.subtitle)}
151
+ subtitle={strings.subtitle}
152
+ icon={{ type: 'none' }}
153
+ />
154
+ </div>
155
+ </div>
156
+ </PrizmaScreenShell>
157
+ );
158
+ },
159
+ );
160
+ RotateToPortraitScreen.displayName = 'RotateToPortraitScreen';
161
+
162
+ export default RotateToPortraitScreen;
@@ -0,0 +1,28 @@
1
+ // Rotate to portrait — copy contract.
2
+ //
3
+ // NOT a module. The one prompt every capture module shows while the phone is
4
+ // held sideways (Face Capture today; ID Capture, Document Capture and Video
5
+ // Selfie as they adopt it). Capture screens stay portrait: this screen is one
6
+ // step before them, shown while the phone is sideways, and the camera opens
7
+ // once the phone is upright, so there is no button and no action in the contract
8
+ // (Camila, 2026-11-03; the auditors' WCAG 2.1 SC 1.3.4 reading of 2026-10-26,
9
+ // "essential" exception for the capture screens).
10
+ //
11
+ // The copy is module-agnostic on purpose: one sentence pair serves every
12
+ // capture module, so a host writes it once. Same rule as every other contract
13
+ // here: all copy via props, no hardcoded English (GUIDELINES.md §9).
14
+ //
15
+ // ⚠️ The English below the type is PROPOSED copy from the playground
16
+ // (artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt). Before this
17
+ // screen ships in the SDK the two strings must come from the SDK's own
18
+ // localization tables; nothing here is hand-translated, and no language is
19
+ // added to this file.
20
+
21
+ export interface RotateToPortraitStrings {
22
+ /** H2 — "Turn your phone upright". */
23
+ title: string;
24
+ /** H5 — "Hold it vertically to continue with your verification."
25
+ * Optional: a host that only wants the headline omits it and the Title
26
+ * renders alone, Core's own `showSubtitle` toggle. */
27
+ subtitle?: string;
28
+ }
@@ -889,7 +889,10 @@
889
889
  * middle, so the band keeps sitting at the bottom of the column, above the
890
890
  * tag, where the portrait has it; the title block stays under the logo nav.
891
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.
892
+ * the tag's reserve 62, Spacing/16 twice: 316. The disclaimer fit, and
893
+ * stayed until 2026-11-03, when Camila took it off the landscape tutorial
894
+ * as well (below): with the art gone the line between the subtitle and the
895
+ * CTA read as a second subtitle, and the task is in the title block.
893
896
  * Addressed by the archetype's own `--tutorial` class (TutorialScreen.tsx),
894
897
  * not by `:has()` on the illustration slot: Core Lab's converter drops
895
898
  * hidden subtrees before the scene is laid out, so a rule that needed the
@@ -917,6 +920,12 @@
917
920
  display: none;
918
921
  }
919
922
 
923
+ /* The disclaimer goes with the art (Camila, 2026-11-03): title, subtitle,
924
+ * CTA. Portrait keeps it; outside the query this rule does not exist. */
925
+ .prizma-screen-layout__content--tutorial > .prizma-screen-layout__band > .prizma-disclaimer {
926
+ display: none;
927
+ }
928
+
920
929
  /* The slot that held the art keeps only its flexible height, not its
921
930
  * Spacing/16 padding above and below: with the illustration gone those 32
922
931
  * are empty, and on the SE they were what pushed the CTA onto the tag once
package/src/index.ts CHANGED
@@ -690,6 +690,17 @@ export {
690
690
  } from './ErrorScreensStrings';
691
691
  export { ErrorScreensScreen, type ErrorScreensScreenProps } from './ErrorScreensScreen';
692
692
 
693
+ // Rotate to portrait — the one prompt every capture module shows while the
694
+ // phone is held sideways; capture screens stay portrait (Camila, 2026-11-03).
695
+ // Not in the generic Figma file: born in the playground
696
+ // `artefactos/playgrounds/2026-10-29__rotate-to-portrait-prompt`. DEVIATIONS #F-42.
697
+ export { type RotateToPortraitStrings } from './RotateToPortraitStrings';
698
+ export {
699
+ RotateToPortraitScreen,
700
+ type RotateToPortraitScreenProps,
701
+ type RotateToPortraitTopEdge,
702
+ } from './RotateToPortraitScreen';
703
+
693
704
  // Demo screens — the end screen; the start screen is owner-owned and pending.
694
705
  export {
695
706
  type DemoScreensStrings,
package/src/styles.css CHANGED
@@ -31,6 +31,7 @@
31
31
  @import './PhoneInputLayout.css';
32
32
  @import './QualifiedSignatureLayout.css';
33
33
  @import './ReusabilityLayout.css';
34
+ @import './RotateToPortraitScreen.css';
34
35
  @import './ScreenLayout.css';
35
36
  @import './ScreenNav.css';
36
37
  @import './ScreenShell.css';