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.
Files changed (56) hide show
  1. package/README.md +967 -3
  2. package/package.json +2 -2
  3. package/src/commands/add.js +143 -45
  4. package/src/commands/list.js +2 -2
  5. package/src/installers/checkRequirements.js +1 -0
  6. package/src/installers/copyTemplateFiles.js +23 -2
  7. package/src/installers/patchPayloadConfigCollections.js +92 -0
  8. package/src/installers/writeInstalledBlockReadme.js +17 -4
  9. package/src/registry/listTemplates.js +16 -7
  10. package/src/registry/loadTemplateManifest.js +20 -11
  11. package/src/registry/validateTemplateManifest.js +6 -5
  12. package/src/utils/paths.js +7 -0
  13. package/templates/blocks/DD-BookCall/Component.tsx +535 -573
  14. package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
  15. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
  16. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
  17. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
  18. package/templates/blocks/DD-Contact/Component.tsx +434 -439
  19. package/templates/blocks/DD-Contact/config.ts +8 -3
  20. package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
  21. package/templates/blocks/DD-FeatCat/config.ts +201 -204
  22. package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
  23. package/templates/blocks/DD-Hero/Component.tsx +297 -317
  24. package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
  25. package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
  26. package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
  27. package/templates/blocks/DD-Pricing/Component.tsx +372 -380
  28. package/templates/blocks/DD-Process/Component.tsx +149 -155
  29. package/templates/blocks/DD-Section/Component.tsx +266 -327
  30. package/templates/blocks/DD-Services/Component.tsx +176 -188
  31. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
  32. package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
  33. package/templates/blocks/DD-StackCards/Component.tsx +137 -160
  34. package/templates/blocks/DD-Team/Component.tsx +247 -265
  35. package/templates/blocks/DD-TechStack/Component.tsx +57 -72
  36. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  37. package/templates/blocks/DD-Testimonials/config.ts +66 -66
  38. package/templates/blocks/DD-Work/Component.tsx +315 -328
  39. package/templates/blocks/DD-Work-B/Component.tsx +294 -320
  40. package/templates/collections/Testimonials/README.md +11 -0
  41. package/templates/collections/Testimonials/Testimonials.ts +161 -0
  42. package/templates/collections/Testimonials/manifest.json +55 -0
  43. package/templates/components/HeaderDD02/MobileBottomNav/index.tsx +545 -0
  44. package/templates/components/HeaderDD02/README.md +45 -0
  45. package/templates/components/HeaderDD02/RowLabel.tsx +33 -0
  46. package/templates/components/HeaderDD02/config.ts +302 -0
  47. package/templates/components/HeaderDD02/index.tsx +517 -0
  48. package/templates/components/HeaderDD02/manifest.json +123 -0
  49. package/templates/components/RichText/README.md +13 -0
  50. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  51. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  52. package/templates/components/RichText/converter/index.tsx +18 -0
  53. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  54. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  55. package/templates/components/RichText/index.tsx +35 -0
  56. 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
+ }