@incodetech/prizma-core-screens 0.2.24 → 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.
@@ -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
+ }