@akinon/pz-theme 2.0.83-rc.0 → 2.0.84-beta.0
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/CHANGELOG.md +39 -33
- package/package.json +4 -4
- package/src/__tests__/auth-flow-block.test.tsx +211 -0
- package/src/__tests__/auth-flow-bus.test.ts +290 -0
- package/src/__tests__/auth-form-error-block.test.tsx +326 -0
- package/src/__tests__/auth-form-validation.test.tsx +204 -0
- package/src/__tests__/auth-step-title.test.tsx +202 -0
- package/src/__tests__/detached-submit-state.test.tsx +216 -0
- package/src/__tests__/forgot-password-composed.test.tsx +313 -0
- package/src/__tests__/otp-form-composed.test.tsx +487 -0
- package/src/__tests__/password-reset-step.test.tsx +269 -0
- package/src/__tests__/register-form-composed.test.tsx +739 -0
- package/src/__tests__/registry-parity.test.ts +9 -0
- package/src/auth/auth-field-errors.tsx +151 -0
- package/src/auth/auth-flow-bus.ts +415 -0
- package/src/auth/form-error-context.tsx +55 -0
- package/src/auth/form-submit-bus.ts +93 -0
- package/src/auth/otp-form-context.tsx +80 -0
- package/src/blocks/__tests__/button-block.test.tsx +55 -0
- package/src/blocks/__tests__/link-block.test.tsx +48 -30
- package/src/blocks/__tests__/overlay-sweep.test.tsx +31 -8
- package/src/blocks/__tests__/product-gallery-block.test.tsx +12 -4
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +9 -2
- package/src/blocks/__tests__/store-locator-block.test.tsx +35 -8
- package/src/blocks/__tests__/text-block.test.tsx +37 -3
- package/src/blocks/__tests__/variant-selector-block.test.tsx +16 -4
- package/src/blocks/auth-consent-block.tsx +214 -0
- package/src/blocks/auth-error-block.tsx +82 -0
- package/src/blocks/auth-field-block.tsx +270 -0
- package/src/blocks/auth-flow-block.tsx +251 -0
- package/src/blocks/auth-form-block.tsx +391 -47
- package/src/blocks/auth-step-block.tsx +81 -0
- package/src/blocks/auth-step-title-block.tsx +81 -0
- package/src/blocks/block-renderer-registry.tsx +18 -0
- package/src/blocks/button-block.tsx +91 -13
- package/src/blocks/forgot-password-block.tsx +318 -24
- package/src/blocks/link-block.tsx +22 -84
- package/src/blocks/otp-code-input-block.tsx +180 -0
- package/src/blocks/otp-form-block.tsx +1092 -0
- package/src/blocks/otp-phase-block.tsx +71 -0
- package/src/blocks/register-form-block.tsx +541 -74
- package/src/blocks/text-block.tsx +17 -40
- package/src/chrome/__tests__/theme-config-source.test.ts +4 -1
- package/src/hooks/__tests__/use-overlay.test.tsx +97 -5
- package/src/hooks/use-block-action.ts +87 -4
- package/src/hooks/use-overlay.ts +60 -9
- package/src/overlays/__tests__/modal-chrome-padding.test.tsx +119 -0
- package/src/overlays/__tests__/modal-registry.test.ts +6 -1
- package/src/overlays/__tests__/panel-stretch.test.tsx +60 -0
- package/src/overlays/__tests__/responsive-chrome.test.tsx +446 -0
- package/src/overlays/__tests__/responsive-presentation.test.tsx +192 -0
- package/src/overlays/__tests__/theme-modal-mount.test.tsx +12 -1
- package/src/overlays/modal-preview-host.tsx +39 -2
- package/src/overlays/modal-registry.ts +344 -4
- package/src/overlays/overlay-surface.tsx +249 -19
- package/src/overlays/overlay-tokens.ts +94 -0
- package/src/overlays/theme-modal-mount.tsx +34 -4
- package/src/theme-block.tsx +6 -0
- package/src/utils/__tests__/inner-root-styles.test.tsx +139 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +9 -3
- package/src/utils/index.ts +107 -6
- package/src/utils/iterator-utils.ts +39 -2
- package/src/utils/part-styles.ts +4 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { partAttrs } from '../utils';
|
|
5
|
+
import { readToggle } from './product-block-utils';
|
|
6
|
+
import { useOtpForm } from '../auth/otp-form-context';
|
|
7
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The code boxes of an SMS verification, as a block of its own.
|
|
11
|
+
*
|
|
12
|
+
* The one piece of a composed OTP form that cannot be a dumb child: a box has
|
|
13
|
+
* to write into the ONE code the form verifies, and the caret has to walk to
|
|
14
|
+
* the next empty box as digits arrive. So the boxes stay together in one block
|
|
15
|
+
* and read the form through `useOtpForm` — everything else about the phase (the
|
|
16
|
+
* copy, the CTA, the resend) is an ordinary block the designer arranges.
|
|
17
|
+
*
|
|
18
|
+
* Standalone it draws the same row, disabled: a designer dropping it on a page
|
|
19
|
+
* sees what it is instead of nothing, and there is no code to collect there.
|
|
20
|
+
* How many boxes that preview shows is not worth a setting — the number that
|
|
21
|
+
* matters is the length the form verifies, and the form owns it — so the row
|
|
22
|
+
* outside a form is a fixed `STANDALONE_BOXES` wide. The editor used to offer a
|
|
23
|
+
* box count here, which inside a form (where the block lives) could only sit at
|
|
24
|
+
* a value the canvas ignored; authors read that as the setting being broken.
|
|
25
|
+
*
|
|
26
|
+
* Properties: showCountdown.
|
|
27
|
+
* Parts: row, box, countdown.
|
|
28
|
+
*/
|
|
29
|
+
const RING_RADIUS = 16;
|
|
30
|
+
|
|
31
|
+
/** The disabled preview's width, when there is no form to take it from. */
|
|
32
|
+
const STANDALONE_BOXES = 6;
|
|
33
|
+
|
|
34
|
+
const OtpCodeInputBlock = ({
|
|
35
|
+
block,
|
|
36
|
+
currentBreakpoint = 'desktop',
|
|
37
|
+
isDesigner = false
|
|
38
|
+
}: BlockRendererProps) => {
|
|
39
|
+
const form = useOtpForm();
|
|
40
|
+
/**
|
|
41
|
+
* The row's own handles on its boxes. The form is handed the same elements
|
|
42
|
+
* (it walks the caret forward as digits arrive), but stepping BACK on a
|
|
43
|
+
* Backspace is the row's own business and needs no round trip through it.
|
|
44
|
+
*/
|
|
45
|
+
const boxesRef = React.useRef<Array<HTMLInputElement | null>>([]);
|
|
46
|
+
|
|
47
|
+
const showCountdown = readToggle(
|
|
48
|
+
block.properties?.showCountdown,
|
|
49
|
+
currentBreakpoint,
|
|
50
|
+
true
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const codeLength = form?.codeLength ?? STANDALONE_BOXES;
|
|
54
|
+
const code = form?.code ?? '';
|
|
55
|
+
const secondsLeft = form?.secondsLeft ?? 0;
|
|
56
|
+
const timerSeconds = form?.timerSeconds ?? 0;
|
|
57
|
+
const inDesigner = form?.isDesigner ?? isDesigner;
|
|
58
|
+
|
|
59
|
+
// In the canvas the ring shows a full circle rather than a live count: there
|
|
60
|
+
// is no send to count down from, and an empty ring reads as a bug.
|
|
61
|
+
const displayedSeconds = inDesigner ? timerSeconds : secondsLeft;
|
|
62
|
+
const countdownRatio =
|
|
63
|
+
timerSeconds > 0 ? Math.max(0, displayedSeconds / timerSeconds) : 0;
|
|
64
|
+
const ringCircumference = 2 * Math.PI * RING_RADIUS;
|
|
65
|
+
|
|
66
|
+
const countdown =
|
|
67
|
+
showCountdown && timerSeconds > 0 && (inDesigner || secondsLeft > 0) ? (
|
|
68
|
+
<span
|
|
69
|
+
data-otp-code-countdown=""
|
|
70
|
+
{...partAttrs('countdown')}
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
style={{ display: 'inline-flex', flexShrink: 0 }}
|
|
73
|
+
>
|
|
74
|
+
<svg width="40" height="40" viewBox="0 0 40 40" role="presentation">
|
|
75
|
+
<circle
|
|
76
|
+
cx="20"
|
|
77
|
+
cy="20"
|
|
78
|
+
r={RING_RADIUS}
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="var(--otp-form-ring-track, #e5e7eb)"
|
|
81
|
+
strokeWidth="3"
|
|
82
|
+
/>
|
|
83
|
+
<circle
|
|
84
|
+
cx="20"
|
|
85
|
+
cy="20"
|
|
86
|
+
r={RING_RADIUS}
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="var(--otp-form-ring, #111827)"
|
|
89
|
+
strokeWidth="3"
|
|
90
|
+
strokeLinecap="round"
|
|
91
|
+
strokeDasharray={ringCircumference}
|
|
92
|
+
strokeDashoffset={ringCircumference * (1 - countdownRatio)}
|
|
93
|
+
transform="rotate(-90 20 20)"
|
|
94
|
+
/>
|
|
95
|
+
<text
|
|
96
|
+
x="20"
|
|
97
|
+
y="24"
|
|
98
|
+
textAnchor="middle"
|
|
99
|
+
fontSize="12"
|
|
100
|
+
fill="currentColor"
|
|
101
|
+
>
|
|
102
|
+
{displayedSeconds}
|
|
103
|
+
</text>
|
|
104
|
+
</svg>
|
|
105
|
+
</span>
|
|
106
|
+
) : null;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
data-otp-code=""
|
|
111
|
+
{...partAttrs('row')}
|
|
112
|
+
style={{
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: 'var(--otp-form-code-gap, 8px)'
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{countdown}
|
|
119
|
+
|
|
120
|
+
{Array.from({ length: codeLength }, (_, index) => (
|
|
121
|
+
<input
|
|
122
|
+
key={index}
|
|
123
|
+
ref={(element) => {
|
|
124
|
+
boxesRef.current[index] = element;
|
|
125
|
+
form?.registerCodeInput(index, element);
|
|
126
|
+
}}
|
|
127
|
+
type="text"
|
|
128
|
+
inputMode="numeric"
|
|
129
|
+
autoComplete={index === 0 ? 'one-time-code' : 'off'}
|
|
130
|
+
aria-label={`${index + 1} / ${codeLength}`}
|
|
131
|
+
// No name on purpose: the code is read from the form's own state, and
|
|
132
|
+
// a named box would put half a code into the submitted payload.
|
|
133
|
+
// A blank stands for a box the shopper skipped; it draws empty.
|
|
134
|
+
value={(code[index] ?? '').trim()}
|
|
135
|
+
disabled={!form}
|
|
136
|
+
// The box the digit was typed in, not the end of the code: a row of
|
|
137
|
+
// boxes has to let a shopper go back and fix one.
|
|
138
|
+
onChange={(event) =>
|
|
139
|
+
form?.setDigitAt(index, event.target.value.replace(/\D/g, ''))
|
|
140
|
+
}
|
|
141
|
+
onKeyDown={(event) => {
|
|
142
|
+
if (event.key === 'Backspace') {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
|
|
145
|
+
// Empty this box, or step back into the one before it when this
|
|
146
|
+
// one is already empty — what every code row does.
|
|
147
|
+
const digit = (code[index] ?? '').trim();
|
|
148
|
+
|
|
149
|
+
if (digit) {
|
|
150
|
+
form?.clearDigitAt(index);
|
|
151
|
+
} else if (index > 0) {
|
|
152
|
+
form?.clearDigitAt(index - 1);
|
|
153
|
+
boxesRef.current[index - 1]?.focus();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
onPaste={(event) => {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
form?.appendDigits(
|
|
160
|
+
event.clipboardData.getData('text').replace(/\D/g, '')
|
|
161
|
+
);
|
|
162
|
+
}}
|
|
163
|
+
{...partAttrs('box')}
|
|
164
|
+
style={{
|
|
165
|
+
width: '40px',
|
|
166
|
+
height: '48px',
|
|
167
|
+
textAlign: 'center',
|
|
168
|
+
fontSize: '18px',
|
|
169
|
+
border: '1px solid var(--otp-form-border, #d1d5db)',
|
|
170
|
+
background: 'transparent',
|
|
171
|
+
color: 'inherit',
|
|
172
|
+
outline: 'none'
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default OtpCodeInputBlock;
|