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
|
@@ -1,361 +1,302 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useRef, useState } from
|
|
3
|
-
import Link from
|
|
4
|
-
import Image from
|
|
5
|
-
import { ArrowRight } from
|
|
6
|
-
import * as LucideIcons from
|
|
7
|
-
import { motion, type Variants } from
|
|
8
|
-
import { cn } from
|
|
9
|
-
export type DDFeatCatProps = Record<string, any
|
|
10
|
-
type Media = { url?: string | null }
|
|
11
|
-
type CategoryItem = {
|
|
12
|
-
id?: string | null
|
|
13
|
-
title?: string | null
|
|
14
|
-
description?: string | null
|
|
15
|
-
link?: string | null
|
|
16
|
-
linkLabel?: string | null
|
|
17
|
-
iconName?: string | null
|
|
18
|
-
iconImage?: Media | string | null
|
|
19
|
-
iconColorClass?: string | null
|
|
20
|
-
iconBgClass?: string | null
|
|
21
|
-
count?: string | null
|
|
22
|
-
image?: Media | string | null
|
|
23
|
-
imageFallbackUrl?: string | null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isLocalImageSrc(src?: string | null): src is string {
|
|
27
|
-
return typeof src ===
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function CategoryPlaceholder({ title }: { title: string }) {
|
|
31
|
-
return (
|
|
32
|
-
<div
|
|
33
|
-
className="w-full h-48 overflow-hidden rounded-t-2xl relative bg-[#12151f]"
|
|
34
|
-
style={{ cornerShape:
|
|
35
|
-
>
|
|
36
|
-
<svg
|
|
37
|
-
aria-hidden="true"
|
|
38
|
-
className="absolute inset-0 h-full w-full"
|
|
39
|
-
viewBox="0 0 800 500"
|
|
40
|
-
preserveAspectRatio="none"
|
|
41
|
-
>
|
|
42
|
-
<defs>
|
|
43
|
-
<linearGradient
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
style={
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
<div
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<div
|
|
304
|
-
className={cn(
|
|
305
|
-
"w-9 h-9 flex items-center justify-center rounded-lg shrink-0",
|
|
306
|
-
cat.iconBgClass ?? "bg-stone-700",
|
|
307
|
-
)}
|
|
308
|
-
>
|
|
309
|
-
{iconSrc ? (
|
|
310
|
-
<Image
|
|
311
|
-
src={iconSrc}
|
|
312
|
-
alt={title}
|
|
313
|
-
width={16}
|
|
314
|
-
height={16}
|
|
315
|
-
className={cn(
|
|
316
|
-
"w-4 h-4 object-contain",
|
|
317
|
-
cat.iconColorClass,
|
|
318
|
-
)}
|
|
319
|
-
/>
|
|
320
|
-
) : (
|
|
321
|
-
<DynamicIcon
|
|
322
|
-
name={cat.iconName ?? ""}
|
|
323
|
-
size={16}
|
|
324
|
-
className={cat.iconColorClass ?? "text-stone-300"}
|
|
325
|
-
/>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
{cat.count && (
|
|
329
|
-
<span className="text-xs font-medium text-stone-700 dark:text-stone-400">
|
|
330
|
-
{cat.count}
|
|
331
|
-
</span>
|
|
332
|
-
)}
|
|
333
|
-
</div>
|
|
334
|
-
|
|
335
|
-
<h3 className="text-lg font-semibold text-stone-900 dark:text-stone-50 mb-2">
|
|
336
|
-
{title}
|
|
337
|
-
</h3>
|
|
338
|
-
<p className="text-sm text-stone-600 dark:text-stone-400 leading-relaxed mb-5">
|
|
339
|
-
{description}
|
|
340
|
-
</p>
|
|
341
|
-
|
|
342
|
-
<Link
|
|
343
|
-
href={link}
|
|
344
|
-
className="inline-flex items-center gap-1.5 text-sm font-semibold text-stone-700 dark:text-stone-300 hover:text-indigo-950 dark:hover:text-stone-50 hover:gap-2.5 transition-all duration-200 cursor-pointer"
|
|
345
|
-
>
|
|
346
|
-
{linkLabel}
|
|
347
|
-
<ArrowRight className="w-4 h-4" />
|
|
348
|
-
</Link>
|
|
349
|
-
</div>
|
|
350
|
-
</SpotlightCard>
|
|
351
|
-
</motion.div>
|
|
352
|
-
);
|
|
353
|
-
})}
|
|
354
|
-
</motion.div>
|
|
355
|
-
</div>
|
|
356
|
-
</div>
|
|
357
|
-
</>
|
|
358
|
-
);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
export default DDFeatCat;
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useRef, useState } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import { ArrowRight } from 'lucide-react'
|
|
6
|
+
import * as LucideIcons from 'lucide-react'
|
|
7
|
+
import { motion, type Variants } from 'framer-motion'
|
|
8
|
+
import { cn } from '@/utilities/ui'
|
|
9
|
+
export type DDFeatCatProps = Record<string, any>
|
|
10
|
+
type Media = { url?: string | null }
|
|
11
|
+
type CategoryItem = {
|
|
12
|
+
id?: string | null
|
|
13
|
+
title?: string | null
|
|
14
|
+
description?: string | null
|
|
15
|
+
link?: string | null
|
|
16
|
+
linkLabel?: string | null
|
|
17
|
+
iconName?: string | null
|
|
18
|
+
iconImage?: Media | string | null
|
|
19
|
+
iconColorClass?: string | null
|
|
20
|
+
iconBgClass?: string | null
|
|
21
|
+
count?: string | null
|
|
22
|
+
image?: Media | string | null
|
|
23
|
+
imageFallbackUrl?: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isLocalImageSrc(src?: string | null): src is string {
|
|
27
|
+
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function CategoryPlaceholder({ title }: { title: string }) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="w-full h-48 overflow-hidden rounded-t-2xl relative bg-[#12151f]"
|
|
34
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
35
|
+
>
|
|
36
|
+
<svg
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
className="absolute inset-0 h-full w-full"
|
|
39
|
+
viewBox="0 0 800 500"
|
|
40
|
+
preserveAspectRatio="none"
|
|
41
|
+
>
|
|
42
|
+
<defs>
|
|
43
|
+
<linearGradient id="dd-featcat-placeholder" x1="0" y1="0" x2="1" y2="1">
|
|
44
|
+
<stop offset="0%" stopColor="#1f2937" />
|
|
45
|
+
<stop offset="48%" stopColor="#251644" />
|
|
46
|
+
<stop offset="100%" stopColor="#0f172a" />
|
|
47
|
+
</linearGradient>
|
|
48
|
+
<pattern id="dd-featcat-grid" width="64" height="64" patternUnits="userSpaceOnUse">
|
|
49
|
+
<path
|
|
50
|
+
d="M 64 0 L 0 0 0 64"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="#ffffff"
|
|
53
|
+
strokeOpacity="0.08"
|
|
54
|
+
strokeWidth="1"
|
|
55
|
+
/>
|
|
56
|
+
</pattern>
|
|
57
|
+
</defs>
|
|
58
|
+
<rect width="800" height="500" fill="url(#dd-featcat-placeholder)" />
|
|
59
|
+
<rect width="800" height="500" fill="url(#dd-featcat-grid)" />
|
|
60
|
+
<rect x="90" y="105" width="190" height="120" rx="18" fill="#ffffff" fillOpacity="0.1" />
|
|
61
|
+
<rect x="310" y="105" width="190" height="120" rx="18" fill="#ffffff" fillOpacity="0.16" />
|
|
62
|
+
<rect x="530" y="105" width="190" height="120" rx="18" fill="#ffffff" fillOpacity="0.1" />
|
|
63
|
+
<rect x="160" y="260" width="190" height="120" rx="18" fill="#ffffff" fillOpacity="0.14" />
|
|
64
|
+
<rect x="450" y="260" width="190" height="120" rx="18" fill="#ffffff" fillOpacity="0.12" />
|
|
65
|
+
</svg>
|
|
66
|
+
<div className="absolute inset-x-4 bottom-4 text-xs font-medium uppercase tracking-widest text-stone-300/70">
|
|
67
|
+
{title || 'Category'}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── SpotlightCard ────────────────────────────────────────────────────────────
|
|
74
|
+
interface SpotlightCardProps {
|
|
75
|
+
children: React.ReactNode
|
|
76
|
+
className?: string
|
|
77
|
+
spotlightColor?: string
|
|
78
|
+
style?: React.CSSProperties
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function SpotlightCard({
|
|
82
|
+
children,
|
|
83
|
+
className,
|
|
84
|
+
spotlightColor = 'rgba(214,211,209,0.08)',
|
|
85
|
+
style,
|
|
86
|
+
}: SpotlightCardProps) {
|
|
87
|
+
const cardRef = useRef<HTMLDivElement>(null)
|
|
88
|
+
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
|
|
89
|
+
|
|
90
|
+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
91
|
+
if (!cardRef.current) return
|
|
92
|
+
const rect = cardRef.current.getBoundingClientRect()
|
|
93
|
+
setPos({ x: e.clientX - rect.left, y: e.clientY - rect.top })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleMouseLeave = () => setPos(null)
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
ref={cardRef}
|
|
101
|
+
className={cn('relative overflow-hidden', className)}
|
|
102
|
+
onMouseMove={handleMouseMove}
|
|
103
|
+
onMouseLeave={handleMouseLeave}
|
|
104
|
+
style={style}
|
|
105
|
+
>
|
|
106
|
+
{pos && (
|
|
107
|
+
<div
|
|
108
|
+
className="pointer-events-none absolute inset-0 z-0 transition-opacity duration-300"
|
|
109
|
+
style={{
|
|
110
|
+
background: `radial-gradient(400px circle at ${pos.x}px ${pos.y}px, ${spotlightColor}, transparent 60%)`,
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
<div className="relative z-10 h-[57%]">{children}</div>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Dynamic Icon ─────────────────────────────────────────────────────────────
|
|
120
|
+
function DynamicIcon({
|
|
121
|
+
name,
|
|
122
|
+
...props
|
|
123
|
+
}: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
|
|
124
|
+
if (!name) return null
|
|
125
|
+
const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
|
|
126
|
+
if (!Icon) return null
|
|
127
|
+
return <Icon {...props} />
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Animation variants ───────────────────────────────────────────────────────
|
|
131
|
+
const containerVariants: Variants = {
|
|
132
|
+
hidden: {},
|
|
133
|
+
show: { transition: { staggerChildren: 0.1, delayChildren: 0.05 } },
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const itemVariants: Variants = {
|
|
137
|
+
hidden: { opacity: 0, y: 24 },
|
|
138
|
+
show: { opacity: 1, y: 0, transition: { duration: 0.45, ease: 'easeOut' } },
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
142
|
+
export const DDFeatCat: React.FC<DDFeatCatProps> = ({
|
|
143
|
+
eyebrow = 'What we offer',
|
|
144
|
+
headlineMain = 'Everything you need',
|
|
145
|
+
headlineAccent = 'to ship faster',
|
|
146
|
+
subtext = 'From individual UI blocks to complete website systems — and the studio team to build it all for you.',
|
|
147
|
+
categories = [],
|
|
148
|
+
sectionId = 'featured-categories',
|
|
149
|
+
customCSS,
|
|
150
|
+
}) => {
|
|
151
|
+
const categoryItems = categories as CategoryItem[]
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
156
|
+
|
|
157
|
+
<div id={sectionId} className="py-24">
|
|
158
|
+
<div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
|
|
159
|
+
{/* Header */}#BlockName-DDFeatCat
|
|
160
|
+
<motion.div
|
|
161
|
+
variants={containerVariants}
|
|
162
|
+
initial="hidden"
|
|
163
|
+
whileInView="show"
|
|
164
|
+
viewport={{ once: false, amount: 0.2 }}
|
|
165
|
+
className="flex flex-col lg:flex-row lg:items-end justify-between gap-6 mb-14 px-0 md:px-4 overflow-hidden"
|
|
166
|
+
>
|
|
167
|
+
<motion.div variants={itemVariants}>
|
|
168
|
+
{eyebrow && (
|
|
169
|
+
<p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
|
|
170
|
+
{eyebrow}
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
<h2 className="text-3d text-4xl md:text-5xl font-bold text-stone-900 dark:text-stone-50 leading-tight">
|
|
174
|
+
{headlineMain}
|
|
175
|
+
{headlineAccent && (
|
|
176
|
+
<>
|
|
177
|
+
<br />
|
|
178
|
+
<span className="text-stone-400">{headlineAccent}</span>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
</h2>
|
|
182
|
+
</motion.div>
|
|
183
|
+
|
|
184
|
+
{subtext && (
|
|
185
|
+
<motion.p
|
|
186
|
+
variants={itemVariants}
|
|
187
|
+
className="text-stone-700 dark:text-stone-300 text-base leading-relaxed max-w-sm lg:text-right"
|
|
188
|
+
>
|
|
189
|
+
{subtext}
|
|
190
|
+
</motion.p>
|
|
191
|
+
)}
|
|
192
|
+
</motion.div>
|
|
193
|
+
{/* Cards */}
|
|
194
|
+
<motion.div
|
|
195
|
+
variants={containerVariants}
|
|
196
|
+
initial="hidden"
|
|
197
|
+
whileInView="show"
|
|
198
|
+
viewport={{ once: false, amount: 0.1 }}
|
|
199
|
+
className="grid grid-cols-1 md:grid-cols-3 gap-5"
|
|
200
|
+
>
|
|
201
|
+
{categoryItems.map((cat, index) => {
|
|
202
|
+
const title = cat.title ?? ''
|
|
203
|
+
const description = cat.description ?? ''
|
|
204
|
+
const link = cat.link ?? '#'
|
|
205
|
+
const linkLabel = cat.linkLabel ?? ''
|
|
206
|
+
const imgSrc =
|
|
207
|
+
cat.image && typeof cat.image === 'object' && cat.image.url
|
|
208
|
+
? cat.image.url
|
|
209
|
+
: (cat.imageFallbackUrl ?? '')
|
|
210
|
+
const safeImgSrc = isLocalImageSrc(imgSrc) ? imgSrc : null
|
|
211
|
+
const iconSrc =
|
|
212
|
+
cat.iconImage &&
|
|
213
|
+
typeof cat.iconImage === 'object' &&
|
|
214
|
+
isLocalImageSrc((cat.iconImage as { url?: string }).url)
|
|
215
|
+
? (cat.iconImage as { url: string }).url
|
|
216
|
+
: null
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<motion.div key={cat.id ?? index} variants={itemVariants}>
|
|
220
|
+
<SpotlightCard
|
|
221
|
+
spotlightColor="rgba(214,211,209,0.08)"
|
|
222
|
+
className="border border-stone-800 bg-stone-900 group transition-colors duration-300 h-full rounded-2xl hover:border-lime-500 corner-squircle overflow-hidden shadow-md outline-none hover:outline-stone-200/10 hover:outline-4 hover:shadow-2xl"
|
|
223
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
224
|
+
>
|
|
225
|
+
{/* Image */}
|
|
226
|
+
{safeImgSrc ? (
|
|
227
|
+
<div className="w-full h-48 overflow-hidden relative">
|
|
228
|
+
<Image
|
|
229
|
+
src={safeImgSrc}
|
|
230
|
+
alt={title}
|
|
231
|
+
fill
|
|
232
|
+
className="object-cover object-top group-hover:scale-110 transition-transform duration-500 studio-card-image rounded-2xl corner-squircle overflow-hidden scale-105"
|
|
233
|
+
sizes="(max-width: 768px) 100vw, 33vw"
|
|
234
|
+
priority={false}
|
|
235
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
) : (
|
|
239
|
+
<CategoryPlaceholder title={title} />
|
|
240
|
+
)}
|
|
241
|
+
{/* Content */}
|
|
242
|
+
<div
|
|
243
|
+
className="card-content bg-stone-100/90 dark:bg-stone-950/90 backdrop-blur-md -mt-6 mx-2 mb-2 p-4 relative z-5 drop-shadow-md rounded-2xl h-full corner-squircle group-hover:-translate-y-1 group-hover:shadow-2xl group-hover:brightness-110 transition-all duration-300"
|
|
244
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
245
|
+
>
|
|
246
|
+
<div className="flex items-center gap-3 mb-4">
|
|
247
|
+
<div
|
|
248
|
+
className={cn(
|
|
249
|
+
'w-9 h-9 flex items-center justify-center rounded-lg shrink-0',
|
|
250
|
+
cat.iconBgClass ?? 'bg-stone-700',
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
{iconSrc ? (
|
|
254
|
+
<Image
|
|
255
|
+
src={iconSrc}
|
|
256
|
+
alt={title}
|
|
257
|
+
width={16}
|
|
258
|
+
height={16}
|
|
259
|
+
className={cn('w-4 h-4 object-contain', cat.iconColorClass)}
|
|
260
|
+
/>
|
|
261
|
+
) : (
|
|
262
|
+
<DynamicIcon
|
|
263
|
+
name={cat.iconName ?? ''}
|
|
264
|
+
size={16}
|
|
265
|
+
className={cat.iconColorClass ?? 'text-stone-300'}
|
|
266
|
+
/>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
{cat.count && (
|
|
270
|
+
<span className="text-xs font-medium text-stone-700 dark:text-stone-400">
|
|
271
|
+
{cat.count}
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<h3 className="text-lg font-semibold text-stone-900 dark:text-stone-50 mb-2">
|
|
277
|
+
{title}
|
|
278
|
+
</h3>
|
|
279
|
+
<p className="text-sm text-stone-600 dark:text-stone-400 leading-relaxed mb-5">
|
|
280
|
+
{description}
|
|
281
|
+
</p>
|
|
282
|
+
|
|
283
|
+
<Link
|
|
284
|
+
href={link}
|
|
285
|
+
className="inline-flex items-center gap-1.5 text-sm font-semibold text-stone-700 dark:text-stone-300 hover:text-indigo-950 dark:hover:text-stone-50 hover:gap-2.5 transition-all duration-200 cursor-pointer"
|
|
286
|
+
>
|
|
287
|
+
{linkLabel}
|
|
288
|
+
<ArrowRight className="w-4 h-4" />
|
|
289
|
+
</Link>
|
|
290
|
+
</div>
|
|
291
|
+
</SpotlightCard>
|
|
292
|
+
</motion.div>
|
|
293
|
+
)
|
|
294
|
+
})}
|
|
295
|
+
</motion.div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export default DDFeatCat
|