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.
Files changed (46) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/templates/blocks/DD-BookCall/Component.tsx +535 -534
  6. package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
  7. package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
  8. package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
  9. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  10. package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
  11. package/templates/blocks/DD-Contact/Component.tsx +8 -7
  12. package/templates/blocks/DD-Counter/Component.tsx +10 -10
  13. package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
  14. package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
  15. package/templates/blocks/DD-Hero/Component.tsx +297 -320
  16. package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
  17. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  18. package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
  19. package/templates/blocks/DD-Pricing/Component.tsx +369 -372
  20. package/templates/blocks/DD-Process/Component.tsx +147 -149
  21. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  22. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  23. package/templates/blocks/DD-Section/Component.tsx +1 -1
  24. package/templates/blocks/DD-Services/Component.tsx +1 -1
  25. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
  26. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  27. package/templates/blocks/DD-StackCards/Component.tsx +473 -473
  28. package/templates/blocks/DD-Team/Component.tsx +1 -1
  29. package/templates/blocks/DD-TechStack/Component.tsx +22 -28
  30. package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
  31. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  32. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  33. package/templates/blocks/DD-Work/Component.tsx +4 -4
  34. package/templates/blocks/DD-Work-B/Component.tsx +1 -1
  35. package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
  36. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
  37. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
  38. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  39. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
  40. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  41. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  42. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
  43. package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
  44. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  45. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  46. 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 !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
- <div
304
- className="relative mx-auto w-full max-w-5xl overflow-hidden rounded-2xl"
305
- style={{
306
- background: 'rgb(8 8 10 / 90%)',
307
- border: '1px solid rgba(255,255,255,0.07)',
308
- backdropFilter: 'blur(8px)',
309
- }}
310
- >
311
- <div
312
- className="absolute left-0 right-0 top-0 h-px"
313
- style={{
314
- background: `linear-gradient(90deg, transparent, rgba(${accent.rgb},0.5), transparent)`,
315
- }}
316
- />
317
-
318
- <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]">
319
- <div className="stackcard-bg flex flex-col justify-between p-6 md:p-10 lg:p-14">
320
- <div>
321
- <div className="mb-6 flex items-start justify-between md:mb-8">
322
- <div className="flex items-center gap-3 md:gap-4">
323
- <div
324
- className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl md:h-12 md:w-12"
325
- style={{
326
- background: `rgba(${accent.rgb},0.1)`,
327
- border: `1px solid rgba(${accent.rgb},0.25)`,
328
- }}
329
- >
330
- <span style={{ color: accent.hex }}>
331
- <DynamicIcon className="shrink-0" name={card.iconName} size={20} />
332
- </span>
333
- </div>
334
-
335
- <div>
336
- {card.number && (
337
- <div
338
- className="mb-0.5 text-[10px] font-black uppercase tracking-widest md:text-xs"
339
- style={{ color: `rgba(${accent.rgb},0.6)` }}
340
- >
341
- {card.number} / {String(totalCards).padStart(2, '0')}
342
- </div>
343
- )}
344
-
345
- {card.title && (
346
- <h3 className="text-xl font-extrabold tracking-tight text-stone-50/80 md:text-2xl lg:text-3xl">
347
- {card.title}
348
- </h3>
349
- )}
350
- </div>
351
- </div>
352
-
353
- {(card.statValue || card.statLabel) && (
354
- <div className="hidden flex-col items-end md:flex">
355
- {card.statValue && (
356
- <span
357
- className="text-4xl font-black leading-none"
358
- style={{ color: accent.hex }}
359
- >
360
- {card.statValue}
361
- </span>
362
- )}
363
-
364
- {card.statLabel && (
365
- <span
366
- className="mt-1 text-xs"
367
- style={{ color: 'rgba(255,255,255,0.3)' }}
368
- >
369
- {card.statLabel}
370
- </span>
371
- )}
372
- </div>
373
- )}
374
- </div>
375
-
376
- {card.content ? (
377
- <RichText
378
- data={card.content as any}
379
- enableProse={false}
380
- enableGutter={false}
381
- className="max-w-xl text-sm leading-relaxed text-stone-200 dark:text-stone-200 md:text-base"
382
- style={{ color: 'rgba(255,255,255,0.45)' }}
383
- />
384
- ) : null}
385
- </div>
386
-
387
- <div className="mt-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center md:mt-8">
388
- {card.tags && card.tags.length > 0 && (
389
- <div className="flex flex-wrap gap-2">
390
- {card.tags.map((tagItem, ti) => (
391
- <span
392
- key={tagItem.id ?? ti}
393
- className="rounded-full px-3 py-1 text-xs font-semibold"
394
- style={{
395
- background: `rgba(${accent.rgb},0.07)`,
396
- border: `1px solid rgba(${accent.rgb},0.18)`,
397
- color: accent.hex,
398
- }}
399
- >
400
- {tagItem.tag}
401
- </span>
402
- ))}
403
- </div>
404
- )}
405
-
406
- {card.ctaLabel && (
407
- <a
408
- href={card.ctaHref ?? '#'}
409
- className="group flex cursor-pointer items-center gap-2 whitespace-nowrap text-xs font-black uppercase tracking-widest"
410
- style={{ color: accent.hex }}
411
- >
412
- {card.ctaLabel}
413
- <ArrowRight
414
- size={14}
415
- className="transition-transform group-hover:translate-x-1"
416
- style={{ color: accent.hex }}
417
- />
418
- </a>
419
- )}
420
- </div>
421
- </div>
422
-
423
- <div
424
- className="relative flex w-22 items-center justify-center overflow-hidden sm:w-32 md:w-48 lg:w-64 xl:w-80"
425
- style={{
426
- borderLeft: `1px solid rgba(${accent.rgb},0.08)`,
427
- background: `linear-gradient(135deg, rgba(${accent.rgb},0.04) 0%, transparent 100%)`,
428
- }}
429
- >
430
- <div
431
- className="absolute inset-0"
432
- style={{
433
- background: `radial-gradient(ellipse at 50% 50%, rgba(${accent.rgb},0.25) 0%, transparent 70%)`,
434
- }}
435
- />
436
-
437
- {isLocalImageSrc(cardMedia?.url) ? (
438
- <Image
439
- src={cardMedia.url}
440
- alt={cardMedia.alt ?? card.title ?? ''}
441
- fill
442
- className="relative z-10 object-cover opacity-80"
443
- sizes="(max-width: 640px) 88px, (max-width: 768px) 128px, (max-width: 1280px) 192px, 320px"
444
- />
445
- ) : (
446
- <>
447
- <span className="relative z-10" style={{ color: `rgba(${accent.rgb},1)` }}>
448
- <DynamicIcon className="shrink-0" name={card.iconName} size={96} />
449
- </span>
450
-
451
- {card.number && (
452
- <div
453
- className="absolute bottom-4 right-6 select-none font-black leading-none"
454
- style={{
455
- fontSize: '80px',
456
- color: `rgba(${accent.rgb},0.2)`,
457
- }}
458
- >
459
- {card.number}
460
- </div>
461
- )}
462
- </>
463
- )}
464
- </div>
465
- </div>
466
- </div>
467
- </section>
468
- )
469
- })}
470
- </section>
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
+ }