blocks-dusted 0.1.11 → 0.1.13

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 (161) 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/src/installers/patchRenderBlocks.js +1 -1
  6. package/src/registry/validateTemplateManifest.js +2 -1
  7. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  8. package/templates/blocks/BannerDD790432/config.ts +54 -0
  9. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  10. package/templates/blocks/Button47/Component.tsx +124 -0
  11. package/templates/blocks/Button47/config.ts +75 -0
  12. package/templates/blocks/Button47/link.ts +22 -0
  13. package/templates/blocks/Button47/manifest.json +105 -0
  14. package/templates/blocks/Button47/styles.module.scss +75 -0
  15. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  16. package/templates/blocks/DD-Accordion/config.ts +71 -59
  17. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  18. package/templates/blocks/DD-AnimText/config.ts +296 -295
  19. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  20. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  21. package/templates/blocks/DD-BookCall/Component.tsx +10 -9
  22. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  23. package/templates/blocks/DD-Card/Component.tsx +4 -4
  24. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
  25. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  28. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  29. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  30. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  31. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  32. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  33. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  34. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  35. package/templates/blocks/DD-Contact/Component.tsx +433 -434
  36. package/templates/blocks/DD-Counter/Component.tsx +18 -13
  37. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  38. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  39. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  40. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  41. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  42. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  43. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  44. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  45. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  46. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  47. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  48. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  49. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  50. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  51. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  52. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  53. package/templates/blocks/DD-Hero/Component.tsx +16 -40
  54. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  55. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  56. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  57. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  58. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
  59. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  60. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  61. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  62. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  63. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  64. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  65. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  66. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
  67. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  68. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  69. package/templates/blocks/DD-Masonry/config.ts +475 -475
  70. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  71. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  72. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  73. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  74. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  75. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  76. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  77. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  78. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  79. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  80. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  81. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  82. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  83. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  84. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  85. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  86. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  87. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  88. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  89. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  90. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  91. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  92. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  95. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  96. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  97. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  98. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  99. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  100. package/templates/blocks/DD-Pricing/Component.tsx +368 -372
  101. package/templates/blocks/DD-Pricing/config.ts +215 -215
  102. package/templates/blocks/DD-Process/Component.tsx +147 -149
  103. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  104. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  105. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  106. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  107. package/templates/blocks/DD-Section/Component.tsx +265 -266
  108. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  109. package/templates/blocks/DD-Services/Component.tsx +176 -176
  110. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  111. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  112. package/templates/blocks/DD-StackCards/Component.tsx +474 -473
  113. package/templates/blocks/DD-Team/Component.tsx +247 -247
  114. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  115. package/templates/blocks/DD-TechStack/Component.tsx +161 -168
  116. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  117. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  118. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  119. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  120. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  121. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  122. package/templates/blocks/DD-Work/Component.tsx +315 -315
  123. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  124. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  125. package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
  126. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
  127. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  128. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
  129. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  130. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  131. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
  132. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  133. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  134. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  135. package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
  136. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  137. package/templates/blocks/Form/Component.tsx +163 -0
  138. package/templates/blocks/Form/Country/index.tsx +65 -0
  139. package/templates/blocks/Form/Country/options.ts +982 -0
  140. package/templates/blocks/Form/Email/index.tsx +38 -0
  141. package/templates/blocks/Form/Error/index.tsx +15 -0
  142. package/templates/blocks/Form/Message/index.tsx +13 -0
  143. package/templates/blocks/Form/Number/index.tsx +36 -0
  144. package/templates/blocks/Form/Select/index.tsx +63 -0
  145. package/templates/blocks/Form/State/index.tsx +64 -0
  146. package/templates/blocks/Form/State/options.ts +52 -0
  147. package/templates/blocks/Form/Text/index.tsx +32 -0
  148. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  149. package/templates/blocks/Form/Width/index.tsx +13 -0
  150. package/templates/blocks/Form/config.ts +51 -0
  151. package/templates/blocks/Form/fields.tsx +21 -0
  152. package/templates/blocks/Form/manifest.json +168 -0
  153. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  154. package/templates/blocks/MediaBlock/config.ts +14 -0
  155. package/templates/blocks/MediaBlock/manifest.json +92 -0
  156. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  157. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  158. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  159. package/templates/blocks/rawHtml/Component.tsx +25 -0
  160. package/templates/blocks/rawHtml/config.ts +56 -0
  161. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -1,307 +1,306 @@
1
- 'use client'
2
-
3
- import Image from 'next/image'
4
- import React, { useEffect, useMemo, useRef, useState } from 'react'
5
- import { cn } from '@/utilities/ui'
6
- type Media = { url?: string | null; alt?: string | null; [key: string]: any }
7
-
8
- export type DDShowcaseGridProps = Record<string, any> & {
9
- stickToTop?: boolean | null
10
- fadeOutOnScroll?: boolean | null
11
- }
12
-
13
- type ColumnImage = {
14
- image?: Media | string | null
15
- imageUrl?: string | null
16
- [key: string]: any
17
- }
18
-
19
- type ShowcaseColumn = {
20
- id?: string | null
21
- images?: ColumnImage[] | null
22
- speed?: number | null
23
- initialOffset?: number | null
24
- }
25
-
26
- interface ScrollColumnProps {
27
- images: ColumnImage[]
28
- speed: number
29
- initialOffset: number
30
- priority?: boolean
31
- }
32
-
33
- function isLocalImageSrc(src?: string | null): src is string {
34
- return typeof src === 'string' && (src.startsWith('/api/media/file/') || src.startsWith('/assets/') || src.startsWith('/logo') || src.startsWith('data:'))
35
- }
36
-
37
- function ScrollColumn({ images, speed, initialOffset, priority = false }: ScrollColumnProps) {
38
- const colRef = useRef<HTMLDivElement>(null)
39
- const posRef = useRef(initialOffset)
40
- const rafRef = useRef<number | null>(null)
41
- const [mounted, setMounted] = useState(false)
42
-
43
- const resolveSrc = (item: ColumnImage): string | null => {
44
- if (item.image && typeof item.image === 'object') {
45
- const media = item.image as Media
46
- const url = media.url
47
-
48
- if (!url) return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null
49
-
50
- return isLocalImageSrc(url) ? url : null
51
- }
52
-
53
- return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null
54
- }
55
-
56
- const validImages = useMemo(() => {
57
- return (images ?? []).filter((item) => resolveSrc(item) !== null)
58
- }, [images])
59
-
60
- useEffect(() => {
61
- setMounted(true)
62
- }, [])
63
-
64
- useEffect(() => {
65
- if (!mounted) return
66
-
67
- const el = colRef.current
68
- if (!el) return
69
-
70
- const animate = () => {
71
- posRef.current -= speed
72
-
73
- const totalHeight = el.scrollHeight / 2
74
-
75
- if (totalHeight > 0 && Math.abs(posRef.current) >= totalHeight) {
76
- posRef.current = 0
77
- }
78
-
79
- el.style.transform = `translateY(${posRef.current}px)`
80
- rafRef.current = requestAnimationFrame(animate)
81
- }
82
-
83
- const timer = window.setTimeout(() => {
84
- rafRef.current = requestAnimationFrame(animate)
85
- }, 1200)
86
-
87
- return () => {
88
- window.clearTimeout(timer)
89
-
90
- if (rafRef.current) {
91
- cancelAnimationFrame(rafRef.current)
92
- rafRef.current = null
93
- }
94
- }
95
- }, [mounted, speed])
96
-
97
- if (validImages.length === 0) return null
98
-
99
- const renderImages = mounted ? [...validImages, ...validImages] : validImages
100
-
101
- return (
102
- <div className="flex flex-col gap-4 will-change-transform" ref={colRef}>
103
- {renderImages.map((item, i) => {
104
- const src = resolveSrc(item)
105
- if (!src) return null
106
-
107
- const isClone = i >= validImages.length
108
- const shouldPrioritize = Boolean(priority) && !isClone
109
-
110
- return (
111
- <div
112
- key={`${item.id ?? 'image'}-${i}`}
113
- className="relative aspect-4/3 w-full shrink-0 overflow-hidden rounded-lg border border-white/6"
114
- >
115
- <Image
116
- src={src}
117
- alt={item.alt ?? ''}
118
- fill
119
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
120
- className="object-cover object-top"
121
- priority={shouldPrioritize}
122
- loading="eager"
123
- // loading={shouldPrioritize ? 'eager' : 'lazy'}
124
- fetchPriority={shouldPrioritize ? 'high' : 'auto'}
125
- />
126
- </div>
127
- )
128
- })}
129
- </div>
130
- )
131
- }
132
-
133
- export const DDShowcaseGrid: React.FC<DDShowcaseGridProps> = ({
134
- columns,
135
- height = 520,
136
- columnHeight = 3200,
137
- sectionId,
138
- customCSS,
139
- priority = false,
140
- stickToTop = false,
141
- fadeOutOnScroll = false,
142
- }) => {
143
- const sectionHeight = height ?? 520
144
- const colHeight = columnHeight ?? 3200
145
- const shouldStick = Boolean(stickToTop)
146
- const shouldFade = Boolean(fadeOutOnScroll)
147
- const columnItems = (columns ?? []) as ShowcaseColumn[]
148
- const wrapperRef = useRef<HTMLDivElement | null>(null)
149
- const stickyRef = useRef<HTMLDivElement | null>(null)
150
- const fadeDistance = Math.max(1, sectionHeight)
151
-
152
- useEffect(() => {
153
- if (!shouldFade) {
154
- if (stickyRef.current) stickyRef.current.style.opacity = '1'
155
- return
156
- }
157
-
158
- let rafId = 0
159
-
160
- const updateOpacity = () => {
161
- if (rafId) window.cancelAnimationFrame(rafId)
162
-
163
- rafId = window.requestAnimationFrame(() => {
164
- const wrapper = wrapperRef.current
165
- const sticky = stickyRef.current
166
-
167
- if (!wrapper || !sticky) return
168
-
169
- const rect = wrapper.getBoundingClientRect()
170
- const progress = Math.min(1, Math.max(0, -rect.top / fadeDistance))
171
-
172
- sticky.style.opacity = String(1 - progress)
173
- })
174
- }
175
-
176
- updateOpacity()
177
-
178
- window.addEventListener('scroll', updateOpacity, { passive: true })
179
- window.addEventListener('resize', updateOpacity)
180
- window.addEventListener('locomotive:scroll', updateOpacity)
181
- window.addEventListener('locomotive:resize', updateOpacity)
182
-
183
- return () => {
184
- window.removeEventListener('scroll', updateOpacity)
185
- window.removeEventListener('resize', updateOpacity)
186
- window.removeEventListener('locomotive:scroll', updateOpacity)
187
- window.removeEventListener('locomotive:resize', updateOpacity)
188
-
189
- if (rafId) {
190
- window.cancelAnimationFrame(rafId)
191
- }
192
- }
193
- }, [fadeDistance, shouldFade])
194
-
195
- return (
196
- <>
197
- <style>{`
198
- .studio-showcase-grid {
199
- --sg-perspective: 900px;
200
- --sg-perspective-origin: 50% 40%;
201
- --sg-transform: rotateX(46deg) rotateZ(-34deg);
202
- --sg-top: -80%;
203
- --sg-bottom: -80%;
204
- --sg-sides: -18%;
205
- }
206
-
207
- @media (max-width: 1023px) {
208
- .studio-showcase-grid {
209
- --sg-perspective-origin: 50% 40%;
210
- --sg-transform: rotateX(52deg) rotateZ(-42deg);
211
- --sg-top: -35%;
212
- --sg-bottom: -35%;
213
- --sg-sides: -25%;
214
- max-height: 420px;
215
- }
216
- }
217
-
218
- @media (max-width: 639px) {
219
- .studio-showcase-grid {
220
- --sg-perspective-origin: 50% 40%;
221
- --sg-transform: rotateX(52deg) rotateZ(-42deg);
222
- --sg-top: -60%;
223
- --sg-bottom: -20%;
224
- --sg-sides: -20%;
225
- max-height: 300px;
226
- }
227
- }
228
- `}</style>
229
-
230
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
231
-
232
- <div
233
- data-content-type="ddShowcaseGrid"
234
- ref={wrapperRef}
235
- className={cn('relative overflow-clip', shouldStick || shouldFade ? 'z-0' : '')}
236
- style={{
237
- height: shouldStick || shouldFade ? `${sectionHeight + fadeDistance}px` : undefined,
238
- }}
239
- >
240
- <div
241
- ref={stickyRef}
242
- className={cn(
243
- 'relative will-change-opacity',
244
- shouldStick || shouldFade ? 'sticky top-0' : '',
245
- )}
246
- >
247
- <div className="pointer-events-none absolute left-0 right-0 top-0 bottom-0 z-10 h-[150vh] bg-[linear-gradient(0deg,#ffffff,#ffffff,#ffffff,#ffffff,transparent)] dark:bg-[linear-gradient(0deg,#0a0a0a,#0a0a0a,#0a0a0a,#0a0a0a,transparent)] opacity-80 mix-blend-color-dodge" />
248
- <div
249
- id={sectionId ?? undefined}
250
- className="studio-showcase-grid relative z-0! overflow-hidden"
251
- style={{ height: `${sectionHeight}px` }}
252
- >
253
-
254
- <div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-32 bg-linear-to-b from-white to-transparent dark:from-[#0a0a0a]" />
255
- <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-32 bg-linear-to-t from-white to-transparent dark:from-[#0a0a0a]" />
256
- <div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-40 bg-linear-to-r from-white to-transparent dark:from-[#0a0a0a]" />
257
- <div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-40 bg-linear-to-l from-white to-transparent dark:from-[#0a0a0a]" />
258
-
259
- <div
260
- className="absolute inset-0"
261
- style={{
262
- perspective: 'var(--sg-perspective)',
263
- perspectiveOrigin: 'var(--sg-perspective-origin)',
264
- }}
265
- >
266
- <div
267
- style={{
268
- transform: 'var(--sg-transform)',
269
- transformOrigin: 'center center',
270
- position: 'absolute',
271
- top: 'var(--sg-top)',
272
- left: 'var(--sg-sides)',
273
- right: 'var(--sg-sides)',
274
- bottom: 'var(--sg-bottom)',
275
- display: 'flex',
276
- gap: '16px',
277
- alignItems: 'flex-start',
278
- }}
279
- >
280
- {columnItems.map((col, colIndex) => (
281
- <div
282
- key={col.id ?? colIndex}
283
- className="studio-showcase-col"
284
- style={{
285
- flex: '1',
286
- overflow: 'hidden',
287
- height: `${colHeight}px`,
288
- }}
289
- >
290
- <ScrollColumn
291
- images={col.images ?? []}
292
- speed={col.speed ?? 0.5}
293
- initialOffset={col.initialOffset ?? 0}
294
- priority={Boolean(priority)}
295
- />
296
- </div>
297
- ))}
298
- </div>
299
- </div>
300
- </div>
301
- </div>
302
- </div>
303
- </>
304
- )
305
- }
306
-
307
- export default DDShowcaseGrid
1
+ 'use client'
2
+
3
+ import Image from 'next/image'
4
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
5
+ import { cn } from '@/utilities/ui'
6
+ type Media = { url?: string | null; alt?: string | null; [key: string]: any }
7
+
8
+ export type DDShowcaseGridProps = Record<string, any> & {
9
+ stickToTop?: boolean | null
10
+ fadeOutOnScroll?: boolean | null
11
+ }
12
+
13
+ type ColumnImage = {
14
+ image?: Media | string | null
15
+ imageUrl?: string | null
16
+ [key: string]: any
17
+ }
18
+
19
+ type ShowcaseColumn = {
20
+ id?: string | null
21
+ images?: ColumnImage[] | null
22
+ speed?: number | null
23
+ initialOffset?: number | null
24
+ }
25
+
26
+ interface ScrollColumnProps {
27
+ images: ColumnImage[]
28
+ speed: number
29
+ initialOffset: number
30
+ priority?: boolean
31
+ }
32
+
33
+ function isLocalImageSrc(src?: string | null): src is string {
34
+ return typeof src === 'string' && src.startsWith('/api/media/file/')
35
+ }
36
+
37
+ function ScrollColumn({ images, speed, initialOffset, priority = false }: ScrollColumnProps) {
38
+ const colRef = useRef<HTMLDivElement>(null)
39
+ const posRef = useRef(initialOffset)
40
+ const rafRef = useRef<number | null>(null)
41
+ const [mounted, setMounted] = useState(false)
42
+
43
+ const resolveSrc = (item: ColumnImage): string | null => {
44
+ if (item.image && typeof item.image === 'object') {
45
+ const media = item.image as Media
46
+ const url = media.url
47
+
48
+ if (!url) return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null
49
+
50
+ return isLocalImageSrc(url) ? url : null
51
+ }
52
+
53
+ return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null
54
+ }
55
+
56
+ const validImages = useMemo(() => {
57
+ return (images ?? []).filter((item) => resolveSrc(item) !== null)
58
+ }, [images])
59
+
60
+ useEffect(() => {
61
+ setMounted(true)
62
+ }, [])
63
+
64
+ useEffect(() => {
65
+ if (!mounted) return
66
+
67
+ const el = colRef.current
68
+ if (!el) return
69
+
70
+ const animate = () => {
71
+ posRef.current -= speed
72
+
73
+ const totalHeight = el.scrollHeight / 2
74
+
75
+ if (totalHeight > 0 && Math.abs(posRef.current) >= totalHeight) {
76
+ posRef.current = 0
77
+ }
78
+
79
+ el.style.transform = `translateY(${posRef.current}px)`
80
+ rafRef.current = requestAnimationFrame(animate)
81
+ }
82
+
83
+ const timer = window.setTimeout(() => {
84
+ rafRef.current = requestAnimationFrame(animate)
85
+ }, 1200)
86
+
87
+ return () => {
88
+ window.clearTimeout(timer)
89
+
90
+ if (rafRef.current) {
91
+ cancelAnimationFrame(rafRef.current)
92
+ rafRef.current = null
93
+ }
94
+ }
95
+ }, [mounted, speed])
96
+
97
+ if (validImages.length === 0) return null
98
+
99
+ const renderImages = mounted ? [...validImages, ...validImages] : validImages
100
+
101
+ return (
102
+ <div className="flex flex-col gap-4 will-change-transform" ref={colRef}>
103
+ {renderImages.map((item, i) => {
104
+ const src = resolveSrc(item)
105
+ if (!src) return null
106
+
107
+ const isClone = i >= validImages.length
108
+ const shouldPrioritize = Boolean(priority) && !isClone
109
+
110
+ return (
111
+ <div
112
+ key={`${item.id ?? 'image'}-${i}`}
113
+ className="relative aspect-4/3 w-full shrink-0 overflow-hidden rounded-lg border border-white/6"
114
+ >
115
+ <Image
116
+ src={src}
117
+ alt={item.alt ?? ''}
118
+ fill
119
+ sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
120
+ className="object-cover object-top"
121
+ priority={shouldPrioritize}
122
+ loading="eager"
123
+ // loading={shouldPrioritize ? 'eager' : 'lazy'}
124
+ fetchPriority={shouldPrioritize ? 'high' : 'auto'}
125
+ />
126
+ </div>
127
+ )
128
+ })}
129
+ </div>
130
+ )
131
+ }
132
+
133
+ export const DDShowcaseGrid: React.FC<DDShowcaseGridProps> = ({
134
+ columns,
135
+ height = 520,
136
+ columnHeight = 3200,
137
+ sectionId,
138
+ customCSS,
139
+ priority = false,
140
+ stickToTop = false,
141
+ fadeOutOnScroll = false,
142
+ }) => {
143
+ const sectionHeight = height ?? 520
144
+ const colHeight = columnHeight ?? 3200
145
+ const shouldStick = Boolean(stickToTop)
146
+ const shouldFade = Boolean(fadeOutOnScroll)
147
+ const columnItems = (columns ?? []) as ShowcaseColumn[]
148
+ const wrapperRef = useRef<HTMLDivElement | null>(null)
149
+ const stickyRef = useRef<HTMLDivElement | null>(null)
150
+ const fadeDistance = Math.max(1, sectionHeight)
151
+
152
+ useEffect(() => {
153
+ if (!shouldFade) {
154
+ if (stickyRef.current) stickyRef.current.style.opacity = '1'
155
+ return
156
+ }
157
+
158
+ let rafId = 0
159
+
160
+ const updateOpacity = () => {
161
+ if (rafId) window.cancelAnimationFrame(rafId)
162
+
163
+ rafId = window.requestAnimationFrame(() => {
164
+ const wrapper = wrapperRef.current
165
+ const sticky = stickyRef.current
166
+
167
+ if (!wrapper || !sticky) return
168
+
169
+ const rect = wrapper.getBoundingClientRect()
170
+ const progress = Math.min(1, Math.max(0, -rect.top / fadeDistance))
171
+
172
+ sticky.style.opacity = String(1 - progress)
173
+ })
174
+ }
175
+
176
+ updateOpacity()
177
+
178
+ window.addEventListener('scroll', updateOpacity, { passive: true })
179
+ window.addEventListener('resize', updateOpacity)
180
+ window.addEventListener('locomotive:scroll', updateOpacity)
181
+ window.addEventListener('locomotive:resize', updateOpacity)
182
+
183
+ return () => {
184
+ window.removeEventListener('scroll', updateOpacity)
185
+ window.removeEventListener('resize', updateOpacity)
186
+ window.removeEventListener('locomotive:scroll', updateOpacity)
187
+ window.removeEventListener('locomotive:resize', updateOpacity)
188
+
189
+ if (rafId) {
190
+ window.cancelAnimationFrame(rafId)
191
+ }
192
+ }
193
+ }, [fadeDistance, shouldFade])
194
+
195
+ return (
196
+ <>
197
+ <style>{`
198
+ .studio-showcase-grid {
199
+ --sg-perspective: 900px;
200
+ --sg-perspective-origin: 50% 40%;
201
+ --sg-transform: rotateX(52deg) rotateZ(-42deg);
202
+ --sg-top: -160%;
203
+ --sg-bottom: -90%;
204
+ --sg-sides: -30%;
205
+ }
206
+
207
+ @media (max-width: 1023px) {
208
+ .studio-showcase-grid {
209
+ --sg-perspective-origin: 50% 40%;
210
+ --sg-transform: rotateX(52deg) rotateZ(-42deg);
211
+ --sg-top: -90%;
212
+ --sg-bottom: -30%;
213
+ --sg-sides: -50%;
214
+ max-height: 420px;
215
+ }
216
+ }
217
+
218
+ @media (max-width: 639px) {
219
+ .studio-showcase-grid {
220
+ --sg-perspective-origin: 50% 40%;
221
+ --sg-transform: rotateX(52deg) rotateZ(-42deg);
222
+ --sg-top: -60%;
223
+ --sg-bottom: 0%;
224
+ --sg-sides: -90%;
225
+ max-height: 300px;
226
+ }
227
+ }
228
+ `}</style>
229
+
230
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
231
+
232
+ <div
233
+ data-content-type="ddShowcaseGrid"
234
+ ref={wrapperRef}
235
+ className={cn('relative w-full overflow-clip', shouldStick || shouldFade ? 'z-0' : '')}
236
+ style={{
237
+ height: shouldStick || shouldFade ? `${sectionHeight + fadeDistance}px` : undefined,
238
+ }}
239
+ >
240
+ <div
241
+ ref={stickyRef}
242
+ className={cn(
243
+ 'relative w-full will-change-opacity',
244
+ shouldStick || shouldFade ? 'sticky top-0' : '',
245
+ )}
246
+ >
247
+ <div className="pointer-events-none absolute left-0 right-0 top-0 bottom-0 z-10 h-[150vh] bg-[linear-gradient(0deg,#ffffff,#ffffff,#ffffff,#ffffff,transparent)] dark:bg-[linear-gradient(0deg,#0a0a0a,#0a0a0a,#0a0a0a,#0a0a0a,transparent)] opacity-80 mix-blend-color-dodge" />
248
+ <div
249
+ id={sectionId ?? undefined}
250
+ className="studio-showcase-grid relative z-0! w-full overflow-hidden"
251
+ style={{ height: `${sectionHeight}px` }}
252
+ >
253
+ <div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-80 bg-linear-to-b from-white to-transparent dark:from-[#0a0a0a]" />
254
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-80 bg-linear-to-t from-white to-transparent dark:from-[#0a0a0a]" />
255
+ <div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-40 bg-linear-to-r from-white to-transparent dark:from-[#0a0a0a]" />
256
+ <div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-40 bg-linear-to-l from-white to-transparent dark:from-[#0a0a0a]" />
257
+
258
+ <div
259
+ className="absolute inset-0"
260
+ style={{
261
+ perspective: 'var(--sg-perspective)',
262
+ perspectiveOrigin: 'var(--sg-perspective-origin)',
263
+ }}
264
+ >
265
+ <div
266
+ style={{
267
+ transform: 'var(--sg-transform)',
268
+ transformOrigin: 'center center',
269
+ position: 'absolute',
270
+ top: 'var(--sg-top)',
271
+ left: 'var(--sg-sides)',
272
+ right: 'var(--sg-sides)',
273
+ bottom: 'var(--sg-bottom)',
274
+ display: 'flex',
275
+ gap: '16px',
276
+ alignItems: 'flex-start',
277
+ }}
278
+ >
279
+ {columnItems.map((col, colIndex) => (
280
+ <div
281
+ key={col.id ?? colIndex}
282
+ className="studio-showcase-col"
283
+ style={{
284
+ flex: '1',
285
+ overflow: 'hidden',
286
+ height: `${colHeight}px`,
287
+ }}
288
+ >
289
+ <ScrollColumn
290
+ images={col.images ?? []}
291
+ speed={col.speed ?? 0.5}
292
+ initialOffset={col.initialOffset ?? 0}
293
+ priority={Boolean(priority)}
294
+ />
295
+ </div>
296
+ ))}
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </>
303
+ )
304
+ }
305
+
306
+ export default DDShowcaseGrid