@incodetech/prizma-core-screens 0.2.18 → 0.2.22
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.
- package/dist/IdCaptureStageScreen.js +15 -1
- package/dist/IdCaptureStrings.d.ts +5 -3
- package/dist/RotateToPortraitScreen.css +162 -0
- package/dist/RotateToPortraitScreen.d.ts +64 -0
- package/dist/RotateToPortraitScreen.js +68 -0
- package/dist/RotateToPortraitStrings.d.ts +8 -0
- package/dist/RotateToPortraitStrings.js +20 -0
- package/dist/ScreenLayout.css +10 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/styles.css +1 -0
- package/package.json +9 -3
- package/src/IdCaptureStageScreen.tsx +24 -1
- package/src/IdCaptureStrings.ts +5 -3
- package/src/RotateToPortraitScreen.css +162 -0
- package/src/RotateToPortraitScreen.tsx +162 -0
- package/src/RotateToPortraitStrings.ts +28 -0
- package/src/ScreenLayout.css +10 -1
- package/src/index.ts +11 -0
- package/src/styles.css +1 -0
|
@@ -11,6 +11,20 @@ import { PrizmaScreenShell } from './ScreenShell';
|
|
|
11
11
|
import './ScreenLayout.css';
|
|
12
12
|
import './ViewfinderScreen.css';
|
|
13
13
|
import './IdCaptureLayout.css';
|
|
14
|
+
/**
|
|
15
|
+
* The manual-mode instruction, in the shape the strings table gives it: a list
|
|
16
|
+
* of steps becomes a real `<ol>` on the Tooltip's content slot (screen readers
|
|
17
|
+
* announce "list, 2 items"); a plain string goes through `tooltipText` and
|
|
18
|
+
* keeps its own line breaks. Nothing set, no bubble.
|
|
19
|
+
*/
|
|
20
|
+
const manualTooltipProps = (copy) => {
|
|
21
|
+
if (Array.isArray(copy)) {
|
|
22
|
+
return {
|
|
23
|
+
tooltip: (_jsx("ol", { children: copy.map((step, index) => (_jsx("li", { children: step }, index))) })),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { tooltipText: copy };
|
|
27
|
+
};
|
|
14
28
|
/**
|
|
15
29
|
* Purple stage state → Core `PrizmaIdCapture` state. The mapping is 1:1 except
|
|
16
30
|
* `flash`, which Figma spells "flash" and Core spells `photo-flash`.
|
|
@@ -118,7 +132,7 @@ export const IdCaptureStageScreen = forwardRef(({ strings, documentType = 'id',
|
|
|
118
132
|
overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
|
|
119
133
|
]
|
|
120
134
|
.filter(Boolean)
|
|
121
|
-
.join(' '), children: [_jsx("div", { className: "id-capture-band id-capture-band--top", children: pillKind && pillCopy ? (_jsx("div", { className: "id-capture-stage__feedback", children: _jsx(PrizmaCaptureFeedback, { feedback: pillKind, title: pillCopy.title, subtitle: pillCopy.subtitle }) })) : (_jsxs(_Fragment, { children: [_jsx("h2", { className: "prizma-viewfinder__title", children: slice.title }), slice.instruction && !manual ? (_jsx("p", { className: "prizma-viewfinder__instruction", children: slice.instruction })) : null] })) }), _jsx(PrizmaIdCapture, { type: documentType, state: CAPTURE_STATE[state], isVertical: vertical, mode: "overlay", scrim: true, countdownValue: countdownValue, encryptedLabel: strings.encryptedLabel, announcement: strings.stage.stateLabels?.[state], still: still }), _jsx("div", { className: "id-capture-band", children: band ? _jsx("div", { className: "id-capture-stage__notice", children: band }) : null })] }), _jsx("div", { className: "prizma-screen-layout__capture-strip", children: _jsx(PrizmaCaptureManual, { className: "id-capture-stage__manual", variant: "id-capture", showButton: manual, showTooltip: manual, showHelpIcon: true,
|
|
135
|
+
.join(' '), children: [_jsx("div", { className: "id-capture-band id-capture-band--top", children: pillKind && pillCopy ? (_jsx("div", { className: "id-capture-stage__feedback", children: _jsx(PrizmaCaptureFeedback, { feedback: pillKind, title: pillCopy.title, subtitle: pillCopy.subtitle }) })) : (_jsxs(_Fragment, { children: [_jsx("h2", { className: "prizma-viewfinder__title", children: slice.title }), slice.instruction && !manual ? (_jsx("p", { className: "prizma-viewfinder__instruction", children: slice.instruction })) : null] })) }), _jsx(PrizmaIdCapture, { type: documentType, state: CAPTURE_STATE[state], isVertical: vertical, mode: "overlay", scrim: true, countdownValue: countdownValue, encryptedLabel: strings.encryptedLabel, announcement: strings.stage.stateLabels?.[state], still: still }), _jsx("div", { className: "id-capture-band", children: band ? _jsx("div", { className: "id-capture-stage__notice", children: band }) : null })] }), _jsx("div", { className: "prizma-screen-layout__capture-strip", children: _jsx(PrizmaCaptureManual, { className: "id-capture-stage__manual", variant: "id-capture", showButton: manual, showTooltip: manual, showHelpIcon: true, ...manualTooltipProps(manual ? strings.stage.manualTooltip : undefined), ariaLabel: strings.stage.captureLabel, helpLabel: strings.stage.helpLabel, onCapture: () => onAction?.({ type: 'cta', screenId }), onHelp: () => onAction?.({ type: 'help', screenId }) }) }), _jsx("div", { className: "prizma-screen-layout__footer", children: _jsx(PrizmaTag, { label: strings.verifiedLabel }) })] }));
|
|
122
136
|
});
|
|
123
137
|
IdCaptureStageScreen.displayName = 'IdCaptureStageScreen';
|
|
124
138
|
export default IdCaptureStageScreen;
|
|
@@ -92,9 +92,11 @@ export interface IdCaptureStrings {
|
|
|
92
92
|
title: string;
|
|
93
93
|
instruction?: string;
|
|
94
94
|
};
|
|
95
|
-
/** Manual mode — the tooltip bubble above the shutter
|
|
96
|
-
* ("
|
|
97
|
-
|
|
95
|
+
/** Manual mode — the tooltip bubble above the shutter. A list of steps
|
|
96
|
+
* renders as a numbered list (`["Fill the frame with your ID", "Press
|
|
97
|
+
* the button"]`), which keeps the list semantics for screen readers; a
|
|
98
|
+
* plain string renders as is, with `\n` breaking the line. */
|
|
99
|
+
manualTooltip?: string | string[];
|
|
98
100
|
/** Accessible name of the shutter button (manual mode). */
|
|
99
101
|
captureLabel?: string;
|
|
100
102
|
/** Accessible name of the "?" help control. */
|
|
@@ -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 {};
|
package/dist/ScreenLayout.css
CHANGED
|
@@ -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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@incodetech/prizma-core-screens",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
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.
|
|
845
|
-
"@incodetech/prizma-core-web": "^0.
|
|
850
|
+
"@incodetech/prizma-core-tokens": "^0.19.0",
|
|
851
|
+
"@incodetech/prizma-core-web": "^0.19.0"
|
|
846
852
|
},
|
|
847
853
|
"devDependencies": {
|
|
848
854
|
"@types/react": "^18.3.12",
|
|
@@ -21,6 +21,29 @@ import './ScreenLayout.css';
|
|
|
21
21
|
import './ViewfinderScreen.css';
|
|
22
22
|
import './IdCaptureLayout.css';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The manual-mode instruction, in the shape the strings table gives it: a list
|
|
26
|
+
* of steps becomes a real `<ol>` on the Tooltip's content slot (screen readers
|
|
27
|
+
* announce "list, 2 items"); a plain string goes through `tooltipText` and
|
|
28
|
+
* keeps its own line breaks. Nothing set, no bubble.
|
|
29
|
+
*/
|
|
30
|
+
const manualTooltipProps = (
|
|
31
|
+
copy: string | string[] | undefined,
|
|
32
|
+
): Pick<React.ComponentProps<typeof PrizmaCaptureManual>, 'tooltip' | 'tooltipText'> => {
|
|
33
|
+
if (Array.isArray(copy)) {
|
|
34
|
+
return {
|
|
35
|
+
tooltip: (
|
|
36
|
+
<ol>
|
|
37
|
+
{copy.map((step, index) => (
|
|
38
|
+
<li key={index}>{step}</li>
|
|
39
|
+
))}
|
|
40
|
+
</ol>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { tooltipText: copy };
|
|
45
|
+
};
|
|
46
|
+
|
|
24
47
|
/**
|
|
25
48
|
* Purple stage state → Core `PrizmaIdCapture` state. The mapping is 1:1 except
|
|
26
49
|
* `flash`, which Figma spells "flash" and Core spells `photo-flash`.
|
|
@@ -297,7 +320,7 @@ export const IdCaptureStageScreen = forwardRef<HTMLDivElement, IdCaptureStageScr
|
|
|
297
320
|
showButton={manual}
|
|
298
321
|
showTooltip={manual}
|
|
299
322
|
showHelpIcon
|
|
300
|
-
|
|
323
|
+
{...manualTooltipProps(manual ? strings.stage.manualTooltip : undefined)}
|
|
301
324
|
ariaLabel={strings.stage.captureLabel}
|
|
302
325
|
helpLabel={strings.stage.helpLabel}
|
|
303
326
|
onCapture={() => onAction?.({ type: 'cta', screenId })}
|
package/src/IdCaptureStrings.ts
CHANGED
|
@@ -116,9 +116,11 @@ export interface IdCaptureStrings {
|
|
|
116
116
|
front: { title: string; instruction?: string };
|
|
117
117
|
back: { title: string; instruction?: string };
|
|
118
118
|
passport: { title: string; instruction?: string };
|
|
119
|
-
/** Manual mode — the tooltip bubble above the shutter
|
|
120
|
-
* ("
|
|
121
|
-
|
|
119
|
+
/** Manual mode — the tooltip bubble above the shutter. A list of steps
|
|
120
|
+
* renders as a numbered list (`["Fill the frame with your ID", "Press
|
|
121
|
+
* the button"]`), which keeps the list semantics for screen readers; a
|
|
122
|
+
* plain string renders as is, with `\n` breaking the line. */
|
|
123
|
+
manualTooltip?: string | string[];
|
|
122
124
|
/** Accessible name of the shutter button (manual mode). */
|
|
123
125
|
captureLabel?: string;
|
|
124
126
|
/** Accessible name of the "?" help control. */
|
|
@@ -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
|
+
}
|
package/src/ScreenLayout.css
CHANGED
|
@@ -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
|
|
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