@akinon/pz-theme 2.0.38-beta.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.
@@ -0,0 +1,712 @@
1
+ 'use client';
2
+
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState
9
+ } from 'react';
10
+ import NextLink from 'next/link';
11
+ import { usePathname } from 'next/navigation';
12
+ import { useGetMenuQuery } from '@akinon/next/data/client/misc';
13
+ import { getResponsiveValue, partAttrs } from '../utils';
14
+ import { useOverlay } from '../hooks/use-overlay';
15
+ import { BlockRendererProps } from './block-renderer-registry';
16
+ import { hasItemTemplate, ItemTemplateList } from './item-template';
17
+ import { DesignerPlaceholder, readToggle } from './product-block-utils';
18
+
19
+ /** Matches the dwell the reference brand header uses before a panel opens. */
20
+ const HOVER_OPEN_DELAY = 200;
21
+ const HOVER_CLOSE_DELAY = 120;
22
+
23
+ /** Query strings and trailing slashes are noise when matching the open page. */
24
+ const normalizePath = (value: unknown): string => {
25
+ const path = String(value || '')
26
+ .split('?')[0]
27
+ .split('#')[0];
28
+
29
+ return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
30
+ };
31
+
32
+ interface MenuNodeLike {
33
+ pk?: number | string;
34
+ label?: string;
35
+ url?: string;
36
+ parent_pk?: number | string | null;
37
+ extra_context?: { attributes?: Record<string, unknown> } | null;
38
+ children?: MenuNodeLike[] | null;
39
+ }
40
+
41
+ interface MegaMenuNode {
42
+ key: string;
43
+ label: string;
44
+ url: string;
45
+ attributes: Record<string, unknown>;
46
+ children: MegaMenuNode[];
47
+ }
48
+
49
+ const clampNumber = (
50
+ value: unknown,
51
+ fallback: number,
52
+ min: number,
53
+ max: number
54
+ ) => {
55
+ const numeric = Math.round(Number(value));
56
+ if (!Number.isFinite(numeric)) return fallback;
57
+ return Math.min(max, Math.max(min, numeric));
58
+ };
59
+
60
+ const toNode = (raw: MenuNodeLike, index: number): MegaMenuNode => ({
61
+ key: String(raw.pk ?? `menu-${index}`),
62
+ label: String(raw.label ?? ''),
63
+ url: String(raw.url ?? ''),
64
+ attributes: (raw.extra_context?.attributes as Record<string, unknown>) || {},
65
+ children: []
66
+ });
67
+
68
+ /**
69
+ * The menu endpoint answers a depth>1 request with a FLAT list linked by
70
+ * `parent_pk`, so the columns have to be assembled here. Core's `menuGenerator`
71
+ * does `data[item.parent_pk].children.push(...)` unguarded and throws the whole
72
+ * header away when a parent falls outside the requested depth window; this
73
+ * version keeps those entries instead of crashing.
74
+ */
75
+ const buildTree = (raw: MenuNodeLike[]): MegaMenuNode[] => {
76
+ if (raw.length === 0) return [];
77
+
78
+ const alreadyNested = raw.some(
79
+ (entry) => Array.isArray(entry.children) && entry.children.length > 0
80
+ );
81
+
82
+ if (alreadyNested) {
83
+ const walk = (entries: MenuNodeLike[]): MegaMenuNode[] =>
84
+ entries.map((entry, index) => ({
85
+ ...toNode(entry, index),
86
+ children: walk(Array.isArray(entry.children) ? entry.children : [])
87
+ }));
88
+
89
+ return walk(raw);
90
+ }
91
+
92
+ const nodes = new Map<string, MegaMenuNode>();
93
+ raw.forEach((entry, index) => {
94
+ const node = toNode(entry, index);
95
+ nodes.set(node.key, node);
96
+ });
97
+
98
+ const roots: MegaMenuNode[] = [];
99
+ const orphans: MegaMenuNode[] = [];
100
+
101
+ raw.forEach((entry, index) => {
102
+ const node = nodes.get(String(entry.pk ?? `menu-${index}`));
103
+ if (!node) return;
104
+
105
+ const parentKey =
106
+ entry.parent_pk === null || entry.parent_pk === undefined
107
+ ? ''
108
+ : String(entry.parent_pk);
109
+
110
+ if (!parentKey) {
111
+ roots.push(node);
112
+ return;
113
+ }
114
+
115
+ const parent = nodes.get(parentKey);
116
+ if (parent) {
117
+ parent.children.push(node);
118
+ } else {
119
+ orphans.push(node);
120
+ }
121
+ });
122
+
123
+ // A `parent` window that starts below the tree root has no parentless entry
124
+ // at all; falling back to the orphans keeps the menu visible.
125
+ return roots.length > 0 ? roots : orphans;
126
+ };
127
+
128
+ /**
129
+ * Brands hang the panel artwork off the menu item under a handful of attribute
130
+ * names, each with its own nesting (`kwargs.value.top_image.url`,
131
+ * `kwargs.url`, ...). Probing a short key list and then walking for a `url`
132
+ * keeps one renderer working across those payload shapes.
133
+ */
134
+ const IMAGE_ATTRIBUTE_KEYS = [
135
+ 'navigation_image',
136
+ 'nav_sub_category_image',
137
+ 'menu_image',
138
+ 'image'
139
+ ];
140
+
141
+ const looksLikeUrl = (value: string) =>
142
+ /^(https?:)?\/\//i.test(value) || value.startsWith('/');
143
+
144
+ const findUrl = (value: unknown, depth = 0): string => {
145
+ if (depth > 5) return '';
146
+
147
+ if (typeof value === 'string') return looksLikeUrl(value) ? value : '';
148
+
149
+ if (Array.isArray(value)) {
150
+ for (const entry of value) {
151
+ const found = findUrl(entry, depth + 1);
152
+ if (found) return found;
153
+ }
154
+ return '';
155
+ }
156
+
157
+ if (value && typeof value === 'object') {
158
+ const record = value as Record<string, unknown>;
159
+
160
+ // `url` is the key commerce renders the asset under; preferring it stops a
161
+ // sibling link field from being picked up as the image source.
162
+ if (typeof record.url === 'string' && looksLikeUrl(record.url)) {
163
+ return record.url;
164
+ }
165
+
166
+ for (const entry of Object.values(record)) {
167
+ const found = findUrl(entry, depth + 1);
168
+ if (found) return found;
169
+ }
170
+ }
171
+
172
+ return '';
173
+ };
174
+
175
+ const readPromoImage = (attributes: Record<string, unknown>): string => {
176
+ for (const key of IMAGE_ATTRIBUTE_KEYS) {
177
+ const found = findUrl(attributes?.[key]);
178
+ if (found) return found;
179
+ }
180
+ return '';
181
+ };
182
+
183
+ /**
184
+ * Functional chrome block: top level menu entries that open a full width panel
185
+ * with the child columns and an optional promo image. Menu CONTENT stays
186
+ * commerce-driven (menu service, depth 2-3); the editor owns the trigger,
187
+ * column count, highlights and styling.
188
+ * Properties: depth, parent, trigger, columns, showImages, highlightLabels,
189
+ * highlightColor, columnGap, openDelay, closeDelay, showParentLink,
190
+ * parentLinkLabel, promoPosition, promoMaxWidth, activeMatch, maxItems,
191
+ * emptyText, loadingText.
192
+ * Parts: menu, item, panel, parent-link, column, column-title, column-image,
193
+ * submenu, submenu-item, promo.
194
+ */
195
+ const MegaMenuBlock = ({
196
+ block,
197
+ placeholderId,
198
+ sectionId,
199
+ selectedBlockId = null,
200
+ currentBreakpoint = 'desktop',
201
+ isDesigner = false
202
+ }: BlockRendererProps) => {
203
+ const pathname = usePathname();
204
+ const [openKey, setOpenKey] = useState<string | null>(null);
205
+ const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
206
+ const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
207
+
208
+ const depth = clampNumber(
209
+ getResponsiveValue(block.properties?.depth, currentBreakpoint, 3),
210
+ 3,
211
+ 2,
212
+ 3
213
+ );
214
+ const parent = String(
215
+ getResponsiveValue(block.properties?.parent, currentBreakpoint, '')
216
+ ).trim();
217
+ const trigger =
218
+ String(
219
+ getResponsiveValue(block.properties?.trigger, currentBreakpoint, 'hover')
220
+ ) === 'click'
221
+ ? 'click'
222
+ : 'hover';
223
+ const columns = clampNumber(
224
+ getResponsiveValue(block.properties?.columns, currentBreakpoint, 4),
225
+ 4,
226
+ 1,
227
+ 6
228
+ );
229
+ const showImages = readToggle(
230
+ block.properties?.showImages,
231
+ currentBreakpoint,
232
+ true
233
+ );
234
+ const highlightColor = String(
235
+ getResponsiveValue(block.properties?.highlightColor, currentBreakpoint, '')
236
+ );
237
+ const highlightLabels = String(
238
+ getResponsiveValue(block.properties?.highlightLabels, currentBreakpoint, '')
239
+ )
240
+ .split(',')
241
+ .map((entry) => entry.trim().toLocaleLowerCase())
242
+ .filter(Boolean);
243
+ const columnGap = clampNumber(
244
+ getResponsiveValue(block.properties?.columnGap, currentBreakpoint, 24),
245
+ 24,
246
+ 0,
247
+ 96
248
+ );
249
+ const openDelay = clampNumber(
250
+ getResponsiveValue(
251
+ block.properties?.openDelay,
252
+ currentBreakpoint,
253
+ HOVER_OPEN_DELAY
254
+ ),
255
+ HOVER_OPEN_DELAY,
256
+ 0,
257
+ 1000
258
+ );
259
+ const closeDelay = clampNumber(
260
+ getResponsiveValue(
261
+ block.properties?.closeDelay,
262
+ currentBreakpoint,
263
+ HOVER_CLOSE_DELAY
264
+ ),
265
+ HOVER_CLOSE_DELAY,
266
+ 0,
267
+ 1000
268
+ );
269
+ const showParentLink = readToggle(
270
+ block.properties?.showParentLink,
271
+ currentBreakpoint,
272
+ true
273
+ );
274
+ const parentLinkLabel = String(
275
+ getResponsiveValue(block.properties?.parentLinkLabel, currentBreakpoint, '')
276
+ );
277
+ const promoPosition = String(
278
+ getResponsiveValue(
279
+ block.properties?.promoPosition,
280
+ currentBreakpoint,
281
+ 'end'
282
+ )
283
+ );
284
+ const promoMaxWidth = clampNumber(
285
+ getResponsiveValue(block.properties?.promoMaxWidth, currentBreakpoint, 256),
286
+ 256,
287
+ 80,
288
+ 720
289
+ );
290
+ const activeMatch = String(
291
+ getResponsiveValue(
292
+ block.properties?.activeMatch,
293
+ currentBreakpoint,
294
+ 'exact'
295
+ )
296
+ );
297
+ const maxItems = clampNumber(
298
+ getResponsiveValue(block.properties?.maxItems, currentBreakpoint, 0),
299
+ 0,
300
+ 0,
301
+ 40
302
+ );
303
+ const emptyText = String(
304
+ getResponsiveValue(
305
+ block.properties?.emptyText,
306
+ currentBreakpoint,
307
+ 'Mega menu (no items)'
308
+ )
309
+ );
310
+ const loadingText = String(
311
+ getResponsiveValue(
312
+ block.properties?.loadingText,
313
+ currentBreakpoint,
314
+ 'Mega menu (loading)'
315
+ )
316
+ );
317
+
318
+ const { data: menu, isLoading } = useGetMenuQuery({
319
+ depth,
320
+ ...(parent ? { parent } : {})
321
+ } as Parameters<typeof useGetMenuQuery>[0]);
322
+
323
+ const allItems = useMemo(
324
+ () => buildTree(Array.isArray(menu) ? (menu as MenuNodeLike[]) : []),
325
+ [menu]
326
+ );
327
+ const items = maxItems > 0 ? allItems.slice(0, maxItems) : allItems;
328
+
329
+ const clearTimers = useCallback(() => {
330
+ if (openTimer.current) clearTimeout(openTimer.current);
331
+ if (closeTimer.current) clearTimeout(closeTimer.current);
332
+ openTimer.current = null;
333
+ closeTimer.current = null;
334
+ }, []);
335
+
336
+ const closeNow = useCallback(() => {
337
+ clearTimers();
338
+ setOpenKey(null);
339
+ }, [clearTimers]);
340
+
341
+ useEffect(() => clearTimers, [clearTimers]);
342
+
343
+ /**
344
+ * Escape is page-wide state, so the shared overlay stack decides whose it
345
+ * is. This panel used to answer it from its own document listener, which
346
+ * meant an Escape aimed at a modal opened over the header collapsed the menu
347
+ * underneath at the same time.
348
+ *
349
+ * Nothing else moves to the primitive: the panel hangs off the header in
350
+ * flow behind no backdrop, so `modal: false` (no `aria-modal`, no focus
351
+ * trap) and `manageFocus: false` — the item's own focus and blur handlers
352
+ * already drive this menu open and closed.
353
+ */
354
+ useOverlay({
355
+ open: Boolean(openKey),
356
+ onClose: closeNow,
357
+ isDesigner,
358
+ modal: false,
359
+ manageFocus: false
360
+ });
361
+
362
+ const scheduleOpen = (key: string) => {
363
+ clearTimers();
364
+ openTimer.current = setTimeout(() => setOpenKey(key), openDelay);
365
+ };
366
+
367
+ const scheduleClose = () => {
368
+ clearTimers();
369
+ closeTimer.current = setTimeout(() => setOpenKey(null), closeDelay);
370
+ };
371
+
372
+ const isHighlighted = (label: string) =>
373
+ Boolean(
374
+ label &&
375
+ highlightLabels.some((entry) =>
376
+ label.toLocaleLowerCase().includes(entry)
377
+ )
378
+ );
379
+
380
+ // The entry for the page the shopper is on carries `selected` so a part
381
+ // style can address it without the brand hand-listing every label.
382
+ const isCurrent = (url: string) => {
383
+ const target = normalizePath(url);
384
+ const current = normalizePath(pathname);
385
+
386
+ if (!target || target === '#' || !current) return false;
387
+ if (activeMatch === 'section' && target !== '/') {
388
+ return current === target || current.startsWith(`${target}/`);
389
+ }
390
+
391
+ return current === target;
392
+ };
393
+
394
+ // Navigating inside the designer iframe would drop the author out of the page
395
+ // they are editing, so every link in the menu is inert there.
396
+ const guard = (event: React.MouseEvent) => {
397
+ if (isDesigner) event.preventDefault();
398
+ };
399
+
400
+ const linkHref = (url: string) => (isDesigner ? '#' : url || '#');
401
+
402
+ if (items.length === 0) {
403
+ return isDesigner ? (
404
+ <DesignerPlaceholder
405
+ label={isLoading ? loadingText : emptyText}
406
+ attribute="data-mega-menu"
407
+ />
408
+ ) : null;
409
+ }
410
+
411
+ const renderPanel = (item: MegaMenuNode) => {
412
+ const promoImage = showImages ? readPromoImage(item.attributes) : '';
413
+ if (item.children.length === 0 && !promoImage) return null;
414
+
415
+ // A designer who marked a child as the column layout gets that layout
416
+ // repeated per column; otherwise the built-in columns render unchanged.
417
+ const customColumns = hasItemTemplate(block, currentBreakpoint) ? (
418
+ <ItemTemplateList
419
+ block={block}
420
+ items={item.children.map((column) => ({
421
+ key: column.key,
422
+ name: column.label,
423
+ label: column.label,
424
+ url: column.url,
425
+ image: showImages ? readPromoImage(column.attributes) : ''
426
+ }))}
427
+ placeholderId={placeholderId}
428
+ sectionId={sectionId}
429
+ isDesigner={isDesigner}
430
+ selectedBlockId={selectedBlockId}
431
+ currentBreakpoint={currentBreakpoint}
432
+ designerPlaceholderCount={4}
433
+ />
434
+ ) : null;
435
+
436
+ const promo = promoImage ? (
437
+ <NextLink
438
+ href={linkHref(item.url)}
439
+ data-mega-menu-promo=""
440
+ {...partAttrs('promo')}
441
+ onClick={guard}
442
+ style={{
443
+ flex: '0 0 auto',
444
+ display: 'block',
445
+ maxWidth: `${promoMaxWidth}px`
446
+ }}
447
+ >
448
+ {/* eslint-disable-next-line @next/next/no-img-element */}
449
+ <img
450
+ src={promoImage}
451
+ alt={item.label}
452
+ loading="lazy"
453
+ data-mega-menu-promo-image=""
454
+ {...partAttrs('promo-image')}
455
+ style={{ display: 'block', width: '100%', height: 'auto' }}
456
+ />
457
+ </NextLink>
458
+ ) : null;
459
+
460
+ return (
461
+ <div
462
+ data-mega-menu-panel=""
463
+ {...partAttrs('panel', { open: true })}
464
+ onMouseEnter={() => {
465
+ if (trigger === 'hover') clearTimers();
466
+ }}
467
+ onMouseLeave={() => {
468
+ if (trigger === 'hover') scheduleClose();
469
+ }}
470
+ style={{
471
+ position: 'absolute',
472
+ top: '100%',
473
+ left: 0,
474
+ minWidth: '100%',
475
+ maxWidth: '100vw',
476
+ zIndex: 60,
477
+ display: 'flex',
478
+ alignItems: 'flex-start',
479
+ gap: '32px',
480
+ padding: 'var(--mega-menu-panel-padding, 24px)',
481
+ background: 'var(--mega-menu-panel-bg, #ffffff)',
482
+ color: 'var(--mega-menu-panel-color, #111827)',
483
+ boxShadow: '0 12px 24px rgba(0,0,0,0.12)',
484
+ textAlign: 'left'
485
+ }}
486
+ >
487
+ {promoPosition === 'start' && promo}
488
+
489
+ {item.children.length > 0 && (
490
+ <div
491
+ data-mega-menu-columns=""
492
+ {...partAttrs('column-list')}
493
+ style={{
494
+ flex: '1 1 auto',
495
+ display: 'grid',
496
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
497
+ // The CSS variable stays the outer fallback: brands that already
498
+ // set it keep the value they shipped with.
499
+ gap: `var(--mega-menu-column-gap, ${columnGap}px)`
500
+ }}
501
+ >
502
+ {trigger === 'click' && showParentLink && (
503
+ // In click mode the top level entry is a disclosure button, so the
504
+ // parent page would otherwise be unreachable from the panel.
505
+ <NextLink
506
+ href={linkHref(item.url)}
507
+ data-mega-menu-parent-link=""
508
+ {...partAttrs('parent-link')}
509
+ onClick={guard}
510
+ style={{
511
+ gridColumn: '1 / -1',
512
+ color: 'inherit',
513
+ textDecoration: 'underline',
514
+ fontSize: '0.9em'
515
+ }}
516
+ >
517
+ {parentLinkLabel || item.label}
518
+ </NextLink>
519
+ )}
520
+
521
+ {customColumns ??
522
+ item.children.map((column) => {
523
+ const columnImage = showImages
524
+ ? readPromoImage(column.attributes)
525
+ : '';
526
+
527
+ return (
528
+ <div
529
+ key={column.key}
530
+ data-mega-menu-column=""
531
+ {...partAttrs('column')}
532
+ >
533
+ <NextLink
534
+ href={linkHref(column.url)}
535
+ data-mega-menu-column-title=""
536
+ {...partAttrs('column-title', {
537
+ selected: isCurrent(column.url)
538
+ })}
539
+ onClick={guard}
540
+ style={{
541
+ display: 'block',
542
+ marginBottom: '10px',
543
+ color: 'inherit',
544
+ textDecoration: 'none',
545
+ fontWeight: 600
546
+ }}
547
+ >
548
+ {column.label}
549
+ </NextLink>
550
+
551
+ {columnImage && (
552
+ // Plain img: menu artwork comes from arbitrary commerce CDNs
553
+ // that the brand next.config may not allow through next/image.
554
+ // eslint-disable-next-line @next/next/no-img-element
555
+ <img
556
+ src={columnImage}
557
+ alt={column.label}
558
+ loading="lazy"
559
+ data-mega-menu-column-image=""
560
+ {...partAttrs('column-image')}
561
+ style={{
562
+ display: 'block',
563
+ width: '100%',
564
+ height: 'auto',
565
+ marginBottom: '10px'
566
+ }}
567
+ />
568
+ )}
569
+
570
+ {column.children.length > 0 && (
571
+ <ul
572
+ data-mega-menu-submenu=""
573
+ {...partAttrs('submenu')}
574
+ style={{
575
+ listStyle: 'none',
576
+ margin: 0,
577
+ padding: 0,
578
+ display: 'flex',
579
+ flexDirection: 'column',
580
+ gap: '6px'
581
+ }}
582
+ >
583
+ {column.children.map((leaf) => (
584
+ <li key={leaf.key}>
585
+ <NextLink
586
+ href={linkHref(leaf.url)}
587
+ data-mega-menu-link=""
588
+ {...partAttrs('submenu-item', {
589
+ selected: isCurrent(leaf.url)
590
+ })}
591
+ onClick={guard}
592
+ style={{
593
+ color: 'inherit',
594
+ textDecoration: 'none',
595
+ opacity: 0.85,
596
+ fontSize: '0.92em'
597
+ }}
598
+ >
599
+ {leaf.label}
600
+ </NextLink>
601
+ </li>
602
+ ))}
603
+ </ul>
604
+ )}
605
+ </div>
606
+ );
607
+ })}
608
+ </div>
609
+ )}
610
+
611
+ {promoPosition !== 'start' && promo}
612
+ </div>
613
+ );
614
+ };
615
+
616
+ return (
617
+ <nav
618
+ data-mega-menu=""
619
+ data-mega-menu-trigger={trigger}
620
+ {...partAttrs('menu')}
621
+ onMouseLeave={() => {
622
+ if (trigger === 'hover') scheduleClose();
623
+ }}
624
+ style={{
625
+ position: 'relative',
626
+ display: 'flex',
627
+ alignItems: 'center',
628
+ gap: 'var(--mega-menu-gap, 24px)'
629
+ }}
630
+ >
631
+ <ul
632
+ data-mega-menu-list=""
633
+ {...partAttrs('item-list')}
634
+ style={{
635
+ display: 'flex',
636
+ alignItems: 'center',
637
+ gap: 'inherit',
638
+ listStyle: 'none',
639
+ margin: 0,
640
+ padding: 0
641
+ }}
642
+ >
643
+ {items.map((item) => {
644
+ const open = openKey === item.key;
645
+ const highlighted = isHighlighted(item.label);
646
+ const current = isCurrent(item.url);
647
+ const entryStyle: React.CSSProperties = {
648
+ color: highlighted && highlightColor ? highlightColor : 'inherit',
649
+ fontWeight: highlighted ? 600 : 'inherit',
650
+ textDecoration: 'none',
651
+ background: 'none',
652
+ border: 'none',
653
+ padding: 0,
654
+ font: trigger === 'click' ? 'inherit' : undefined,
655
+ cursor: 'pointer'
656
+ };
657
+
658
+ return (
659
+ <li
660
+ key={item.key}
661
+ data-mega-menu-item={highlighted ? 'highlighted' : 'default'}
662
+ data-mega-menu-open={open ? 'true' : 'false'}
663
+ onMouseEnter={() => {
664
+ if (trigger === 'hover') scheduleOpen(item.key);
665
+ }}
666
+ onFocus={() => {
667
+ // Keyboard users never fire mouseenter, so focus opens the panel
668
+ // in both trigger modes.
669
+ clearTimers();
670
+ setOpenKey(item.key);
671
+ }}
672
+ onBlur={(event) => {
673
+ if (
674
+ !event.currentTarget.contains(event.relatedTarget as Node)
675
+ ) {
676
+ closeNow();
677
+ }
678
+ }}
679
+ style={{ display: 'flex', alignItems: 'center' }}
680
+ >
681
+ {trigger === 'click' ? (
682
+ <button
683
+ type="button"
684
+ aria-expanded={open}
685
+ {...partAttrs('item', { open, selected: current })}
686
+ onClick={() => setOpenKey(open ? null : item.key)}
687
+ style={entryStyle}
688
+ >
689
+ {item.label}
690
+ </button>
691
+ ) : (
692
+ <NextLink
693
+ href={linkHref(item.url)}
694
+ aria-expanded={open}
695
+ {...partAttrs('item', { open, selected: current })}
696
+ onClick={guard}
697
+ style={entryStyle}
698
+ >
699
+ {item.label}
700
+ </NextLink>
701
+ )}
702
+
703
+ {open && renderPanel(item)}
704
+ </li>
705
+ );
706
+ })}
707
+ </ul>
708
+ </nav>
709
+ );
710
+ };
711
+
712
+ export default MegaMenuBlock;