@akinon/pz-theme 2.0.38-rc.0 → 2.0.38
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 +7 -31
- package/package.json +4 -4
- package/src/__tests__/chrome-takeover-gate.test.tsx +252 -0
- package/src/__tests__/registry-parity.test.ts +14 -1
- package/src/__tests__/route-slug.test.ts +116 -0
- package/src/blocks/__tests__/overlay-sweep.test.tsx +592 -0
- package/src/blocks/account-button-block.tsx +133 -0
- package/src/blocks/announcement-bar-block.tsx +407 -0
- package/src/blocks/block-renderer-registry.tsx +26 -0
- package/src/blocks/currency-selector-block.tsx +177 -0
- package/src/blocks/hamburger-menu-block.tsx +461 -0
- package/src/blocks/language-selector-block.tsx +172 -0
- package/src/blocks/mega-menu-block.tsx +712 -0
- package/src/blocks/mini-basket-block.tsx +173 -0
- package/src/blocks/nav-menu-block.tsx +309 -0
- package/src/blocks/newsletter-form-block.tsx +184 -0
- package/src/blocks/payment-badges-block.tsx +393 -0
- package/src/blocks/search-box-block.tsx +277 -0
- package/src/blocks/social-links-block.tsx +391 -0
- package/src/blocks/wishlist-button-block.tsx +115 -0
- package/src/chrome/chrome-fallback.tsx +147 -0
- package/src/chrome/chrome-slot.tsx +68 -0
- package/src/chrome/composition-state.ts +92 -0
- package/src/chrome/page-context.ts +387 -0
- package/src/chrome/placeholder-slugs.ts +86 -0
- package/src/chrome/route-slug.ts +159 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { useSession } from 'next-auth/react';
|
|
6
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
7
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
|
+
import { readToggle } from './product-block-utils';
|
|
9
|
+
|
|
10
|
+
const clampNumber = (
|
|
11
|
+
value: unknown,
|
|
12
|
+
fallback: number,
|
|
13
|
+
min: number,
|
|
14
|
+
max: number
|
|
15
|
+
) => {
|
|
16
|
+
const numeric = Math.round(Number(value));
|
|
17
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
18
|
+
return Math.min(max, Math.max(min, numeric));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Functional chrome block: session-aware account entry point. Renders a
|
|
23
|
+
* login link for guests and an account link for authenticated users; the
|
|
24
|
+
* editor owns placement, labels, icon and styling.
|
|
25
|
+
* Properties: accountHref, loginHref, accountLabel, loginLabel, showLabel,
|
|
26
|
+
* showIcon, iconSize, gap, labelPosition, ariaLabel.
|
|
27
|
+
* Parts: link, icon, label. The signed-in state stays readable from custom
|
|
28
|
+
* CSS through the block's own `data-auth-state` attribute.
|
|
29
|
+
*/
|
|
30
|
+
const AccountButtonBlock = ({
|
|
31
|
+
block,
|
|
32
|
+
currentBreakpoint = 'desktop',
|
|
33
|
+
isDesigner = false
|
|
34
|
+
}: BlockRendererProps) => {
|
|
35
|
+
const { status } = useSession();
|
|
36
|
+
const isAuthenticated = status === 'authenticated';
|
|
37
|
+
|
|
38
|
+
const prop = (key: string, fallback: string) =>
|
|
39
|
+
String(
|
|
40
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const href = isAuthenticated
|
|
44
|
+
? prop('accountHref', '/account')
|
|
45
|
+
: prop('loginHref', '/auth');
|
|
46
|
+
const label = isAuthenticated
|
|
47
|
+
? prop('accountLabel', 'Account')
|
|
48
|
+
: prop('loginLabel', 'Login');
|
|
49
|
+
const showLabel =
|
|
50
|
+
getResponsiveValue(
|
|
51
|
+
block.properties?.showLabel,
|
|
52
|
+
currentBreakpoint,
|
|
53
|
+
false
|
|
54
|
+
) === true ||
|
|
55
|
+
String(
|
|
56
|
+
getResponsiveValue(
|
|
57
|
+
block.properties?.showLabel,
|
|
58
|
+
currentBreakpoint,
|
|
59
|
+
'false'
|
|
60
|
+
)
|
|
61
|
+
) === 'true';
|
|
62
|
+
const showIcon = readToggle(
|
|
63
|
+
block.properties?.showIcon,
|
|
64
|
+
currentBreakpoint,
|
|
65
|
+
true
|
|
66
|
+
);
|
|
67
|
+
const iconSize = clampNumber(
|
|
68
|
+
getResponsiveValue(block.properties?.iconSize, currentBreakpoint, 24),
|
|
69
|
+
24,
|
|
70
|
+
12,
|
|
71
|
+
64
|
|
72
|
+
);
|
|
73
|
+
const gap = clampNumber(
|
|
74
|
+
getResponsiveValue(block.properties?.gap, currentBreakpoint, 6),
|
|
75
|
+
6,
|
|
76
|
+
0,
|
|
77
|
+
32
|
|
78
|
+
);
|
|
79
|
+
const labelPosition = prop('labelPosition', 'after');
|
|
80
|
+
// Empty on purpose: the link has always announced itself with its own
|
|
81
|
+
// label, so an unset override must keep doing exactly that.
|
|
82
|
+
const ariaLabel = prop('ariaLabel', '');
|
|
83
|
+
|
|
84
|
+
const labelNode = showLabel ? (
|
|
85
|
+
<span data-account-label="" {...partAttrs('label')}>
|
|
86
|
+
{label}
|
|
87
|
+
</span>
|
|
88
|
+
) : null;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<NextLink
|
|
92
|
+
href={href}
|
|
93
|
+
data-account-button=""
|
|
94
|
+
data-auth-state={isAuthenticated ? 'authenticated' : 'guest'}
|
|
95
|
+
{...partAttrs('link')}
|
|
96
|
+
aria-label={ariaLabel || label}
|
|
97
|
+
// Navigating inside the designer iframe would drop the author out of the
|
|
98
|
+
// page they are editing.
|
|
99
|
+
onClick={(event) => {
|
|
100
|
+
if (isDesigner) event.preventDefault();
|
|
101
|
+
}}
|
|
102
|
+
style={{
|
|
103
|
+
display: 'inline-flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: `${gap}px`,
|
|
106
|
+
color: 'inherit',
|
|
107
|
+
textDecoration: 'none'
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{labelPosition === 'before' && labelNode}
|
|
111
|
+
|
|
112
|
+
{showIcon && (
|
|
113
|
+
<svg
|
|
114
|
+
width={iconSize}
|
|
115
|
+
height={iconSize}
|
|
116
|
+
viewBox="0 0 24 24"
|
|
117
|
+
fill="none"
|
|
118
|
+
stroke="currentColor"
|
|
119
|
+
strokeWidth="1.5"
|
|
120
|
+
data-account-icon=""
|
|
121
|
+
{...partAttrs('icon')}
|
|
122
|
+
>
|
|
123
|
+
<circle cx="12" cy="8" r="4" />
|
|
124
|
+
<path d="M4 20c0-4 3.6-6 8-6s8 2 8 6" />
|
|
125
|
+
</svg>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{labelPosition !== 'before' && labelNode}
|
|
129
|
+
</NextLink>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default AccountButtonBlock;
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import ThemeBlock, { Block } from '../theme-block';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import { readToggle } from './product-block-utils';
|
|
8
|
+
import {
|
|
9
|
+
findItemTemplate,
|
|
10
|
+
hasItemTemplate,
|
|
11
|
+
ItemTemplateList
|
|
12
|
+
} from './item-template';
|
|
13
|
+
|
|
14
|
+
const DISMISS_KEY_PREFIX = 'pz-announcement-dismissed:';
|
|
15
|
+
|
|
16
|
+
const readDismissed = (key: string): boolean => {
|
|
17
|
+
// sessionStorage throws in Safari private mode and is absent during SSR.
|
|
18
|
+
try {
|
|
19
|
+
return window.sessionStorage.getItem(key) === '1';
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const writeDismissed = (key: string) => {
|
|
26
|
+
try {
|
|
27
|
+
window.sessionStorage.setItem(key, '1');
|
|
28
|
+
} catch {
|
|
29
|
+
// A storage failure only costs the shopper the memory of the dismissal.
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Close glyphs on the same 24x24 stroke grid the other chrome blocks draw on,
|
|
35
|
+
* so `stroke: currentColor` keeps the strip's colour. `cross` is the shape the
|
|
36
|
+
* bar has always shipped with and stays the default.
|
|
37
|
+
*/
|
|
38
|
+
const CLOSE_ICONS: Record<string, React.ReactNode> = {
|
|
39
|
+
cross: <path d="M6 6l12 12M18 6L6 18" />,
|
|
40
|
+
'cross-circle': (
|
|
41
|
+
<>
|
|
42
|
+
<circle cx="12" cy="12" r="9" />
|
|
43
|
+
<path d="M9.5 9.5l5 5M14.5 9.5l-5 5" />
|
|
44
|
+
</>
|
|
45
|
+
),
|
|
46
|
+
chevron: <path d="M9 6l6 6-6 6" />,
|
|
47
|
+
minus: <path d="M6 12h12" />
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ALIGNMENTS = new Set([
|
|
51
|
+
'center',
|
|
52
|
+
'flex-start',
|
|
53
|
+
'flex-end',
|
|
54
|
+
'space-between'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const clampNumber = (
|
|
58
|
+
value: unknown,
|
|
59
|
+
fallback: number,
|
|
60
|
+
min: number,
|
|
61
|
+
max: number
|
|
62
|
+
) => {
|
|
63
|
+
const numeric = Math.round(Number(value));
|
|
64
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
65
|
+
return Math.min(max, Math.max(min, numeric));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Functional chrome block: the thin strip above the header. Messages are
|
|
70
|
+
* authored either as child blocks (text, link, icon — the richer path, which
|
|
71
|
+
* wins when present) or as a separator-delimited list in the `messages`
|
|
72
|
+
* property; a group marked as the row layout repeats the designer's own layout
|
|
73
|
+
* once per message instead. The strip rotates on an editor-owned interval and
|
|
74
|
+
* can be dismissed for the rest of the browsing session.
|
|
75
|
+
* Properties: messages, separator, maxMessages, rotateSeconds, pauseOnHover,
|
|
76
|
+
* align, gap, regionLabel, dismissible, closeLabel, closeIcon, closeIconSize,
|
|
77
|
+
* closePosition.
|
|
78
|
+
* Parts: strip, message-list, message, close, icon, empty-state.
|
|
79
|
+
*/
|
|
80
|
+
const AnnouncementBarBlock = ({
|
|
81
|
+
block,
|
|
82
|
+
placeholderId,
|
|
83
|
+
sectionId,
|
|
84
|
+
selectedBlockId = null,
|
|
85
|
+
currentBreakpoint = 'desktop',
|
|
86
|
+
isDesigner = false
|
|
87
|
+
}: BlockRendererProps) => {
|
|
88
|
+
const readText = (key: string, fallback: string): string => {
|
|
89
|
+
const value = getResponsiveValue(
|
|
90
|
+
block.properties?.[key],
|
|
91
|
+
currentBreakpoint,
|
|
92
|
+
fallback
|
|
93
|
+
);
|
|
94
|
+
return value === undefined || value === null ? fallback : String(value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const rotateSeconds = Number(
|
|
98
|
+
getResponsiveValue(block.properties?.rotateSeconds, currentBreakpoint, 5)
|
|
99
|
+
);
|
|
100
|
+
const dismissible = readToggle(
|
|
101
|
+
block.properties?.dismissible,
|
|
102
|
+
currentBreakpoint,
|
|
103
|
+
true
|
|
104
|
+
);
|
|
105
|
+
const pauseOnHover = readToggle(
|
|
106
|
+
block.properties?.pauseOnHover,
|
|
107
|
+
currentBreakpoint,
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
const messagesText = String(
|
|
111
|
+
getResponsiveValue(block.properties?.messages, currentBreakpoint, '')
|
|
112
|
+
);
|
|
113
|
+
// An empty separator would split the text into single characters, so the
|
|
114
|
+
// authored value only counts when it has something to split on.
|
|
115
|
+
const separatorValue = readText('separator', '|');
|
|
116
|
+
const separator = separatorValue.length > 0 ? separatorValue : '|';
|
|
117
|
+
const regionLabel = readText('regionLabel', 'Announcements');
|
|
118
|
+
const closeLabel = readText('closeLabel', 'Close announcement');
|
|
119
|
+
const closeIcon =
|
|
120
|
+
CLOSE_ICONS[readText('closeIcon', 'cross')] || CLOSE_ICONS.cross;
|
|
121
|
+
const closeIconSize = clampNumber(
|
|
122
|
+
getResponsiveValue(block.properties?.closeIconSize, currentBreakpoint, 16),
|
|
123
|
+
16,
|
|
124
|
+
8,
|
|
125
|
+
48
|
|
126
|
+
);
|
|
127
|
+
const closeAtStart = readText('closePosition', 'end') === 'start';
|
|
128
|
+
const alignValue = readText('align', 'center');
|
|
129
|
+
const align = ALIGNMENTS.has(alignValue) ? alignValue : 'center';
|
|
130
|
+
const gap = clampNumber(
|
|
131
|
+
getResponsiveValue(block.properties?.gap, currentBreakpoint, 12),
|
|
132
|
+
12,
|
|
133
|
+
0,
|
|
134
|
+
64
|
|
135
|
+
);
|
|
136
|
+
const maxMessages = clampNumber(
|
|
137
|
+
getResponsiveValue(block.properties?.maxMessages, currentBreakpoint, 0),
|
|
138
|
+
0,
|
|
139
|
+
0,
|
|
140
|
+
20
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// The row layout lives among the children, so it must not also be drawn as
|
|
144
|
+
// one of the messages.
|
|
145
|
+
const template = findItemTemplate(block, currentBreakpoint);
|
|
146
|
+
const templateId = template?.id;
|
|
147
|
+
|
|
148
|
+
const childBlocks: Block[] = useMemo(
|
|
149
|
+
() =>
|
|
150
|
+
(block.blocks || [])
|
|
151
|
+
.filter((child) => (isDesigner ? true : !child.hidden))
|
|
152
|
+
.filter((child) => !templateId || child.id !== templateId)
|
|
153
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0)),
|
|
154
|
+
[block.blocks, isDesigner, templateId]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const textMessages = useMemo(() => {
|
|
158
|
+
const entries = messagesText
|
|
159
|
+
.split(separator)
|
|
160
|
+
.map((entry) => entry.trim())
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
|
|
163
|
+
return maxMessages > 0 ? entries.slice(0, maxMessages) : entries;
|
|
164
|
+
}, [messagesText, separator, maxMessages]);
|
|
165
|
+
|
|
166
|
+
const messageItems = useMemo(
|
|
167
|
+
() =>
|
|
168
|
+
textMessages.map((text, index) => ({
|
|
169
|
+
text,
|
|
170
|
+
label: text,
|
|
171
|
+
value: text,
|
|
172
|
+
index: index + 1
|
|
173
|
+
})),
|
|
174
|
+
[textMessages]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const visibleChildBlocks =
|
|
178
|
+
maxMessages > 0 ? childBlocks.slice(0, maxMessages) : childBlocks;
|
|
179
|
+
|
|
180
|
+
const useCustomRows =
|
|
181
|
+
hasItemTemplate(block, currentBreakpoint) &&
|
|
182
|
+
(textMessages.length > 0 || isDesigner);
|
|
183
|
+
|
|
184
|
+
const count = useCustomRows
|
|
185
|
+
? textMessages.length
|
|
186
|
+
: visibleChildBlocks.length > 0
|
|
187
|
+
? visibleChildBlocks.length
|
|
188
|
+
: textMessages.length;
|
|
189
|
+
|
|
190
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
191
|
+
const [dismissed, setDismissed] = useState(false);
|
|
192
|
+
const [paused, setPaused] = useState(false);
|
|
193
|
+
|
|
194
|
+
const dismissKey = `${DISMISS_KEY_PREFIX}${block.id || 'announcement'}`;
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
// Read after mount only: the server has no session storage, so deciding
|
|
198
|
+
// during render would hydrate as a mismatch. The designer always shows the
|
|
199
|
+
// strip, otherwise an author could not style a bar they once closed.
|
|
200
|
+
if (isDesigner) return;
|
|
201
|
+
setDismissed(readDismissed(dismissKey));
|
|
202
|
+
}, [dismissKey, isDesigner]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setActiveIndex(0);
|
|
206
|
+
}, [count]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
// The designer stacks every message so each one stays selectable, so there
|
|
210
|
+
// is nothing to rotate there.
|
|
211
|
+
if (isDesigner || count < 2 || paused) return;
|
|
212
|
+
if (!Number.isFinite(rotateSeconds) || rotateSeconds <= 0) return;
|
|
213
|
+
|
|
214
|
+
const timer = setInterval(
|
|
215
|
+
() => setActiveIndex((index) => (index + 1) % count),
|
|
216
|
+
rotateSeconds * 1000
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return () => clearInterval(timer);
|
|
220
|
+
}, [isDesigner, count, rotateSeconds, paused]);
|
|
221
|
+
|
|
222
|
+
if (count === 0 && !useCustomRows) {
|
|
223
|
+
return isDesigner ? (
|
|
224
|
+
<div
|
|
225
|
+
data-announcement-bar=""
|
|
226
|
+
data-announcement-bar-state="empty"
|
|
227
|
+
{...partAttrs('empty-state', { empty: true })}
|
|
228
|
+
>
|
|
229
|
+
Announcement bar (no messages)
|
|
230
|
+
</div>
|
|
231
|
+
) : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (dismissed && !isDesigner) return null;
|
|
235
|
+
|
|
236
|
+
const dismiss = () => {
|
|
237
|
+
// Dismissal must not leak out of the designer into the author's session.
|
|
238
|
+
if (isDesigner) return;
|
|
239
|
+
setDismissed(true);
|
|
240
|
+
writeDismissed(dismissKey);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const messageStyle = (index: number): React.CSSProperties =>
|
|
244
|
+
isDesigner
|
|
245
|
+
? { display: 'block' }
|
|
246
|
+
: { display: index === activeIndex ? 'block' : 'none' };
|
|
247
|
+
|
|
248
|
+
const createActionHandler = (actionType: string, blockId?: string) => () => {
|
|
249
|
+
if (window.parent) {
|
|
250
|
+
window.parent.postMessage(
|
|
251
|
+
{ type: actionType, data: { placeholderId, sectionId, blockId } },
|
|
252
|
+
'*'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Rename is a separate message because it carries the new label; without it
|
|
258
|
+
// the toolbar exposes no way to retitle a message block.
|
|
259
|
+
const createRenameHandler =
|
|
260
|
+
(blockId?: string) => (newLabel: string) => {
|
|
261
|
+
if (window.parent) {
|
|
262
|
+
window.parent.postMessage(
|
|
263
|
+
{
|
|
264
|
+
type: 'RENAME_BLOCK',
|
|
265
|
+
data: { placeholderId, sectionId, blockId, label: newLabel }
|
|
266
|
+
},
|
|
267
|
+
'*'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const customRows = useCustomRows ? (
|
|
273
|
+
<ItemTemplateList
|
|
274
|
+
block={block}
|
|
275
|
+
items={messageItems}
|
|
276
|
+
placeholderId={placeholderId}
|
|
277
|
+
sectionId={sectionId}
|
|
278
|
+
isDesigner={isDesigner}
|
|
279
|
+
selectedBlockId={selectedBlockId}
|
|
280
|
+
currentBreakpoint={currentBreakpoint}
|
|
281
|
+
designerPlaceholderCount={2}
|
|
282
|
+
/>
|
|
283
|
+
) : null;
|
|
284
|
+
|
|
285
|
+
// Custom rows are rendered by the iterator, so the per-message `display`
|
|
286
|
+
// the built-in path applies inline is not available. A scoped rule hides
|
|
287
|
+
// every row but the active one, which keeps rotation working for a
|
|
288
|
+
// designer-authored layout. `:not()` rather than a display value so the
|
|
289
|
+
// row keeps whatever display its own styles give it.
|
|
290
|
+
const stripId = String(block.id || 'announcement').replace(
|
|
291
|
+
/[^a-zA-Z0-9_-]/g,
|
|
292
|
+
''
|
|
293
|
+
);
|
|
294
|
+
const rotationCss =
|
|
295
|
+
!isDesigner && useCustomRows && count > 1
|
|
296
|
+
? `[data-announcement-id="${stripId}"] [data-part="message-list"] > *:not(:nth-child(${
|
|
297
|
+
activeIndex + 1
|
|
298
|
+
})){display:none !important;}`
|
|
299
|
+
: '';
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
data-announcement-bar=""
|
|
304
|
+
data-announcement-bar-index={activeIndex}
|
|
305
|
+
data-announcement-id={stripId}
|
|
306
|
+
{...partAttrs('strip')}
|
|
307
|
+
role="region"
|
|
308
|
+
aria-label={regionLabel || undefined}
|
|
309
|
+
aria-live="polite"
|
|
310
|
+
onMouseEnter={pauseOnHover ? () => setPaused(true) : undefined}
|
|
311
|
+
onMouseLeave={pauseOnHover ? () => setPaused(false) : undefined}
|
|
312
|
+
style={{
|
|
313
|
+
display: 'flex',
|
|
314
|
+
alignItems: 'center',
|
|
315
|
+
justifyContent: align,
|
|
316
|
+
flexDirection: closeAtStart ? 'row-reverse' : 'row',
|
|
317
|
+
gap: `${gap}px`,
|
|
318
|
+
width: '100%'
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{rotationCss && <style>{rotationCss}</style>}
|
|
322
|
+
|
|
323
|
+
<div
|
|
324
|
+
data-announcement-messages=""
|
|
325
|
+
{...partAttrs('message-list')}
|
|
326
|
+
style={{ flex: '1 1 auto', textAlign: 'center', minWidth: 0 }}
|
|
327
|
+
>
|
|
328
|
+
{customRows ??
|
|
329
|
+
(visibleChildBlocks.length > 0
|
|
330
|
+
? visibleChildBlocks.map((child, index) => (
|
|
331
|
+
<div
|
|
332
|
+
key={child.id || `announcement-${index}`}
|
|
333
|
+
data-announcement-message=""
|
|
334
|
+
{...partAttrs('message', { selected: index === activeIndex })}
|
|
335
|
+
style={messageStyle(index)}
|
|
336
|
+
>
|
|
337
|
+
<ThemeBlock
|
|
338
|
+
block={child}
|
|
339
|
+
placeholderId={placeholderId}
|
|
340
|
+
sectionId={sectionId}
|
|
341
|
+
isDesigner={isDesigner}
|
|
342
|
+
isSelected={selectedBlockId === child.id}
|
|
343
|
+
selectedBlockId={selectedBlockId}
|
|
344
|
+
currentBreakpoint={currentBreakpoint}
|
|
345
|
+
onMoveUp={createActionHandler('MOVE_BLOCK_UP', child.id)}
|
|
346
|
+
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN', child.id)}
|
|
347
|
+
onDuplicate={createActionHandler(
|
|
348
|
+
'DUPLICATE_BLOCK',
|
|
349
|
+
child.id
|
|
350
|
+
)}
|
|
351
|
+
onToggleVisibility={createActionHandler(
|
|
352
|
+
'TOGGLE_BLOCK_VISIBILITY',
|
|
353
|
+
child.id
|
|
354
|
+
)}
|
|
355
|
+
onDelete={createActionHandler('DELETE_BLOCK', child.id)}
|
|
356
|
+
onRename={createRenameHandler(child.id)}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
))
|
|
360
|
+
: textMessages.map((message, index) => (
|
|
361
|
+
<div
|
|
362
|
+
key={`${message}-${index}`}
|
|
363
|
+
data-announcement-message=""
|
|
364
|
+
{...partAttrs('message', { selected: index === activeIndex })}
|
|
365
|
+
style={messageStyle(index)}
|
|
366
|
+
>
|
|
367
|
+
{message}
|
|
368
|
+
</div>
|
|
369
|
+
)))}
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{dismissible && (
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
data-announcement-close=""
|
|
376
|
+
{...partAttrs('close')}
|
|
377
|
+
aria-label={closeLabel || undefined}
|
|
378
|
+
onClick={dismiss}
|
|
379
|
+
style={{
|
|
380
|
+
flex: '0 0 auto',
|
|
381
|
+
background: 'none',
|
|
382
|
+
border: 'none',
|
|
383
|
+
padding: 0,
|
|
384
|
+
cursor: 'pointer',
|
|
385
|
+
color: 'inherit',
|
|
386
|
+
lineHeight: 0
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<svg
|
|
390
|
+
width={closeIconSize}
|
|
391
|
+
height={closeIconSize}
|
|
392
|
+
viewBox="0 0 24 24"
|
|
393
|
+
fill="none"
|
|
394
|
+
stroke="currentColor"
|
|
395
|
+
strokeWidth="1.8"
|
|
396
|
+
strokeLinecap="round"
|
|
397
|
+
{...partAttrs('icon')}
|
|
398
|
+
>
|
|
399
|
+
{closeIcon}
|
|
400
|
+
</svg>
|
|
401
|
+
</button>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export default AnnouncementBarBlock;
|
|
@@ -18,6 +18,13 @@ import LinkBlock from './link-block';
|
|
|
18
18
|
import LottieBlock from './lottie-block';
|
|
19
19
|
import EmbedBlock from './embed-block';
|
|
20
20
|
import MapBlock from './map-block';
|
|
21
|
+
import NavMenuBlock from './nav-menu-block';
|
|
22
|
+
import MiniBasketBlock from './mini-basket-block';
|
|
23
|
+
import HamburgerMenuBlock from './hamburger-menu-block';
|
|
24
|
+
import AccountButtonBlock from './account-button-block';
|
|
25
|
+
import SearchBoxBlock from './search-box-block';
|
|
26
|
+
import WishlistButtonBlock from './wishlist-button-block';
|
|
27
|
+
import LanguageSelectorBlock from './language-selector-block';
|
|
21
28
|
import ProductGalleryBlock from './product-gallery-block';
|
|
22
29
|
import ProductTitleBlock from './product-title-block';
|
|
23
30
|
import ProductPriceBlock from './product-price-block';
|
|
@@ -43,6 +50,12 @@ import ChangePasswordFormBlock from './change-password-form-block';
|
|
|
43
50
|
import OrderDetailBlock from './order-detail-block';
|
|
44
51
|
import CouponListBlock from './coupon-list-block';
|
|
45
52
|
import FavouritesGridBlock from './favourites-grid-block';
|
|
53
|
+
import MegaMenuBlock from './mega-menu-block';
|
|
54
|
+
import CurrencySelectorBlock from './currency-selector-block';
|
|
55
|
+
import AnnouncementBarBlock from './announcement-bar-block';
|
|
56
|
+
import NewsletterFormBlock from './newsletter-form-block';
|
|
57
|
+
import SocialLinksBlock from './social-links-block';
|
|
58
|
+
import PaymentBadgesBlock from './payment-badges-block';
|
|
46
59
|
import CheckoutSummaryBlock from './checkout-summary-block';
|
|
47
60
|
import CheckoutStepsBlock from './checkout-steps-block';
|
|
48
61
|
import OrderSuccessBlock from './order-success-block';
|
|
@@ -115,6 +128,13 @@ blockRendererRegistry.register('link', LinkBlock);
|
|
|
115
128
|
blockRendererRegistry.register('lottie', LottieBlock);
|
|
116
129
|
blockRendererRegistry.register('embed', EmbedBlock);
|
|
117
130
|
blockRendererRegistry.register('map', MapBlock);
|
|
131
|
+
blockRendererRegistry.register('nav-menu', NavMenuBlock);
|
|
132
|
+
blockRendererRegistry.register('mini-basket', MiniBasketBlock);
|
|
133
|
+
blockRendererRegistry.register('hamburger-menu', HamburgerMenuBlock);
|
|
134
|
+
blockRendererRegistry.register('account-button', AccountButtonBlock);
|
|
135
|
+
blockRendererRegistry.register('search-box', SearchBoxBlock);
|
|
136
|
+
blockRendererRegistry.register('wishlist-button', WishlistButtonBlock);
|
|
137
|
+
blockRendererRegistry.register('language-selector', LanguageSelectorBlock);
|
|
118
138
|
blockRendererRegistry.register('product-gallery', ProductGalleryBlock);
|
|
119
139
|
blockRendererRegistry.register('product-title', ProductTitleBlock);
|
|
120
140
|
blockRendererRegistry.register('product-price', ProductPriceBlock);
|
|
@@ -144,6 +164,12 @@ blockRendererRegistry.register('coupon-list', CouponListBlock);
|
|
|
144
164
|
blockRendererRegistry.register('favourites-grid', FavouritesGridBlock);
|
|
145
165
|
|
|
146
166
|
// Chrome extras
|
|
167
|
+
blockRendererRegistry.register('mega-menu', MegaMenuBlock);
|
|
168
|
+
blockRendererRegistry.register('currency-selector', CurrencySelectorBlock);
|
|
169
|
+
blockRendererRegistry.register('announcement-bar', AnnouncementBarBlock);
|
|
170
|
+
blockRendererRegistry.register('newsletter-form', NewsletterFormBlock);
|
|
171
|
+
blockRendererRegistry.register('social-links', SocialLinksBlock);
|
|
172
|
+
blockRendererRegistry.register('payment-badges', PaymentBadgesBlock);
|
|
147
173
|
|
|
148
174
|
// Order flow
|
|
149
175
|
blockRendererRegistry.register('checkout-summary', CheckoutSummaryBlock);
|