blocks-dusted 0.1.9 → 0.1.10
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.
|
@@ -1,199 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useMemo, useState } from
|
|
4
|
-
import Link from
|
|
5
|
-
import { usePathname } from
|
|
6
|
-
import { motion, AnimatePresence } from
|
|
7
|
-
import * as LucideIcons from
|
|
8
|
-
import { Home, ChevronDown } from
|
|
9
|
-
import type { Header as HeaderType } from
|
|
10
|
-
import RichText from
|
|
11
|
-
import { cn } from
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
7
|
+
import * as LucideIcons from "lucide-react";
|
|
8
|
+
import { Home, ChevronDown } from "lucide-react";
|
|
9
|
+
import type { Header as HeaderType } from "@/payload-types";
|
|
10
|
+
import RichText from "@/components/RichText";
|
|
11
|
+
import { cn } from "@/utilities/ui";
|
|
12
12
|
import {
|
|
13
13
|
TooltipProvider,
|
|
14
14
|
Tooltip,
|
|
15
15
|
TooltipTrigger,
|
|
16
|
-
TooltipContent
|
|
17
|
-
} from
|
|
16
|
+
TooltipContent,
|
|
17
|
+
} from "@/components/animate-ui/components/animate/tooltip";
|
|
18
18
|
|
|
19
19
|
interface MobileBottomNavProps {
|
|
20
|
-
data?: HeaderType
|
|
20
|
+
data?: HeaderType;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
type HeaderNavItem = NonNullable<
|
|
24
|
+
NonNullable<HeaderType["header02"]>["navItems"]
|
|
25
|
+
>[number] &
|
|
26
|
+
Record<string, any>;
|
|
27
|
+
type HeaderChildItem = Record<string, any>;
|
|
28
|
+
|
|
23
29
|
const getLinkHref = (link: any): string => {
|
|
24
|
-
if (!link) return
|
|
30
|
+
if (!link) return "#";
|
|
25
31
|
|
|
26
|
-
if (link.type ===
|
|
27
|
-
const value = link.reference?.value
|
|
32
|
+
if (link.type === "reference") {
|
|
33
|
+
const value = link.reference?.value;
|
|
28
34
|
|
|
29
|
-
if (typeof value ===
|
|
30
|
-
return `/${value.slug}
|
|
35
|
+
if (typeof value === "object" && "slug" in value) {
|
|
36
|
+
return `/${value.slug}`;
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
return link.url ||
|
|
35
|
-
}
|
|
40
|
+
return link.url || "#";
|
|
41
|
+
};
|
|
36
42
|
|
|
37
43
|
const resolveCustomIconUrl = (customIcon?: any): string | null => {
|
|
38
|
-
if (!customIcon) return null
|
|
39
|
-
if (typeof customIcon ===
|
|
40
|
-
if (typeof customIcon ===
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
+
if (!customIcon) return null;
|
|
45
|
+
if (typeof customIcon === "string") return customIcon;
|
|
46
|
+
if (typeof customIcon === "number") return null;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
typeof customIcon === "object" &&
|
|
50
|
+
"url" in customIcon &&
|
|
51
|
+
typeof customIcon.url === "string"
|
|
52
|
+
) {
|
|
53
|
+
return customIcon.url;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
48
58
|
|
|
49
59
|
const normalizePath = (value?: string | null) => {
|
|
50
|
-
if (!value) return
|
|
60
|
+
if (!value) return "/";
|
|
51
61
|
|
|
52
|
-
const [base] = value.split(/[?#]/)
|
|
62
|
+
const [base] = value.split(/[?#]/);
|
|
53
63
|
|
|
54
|
-
if (!base) return
|
|
64
|
+
if (!base) return "/";
|
|
55
65
|
|
|
56
|
-
if (base !==
|
|
57
|
-
return base.slice(0, -1)
|
|
66
|
+
if (base !== "/" && base.endsWith("/")) {
|
|
67
|
+
return base.slice(0, -1);
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
return base ||
|
|
61
|
-
}
|
|
70
|
+
return base || "/";
|
|
71
|
+
};
|
|
62
72
|
|
|
63
73
|
const normalizeProductRoute = (href?: string | null, label?: string | null) => {
|
|
64
|
-
const normalizedHref = normalizePath(href)
|
|
65
|
-
const normalizedLabel = (label ||
|
|
74
|
+
const normalizedHref = normalizePath(href);
|
|
75
|
+
const normalizedLabel = (label || "").toLowerCase().trim();
|
|
66
76
|
|
|
67
|
-
if (normalizedHref ===
|
|
68
|
-
return
|
|
77
|
+
if (normalizedHref === "/templates" || normalizedLabel === "templates") {
|
|
78
|
+
return "/products/templates";
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
if (normalizedHref ===
|
|
72
|
-
return
|
|
81
|
+
if (normalizedHref === "/components" || normalizedLabel === "components") {
|
|
82
|
+
return "/products/components";
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
return normalizedHref
|
|
76
|
-
}
|
|
85
|
+
return normalizedHref;
|
|
86
|
+
};
|
|
77
87
|
|
|
78
|
-
const isPathActive = (
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
const isPathActive = (
|
|
89
|
+
href: string,
|
|
90
|
+
currentPathname: string,
|
|
91
|
+
label?: string | null,
|
|
92
|
+
) => {
|
|
93
|
+
const normalizedHref = normalizeProductRoute(href, label);
|
|
94
|
+
const normalizedCurrentPathname = normalizePath(currentPathname);
|
|
81
95
|
|
|
82
|
-
if (normalizedHref === normalizedCurrentPathname) return true
|
|
83
|
-
if (normalizedHref ===
|
|
96
|
+
if (normalizedHref === normalizedCurrentPathname) return true;
|
|
97
|
+
if (normalizedHref === "/") return false;
|
|
84
98
|
|
|
85
|
-
return normalizedCurrentPathname.startsWith(`${normalizedHref}/`)
|
|
86
|
-
}
|
|
99
|
+
return normalizedCurrentPathname.startsWith(`${normalizedHref}/`);
|
|
100
|
+
};
|
|
87
101
|
|
|
88
|
-
const renderIcon = (
|
|
89
|
-
|
|
102
|
+
const renderIcon = (
|
|
103
|
+
iconName?: string | null,
|
|
104
|
+
className?: string,
|
|
105
|
+
customIcon?: any,
|
|
106
|
+
) => {
|
|
107
|
+
const customUrl = resolveCustomIconUrl(customIcon);
|
|
90
108
|
|
|
91
109
|
if (customUrl) {
|
|
92
110
|
return (
|
|
93
111
|
<img
|
|
94
112
|
src={customUrl}
|
|
95
|
-
className={className ||
|
|
113
|
+
className={className || "size-5"}
|
|
96
114
|
alt=""
|
|
97
115
|
aria-hidden="true"
|
|
98
116
|
loading="lazy"
|
|
99
117
|
/>
|
|
100
|
-
)
|
|
118
|
+
);
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
if (!iconName) return null
|
|
121
|
+
if (!iconName) return null;
|
|
104
122
|
|
|
105
|
-
const IconComponent = (LucideIcons as any)[iconName]
|
|
123
|
+
const IconComponent = (LucideIcons as any)[iconName];
|
|
106
124
|
|
|
107
|
-
if (!IconComponent) return null
|
|
125
|
+
if (!IconComponent) return null;
|
|
108
126
|
|
|
109
|
-
return <IconComponent className={className ||
|
|
110
|
-
}
|
|
127
|
+
return <IconComponent className={className || "size-5"} />;
|
|
128
|
+
};
|
|
111
129
|
|
|
112
130
|
const menuButtonVariants = {
|
|
113
131
|
top: {
|
|
114
132
|
closed: { rotate: 0, translateY: 0 },
|
|
115
|
-
open: { rotate: 45, translateY: 6 }
|
|
133
|
+
open: { rotate: 45, translateY: 6 },
|
|
116
134
|
},
|
|
117
135
|
middle: {
|
|
118
136
|
closed: { opacity: 1 },
|
|
119
|
-
open: { opacity: 0 }
|
|
137
|
+
open: { opacity: 0 },
|
|
120
138
|
},
|
|
121
139
|
bottom: {
|
|
122
140
|
closed: { rotate: 0, translateY: 0 },
|
|
123
|
-
open: { rotate: -45, translateY: -6 }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
141
|
+
open: { rotate: -45, translateY: -6 },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
126
144
|
|
|
127
|
-
const barClass =
|
|
145
|
+
const barClass = "menubar absolute left-0 h-[1.8px] w-full bg-current";
|
|
128
146
|
|
|
129
147
|
export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
130
|
-
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
131
|
-
const [expandedDropdown, setExpandedDropdown] = useState<number | null>(null)
|
|
148
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
149
|
+
const [expandedDropdown, setExpandedDropdown] = useState<number | null>(null);
|
|
132
150
|
|
|
133
|
-
const pathname = usePathname()
|
|
134
|
-
const normalizedCurrentPathname = useMemo(
|
|
151
|
+
const pathname = usePathname();
|
|
152
|
+
const normalizedCurrentPathname = useMemo(
|
|
153
|
+
() => normalizePath(pathname),
|
|
154
|
+
[pathname],
|
|
155
|
+
);
|
|
135
156
|
|
|
136
|
-
const navItems = data?.header02?.navItems ||
|
|
137
|
-
|
|
157
|
+
const navItems: HeaderNavItem[] = (data?.header02?.navItems ||
|
|
158
|
+
(data as any)?.navItems ||
|
|
159
|
+
[]) as HeaderNavItem[];
|
|
160
|
+
const showLabels = data?.header02?.showMobileLabels ?? true;
|
|
138
161
|
|
|
139
162
|
useEffect(() => {
|
|
140
163
|
if (isMenuOpen) {
|
|
141
|
-
const scrollY = window.scrollY
|
|
142
|
-
|
|
143
|
-
document.documentElement.style.overflow =
|
|
144
|
-
document.documentElement.style.touchAction =
|
|
145
|
-
document.documentElement.style.overscrollBehavior =
|
|
146
|
-
|
|
147
|
-
document.body.style.overflow =
|
|
148
|
-
document.body.style.position =
|
|
149
|
-
document.body.style.top = `-${scrollY}px
|
|
150
|
-
document.body.style.width =
|
|
151
|
-
document.body.style.touchAction =
|
|
152
|
-
document.body.style.overscrollBehavior =
|
|
164
|
+
const scrollY = window.scrollY;
|
|
165
|
+
|
|
166
|
+
document.documentElement.style.overflow = "hidden";
|
|
167
|
+
document.documentElement.style.touchAction = "none";
|
|
168
|
+
document.documentElement.style.overscrollBehavior = "none";
|
|
169
|
+
|
|
170
|
+
document.body.style.overflow = "hidden";
|
|
171
|
+
document.body.style.position = "fixed";
|
|
172
|
+
document.body.style.top = `-${scrollY}px`;
|
|
173
|
+
document.body.style.width = "100%";
|
|
174
|
+
document.body.style.touchAction = "none";
|
|
175
|
+
document.body.style.overscrollBehavior = "none";
|
|
153
176
|
} else {
|
|
154
|
-
const scrollY = document.body.style.top
|
|
177
|
+
const scrollY = document.body.style.top;
|
|
155
178
|
|
|
156
|
-
document.documentElement.style.overflow =
|
|
157
|
-
document.documentElement.style.touchAction =
|
|
158
|
-
document.documentElement.style.overscrollBehavior =
|
|
179
|
+
document.documentElement.style.overflow = "";
|
|
180
|
+
document.documentElement.style.touchAction = "";
|
|
181
|
+
document.documentElement.style.overscrollBehavior = "";
|
|
159
182
|
|
|
160
|
-
document.body.style.overflow =
|
|
161
|
-
document.body.style.position =
|
|
162
|
-
document.body.style.top =
|
|
163
|
-
document.body.style.width =
|
|
164
|
-
document.body.style.touchAction =
|
|
165
|
-
document.body.style.overscrollBehavior =
|
|
183
|
+
document.body.style.overflow = "";
|
|
184
|
+
document.body.style.position = "";
|
|
185
|
+
document.body.style.top = "";
|
|
186
|
+
document.body.style.width = "";
|
|
187
|
+
document.body.style.touchAction = "";
|
|
188
|
+
document.body.style.overscrollBehavior = "";
|
|
166
189
|
|
|
167
|
-
window.scrollTo(0, parseInt(scrollY ||
|
|
190
|
+
window.scrollTo(0, parseInt(scrollY || "0", 10) * -1);
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
return () => {
|
|
171
|
-
document.documentElement.style.overflow =
|
|
172
|
-
document.documentElement.style.touchAction =
|
|
173
|
-
document.documentElement.style.overscrollBehavior =
|
|
174
|
-
|
|
175
|
-
document.body.style.overflow =
|
|
176
|
-
document.body.style.position =
|
|
177
|
-
document.body.style.top =
|
|
178
|
-
document.body.style.width =
|
|
179
|
-
document.body.style.touchAction =
|
|
180
|
-
document.body.style.overscrollBehavior =
|
|
181
|
-
}
|
|
182
|
-
}, [isMenuOpen])
|
|
194
|
+
document.documentElement.style.overflow = "";
|
|
195
|
+
document.documentElement.style.touchAction = "";
|
|
196
|
+
document.documentElement.style.overscrollBehavior = "";
|
|
197
|
+
|
|
198
|
+
document.body.style.overflow = "";
|
|
199
|
+
document.body.style.position = "";
|
|
200
|
+
document.body.style.top = "";
|
|
201
|
+
document.body.style.width = "";
|
|
202
|
+
document.body.style.touchAction = "";
|
|
203
|
+
document.body.style.overscrollBehavior = "";
|
|
204
|
+
};
|
|
205
|
+
}, [isMenuOpen]);
|
|
183
206
|
|
|
184
207
|
// Include both simple links and dropdowns (using first child's link)
|
|
185
|
-
const bottomNavLinks = navItems.slice(0, 4)
|
|
208
|
+
const bottomNavLinks = navItems.slice(0, 4);
|
|
186
209
|
|
|
187
210
|
const toggleDropdown = (index: number) => {
|
|
188
|
-
setExpandedDropdown(expandedDropdown === index ? null : index)
|
|
189
|
-
}
|
|
211
|
+
setExpandedDropdown(expandedDropdown === index ? null : index);
|
|
212
|
+
};
|
|
190
213
|
|
|
191
214
|
// Helper to get URL for bottom nav items (handles both links and dropdowns)
|
|
192
215
|
const getBottomNavHref = (navItem: any) => {
|
|
193
|
-
const label = getBottomNavLabel(navItem)
|
|
216
|
+
const label = getBottomNavLabel(navItem);
|
|
194
217
|
|
|
195
|
-
if (navItem.type ===
|
|
196
|
-
return normalizeProductRoute(getLinkHref(navItem.link), label)
|
|
218
|
+
if (navItem.type === "link") {
|
|
219
|
+
return normalizeProductRoute(getLinkHref(navItem.link), label);
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
const children =
|
|
@@ -201,33 +224,40 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
201
224
|
navItem.v2_children ||
|
|
202
225
|
navItem.v4_children ||
|
|
203
226
|
navItem.v5_children ||
|
|
204
|
-
navItem.v6_children
|
|
227
|
+
navItem.v6_children;
|
|
205
228
|
|
|
206
|
-
return normalizeProductRoute(
|
|
207
|
-
|
|
229
|
+
return normalizeProductRoute(
|
|
230
|
+
children?.[0]?.link ? getLinkHref(children[0].link) : "#",
|
|
231
|
+
label,
|
|
232
|
+
);
|
|
233
|
+
};
|
|
208
234
|
|
|
209
235
|
// Helper to get label for bottom nav items
|
|
210
236
|
const getBottomNavLabel = (navItem: any) => {
|
|
211
|
-
return navItem.type ===
|
|
212
|
-
}
|
|
237
|
+
return navItem.type === "link" ? navItem.link?.label : navItem.label;
|
|
238
|
+
};
|
|
213
239
|
|
|
214
240
|
// Helper to get icon for bottom nav items
|
|
215
241
|
const getBottomNavIcon = (navItem: any) => {
|
|
216
|
-
if (navItem.type ===
|
|
217
|
-
return renderIcon(
|
|
242
|
+
if (navItem.type === "link") {
|
|
243
|
+
return renderIcon(
|
|
244
|
+
navItem.link?.icon,
|
|
245
|
+
"size-6",
|
|
246
|
+
(navItem.link as any)?.customIcon,
|
|
247
|
+
);
|
|
218
248
|
}
|
|
219
|
-
return renderIcon(navItem.triggerIcon,
|
|
220
|
-
}
|
|
249
|
+
return renderIcon(navItem.triggerIcon, "size-6");
|
|
250
|
+
};
|
|
221
251
|
|
|
222
252
|
return (
|
|
223
253
|
<>
|
|
224
254
|
<nav
|
|
225
255
|
className={cn(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
"mobile-bottom-nav fixed bottom-0 left-0 right-0 z-[999] mx-4 mb-4 flex rounded-full border-t border-border px-4 backdrop-blur-md",
|
|
257
|
+
"bg-[var(--navbar-background)] shadow-[0_-2px_10px_rgba(0,0,0,0.1)]",
|
|
258
|
+
"pb-[env(safe-area-inset-bottom)] [filter:drop-shadow(2px_4px_6px_black)]",
|
|
259
|
+
"animate-[slideUpNav_0.3s_ease-out]",
|
|
260
|
+
"min-[978px]:hidden",
|
|
231
261
|
)}
|
|
232
262
|
>
|
|
233
263
|
<TooltipProvider>
|
|
@@ -235,7 +265,7 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
235
265
|
<Tooltip>
|
|
236
266
|
<TooltipTrigger asChild>
|
|
237
267
|
<motion.button
|
|
238
|
-
aria-label={isMenuOpen ?
|
|
268
|
+
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
|
239
269
|
aria-expanded={isMenuOpen}
|
|
240
270
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
241
271
|
whileTap={{ scale: 0.96 }}
|
|
@@ -244,45 +274,61 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
244
274
|
<span className="relative block h-4 w-4">
|
|
245
275
|
<motion.span
|
|
246
276
|
className={barClass}
|
|
247
|
-
style={{ top:
|
|
248
|
-
animate={isMenuOpen ?
|
|
277
|
+
style={{ top: "0px" }}
|
|
278
|
+
animate={isMenuOpen ? "open" : "closed"}
|
|
249
279
|
variants={menuButtonVariants.top}
|
|
250
|
-
transition={{
|
|
280
|
+
transition={{
|
|
281
|
+
type: "spring",
|
|
282
|
+
stiffness: 600,
|
|
283
|
+
damping: 30,
|
|
284
|
+
}}
|
|
251
285
|
/>
|
|
252
286
|
|
|
253
287
|
<motion.span
|
|
254
288
|
className={barClass}
|
|
255
|
-
style={{ top:
|
|
256
|
-
animate={isMenuOpen ?
|
|
289
|
+
style={{ top: "6px" }}
|
|
290
|
+
animate={isMenuOpen ? "open" : "closed"}
|
|
257
291
|
variants={menuButtonVariants.middle}
|
|
258
292
|
transition={{ duration: 0.18, ease: [0.22, 1, 0.36, 1] }}
|
|
259
293
|
/>
|
|
260
294
|
|
|
261
295
|
<motion.span
|
|
262
296
|
className="menubar absolute left-0 h-[1.8px] w-full bg-lime-500"
|
|
263
|
-
style={{ top:
|
|
264
|
-
animate={isMenuOpen ?
|
|
297
|
+
style={{ top: "12px" }}
|
|
298
|
+
animate={isMenuOpen ? "open" : "closed"}
|
|
265
299
|
variants={menuButtonVariants.bottom}
|
|
266
|
-
transition={{
|
|
300
|
+
transition={{
|
|
301
|
+
type: "spring",
|
|
302
|
+
stiffness: 600,
|
|
303
|
+
damping: 30,
|
|
304
|
+
}}
|
|
267
305
|
/>
|
|
268
306
|
</span>
|
|
269
307
|
|
|
270
|
-
{showLabels &&
|
|
308
|
+
{showLabels && (
|
|
309
|
+
<span className="text-xs font-medium">Menu</span>
|
|
310
|
+
)}
|
|
271
311
|
</motion.button>
|
|
272
312
|
</TooltipTrigger>
|
|
273
313
|
|
|
274
314
|
<TooltipContent className="bg-white text-black">
|
|
275
|
-
{isMenuOpen ?
|
|
315
|
+
{isMenuOpen ? "Close menu" : "Menu"}
|
|
276
316
|
</TooltipContent>
|
|
277
317
|
</Tooltip>
|
|
278
318
|
|
|
279
319
|
{bottomNavLinks.map((navItem, index) => {
|
|
280
|
-
const label = getBottomNavLabel(navItem)
|
|
281
|
-
const href = getBottomNavHref(navItem)
|
|
282
|
-
const isActive = isPathActive(
|
|
283
|
-
|
|
320
|
+
const label = getBottomNavLabel(navItem);
|
|
321
|
+
const href = getBottomNavHref(navItem);
|
|
322
|
+
const isActive = isPathActive(
|
|
323
|
+
href,
|
|
324
|
+
normalizedCurrentPathname,
|
|
325
|
+
label,
|
|
326
|
+
);
|
|
327
|
+
const icon = getBottomNavIcon(navItem);
|
|
284
328
|
const showLabelForItem =
|
|
285
|
-
navItem.type ===
|
|
329
|
+
navItem.type === "link"
|
|
330
|
+
? showLabels && (navItem.showLabel ?? true)
|
|
331
|
+
: showLabels;
|
|
286
332
|
|
|
287
333
|
return (
|
|
288
334
|
<Tooltip key={index}>
|
|
@@ -290,28 +336,32 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
290
336
|
<Link
|
|
291
337
|
href={href}
|
|
292
338
|
className={cn(
|
|
293
|
-
|
|
339
|
+
"nav-button flex h-8 w-8 flex-col items-center justify-center gap-1 px-3 py-0 text-stone-300 dark:text-stone-300 transition-colors hover:text-foreground",
|
|
294
340
|
isActive &&
|
|
295
|
-
|
|
341
|
+
"nav-button-active border-b-[3px] border-lime-500 font-semibold text-foreground transition-all duration-300",
|
|
296
342
|
)}
|
|
297
|
-
{...(navItem.type ===
|
|
298
|
-
? { target:
|
|
343
|
+
{...(navItem.type === "link" && navItem.link?.newTab
|
|
344
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
299
345
|
: {})}
|
|
300
|
-
aria-current={isActive ?
|
|
346
|
+
aria-current={isActive ? "page" : undefined}
|
|
301
347
|
aria-label={label || undefined}
|
|
302
348
|
onClick={() => setIsMenuOpen(false)}
|
|
303
349
|
>
|
|
304
350
|
{icon || <Home className="size-6" />}
|
|
305
351
|
|
|
306
352
|
{showLabelForItem && (
|
|
307
|
-
<span className="max-w-[60px] truncate text-xs font-medium">
|
|
353
|
+
<span className="max-w-[60px] truncate text-xs font-medium">
|
|
354
|
+
{label}
|
|
355
|
+
</span>
|
|
308
356
|
)}
|
|
309
357
|
</Link>
|
|
310
358
|
</TooltipTrigger>
|
|
311
359
|
|
|
312
|
-
<TooltipContent className="bg-white text-black">
|
|
360
|
+
<TooltipContent className="bg-white text-black">
|
|
361
|
+
{label}
|
|
362
|
+
</TooltipContent>
|
|
313
363
|
</Tooltip>
|
|
314
|
-
)
|
|
364
|
+
);
|
|
315
365
|
})}
|
|
316
366
|
</div>
|
|
317
367
|
</TooltipProvider>
|
|
@@ -330,10 +380,10 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
330
380
|
onClick={() => setIsMenuOpen(false)}
|
|
331
381
|
>
|
|
332
382
|
<motion.div
|
|
333
|
-
initial={{ y:
|
|
383
|
+
initial={{ y: "100%" }}
|
|
334
384
|
animate={{ y: 0 }}
|
|
335
|
-
exit={{ y:
|
|
336
|
-
transition={{ duration: 0.3, ease:
|
|
385
|
+
exit={{ y: "100%" }}
|
|
386
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
337
387
|
className="absolute inset-0 flex flex-col bg-background"
|
|
338
388
|
onClick={(e) => e.stopPropagation()}
|
|
339
389
|
>
|
|
@@ -346,39 +396,57 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
346
396
|
data-lenis-prevent
|
|
347
397
|
>
|
|
348
398
|
{navItems.map((navItem, index) => {
|
|
349
|
-
if (navItem.type ===
|
|
350
|
-
const label = getBottomNavLabel(navItem)
|
|
351
|
-
const href = normalizeProductRoute(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
399
|
+
if (navItem.type === "link") {
|
|
400
|
+
const label = getBottomNavLabel(navItem);
|
|
401
|
+
const href = normalizeProductRoute(
|
|
402
|
+
getLinkHref(navItem.link),
|
|
403
|
+
label,
|
|
404
|
+
);
|
|
405
|
+
const isActive = isPathActive(
|
|
406
|
+
href,
|
|
407
|
+
normalizedCurrentPathname,
|
|
408
|
+
label,
|
|
409
|
+
);
|
|
410
|
+
const icon = renderIcon(
|
|
411
|
+
navItem.link?.icon,
|
|
412
|
+
"size-5",
|
|
413
|
+
(navItem.link as any)?.customIcon,
|
|
414
|
+
);
|
|
415
|
+
const iconPosition =
|
|
416
|
+
(navItem.link as any)?.iconPosition || "before";
|
|
355
417
|
|
|
356
418
|
return (
|
|
357
419
|
<Link
|
|
358
420
|
key={index}
|
|
359
421
|
href={href}
|
|
360
|
-
aria-current={isActive ?
|
|
422
|
+
aria-current={isActive ? "page" : undefined}
|
|
361
423
|
onClick={() => setIsMenuOpen(false)}
|
|
362
424
|
className={cn(
|
|
363
|
-
|
|
364
|
-
isActive &&
|
|
425
|
+
"flex items-center gap-3 rounded-lg px-4 py-3 transition-colors hover:bg-stone-800/30",
|
|
426
|
+
isActive &&
|
|
427
|
+
"bg-accent font-semibold text-accent-foreground",
|
|
365
428
|
)}
|
|
366
429
|
{...(navItem.link?.newTab
|
|
367
|
-
? { target:
|
|
430
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
368
431
|
: {})}
|
|
369
432
|
>
|
|
370
|
-
{icon && iconPosition ===
|
|
371
|
-
<span className="text-base font-medium">
|
|
372
|
-
|
|
433
|
+
{icon && iconPosition === "before" && icon}
|
|
434
|
+
<span className="text-base font-medium">
|
|
435
|
+
{navItem.link?.label}
|
|
436
|
+
</span>
|
|
437
|
+
{icon && iconPosition === "after" && icon}
|
|
373
438
|
</Link>
|
|
374
|
-
)
|
|
439
|
+
);
|
|
375
440
|
}
|
|
376
441
|
|
|
377
|
-
const triggerIcon = renderIcon(navItem.triggerIcon,
|
|
378
|
-
const isExpanded = expandedDropdown === index
|
|
442
|
+
const triggerIcon = renderIcon(navItem.triggerIcon, "size-5");
|
|
443
|
+
const isExpanded = expandedDropdown === index;
|
|
379
444
|
|
|
380
445
|
return (
|
|
381
|
-
<div
|
|
446
|
+
<div
|
|
447
|
+
key={index}
|
|
448
|
+
className="overflow-hidden rounded-lg border border-border"
|
|
449
|
+
>
|
|
382
450
|
<button
|
|
383
451
|
aria-expanded={isExpanded}
|
|
384
452
|
onClick={() => toggleDropdown(index)}
|
|
@@ -386,13 +454,15 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
386
454
|
>
|
|
387
455
|
<div className="flex items-center gap-3">
|
|
388
456
|
{triggerIcon}
|
|
389
|
-
<span className="text-base font-medium">
|
|
457
|
+
<span className="text-base font-medium">
|
|
458
|
+
{navItem.label}
|
|
459
|
+
</span>
|
|
390
460
|
</div>
|
|
391
461
|
|
|
392
462
|
<ChevronDown
|
|
393
463
|
className={cn(
|
|
394
|
-
|
|
395
|
-
isExpanded &&
|
|
464
|
+
"size-5 transition-transform duration-200",
|
|
465
|
+
isExpanded && "rotate-180",
|
|
396
466
|
)}
|
|
397
467
|
/>
|
|
398
468
|
</button>
|
|
@@ -401,12 +471,12 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
401
471
|
{isExpanded && (
|
|
402
472
|
<motion.div
|
|
403
473
|
initial={{ height: 0, opacity: 0 }}
|
|
404
|
-
animate={{ height:
|
|
474
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
405
475
|
exit={{ height: 0, opacity: 0 }}
|
|
406
476
|
transition={{ duration: 0.2 }}
|
|
407
477
|
className="overflow-hidden bg-muted/50"
|
|
408
478
|
>
|
|
409
|
-
{navItem.type ===
|
|
479
|
+
{navItem.type === "dropdown-richtext" && (
|
|
410
480
|
<div className="space-y-4 p-4">
|
|
411
481
|
{navItem.richTextContent && (
|
|
412
482
|
<div className="rounded-md bg-gradient-to-b from-muted/50 to-muted px-3 py-2">
|
|
@@ -420,106 +490,131 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
420
490
|
)}
|
|
421
491
|
|
|
422
492
|
<div className="space-y-1 grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-8 ">
|
|
423
|
-
{navItem.v1_children?.map(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
{
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
493
|
+
{navItem.v1_children?.map(
|
|
494
|
+
(
|
|
495
|
+
child: HeaderChildItem,
|
|
496
|
+
childIndex: number,
|
|
497
|
+
) => {
|
|
498
|
+
const childLabel = child.link?.label;
|
|
499
|
+
const childHref = normalizeProductRoute(
|
|
500
|
+
getLinkHref(child.link),
|
|
501
|
+
childLabel,
|
|
502
|
+
);
|
|
503
|
+
const isChildActive = isPathActive(
|
|
504
|
+
childHref,
|
|
505
|
+
normalizedCurrentPathname,
|
|
506
|
+
childLabel,
|
|
507
|
+
);
|
|
508
|
+
const childIcon = renderIcon(
|
|
509
|
+
child.link?.icon,
|
|
510
|
+
"size-5",
|
|
511
|
+
child.link?.customIcon,
|
|
512
|
+
);
|
|
513
|
+
const childIconPosition =
|
|
514
|
+
child.link?.iconPosition || "before";
|
|
515
|
+
const showChildLabel =
|
|
516
|
+
showLabels &&
|
|
517
|
+
(("showLabel" in child
|
|
518
|
+
? child.showLabel
|
|
519
|
+
: undefined) ??
|
|
520
|
+
true);
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<Link
|
|
524
|
+
key={childIndex}
|
|
525
|
+
href={childHref}
|
|
526
|
+
aria-current={
|
|
527
|
+
isChildActive ? "page" : undefined
|
|
528
|
+
}
|
|
529
|
+
onClick={() => setIsMenuOpen(false)}
|
|
530
|
+
className={cn(
|
|
531
|
+
"rounded-md px-1 py-1 md:px-3 md:py-2 transition-colors hover:bg-stone-800/30 flex flex-col items-start justify-end align-top",
|
|
532
|
+
isChildActive &&
|
|
533
|
+
"bg-accent font-semibold text-stone-200 dark:text-stone-200",
|
|
534
|
+
)}
|
|
535
|
+
>
|
|
536
|
+
<div className="flex items-center gap-3">
|
|
537
|
+
{childIcon &&
|
|
538
|
+
childIconPosition === "before" &&
|
|
539
|
+
childIcon}
|
|
540
|
+
|
|
541
|
+
{/* show Label inside menu */}
|
|
542
|
+
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
543
|
+
{child.link?.label}
|
|
544
|
+
</span>
|
|
463
545
|
|
|
464
|
-
|
|
546
|
+
{/* {showChildLabel && (
|
|
465
547
|
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
466
548
|
{child.link?.label}
|
|
467
549
|
</span>
|
|
468
550
|
)} */}
|
|
469
551
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
552
|
+
{childIcon &&
|
|
553
|
+
childIconPosition === "after" &&
|
|
554
|
+
childIcon}
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{"description" in child &&
|
|
558
|
+
child.description && (
|
|
559
|
+
<p className="mt-1 text-xs text-stone-400 dark:text-stone-400 text-ellipsis [-webkit-line-clamp:2] [display:-webkit-box] [-webkit-box-orient:vertical] overflow-hidden">
|
|
560
|
+
{child.description}
|
|
561
|
+
</p>
|
|
562
|
+
)}
|
|
563
|
+
</Link>
|
|
564
|
+
);
|
|
565
|
+
},
|
|
566
|
+
)}
|
|
481
567
|
</div>
|
|
482
568
|
</div>
|
|
483
569
|
)}
|
|
484
570
|
|
|
485
|
-
{(navItem.type ===
|
|
486
|
-
navItem.type ===
|
|
487
|
-
navItem.type ===
|
|
571
|
+
{(navItem.type === "dropdown-single-desc" ||
|
|
572
|
+
navItem.type === "dropdown-single" ||
|
|
573
|
+
navItem.type === "dropdown-icons") && (
|
|
488
574
|
<div className="space-y-1 p-4">
|
|
489
|
-
{(navItem.type ===
|
|
575
|
+
{(navItem.type === "dropdown-single-desc"
|
|
490
576
|
? navItem.v4_children
|
|
491
|
-
: navItem.type ===
|
|
577
|
+
: navItem.type === "dropdown-single"
|
|
492
578
|
? navItem.v5_children
|
|
493
579
|
: navItem.v6_children
|
|
494
|
-
)?.map(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
580
|
+
)?.map(
|
|
581
|
+
(
|
|
582
|
+
child: HeaderChildItem,
|
|
583
|
+
childIndex: number,
|
|
584
|
+
) => (
|
|
585
|
+
<Link
|
|
586
|
+
key={childIndex}
|
|
587
|
+
href={normalizeProductRoute(
|
|
588
|
+
getLinkHref(child.link),
|
|
589
|
+
child.link?.label,
|
|
590
|
+
)}
|
|
591
|
+
onClick={() => setIsMenuOpen(false)}
|
|
592
|
+
className="block rounded-md px-3 py-2 transition-colors hover:bg-stone-800/30"
|
|
593
|
+
>
|
|
594
|
+
<div className="flex items-center gap-2">
|
|
595
|
+
{renderIcon(child.link?.icon, "size-4")}
|
|
596
|
+
|
|
597
|
+
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
598
|
+
{child.link?.label}
|
|
599
|
+
</span>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
{"description" in child &&
|
|
603
|
+
child.description && (
|
|
604
|
+
<p className="mt-1 text-xs text-stone-300 dark:text-stone-300 text-ellipsis [-webkit-line-clamp:2] [display:-webkit-box] [-webkit-box-orient:vertical] overflow-hidden">
|
|
605
|
+
{String(child.description)}
|
|
606
|
+
</p>
|
|
607
|
+
)}
|
|
608
|
+
</Link>
|
|
609
|
+
),
|
|
610
|
+
)}
|
|
516
611
|
</div>
|
|
517
612
|
)}
|
|
518
613
|
</motion.div>
|
|
519
614
|
)}
|
|
520
615
|
</AnimatePresence>
|
|
521
616
|
</div>
|
|
522
|
-
)
|
|
617
|
+
);
|
|
523
618
|
})}
|
|
524
619
|
</div>
|
|
525
620
|
</motion.div>
|
|
@@ -541,5 +636,5 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
|
541
636
|
}
|
|
542
637
|
`}</style>
|
|
543
638
|
</>
|
|
544
|
-
)
|
|
639
|
+
);
|
|
545
640
|
}
|