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,517 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
4
|
+
import React, { useEffect, useMemo, useState, useRef } from 'react'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import {
|
|
7
|
+
NavigationMenu,
|
|
8
|
+
NavigationMenuContent,
|
|
9
|
+
NavigationMenuItem,
|
|
10
|
+
NavigationMenuLink,
|
|
11
|
+
NavigationMenuList,
|
|
12
|
+
NavigationMenuTrigger
|
|
13
|
+
} from '@/components/ui/navigation-menu'
|
|
14
|
+
import { CircleCheck, CircleHelp, Circle, Home, User, Settings, Mail, Phone } from 'lucide-react'
|
|
15
|
+
import type { Header as HeaderType } from '@/payload-types'
|
|
16
|
+
import RichText from '@/components/RichText'
|
|
17
|
+
import { cn } from '@/utilities/ui'
|
|
18
|
+
|
|
19
|
+
import * as LucideIcons from 'lucide-react'
|
|
20
|
+
import { motion } from 'framer-motion'
|
|
21
|
+
|
|
22
|
+
import MobileBottomNav from './MobileBottomNav'
|
|
23
|
+
|
|
24
|
+
interface HeaderDD02Props {
|
|
25
|
+
data?: HeaderType
|
|
26
|
+
variant?: 'default' | 'centered' | 'fullwidth'
|
|
27
|
+
sticky?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const iconMap = {
|
|
31
|
+
CircleHelp,
|
|
32
|
+
Circle,
|
|
33
|
+
CircleCheck,
|
|
34
|
+
Home,
|
|
35
|
+
User,
|
|
36
|
+
Settings,
|
|
37
|
+
Mail,
|
|
38
|
+
Phone
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type CartVisibilityMode = 'always' | 'hide' | 'show'
|
|
42
|
+
|
|
43
|
+
type CartVisibilityEntry = {
|
|
44
|
+
path?: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type CartVisibilityConfig = {
|
|
48
|
+
mode?: CartVisibilityMode
|
|
49
|
+
paths?: CartVisibilityEntry[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type HeaderDD02Data = HeaderType['header02'] & {
|
|
53
|
+
cartButtonVisibility?: CartVisibilityConfig
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalizePath = (value?: string | null) => {
|
|
57
|
+
if (!value) return '/'
|
|
58
|
+
const [base] = value.split(/[?#]/)
|
|
59
|
+
if (!base) return '/'
|
|
60
|
+
if (base !== '/' && base.endsWith('/')) {
|
|
61
|
+
return base.slice(0, -1)
|
|
62
|
+
}
|
|
63
|
+
return base || '/'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Helper to extract href from link field
|
|
67
|
+
const getLinkHref = (link: any): string => {
|
|
68
|
+
if (!link) return '#'
|
|
69
|
+
if (link.type === 'reference') {
|
|
70
|
+
const value = link.reference?.value
|
|
71
|
+
if (typeof value === 'object' && 'slug' in value) {
|
|
72
|
+
return `/${value.slug}`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return link.url || '#'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolveCustomIconUrl = (customIcon?: any): string | null => {
|
|
79
|
+
if (!customIcon) return null
|
|
80
|
+
if (typeof customIcon === 'string') return customIcon
|
|
81
|
+
if (typeof customIcon === 'number') return null
|
|
82
|
+
if (typeof customIcon === 'object' && 'url' in customIcon && typeof customIcon.url === 'string') {
|
|
83
|
+
return customIcon.url
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Helper to render icon from link field or fallback
|
|
89
|
+
const renderIcon = (iconName?: string | null, className?: string, customIcon?: any) => {
|
|
90
|
+
const customUrl = resolveCustomIconUrl(customIcon)
|
|
91
|
+
if (customUrl) {
|
|
92
|
+
return (
|
|
93
|
+
<img
|
|
94
|
+
src={customUrl}
|
|
95
|
+
className={className || 'size-4'}
|
|
96
|
+
alt=""
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
loading="lazy"
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!iconName) return null
|
|
104
|
+
const IconComponent = (LucideIcons as any)[iconName]
|
|
105
|
+
if (!IconComponent) return null
|
|
106
|
+
return <IconComponent className={className || 'size-4'} />
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dropdownStyles = `
|
|
110
|
+
.dropdown-richtext-content {
|
|
111
|
+
max-height: 80vh;
|
|
112
|
+
overflow-y: auto;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:where(.navigation-menu-viewport) {
|
|
116
|
+
position: absolute;
|
|
117
|
+
top: 100%;
|
|
118
|
+
left: 0;
|
|
119
|
+
width: 100%;
|
|
120
|
+
perspective: 2000px;
|
|
121
|
+
transform-origin: top center;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.NavigationMenuContent {
|
|
125
|
+
position: relative;
|
|
126
|
+
top: 0;
|
|
127
|
+
left: 0;
|
|
128
|
+
animation-duration: 250ms;
|
|
129
|
+
max-height: var(--radix-navigation-menu-viewport-height);
|
|
130
|
+
overflow: auto;
|
|
131
|
+
}
|
|
132
|
+
`
|
|
133
|
+
|
|
134
|
+
function ListItem({
|
|
135
|
+
title,
|
|
136
|
+
children,
|
|
137
|
+
href,
|
|
138
|
+
className,
|
|
139
|
+
icon,
|
|
140
|
+
iconPosition = 'before'
|
|
141
|
+
}: {
|
|
142
|
+
title: string
|
|
143
|
+
children?: React.ReactNode
|
|
144
|
+
href: string
|
|
145
|
+
className?: string
|
|
146
|
+
icon?: React.ReactNode
|
|
147
|
+
iconPosition?: 'before' | 'after'
|
|
148
|
+
}) {
|
|
149
|
+
return (
|
|
150
|
+
<li>
|
|
151
|
+
<NavigationMenuLink asChild>
|
|
152
|
+
<Link
|
|
153
|
+
href={href}
|
|
154
|
+
className={cn(
|
|
155
|
+
'block select-none space-y-1 no-underline outline-none transition-colors hover:bg-zinc-800/50 duration-300 rounded-sm hover:text-accent focus:bg-accent focus:text-stone-200 py-2 px-3',
|
|
156
|
+
className
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
<div className="text-sm font-medium leading-none flex items-center gap-2">
|
|
160
|
+
{icon && iconPosition === 'before' && icon}
|
|
161
|
+
{title}
|
|
162
|
+
{icon && iconPosition === 'after' && icon}
|
|
163
|
+
</div>
|
|
164
|
+
{children && (
|
|
165
|
+
<p
|
|
166
|
+
className="line-clamp-2 text-xs leading-snug text-stone-400 mt-0 !pt-1 -translate-y-2"
|
|
167
|
+
style={{ lineHeight: '1.3' }}
|
|
168
|
+
>
|
|
169
|
+
{children}
|
|
170
|
+
</p>
|
|
171
|
+
)}
|
|
172
|
+
</Link>
|
|
173
|
+
</NavigationMenuLink>
|
|
174
|
+
</li>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ListItemWithIcon({
|
|
179
|
+
title,
|
|
180
|
+
href,
|
|
181
|
+
icon,
|
|
182
|
+
className
|
|
183
|
+
}: {
|
|
184
|
+
title: string
|
|
185
|
+
href: string
|
|
186
|
+
icon: string
|
|
187
|
+
className?: string
|
|
188
|
+
}) {
|
|
189
|
+
const IconComponent = iconMap[icon as keyof typeof iconMap]
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<li>
|
|
193
|
+
<NavigationMenuLink asChild>
|
|
194
|
+
<Link
|
|
195
|
+
href={href}
|
|
196
|
+
className={cn(
|
|
197
|
+
'flex items-center gap-2 select-none no-underline outline-none transition-colors hover:bg-accent hover:text-stone-200 focus:bg-accent focus:text-stone-200 py-2 px-3',
|
|
198
|
+
className
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{IconComponent && <IconComponent className="size-4" />}
|
|
202
|
+
<span className="text-sm font-medium leading-none">{title}</span>
|
|
203
|
+
</Link>
|
|
204
|
+
</NavigationMenuLink>
|
|
205
|
+
</li>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default function HeaderDD02({ data, variant = 'default', sticky = true }: HeaderDD02Props) {
|
|
210
|
+
// Get scroll settings from data with defaults
|
|
211
|
+
const enableHideOnScroll = data?.header02?.enableHideOnScroll ?? true
|
|
212
|
+
const scrollThreshold = data?.header02?.scrollThreshold ?? 100
|
|
213
|
+
const animationDuration = data?.header02?.animationDuration ?? 0.3
|
|
214
|
+
const [forceOpen, setForceOpen] = useState(true)
|
|
215
|
+
|
|
216
|
+
// State for handling scroll behavior
|
|
217
|
+
const [isHidden, setIsHidden] = useState(false)
|
|
218
|
+
const lastScrollY = useRef(0)
|
|
219
|
+
const pathname = usePathname()
|
|
220
|
+
const normalizedCurrentPathname = useMemo(() => normalizePath(pathname), [pathname])
|
|
221
|
+
const header02Data = data?.header02 as HeaderDD02Data | undefined
|
|
222
|
+
const cartVisibilityConfig =
|
|
223
|
+
header02Data?.cartButtonVisibility ?? (data as any)?.header02CartButtonVisibility
|
|
224
|
+
const normalizedPathname = useMemo(() => normalizePath(pathname), [pathname])
|
|
225
|
+
const cartVisibilityPaths = useMemo(
|
|
226
|
+
() =>
|
|
227
|
+
(cartVisibilityConfig?.paths ?? [])
|
|
228
|
+
.map((entry) => normalizePath(entry?.path))
|
|
229
|
+
.filter((path): path is string => Boolean(path)),
|
|
230
|
+
[cartVisibilityConfig?.paths]
|
|
231
|
+
)
|
|
232
|
+
const cartVisibilityMode = cartVisibilityConfig?.mode ?? 'always'
|
|
233
|
+
const showCartButton = useMemo(() => {
|
|
234
|
+
if (cartVisibilityMode === 'always') return true
|
|
235
|
+
const matches = cartVisibilityPaths.includes(normalizedPathname)
|
|
236
|
+
return cartVisibilityMode === 'hide' ? !matches : matches
|
|
237
|
+
}, [cartVisibilityMode, cartVisibilityPaths, normalizedPathname])
|
|
238
|
+
|
|
239
|
+
// Effect for scroll hide/show behavior
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!enableHideOnScroll) return
|
|
242
|
+
|
|
243
|
+
const handleScroll = () => {
|
|
244
|
+
const currentScrollY = window.scrollY
|
|
245
|
+
setIsHidden(currentScrollY > lastScrollY.current && currentScrollY > scrollThreshold)
|
|
246
|
+
lastScrollY.current = currentScrollY
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
250
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
251
|
+
}, [enableHideOnScroll, scrollThreshold])
|
|
252
|
+
|
|
253
|
+
// Extract navItems from header02 data
|
|
254
|
+
const navItems = data?.header02?.navItems || []
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<>
|
|
258
|
+
<style jsx global>
|
|
259
|
+
{dropdownStyles}
|
|
260
|
+
</style>
|
|
261
|
+
{/* Desktop Navigation - Hidden on mobile */}
|
|
262
|
+
<header className="header-02 fixed container left-0 right-0 mx-auto w-screen mt-2 justify-between hidden md:flex">
|
|
263
|
+
<motion.nav
|
|
264
|
+
initial={{ opacity: 1, y: 0 }}
|
|
265
|
+
animate={{ opacity: 1, y: enableHideOnScroll && isHidden ? -100 : 0 }}
|
|
266
|
+
transition={{ duration: animationDuration }}
|
|
267
|
+
className={`navbar fixed top-0 z-50 right-0 ${variant === 'default' ? 'text-left' : ''} ${sticky ? 'sticky top-0 z-50' : ''} transition-transform duration-300 ease-out`}
|
|
268
|
+
id="topnav"
|
|
269
|
+
>
|
|
270
|
+
<div className="nav">
|
|
271
|
+
<NavigationMenu
|
|
272
|
+
viewport={false}
|
|
273
|
+
className="header02-navigation-menu shadow-glass dark:border-neutral-700 !flex w-full items-center justify-between rounded-2xl border px-5 py-0 pr-2.5 backdrop-blur-md relative"
|
|
274
|
+
>
|
|
275
|
+
<NavigationMenuList className="ml-2 gap-4 md:gap-6 flex">
|
|
276
|
+
{navItems.map((navItem, index) => {
|
|
277
|
+
// Simple Link - works
|
|
278
|
+
if (navItem.type === 'link') {
|
|
279
|
+
const href = getLinkHref(navItem.link)
|
|
280
|
+
const normalizedHref = normalizePath(href)
|
|
281
|
+
const isActive = normalizedHref === normalizedCurrentPathname
|
|
282
|
+
const icon = renderIcon(navItem.link?.icon, undefined, navItem.link?.customIcon)
|
|
283
|
+
const iconPosition = navItem.link?.iconPosition || 'before'
|
|
284
|
+
const showLabel = navItem.showLabel ?? true
|
|
285
|
+
return (
|
|
286
|
+
<NavigationMenuItem key={index}>
|
|
287
|
+
<NavigationMenuLink
|
|
288
|
+
asChild
|
|
289
|
+
className={cn(
|
|
290
|
+
'header02-nav-btn !flex flex-row items-center justify-center px-0 bg-transparent text-stone-200 dark:text-stone-200 gap-0 text-sm transition-colors focus:outline-none',
|
|
291
|
+
isActive &&
|
|
292
|
+
'header02-nav-btn-active text-stone-200 dark:text-stone-200 font-semibold'
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<Link
|
|
296
|
+
href={href}
|
|
297
|
+
{...(navItem.link?.newTab
|
|
298
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
299
|
+
: {})}
|
|
300
|
+
className="flex items-center gap-1 h-[2.5rem] card-bottom-lit"
|
|
301
|
+
>
|
|
302
|
+
{icon && iconPosition === 'before' && icon}
|
|
303
|
+
{showLabel && navItem.link?.label}
|
|
304
|
+
{icon && iconPosition === 'after' && icon}
|
|
305
|
+
</Link>
|
|
306
|
+
</NavigationMenuLink>
|
|
307
|
+
</NavigationMenuItem>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Dropdown with RichText + Links (Variant 1) - works
|
|
312
|
+
if (navItem.type === 'dropdown-richtext') {
|
|
313
|
+
const triggerIcon = renderIcon(navItem.triggerIcon)
|
|
314
|
+
const triggerIconPosition = navItem.triggerIconPosition || 'before'
|
|
315
|
+
return (
|
|
316
|
+
<NavigationMenuItem
|
|
317
|
+
key={index}
|
|
318
|
+
className="header02-nav-btn card-bottom-lit bg-transparent text-stone-200 dark:text-stone-200"
|
|
319
|
+
>
|
|
320
|
+
<NavigationMenuTrigger className="flex items-center gap-2">
|
|
321
|
+
{triggerIcon && triggerIconPosition === 'before' && triggerIcon}
|
|
322
|
+
{navItem.label}
|
|
323
|
+
{triggerIcon && triggerIconPosition === 'after' && triggerIcon}
|
|
324
|
+
</NavigationMenuTrigger>
|
|
325
|
+
<NavigationMenuContent className="header02-dropdown">
|
|
326
|
+
<motion.div
|
|
327
|
+
initial={{ opacity: 0, x: -50 }}
|
|
328
|
+
animate={{ opacity: 1, x: 0 }}
|
|
329
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
330
|
+
className="border border-border rounded-md overflow-hidden grid gap-3 pt-0 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]"
|
|
331
|
+
>
|
|
332
|
+
<div className="row-span-4">
|
|
333
|
+
<NavigationMenuLink asChild>
|
|
334
|
+
<div className="bg-card flex h-full w-full select-none flex-col justify-end rounded-none p-4 no-underline outline-none focus:shadow-md">
|
|
335
|
+
{navItem.richTextContent && (
|
|
336
|
+
<RichText
|
|
337
|
+
data={navItem.richTextContent}
|
|
338
|
+
enableGutter={false}
|
|
339
|
+
enableProse={false}
|
|
340
|
+
className="text-sm"
|
|
341
|
+
/>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
</NavigationMenuLink>
|
|
345
|
+
</div>
|
|
346
|
+
<ul className="flex flex-col gap-2 pt-4 p-1">
|
|
347
|
+
{navItem.v1_children?.map((child, childIndex) => (
|
|
348
|
+
<ListItem
|
|
349
|
+
key={childIndex}
|
|
350
|
+
title={child.link?.label || ''}
|
|
351
|
+
href={getLinkHref(child.link)}
|
|
352
|
+
icon={renderIcon(
|
|
353
|
+
child.link?.icon,
|
|
354
|
+
undefined,
|
|
355
|
+
child.link?.customIcon
|
|
356
|
+
)}
|
|
357
|
+
iconPosition={child.link?.iconPosition || 'before'}
|
|
358
|
+
className="text-sm"
|
|
359
|
+
>
|
|
360
|
+
{child.description}
|
|
361
|
+
</ListItem>
|
|
362
|
+
))}
|
|
363
|
+
</ul>
|
|
364
|
+
</motion.div>
|
|
365
|
+
</NavigationMenuContent>
|
|
366
|
+
</NavigationMenuItem>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Dropdown - Single Column with Description (Variant 4) - works
|
|
371
|
+
if (navItem.type === 'dropdown-single-desc') {
|
|
372
|
+
const triggerIcon = renderIcon(navItem.triggerIcon)
|
|
373
|
+
const triggerIconPosition = navItem.triggerIconPosition || 'before'
|
|
374
|
+
return (
|
|
375
|
+
<NavigationMenuItem
|
|
376
|
+
key={index}
|
|
377
|
+
className="header02-nav-btn card-bottom-lit text-stone-200 dark:text-stone-200"
|
|
378
|
+
>
|
|
379
|
+
<NavigationMenuTrigger className="flex items-center gap-2">
|
|
380
|
+
{triggerIcon && triggerIconPosition === 'before' && triggerIcon}
|
|
381
|
+
{navItem.label}
|
|
382
|
+
{triggerIcon && triggerIconPosition === 'after' && triggerIcon}
|
|
383
|
+
</NavigationMenuTrigger>
|
|
384
|
+
<NavigationMenuContent className="p-0">
|
|
385
|
+
<motion.ul
|
|
386
|
+
initial={{ opacity: 0, x: -50 }}
|
|
387
|
+
animate={{ opacity: 1, x: 0 }}
|
|
388
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
389
|
+
className="bg-7 border border-border rounded-md overflow-hidden grid gap-3 p-1 w-fit md:w-[14rem]"
|
|
390
|
+
>
|
|
391
|
+
{navItem.v1_children?.map((child, childIndex) => (
|
|
392
|
+
<ListItem
|
|
393
|
+
key={childIndex}
|
|
394
|
+
title={child.link?.label || ''}
|
|
395
|
+
href={getLinkHref(child.link)}
|
|
396
|
+
icon={renderIcon(
|
|
397
|
+
child.link?.icon,
|
|
398
|
+
undefined,
|
|
399
|
+
child.link?.customIcon
|
|
400
|
+
)}
|
|
401
|
+
iconPosition={child.link?.iconPosition || 'before'}
|
|
402
|
+
>
|
|
403
|
+
{child.description}
|
|
404
|
+
</ListItem>
|
|
405
|
+
))}
|
|
406
|
+
</motion.ul>
|
|
407
|
+
</NavigationMenuContent>
|
|
408
|
+
</NavigationMenuItem>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Dropdown - Single Column no Description (Variant 5) - works
|
|
413
|
+
if (navItem.type === 'dropdown-single') {
|
|
414
|
+
const triggerIcon = renderIcon(navItem.triggerIcon)
|
|
415
|
+
const triggerIconPosition = navItem.triggerIconPosition || 'before'
|
|
416
|
+
return (
|
|
417
|
+
<NavigationMenuItem
|
|
418
|
+
key={index}
|
|
419
|
+
className="header02-nav-btn card-bottom-lit bg-transparent text-stone-200 dark:text-stone-200"
|
|
420
|
+
>
|
|
421
|
+
<NavigationMenuTrigger className="flex items-center gap-2">
|
|
422
|
+
{triggerIcon && triggerIconPosition === 'before' && triggerIcon}
|
|
423
|
+
{navItem.label}
|
|
424
|
+
{triggerIcon && triggerIconPosition === 'after' && triggerIcon}
|
|
425
|
+
</NavigationMenuTrigger>
|
|
426
|
+
<NavigationMenuContent className="p-0">
|
|
427
|
+
<motion.ul
|
|
428
|
+
initial={{ opacity: 0, x: -50 }}
|
|
429
|
+
animate={{ opacity: 1, x: 0 }}
|
|
430
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
431
|
+
className="bg-7 border border-border rounded-md overflow-hidden flex flex-col gap-2 p-1 w-fit"
|
|
432
|
+
>
|
|
433
|
+
{navItem.v5_children?.map((child, childIndex) => (
|
|
434
|
+
<ListItem
|
|
435
|
+
key={childIndex}
|
|
436
|
+
title={child.link?.label || ''}
|
|
437
|
+
href={getLinkHref(child.link)}
|
|
438
|
+
icon={renderIcon(
|
|
439
|
+
child.link?.icon,
|
|
440
|
+
undefined,
|
|
441
|
+
child.link?.customIcon
|
|
442
|
+
)}
|
|
443
|
+
iconPosition={child.link?.iconPosition || 'before'}
|
|
444
|
+
/>
|
|
445
|
+
))}
|
|
446
|
+
</motion.ul>
|
|
447
|
+
</NavigationMenuContent>
|
|
448
|
+
</NavigationMenuItem>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Dropdown - Single Column with Icons (Variant 6) - works
|
|
453
|
+
if (navItem.type === 'dropdown-icons') {
|
|
454
|
+
const triggerIcon = renderIcon(navItem.triggerIcon)
|
|
455
|
+
const triggerIconPosition = navItem.triggerIconPosition || 'before'
|
|
456
|
+
return (
|
|
457
|
+
<NavigationMenuItem
|
|
458
|
+
key={index}
|
|
459
|
+
className="header02-nav-btn card-bottom-lit bg-transparent text-stone-200 dark:text-stone-200"
|
|
460
|
+
>
|
|
461
|
+
<NavigationMenuTrigger className="flex items-center gap-2">
|
|
462
|
+
{triggerIcon && triggerIconPosition === 'before' && triggerIcon}
|
|
463
|
+
{navItem.label}
|
|
464
|
+
{triggerIcon && triggerIconPosition === 'after' && triggerIcon}
|
|
465
|
+
</NavigationMenuTrigger>
|
|
466
|
+
<NavigationMenuContent className="p-0">
|
|
467
|
+
<motion.ul
|
|
468
|
+
initial={{ opacity: 0, x: -50 }}
|
|
469
|
+
animate={{ opacity: 1, x: 0 }}
|
|
470
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
471
|
+
className="bg-7 border border-border rounded-md overflow-hidden flex flex-col gap-2 p-1 w-fit"
|
|
472
|
+
>
|
|
473
|
+
{navItem.v6_children?.map((child, childIndex) => {
|
|
474
|
+
const icon = renderIcon(
|
|
475
|
+
child.link?.icon,
|
|
476
|
+
'size-4',
|
|
477
|
+
child.link?.customIcon
|
|
478
|
+
)
|
|
479
|
+
const iconPosition = child.link?.iconPosition || 'before'
|
|
480
|
+
return (
|
|
481
|
+
<li key={childIndex}>
|
|
482
|
+
<NavigationMenuLink asChild>
|
|
483
|
+
<Link
|
|
484
|
+
href={getLinkHref(child.link)}
|
|
485
|
+
className={cn(
|
|
486
|
+
'flex items-center gap-2 select-none no-underline outline-none transition-colors rounded-sm group bg-8 hover:bg-zinc-600/50 !hover:text-stone-200 focus:bg-accent focus:text-stone-200 py-2 px-3'
|
|
487
|
+
)}
|
|
488
|
+
>
|
|
489
|
+
{' '}
|
|
490
|
+
{icon && iconPosition === 'before' && icon}
|
|
491
|
+
<span className="!text-xs group font-medium leading-none scale-75 !hover:text-stone-200 ">
|
|
492
|
+
{child.link?.label}
|
|
493
|
+
</span>
|
|
494
|
+
{icon && iconPosition === 'after' && icon}
|
|
495
|
+
</Link>
|
|
496
|
+
</NavigationMenuLink>
|
|
497
|
+
</li>
|
|
498
|
+
)
|
|
499
|
+
})}
|
|
500
|
+
</motion.ul>
|
|
501
|
+
</NavigationMenuContent>
|
|
502
|
+
</NavigationMenuItem>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
return null
|
|
506
|
+
})}
|
|
507
|
+
</NavigationMenuList>
|
|
508
|
+
</NavigationMenu>
|
|
509
|
+
</div>
|
|
510
|
+
</motion.nav>
|
|
511
|
+
</header>
|
|
512
|
+
|
|
513
|
+
{/* Mobile Bottom Navigation - Visible only on mobile */}
|
|
514
|
+
<MobileBottomNav data={data} />
|
|
515
|
+
</>
|
|
516
|
+
)
|
|
517
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "component",
|
|
3
|
+
"name": "HeaderDD02",
|
|
4
|
+
"label": "Header DD 02",
|
|
5
|
+
"sourceName": "Header02",
|
|
6
|
+
"portableName": "HeaderDD02",
|
|
7
|
+
"slug": "HeaderDD02",
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"source": {
|
|
10
|
+
"repo": "Hadizainal/DesignsDustedv3",
|
|
11
|
+
"originalPath": "src/Header/variants/Header02",
|
|
12
|
+
"originalConfigExport": "header02Fields",
|
|
13
|
+
"originalComponentExport": "Header02",
|
|
14
|
+
"removedLexicalBlockImports": [
|
|
15
|
+
"@/blocks/AnimatedText/config",
|
|
16
|
+
"@/blocks/ButtonWithIcon/config",
|
|
17
|
+
"@/blocks/CounterBlock/config",
|
|
18
|
+
"@/blocks/MediaBlock/config"
|
|
19
|
+
],
|
|
20
|
+
"excludedHeaderFiles": [
|
|
21
|
+
"src/Header/Component.tsx",
|
|
22
|
+
"src/Header/Component.client.tsx",
|
|
23
|
+
"src/Header/config.ts",
|
|
24
|
+
"src/Header/Nav/index.tsx",
|
|
25
|
+
"src/Header/hooks/revalidateHeader.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
{
|
|
30
|
+
"from": "index.tsx",
|
|
31
|
+
"to": "src/Header/variants/HeaderDD02/index.tsx"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"from": "MobileBottomNav/index.tsx",
|
|
35
|
+
"to": "src/Header/variants/HeaderDD02/MobileBottomNav/index.tsx"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"from": "config.ts",
|
|
39
|
+
"to": "src/Header/variants/HeaderDD02/config.ts"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"from": "RowLabel.tsx",
|
|
43
|
+
"to": "src/Header/RowLabel.tsx"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"from": "README.md",
|
|
47
|
+
"to": "src/Header/variants/HeaderDD02/README.md"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"dependencies": [
|
|
51
|
+
{
|
|
52
|
+
"name": "@payloadcms/richtext-lexical"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "framer-motion"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "lucide-react"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"requiredProjectFiles": [
|
|
62
|
+
{
|
|
63
|
+
"path": "src/fields/defaultLexical.ts",
|
|
64
|
+
"reason": "HeaderDD02 richText fields use the target project's shared defaultLexical helper."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"path": "src/fields/link.ts",
|
|
68
|
+
"reason": "HeaderDD02 navigation fields reuse the target project's link field helper."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"path": "src/fields/lucide-icon-picker/field.ts",
|
|
72
|
+
"reason": "HeaderDD02 dropdown trigger icons reuse the shared icon picker field."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "src/components/ui/navigation-menu.tsx",
|
|
76
|
+
"reason": "HeaderDD02 desktop navigation renders the target project's NavigationMenu primitives."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"path": "src/components/RichText/index.tsx",
|
|
80
|
+
"reason": "HeaderDD02 renders richTextContent and floatingContent through the target project's RichText renderer."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"path": "src/utilities/ui.ts",
|
|
84
|
+
"reason": "HeaderDD02 uses cn from @/utilities/ui."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"path": "src/components/animate-ui/components/animate/tooltip.tsx",
|
|
88
|
+
"reason": "HeaderDD02 MobileBottomNav uses the target tooltip primitives."
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"collectionRegistration": {
|
|
92
|
+
"enabled": false
|
|
93
|
+
},
|
|
94
|
+
"renderBlocksRegistration": {
|
|
95
|
+
"enabled": false
|
|
96
|
+
},
|
|
97
|
+
"manual": [
|
|
98
|
+
"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.",
|
|
99
|
+
"Removed source Lexical block imports: `@/blocks/AnimatedText/config`, `@/blocks/ButtonWithIcon/config`, `@/blocks/CounterBlock/config`, `@/blocks/MediaBlock/config`.",
|
|
100
|
+
"HeaderDD02 installs the Header02 runtime component, MobileBottomNav, Header02 field config, and the RowLabel admin helper only. Header01 and Header03 are not bundled.",
|
|
101
|
+
"This component template does not yet patch an existing active Header global, replace Header/Component.tsx, or inspect database-stored Header content automatically. Review the existing Header global before activation and back up active files before wiring the variant into the target project.",
|
|
102
|
+
"Inspect any existing target Header richText data before replacing an active Header schema. Do not apply the empty nested blocks extension point to stored content that already depends on registered block node types.",
|
|
103
|
+
"Verify the target `src/fields/defaultLexical.ts` exports a compatible `defaultLexical` helper that accepts `defaultLexical({ features: [] })` and does not inject unrelated project-specific blocks.",
|
|
104
|
+
"Wire `HeaderDD02` into the target Header global or render it directly after confirming the target project will not render two headers."
|
|
105
|
+
],
|
|
106
|
+
"manualSteps": [
|
|
107
|
+
"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.",
|
|
108
|
+
"Removed source Lexical block imports: `@/blocks/AnimatedText/config`, `@/blocks/ButtonWithIcon/config`, `@/blocks/CounterBlock/config`, `@/blocks/MediaBlock/config`.",
|
|
109
|
+
"HeaderDD02 installs the Header02 runtime component, MobileBottomNav, Header02 field config, and the RowLabel admin helper only. Header01 and Header03 are not bundled.",
|
|
110
|
+
"This component template does not yet patch an existing active Header global, replace Header/Component.tsx, or inspect database-stored Header content automatically. Review the existing Header global before activation and back up active files before wiring the variant into the target project.",
|
|
111
|
+
"Inspect any existing target Header richText data before replacing an active Header schema. Do not apply the empty nested blocks extension point to stored content that already depends on registered block node types.",
|
|
112
|
+
"Verify the target `src/fields/defaultLexical.ts` exports a compatible `defaultLexical` helper that accepts `defaultLexical({ features: [] })` and does not inject unrelated project-specific blocks.",
|
|
113
|
+
"Wire `HeaderDD02` into the target Header global or render it directly after confirming the target project will not render two headers."
|
|
114
|
+
],
|
|
115
|
+
"notes": [
|
|
116
|
+
"Converted from DesignsDustedv3 Header02 after tracing Header/variants/Header02/index.tsx and its direct imports.",
|
|
117
|
+
"Header02 styling is inline Tailwind class usage plus style jsx global blocks in index.tsx and MobileBottomNav/index.tsx; no separate Header02 stylesheet was imported.",
|
|
118
|
+
"Parent Header files were audited but excluded because the reusable export targets Header02 only, not the multi-variant app header shell."
|
|
119
|
+
],
|
|
120
|
+
"backupExistingDestination": true,
|
|
121
|
+
"exportName": "HeaderDD02",
|
|
122
|
+
"interfaceName": "HeaderDD02"
|
|
123
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# RichText Component Template
|
|
2
|
+
|
|
3
|
+
This template restructures the default Payload starter RichText component into the Payload Rich Text Dusted Standard folder shape.
|
|
4
|
+
|
|
5
|
+
It is intentionally limited to default Payload starter behaviour:
|
|
6
|
+
|
|
7
|
+
- internal page and post links
|
|
8
|
+
- Lexical text formatting
|
|
9
|
+
- embedded `banner`, `code`, and `mediaBlock` blocks
|
|
10
|
+
|
|
11
|
+
Do not add custom reference-project block mappings unless the target project's RichText editor configuration enables those blocks.
|
|
12
|
+
|
|
13
|
+
The current `blocks-dusted` installer is still block-template-first, so this component template is source material for the next component installer milestone rather than an installable block template.
|