@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,177 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import { readToggle } from './product-block-utils';
|
|
8
|
+
|
|
9
|
+
const clampNumber = (
|
|
10
|
+
value: unknown,
|
|
11
|
+
fallback: number,
|
|
12
|
+
min: number,
|
|
13
|
+
max: number
|
|
14
|
+
) => {
|
|
15
|
+
const numeric = Math.round(Number(value));
|
|
16
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
17
|
+
return Math.min(max, Math.max(min, numeric));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Functional chrome block: currency switcher, the mirror of the language
|
|
22
|
+
* selector. Currencies come from the storefront settings through
|
|
23
|
+
* `useLocalization()`; the editor owns placement, wording, separator and
|
|
24
|
+
* styling.
|
|
25
|
+
* Properties: mode, separator, showSeparator, uppercase, labelSource, gap,
|
|
26
|
+
* ariaLabel, emptyText.
|
|
27
|
+
* Parts: option-list, option (selected/hover), separator, dropdown.
|
|
28
|
+
*/
|
|
29
|
+
const CurrencySelectorBlock = ({
|
|
30
|
+
block,
|
|
31
|
+
currentBreakpoint = 'desktop',
|
|
32
|
+
isDesigner = false
|
|
33
|
+
}: BlockRendererProps) => {
|
|
34
|
+
const { currency, currencies, setCurrency } = useLocalization();
|
|
35
|
+
|
|
36
|
+
const mode = String(
|
|
37
|
+
getResponsiveValue(block.properties?.mode, currentBreakpoint, 'inline')
|
|
38
|
+
);
|
|
39
|
+
const separator = String(
|
|
40
|
+
getResponsiveValue(block.properties?.separator, currentBreakpoint, '|')
|
|
41
|
+
);
|
|
42
|
+
const labelSource = String(
|
|
43
|
+
getResponsiveValue(
|
|
44
|
+
block.properties?.labelSource,
|
|
45
|
+
currentBreakpoint,
|
|
46
|
+
'label'
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
const ariaLabel = String(
|
|
50
|
+
getResponsiveValue(
|
|
51
|
+
block.properties?.ariaLabel,
|
|
52
|
+
currentBreakpoint,
|
|
53
|
+
'Currency'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
// Empty on purpose: the editor placeholder has always been built from the
|
|
57
|
+
// separator, so an unset override must keep doing exactly that.
|
|
58
|
+
const emptyText = String(
|
|
59
|
+
getResponsiveValue(block.properties?.emptyText, currentBreakpoint, '')
|
|
60
|
+
);
|
|
61
|
+
const gap = clampNumber(
|
|
62
|
+
getResponsiveValue(block.properties?.gap, currentBreakpoint, 6),
|
|
63
|
+
6,
|
|
64
|
+
0,
|
|
65
|
+
32
|
|
66
|
+
);
|
|
67
|
+
const showSeparator = readToggle(
|
|
68
|
+
block.properties?.showSeparator,
|
|
69
|
+
currentBreakpoint,
|
|
70
|
+
true
|
|
71
|
+
);
|
|
72
|
+
const uppercase = readToggle(
|
|
73
|
+
block.properties?.uppercase,
|
|
74
|
+
currentBreakpoint,
|
|
75
|
+
false
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const items = Array.isArray(currencies) ? currencies : [];
|
|
79
|
+
|
|
80
|
+
const format = (value: string) =>
|
|
81
|
+
uppercase ? String(value).toUpperCase() : String(value);
|
|
82
|
+
|
|
83
|
+
const readLabel = (item: { label?: string; code?: string }): string =>
|
|
84
|
+
labelSource === 'code'
|
|
85
|
+
? String(item.code ?? '')
|
|
86
|
+
: String(item.label || item.code || '');
|
|
87
|
+
|
|
88
|
+
const change = (value: string) => {
|
|
89
|
+
// setCurrency drops a cookie and reloads the page, which would tear the
|
|
90
|
+
// designer iframe out from under the author.
|
|
91
|
+
if (isDesigner || !value || value === currency) return;
|
|
92
|
+
setCurrency?.(value);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (items.length === 0) {
|
|
96
|
+
return isDesigner ? (
|
|
97
|
+
<span
|
|
98
|
+
data-currency-selector=""
|
|
99
|
+
{...partAttrs('option-list', { empty: true })}
|
|
100
|
+
>
|
|
101
|
+
{emptyText || `TRY ${separator} USD`}
|
|
102
|
+
</span>
|
|
103
|
+
) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (mode === 'select') {
|
|
107
|
+
return (
|
|
108
|
+
<select
|
|
109
|
+
data-currency-selector=""
|
|
110
|
+
{...partAttrs('dropdown')}
|
|
111
|
+
value={currency || ''}
|
|
112
|
+
onChange={(event) => change(event.target.value)}
|
|
113
|
+
aria-label={ariaLabel}
|
|
114
|
+
style={{
|
|
115
|
+
background: 'transparent',
|
|
116
|
+
border: 'none',
|
|
117
|
+
color: 'inherit',
|
|
118
|
+
cursor: 'pointer',
|
|
119
|
+
outline: 'none'
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{items.map((item) => (
|
|
123
|
+
<option key={item.code} value={item.code}>
|
|
124
|
+
{format(readLabel(item))}
|
|
125
|
+
</option>
|
|
126
|
+
))}
|
|
127
|
+
</select>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<span
|
|
133
|
+
data-currency-selector=""
|
|
134
|
+
{...partAttrs('option-list')}
|
|
135
|
+
style={{
|
|
136
|
+
display: 'inline-flex',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
gap: `${gap}px`
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{items.map((item, index) => {
|
|
142
|
+
const active = item.code === currency;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<React.Fragment key={item.code}>
|
|
146
|
+
{index > 0 && showSeparator && (
|
|
147
|
+
<span aria-hidden="true" {...partAttrs('separator')}>
|
|
148
|
+
{separator}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() => change(item.code)}
|
|
154
|
+
aria-current={active}
|
|
155
|
+
data-currency-option={active ? 'selected' : 'default'}
|
|
156
|
+
{...partAttrs('option', { selected: active })}
|
|
157
|
+
style={{
|
|
158
|
+
background: 'none',
|
|
159
|
+
border: 'none',
|
|
160
|
+
padding: 0,
|
|
161
|
+
cursor: 'pointer',
|
|
162
|
+
color: 'inherit',
|
|
163
|
+
font: 'inherit',
|
|
164
|
+
opacity: active ? 1 : 0.6,
|
|
165
|
+
fontWeight: active ? 600 : 400
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{format(readLabel(item))}
|
|
169
|
+
</button>
|
|
170
|
+
</React.Fragment>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
</span>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default CurrencySelectorBlock;
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { useGetMenuQuery } from '@akinon/next/data/client/misc';
|
|
6
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
7
|
+
import { useOverlay } from '../hooks/use-overlay';
|
|
8
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
9
|
+
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
10
|
+
import { readToggle } from './product-block-utils';
|
|
11
|
+
|
|
12
|
+
interface MenuItemLike {
|
|
13
|
+
pk?: number | string;
|
|
14
|
+
label?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
children?: MenuItemLike[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const clampNumber = (
|
|
20
|
+
value: unknown,
|
|
21
|
+
fallback: number,
|
|
22
|
+
min: number,
|
|
23
|
+
max: number
|
|
24
|
+
) => {
|
|
25
|
+
const numeric = Math.round(Number(value));
|
|
26
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
27
|
+
return Math.min(max, Math.max(min, numeric));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Functional chrome block: hamburger button opening a slide-in drawer
|
|
32
|
+
* with the commerce menu tree — the standard mobile navigation. Menu
|
|
33
|
+
* CONTENT stays commerce-driven; the editor owns placement/visibility
|
|
34
|
+
* (typically visible on mobile only), the drawer geometry, every label and
|
|
35
|
+
* the styling of each piece.
|
|
36
|
+
* Properties: depth, parent, side, drawerWidth, drawerMaxWidth, iconSize,
|
|
37
|
+
* showTriggerLabel, triggerLabel, ariaLabel, closeLabel, toggleLabel,
|
|
38
|
+
* showOverlay, showClose, lockScroll, closeOnNavigate, expandMode, maxItems,
|
|
39
|
+
* emptyText.
|
|
40
|
+
* Parts: trigger, icon, overlay, drawer, header, close, menu, item-row, item,
|
|
41
|
+
* toggle, submenu, submenu-item, empty.
|
|
42
|
+
*/
|
|
43
|
+
const HamburgerMenuBlock = ({
|
|
44
|
+
block,
|
|
45
|
+
placeholderId,
|
|
46
|
+
sectionId,
|
|
47
|
+
selectedBlockId = null,
|
|
48
|
+
currentBreakpoint = 'desktop',
|
|
49
|
+
isDesigner = false
|
|
50
|
+
}: BlockRendererProps) => {
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const [expanded, setExpanded] = useState<string[]>([]);
|
|
53
|
+
|
|
54
|
+
const depth = Number(
|
|
55
|
+
getResponsiveValue(block.properties?.depth, currentBreakpoint, 2)
|
|
56
|
+
);
|
|
57
|
+
const parent = block.properties?.parent
|
|
58
|
+
? String(getResponsiveValue(block.properties.parent, currentBreakpoint, ''))
|
|
59
|
+
: undefined;
|
|
60
|
+
const side = String(
|
|
61
|
+
getResponsiveValue(block.properties?.side, currentBreakpoint, 'left')
|
|
62
|
+
);
|
|
63
|
+
const drawerWidth = clampNumber(
|
|
64
|
+
getResponsiveValue(block.properties?.drawerWidth, currentBreakpoint, 320),
|
|
65
|
+
320,
|
|
66
|
+
200,
|
|
67
|
+
720
|
|
68
|
+
);
|
|
69
|
+
const drawerMaxWidth = clampNumber(
|
|
70
|
+
getResponsiveValue(block.properties?.drawerMaxWidth, currentBreakpoint, 85),
|
|
71
|
+
85,
|
|
72
|
+
40,
|
|
73
|
+
100
|
|
74
|
+
);
|
|
75
|
+
const iconSize = clampNumber(
|
|
76
|
+
getResponsiveValue(block.properties?.iconSize, currentBreakpoint, 24),
|
|
77
|
+
24,
|
|
78
|
+
12,
|
|
79
|
+
64
|
|
80
|
+
);
|
|
81
|
+
const ariaLabel = String(
|
|
82
|
+
getResponsiveValue(block.properties?.ariaLabel, currentBreakpoint, 'Menu')
|
|
83
|
+
);
|
|
84
|
+
const triggerLabel = String(
|
|
85
|
+
getResponsiveValue(
|
|
86
|
+
block.properties?.triggerLabel,
|
|
87
|
+
currentBreakpoint,
|
|
88
|
+
'Menu'
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
const showTriggerLabel = readToggle(
|
|
92
|
+
block.properties?.showTriggerLabel,
|
|
93
|
+
currentBreakpoint,
|
|
94
|
+
false
|
|
95
|
+
);
|
|
96
|
+
const closeLabel = String(
|
|
97
|
+
getResponsiveValue(
|
|
98
|
+
block.properties?.closeLabel,
|
|
99
|
+
currentBreakpoint,
|
|
100
|
+
'Close menu'
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
const toggleLabel = String(
|
|
104
|
+
getResponsiveValue(
|
|
105
|
+
block.properties?.toggleLabel,
|
|
106
|
+
currentBreakpoint,
|
|
107
|
+
'Toggle submenu'
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
const emptyText = String(
|
|
111
|
+
getResponsiveValue(
|
|
112
|
+
block.properties?.emptyText,
|
|
113
|
+
currentBreakpoint,
|
|
114
|
+
'Menu (no items)'
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
const showOverlay = readToggle(
|
|
118
|
+
block.properties?.showOverlay,
|
|
119
|
+
currentBreakpoint,
|
|
120
|
+
true
|
|
121
|
+
);
|
|
122
|
+
const showClose = readToggle(
|
|
123
|
+
block.properties?.showClose,
|
|
124
|
+
currentBreakpoint,
|
|
125
|
+
true
|
|
126
|
+
);
|
|
127
|
+
const lockScroll = readToggle(
|
|
128
|
+
block.properties?.lockScroll,
|
|
129
|
+
currentBreakpoint,
|
|
130
|
+
true
|
|
131
|
+
);
|
|
132
|
+
const closeOnNavigate = readToggle(
|
|
133
|
+
block.properties?.closeOnNavigate,
|
|
134
|
+
currentBreakpoint,
|
|
135
|
+
true
|
|
136
|
+
);
|
|
137
|
+
const expandMode = String(
|
|
138
|
+
getResponsiveValue(
|
|
139
|
+
block.properties?.expandMode,
|
|
140
|
+
currentBreakpoint,
|
|
141
|
+
'single'
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
const maxItems = clampNumber(
|
|
145
|
+
getResponsiveValue(block.properties?.maxItems, currentBreakpoint, 0),
|
|
146
|
+
0,
|
|
147
|
+
0,
|
|
148
|
+
60
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const { data: menu } = useGetMenuQuery(
|
|
152
|
+
{
|
|
153
|
+
depth: Number.isFinite(depth) && depth > 0 ? depth : 2,
|
|
154
|
+
...(parent ? { parent } : {})
|
|
155
|
+
} as Parameters<typeof useGetMenuQuery>[0],
|
|
156
|
+
{ skip: !open }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const allItems: MenuItemLike[] = Array.isArray(menu) ? menu : [];
|
|
160
|
+
const items = maxItems > 0 ? allItems.slice(0, maxItems) : allItems;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The scroll lock is page-wide state, so it is the shared overlay
|
|
164
|
+
* primitive's counted one rather than a capture of `document.body` taken
|
|
165
|
+
* here.
|
|
166
|
+
*
|
|
167
|
+
* This block used to record the body's overflow itself, with no counter at
|
|
168
|
+
* all. Opened over a panel that had already frozen the page — a filter
|
|
169
|
+
* drawer, a quick view — it recorded `hidden` as "the page's own value", so
|
|
170
|
+
* that panel closing thawed the page under a drawer still up, and this
|
|
171
|
+
* drawer closing wrote `hidden` back for good and left a storefront nobody
|
|
172
|
+
* could scroll. The primitive keeps the page's own value with the first
|
|
173
|
+
* holder and restores it only when the last one lets go.
|
|
174
|
+
*
|
|
175
|
+
* `modal: false` is deliberate: the drawer draws a backdrop but has never
|
|
176
|
+
* carried `role="dialog"`, `aria-modal` or a focus trap, and giving it any
|
|
177
|
+
* of those means moving markup a published storefront already renders.
|
|
178
|
+
* `closeOnEscape: false` for the same reason — the drawer has never answered
|
|
179
|
+
* Escape, and arming it is a product decision, not part of this migration.
|
|
180
|
+
* What it joins the primitive for is the one counted lock and the one stack.
|
|
181
|
+
*
|
|
182
|
+
* `isDesigner` keeps it inert: the preview iframe is the author's canvas,
|
|
183
|
+
* not a shopper's viewport.
|
|
184
|
+
*/
|
|
185
|
+
useOverlay({
|
|
186
|
+
open,
|
|
187
|
+
isDesigner,
|
|
188
|
+
modal: false,
|
|
189
|
+
lockScroll,
|
|
190
|
+
closeOnEscape: false
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const isExpanded = (key: string) => expanded.includes(key);
|
|
194
|
+
|
|
195
|
+
const toggleExpanded = (key: string) =>
|
|
196
|
+
setExpanded((current) => {
|
|
197
|
+
if (current.includes(key)) {
|
|
198
|
+
return current.filter((entry) => entry !== key);
|
|
199
|
+
}
|
|
200
|
+
return expandMode === 'multi' ? [...current, key] : [key];
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Navigating inside the designer iframe would drop the author out of the
|
|
204
|
+
// page they are editing, so every drawer link is inert there.
|
|
205
|
+
const handleNavigate = (event: React.MouseEvent) => {
|
|
206
|
+
if (isDesigner) event.preventDefault();
|
|
207
|
+
if (closeOnNavigate) setOpen(false);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// A designer who marked a child as the row layout gets that layout repeated
|
|
211
|
+
// per drawer entry; otherwise the built-in rows render unchanged.
|
|
212
|
+
const customRows = hasItemTemplate(block, currentBreakpoint) ? (
|
|
213
|
+
<ItemTemplateList
|
|
214
|
+
block={block}
|
|
215
|
+
items={items.map((item, index) => ({
|
|
216
|
+
key: String(item.pk ?? index),
|
|
217
|
+
name: String(item.label ?? ''),
|
|
218
|
+
label: String(item.label ?? ''),
|
|
219
|
+
url: String(item.url ?? ''),
|
|
220
|
+
hasChildren: (item.children || []).length > 0
|
|
221
|
+
}))}
|
|
222
|
+
placeholderId={placeholderId}
|
|
223
|
+
sectionId={sectionId}
|
|
224
|
+
isDesigner={isDesigner}
|
|
225
|
+
selectedBlockId={selectedBlockId}
|
|
226
|
+
currentBreakpoint={currentBreakpoint}
|
|
227
|
+
designerPlaceholderCount={4}
|
|
228
|
+
/>
|
|
229
|
+
) : null;
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
data-hamburger-menu=""
|
|
236
|
+
{...partAttrs('trigger', { open })}
|
|
237
|
+
aria-label={ariaLabel}
|
|
238
|
+
aria-expanded={open}
|
|
239
|
+
onClick={() => setOpen(true)}
|
|
240
|
+
style={{
|
|
241
|
+
display: 'inline-flex',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
gap: showTriggerLabel ? '8px' : undefined,
|
|
244
|
+
background: 'none',
|
|
245
|
+
border: 'none',
|
|
246
|
+
padding: 0,
|
|
247
|
+
cursor: 'pointer',
|
|
248
|
+
color: 'inherit'
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<svg
|
|
252
|
+
width={iconSize}
|
|
253
|
+
height={iconSize}
|
|
254
|
+
viewBox="0 0 24 24"
|
|
255
|
+
fill="none"
|
|
256
|
+
stroke="currentColor"
|
|
257
|
+
strokeWidth="1.8"
|
|
258
|
+
strokeLinecap="round"
|
|
259
|
+
data-hamburger-icon=""
|
|
260
|
+
{...partAttrs('icon')}
|
|
261
|
+
>
|
|
262
|
+
<path d="M4 6h16M4 12h16M4 18h16" />
|
|
263
|
+
</svg>
|
|
264
|
+
{showTriggerLabel && (
|
|
265
|
+
<span data-hamburger-label="" {...partAttrs('label')}>
|
|
266
|
+
{triggerLabel}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</button>
|
|
270
|
+
|
|
271
|
+
{open && (
|
|
272
|
+
<div
|
|
273
|
+
data-hamburger-drawer=""
|
|
274
|
+
style={{ position: 'fixed', inset: 0, zIndex: 1000 }}
|
|
275
|
+
>
|
|
276
|
+
{showOverlay && (
|
|
277
|
+
<div
|
|
278
|
+
data-hamburger-overlay=""
|
|
279
|
+
{...partAttrs('overlay')}
|
|
280
|
+
onClick={() => setOpen(false)}
|
|
281
|
+
style={{
|
|
282
|
+
position: 'absolute',
|
|
283
|
+
inset: 0,
|
|
284
|
+
background: 'rgba(0,0,0,0.5)'
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
<div
|
|
289
|
+
data-hamburger-panel=""
|
|
290
|
+
{...partAttrs('drawer', { open: true })}
|
|
291
|
+
style={{
|
|
292
|
+
position: 'absolute',
|
|
293
|
+
top: 0,
|
|
294
|
+
bottom: 0,
|
|
295
|
+
[side === 'right' ? 'right' : 'left']: 0,
|
|
296
|
+
width: `min(${drawerWidth}px, ${drawerMaxWidth}vw)`,
|
|
297
|
+
background: '#ffffff',
|
|
298
|
+
color: '#111827',
|
|
299
|
+
display: 'flex',
|
|
300
|
+
flexDirection: 'column',
|
|
301
|
+
boxShadow: '0 0 24px rgba(0,0,0,0.2)',
|
|
302
|
+
overflowY: 'auto'
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
{showClose && (
|
|
306
|
+
<div
|
|
307
|
+
data-hamburger-header=""
|
|
308
|
+
{...partAttrs('header')}
|
|
309
|
+
style={{
|
|
310
|
+
display: 'flex',
|
|
311
|
+
justifyContent: 'flex-end',
|
|
312
|
+
padding: '12px'
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
aria-label={closeLabel}
|
|
318
|
+
data-hamburger-close=""
|
|
319
|
+
{...partAttrs('close')}
|
|
320
|
+
onClick={() => setOpen(false)}
|
|
321
|
+
style={{
|
|
322
|
+
background: 'none',
|
|
323
|
+
border: 'none',
|
|
324
|
+
cursor: 'pointer',
|
|
325
|
+
padding: '4px'
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
<svg
|
|
329
|
+
width="20"
|
|
330
|
+
height="20"
|
|
331
|
+
viewBox="0 0 24 24"
|
|
332
|
+
fill="none"
|
|
333
|
+
stroke="currentColor"
|
|
334
|
+
strokeWidth="1.8"
|
|
335
|
+
strokeLinecap="round"
|
|
336
|
+
>
|
|
337
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
338
|
+
</svg>
|
|
339
|
+
</button>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
<nav
|
|
343
|
+
data-hamburger-nav=""
|
|
344
|
+
{...partAttrs('menu')}
|
|
345
|
+
style={{ padding: '0 16px 24px' }}
|
|
346
|
+
>
|
|
347
|
+
{items.length === 0 && !customRows && isDesigner && (
|
|
348
|
+
<span
|
|
349
|
+
data-hamburger-empty=""
|
|
350
|
+
{...partAttrs('empty', { empty: true })}
|
|
351
|
+
style={{ display: 'block', padding: '14px 0', opacity: 0.6 }}
|
|
352
|
+
>
|
|
353
|
+
{emptyText}
|
|
354
|
+
</span>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{customRows ??
|
|
358
|
+
items.map((item, index) => {
|
|
359
|
+
const key = String(item.pk ?? index);
|
|
360
|
+
const hasChildren = (item.children || []).length > 0;
|
|
361
|
+
const rowOpen = hasChildren && isExpanded(key);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
key={key}
|
|
366
|
+
data-hamburger-item=""
|
|
367
|
+
{...partAttrs('item-row', { open: rowOpen })}
|
|
368
|
+
style={{ borderBottom: '1px solid #f3f4f6' }}
|
|
369
|
+
>
|
|
370
|
+
<div
|
|
371
|
+
style={{
|
|
372
|
+
display: 'flex',
|
|
373
|
+
alignItems: 'center',
|
|
374
|
+
justifyContent: 'space-between'
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
<NextLink
|
|
378
|
+
href={item.url || '#'}
|
|
379
|
+
data-hamburger-link=""
|
|
380
|
+
{...partAttrs('item')}
|
|
381
|
+
onClick={handleNavigate}
|
|
382
|
+
style={{
|
|
383
|
+
display: 'block',
|
|
384
|
+
padding: '14px 0',
|
|
385
|
+
color: 'inherit',
|
|
386
|
+
textDecoration: 'none',
|
|
387
|
+
fontWeight: 500
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
{item.label}
|
|
391
|
+
</NextLink>
|
|
392
|
+
{hasChildren && (
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
aria-label={toggleLabel}
|
|
396
|
+
aria-expanded={rowOpen}
|
|
397
|
+
data-hamburger-toggle=""
|
|
398
|
+
{...partAttrs('toggle', { open: rowOpen })}
|
|
399
|
+
onClick={() => toggleExpanded(key)}
|
|
400
|
+
style={{
|
|
401
|
+
background: 'none',
|
|
402
|
+
border: 'none',
|
|
403
|
+
cursor: 'pointer',
|
|
404
|
+
padding: '8px'
|
|
405
|
+
}}
|
|
406
|
+
>
|
|
407
|
+
<svg
|
|
408
|
+
width="16"
|
|
409
|
+
height="16"
|
|
410
|
+
viewBox="0 0 24 24"
|
|
411
|
+
fill="none"
|
|
412
|
+
stroke="currentColor"
|
|
413
|
+
strokeWidth="1.8"
|
|
414
|
+
style={{
|
|
415
|
+
transform: rowOpen ? 'rotate(180deg)' : 'none',
|
|
416
|
+
transition: 'transform 150ms'
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
<path d="M6 9l6 6 6-6" />
|
|
420
|
+
</svg>
|
|
421
|
+
</button>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
{hasChildren && rowOpen && (
|
|
425
|
+
<div
|
|
426
|
+
data-hamburger-submenu=""
|
|
427
|
+
{...partAttrs('submenu', { open: true })}
|
|
428
|
+
style={{ paddingBottom: '10px' }}
|
|
429
|
+
>
|
|
430
|
+
{(item.children || []).map((child, childIndex) => (
|
|
431
|
+
<NextLink
|
|
432
|
+
key={child.pk ?? childIndex}
|
|
433
|
+
href={child.url || '#'}
|
|
434
|
+
data-hamburger-submenu-link=""
|
|
435
|
+
{...partAttrs('submenu-item')}
|
|
436
|
+
onClick={handleNavigate}
|
|
437
|
+
style={{
|
|
438
|
+
display: 'block',
|
|
439
|
+
padding: '8px 0 8px 12px',
|
|
440
|
+
color: '#4b5563',
|
|
441
|
+
textDecoration: 'none',
|
|
442
|
+
fontSize: '14px'
|
|
443
|
+
}}
|
|
444
|
+
>
|
|
445
|
+
{child.label}
|
|
446
|
+
</NextLink>
|
|
447
|
+
))}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
})}
|
|
453
|
+
</nav>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export default HamburgerMenuBlock;
|