blocks-dusted 0.1.11 → 0.1.12
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/package.json +5 -2
- package/src/cli.js +39 -3
- package/src/commands/shop.js +59 -0
- package/src/commands/warehouse.js +73 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -534
- package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
- package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
- package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
- package/templates/blocks/DD-Chip/Component.tsx +14 -14
- package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
- package/templates/blocks/DD-Contact/Component.tsx +8 -7
- package/templates/blocks/DD-Counter/Component.tsx +10 -10
- package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
- package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
- package/templates/blocks/DD-Hero/Component.tsx +297 -320
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
- package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
- package/templates/blocks/DD-Pricing/Component.tsx +369 -372
- package/templates/blocks/DD-Process/Component.tsx +147 -149
- package/templates/blocks/DD-Progress/Component.tsx +156 -141
- package/templates/blocks/DD-ProjSlider/config.ts +2 -2
- package/templates/blocks/DD-Section/Component.tsx +1 -1
- package/templates/blocks/DD-Services/Component.tsx +1 -1
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
- package/templates/blocks/DD-StackCards/Component.tsx +473 -473
- package/templates/blocks/DD-Team/Component.tsx +1 -1
- package/templates/blocks/DD-TechStack/Component.tsx +22 -28
- package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
- package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
- package/templates/blocks/DD-Thanks/Component.tsx +501 -500
- package/templates/blocks/DD-Work/Component.tsx +4 -4
- package/templates/blocks/DD-Work-B/Component.tsx +1 -1
- package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
- package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
- package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
- package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
- package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
- package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
- package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -131
|
@@ -1,473 +1,473 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React, { useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
-
import Image from 'next/image'
|
|
5
|
-
import * as LucideIcons from 'lucide-react'
|
|
6
|
-
import { ArrowRight } from 'lucide-react'
|
|
7
|
-
import gsap from 'gsap'
|
|
8
|
-
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
9
|
-
type Media = { url?: string | null; alt?: string | null; [key: string]: any }
|
|
10
|
-
|
|
11
|
-
import RichText from '@/components/RichText'
|
|
12
|
-
|
|
13
|
-
if (typeof window !== 'undefined') {
|
|
14
|
-
gsap.registerPlugin(ScrollTrigger)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const ACCENTS = {
|
|
18
|
-
lime: { hex: '#84cc16', rgb: '132,204,22' },
|
|
19
|
-
purple: { hex: '#9803e2', rgb: '152,3,226' },
|
|
20
|
-
} as const
|
|
21
|
-
|
|
22
|
-
type AccentVariant = keyof typeof ACCENTS
|
|
23
|
-
|
|
24
|
-
function isLocalImageSrc(src?: string | null): src is string {
|
|
25
|
-
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function DynamicIcon({
|
|
29
|
-
name,
|
|
30
|
-
size = 22,
|
|
31
|
-
...props
|
|
32
|
-
}: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
|
|
33
|
-
if (!name) return null
|
|
34
|
-
|
|
35
|
-
const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
|
|
36
|
-
|
|
37
|
-
if (!Icon) return null
|
|
38
|
-
|
|
39
|
-
return <Icon size={size} {...props} />
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface TagItem {
|
|
43
|
-
id?: string | null
|
|
44
|
-
tag: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface CardItem {
|
|
48
|
-
id?: string | null
|
|
49
|
-
iconName?: string | null
|
|
50
|
-
number?: string | null
|
|
51
|
-
title?: string | null
|
|
52
|
-
content?: unknown
|
|
53
|
-
statValue?: string | null
|
|
54
|
-
statLabel?: string | null
|
|
55
|
-
tags?: TagItem[] | null
|
|
56
|
-
ctaLabel?: string | null
|
|
57
|
-
ctaHref?: string | null
|
|
58
|
-
accentVariant?: AccentVariant | null
|
|
59
|
-
image?: Media | string | null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface DDStackCardsProps {
|
|
63
|
-
cards?: CardItem[] | null
|
|
64
|
-
|
|
65
|
-
cardOffset?: number | null
|
|
66
|
-
initialStickyTop?: number | null
|
|
67
|
-
depthZ?: number | null
|
|
68
|
-
scaleStep?: number | null
|
|
69
|
-
|
|
70
|
-
cardOffsetTablet?: number | null
|
|
71
|
-
initialStickyTopTablet?: number | null
|
|
72
|
-
depthZTablet?: number | null
|
|
73
|
-
scaleStepTablet?: number | null
|
|
74
|
-
|
|
75
|
-
cardOffsetMobile?: number | null
|
|
76
|
-
initialStickyTopMobile?: number | null
|
|
77
|
-
depthZMobile?: number | null
|
|
78
|
-
scaleStepMobile?: number | null
|
|
79
|
-
|
|
80
|
-
sectionId?: string | null
|
|
81
|
-
[key: string]: unknown
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
type ResponsiveStackValues = {
|
|
85
|
-
cardOffset: number
|
|
86
|
-
initialStickyTop: number
|
|
87
|
-
depthZ: number
|
|
88
|
-
scaleStep: number
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getResponsiveStackValues({
|
|
92
|
-
cardOffset,
|
|
93
|
-
initialStickyTop,
|
|
94
|
-
depthZ,
|
|
95
|
-
scaleStep,
|
|
96
|
-
cardOffsetTablet,
|
|
97
|
-
initialStickyTopTablet,
|
|
98
|
-
depthZTablet,
|
|
99
|
-
scaleStepTablet,
|
|
100
|
-
cardOffsetMobile,
|
|
101
|
-
initialStickyTopMobile,
|
|
102
|
-
depthZMobile,
|
|
103
|
-
scaleStepMobile,
|
|
104
|
-
}: DDStackCardsProps): ResponsiveStackValues {
|
|
105
|
-
const desktopValues = {
|
|
106
|
-
cardOffset: typeof cardOffset === 'number' ? cardOffset : 14,
|
|
107
|
-
initialStickyTop: typeof initialStickyTop === 'number' ? initialStickyTop : 0,
|
|
108
|
-
depthZ: typeof depthZ === 'number' ? depthZ : 40,
|
|
109
|
-
scaleStep: typeof scaleStep === 'number' ? scaleStep : 0.18,
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof window === 'undefined') return desktopValues
|
|
113
|
-
|
|
114
|
-
const tabletValues = {
|
|
115
|
-
cardOffset:
|
|
116
|
-
typeof cardOffsetTablet === 'number'
|
|
117
|
-
? cardOffsetTablet
|
|
118
|
-
: Math.max(8, desktopValues.cardOffset * 0.75),
|
|
119
|
-
initialStickyTop:
|
|
120
|
-
typeof initialStickyTopTablet === 'number'
|
|
121
|
-
? initialStickyTopTablet
|
|
122
|
-
: desktopValues.initialStickyTop,
|
|
123
|
-
depthZ:
|
|
124
|
-
typeof depthZTablet === 'number' ? depthZTablet : Math.max(16, desktopValues.depthZ * 0.65),
|
|
125
|
-
scaleStep:
|
|
126
|
-
typeof scaleStepTablet === 'number'
|
|
127
|
-
? scaleStepTablet
|
|
128
|
-
: Math.min(desktopValues.scaleStep, 0.08),
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const mobileValues = {
|
|
132
|
-
cardOffset:
|
|
133
|
-
typeof cardOffsetMobile === 'number'
|
|
134
|
-
? cardOffsetMobile
|
|
135
|
-
: Math.max(6, desktopValues.cardOffset * 0.5),
|
|
136
|
-
initialStickyTop:
|
|
137
|
-
typeof initialStickyTopMobile === 'number'
|
|
138
|
-
? initialStickyTopMobile
|
|
139
|
-
: desktopValues.initialStickyTop,
|
|
140
|
-
depthZ:
|
|
141
|
-
typeof depthZMobile === 'number' ? depthZMobile : Math.max(8, desktopValues.depthZ * 0.35),
|
|
142
|
-
scaleStep:
|
|
143
|
-
typeof scaleStepMobile === 'number'
|
|
144
|
-
? scaleStepMobile
|
|
145
|
-
: Math.min(desktopValues.scaleStep, 0.045),
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (window.matchMedia('(max-width: 767px)').matches) return mobileValues
|
|
149
|
-
if (window.matchMedia('(max-width: 1023px)').matches) return tabletValues
|
|
150
|
-
|
|
151
|
-
return desktopValues
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function DDStackCards(props: DDStackCardsProps) {
|
|
155
|
-
const { cards, sectionId } = props
|
|
156
|
-
|
|
157
|
-
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
|
158
|
-
const cardRefs = useRef<(HTMLElement | null)[]>([])
|
|
159
|
-
const [layoutValues, setLayoutValues] = useState<ResponsiveStackValues>(() => ({
|
|
160
|
-
cardOffset: typeof props.cardOffset === 'number' ? props.cardOffset : 14,
|
|
161
|
-
initialStickyTop: typeof props.initialStickyTop === 'number' ? props.initialStickyTop : 0,
|
|
162
|
-
depthZ: typeof props.depthZ === 'number' ? props.depthZ : 40,
|
|
163
|
-
scaleStep: typeof props.scaleStep === 'number' ? props.scaleStep : 0.18,
|
|
164
|
-
}))
|
|
165
|
-
|
|
166
|
-
useLayoutEffect(() => {
|
|
167
|
-
const updateValues = () => {
|
|
168
|
-
setLayoutValues(getResponsiveStackValues(props))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
updateValues()
|
|
172
|
-
|
|
173
|
-
window.addEventListener('resize', updateValues)
|
|
174
|
-
|
|
175
|
-
return () => {
|
|
176
|
-
window.removeEventListener('resize', updateValues)
|
|
177
|
-
}
|
|
178
|
-
}, [
|
|
179
|
-
props.cardOffset,
|
|
180
|
-
props.initialStickyTop,
|
|
181
|
-
props.depthZ,
|
|
182
|
-
props.scaleStep,
|
|
183
|
-
props.cardOffsetTablet,
|
|
184
|
-
props.initialStickyTopTablet,
|
|
185
|
-
props.depthZTablet,
|
|
186
|
-
props.scaleStepTablet,
|
|
187
|
-
props.cardOffsetMobile,
|
|
188
|
-
props.initialStickyTopMobile,
|
|
189
|
-
props.depthZMobile,
|
|
190
|
-
props.scaleStepMobile,
|
|
191
|
-
])
|
|
192
|
-
|
|
193
|
-
useLayoutEffect(() => {
|
|
194
|
-
const timer = window.setTimeout(() => {
|
|
195
|
-
const wrapper = wrapperRef.current
|
|
196
|
-
const cardEls = cardRefs.current.filter(Boolean) as HTMLDivElement[]
|
|
197
|
-
|
|
198
|
-
if (!wrapper || !cardEls.length) return
|
|
199
|
-
|
|
200
|
-
const ctx = gsap.context(() => {
|
|
201
|
-
gsap.set(wrapper, {
|
|
202
|
-
autoAlpha: 1,
|
|
203
|
-
willChange: 'opacity',
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
gsap.set(cardEls, {
|
|
207
|
-
transformPerspective: 1200,
|
|
208
|
-
transformOrigin: 'top center',
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
cardEls.forEach((card, i) => {
|
|
212
|
-
for (let j = i + 1; j < cardEls.length; j++) {
|
|
213
|
-
const step = j - i
|
|
214
|
-
const prevStep = step - 1
|
|
215
|
-
const triggerStickyTop =
|
|
216
|
-
layoutValues.initialStickyTop + (j - 1) * layoutValues.cardOffset
|
|
217
|
-
|
|
218
|
-
gsap.fromTo(
|
|
219
|
-
card,
|
|
220
|
-
{
|
|
221
|
-
scale: 1 - prevStep * layoutValues.scaleStep,
|
|
222
|
-
z: -prevStep * layoutValues.depthZ,
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
scale: 1 - step * layoutValues.scaleStep,
|
|
226
|
-
z: -step * layoutValues.depthZ,
|
|
227
|
-
force3D: true,
|
|
228
|
-
filter: `brightness(${Math.max(0.65, 1 - step * 0.06)})`,
|
|
229
|
-
borderRadius: '20px',
|
|
230
|
-
immediateRender: false,
|
|
231
|
-
scrollTrigger: {
|
|
232
|
-
trigger: cardEls[j],
|
|
233
|
-
start: `top ${triggerStickyTop + layoutValues.cardOffset}px`,
|
|
234
|
-
end: `top ${triggerStickyTop}px`,
|
|
235
|
-
scrub: true,
|
|
236
|
-
invalidateOnRefresh: true,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
const lastCard = cardEls[cardEls.length - 1]
|
|
244
|
-
|
|
245
|
-
gsap.to(wrapper, {
|
|
246
|
-
autoAlpha: 0,
|
|
247
|
-
ease: 'none',
|
|
248
|
-
scrollTrigger: {
|
|
249
|
-
trigger: lastCard,
|
|
250
|
-
start: 'top top',
|
|
251
|
-
end: () => `+=${window.innerHeight * 0.3}`,
|
|
252
|
-
scrub: true,
|
|
253
|
-
invalidateOnRefresh: true,
|
|
254
|
-
},
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
requestAnimationFrame(() => {
|
|
258
|
-
ScrollTrigger.refresh()
|
|
259
|
-
})
|
|
260
|
-
}, wrapper)
|
|
261
|
-
|
|
262
|
-
return () => ctx.revert()
|
|
263
|
-
}, 120)
|
|
264
|
-
|
|
265
|
-
return () => {
|
|
266
|
-
window.clearTimeout(timer)
|
|
267
|
-
}
|
|
268
|
-
}, [
|
|
269
|
-
layoutValues.cardOffset,
|
|
270
|
-
layoutValues.initialStickyTop,
|
|
271
|
-
layoutValues.depthZ,
|
|
272
|
-
layoutValues.scaleStep,
|
|
273
|
-
])
|
|
274
|
-
|
|
275
|
-
if (!cards?.length) return null
|
|
276
|
-
|
|
277
|
-
const totalCards = cards.length
|
|
278
|
-
|
|
279
|
-
return (
|
|
280
|
-
<section
|
|
281
|
-
ref={wrapperRef}
|
|
282
|
-
className="stack-cards-block relative w-full invert-[0.95] brightness-110 contrast-125 dark:brightness-100 dark:contrast-100 dark:invert-[0]"
|
|
283
|
-
>
|
|
284
|
-
{cards.map((card, i) => {
|
|
285
|
-
const accent = ACCENTS[card.accentVariant ?? 'lime']
|
|
286
|
-
const cardMedia =
|
|
287
|
-
card.image && typeof card.image === 'object' ? (card.image as Media) : null
|
|
288
|
-
|
|
289
|
-
return (
|
|
290
|
-
<section
|
|
291
|
-
key={card.id ?? i}
|
|
292
|
-
id={i === 0 ? (sectionId ?? undefined) : undefined}
|
|
293
|
-
ref={(el) => {
|
|
294
|
-
cardRefs.current[i] = el
|
|
295
|
-
}}
|
|
296
|
-
className="stack-card renderblock-container !
|
|
297
|
-
style={{
|
|
298
|
-
top: `${layoutValues.initialStickyTop + i * layoutValues.cardOffset}px`,
|
|
299
|
-
zIndex: i + 1,
|
|
300
|
-
willChange: 'transform',
|
|
301
|
-
}}
|
|
302
|
-
>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import * as LucideIcons from 'lucide-react'
|
|
6
|
+
import { ArrowRight } from 'lucide-react'
|
|
7
|
+
import gsap from 'gsap'
|
|
8
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
9
|
+
type Media = { url?: string | null; alt?: string | null; [key: string]: any }
|
|
10
|
+
|
|
11
|
+
import RichText from '@/components/RichText'
|
|
12
|
+
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
gsap.registerPlugin(ScrollTrigger)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ACCENTS = {
|
|
18
|
+
lime: { hex: '#84cc16', rgb: '132,204,22' },
|
|
19
|
+
purple: { hex: '#9803e2', rgb: '152,3,226' },
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
type AccentVariant = keyof typeof ACCENTS
|
|
23
|
+
|
|
24
|
+
function isLocalImageSrc(src?: string | null): src is string {
|
|
25
|
+
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function DynamicIcon({
|
|
29
|
+
name,
|
|
30
|
+
size = 22,
|
|
31
|
+
...props
|
|
32
|
+
}: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
|
|
33
|
+
if (!name) return null
|
|
34
|
+
|
|
35
|
+
const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
|
|
36
|
+
|
|
37
|
+
if (!Icon) return null
|
|
38
|
+
|
|
39
|
+
return <Icon size={size} {...props} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TagItem {
|
|
43
|
+
id?: string | null
|
|
44
|
+
tag: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CardItem {
|
|
48
|
+
id?: string | null
|
|
49
|
+
iconName?: string | null
|
|
50
|
+
number?: string | null
|
|
51
|
+
title?: string | null
|
|
52
|
+
content?: unknown
|
|
53
|
+
statValue?: string | null
|
|
54
|
+
statLabel?: string | null
|
|
55
|
+
tags?: TagItem[] | null
|
|
56
|
+
ctaLabel?: string | null
|
|
57
|
+
ctaHref?: string | null
|
|
58
|
+
accentVariant?: AccentVariant | null
|
|
59
|
+
image?: Media | string | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DDStackCardsProps {
|
|
63
|
+
cards?: CardItem[] | null
|
|
64
|
+
|
|
65
|
+
cardOffset?: number | null
|
|
66
|
+
initialStickyTop?: number | null
|
|
67
|
+
depthZ?: number | null
|
|
68
|
+
scaleStep?: number | null
|
|
69
|
+
|
|
70
|
+
cardOffsetTablet?: number | null
|
|
71
|
+
initialStickyTopTablet?: number | null
|
|
72
|
+
depthZTablet?: number | null
|
|
73
|
+
scaleStepTablet?: number | null
|
|
74
|
+
|
|
75
|
+
cardOffsetMobile?: number | null
|
|
76
|
+
initialStickyTopMobile?: number | null
|
|
77
|
+
depthZMobile?: number | null
|
|
78
|
+
scaleStepMobile?: number | null
|
|
79
|
+
|
|
80
|
+
sectionId?: string | null
|
|
81
|
+
[key: string]: unknown
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type ResponsiveStackValues = {
|
|
85
|
+
cardOffset: number
|
|
86
|
+
initialStickyTop: number
|
|
87
|
+
depthZ: number
|
|
88
|
+
scaleStep: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getResponsiveStackValues({
|
|
92
|
+
cardOffset,
|
|
93
|
+
initialStickyTop,
|
|
94
|
+
depthZ,
|
|
95
|
+
scaleStep,
|
|
96
|
+
cardOffsetTablet,
|
|
97
|
+
initialStickyTopTablet,
|
|
98
|
+
depthZTablet,
|
|
99
|
+
scaleStepTablet,
|
|
100
|
+
cardOffsetMobile,
|
|
101
|
+
initialStickyTopMobile,
|
|
102
|
+
depthZMobile,
|
|
103
|
+
scaleStepMobile,
|
|
104
|
+
}: DDStackCardsProps): ResponsiveStackValues {
|
|
105
|
+
const desktopValues = {
|
|
106
|
+
cardOffset: typeof cardOffset === 'number' ? cardOffset : 14,
|
|
107
|
+
initialStickyTop: typeof initialStickyTop === 'number' ? initialStickyTop : 0,
|
|
108
|
+
depthZ: typeof depthZ === 'number' ? depthZ : 40,
|
|
109
|
+
scaleStep: typeof scaleStep === 'number' ? scaleStep : 0.18,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof window === 'undefined') return desktopValues
|
|
113
|
+
|
|
114
|
+
const tabletValues = {
|
|
115
|
+
cardOffset:
|
|
116
|
+
typeof cardOffsetTablet === 'number'
|
|
117
|
+
? cardOffsetTablet
|
|
118
|
+
: Math.max(8, desktopValues.cardOffset * 0.75),
|
|
119
|
+
initialStickyTop:
|
|
120
|
+
typeof initialStickyTopTablet === 'number'
|
|
121
|
+
? initialStickyTopTablet
|
|
122
|
+
: desktopValues.initialStickyTop,
|
|
123
|
+
depthZ:
|
|
124
|
+
typeof depthZTablet === 'number' ? depthZTablet : Math.max(16, desktopValues.depthZ * 0.65),
|
|
125
|
+
scaleStep:
|
|
126
|
+
typeof scaleStepTablet === 'number'
|
|
127
|
+
? scaleStepTablet
|
|
128
|
+
: Math.min(desktopValues.scaleStep, 0.08),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const mobileValues = {
|
|
132
|
+
cardOffset:
|
|
133
|
+
typeof cardOffsetMobile === 'number'
|
|
134
|
+
? cardOffsetMobile
|
|
135
|
+
: Math.max(6, desktopValues.cardOffset * 0.5),
|
|
136
|
+
initialStickyTop:
|
|
137
|
+
typeof initialStickyTopMobile === 'number'
|
|
138
|
+
? initialStickyTopMobile
|
|
139
|
+
: desktopValues.initialStickyTop,
|
|
140
|
+
depthZ:
|
|
141
|
+
typeof depthZMobile === 'number' ? depthZMobile : Math.max(8, desktopValues.depthZ * 0.35),
|
|
142
|
+
scaleStep:
|
|
143
|
+
typeof scaleStepMobile === 'number'
|
|
144
|
+
? scaleStepMobile
|
|
145
|
+
: Math.min(desktopValues.scaleStep, 0.045),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (window.matchMedia('(max-width: 767px)').matches) return mobileValues
|
|
149
|
+
if (window.matchMedia('(max-width: 1023px)').matches) return tabletValues
|
|
150
|
+
|
|
151
|
+
return desktopValues
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function DDStackCards(props: DDStackCardsProps) {
|
|
155
|
+
const { cards, sectionId } = props
|
|
156
|
+
|
|
157
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
|
158
|
+
const cardRefs = useRef<(HTMLElement | null)[]>([])
|
|
159
|
+
const [layoutValues, setLayoutValues] = useState<ResponsiveStackValues>(() => ({
|
|
160
|
+
cardOffset: typeof props.cardOffset === 'number' ? props.cardOffset : 14,
|
|
161
|
+
initialStickyTop: typeof props.initialStickyTop === 'number' ? props.initialStickyTop : 0,
|
|
162
|
+
depthZ: typeof props.depthZ === 'number' ? props.depthZ : 40,
|
|
163
|
+
scaleStep: typeof props.scaleStep === 'number' ? props.scaleStep : 0.18,
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
const updateValues = () => {
|
|
168
|
+
setLayoutValues(getResponsiveStackValues(props))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
updateValues()
|
|
172
|
+
|
|
173
|
+
window.addEventListener('resize', updateValues)
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
window.removeEventListener('resize', updateValues)
|
|
177
|
+
}
|
|
178
|
+
}, [
|
|
179
|
+
props.cardOffset,
|
|
180
|
+
props.initialStickyTop,
|
|
181
|
+
props.depthZ,
|
|
182
|
+
props.scaleStep,
|
|
183
|
+
props.cardOffsetTablet,
|
|
184
|
+
props.initialStickyTopTablet,
|
|
185
|
+
props.depthZTablet,
|
|
186
|
+
props.scaleStepTablet,
|
|
187
|
+
props.cardOffsetMobile,
|
|
188
|
+
props.initialStickyTopMobile,
|
|
189
|
+
props.depthZMobile,
|
|
190
|
+
props.scaleStepMobile,
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
useLayoutEffect(() => {
|
|
194
|
+
const timer = window.setTimeout(() => {
|
|
195
|
+
const wrapper = wrapperRef.current
|
|
196
|
+
const cardEls = cardRefs.current.filter(Boolean) as HTMLDivElement[]
|
|
197
|
+
|
|
198
|
+
if (!wrapper || !cardEls.length) return
|
|
199
|
+
|
|
200
|
+
const ctx = gsap.context(() => {
|
|
201
|
+
gsap.set(wrapper, {
|
|
202
|
+
autoAlpha: 1,
|
|
203
|
+
willChange: 'opacity',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
gsap.set(cardEls, {
|
|
207
|
+
transformPerspective: 1200,
|
|
208
|
+
transformOrigin: 'top center',
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
cardEls.forEach((card, i) => {
|
|
212
|
+
for (let j = i + 1; j < cardEls.length; j++) {
|
|
213
|
+
const step = j - i
|
|
214
|
+
const prevStep = step - 1
|
|
215
|
+
const triggerStickyTop =
|
|
216
|
+
layoutValues.initialStickyTop + (j - 1) * layoutValues.cardOffset
|
|
217
|
+
|
|
218
|
+
gsap.fromTo(
|
|
219
|
+
card,
|
|
220
|
+
{
|
|
221
|
+
scale: 1 - prevStep * layoutValues.scaleStep,
|
|
222
|
+
z: -prevStep * layoutValues.depthZ,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
scale: 1 - step * layoutValues.scaleStep,
|
|
226
|
+
z: -step * layoutValues.depthZ,
|
|
227
|
+
force3D: true,
|
|
228
|
+
filter: `brightness(${Math.max(0.65, 1 - step * 0.06)})`,
|
|
229
|
+
borderRadius: '20px',
|
|
230
|
+
immediateRender: false,
|
|
231
|
+
scrollTrigger: {
|
|
232
|
+
trigger: cardEls[j],
|
|
233
|
+
start: `top ${triggerStickyTop + layoutValues.cardOffset}px`,
|
|
234
|
+
end: `top ${triggerStickyTop}px`,
|
|
235
|
+
scrub: true,
|
|
236
|
+
invalidateOnRefresh: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const lastCard = cardEls[cardEls.length - 1]
|
|
244
|
+
|
|
245
|
+
gsap.to(wrapper, {
|
|
246
|
+
autoAlpha: 0,
|
|
247
|
+
ease: 'none',
|
|
248
|
+
scrollTrigger: {
|
|
249
|
+
trigger: lastCard,
|
|
250
|
+
start: 'top top',
|
|
251
|
+
end: () => `+=${window.innerHeight * 0.3}`,
|
|
252
|
+
scrub: true,
|
|
253
|
+
invalidateOnRefresh: true,
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
requestAnimationFrame(() => {
|
|
258
|
+
ScrollTrigger.refresh()
|
|
259
|
+
})
|
|
260
|
+
}, wrapper)
|
|
261
|
+
|
|
262
|
+
return () => ctx.revert()
|
|
263
|
+
}, 120)
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
window.clearTimeout(timer)
|
|
267
|
+
}
|
|
268
|
+
}, [
|
|
269
|
+
layoutValues.cardOffset,
|
|
270
|
+
layoutValues.initialStickyTop,
|
|
271
|
+
layoutValues.depthZ,
|
|
272
|
+
layoutValues.scaleStep,
|
|
273
|
+
])
|
|
274
|
+
|
|
275
|
+
if (!cards?.length) return null
|
|
276
|
+
|
|
277
|
+
const totalCards = cards.length
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<section
|
|
281
|
+
ref={wrapperRef}
|
|
282
|
+
className="stack-cards-block relative w-full invert-[0.95] brightness-110 contrast-125 dark:brightness-100 dark:contrast-100 dark:invert-[0]"
|
|
283
|
+
>
|
|
284
|
+
{cards.map((card, i) => {
|
|
285
|
+
const accent = ACCENTS[card.accentVariant ?? 'lime']
|
|
286
|
+
const cardMedia =
|
|
287
|
+
card.image && typeof card.image === 'object' ? (card.image as Media) : null
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<section
|
|
291
|
+
key={card.id ?? i}
|
|
292
|
+
id={i === 0 ? (sectionId ?? undefined) : undefined}
|
|
293
|
+
ref={(el) => {
|
|
294
|
+
cardRefs.current[i] = el
|
|
295
|
+
}}
|
|
296
|
+
className="stack-card renderblock-container sticky! w-full overflow-hidden px-4 py-16 sm:px-6 lg:py-32"
|
|
297
|
+
style={{
|
|
298
|
+
top: `${layoutValues.initialStickyTop + i * layoutValues.cardOffset}px`,
|
|
299
|
+
zIndex: i + 1,
|
|
300
|
+
willChange: 'transform',
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
{i === 0 ? '#BlockName-DDStackCards' : null}
|
|
304
|
+
<div
|
|
305
|
+
className="relative mx-auto w-full max-w-5xl overflow-hidden rounded-2xl"
|
|
306
|
+
style={{
|
|
307
|
+
background: 'rgb(8 8 10 / 90%)',
|
|
308
|
+
border: '1px solid rgba(255,255,255,0.07)',
|
|
309
|
+
backdropFilter: 'blur(8px)',
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<div
|
|
313
|
+
className="absolute left-0 right-0 top-0 h-px"
|
|
314
|
+
style={{
|
|
315
|
+
background: `linear-gradient(90deg, transparent, rgba(${accent.rgb},0.5), transparent)`,
|
|
316
|
+
}}
|
|
317
|
+
/>
|
|
318
|
+
|
|
319
|
+
<div className="grid min-h-70 grid-cols-[1fr_5.5rem] sm:grid-cols-[1fr_8rem] md:min-h-85 md:grid-cols-[1fr_auto]">
|
|
320
|
+
<div className="stackcard-bg flex flex-col justify-between p-6 md:p-10 lg:p-14">
|
|
321
|
+
<div>
|
|
322
|
+
<div className="mb-6 flex items-start justify-between md:mb-8">
|
|
323
|
+
<div className="flex items-center gap-3 md:gap-4">
|
|
324
|
+
<div
|
|
325
|
+
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl md:h-12 md:w-12"
|
|
326
|
+
style={{
|
|
327
|
+
background: `rgba(${accent.rgb},0.1)`,
|
|
328
|
+
border: `1px solid rgba(${accent.rgb},0.25)`,
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<span style={{ color: accent.hex }}>
|
|
332
|
+
<DynamicIcon className="shrink-0" name={card.iconName} size={20} />
|
|
333
|
+
</span>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div>
|
|
337
|
+
{card.number && (
|
|
338
|
+
<div
|
|
339
|
+
className="mb-0.5 text-xs font-black uppercase tracking-widest md:text-xs"
|
|
340
|
+
style={{ color: `rgba(${accent.rgb},0.6)` }}
|
|
341
|
+
>
|
|
342
|
+
{card.number} / {String(totalCards).padStart(2, '0')}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{card.title && (
|
|
347
|
+
<h3 className="text-xl font-extrabold tracking-tight text-stone-50/80 md:text-2xl lg:text-3xl">
|
|
348
|
+
{card.title}
|
|
349
|
+
</h3>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{(card.statValue || card.statLabel) && (
|
|
355
|
+
<div className="hidden flex-col items-end md:flex">
|
|
356
|
+
{card.statValue && (
|
|
357
|
+
<span
|
|
358
|
+
className="text-4xl font-black leading-none"
|
|
359
|
+
style={{ color: accent.hex }}
|
|
360
|
+
>
|
|
361
|
+
{card.statValue}
|
|
362
|
+
</span>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{card.statLabel && (
|
|
366
|
+
<span
|
|
367
|
+
className="mt-1 text-xs"
|
|
368
|
+
style={{ color: 'rgba(255,255,255,0.3)' }}
|
|
369
|
+
>
|
|
370
|
+
{card.statLabel}
|
|
371
|
+
</span>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{card.content ? (
|
|
378
|
+
<RichText
|
|
379
|
+
data={card.content as any}
|
|
380
|
+
enableProse={false}
|
|
381
|
+
enableGutter={false}
|
|
382
|
+
className="max-w-xl text-sm leading-relaxed text-stone-200 dark:text-stone-200 md:text-base"
|
|
383
|
+
style={{ color: 'rgba(255,255,255,0.45)' }}
|
|
384
|
+
/>
|
|
385
|
+
) : null}
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div className="mt-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center md:mt-8">
|
|
389
|
+
{card.tags && card.tags.length > 0 && (
|
|
390
|
+
<div className="flex flex-wrap gap-2">
|
|
391
|
+
{card.tags.map((tagItem, ti) => (
|
|
392
|
+
<span
|
|
393
|
+
key={tagItem.id ?? ti}
|
|
394
|
+
className="rounded-full px-3 py-1 text-xs font-semibold"
|
|
395
|
+
style={{
|
|
396
|
+
background: `rgba(${accent.rgb},0.07)`,
|
|
397
|
+
border: `1px solid rgba(${accent.rgb},0.18)`,
|
|
398
|
+
color: accent.hex,
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
{tagItem.tag}
|
|
402
|
+
</span>
|
|
403
|
+
))}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{card.ctaLabel && (
|
|
408
|
+
<a
|
|
409
|
+
href={card.ctaHref ?? '#'}
|
|
410
|
+
className="group flex cursor-pointer items-center gap-2 whitespace-nowrap text-xs font-black uppercase tracking-widest"
|
|
411
|
+
style={{ color: accent.hex }}
|
|
412
|
+
>
|
|
413
|
+
{card.ctaLabel}
|
|
414
|
+
<ArrowRight
|
|
415
|
+
size={14}
|
|
416
|
+
className="transition-transform group-hover:translate-x-1"
|
|
417
|
+
style={{ color: accent.hex }}
|
|
418
|
+
/>
|
|
419
|
+
</a>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div
|
|
425
|
+
className="relative flex w-22 items-center justify-center overflow-hidden sm:w-32 md:w-48 lg:w-64 xl:w-80"
|
|
426
|
+
style={{
|
|
427
|
+
borderLeft: `1px solid rgba(${accent.rgb},0.08)`,
|
|
428
|
+
background: `linear-gradient(135deg, rgba(${accent.rgb},0.04) 0%, transparent 100%)`,
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
<div
|
|
432
|
+
className="absolute inset-0"
|
|
433
|
+
style={{
|
|
434
|
+
background: `radial-gradient(ellipse at 50% 50%, rgba(${accent.rgb},0.25) 0%, transparent 70%)`,
|
|
435
|
+
}}
|
|
436
|
+
/>
|
|
437
|
+
|
|
438
|
+
{isLocalImageSrc(cardMedia?.url) ? (
|
|
439
|
+
<Image
|
|
440
|
+
src={cardMedia.url}
|
|
441
|
+
alt={cardMedia.alt ?? card.title ?? ''}
|
|
442
|
+
fill
|
|
443
|
+
className="relative z-10 object-cover opacity-80"
|
|
444
|
+
sizes="(max-width: 640px) 88px, (max-width: 768px) 128px, (max-width: 1280px) 192px, 320px"
|
|
445
|
+
/>
|
|
446
|
+
) : (
|
|
447
|
+
<>
|
|
448
|
+
<span className="relative z-10" style={{ color: `rgba(${accent.rgb},1)` }}>
|
|
449
|
+
<DynamicIcon className="shrink-0" name={card.iconName} size={96} />
|
|
450
|
+
</span>
|
|
451
|
+
|
|
452
|
+
{card.number && (
|
|
453
|
+
<div
|
|
454
|
+
className="absolute bottom-4 right-6 select-none font-black leading-none"
|
|
455
|
+
style={{
|
|
456
|
+
fontSize: '80px',
|
|
457
|
+
color: `rgba(${accent.rgb},0.2)`,
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{card.number}
|
|
461
|
+
</div>
|
|
462
|
+
)}
|
|
463
|
+
</>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</section>
|
|
469
|
+
)
|
|
470
|
+
})}
|
|
471
|
+
</section>
|
|
472
|
+
)
|
|
473
|
+
}
|