blocks-dusted 0.1.1 → 0.1.3
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/README.md +967 -3
- package/package.json +2 -2
- package/src/commands/add.js +143 -45
- package/src/commands/list.js +2 -2
- package/src/installers/checkRequirements.js +1 -0
- package/src/installers/copyTemplateFiles.js +23 -2
- package/src/installers/patchPayloadConfigCollections.js +92 -0
- package/src/installers/writeInstalledBlockReadme.js +17 -4
- package/src/registry/listTemplates.js +16 -7
- package/src/registry/loadTemplateManifest.js +20 -11
- package/src/registry/validateTemplateManifest.js +6 -5
- package/src/utils/paths.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -573
- package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
- package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
- package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
- package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
- package/templates/blocks/DD-Contact/Component.tsx +434 -439
- package/templates/blocks/DD-Contact/config.ts +8 -3
- package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
- package/templates/blocks/DD-FeatCat/config.ts +201 -204
- package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
- package/templates/blocks/DD-Hero/Component.tsx +297 -317
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
- package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
- package/templates/blocks/DD-Pricing/Component.tsx +372 -380
- package/templates/blocks/DD-Process/Component.tsx +149 -155
- package/templates/blocks/DD-Section/Component.tsx +266 -327
- package/templates/blocks/DD-Services/Component.tsx +176 -188
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
- package/templates/blocks/DD-StackCards/Component.tsx +137 -160
- package/templates/blocks/DD-Team/Component.tsx +247 -265
- package/templates/blocks/DD-TechStack/Component.tsx +57 -72
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
- package/templates/blocks/DD-Testimonials/config.ts +66 -66
- package/templates/blocks/DD-Work/Component.tsx +315 -328
- package/templates/blocks/DD-Work-B/Component.tsx +294 -320
- package/templates/collections/Testimonials/README.md +11 -0
- package/templates/collections/Testimonials/Testimonials.ts +161 -0
- package/templates/collections/Testimonials/manifest.json +55 -0
- package/templates/components/HeaderDD02/MobileBottomNav/index.tsx +545 -0
- package/templates/components/HeaderDD02/README.md +45 -0
- package/templates/components/HeaderDD02/RowLabel.tsx +33 -0
- package/templates/components/HeaderDD02/config.ts +302 -0
- package/templates/components/HeaderDD02/index.tsx +517 -0
- package/templates/components/HeaderDD02/manifest.json +123 -0
- package/templates/components/RichText/README.md +13 -0
- package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
- package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
- package/templates/components/RichText/converter/index.tsx +18 -0
- package/templates/components/RichText/converter/internalLinks.tsx +45 -0
- package/templates/components/RichText/converter/textConverter.tsx +27 -0
- package/templates/components/RichText/index.tsx +35 -0
- package/templates/components/RichText/manifest.json +80 -0
|
@@ -0,0 +1,545 @@
|
|
|
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
|
+
import {
|
|
13
|
+
TooltipProvider,
|
|
14
|
+
Tooltip,
|
|
15
|
+
TooltipTrigger,
|
|
16
|
+
TooltipContent
|
|
17
|
+
} from '@/components/animate-ui/components/animate/tooltip'
|
|
18
|
+
|
|
19
|
+
interface MobileBottomNavProps {
|
|
20
|
+
data?: HeaderType
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getLinkHref = (link: any): string => {
|
|
24
|
+
if (!link) return '#'
|
|
25
|
+
|
|
26
|
+
if (link.type === 'reference') {
|
|
27
|
+
const value = link.reference?.value
|
|
28
|
+
|
|
29
|
+
if (typeof value === 'object' && 'slug' in value) {
|
|
30
|
+
return `/${value.slug}`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return link.url || '#'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resolveCustomIconUrl = (customIcon?: any): string | null => {
|
|
38
|
+
if (!customIcon) return null
|
|
39
|
+
if (typeof customIcon === 'string') return customIcon
|
|
40
|
+
if (typeof customIcon === 'number') return null
|
|
41
|
+
|
|
42
|
+
if (typeof customIcon === 'object' && 'url' in customIcon && typeof customIcon.url === 'string') {
|
|
43
|
+
return customIcon.url
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalizePath = (value?: string | null) => {
|
|
50
|
+
if (!value) return '/'
|
|
51
|
+
|
|
52
|
+
const [base] = value.split(/[?#]/)
|
|
53
|
+
|
|
54
|
+
if (!base) return '/'
|
|
55
|
+
|
|
56
|
+
if (base !== '/' && base.endsWith('/')) {
|
|
57
|
+
return base.slice(0, -1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return base || '/'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalizeProductRoute = (href?: string | null, label?: string | null) => {
|
|
64
|
+
const normalizedHref = normalizePath(href)
|
|
65
|
+
const normalizedLabel = (label || '').toLowerCase().trim()
|
|
66
|
+
|
|
67
|
+
if (normalizedHref === '/templates' || normalizedLabel === 'templates') {
|
|
68
|
+
return '/products/templates'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (normalizedHref === '/components' || normalizedLabel === 'components') {
|
|
72
|
+
return '/products/components'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return normalizedHref
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isPathActive = (href: string, currentPathname: string, label?: string | null) => {
|
|
79
|
+
const normalizedHref = normalizeProductRoute(href, label)
|
|
80
|
+
const normalizedCurrentPathname = normalizePath(currentPathname)
|
|
81
|
+
|
|
82
|
+
if (normalizedHref === normalizedCurrentPathname) return true
|
|
83
|
+
if (normalizedHref === '/') return false
|
|
84
|
+
|
|
85
|
+
return normalizedCurrentPathname.startsWith(`${normalizedHref}/`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const renderIcon = (iconName?: string | null, className?: string, customIcon?: any) => {
|
|
89
|
+
const customUrl = resolveCustomIconUrl(customIcon)
|
|
90
|
+
|
|
91
|
+
if (customUrl) {
|
|
92
|
+
return (
|
|
93
|
+
<img
|
|
94
|
+
src={customUrl}
|
|
95
|
+
className={className || 'size-5'}
|
|
96
|
+
alt=""
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
loading="lazy"
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!iconName) return null
|
|
104
|
+
|
|
105
|
+
const IconComponent = (LucideIcons as any)[iconName]
|
|
106
|
+
|
|
107
|
+
if (!IconComponent) return null
|
|
108
|
+
|
|
109
|
+
return <IconComponent className={className || 'size-5'} />
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const menuButtonVariants = {
|
|
113
|
+
top: {
|
|
114
|
+
closed: { rotate: 0, translateY: 0 },
|
|
115
|
+
open: { rotate: 45, translateY: 6 }
|
|
116
|
+
},
|
|
117
|
+
middle: {
|
|
118
|
+
closed: { opacity: 1 },
|
|
119
|
+
open: { opacity: 0 }
|
|
120
|
+
},
|
|
121
|
+
bottom: {
|
|
122
|
+
closed: { rotate: 0, translateY: 0 },
|
|
123
|
+
open: { rotate: -45, translateY: -6 }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const barClass = 'menubar absolute left-0 h-[1.8px] w-full bg-current'
|
|
128
|
+
|
|
129
|
+
export default function MobileBottomNav({ data }: MobileBottomNavProps) {
|
|
130
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
|
131
|
+
const [expandedDropdown, setExpandedDropdown] = useState<number | null>(null)
|
|
132
|
+
|
|
133
|
+
const pathname = usePathname()
|
|
134
|
+
const normalizedCurrentPathname = useMemo(() => normalizePath(pathname), [pathname])
|
|
135
|
+
|
|
136
|
+
const navItems = data?.header02?.navItems || []
|
|
137
|
+
const showLabels = data?.header02?.showMobileLabels ?? true
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (isMenuOpen) {
|
|
141
|
+
const scrollY = window.scrollY
|
|
142
|
+
|
|
143
|
+
document.documentElement.style.overflow = 'hidden'
|
|
144
|
+
document.documentElement.style.touchAction = 'none'
|
|
145
|
+
document.documentElement.style.overscrollBehavior = 'none'
|
|
146
|
+
|
|
147
|
+
document.body.style.overflow = 'hidden'
|
|
148
|
+
document.body.style.position = 'fixed'
|
|
149
|
+
document.body.style.top = `-${scrollY}px`
|
|
150
|
+
document.body.style.width = '100%'
|
|
151
|
+
document.body.style.touchAction = 'none'
|
|
152
|
+
document.body.style.overscrollBehavior = 'none'
|
|
153
|
+
} else {
|
|
154
|
+
const scrollY = document.body.style.top
|
|
155
|
+
|
|
156
|
+
document.documentElement.style.overflow = ''
|
|
157
|
+
document.documentElement.style.touchAction = ''
|
|
158
|
+
document.documentElement.style.overscrollBehavior = ''
|
|
159
|
+
|
|
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 = ''
|
|
166
|
+
|
|
167
|
+
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
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])
|
|
183
|
+
|
|
184
|
+
// Include both simple links and dropdowns (using first child's link)
|
|
185
|
+
const bottomNavLinks = navItems.slice(0, 4)
|
|
186
|
+
|
|
187
|
+
const toggleDropdown = (index: number) => {
|
|
188
|
+
setExpandedDropdown(expandedDropdown === index ? null : index)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Helper to get URL for bottom nav items (handles both links and dropdowns)
|
|
192
|
+
const getBottomNavHref = (navItem: any) => {
|
|
193
|
+
const label = getBottomNavLabel(navItem)
|
|
194
|
+
|
|
195
|
+
if (navItem.type === 'link') {
|
|
196
|
+
return normalizeProductRoute(getLinkHref(navItem.link), label)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const children =
|
|
200
|
+
navItem.v1_children ||
|
|
201
|
+
navItem.v2_children ||
|
|
202
|
+
navItem.v4_children ||
|
|
203
|
+
navItem.v5_children ||
|
|
204
|
+
navItem.v6_children
|
|
205
|
+
|
|
206
|
+
return normalizeProductRoute(children?.[0]?.link ? getLinkHref(children[0].link) : '#', label)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Helper to get label for bottom nav items
|
|
210
|
+
const getBottomNavLabel = (navItem: any) => {
|
|
211
|
+
return navItem.type === 'link' ? navItem.link?.label : navItem.label
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Helper to get icon for bottom nav items
|
|
215
|
+
const getBottomNavIcon = (navItem: any) => {
|
|
216
|
+
if (navItem.type === 'link') {
|
|
217
|
+
return renderIcon(navItem.link?.icon, 'size-6', navItem.link?.customIcon)
|
|
218
|
+
}
|
|
219
|
+
return renderIcon(navItem.triggerIcon, 'size-6')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<>
|
|
224
|
+
<nav
|
|
225
|
+
className={cn(
|
|
226
|
+
'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',
|
|
227
|
+
'bg-[var(--navbar-background)] shadow-[0_-2px_10px_rgba(0,0,0,0.1)]',
|
|
228
|
+
'pb-[env(safe-area-inset-bottom)] [filter:drop-shadow(2px_4px_6px_black)]',
|
|
229
|
+
'animate-[slideUpNav_0.3s_ease-out]',
|
|
230
|
+
'min-[978px]:hidden'
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
<TooltipProvider>
|
|
234
|
+
<div className="relative flex w-full items-center justify-around px-2 py-0">
|
|
235
|
+
<Tooltip>
|
|
236
|
+
<TooltipTrigger asChild>
|
|
237
|
+
<motion.button
|
|
238
|
+
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
|
|
239
|
+
aria-expanded={isMenuOpen}
|
|
240
|
+
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
241
|
+
whileTap={{ scale: 0.96 }}
|
|
242
|
+
className="menu-btn flex h-8 flex-col items-center justify-center gap-1 bg-transparent px-3 pt-1 text-stone-300 dark:text-stone-300 transition-all duration-200 hover:bg-black/10 hover:text-foreground hover:duration-200 [&_svg]:scale-100 [&_svg]:transition-all [&_svg]:duration-200 hover:[&_svg]:scale-105"
|
|
243
|
+
>
|
|
244
|
+
<span className="relative block h-4 w-4">
|
|
245
|
+
<motion.span
|
|
246
|
+
className={barClass}
|
|
247
|
+
style={{ top: '0px' }}
|
|
248
|
+
animate={isMenuOpen ? 'open' : 'closed'}
|
|
249
|
+
variants={menuButtonVariants.top}
|
|
250
|
+
transition={{ type: 'spring', stiffness: 600, damping: 30 }}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
<motion.span
|
|
254
|
+
className={barClass}
|
|
255
|
+
style={{ top: '6px' }}
|
|
256
|
+
animate={isMenuOpen ? 'open' : 'closed'}
|
|
257
|
+
variants={menuButtonVariants.middle}
|
|
258
|
+
transition={{ duration: 0.18, ease: [0.22, 1, 0.36, 1] }}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<motion.span
|
|
262
|
+
className="menubar absolute left-0 h-[1.8px] w-full bg-lime-500"
|
|
263
|
+
style={{ top: '12px' }}
|
|
264
|
+
animate={isMenuOpen ? 'open' : 'closed'}
|
|
265
|
+
variants={menuButtonVariants.bottom}
|
|
266
|
+
transition={{ type: 'spring', stiffness: 600, damping: 30 }}
|
|
267
|
+
/>
|
|
268
|
+
</span>
|
|
269
|
+
|
|
270
|
+
{showLabels && <span className="text-xs font-medium">Menu</span>}
|
|
271
|
+
</motion.button>
|
|
272
|
+
</TooltipTrigger>
|
|
273
|
+
|
|
274
|
+
<TooltipContent className="bg-white text-black">
|
|
275
|
+
{isMenuOpen ? 'Close menu' : 'Menu'}
|
|
276
|
+
</TooltipContent>
|
|
277
|
+
</Tooltip>
|
|
278
|
+
|
|
279
|
+
{bottomNavLinks.map((navItem, index) => {
|
|
280
|
+
const label = getBottomNavLabel(navItem)
|
|
281
|
+
const href = getBottomNavHref(navItem)
|
|
282
|
+
const isActive = isPathActive(href, normalizedCurrentPathname, label)
|
|
283
|
+
const icon = getBottomNavIcon(navItem)
|
|
284
|
+
const showLabelForItem =
|
|
285
|
+
navItem.type === 'link' ? showLabels && (navItem.showLabel ?? true) : showLabels
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<Tooltip key={index}>
|
|
289
|
+
<TooltipTrigger asChild>
|
|
290
|
+
<Link
|
|
291
|
+
href={href}
|
|
292
|
+
className={cn(
|
|
293
|
+
'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
|
+
isActive &&
|
|
295
|
+
'nav-button-active border-b-[3px] border-lime-500 font-semibold text-foreground transition-all duration-300'
|
|
296
|
+
)}
|
|
297
|
+
{...(navItem.type === 'link' && navItem.link?.newTab
|
|
298
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
299
|
+
: {})}
|
|
300
|
+
aria-current={isActive ? 'page' : undefined}
|
|
301
|
+
aria-label={label || undefined}
|
|
302
|
+
onClick={() => setIsMenuOpen(false)}
|
|
303
|
+
>
|
|
304
|
+
{icon || <Home className="size-6" />}
|
|
305
|
+
|
|
306
|
+
{showLabelForItem && (
|
|
307
|
+
<span className="max-w-[60px] truncate text-xs font-medium">{label}</span>
|
|
308
|
+
)}
|
|
309
|
+
</Link>
|
|
310
|
+
</TooltipTrigger>
|
|
311
|
+
|
|
312
|
+
<TooltipContent className="bg-white text-black">{label}</TooltipContent>
|
|
313
|
+
</Tooltip>
|
|
314
|
+
)
|
|
315
|
+
})}
|
|
316
|
+
</div>
|
|
317
|
+
</TooltipProvider>
|
|
318
|
+
</nav>
|
|
319
|
+
|
|
320
|
+
{/* <div className="fixed bottom-[0.3rem] left-0 right-0 mx-auto h-16 w-[calc(100dvw-1.5rem)] rounded-[10rem] bg-[#f0f8ff30] backdrop-blur-[10px] min-[978px]:hidden" /> */}
|
|
321
|
+
|
|
322
|
+
<AnimatePresence>
|
|
323
|
+
{isMenuOpen && (
|
|
324
|
+
<motion.div
|
|
325
|
+
initial={{ opacity: 0 }}
|
|
326
|
+
animate={{ opacity: 1 }}
|
|
327
|
+
exit={{ opacity: 0 }}
|
|
328
|
+
transition={{ duration: 0.2 }}
|
|
329
|
+
className="fixed inset-0 z-[100] bg-background/95 backdrop-blur-md"
|
|
330
|
+
onClick={() => setIsMenuOpen(false)}
|
|
331
|
+
>
|
|
332
|
+
<motion.div
|
|
333
|
+
initial={{ y: '100%' }}
|
|
334
|
+
animate={{ y: 0 }}
|
|
335
|
+
exit={{ y: '100%' }}
|
|
336
|
+
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
337
|
+
className="absolute inset-0 flex flex-col bg-background"
|
|
338
|
+
onClick={(e) => e.stopPropagation()}
|
|
339
|
+
>
|
|
340
|
+
<div className="MobileBottomNav-drawer sticky top-0 z-10 flex flex-shrink-0 items-center justify-between border-b border-border bg-background/95 px-4 py-4 pt-16 backdrop-blur-md">
|
|
341
|
+
<h2 className="text-lg font-semibold">Menu</h2>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div
|
|
345
|
+
className="scrollContainer min-h-0 flex-1 space-y-2 overflow-y-auto px-4 py-6 pb-20"
|
|
346
|
+
data-lenis-prevent
|
|
347
|
+
>
|
|
348
|
+
{navItems.map((navItem, index) => {
|
|
349
|
+
if (navItem.type === 'link') {
|
|
350
|
+
const label = getBottomNavLabel(navItem)
|
|
351
|
+
const href = normalizeProductRoute(getLinkHref(navItem.link), label)
|
|
352
|
+
const isActive = isPathActive(href, normalizedCurrentPathname, label)
|
|
353
|
+
const icon = renderIcon(navItem.link?.icon, 'size-5', navItem.link?.customIcon)
|
|
354
|
+
const iconPosition = navItem.link?.iconPosition || 'before'
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<Link
|
|
358
|
+
key={index}
|
|
359
|
+
href={href}
|
|
360
|
+
aria-current={isActive ? 'page' : undefined}
|
|
361
|
+
onClick={() => setIsMenuOpen(false)}
|
|
362
|
+
className={cn(
|
|
363
|
+
'flex items-center gap-3 rounded-lg px-4 py-3 transition-colors hover:bg-stone-800/30',
|
|
364
|
+
isActive && 'bg-accent font-semibold text-accent-foreground'
|
|
365
|
+
)}
|
|
366
|
+
{...(navItem.link?.newTab
|
|
367
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
368
|
+
: {})}
|
|
369
|
+
>
|
|
370
|
+
{icon && iconPosition === 'before' && icon}
|
|
371
|
+
<span className="text-base font-medium">{navItem.link?.label}</span>
|
|
372
|
+
{icon && iconPosition === 'after' && icon}
|
|
373
|
+
</Link>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const triggerIcon = renderIcon(navItem.triggerIcon, 'size-5')
|
|
378
|
+
const isExpanded = expandedDropdown === index
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div key={index} className="overflow-hidden rounded-lg border border-border">
|
|
382
|
+
<button
|
|
383
|
+
aria-expanded={isExpanded}
|
|
384
|
+
onClick={() => toggleDropdown(index)}
|
|
385
|
+
className="flex w-full items-center justify-between px-4 py-3 transition-colors hover:bg-stone-800/30"
|
|
386
|
+
>
|
|
387
|
+
<div className="flex items-center gap-3">
|
|
388
|
+
{triggerIcon}
|
|
389
|
+
<span className="text-base font-medium">{navItem.label}</span>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<ChevronDown
|
|
393
|
+
className={cn(
|
|
394
|
+
'size-5 transition-transform duration-200',
|
|
395
|
+
isExpanded && 'rotate-180'
|
|
396
|
+
)}
|
|
397
|
+
/>
|
|
398
|
+
</button>
|
|
399
|
+
|
|
400
|
+
<AnimatePresence>
|
|
401
|
+
{isExpanded && (
|
|
402
|
+
<motion.div
|
|
403
|
+
initial={{ height: 0, opacity: 0 }}
|
|
404
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
405
|
+
exit={{ height: 0, opacity: 0 }}
|
|
406
|
+
transition={{ duration: 0.2 }}
|
|
407
|
+
className="overflow-hidden bg-muted/50"
|
|
408
|
+
>
|
|
409
|
+
{navItem.type === 'dropdown-richtext' && (
|
|
410
|
+
<div className="space-y-4 p-4">
|
|
411
|
+
{navItem.richTextContent && (
|
|
412
|
+
<div className="rounded-md bg-gradient-to-b from-muted/50 to-muted px-3 py-2">
|
|
413
|
+
<RichText
|
|
414
|
+
data={navItem.richTextContent}
|
|
415
|
+
enableGutter={false}
|
|
416
|
+
enableProse={false}
|
|
417
|
+
className="text-sm"
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
<div className="space-y-1 grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-8 ">
|
|
423
|
+
{navItem.v1_children?.map((child, childIndex) => {
|
|
424
|
+
const childLabel = child.link?.label
|
|
425
|
+
const childHref = normalizeProductRoute(
|
|
426
|
+
getLinkHref(child.link),
|
|
427
|
+
childLabel
|
|
428
|
+
)
|
|
429
|
+
const isChildActive = isPathActive(
|
|
430
|
+
childHref,
|
|
431
|
+
normalizedCurrentPathname,
|
|
432
|
+
childLabel
|
|
433
|
+
)
|
|
434
|
+
const childIcon = renderIcon(
|
|
435
|
+
child.link?.icon,
|
|
436
|
+
'size-5',
|
|
437
|
+
child.link?.customIcon
|
|
438
|
+
)
|
|
439
|
+
const childIconPosition = child.link?.iconPosition || 'before'
|
|
440
|
+
const showChildLabel =
|
|
441
|
+
showLabels &&
|
|
442
|
+
(('showLabel' in child ? child.showLabel : undefined) ?? true)
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<Link
|
|
446
|
+
key={childIndex}
|
|
447
|
+
href={childHref}
|
|
448
|
+
aria-current={isChildActive ? 'page' : undefined}
|
|
449
|
+
onClick={() => setIsMenuOpen(false)}
|
|
450
|
+
className={cn(
|
|
451
|
+
'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',
|
|
452
|
+
isChildActive &&
|
|
453
|
+
'bg-accent font-semibold text-stone-200 dark:text-stone-200'
|
|
454
|
+
)}
|
|
455
|
+
>
|
|
456
|
+
<div className="flex items-center gap-3">
|
|
457
|
+
{childIcon && childIconPosition === 'before' && childIcon}
|
|
458
|
+
|
|
459
|
+
{/* show Label inside menu */}
|
|
460
|
+
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
461
|
+
{child.link?.label}
|
|
462
|
+
</span>
|
|
463
|
+
|
|
464
|
+
{/* {showChildLabel && (
|
|
465
|
+
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
466
|
+
{child.link?.label}
|
|
467
|
+
</span>
|
|
468
|
+
)} */}
|
|
469
|
+
|
|
470
|
+
{childIcon && childIconPosition === 'after' && childIcon}
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
{'description' in child && child.description && (
|
|
474
|
+
<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">
|
|
475
|
+
{child.description}
|
|
476
|
+
</p>
|
|
477
|
+
)}
|
|
478
|
+
</Link>
|
|
479
|
+
)
|
|
480
|
+
})}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
{(navItem.type === 'dropdown-single-desc' ||
|
|
486
|
+
navItem.type === 'dropdown-single' ||
|
|
487
|
+
navItem.type === 'dropdown-icons') && (
|
|
488
|
+
<div className="space-y-1 p-4">
|
|
489
|
+
{(navItem.type === 'dropdown-single-desc'
|
|
490
|
+
? navItem.v4_children
|
|
491
|
+
: navItem.type === 'dropdown-single'
|
|
492
|
+
? navItem.v5_children
|
|
493
|
+
: navItem.v6_children
|
|
494
|
+
)?.map((child, childIndex) => (
|
|
495
|
+
<Link
|
|
496
|
+
key={childIndex}
|
|
497
|
+
href={normalizeProductRoute(getLinkHref(child.link), child.link?.label)}
|
|
498
|
+
onClick={() => setIsMenuOpen(false)}
|
|
499
|
+
className="block rounded-md px-3 py-2 transition-colors hover:bg-stone-800/30"
|
|
500
|
+
>
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
502
|
+
{renderIcon(child.link?.icon, 'size-4')}
|
|
503
|
+
|
|
504
|
+
<span className="text-sm font-medium text-stone-200 dark:text-stone-200">
|
|
505
|
+
{child.link?.label}
|
|
506
|
+
</span>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{'description' in child && child.description && (
|
|
510
|
+
<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">
|
|
511
|
+
{String(child.description)}
|
|
512
|
+
</p>
|
|
513
|
+
)}
|
|
514
|
+
</Link>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
</motion.div>
|
|
519
|
+
)}
|
|
520
|
+
</AnimatePresence>
|
|
521
|
+
</div>
|
|
522
|
+
)
|
|
523
|
+
})}
|
|
524
|
+
</div>
|
|
525
|
+
</motion.div>
|
|
526
|
+
</motion.div>
|
|
527
|
+
)}
|
|
528
|
+
</AnimatePresence>
|
|
529
|
+
|
|
530
|
+
<style jsx global>{`
|
|
531
|
+
@keyframes slideUpNav {
|
|
532
|
+
from {
|
|
533
|
+
transform: translateY(100%);
|
|
534
|
+
opacity: 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
to {
|
|
538
|
+
transform: translateY(0);
|
|
539
|
+
opacity: 1;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
`}</style>
|
|
543
|
+
</>
|
|
544
|
+
)
|
|
545
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# HeaderDD02
|
|
2
|
+
|
|
3
|
+
HeaderDD02 is the exported Header02 implementation from DesignsDustedv3.
|
|
4
|
+
|
|
5
|
+
This header uses the shared `defaultLexical` editor standard from `@/fields/defaultLexical`. Project-specific Lexical blocks are intentionally not bundled. The nested `BlocksFeature` uses an empty `blocks` array so each target project can register only the blocks it requires.
|
|
6
|
+
|
|
7
|
+
Included files:
|
|
8
|
+
|
|
9
|
+
- `index.tsx` for the HeaderDD02 desktop runtime.
|
|
10
|
+
- `MobileBottomNav/index.tsx` because Header02 imports it directly.
|
|
11
|
+
- `config.ts` for the Header02 field structure adapted to `HeaderDD02` and `defaultLexical`.
|
|
12
|
+
- `RowLabel.tsx` because the Header02 config references `/Header/RowLabel.tsx#RowLabel` for Payload admin array labels.
|
|
13
|
+
|
|
14
|
+
Not included:
|
|
15
|
+
|
|
16
|
+
- Header01 and Header03 implementations.
|
|
17
|
+
- The parent multi-variant Header shell.
|
|
18
|
+
- Source project Lexical blocks: `AnimatedText`, `ButtonWithIcon`, `CounterBlock`, and `MediaBlock`.
|
|
19
|
+
|
|
20
|
+
Before activating this header in an existing project, inspect the current Header global schema and stored Header data. The template installer copies files and backs up the `HeaderDD02` destination folder when it already exists, but it does not automatically replace the active Header global or inspect database content.
|
|
21
|
+
|
|
22
|
+
## AI handover prompt
|
|
23
|
+
|
|
24
|
+
```prompt
|
|
25
|
+
Review the newly installed HeaderDD02 files and integrate them into this project
|
|
26
|
+
|
|
27
|
+
Do not assume the source project architecture matches this repository.
|
|
28
|
+
|
|
29
|
+
Tasks:
|
|
30
|
+
|
|
31
|
+
1. Inspect the current Header component, Payload Header global, frontend layout, fields, utilities, generated types and styling conventions.
|
|
32
|
+
2. Resolve HeaderDD02 imports against the current project.
|
|
33
|
+
3. Reuse existing project utilities and fields where appropriate.
|
|
34
|
+
4. Adapt HeaderDD02 config to the current Header global without deleting or destructively renaming existing database-backed fields.
|
|
35
|
+
5. Keep the shared defaultLexical standard.
|
|
36
|
+
6. Keep BlocksFeature({ blocks: [] }).
|
|
37
|
+
7. Integrate the component into the existing header render flow.
|
|
38
|
+
8. Preserve the existing layout, providers, transitions and mobile behaviour.
|
|
39
|
+
9. Add any required CSS variables or utility classes using the current project's styling system.
|
|
40
|
+
10. Report every file modified and any manual content migration required.
|
|
41
|
+
|
|
42
|
+
Do not modify database records automatically.
|
|
43
|
+
Do not run destructive migrations.
|
|
44
|
+
Do not remove the existing header until the replacement is verified.
|
|
45
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { RowLabelProps, useRowLabel } from '@payloadcms/ui'
|
|
3
|
+
|
|
4
|
+
export const RowLabel: React.FC<RowLabelProps> = () => {
|
|
5
|
+
const data = useRowLabel<any>()
|
|
6
|
+
|
|
7
|
+
// Handle different data structures:
|
|
8
|
+
// 2. Nested link in Header02: data.link.label, data.link.url
|
|
9
|
+
// Type-based items in HeaderDD02 use data.label for dropdowns.
|
|
10
|
+
|
|
11
|
+
const getLinkLabel = () => {
|
|
12
|
+
// Check for dropdown type items (Header02 dropdowns)
|
|
13
|
+
if (data?.data?.type && data?.data?.type !== 'link') {
|
|
14
|
+
return data?.data?.label || 'Dropdown'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check for nested link structure (Header02 simple links)
|
|
18
|
+
if (data?.data?.link) {
|
|
19
|
+
return data.data.link.label || data.data.link.url
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback for direct link fields in customised target headers.
|
|
23
|
+
return data?.data?.label || data?.data?.url
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const linkLabel = getLinkLabel()
|
|
27
|
+
|
|
28
|
+
const label = linkLabel
|
|
29
|
+
? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${linkLabel}`
|
|
30
|
+
: `Row ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}`
|
|
31
|
+
|
|
32
|
+
return <div>{label}</div>
|
|
33
|
+
}
|