@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.
@@ -27,7 +27,7 @@ export const DemoScreensConfigSheetScreen = forwardRef(({ strings, state = 'defa
27
27
  const s = strings.configSheet;
28
28
  const options = [{ value: s.flowValue, label: s.flowValue }, ...s.flows];
29
29
  const value = state === 'selected' ? s.flowSelected : s.flowValue;
30
- return (_jsxs(PrizmaScreenShell, { ref: ref, "data-screen-id": screenId, ...shell, children: [state === 'saved' ? (_jsx("div", { className: "demo-screens-toast", children: _jsx(PrizmaSnackbar, { state: "success", contentResizing: "fill", isMultiline: true, children: s.savedSnackbar }) })) : null, _jsxs(DemoScreensSheet, { ariaLabel: s.title, children: [_jsx("div", { className: "demo-screens-sheet__nav", children: _jsx(PrizmaNavigation, { showIncodeLogo: false, showRightIcon: true, closeLabel: s.closeLabel, onRightIconClick: () => onAction?.({ type: 'help', screenId }), "aria-label": s.title }) }), _jsx("div", { className: "demo-screens-sheet__title", children: _jsx(PrizmaTitle, { variant: "left", title: s.title, showSubtitle: true, subtitle: s.subtitle }) }), _jsxs("div", { className: "demo-screens-sheet__fields", children: [_jsx(PrizmaDropdown, { label: s.flowLabel, options: options, value: value, showRequiredMarker: false, searchable: true, searchPlaceholder: s.searchPlaceholder, clearSearchLabel: s.clearSearchLabel, forceOpen: state === 'flowOpen' }), _jsxs("div", { className: "demo-screens-sheet__lines", children: [_jsx("p", { className: "demo-screens-sheet__line", children: s.clientIdLine }), _jsx("p", { className: "demo-screens-sheet__line", children: s.environmentLine })] })] }), _jsxs("div", { className: "demo-screens-sheet__band", children: [_jsx(PrizmaButton, { variant: "primary", size: "L", disabled: state !== 'selected', onClick: () => onAction?.({ type: 'cta', screenId }), children: s.saveLabel }), _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId }), children: s.editLinkLabel }), _jsx("p", { className: "demo-screens-build", children: s.buildLabel })] })] })] }));
30
+ return (_jsxs(PrizmaScreenShell, { ref: ref, "data-screen-id": screenId, ...shell, children: [state === 'saved' ? (_jsx("div", { className: "demo-screens-toast", children: _jsx(PrizmaSnackbar, { state: "success", contentResizing: "fill", children: s.savedSnackbar }) })) : null, _jsxs(DemoScreensSheet, { ariaLabel: s.title, children: [_jsx("div", { className: "demo-screens-sheet__nav", children: _jsx(PrizmaNavigation, { showIncodeLogo: false, showRightIcon: true, closeLabel: s.closeLabel, onRightIconClick: () => onAction?.({ type: 'help', screenId }), "aria-label": s.title }) }), _jsx("div", { className: "demo-screens-sheet__title", children: _jsx(PrizmaTitle, { variant: "left", title: s.title, showSubtitle: true, subtitle: s.subtitle }) }), _jsxs("div", { className: "demo-screens-sheet__fields", children: [_jsx(PrizmaDropdown, { label: s.flowLabel, options: options, value: value, showRequiredMarker: false, searchable: true, searchPlaceholder: s.searchPlaceholder, clearSearchLabel: s.clearSearchLabel, forceOpen: state === 'flowOpen' }), _jsxs("div", { className: "demo-screens-sheet__lines", children: [_jsx("p", { className: "demo-screens-sheet__line", children: s.clientIdLine }), _jsx("p", { className: "demo-screens-sheet__line", children: s.environmentLine })] })] }), _jsxs("div", { className: "demo-screens-sheet__band", children: [_jsx(PrizmaButton, { variant: "primary", size: "L", disabled: state !== 'selected', onClick: () => onAction?.({ type: 'cta', screenId }), children: s.saveLabel }), _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId }), children: s.editLinkLabel }), _jsx("p", { className: "demo-screens-build", children: s.buildLabel })] })] })] }));
31
31
  });
32
32
  DemoScreensConfigSheetScreen.displayName = 'DemoScreensConfigSheetScreen';
33
33
  export default DemoScreensConfigSheetScreen;
@@ -0,0 +1,160 @@
1
+ /* from the `read-and-sign-in-one-screen` exploration
2
+ *
3
+ * Document Electronic Signature — layout glue only. No archetype value is
4
+ * redeclared (the gutters, the title block, the CTA band and the tag stay the
5
+ * foundation's) and nothing here draws a component: the viewer frame is
6
+ * Electronic Signature's `.electronic-signature-document`, the Stepper,
7
+ * Scroll pill, Segmented control, pad, field, Signature preview and sheet are
8
+ * Core's. Every value is a --prizma-*
9
+ * token. Ported from Option B (`read-and-sign.css`) of the exploration.
10
+ */
11
+
12
+ /* The viewer fills what the Stepper leaves. Electronic Signature's frame
13
+ carries a Spacing/16 bottom margin for its List screen; in the Form
14
+ archetype the field stack closes at Spacing/16 instead, so it is released
15
+ here and the stack takes it (the same gap, the same token). */
16
+ .document-signature-screen .prizma-form-screen__fields {
17
+ margin-bottom: var(--prizma-spacing-16);
18
+ }
19
+
20
+ .document-signature-screen .prizma-form-screen__fields > :not(.document-signature-viewer) {
21
+ flex-shrink: 0;
22
+ }
23
+
24
+ .document-signature-viewer.electronic-signature-document {
25
+ flex: 1 1 auto;
26
+ min-height: 0;
27
+ margin-bottom: 0;
28
+ /* The sections' offset parent, so the screen reads where each document is
29
+ in layout pixels, whatever transform a spec card scales the frame with. */
30
+ position: relative;
31
+ }
32
+
33
+ /* One document: its title, then the host's prose. No divider between
34
+ documents: the count is the Stepper's caption. Electronic Signature's frame already spaces its direct
35
+ children; a section spaces its own. */
36
+ .document-signature-doc {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: var(--prizma-spacing-12);
40
+ }
41
+
42
+ .document-signature-doc + .document-signature-doc {
43
+ margin-top: var(--prizma-spacing-24);
44
+ }
45
+
46
+ .document-signature-doc > * {
47
+ margin: 0;
48
+ }
49
+
50
+ /* A document's title is a step above its section headings: Headline/H4 on the
51
+ frame's own Text/Body/800. */
52
+ .electronic-signature-document .document-signature-doc__title {
53
+ font-size: var(--prizma-headline-h4-font-size);
54
+ line-height: var(--prizma-headline-h4-line-height);
55
+ font-weight: var(--prizma-headline-h4-font-weight);
56
+ letter-spacing: var(--prizma-headline-h4-letter-spacing);
57
+ }
58
+
59
+ /* The sheet body: Core's Segmented control (Draw / Type), then the mode's
60
+ body. Spacing/8 between them, and between the field and the preview, the
61
+ exploration's own gap (`.rs-b-sheet`). */
62
+ .document-signature-sheet-body,
63
+ .document-signature-sheet-mode,
64
+ .document-signature-sheet-mode__type {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--prizma-spacing-8);
68
+ }
69
+
70
+ /* Type mode keeps the Draw body's height: an invisible copy of Core's pad and
71
+ the field + preview share one grid cell, so the cell is as tall as the taller
72
+ of the two and the sheet does not jump when the mode switches. */
73
+ .document-signature-sheet-mode--type {
74
+ display: grid;
75
+ }
76
+
77
+ .document-signature-sheet-mode--type > * {
78
+ grid-area: 1 / 1;
79
+ min-width: 0;
80
+ }
81
+
82
+ .document-signature-sheet-mode__floor {
83
+ visibility: hidden;
84
+ pointer-events: none;
85
+ }
86
+
87
+ .document-signature-sheet-mode__type {
88
+ align-self: start;
89
+ }
90
+
91
+ /* ── Short panels: the sheet stops under the status bar ──────────────────────
92
+ * The signing sheet is 652 tall (Draw, and Type keeps that height). On the
93
+ * Galaxy Z Fold 8 cover (416 x 657) it rose to 5px from the top, over the
94
+ * status bar (measured 2026-11-13); on every other frame Core Lab offers it
95
+ * clears with room (iPhone Duo closed 26, open 17, Fold main 164, the 844
96
+ * reference 192). Under the 720 short-panel line the panel stops at the status
97
+ * bar plus Spacing/24, the Demo screens sheet's own top, and the one element
98
+ * that gives ground is the pad's drawing area (step 1 of the order of
99
+ * concessions: it is space, not task information), 256 to 190 on the Fold
100
+ * cover. The title, the consent, Draw / Type, Clear canvas, Confirm and
101
+ * continue and Core's 34 safe area keep their size. Type keeps the field and
102
+ * the preview whole: only the invisible Draw floor gives way. Outside the
103
+ * query not one declaration exists, so the reference is untouched. */
104
+ @container pcs-screen (max-height: 720px) {
105
+ .document-signature-screen .prizma-screen-layout__sheet-panel {
106
+ max-height: calc(100% - var(--pcs-status-bar, 47px) - var(--prizma-spacing-24));
107
+ display: flex;
108
+ flex-direction: column;
109
+ }
110
+
111
+ .document-signature-screen .prizma-screen-layout__sheet-panel .prizma-bottom-sheet,
112
+ .document-signature-screen .prizma-screen-layout__sheet-panel .prizma-bottom-sheet__content,
113
+ .document-signature-screen .prizma-screen-layout__sheet-panel .prizma-bottom-sheet__body,
114
+ .document-signature-screen .document-signature-sheet-body,
115
+ .document-signature-screen .document-signature-sheet-mode,
116
+ .document-signature-screen .document-signature-sheet-mode .prizma-signature-pad,
117
+ .document-signature-screen .document-signature-sheet-mode .prizma-signature-pad__canvas-wrapper {
118
+ flex: 0 1 auto;
119
+ min-height: 0;
120
+ }
121
+
122
+ .document-signature-screen .prizma-screen-layout__sheet-panel .prizma-bottom-sheet__body {
123
+ display: flex;
124
+ flex-direction: column;
125
+ }
126
+
127
+ .document-signature-screen .prizma-bottom-sheet__content > :not(.prizma-bottom-sheet__body),
128
+ .document-signature-screen .document-signature-sheet-body > :not(.document-signature-sheet-mode) {
129
+ flex-shrink: 0;
130
+ }
131
+
132
+ /* The grid cell may be shorter than the invisible Draw copy it holds. */
133
+ .document-signature-screen .document-signature-sheet-mode--type {
134
+ grid-template-rows: minmax(0, auto);
135
+ }
136
+ }
137
+
138
+ /* ── The sheet's motion (2026-11-13) ─────────────────────────────────────────
139
+ Interactive only (`--moving` is set when the screen is live; static spec
140
+ cards stay still, as ScreenLayout's sheet rules intend). Core's Sheet/Motion
141
+ tokens and Core's own keyframes (`prizma-sheet-scrim-in/out`,
142
+ `prizma-sheet-slide-in/out` in bottom-sheet.css), as Core's presenter does: the scrim fades
143
+ in and the panel rises on Enter + Decelerate, and both leave on Exit +
144
+ Accelerate. Nothing moves under reduced motion. */
145
+ .document-signature-sheet-scrim--moving {
146
+ animation: prizma-sheet-scrim-in var(--prizma-sheet-motion-enter-duration) var(--prizma-sheet-motion-enter-easing) both;
147
+ }
148
+ .document-signature-sheet-panel--moving {
149
+ animation: prizma-sheet-slide-in var(--prizma-sheet-motion-enter-duration) var(--prizma-sheet-motion-enter-easing) both;
150
+ }
151
+ .document-signature-sheet-scrim--moving.is-closing {
152
+ animation: prizma-sheet-scrim-out var(--prizma-sheet-motion-exit-duration) var(--prizma-sheet-motion-exit-easing) both;
153
+ }
154
+ .document-signature-sheet-panel--moving.is-closing {
155
+ animation: prizma-sheet-slide-out var(--prizma-sheet-motion-exit-duration) var(--prizma-sheet-motion-exit-easing) both;
156
+ }
157
+ @media (prefers-reduced-motion: reduce) {
158
+ .document-signature-sheet-scrim--moving,
159
+ .document-signature-sheet-panel--moving { animation: none; }
160
+ }
@@ -0,0 +1,88 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import { type PrizmaFormScreenProps } from './FormScreen';
3
+ import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
4
+ import type { DocumentSignatureState, DocumentSignatureStrings } from './DocumentSignatureStrings';
5
+ import './ScreenLayout.css';
6
+ import './ElectronicSignatureLayout.css';
7
+ import './DocumentSignatureLayout.css';
8
+ /** One document to read and sign. Runtime DATA the host supplies, never copy
9
+ * the design system owns (the same slot contract as Electronic Signature's
10
+ * `document`). `content` is plain `<h4>` / `<p>` markup: the viewer's frame
11
+ * supplies the typography. */
12
+ export interface DocumentSignatureDocument {
13
+ title: string;
14
+ content: ReactNode;
15
+ }
16
+ /** What the person signed with, reported on Confirm and continue. */
17
+ export type DocumentSignatureSignature = {
18
+ method: 'draw';
19
+ } | {
20
+ method: 'type';
21
+ name: string;
22
+ };
23
+ export interface DocumentSignatureReviewScreenProps extends Omit<PrizmaFormScreenProps, 'progress' | 'title' | 'subtitle' | 'ctaLabel' | 'ctaDisabled' | 'ctaLoading' | 'ctaLoadingLabel' | 'linkLabel' | 'linkDisabled' | 'linkContent' | 'verifiedLabel' | 'navLabel' | 'children' | 'fieldsOffset' | 'fieldsGap' | 'scrollFields' | 'fieldsAlign' | 'overlay'> {
24
+ /** Module copy. */
25
+ strings: DocumentSignatureStrings;
26
+ /** Every document to sign, in reading order. One or more. */
27
+ documents: DocumentSignatureDocument[];
28
+ /** The pose to draw (and, when `interactive`, the pose to start from). */
29
+ state?: DocumentSignatureState;
30
+ /** The name in the field for `state="type"`. */
31
+ typedName?: string;
32
+ /**
33
+ * Run the screen: the Stepper follows the scroll, the Scroll pill moves the
34
+ * viewer on, reaching the end of the last document turns Sign on AND opens
35
+ * the sheet (once), Sign reopens it after a dismiss, the Draw / Type switch
36
+ * clears the other mode, and Confirm and continue reports the signature. Off (the default), every pose is fixed by
37
+ * `state`, so a spec card always renders the same frame.
38
+ */
39
+ interactive?: boolean;
40
+ /**
41
+ * `PrizmaSignaturePad.onCanvasReady`: the host's drawing engine, Core's
42
+ * documented division of labour (the pad ships the chrome, not the ink). For
43
+ * `state="drawn"` a spec host paints a specimen stroke here.
44
+ */
45
+ onCanvasReady?: (canvas: HTMLCanvasElement) => void;
46
+ /** Confirm and continue was pressed with a signature on the sheet. */
47
+ onSignatureConfirm?: (signature: DocumentSignatureSignature) => void;
48
+ screenId?: string;
49
+ overrides?: PrizmaScreenOverrides;
50
+ /** Emits `cta` on Confirm and continue. Sign is handled inside the screen. */
51
+ onAction?: PrizmaScreenActionHandler;
52
+ }
53
+ /**
54
+ * Document Electronic Signature — read every document and sign them once, on
55
+ * ONE screen. Option B of the `read-and-sign-in-one-screen` exploration,
56
+ * decided with the owner (Elena, 2026-11-12), built on the Form archetype
57
+ * (GUIDELINES.md §6-E, `PrizmaFormScreen`).
58
+ *
59
+ * Core only, used as shipped:
60
+ *
61
+ * - `PrizmaFormScreen`: logo nav, "Review and sign", the Sign CTA (disabled
62
+ * until the reader reaches the end of the last document: consent needs
63
+ * proof the documents were read) and the verified tag.
64
+ * - `PrizmaStepper`, one segment per document, following the one being read,
65
+ * `Completed` once everything is read, with its `caption` ("Document 1 of
66
+ * 2", then "All documents read") naming the bar.
67
+ * - Every document in ONE scrolling viewer, Electronic Signature's own frame
68
+ * (`.electronic-signature-document`), one after the other with no divider:
69
+ * the count lives on the Stepper. One document: no Stepper.
70
+ * - `PrizmaScrollPill` ("Scroll to sign") in its `PrizmaScrollPillDock`, the
71
+ * viewer's last child, while there is reading left. A tap scrolls about 80%
72
+ * of the viewer; it unmounts once the end is reached.
73
+ * - `PrizmaBottomSheetContent` (dialog), "Add your signature": the consent as
74
+ * its subtitle; the body opens with `PrizmaSegmentedControl` (Draw / Type),
75
+ * then `PrizmaSignaturePad`, or `PrizmaTextField` with
76
+ * `PrizmaSignaturePreview` under it; its Button group holds only Confirm
77
+ * and continue, bottom bar on.
78
+ *
79
+ * The sheet rides the archetype's `overlay` slot (scrim + panel from
80
+ * `ScreenLayout.css`, the Reusability / Biometric Consent contract), not
81
+ * `PrizmaBottomSheet`'s presenter: the presenter portals to `document.body`,
82
+ * which escapes the 390 x 844 frame a spec card and a prototype draw in.
83
+ *
84
+ * Nothing new: the only module CSS is layout glue on Core tokens
85
+ * (`DocumentSignatureLayout.css`).
86
+ */
87
+ export declare const DocumentSignatureReviewScreen: React.ForwardRefExoticComponent<DocumentSignatureReviewScreenProps & React.RefAttributes<HTMLDivElement>>;
88
+ export default DocumentSignatureReviewScreen;
@@ -0,0 +1,274 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // from the `read-and-sign-in-one-screen` exploration
4
+ import { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, } from 'react';
5
+ import { PrizmaBottomSheetContent } from '@incodetech/prizma-core-web/bottom-sheet';
6
+ import { PrizmaScrollPill, PrizmaScrollPillDock } from '@incodetech/prizma-core-web/scroll-pill';
7
+ import { PrizmaSegmentedControl } from '@incodetech/prizma-core-web/segmented-control';
8
+ import { PrizmaSignaturePad } from '@incodetech/prizma-core-web/signature-pad';
9
+ import { PrizmaSignaturePreview } from '@incodetech/prizma-core-web/signature-preview';
10
+ import { PrizmaStepper, } from '@incodetech/prizma-core-web/stepper';
11
+ import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
12
+ import { PrizmaFormScreen } from './FormScreen';
13
+ import './ScreenLayout.css';
14
+ import './ElectronicSignatureLayout.css';
15
+ import './DocumentSignatureLayout.css';
16
+ const POSE = {
17
+ start: { at: 'start', sheet: null },
18
+ second: { at: 'second', sheet: null },
19
+ end: { at: 'end', sheet: null },
20
+ draw: { at: 'end', sheet: 'draw' },
21
+ drawn: { at: 'end', sheet: 'draw' },
22
+ type: { at: 'end', sheet: 'type' },
23
+ };
24
+ /** Core's Stepper is authored for 2 to 7 segments. */
25
+ const MAX_STEPS = 7;
26
+ /** One tap on the Scroll pill: about a page, never straight to the end
27
+ * (Core's documented action for the pill, `ScrollPill.tsx`). */
28
+ const PILL_STEP = 0.8;
29
+ const count = (template, current, total) => template.replace('{{current}}', String(current)).replace('{{total}}', String(total));
30
+ /**
31
+ * Document Electronic Signature — read every document and sign them once, on
32
+ * ONE screen. Option B of the `read-and-sign-in-one-screen` exploration,
33
+ * decided with the owner (Elena, 2026-11-12), built on the Form archetype
34
+ * (GUIDELINES.md §6-E, `PrizmaFormScreen`).
35
+ *
36
+ * Core only, used as shipped:
37
+ *
38
+ * - `PrizmaFormScreen`: logo nav, "Review and sign", the Sign CTA (disabled
39
+ * until the reader reaches the end of the last document: consent needs
40
+ * proof the documents were read) and the verified tag.
41
+ * - `PrizmaStepper`, one segment per document, following the one being read,
42
+ * `Completed` once everything is read, with its `caption` ("Document 1 of
43
+ * 2", then "All documents read") naming the bar.
44
+ * - Every document in ONE scrolling viewer, Electronic Signature's own frame
45
+ * (`.electronic-signature-document`), one after the other with no divider:
46
+ * the count lives on the Stepper. One document: no Stepper.
47
+ * - `PrizmaScrollPill` ("Scroll to sign") in its `PrizmaScrollPillDock`, the
48
+ * viewer's last child, while there is reading left. A tap scrolls about 80%
49
+ * of the viewer; it unmounts once the end is reached.
50
+ * - `PrizmaBottomSheetContent` (dialog), "Add your signature": the consent as
51
+ * its subtitle; the body opens with `PrizmaSegmentedControl` (Draw / Type),
52
+ * then `PrizmaSignaturePad`, or `PrizmaTextField` with
53
+ * `PrizmaSignaturePreview` under it; its Button group holds only Confirm
54
+ * and continue, bottom bar on.
55
+ *
56
+ * The sheet rides the archetype's `overlay` slot (scrim + panel from
57
+ * `ScreenLayout.css`, the Reusability / Biometric Consent contract), not
58
+ * `PrizmaBottomSheet`'s presenter: the presenter portals to `document.body`,
59
+ * which escapes the 390 x 844 frame a spec card and a prototype draw in.
60
+ *
61
+ * Nothing new: the only module CSS is layout glue on Core tokens
62
+ * (`DocumentSignatureLayout.css`).
63
+ */
64
+ export const DocumentSignatureReviewScreen = forwardRef(({ strings, documents, state = 'start', typedName = '', interactive = false, onCanvasReady, onSignatureConfirm, screenId = 'document-electronic-signature-review', overrides, onAction, className, ...shell }, ref) => {
65
+ const pose = POSE[state];
66
+ const total = documents.length;
67
+ const many = total > 1;
68
+ const { review, sign } = strings;
69
+ const [read, setRead] = useState(pose.at === 'end');
70
+ const [current, setCurrent] = useState(pose.at === 'start' ? 1 : Math.min(2, total));
71
+ const [sheet, setSheet] = useState(pose.sheet);
72
+ // Motion (Elena, 2026-11-13: "refinar el motion cuando llego abajo y
73
+ // aparece el pop up"). Interactive only; a static spec card never moves.
74
+ // `closing` keeps the sheet mounted while it slides out. Every duration is
75
+ // read from Core's Sheet/Motion tokens, never typed here.
76
+ const [closing, setClosing] = useState(false);
77
+ const sheetMs = (which) => {
78
+ if (typeof window === 'undefined')
79
+ return 0;
80
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
81
+ return 0;
82
+ const v = getComputedStyle(document.documentElement).getPropertyValue(`--prizma-sheet-motion-${which}-duration`).trim();
83
+ const n = parseFloat(v);
84
+ return Number.isFinite(n) ? (v.endsWith('ms') ? n : n * 1000) : 0;
85
+ };
86
+ const [mode, setMode] = useState(pose.sheet === 'type' ? 'type' : 'draw');
87
+ const [inked, setInked] = useState(state === 'drawn');
88
+ const [name, setName] = useState(state === 'type' ? typedName : '');
89
+ const [padKey, setPadKey] = useState(0);
90
+ const viewer = useRef(null);
91
+ const panel = useRef(null);
92
+ const panelId = useId();
93
+ // A new pose from the host restarts the screen from it.
94
+ useEffect(() => {
95
+ setRead(pose.at === 'end');
96
+ setCurrent(pose.at === 'start' ? 1 : pose.at === 'second' ? Math.min(2, total) : total);
97
+ setSheet(pose.sheet);
98
+ setMode(pose.sheet === 'type' ? 'type' : 'draw');
99
+ setInked(state === 'drawn');
100
+ setName(state === 'type' ? typedName : '');
101
+ }, [state, typedName, total, pose.at, pose.sheet]);
102
+ // Which document is being read (the one crossing the top third of the
103
+ // viewer), and whether the end of the last one was reached. Offsets, not
104
+ // bounding boxes: a spec card scales the frame with a CSS transform, and
105
+ // the viewer is the sections' offset parent (DocumentSignatureLayout.css).
106
+ const measure = useCallback(() => {
107
+ const v = viewer.current;
108
+ // A viewer that has not laid out yet reads as "already at the end" and
109
+ // would unlock Sign before anything was read.
110
+ if (!v || !interactive || v.clientHeight < 40)
111
+ return;
112
+ let at = 1;
113
+ v.querySelectorAll('[data-doc]').forEach((d, i) => {
114
+ if (d.offsetTop - v.scrollTop <= v.clientHeight * 0.3)
115
+ at = i + 1;
116
+ });
117
+ setCurrent(at);
118
+ if (v.scrollTop + v.clientHeight >= v.scrollHeight - 2) {
119
+ // Reaching the end opens the sheet by itself, once (Elena,
120
+ // 2026-11-12); Sign reopens it after a dismiss.
121
+ // A beat first, one Sheet/Motion/Enter long: the scroll settles and
122
+ // the Stepper's "All documents read" lands before the sheet rises, so
123
+ // the two changes read one after the other instead of at once.
124
+ setRead((was) => {
125
+ if (!was)
126
+ window.setTimeout(() => setSheet((cur) => cur ?? mode), sheetMs('enter'));
127
+ return true;
128
+ });
129
+ }
130
+ }, [interactive, mode]);
131
+ // Place the viewer for the pose: the top, the start of document 2, or the end.
132
+ useLayoutEffect(() => {
133
+ const v = viewer.current;
134
+ if (!v)
135
+ return;
136
+ if (pose.at === 'end')
137
+ v.scrollTop = v.scrollHeight;
138
+ else if (pose.at === 'second') {
139
+ const d2 = v.querySelector('[data-doc="2"]');
140
+ v.scrollTop = d2 ? d2.offsetTop : 0;
141
+ }
142
+ else
143
+ v.scrollTop = 0;
144
+ }, [pose.at, total]);
145
+ // Measure again once the viewer has its real height.
146
+ useEffect(() => {
147
+ const v = viewer.current;
148
+ if (!v || !interactive || typeof ResizeObserver === 'undefined')
149
+ return;
150
+ const ro = new ResizeObserver(() => measure());
151
+ ro.observe(v);
152
+ return () => ro.disconnect();
153
+ }, [interactive, measure]);
154
+ // Focus moves into the sheet when it opens (it is a modal dialog).
155
+ useEffect(() => {
156
+ if (interactive && sheet)
157
+ panel.current?.focus();
158
+ }, [interactive, sheet]);
159
+ const ready = mode === 'draw' ? inked : name.trim().length > 1;
160
+ // Sign reopens it the same way the end of the documents opens it (Elena,
161
+ // 2026-11-13): the same Sheet/Motion/Enter beat, then the same rise.
162
+ const openSheet = () => {
163
+ if (!interactive || !read)
164
+ return;
165
+ window.setTimeout(() => setSheet((cur) => cur ?? mode), sheetMs('enter'));
166
+ };
167
+ // Close with Core's exit curve: the sheet slides down, then unmounts.
168
+ const close = () => {
169
+ const ms = sheetMs('exit');
170
+ if (!ms) {
171
+ setSheet(null);
172
+ return;
173
+ }
174
+ setClosing(true);
175
+ window.setTimeout(() => {
176
+ setSheet(null);
177
+ setClosing(false);
178
+ }, ms);
179
+ };
180
+ const dismiss = () => {
181
+ if (!interactive)
182
+ return;
183
+ close();
184
+ viewer.current?.focus();
185
+ };
186
+ const confirm = () => {
187
+ if (!ready)
188
+ return;
189
+ onSignatureConfirm?.(mode === 'draw' ? { method: 'draw' } : { method: 'type', name: name.trim() });
190
+ onAction?.({ type: 'cta', screenId });
191
+ if (interactive)
192
+ close();
193
+ };
194
+ // Switching modes clears what the other mode held (Elena, 2026-11-12:
195
+ // "clear on switch"), so Confirm only turns on for the signature on screen.
196
+ const switchMode = (next) => {
197
+ if (!interactive || next === mode)
198
+ return;
199
+ setPadKey((k) => k + 1);
200
+ setInked(false);
201
+ setName('');
202
+ setMode(next);
203
+ setSheet(next);
204
+ };
205
+ const clear = () => {
206
+ if (!interactive)
207
+ return;
208
+ setPadKey((k) => k + 1);
209
+ setInked(false);
210
+ };
211
+ // The pill moves the reader on by about a page. Reaching the end stays a
212
+ // scroll, so the viewer's own onScroll unlocks Sign, not the pill.
213
+ const scrollOn = () => {
214
+ const v = viewer.current;
215
+ if (!interactive || !v)
216
+ return;
217
+ const still = typeof window !== 'undefined' &&
218
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
219
+ v.scrollBy({ top: v.clientHeight * PILL_STEP, behavior: still ? 'auto' : 'smooth' });
220
+ };
221
+ const stepperState = read
222
+ ? 'Completed'
223
+ : current <= 1
224
+ ? 'Start'
225
+ : Math.min(current, MAX_STEPS);
226
+ const pad = (ghost) => (_jsx(PrizmaSignaturePad, { isFilled: ghost ? false : inked, isInitials: false, placeholder: sign.placeholder, canvasLabel: sign.canvasLabel, clearLabel: sign.clearLabel, onClear: ghost ? undefined : clear, onCanvasReady: ghost ? undefined : onCanvasReady }, ghost ? 'ghost' : padKey));
227
+ // The Type body keeps at least the Draw body's height, so the sheet does
228
+ // not jump when the mode switches (Elena, 2026-11-12). The exploration
229
+ // measured the Draw body in JS; here an invisible copy of the pad shares the
230
+ // Type body's grid cell, so the floor holds on a static spec card too.
231
+ // Layout glue, not a component: `visibility: hidden` keeps it out of the
232
+ // tab order and the accessibility tree.
233
+ const modeBody = mode === 'draw' ? (
234
+ // The first pointerdown on the canvas is ink: Core's pad flips its own
235
+ // Clear on it but reports nothing, so the screen hears it bubble up.
236
+ _jsx("div", { id: panelId, role: "tabpanel", "aria-label": sign.drawLabel, className: "document-signature-sheet-mode", onPointerDown: interactive ? () => setInked(true) : undefined, children: pad(false) })) : (_jsxs("div", { id: panelId, role: "tabpanel", "aria-label": sign.typeLabel, className: "document-signature-sheet-mode document-signature-sheet-mode--type", children: [_jsx("div", { className: "document-signature-sheet-mode__floor", "aria-hidden": "true", inert: '', children: pad(true) }), _jsxs("div", { className: "document-signature-sheet-mode__type", children: [_jsx(PrizmaTextField, { label: sign.nameLabel, ...(interactive ? { value: name, onValueChange: setName } : { defaultValue: name }), autoComplete: "name", autoCapitalize: "words", autoCorrect: "off", spellCheck: false, enterKeyHint: "done", onKeyDown: (e) => {
237
+ if (e.key === 'Enter' && ready) {
238
+ e.preventDefault();
239
+ confirm();
240
+ }
241
+ } }, interactive ? 'live' : `static-${name}`), _jsx(PrizmaSignaturePreview, { label: sign.previewLabel, name: name, placeholder: sign.previewPlaceholder })] })] }));
242
+ // Draw / Type sits right under the consent, at the top of the body, where
243
+ // the exploration's final Option B put it (it replaced the Secondary
244
+ // "Type your name instead / Draw instead" button).
245
+ const body = (_jsxs("div", { className: "document-signature-sheet-body", children: [_jsx(PrizmaSegmentedControl, { "aria-label": sign.methodLabel, value: mode, onValueChange: (v) => switchMode(v === 'type' ? 'type' : 'draw'), options: [
246
+ { value: 'draw', label: sign.drawLabel, controls: mode === 'draw' ? panelId : undefined },
247
+ { value: 'type', label: sign.typeLabel, controls: mode === 'type' ? panelId : undefined },
248
+ ] }), modeBody] }));
249
+ const overlay = sheet ? (_jsxs(_Fragment, { children: [_jsx("div", { className: `prizma-screen-layout__sheet-scrim${interactive ? ' document-signature-sheet-scrim--moving' : ''}${closing ? ' is-closing' : ''}`, "aria-hidden": "true", onClick: dismiss }), _jsx("div", { className: `prizma-screen-layout__sheet-panel${interactive ? ' document-signature-sheet-panel--moving' : ''}${closing ? ' is-closing' : ''}`, children: _jsx(PrizmaBottomSheetContent, { ref: panel, variant: "dialog", title: sign.title, subtitle: sign.consent, body: body, showBottomBar: true, onKeyDown: (e) => {
250
+ if (e.key === 'Escape')
251
+ dismiss();
252
+ }, buttonGroupProps: {
253
+ orientation: 'vertical-v1',
254
+ showPrimary: true,
255
+ showSecondary: false,
256
+ showTertiary: false,
257
+ primaryLabel: sign.confirmLabel,
258
+ primaryDisabled: !ready,
259
+ onPrimaryClick: confirm,
260
+ } }) })] })) : undefined;
261
+ return (_jsxs(PrizmaFormScreen, { ref: ref, ...shell, className: ['document-signature-screen', className].filter(Boolean).join(' '), title: review.title, fieldsOffset: "12", ctaLabel: review.ctaLabel, ctaDisabled: !read, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides,
262
+ // Sign is a step inside this screen, not the module's result: it opens
263
+ // the sheet and reports nothing. Under the sheet's scrim it is inert.
264
+ onAction: (a) => {
265
+ if (a.type === 'cta' && !sheet)
266
+ openSheet();
267
+ }, overlay: overlay, children: [many ? (_jsx(PrizmaStepper, { steps: Math.min(total, MAX_STEPS), currentState: stepperState, showIcons: false,
268
+ // The caption also names the progressbar (Core wires
269
+ // aria-labelledby when there is no aria-label), so the count is
270
+ // announced once.
271
+ caption: read ? review.completedLabel : count(review.documentCount, current, total), completedValueText: review.completedLabel })) : null, _jsxs("div", { ref: viewer, onScroll: interactive ? measure : undefined, className: "electronic-signature-document document-signature-viewer", role: "region", "aria-label": review.documentsLabel, tabIndex: 0, children: [documents.map((d, i) => (_jsxs("section", { "data-doc": i + 1, className: "document-signature-doc", children: [_jsx("h4", { className: "document-signature-doc__title", children: d.title }), d.content] }, `${i}-${d.title}`))), read ? null : (_jsx(PrizmaScrollPillDock, { children: _jsx(PrizmaScrollPill, { label: review.scrollLabel, onClick: scrollOn }) }))] })] }));
272
+ });
273
+ DocumentSignatureReviewScreen.displayName = 'DocumentSignatureReviewScreen';
274
+ export default DocumentSignatureReviewScreen;
@@ -0,0 +1,82 @@
1
+ import type { ElectronicSignatureStrings } from './ElectronicSignatureStrings';
2
+ /**
3
+ * The poses the review screen draws, deterministic from props:
4
+ *
5
+ * - `start` reading document 1, Sign disabled.
6
+ * - `second` document 2 reached in the same scroll, Stepper on 2.
7
+ * - `end` every document read, Stepper `Completed`, Sign live.
8
+ * - `draw` the sheet over the read screen, empty pad, Confirm disabled.
9
+ * - `drawn` the sheet with a signature on the pad, Confirm live.
10
+ * - `type` the sheet in type mode; `typedName` fills the field.
11
+ */
12
+ export type DocumentSignatureState = 'start' | 'second' | 'end' | 'draw' | 'drawn' | 'type';
13
+ export interface DocumentSignatureStrings {
14
+ /** "verified by" attribution copy for the footer tag. */
15
+ verifiedLabel: string;
16
+ /** Accessible name for the logo row. */
17
+ navLabel: string;
18
+ /** The review screen: every document in one scrolling viewer. */
19
+ review: {
20
+ /** H2 — "Review and sign". */
21
+ title: string;
22
+ /** Accessible name for the scrolling viewer — "Documents to sign". */
23
+ documentsLabel: string;
24
+ /**
25
+ * The Stepper's caption while reading, which also names its progressbar —
26
+ * "Document {{current}} of {{total}}". Both placeholders are replaced by
27
+ * the screen.
28
+ */
29
+ documentCount: string;
30
+ /**
31
+ * The Stepper's caption and `aria-valuetext` once every document is read —
32
+ * "All documents read".
33
+ */
34
+ completedLabel: string;
35
+ /**
36
+ * Core's Scroll pill over the viewer while there is reading left —
37
+ * "Scroll to sign". A tap scrolls about 80% of the viewer.
38
+ */
39
+ scrollLabel: string;
40
+ /** Primary CTA — "Sign". Disabled until the end of the last document. */
41
+ ctaLabel: string;
42
+ };
43
+ /** The signing sheet (Core's Bottom sheet, dialog variant). */
44
+ sign: {
45
+ /** Sheet title, the same in both modes — "Add your signature". */
46
+ title: string;
47
+ /** Accessible name for the Draw / Type Segmented control — "Signature method". */
48
+ methodLabel: string;
49
+ /** Segmented control, the draw option — "Draw". */
50
+ drawLabel: string;
51
+ /** Segmented control, the type option — "Type". */
52
+ typeLabel: string;
53
+ /** Sheet subtitle, the consent — "By confirming, you accept and sign the documents electronically." */
54
+ consent: string;
55
+ /** Pad placeholder — "Sign here". */
56
+ placeholder: string;
57
+ /** Accessible name for the drawing canvas — "Signature capture area". */
58
+ canvasLabel: string;
59
+ /** Core's Clear-canvas link — "Clear canvas". */
60
+ clearLabel: string;
61
+ /** Type mode field label — "Full name". */
62
+ nameLabel: string;
63
+ /** Core's Signature preview label, under the field — "Preview". */
64
+ previewLabel: string;
65
+ /** Signature preview while the field is empty — "Your signature". */
66
+ previewPlaceholder: string;
67
+ /** The only button — "Confirm and continue". */
68
+ confirmLabel: string;
69
+ };
70
+ /** The signed verdict, drawn by the shipped `ElectronicSignatureSuccessScreen`. */
71
+ success: ElectronicSignatureStrings['success'];
72
+ /** The failure verdict, drawn by the shipped `ElectronicSignatureErrorScreen`. */
73
+ error: ElectronicSignatureStrings['error'];
74
+ }
75
+ /**
76
+ * The verdict screens are Electronic Signature's own, reused as shipped
77
+ * (`ElectronicSignatureSuccessScreen`, `ElectronicSignatureErrorScreen`). They
78
+ * take that module's strings object and read only its `success` / `error`
79
+ * slices plus the two chrome labels; this maps the rest from this module's copy
80
+ * so nothing is left blank and no second copy of a sentence is kept.
81
+ */
82
+ export declare function documentSignatureResultStrings(strings: DocumentSignatureStrings): ElectronicSignatureStrings;
@@ -0,0 +1,39 @@
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
+ * The verdict screens are Electronic Signature's own, reused as shipped
14
+ * (`ElectronicSignatureSuccessScreen`, `ElectronicSignatureErrorScreen`). They
15
+ * take that module's strings object and read only its `success` / `error`
16
+ * slices plus the two chrome labels; this maps the rest from this module's copy
17
+ * so nothing is left blank and no second copy of a sentence is kept.
18
+ */
19
+ export function documentSignatureResultStrings(strings) {
20
+ return {
21
+ verifiedLabel: strings.verifiedLabel,
22
+ navLabel: strings.navLabel,
23
+ review: {
24
+ title: strings.review.title,
25
+ documentLabel: strings.review.documentsLabel,
26
+ ctaLabel: strings.review.ctaLabel,
27
+ },
28
+ draw: {
29
+ title: strings.sign.title,
30
+ subtitle: strings.sign.consent,
31
+ placeholder: strings.sign.placeholder,
32
+ canvasLabel: strings.sign.canvasLabel,
33
+ clearLabel: strings.sign.clearLabel,
34
+ ctaLabel: strings.sign.confirmLabel,
35
+ },
36
+ success: strings.success,
37
+ error: strings.error,
38
+ };
39
+ }