@akinon/pz-theme 2.0.83-beta.0 → 2.0.83

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +31 -7
  2. package/package.json +4 -4
  3. package/src/__tests__/auth-flow-block.test.tsx +211 -0
  4. package/src/__tests__/auth-flow-bus.test.ts +290 -0
  5. package/src/__tests__/auth-form-error-block.test.tsx +326 -0
  6. package/src/__tests__/auth-form-validation.test.tsx +204 -0
  7. package/src/__tests__/auth-step-title.test.tsx +202 -0
  8. package/src/__tests__/detached-submit-state.test.tsx +216 -0
  9. package/src/__tests__/forgot-password-composed.test.tsx +313 -0
  10. package/src/__tests__/otp-form-composed.test.tsx +487 -0
  11. package/src/__tests__/password-reset-step.test.tsx +269 -0
  12. package/src/__tests__/register-form-composed.test.tsx +739 -0
  13. package/src/__tests__/registry-parity.test.ts +9 -0
  14. package/src/auth/auth-field-errors.tsx +151 -0
  15. package/src/auth/auth-flow-bus.ts +415 -0
  16. package/src/auth/form-error-context.tsx +55 -0
  17. package/src/auth/form-submit-bus.ts +93 -0
  18. package/src/auth/otp-form-context.tsx +80 -0
  19. package/src/blocks/__tests__/button-block.test.tsx +55 -0
  20. package/src/blocks/__tests__/link-block.test.tsx +48 -30
  21. package/src/blocks/__tests__/overlay-sweep.test.tsx +31 -8
  22. package/src/blocks/__tests__/product-gallery-block.test.tsx +12 -4
  23. package/src/blocks/__tests__/product-recommendations-block.test.tsx +9 -2
  24. package/src/blocks/__tests__/store-locator-block.test.tsx +35 -8
  25. package/src/blocks/__tests__/text-block.test.tsx +37 -3
  26. package/src/blocks/__tests__/variant-selector-block.test.tsx +16 -4
  27. package/src/blocks/auth-consent-block.tsx +214 -0
  28. package/src/blocks/auth-error-block.tsx +82 -0
  29. package/src/blocks/auth-field-block.tsx +270 -0
  30. package/src/blocks/auth-flow-block.tsx +251 -0
  31. package/src/blocks/auth-form-block.tsx +391 -47
  32. package/src/blocks/auth-step-block.tsx +81 -0
  33. package/src/blocks/auth-step-title-block.tsx +81 -0
  34. package/src/blocks/block-renderer-registry.tsx +18 -0
  35. package/src/blocks/button-block.tsx +91 -13
  36. package/src/blocks/forgot-password-block.tsx +318 -24
  37. package/src/blocks/link-block.tsx +22 -84
  38. package/src/blocks/otp-code-input-block.tsx +180 -0
  39. package/src/blocks/otp-form-block.tsx +1092 -0
  40. package/src/blocks/otp-phase-block.tsx +71 -0
  41. package/src/blocks/register-form-block.tsx +541 -74
  42. package/src/blocks/text-block.tsx +17 -40
  43. package/src/chrome/__tests__/theme-config-source.test.ts +4 -1
  44. package/src/hooks/__tests__/use-overlay.test.tsx +97 -5
  45. package/src/hooks/use-block-action.ts +87 -4
  46. package/src/hooks/use-overlay.ts +60 -9
  47. package/src/overlays/__tests__/modal-chrome-padding.test.tsx +119 -0
  48. package/src/overlays/__tests__/modal-registry.test.ts +6 -1
  49. package/src/overlays/__tests__/panel-stretch.test.tsx +60 -0
  50. package/src/overlays/__tests__/responsive-chrome.test.tsx +446 -0
  51. package/src/overlays/__tests__/responsive-presentation.test.tsx +192 -0
  52. package/src/overlays/__tests__/theme-modal-mount.test.tsx +12 -1
  53. package/src/overlays/modal-preview-host.tsx +39 -2
  54. package/src/overlays/modal-registry.ts +344 -4
  55. package/src/overlays/overlay-surface.tsx +249 -19
  56. package/src/overlays/overlay-tokens.ts +94 -0
  57. package/src/overlays/theme-modal-mount.tsx +34 -4
  58. package/src/theme-block.tsx +6 -0
  59. package/src/utils/__tests__/inner-root-styles.test.tsx +139 -0
  60. package/src/utils/__tests__/iterator-bindings.test.tsx +9 -3
  61. package/src/utils/index.ts +107 -6
  62. package/src/utils/iterator-utils.ts +39 -2
  63. 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;