@incodetech/prizma-core-screens 0.2.22 → 0.2.26
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/DemoScreensConfigSheetScreen.js +1 -1
- package/dist/DocumentSignatureLayout.css +160 -0
- package/dist/DocumentSignatureReviewScreen.d.ts +88 -0
- package/dist/DocumentSignatureReviewScreen.js +274 -0
- package/dist/DocumentSignatureStrings.d.ts +82 -0
- package/dist/DocumentSignatureStrings.js +39 -0
- package/dist/ElectronicSignatureDrawScreen.d.ts +17 -2
- package/dist/ElectronicSignatureDrawScreen.js +60 -12
- package/dist/ElectronicSignatureLayout.css +32 -0
- package/dist/ElectronicSignatureStrings.d.ts +26 -1
- package/dist/FaceCaptureCapturingScreen.js +1 -1
- package/dist/IdCaptureStageScreen.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/styles.css +1 -0
- package/package.json +9 -3
- package/src/DemoScreensConfigSheetScreen.tsx +1 -1
- package/src/DocumentSignatureLayout.css +160 -0
- package/src/DocumentSignatureReviewScreen.tsx +517 -0
- package/src/DocumentSignatureStrings.ts +122 -0
- package/src/ElectronicSignatureDrawScreen.tsx +143 -19
- package/src/ElectronicSignatureLayout.css +32 -0
- package/src/ElectronicSignatureStrings.ts +28 -1
- package/src/FaceCaptureCapturingScreen.tsx +0 -1
- package/src/IdCaptureStageScreen.tsx +0 -1
- package/src/index.ts +18 -1
- package/src/styles.css +1 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// from the `read-and-sign-in-one-screen` exploration
|
|
2
|
+
import React, {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { PrizmaBottomSheetContent } from '@incodetech/prizma-core-web/bottom-sheet';
|
|
13
|
+
import { PrizmaScrollPill, PrizmaScrollPillDock } from '@incodetech/prizma-core-web/scroll-pill';
|
|
14
|
+
import { PrizmaSegmentedControl } from '@incodetech/prizma-core-web/segmented-control';
|
|
15
|
+
import { PrizmaSignaturePad } from '@incodetech/prizma-core-web/signature-pad';
|
|
16
|
+
import { PrizmaSignaturePreview } from '@incodetech/prizma-core-web/signature-preview';
|
|
17
|
+
import {
|
|
18
|
+
PrizmaStepper,
|
|
19
|
+
type PrizmaStepperState,
|
|
20
|
+
type PrizmaStepperSteps,
|
|
21
|
+
} from '@incodetech/prizma-core-web/stepper';
|
|
22
|
+
import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
|
|
23
|
+
import { PrizmaFormScreen, type PrizmaFormScreenProps } from './FormScreen';
|
|
24
|
+
import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
|
|
25
|
+
import type { DocumentSignatureState, DocumentSignatureStrings } from './DocumentSignatureStrings';
|
|
26
|
+
import './ScreenLayout.css';
|
|
27
|
+
import './ElectronicSignatureLayout.css';
|
|
28
|
+
import './DocumentSignatureLayout.css';
|
|
29
|
+
|
|
30
|
+
/** One document to read and sign. Runtime DATA the host supplies, never copy
|
|
31
|
+
* the design system owns (the same slot contract as Electronic Signature's
|
|
32
|
+
* `document`). `content` is plain `<h4>` / `<p>` markup: the viewer's frame
|
|
33
|
+
* supplies the typography. */
|
|
34
|
+
export interface DocumentSignatureDocument {
|
|
35
|
+
title: string;
|
|
36
|
+
content: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What the person signed with, reported on Confirm and continue. */
|
|
40
|
+
export type DocumentSignatureSignature = { method: 'draw' } | { method: 'type'; name: string };
|
|
41
|
+
|
|
42
|
+
export interface DocumentSignatureReviewScreenProps
|
|
43
|
+
extends Omit<
|
|
44
|
+
PrizmaFormScreenProps,
|
|
45
|
+
| 'progress'
|
|
46
|
+
| 'title'
|
|
47
|
+
| 'subtitle'
|
|
48
|
+
| 'ctaLabel'
|
|
49
|
+
| 'ctaDisabled'
|
|
50
|
+
| 'ctaLoading'
|
|
51
|
+
| 'ctaLoadingLabel'
|
|
52
|
+
| 'linkLabel'
|
|
53
|
+
| 'linkDisabled'
|
|
54
|
+
| 'linkContent'
|
|
55
|
+
| 'verifiedLabel'
|
|
56
|
+
| 'navLabel'
|
|
57
|
+
| 'children'
|
|
58
|
+
| 'fieldsOffset'
|
|
59
|
+
| 'fieldsGap'
|
|
60
|
+
| 'scrollFields'
|
|
61
|
+
| 'fieldsAlign'
|
|
62
|
+
| 'overlay'
|
|
63
|
+
> {
|
|
64
|
+
/** Module copy. */
|
|
65
|
+
strings: DocumentSignatureStrings;
|
|
66
|
+
/** Every document to sign, in reading order. One or more. */
|
|
67
|
+
documents: DocumentSignatureDocument[];
|
|
68
|
+
/** The pose to draw (and, when `interactive`, the pose to start from). */
|
|
69
|
+
state?: DocumentSignatureState;
|
|
70
|
+
/** The name in the field for `state="type"`. */
|
|
71
|
+
typedName?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Run the screen: the Stepper follows the scroll, the Scroll pill moves the
|
|
74
|
+
* viewer on, reaching the end of the last document turns Sign on AND opens
|
|
75
|
+
* the sheet (once), Sign reopens it after a dismiss, the Draw / Type switch
|
|
76
|
+
* clears the other mode, and Confirm and continue reports the signature. Off (the default), every pose is fixed by
|
|
77
|
+
* `state`, so a spec card always renders the same frame.
|
|
78
|
+
*/
|
|
79
|
+
interactive?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* `PrizmaSignaturePad.onCanvasReady`: the host's drawing engine, Core's
|
|
82
|
+
* documented division of labour (the pad ships the chrome, not the ink). For
|
|
83
|
+
* `state="drawn"` a spec host paints a specimen stroke here.
|
|
84
|
+
*/
|
|
85
|
+
onCanvasReady?: (canvas: HTMLCanvasElement) => void;
|
|
86
|
+
/** Confirm and continue was pressed with a signature on the sheet. */
|
|
87
|
+
onSignatureConfirm?: (signature: DocumentSignatureSignature) => void;
|
|
88
|
+
screenId?: string;
|
|
89
|
+
overrides?: PrizmaScreenOverrides;
|
|
90
|
+
/** Emits `cta` on Confirm and continue. Sign is handled inside the screen. */
|
|
91
|
+
onAction?: PrizmaScreenActionHandler;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type Sheet = null | 'draw' | 'type';
|
|
95
|
+
|
|
96
|
+
const POSE: Record<DocumentSignatureState, { at: 'start' | 'second' | 'end'; sheet: Sheet }> = {
|
|
97
|
+
start: { at: 'start', sheet: null },
|
|
98
|
+
second: { at: 'second', sheet: null },
|
|
99
|
+
end: { at: 'end', sheet: null },
|
|
100
|
+
draw: { at: 'end', sheet: 'draw' },
|
|
101
|
+
drawn: { at: 'end', sheet: 'draw' },
|
|
102
|
+
type: { at: 'end', sheet: 'type' },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Core's Stepper is authored for 2 to 7 segments. */
|
|
106
|
+
const MAX_STEPS = 7;
|
|
107
|
+
|
|
108
|
+
/** One tap on the Scroll pill: about a page, never straight to the end
|
|
109
|
+
* (Core's documented action for the pill, `ScrollPill.tsx`). */
|
|
110
|
+
const PILL_STEP = 0.8;
|
|
111
|
+
|
|
112
|
+
const count = (template: string, current: number, total: number) =>
|
|
113
|
+
template.replace('{{current}}', String(current)).replace('{{total}}', String(total));
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Document Electronic Signature — read every document and sign them once, on
|
|
117
|
+
* ONE screen. Option B of the `read-and-sign-in-one-screen` exploration,
|
|
118
|
+
* decided with the owner (Elena, 2026-11-12), built on the Form archetype
|
|
119
|
+
* (GUIDELINES.md §6-E, `PrizmaFormScreen`).
|
|
120
|
+
*
|
|
121
|
+
* Core only, used as shipped:
|
|
122
|
+
*
|
|
123
|
+
* - `PrizmaFormScreen`: logo nav, "Review and sign", the Sign CTA (disabled
|
|
124
|
+
* until the reader reaches the end of the last document: consent needs
|
|
125
|
+
* proof the documents were read) and the verified tag.
|
|
126
|
+
* - `PrizmaStepper`, one segment per document, following the one being read,
|
|
127
|
+
* `Completed` once everything is read, with its `caption` ("Document 1 of
|
|
128
|
+
* 2", then "All documents read") naming the bar.
|
|
129
|
+
* - Every document in ONE scrolling viewer, Electronic Signature's own frame
|
|
130
|
+
* (`.electronic-signature-document`), one after the other with no divider:
|
|
131
|
+
* the count lives on the Stepper. One document: no Stepper.
|
|
132
|
+
* - `PrizmaScrollPill` ("Scroll to sign") in its `PrizmaScrollPillDock`, the
|
|
133
|
+
* viewer's last child, while there is reading left. A tap scrolls about 80%
|
|
134
|
+
* of the viewer; it unmounts once the end is reached.
|
|
135
|
+
* - `PrizmaBottomSheetContent` (dialog), "Add your signature": the consent as
|
|
136
|
+
* its subtitle; the body opens with `PrizmaSegmentedControl` (Draw / Type),
|
|
137
|
+
* then `PrizmaSignaturePad`, or `PrizmaTextField` with
|
|
138
|
+
* `PrizmaSignaturePreview` under it; its Button group holds only Confirm
|
|
139
|
+
* and continue, bottom bar on.
|
|
140
|
+
*
|
|
141
|
+
* The sheet rides the archetype's `overlay` slot (scrim + panel from
|
|
142
|
+
* `ScreenLayout.css`, the Reusability / Biometric Consent contract), not
|
|
143
|
+
* `PrizmaBottomSheet`'s presenter: the presenter portals to `document.body`,
|
|
144
|
+
* which escapes the 390 x 844 frame a spec card and a prototype draw in.
|
|
145
|
+
*
|
|
146
|
+
* Nothing new: the only module CSS is layout glue on Core tokens
|
|
147
|
+
* (`DocumentSignatureLayout.css`).
|
|
148
|
+
*/
|
|
149
|
+
export const DocumentSignatureReviewScreen = forwardRef<
|
|
150
|
+
HTMLDivElement,
|
|
151
|
+
DocumentSignatureReviewScreenProps
|
|
152
|
+
>(
|
|
153
|
+
(
|
|
154
|
+
{
|
|
155
|
+
strings,
|
|
156
|
+
documents,
|
|
157
|
+
state = 'start',
|
|
158
|
+
typedName = '',
|
|
159
|
+
interactive = false,
|
|
160
|
+
onCanvasReady,
|
|
161
|
+
onSignatureConfirm,
|
|
162
|
+
screenId = 'document-electronic-signature-review',
|
|
163
|
+
overrides,
|
|
164
|
+
onAction,
|
|
165
|
+
className,
|
|
166
|
+
...shell
|
|
167
|
+
},
|
|
168
|
+
ref,
|
|
169
|
+
) => {
|
|
170
|
+
const pose = POSE[state];
|
|
171
|
+
const total = documents.length;
|
|
172
|
+
const many = total > 1;
|
|
173
|
+
const { review, sign } = strings;
|
|
174
|
+
|
|
175
|
+
const [read, setRead] = useState(pose.at === 'end');
|
|
176
|
+
const [current, setCurrent] = useState(pose.at === 'start' ? 1 : Math.min(2, total));
|
|
177
|
+
const [sheet, setSheet] = useState<Sheet>(pose.sheet);
|
|
178
|
+
// Motion (Elena, 2026-11-13: "refinar el motion cuando llego abajo y
|
|
179
|
+
// aparece el pop up"). Interactive only; a static spec card never moves.
|
|
180
|
+
// `closing` keeps the sheet mounted while it slides out. Every duration is
|
|
181
|
+
// read from Core's Sheet/Motion tokens, never typed here.
|
|
182
|
+
const [closing, setClosing] = useState(false);
|
|
183
|
+
const sheetMs = (which: 'enter' | 'exit') => {
|
|
184
|
+
if (typeof window === 'undefined') return 0;
|
|
185
|
+
if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return 0;
|
|
186
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue(`--prizma-sheet-motion-${which}-duration`).trim();
|
|
187
|
+
const n = parseFloat(v);
|
|
188
|
+
return Number.isFinite(n) ? (v.endsWith('ms') ? n : n * 1000) : 0;
|
|
189
|
+
};
|
|
190
|
+
const [mode, setMode] = useState<'draw' | 'type'>(pose.sheet === 'type' ? 'type' : 'draw');
|
|
191
|
+
const [inked, setInked] = useState(state === 'drawn');
|
|
192
|
+
const [name, setName] = useState(state === 'type' ? typedName : '');
|
|
193
|
+
const [padKey, setPadKey] = useState(0);
|
|
194
|
+
const viewer = useRef<HTMLDivElement | null>(null);
|
|
195
|
+
const panel = useRef<HTMLDivElement | null>(null);
|
|
196
|
+
const panelId = useId();
|
|
197
|
+
|
|
198
|
+
// A new pose from the host restarts the screen from it.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
setRead(pose.at === 'end');
|
|
201
|
+
setCurrent(pose.at === 'start' ? 1 : pose.at === 'second' ? Math.min(2, total) : total);
|
|
202
|
+
setSheet(pose.sheet);
|
|
203
|
+
setMode(pose.sheet === 'type' ? 'type' : 'draw');
|
|
204
|
+
setInked(state === 'drawn');
|
|
205
|
+
setName(state === 'type' ? typedName : '');
|
|
206
|
+
}, [state, typedName, total, pose.at, pose.sheet]);
|
|
207
|
+
|
|
208
|
+
// Which document is being read (the one crossing the top third of the
|
|
209
|
+
// viewer), and whether the end of the last one was reached. Offsets, not
|
|
210
|
+
// bounding boxes: a spec card scales the frame with a CSS transform, and
|
|
211
|
+
// the viewer is the sections' offset parent (DocumentSignatureLayout.css).
|
|
212
|
+
const measure = useCallback(() => {
|
|
213
|
+
const v = viewer.current;
|
|
214
|
+
// A viewer that has not laid out yet reads as "already at the end" and
|
|
215
|
+
// would unlock Sign before anything was read.
|
|
216
|
+
if (!v || !interactive || v.clientHeight < 40) return;
|
|
217
|
+
let at = 1;
|
|
218
|
+
v.querySelectorAll<HTMLElement>('[data-doc]').forEach((d, i) => {
|
|
219
|
+
if (d.offsetTop - v.scrollTop <= v.clientHeight * 0.3) at = i + 1;
|
|
220
|
+
});
|
|
221
|
+
setCurrent(at);
|
|
222
|
+
if (v.scrollTop + v.clientHeight >= v.scrollHeight - 2) {
|
|
223
|
+
// Reaching the end opens the sheet by itself, once (Elena,
|
|
224
|
+
// 2026-11-12); Sign reopens it after a dismiss.
|
|
225
|
+
// A beat first, one Sheet/Motion/Enter long: the scroll settles and
|
|
226
|
+
// the Stepper's "All documents read" lands before the sheet rises, so
|
|
227
|
+
// the two changes read one after the other instead of at once.
|
|
228
|
+
setRead((was) => {
|
|
229
|
+
if (!was) window.setTimeout(() => setSheet((cur) => cur ?? mode), sheetMs('enter'));
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}, [interactive, mode]);
|
|
234
|
+
|
|
235
|
+
// Place the viewer for the pose: the top, the start of document 2, or the end.
|
|
236
|
+
useLayoutEffect(() => {
|
|
237
|
+
const v = viewer.current;
|
|
238
|
+
if (!v) return;
|
|
239
|
+
if (pose.at === 'end') v.scrollTop = v.scrollHeight;
|
|
240
|
+
else if (pose.at === 'second') {
|
|
241
|
+
const d2 = v.querySelector<HTMLElement>('[data-doc="2"]');
|
|
242
|
+
v.scrollTop = d2 ? d2.offsetTop : 0;
|
|
243
|
+
} else v.scrollTop = 0;
|
|
244
|
+
}, [pose.at, total]);
|
|
245
|
+
|
|
246
|
+
// Measure again once the viewer has its real height.
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
const v = viewer.current;
|
|
249
|
+
if (!v || !interactive || typeof ResizeObserver === 'undefined') return;
|
|
250
|
+
const ro = new ResizeObserver(() => measure());
|
|
251
|
+
ro.observe(v);
|
|
252
|
+
return () => ro.disconnect();
|
|
253
|
+
}, [interactive, measure]);
|
|
254
|
+
|
|
255
|
+
// Focus moves into the sheet when it opens (it is a modal dialog).
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (interactive && sheet) panel.current?.focus();
|
|
258
|
+
}, [interactive, sheet]);
|
|
259
|
+
|
|
260
|
+
const ready = mode === 'draw' ? inked : name.trim().length > 1;
|
|
261
|
+
|
|
262
|
+
// Sign reopens it the same way the end of the documents opens it (Elena,
|
|
263
|
+
// 2026-11-13): the same Sheet/Motion/Enter beat, then the same rise.
|
|
264
|
+
const openSheet = () => {
|
|
265
|
+
if (!interactive || !read) return;
|
|
266
|
+
window.setTimeout(() => setSheet((cur) => cur ?? mode), sheetMs('enter'));
|
|
267
|
+
};
|
|
268
|
+
// Close with Core's exit curve: the sheet slides down, then unmounts.
|
|
269
|
+
const close = () => {
|
|
270
|
+
const ms = sheetMs('exit');
|
|
271
|
+
if (!ms) {
|
|
272
|
+
setSheet(null);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
setClosing(true);
|
|
276
|
+
window.setTimeout(() => {
|
|
277
|
+
setSheet(null);
|
|
278
|
+
setClosing(false);
|
|
279
|
+
}, ms);
|
|
280
|
+
};
|
|
281
|
+
const dismiss = () => {
|
|
282
|
+
if (!interactive) return;
|
|
283
|
+
close();
|
|
284
|
+
viewer.current?.focus();
|
|
285
|
+
};
|
|
286
|
+
const confirm = () => {
|
|
287
|
+
if (!ready) return;
|
|
288
|
+
onSignatureConfirm?.(mode === 'draw' ? { method: 'draw' } : { method: 'type', name: name.trim() });
|
|
289
|
+
onAction?.({ type: 'cta', screenId });
|
|
290
|
+
if (interactive) close();
|
|
291
|
+
};
|
|
292
|
+
// Switching modes clears what the other mode held (Elena, 2026-11-12:
|
|
293
|
+
// "clear on switch"), so Confirm only turns on for the signature on screen.
|
|
294
|
+
const switchMode = (next: 'draw' | 'type') => {
|
|
295
|
+
if (!interactive || next === mode) return;
|
|
296
|
+
setPadKey((k) => k + 1);
|
|
297
|
+
setInked(false);
|
|
298
|
+
setName('');
|
|
299
|
+
setMode(next);
|
|
300
|
+
setSheet(next);
|
|
301
|
+
};
|
|
302
|
+
const clear = () => {
|
|
303
|
+
if (!interactive) return;
|
|
304
|
+
setPadKey((k) => k + 1);
|
|
305
|
+
setInked(false);
|
|
306
|
+
};
|
|
307
|
+
// The pill moves the reader on by about a page. Reaching the end stays a
|
|
308
|
+
// scroll, so the viewer's own onScroll unlocks Sign, not the pill.
|
|
309
|
+
const scrollOn = () => {
|
|
310
|
+
const v = viewer.current;
|
|
311
|
+
if (!interactive || !v) return;
|
|
312
|
+
const still =
|
|
313
|
+
typeof window !== 'undefined' &&
|
|
314
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
315
|
+
v.scrollBy({ top: v.clientHeight * PILL_STEP, behavior: still ? 'auto' : 'smooth' });
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const stepperState: PrizmaStepperState = read
|
|
319
|
+
? 'Completed'
|
|
320
|
+
: current <= 1
|
|
321
|
+
? 'Start'
|
|
322
|
+
: (Math.min(current, MAX_STEPS) as PrizmaStepperState);
|
|
323
|
+
|
|
324
|
+
const pad = (ghost: boolean) => (
|
|
325
|
+
<PrizmaSignaturePad
|
|
326
|
+
key={ghost ? 'ghost' : padKey}
|
|
327
|
+
isFilled={ghost ? false : inked}
|
|
328
|
+
isInitials={false}
|
|
329
|
+
placeholder={sign.placeholder}
|
|
330
|
+
canvasLabel={sign.canvasLabel}
|
|
331
|
+
clearLabel={sign.clearLabel}
|
|
332
|
+
onClear={ghost ? undefined : clear}
|
|
333
|
+
onCanvasReady={ghost ? undefined : onCanvasReady}
|
|
334
|
+
/>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// The Type body keeps at least the Draw body's height, so the sheet does
|
|
338
|
+
// not jump when the mode switches (Elena, 2026-11-12). The exploration
|
|
339
|
+
// measured the Draw body in JS; here an invisible copy of the pad shares the
|
|
340
|
+
// Type body's grid cell, so the floor holds on a static spec card too.
|
|
341
|
+
// Layout glue, not a component: `visibility: hidden` keeps it out of the
|
|
342
|
+
// tab order and the accessibility tree.
|
|
343
|
+
const modeBody =
|
|
344
|
+
mode === 'draw' ? (
|
|
345
|
+
// The first pointerdown on the canvas is ink: Core's pad flips its own
|
|
346
|
+
// Clear on it but reports nothing, so the screen hears it bubble up.
|
|
347
|
+
<div
|
|
348
|
+
id={panelId}
|
|
349
|
+
role="tabpanel"
|
|
350
|
+
aria-label={sign.drawLabel}
|
|
351
|
+
className="document-signature-sheet-mode"
|
|
352
|
+
onPointerDown={interactive ? () => setInked(true) : undefined}
|
|
353
|
+
>
|
|
354
|
+
{pad(false)}
|
|
355
|
+
</div>
|
|
356
|
+
) : (
|
|
357
|
+
<div
|
|
358
|
+
id={panelId}
|
|
359
|
+
role="tabpanel"
|
|
360
|
+
aria-label={sign.typeLabel}
|
|
361
|
+
className="document-signature-sheet-mode document-signature-sheet-mode--type"
|
|
362
|
+
>
|
|
363
|
+
{/* `inert`: the copy only holds the Draw height. Hidden is not
|
|
364
|
+
enough: its Clear canvas button was still picked up by Core
|
|
365
|
+
Lab's focus highlight and drawn over Confirm (Elena's bug,
|
|
366
|
+
2026-11-13). Inert takes it out of focus and hit-testing. */}
|
|
367
|
+
<div className="document-signature-sheet-mode__floor" aria-hidden="true" {...{ inert: '' }}>
|
|
368
|
+
{pad(true)}
|
|
369
|
+
</div>
|
|
370
|
+
<div className="document-signature-sheet-mode__type">
|
|
371
|
+
{/* Set up for a name: the phone offers the person's own, capitalises
|
|
372
|
+
each word, does not autocorrect it, and Return confirms.
|
|
373
|
+
Static states pass `defaultValue`, never `readOnly`: Core paints
|
|
374
|
+
a read-only field on the disabled grey surface, and the canvas
|
|
375
|
+
showed the Type sheet's field grey (Elena, 2026-11-13). Same
|
|
376
|
+
rule as CurpGenerateScreen and DemoScreensRedirectScreen. The
|
|
377
|
+
key re-mounts it when a state changes the name. */}
|
|
378
|
+
<PrizmaTextField
|
|
379
|
+
key={interactive ? 'live' : `static-${name}`}
|
|
380
|
+
label={sign.nameLabel}
|
|
381
|
+
{...(interactive ? { value: name, onValueChange: setName } : { defaultValue: name })}
|
|
382
|
+
autoComplete="name"
|
|
383
|
+
autoCapitalize="words"
|
|
384
|
+
autoCorrect="off"
|
|
385
|
+
spellCheck={false}
|
|
386
|
+
enterKeyHint="done"
|
|
387
|
+
onKeyDown={(e) => {
|
|
388
|
+
if (e.key === 'Enter' && ready) {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
confirm();
|
|
391
|
+
}
|
|
392
|
+
}}
|
|
393
|
+
/>
|
|
394
|
+
{/* The typed name shown back as the signature, the exploration's
|
|
395
|
+
"tabs with cursive" page. Core's preview truncates a long name
|
|
396
|
+
with an ellipsis on its own. */}
|
|
397
|
+
<PrizmaSignaturePreview
|
|
398
|
+
label={sign.previewLabel}
|
|
399
|
+
name={name}
|
|
400
|
+
placeholder={sign.previewPlaceholder}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Draw / Type sits right under the consent, at the top of the body, where
|
|
407
|
+
// the exploration's final Option B put it (it replaced the Secondary
|
|
408
|
+
// "Type your name instead / Draw instead" button).
|
|
409
|
+
const body = (
|
|
410
|
+
<div className="document-signature-sheet-body">
|
|
411
|
+
<PrizmaSegmentedControl
|
|
412
|
+
aria-label={sign.methodLabel}
|
|
413
|
+
value={mode}
|
|
414
|
+
onValueChange={(v: string) => switchMode(v === 'type' ? 'type' : 'draw')}
|
|
415
|
+
options={[
|
|
416
|
+
{ value: 'draw', label: sign.drawLabel, controls: mode === 'draw' ? panelId : undefined },
|
|
417
|
+
{ value: 'type', label: sign.typeLabel, controls: mode === 'type' ? panelId : undefined },
|
|
418
|
+
]}
|
|
419
|
+
/>
|
|
420
|
+
{modeBody}
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const overlay = sheet ? (
|
|
425
|
+
<>
|
|
426
|
+
<div
|
|
427
|
+
className={`prizma-screen-layout__sheet-scrim${interactive ? ' document-signature-sheet-scrim--moving' : ''}${closing ? ' is-closing' : ''}`}
|
|
428
|
+
aria-hidden="true"
|
|
429
|
+
onClick={dismiss}
|
|
430
|
+
/>
|
|
431
|
+
<div className={`prizma-screen-layout__sheet-panel${interactive ? ' document-signature-sheet-panel--moving' : ''}${closing ? ' is-closing' : ''}`}>
|
|
432
|
+
<PrizmaBottomSheetContent
|
|
433
|
+
ref={panel}
|
|
434
|
+
variant="dialog"
|
|
435
|
+
title={sign.title}
|
|
436
|
+
subtitle={sign.consent}
|
|
437
|
+
body={body}
|
|
438
|
+
showBottomBar
|
|
439
|
+
onKeyDown={(e) => {
|
|
440
|
+
if (e.key === 'Escape') dismiss();
|
|
441
|
+
}}
|
|
442
|
+
buttonGroupProps={{
|
|
443
|
+
orientation: 'vertical-v1',
|
|
444
|
+
showPrimary: true,
|
|
445
|
+
showSecondary: false,
|
|
446
|
+
showTertiary: false,
|
|
447
|
+
primaryLabel: sign.confirmLabel,
|
|
448
|
+
primaryDisabled: !ready,
|
|
449
|
+
onPrimaryClick: confirm,
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</>
|
|
454
|
+
) : undefined;
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<PrizmaFormScreen
|
|
458
|
+
ref={ref}
|
|
459
|
+
{...shell}
|
|
460
|
+
className={['document-signature-screen', className].filter(Boolean).join(' ')}
|
|
461
|
+
title={review.title}
|
|
462
|
+
fieldsOffset="12"
|
|
463
|
+
ctaLabel={review.ctaLabel}
|
|
464
|
+
ctaDisabled={!read}
|
|
465
|
+
verifiedLabel={strings.verifiedLabel}
|
|
466
|
+
navLabel={strings.navLabel}
|
|
467
|
+
screenId={screenId}
|
|
468
|
+
overrides={overrides}
|
|
469
|
+
// Sign is a step inside this screen, not the module's result: it opens
|
|
470
|
+
// the sheet and reports nothing. Under the sheet's scrim it is inert.
|
|
471
|
+
onAction={(a) => {
|
|
472
|
+
if (a.type === 'cta' && !sheet) openSheet();
|
|
473
|
+
}}
|
|
474
|
+
overlay={overlay}
|
|
475
|
+
>
|
|
476
|
+
{many ? (
|
|
477
|
+
<PrizmaStepper
|
|
478
|
+
steps={Math.min(total, MAX_STEPS) as PrizmaStepperSteps}
|
|
479
|
+
currentState={stepperState}
|
|
480
|
+
showIcons={false}
|
|
481
|
+
// The caption also names the progressbar (Core wires
|
|
482
|
+
// aria-labelledby when there is no aria-label), so the count is
|
|
483
|
+
// announced once.
|
|
484
|
+
caption={read ? review.completedLabel : count(review.documentCount, current, total)}
|
|
485
|
+
completedValueText={review.completedLabel}
|
|
486
|
+
/>
|
|
487
|
+
) : null}
|
|
488
|
+
<div
|
|
489
|
+
ref={viewer}
|
|
490
|
+
onScroll={interactive ? measure : undefined}
|
|
491
|
+
className="electronic-signature-document document-signature-viewer"
|
|
492
|
+
role="region"
|
|
493
|
+
aria-label={review.documentsLabel}
|
|
494
|
+
tabIndex={0}
|
|
495
|
+
>
|
|
496
|
+
{documents.map((d, i) => (
|
|
497
|
+
<section key={`${i}-${d.title}`} data-doc={i + 1} className="document-signature-doc">
|
|
498
|
+
<h4 className="document-signature-doc__title">{d.title}</h4>
|
|
499
|
+
{d.content}
|
|
500
|
+
</section>
|
|
501
|
+
))}
|
|
502
|
+
{/* Last child of the scroll area, Core's placement contract: sticky
|
|
503
|
+
at the bottom, only the pill takes pointer events. Gone once the
|
|
504
|
+
end is reached, so it never covers the last line. */}
|
|
505
|
+
{read ? null : (
|
|
506
|
+
<PrizmaScrollPillDock>
|
|
507
|
+
<PrizmaScrollPill label={review.scrollLabel} onClick={scrollOn} />
|
|
508
|
+
</PrizmaScrollPillDock>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
</PrizmaFormScreen>
|
|
512
|
+
);
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
DocumentSignatureReviewScreen.displayName = 'DocumentSignatureReviewScreen';
|
|
516
|
+
|
|
517
|
+
export default DocumentSignatureReviewScreen;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// from the `read-and-sign-in-one-screen` exploration
|
|
2
|
+
//
|
|
3
|
+
// Document Electronic Signature — the module's whole copy surface.
|
|
4
|
+
//
|
|
5
|
+
// Every string the review-and-sign screen renders lives here, so the screen
|
|
6
|
+
// carries no hardcoded English and a host translates the module by supplying
|
|
7
|
+
// one object (README rule 3, the same contract as `ElectronicSignatureStrings`).
|
|
8
|
+
//
|
|
9
|
+
// Source: the `read-and-sign-in-one-screen` exploration, Option B, decided with
|
|
10
|
+
// the owner (Elena, 2026-11-12). No Figma file yet and no SDK release, so no
|
|
11
|
+
// i18n namespace either: the demo passes the English.
|
|
12
|
+
|
|
13
|
+
import type { ElectronicSignatureStrings } from './ElectronicSignatureStrings';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The poses the review screen draws, deterministic from props:
|
|
17
|
+
*
|
|
18
|
+
* - `start` reading document 1, Sign disabled.
|
|
19
|
+
* - `second` document 2 reached in the same scroll, Stepper on 2.
|
|
20
|
+
* - `end` every document read, Stepper `Completed`, Sign live.
|
|
21
|
+
* - `draw` the sheet over the read screen, empty pad, Confirm disabled.
|
|
22
|
+
* - `drawn` the sheet with a signature on the pad, Confirm live.
|
|
23
|
+
* - `type` the sheet in type mode; `typedName` fills the field.
|
|
24
|
+
*/
|
|
25
|
+
export type DocumentSignatureState = 'start' | 'second' | 'end' | 'draw' | 'drawn' | 'type';
|
|
26
|
+
|
|
27
|
+
export interface DocumentSignatureStrings {
|
|
28
|
+
/** "verified by" attribution copy for the footer tag. */
|
|
29
|
+
verifiedLabel: string;
|
|
30
|
+
/** Accessible name for the logo row. */
|
|
31
|
+
navLabel: string;
|
|
32
|
+
|
|
33
|
+
/** The review screen: every document in one scrolling viewer. */
|
|
34
|
+
review: {
|
|
35
|
+
/** H2 — "Review and sign". */
|
|
36
|
+
title: string;
|
|
37
|
+
/** Accessible name for the scrolling viewer — "Documents to sign". */
|
|
38
|
+
documentsLabel: string;
|
|
39
|
+
/**
|
|
40
|
+
* The Stepper's caption while reading, which also names its progressbar —
|
|
41
|
+
* "Document {{current}} of {{total}}". Both placeholders are replaced by
|
|
42
|
+
* the screen.
|
|
43
|
+
*/
|
|
44
|
+
documentCount: string;
|
|
45
|
+
/**
|
|
46
|
+
* The Stepper's caption and `aria-valuetext` once every document is read —
|
|
47
|
+
* "All documents read".
|
|
48
|
+
*/
|
|
49
|
+
completedLabel: string;
|
|
50
|
+
/**
|
|
51
|
+
* Core's Scroll pill over the viewer while there is reading left —
|
|
52
|
+
* "Scroll to sign". A tap scrolls about 80% of the viewer.
|
|
53
|
+
*/
|
|
54
|
+
scrollLabel: string;
|
|
55
|
+
/** Primary CTA — "Sign". Disabled until the end of the last document. */
|
|
56
|
+
ctaLabel: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The signing sheet (Core's Bottom sheet, dialog variant). */
|
|
60
|
+
sign: {
|
|
61
|
+
/** Sheet title, the same in both modes — "Add your signature". */
|
|
62
|
+
title: string;
|
|
63
|
+
/** Accessible name for the Draw / Type Segmented control — "Signature method". */
|
|
64
|
+
methodLabel: string;
|
|
65
|
+
/** Segmented control, the draw option — "Draw". */
|
|
66
|
+
drawLabel: string;
|
|
67
|
+
/** Segmented control, the type option — "Type". */
|
|
68
|
+
typeLabel: string;
|
|
69
|
+
/** Sheet subtitle, the consent — "By confirming, you accept and sign the documents electronically." */
|
|
70
|
+
consent: string;
|
|
71
|
+
/** Pad placeholder — "Sign here". */
|
|
72
|
+
placeholder: string;
|
|
73
|
+
/** Accessible name for the drawing canvas — "Signature capture area". */
|
|
74
|
+
canvasLabel: string;
|
|
75
|
+
/** Core's Clear-canvas link — "Clear canvas". */
|
|
76
|
+
clearLabel: string;
|
|
77
|
+
/** Type mode field label — "Full name". */
|
|
78
|
+
nameLabel: string;
|
|
79
|
+
/** Core's Signature preview label, under the field — "Preview". */
|
|
80
|
+
previewLabel: string;
|
|
81
|
+
/** Signature preview while the field is empty — "Your signature". */
|
|
82
|
+
previewPlaceholder: string;
|
|
83
|
+
/** The only button — "Confirm and continue". */
|
|
84
|
+
confirmLabel: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The signed verdict, drawn by the shipped `ElectronicSignatureSuccessScreen`. */
|
|
88
|
+
success: ElectronicSignatureStrings['success'];
|
|
89
|
+
/** The failure verdict, drawn by the shipped `ElectronicSignatureErrorScreen`. */
|
|
90
|
+
error: ElectronicSignatureStrings['error'];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The verdict screens are Electronic Signature's own, reused as shipped
|
|
95
|
+
* (`ElectronicSignatureSuccessScreen`, `ElectronicSignatureErrorScreen`). They
|
|
96
|
+
* take that module's strings object and read only its `success` / `error`
|
|
97
|
+
* slices plus the two chrome labels; this maps the rest from this module's copy
|
|
98
|
+
* so nothing is left blank and no second copy of a sentence is kept.
|
|
99
|
+
*/
|
|
100
|
+
export function documentSignatureResultStrings(
|
|
101
|
+
strings: DocumentSignatureStrings,
|
|
102
|
+
): ElectronicSignatureStrings {
|
|
103
|
+
return {
|
|
104
|
+
verifiedLabel: strings.verifiedLabel,
|
|
105
|
+
navLabel: strings.navLabel,
|
|
106
|
+
review: {
|
|
107
|
+
title: strings.review.title,
|
|
108
|
+
documentLabel: strings.review.documentsLabel,
|
|
109
|
+
ctaLabel: strings.review.ctaLabel,
|
|
110
|
+
},
|
|
111
|
+
draw: {
|
|
112
|
+
title: strings.sign.title,
|
|
113
|
+
subtitle: strings.sign.consent,
|
|
114
|
+
placeholder: strings.sign.placeholder,
|
|
115
|
+
canvasLabel: strings.sign.canvasLabel,
|
|
116
|
+
clearLabel: strings.sign.clearLabel,
|
|
117
|
+
ctaLabel: strings.sign.confirmLabel,
|
|
118
|
+
},
|
|
119
|
+
success: strings.success,
|
|
120
|
+
error: strings.error,
|
|
121
|
+
};
|
|
122
|
+
}
|