@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
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useContext, useState } from 'react';
|
|
4
4
|
import { useForgotPasswordMutation } from '@akinon/next/data/client/user';
|
|
5
5
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
6
|
import { readToggle } from './product-block-utils';
|
|
7
|
+
import {
|
|
8
|
+
AuthFieldErrors,
|
|
9
|
+
AuthFieldErrorsProvider,
|
|
10
|
+
clearAuthFieldError,
|
|
11
|
+
collectAuthFieldErrors
|
|
12
|
+
} from '../auth/auth-field-errors';
|
|
13
|
+
import {
|
|
14
|
+
AuthFormErrorProvider,
|
|
15
|
+
containsAuthErrorBlock
|
|
16
|
+
} from '../auth/form-error-context';
|
|
17
|
+
import {
|
|
18
|
+
clearFormSubmitState,
|
|
19
|
+
publishFormSubmitState
|
|
20
|
+
} from '../auth/form-submit-bus';
|
|
21
|
+
import {
|
|
22
|
+
AuthFlowIdContext,
|
|
23
|
+
resolveAuthStepTarget,
|
|
24
|
+
setAuthStep
|
|
25
|
+
} from '../auth/auth-flow-bus';
|
|
26
|
+
import { renderComposedChildren } from './render-composed-children';
|
|
7
27
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
28
|
|
|
9
29
|
/**
|
|
10
30
|
* Functional auth block: e-mail input that triggers the password reset mail
|
|
11
31
|
* through the v1 `forgotPassword` mutation. Behaviour is fixed (commerce sends
|
|
12
32
|
* the mail); the editor owns the labels and the styling.
|
|
33
|
+
*
|
|
34
|
+
* Composable, the same way the Auth Form is: with children the form draws THEM
|
|
35
|
+
* — an `auth-field` for the address, a Form Error block for the message, the
|
|
36
|
+
* CTA an ordinary Button — and reads the address back out of `FormData` on
|
|
37
|
+
* submit. Left empty it renders the fixed row exactly as before, so a published
|
|
38
|
+
* composition is untouched. Its pieces were reachable only as STYLE parts,
|
|
39
|
+
* which meant a brand could recolour the input but not put copy above it, move
|
|
40
|
+
* the message, or replace the button with the drawer's own bottom-bar CTA.
|
|
41
|
+
*
|
|
13
42
|
* Properties: submitLabel, busyLabel, successLabel, emailLabel,
|
|
14
|
-
* emailPlaceholder, showLabels, errorLabel, previewSuccess
|
|
15
|
-
*
|
|
43
|
+
* emailPlaceholder, showLabels, errorLabel, previewSuccess, requiredMessage,
|
|
44
|
+
* invalidMessage, formHtmlId, showSubmit, backLabel, backStepTarget,
|
|
45
|
+
* successStepTarget.
|
|
46
|
+
* Parts: form, field, label, input, error, submit, success, back.
|
|
16
47
|
*/
|
|
17
48
|
const ForgotPasswordBlock = ({
|
|
18
49
|
block,
|
|
50
|
+
placeholderId,
|
|
51
|
+
sectionId,
|
|
52
|
+
selectedBlockId,
|
|
19
53
|
currentBreakpoint = 'desktop',
|
|
20
54
|
isDesigner = false
|
|
21
55
|
}: BlockRendererProps) => {
|
|
@@ -50,46 +84,286 @@ const ForgotPasswordBlock = ({
|
|
|
50
84
|
currentBreakpoint,
|
|
51
85
|
false
|
|
52
86
|
);
|
|
87
|
+
/**
|
|
88
|
+
* The copy for an address left empty or typed wrongly — the brand's own
|
|
89
|
+
* sentence in place of the browser's bubble. See `auth-field-errors.tsx`.
|
|
90
|
+
*/
|
|
91
|
+
const requiredMessage = prop('requiredMessage', 'This field is required.');
|
|
92
|
+
const invalidMessage = prop('invalidMessage', 'Please check this field.');
|
|
93
|
+
/**
|
|
94
|
+
* Detached submit, the same pair the other auth forms carry: `formHtmlId`
|
|
95
|
+
* names the <form> so a Button block elsewhere can submit it through the
|
|
96
|
+
* native `form` attribute — which is how a drawer pins its CTA to the bottom
|
|
97
|
+
* bar — and `showSubmit` off hides the built-in button.
|
|
98
|
+
*/
|
|
99
|
+
const formHtmlId = prop('formHtmlId', '').trim();
|
|
100
|
+
const showSubmit = readToggle(
|
|
101
|
+
block.properties?.showSubmit,
|
|
102
|
+
currentBreakpoint,
|
|
103
|
+
true
|
|
104
|
+
);
|
|
105
|
+
/**
|
|
106
|
+
* The way back once the mail is on its way.
|
|
107
|
+
*
|
|
108
|
+
* Only shown in the success state, and only when the designer named a step:
|
|
109
|
+
* before submitting, the flow's own Back button is the way back, but AFTER
|
|
110
|
+
* submitting there is nothing left to do in this step and "check your inbox"
|
|
111
|
+
* with no exit is a dead end. Left empty the block behaves exactly as it
|
|
112
|
+
* did — the success message and nothing else.
|
|
113
|
+
*/
|
|
114
|
+
const backLabel = prop('backLabel', 'Back to log in');
|
|
115
|
+
const backStepTarget = prop('backStepTarget', '').trim();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Where the flow goes once the mail is on its way.
|
|
119
|
+
*
|
|
120
|
+
* The alternative to composing the success state inside this block: a step of
|
|
121
|
+
* the surrounding flow, authored out of ordinary blocks — a heading, the
|
|
122
|
+
* brand's own illustration, a "check your inbox" line, a Button back to the
|
|
123
|
+
* login step — which is more than a message and a link could ever be, and
|
|
124
|
+
* needs no new kind of block to express. Left empty the block shows its own
|
|
125
|
+
* success copy, exactly as it did.
|
|
126
|
+
*
|
|
127
|
+
* Same shape as the register form's `otpStepTarget` handoff, for the same
|
|
128
|
+
* reason: the form knows it has finished, the flow knows what to show next.
|
|
129
|
+
*/
|
|
130
|
+
const successStepTarget = prop('successStepTarget', '').trim();
|
|
131
|
+
|
|
132
|
+
const contextFlowId = useContext(AuthFlowIdContext);
|
|
133
|
+
const backStep = resolveAuthStepTarget(backStepTarget, contextFlowId);
|
|
134
|
+
const successStep = resolveAuthStepTarget(successStepTarget, contextFlowId);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Composed mode: the address is a child `auth-field`, the message a Form
|
|
138
|
+
* Error block, the CTA a Button. Same contract as every other auth form —
|
|
139
|
+
* children present means they draw the form, empty means the fixed row
|
|
140
|
+
* renders exactly as it always did.
|
|
141
|
+
*/
|
|
142
|
+
const childBlocks = (block.blocks || [])
|
|
143
|
+
.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
|
|
144
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
145
|
+
const isComposed = childBlocks.length > 0;
|
|
53
146
|
|
|
54
147
|
const [email, setEmail] = useState('');
|
|
55
148
|
const [error, setError] = useState('');
|
|
56
|
-
|
|
149
|
+
// Per FIELD, as opposed to `error`, which is what commerce answered about
|
|
150
|
+
// the request as a whole.
|
|
151
|
+
const [fieldErrors, setFieldErrors] = useState<AuthFieldErrors>({});
|
|
152
|
+
const [forgotPassword, { isLoading, isSuccess }] =
|
|
153
|
+
useForgotPasswordMutation();
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A message describes what the field WAS: it goes as soon as it is edited.
|
|
157
|
+
* Both events, because a keystroke raises `input` while autofill raises
|
|
158
|
+
* `change`.
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* A detached CTA (a Button carrying this form's id) is outside this subtree
|
|
162
|
+
* and cannot see `isLoading` — so the form publishes it. See
|
|
163
|
+
* `auth/form-submit-bus.ts`.
|
|
164
|
+
*/
|
|
165
|
+
React.useEffect(() => {
|
|
166
|
+
if (!formHtmlId) return undefined;
|
|
167
|
+
|
|
168
|
+
publishFormSubmitState(formHtmlId, isLoading);
|
|
169
|
+
|
|
170
|
+
return () => clearFormSubmitState(formHtmlId);
|
|
171
|
+
}, [formHtmlId, isLoading]);
|
|
172
|
+
|
|
173
|
+
const handleInput = (event: React.FormEvent<HTMLFormElement>) => {
|
|
174
|
+
const target = event.target as { name?: string } | null;
|
|
57
175
|
|
|
58
|
-
|
|
176
|
+
setFieldErrors((current) =>
|
|
177
|
+
clearAuthFieldError(current, target?.name || '')
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
59
182
|
event.preventDefault();
|
|
60
183
|
setError('');
|
|
61
184
|
|
|
62
185
|
// Submitting inside the designer would send a real reset mail.
|
|
63
186
|
if (isDesigner || isLoading) return;
|
|
64
187
|
|
|
188
|
+
// The form carries `noValidate`, so this is what stops an empty submit —
|
|
189
|
+
// and it says so under the field rather than in a browser bubble.
|
|
190
|
+
const { errors: nextFieldErrors, firstInvalid } = collectAuthFieldErrors(
|
|
191
|
+
event.currentTarget,
|
|
192
|
+
{ required: requiredMessage, invalid: invalidMessage }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
setFieldErrors(nextFieldErrors);
|
|
196
|
+
|
|
197
|
+
if (firstInvalid) {
|
|
198
|
+
firstInvalid.focus();
|
|
199
|
+
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Composed: the address lives in a child Auth Field, so it is read off the
|
|
204
|
+
// form the way every other composed auth form reads its fields, rather
|
|
205
|
+
// than from state no child writes to.
|
|
206
|
+
const address = isComposed
|
|
207
|
+
? String(new FormData(event.currentTarget).get('email') ?? '').trim()
|
|
208
|
+
: email;
|
|
209
|
+
|
|
65
210
|
try {
|
|
66
|
-
await forgotPassword({ email }).unwrap();
|
|
211
|
+
await forgotPassword({ email: address }).unwrap();
|
|
212
|
+
|
|
213
|
+
// The mail is away and there is nothing left to do in this step, so a
|
|
214
|
+
// flow that named a step for it moves there instead of swapping this
|
|
215
|
+
// block's own contents.
|
|
216
|
+
if (successStep) {
|
|
217
|
+
setAuthStep(successStep.flowId, successStep.stepKey);
|
|
218
|
+
}
|
|
67
219
|
} catch {
|
|
68
220
|
setError(errorLabel);
|
|
69
221
|
}
|
|
70
222
|
};
|
|
71
223
|
|
|
224
|
+
const backControl = backStep ? (
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
data-forgot-password-back=""
|
|
228
|
+
{...partAttrs('back')}
|
|
229
|
+
onClick={() => {
|
|
230
|
+
// The canvas has its own step switcher; a click here must not move the
|
|
231
|
+
// flow under the designer's feet. Same guard as `use-block-action`.
|
|
232
|
+
if (isDesigner) return;
|
|
233
|
+
|
|
234
|
+
setAuthStep(backStep.flowId, backStep.stepKey);
|
|
235
|
+
}}
|
|
236
|
+
style={{
|
|
237
|
+
alignSelf: 'flex-start',
|
|
238
|
+
padding: 0,
|
|
239
|
+
margin: 0,
|
|
240
|
+
border: 'none',
|
|
241
|
+
background: 'transparent',
|
|
242
|
+
color: 'inherit',
|
|
243
|
+
font: 'inherit',
|
|
244
|
+
textDecoration: 'underline',
|
|
245
|
+
cursor: 'pointer'
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{backLabel}
|
|
249
|
+
</button>
|
|
250
|
+
) : null;
|
|
251
|
+
|
|
72
252
|
// The success copy replaces the form so the shopper is not invited to submit
|
|
73
253
|
// the same address twice; the root attribute is kept so styling still applies.
|
|
74
254
|
if ((isSuccess && !isDesigner) || (isDesigner && previewSuccess)) {
|
|
75
255
|
return (
|
|
76
256
|
<div
|
|
77
257
|
data-forgot-password=""
|
|
258
|
+
// Fills its block wrapper; see the note in auth-step-block.tsx.
|
|
259
|
+
className="w-full"
|
|
78
260
|
data-forgot-password-state="success"
|
|
79
261
|
{...partAttrs('success')}
|
|
80
262
|
role="status"
|
|
81
263
|
>
|
|
82
264
|
{successLabel}
|
|
265
|
+
{backControl}
|
|
83
266
|
</div>
|
|
84
267
|
);
|
|
85
268
|
}
|
|
86
269
|
|
|
270
|
+
if (isComposed) {
|
|
271
|
+
/**
|
|
272
|
+
* The composed form: the children carry the address (`auth-field`), the
|
|
273
|
+
* message (a Form Error block) and whatever chrome the design wants around
|
|
274
|
+
* them, and submit reads the address back out of `FormData`. The built-in
|
|
275
|
+
* button stays available under its toggle so a fresh composition works
|
|
276
|
+
* before the designer has wired their own CTA.
|
|
277
|
+
*/
|
|
278
|
+
const errorIsAuthored = containsAuthErrorBlock(childBlocks);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<AuthFormErrorProvider value={error}>
|
|
282
|
+
<AuthFieldErrorsProvider value={fieldErrors}>
|
|
283
|
+
<form
|
|
284
|
+
id={formHtmlId || undefined}
|
|
285
|
+
data-forgot-password=""
|
|
286
|
+
data-forgot-password-composed=""
|
|
287
|
+
// Fills its block wrapper; see the note in auth-step-block.tsx.
|
|
288
|
+
className="w-full"
|
|
289
|
+
data-forgot-password-state={isLoading ? 'busy' : 'idle'}
|
|
290
|
+
{...partAttrs('form')}
|
|
291
|
+
onSubmit={submit}
|
|
292
|
+
// Our own message under the field instead of the browser's bubble
|
|
293
|
+
// over it; the constraints it reads stay on the controls.
|
|
294
|
+
noValidate
|
|
295
|
+
onInput={handleInput}
|
|
296
|
+
onChange={handleInput}
|
|
297
|
+
style={{
|
|
298
|
+
display: 'flex',
|
|
299
|
+
flexDirection: 'column',
|
|
300
|
+
gap: 'var(--forgot-password-gap, 12px)'
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{renderComposedChildren({
|
|
304
|
+
blocks: childBlocks,
|
|
305
|
+
placeholderId,
|
|
306
|
+
sectionId,
|
|
307
|
+
isDesigner,
|
|
308
|
+
selectedBlockId,
|
|
309
|
+
currentBreakpoint
|
|
310
|
+
})}
|
|
311
|
+
|
|
312
|
+
{/* The composition's own Form Error block draws the message where
|
|
313
|
+
the design wants it; this node is the fallback for one that has
|
|
314
|
+
not placed it. See `auth/form-error-context.tsx`. */}
|
|
315
|
+
{error && !errorIsAuthored && (
|
|
316
|
+
<div
|
|
317
|
+
data-forgot-password-error=""
|
|
318
|
+
{...partAttrs('error')}
|
|
319
|
+
role="alert"
|
|
320
|
+
>
|
|
321
|
+
{error}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{showSubmit && (
|
|
326
|
+
<button
|
|
327
|
+
type="submit"
|
|
328
|
+
data-forgot-password-submit=""
|
|
329
|
+
{...partAttrs('submit', {
|
|
330
|
+
loading: isLoading,
|
|
331
|
+
disabled: isLoading
|
|
332
|
+
})}
|
|
333
|
+
disabled={isLoading}
|
|
334
|
+
aria-busy={isLoading}
|
|
335
|
+
style={{
|
|
336
|
+
padding: '12px 20px',
|
|
337
|
+
background: 'var(--forgot-password-cta-bg, #111827)',
|
|
338
|
+
color: 'var(--forgot-password-cta-color, #ffffff)',
|
|
339
|
+
border: 'none',
|
|
340
|
+
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
341
|
+
opacity: isLoading ? 0.7 : 1
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
{isLoading && busyLabel ? busyLabel : submitLabel}
|
|
345
|
+
</button>
|
|
346
|
+
)}
|
|
347
|
+
</form>
|
|
348
|
+
</AuthFieldErrorsProvider>
|
|
349
|
+
</AuthFormErrorProvider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
87
353
|
return (
|
|
88
354
|
<form
|
|
355
|
+
id={formHtmlId || undefined}
|
|
89
356
|
data-forgot-password=""
|
|
357
|
+
// Fills its block wrapper; see the note in auth-step-block.tsx.
|
|
358
|
+
className="w-full"
|
|
90
359
|
data-forgot-password-state={isLoading ? 'busy' : 'idle'}
|
|
91
360
|
{...partAttrs('form')}
|
|
92
361
|
onSubmit={submit}
|
|
362
|
+
// Our own message under the field instead of the browser's bubble over
|
|
363
|
+
// it; `required` below is still what decides.
|
|
364
|
+
noValidate
|
|
365
|
+
onInput={handleInput}
|
|
366
|
+
onChange={handleInput}
|
|
93
367
|
style={{
|
|
94
368
|
display: 'flex',
|
|
95
369
|
flexDirection: 'column',
|
|
@@ -112,17 +386,35 @@ const ForgotPasswordBlock = ({
|
|
|
112
386
|
autoComplete="email"
|
|
113
387
|
placeholder={emailPlaceholder || undefined}
|
|
114
388
|
aria-label={showLabels ? undefined : emailLabel}
|
|
389
|
+
aria-invalid={fieldErrors.email ? true : undefined}
|
|
115
390
|
required
|
|
116
391
|
{...partAttrs('input')}
|
|
117
392
|
style={{
|
|
118
393
|
width: '100%',
|
|
119
|
-
border:
|
|
394
|
+
border: `1px solid ${
|
|
395
|
+
fieldErrors.email
|
|
396
|
+
? 'var(--forgot-password-border-invalid, #dc2626)'
|
|
397
|
+
: 'var(--forgot-password-border, #d1d5db)'
|
|
398
|
+
}`,
|
|
120
399
|
background: 'transparent',
|
|
121
400
|
color: 'inherit',
|
|
122
401
|
padding: '10px 12px',
|
|
123
402
|
outline: 'none'
|
|
124
403
|
}}
|
|
125
404
|
/>
|
|
405
|
+
{fieldErrors.email && (
|
|
406
|
+
<span
|
|
407
|
+
data-forgot-password-field-error="email"
|
|
408
|
+
{...partAttrs('error')}
|
|
409
|
+
role="alert"
|
|
410
|
+
style={{
|
|
411
|
+
color: 'var(--forgot-password-error-color, #dc2626)',
|
|
412
|
+
fontSize: '0.875em'
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
{fieldErrors.email}
|
|
416
|
+
</span>
|
|
417
|
+
)}
|
|
126
418
|
</label>
|
|
127
419
|
|
|
128
420
|
{error && (
|
|
@@ -131,23 +423,25 @@ const ForgotPasswordBlock = ({
|
|
|
131
423
|
</div>
|
|
132
424
|
)}
|
|
133
425
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
426
|
+
{showSubmit && (
|
|
427
|
+
<button
|
|
428
|
+
type="submit"
|
|
429
|
+
data-forgot-password-submit=""
|
|
430
|
+
{...partAttrs('submit', { loading: isLoading, disabled: isLoading })}
|
|
431
|
+
disabled={isLoading}
|
|
432
|
+
aria-busy={isLoading}
|
|
433
|
+
style={{
|
|
434
|
+
padding: '12px 20px',
|
|
435
|
+
background: 'var(--forgot-password-cta-bg, #111827)',
|
|
436
|
+
color: 'var(--forgot-password-cta-color, #ffffff)',
|
|
437
|
+
border: 'none',
|
|
438
|
+
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
439
|
+
opacity: isLoading ? 0.7 : 1
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
{isLoading && busyLabel ? busyLabel : submitLabel}
|
|
443
|
+
</button>
|
|
444
|
+
)}
|
|
151
445
|
</form>
|
|
152
446
|
);
|
|
153
447
|
};
|
|
@@ -5,50 +5,16 @@ import NextLink from 'next/link';
|
|
|
5
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
6
6
|
import { LocaleUrlStrategy } from '@akinon/next/localization';
|
|
7
7
|
import { urlLocaleMatcherRegex, urlSchemes } from '@akinon/next/utils';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getInnerRootInlineStyles,
|
|
10
|
+
getResponsiveValue,
|
|
11
|
+
partAttrs
|
|
12
|
+
} from '../utils';
|
|
13
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
9
14
|
import { useBlockAction } from '../hooks/use-block-action';
|
|
10
15
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
11
16
|
import ThemeBlock from '../theme-block';
|
|
12
17
|
|
|
13
|
-
/**
|
|
14
|
-
* The lines and gaps of the block's box, which this anchor must not draw a
|
|
15
|
-
* second time.
|
|
16
|
-
*
|
|
17
|
-
* `data-block-id` sits on the designer wrapper this anchor renders inside, and
|
|
18
|
-
* the generated stylesheet already paints the block's box there. Copying these
|
|
19
|
-
* keys inline as well draws them twice at two different sizes: the wrapper is
|
|
20
|
-
* the grid item and stretches to fill its row, while the anchor only hugs its
|
|
21
|
-
* content. Measured on a live page: one border set on a card rendered as two,
|
|
22
|
-
* one closing under the label and another at the bottom of the row. Padding
|
|
23
|
-
* and margin compound the same way — the spacing came out doubled.
|
|
24
|
-
*
|
|
25
|
-
* `background` is deliberately NOT here. Painting the same colour on both
|
|
26
|
-
* boxes is idempotent, so there is no artifact to fix, and an iterator resolves
|
|
27
|
-
* a bound colour per row into this inline copy — the shared stylesheet rule
|
|
28
|
-
* cannot hold a different value per row, so stripping it would lose it.
|
|
29
|
-
*
|
|
30
|
-
* Layout keys stay inline too. The wrapper's only child is this anchor, so
|
|
31
|
-
* `display: flex` up there arranges nothing — the anchor is what lays out the
|
|
32
|
-
* card's image and label.
|
|
33
|
-
*
|
|
34
|
-
* A row-bound `border-color` is the one thing this gives up: it would tint a
|
|
35
|
-
* border the anchor no longer has. Row data carries prices and colours for the
|
|
36
|
-
* content, not the outline of the box it sits in.
|
|
37
|
-
*/
|
|
38
|
-
const BOX_STYLE_PREFIXES = [
|
|
39
|
-
'margin',
|
|
40
|
-
'padding',
|
|
41
|
-
'border',
|
|
42
|
-
'outline',
|
|
43
|
-
'box-shadow'
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const isBoxStyleKey = (normalizedKey: string): boolean =>
|
|
47
|
-
BOX_STYLE_PREFIXES.some(
|
|
48
|
-
(prefix) =>
|
|
49
|
-
normalizedKey === prefix || normalizedKey.startsWith(`${prefix}-`)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
18
|
import { postToEditor } from '../editor-bridge';
|
|
53
19
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
54
20
|
const LinkBlock = ({
|
|
@@ -60,6 +26,7 @@ const LinkBlock = ({
|
|
|
60
26
|
isDesigner = false
|
|
61
27
|
}: BlockRendererProps) => {
|
|
62
28
|
const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
|
|
29
|
+
const themeSettings = useThemeSettingsContext();
|
|
63
30
|
const action = useBlockAction(block, { currentBreakpoint, isDesigner });
|
|
64
31
|
|
|
65
32
|
/**
|
|
@@ -144,50 +111,19 @@ const LinkBlock = ({
|
|
|
144
111
|
return trimmedHref || '#';
|
|
145
112
|
}, [href, defaultLocaleValue, locale, localeUrlStrategy]);
|
|
146
113
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const inlineStyles: React.CSSProperties = {};
|
|
162
|
-
if (block.styles) {
|
|
163
|
-
const keys = Object.keys(block.styles);
|
|
164
|
-
keys.forEach((key) => {
|
|
165
|
-
const normalizedKey = normalizeKey(key);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
normalizedKey === 'hover-flex' ||
|
|
169
|
-
normalizedKey === 'hover-child-flex' ||
|
|
170
|
-
normalizedKey === 'first-child-flex'
|
|
171
|
-
) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const styleEntry = getStyleEntry(normalizedKey);
|
|
176
|
-
if (styleEntry) {
|
|
177
|
-
const val = getResponsiveValue(styleEntry, currentBreakpoint);
|
|
178
|
-
if (val !== undefined && val !== null) {
|
|
179
|
-
if (isBoxStyleKey(normalizedKey)) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const camelKey = normalizedKey.replace(/-([a-z])/g, (_, letter) =>
|
|
184
|
-
letter.toUpperCase()
|
|
185
|
-
);
|
|
186
|
-
(inlineStyles as any)[camelKey] = val;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
114
|
+
/**
|
|
115
|
+
* Only the values the generated stylesheet cannot produce; the rest now
|
|
116
|
+
* arrives as a rule against this element (`data-part="root"`). Inlining the
|
|
117
|
+
* whole style object here applied every padding twice — once on the designer
|
|
118
|
+
* wrapper, once on the anchor — and shadowed the media queries and `:hover`
|
|
119
|
+
* the same stylesheet emits.
|
|
120
|
+
*/
|
|
121
|
+
const inlineStyles = getInnerRootInlineStyles(
|
|
122
|
+
block.styles as Record<string, unknown> | undefined,
|
|
123
|
+
themeSettings,
|
|
124
|
+
currentBreakpoint,
|
|
125
|
+
block.rowStyleKeys
|
|
126
|
+
);
|
|
191
127
|
|
|
192
128
|
if (!block.blocks || block.blocks.length === 0) {
|
|
193
129
|
return (
|
|
@@ -195,6 +131,7 @@ const LinkBlock = ({
|
|
|
195
131
|
href={isDesigner ? '#' : formattedHref}
|
|
196
132
|
target={isDesigner ? undefined : target}
|
|
197
133
|
rel={isDesigner ? undefined : rel}
|
|
134
|
+
{...partAttrs('root')}
|
|
198
135
|
style={inlineStyles}
|
|
199
136
|
onClick={handleClick}
|
|
200
137
|
{...action.ariaProps}
|
|
@@ -211,6 +148,7 @@ const LinkBlock = ({
|
|
|
211
148
|
href={isDesigner ? '#' : formattedHref}
|
|
212
149
|
target={isDesigner ? undefined : target}
|
|
213
150
|
rel={isDesigner ? undefined : rel}
|
|
151
|
+
{...partAttrs('root')}
|
|
214
152
|
style={inlineStyles}
|
|
215
153
|
onClick={handleClick}
|
|
216
154
|
{...action.ariaProps}
|