blocks-dusted 0.1.1 → 0.1.2

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 (50) hide show
  1. package/README.md +967 -3
  2. package/package.json +2 -2
  3. package/src/commands/add.js +143 -45
  4. package/src/commands/list.js +2 -2
  5. package/src/installers/checkRequirements.js +1 -0
  6. package/src/installers/copyTemplateFiles.js +23 -2
  7. package/src/installers/patchPayloadConfigCollections.js +92 -0
  8. package/src/installers/writeInstalledBlockReadme.js +17 -4
  9. package/src/registry/listTemplates.js +16 -7
  10. package/src/registry/loadTemplateManifest.js +20 -11
  11. package/src/registry/validateTemplateManifest.js +6 -5
  12. package/src/utils/paths.js +7 -0
  13. package/templates/blocks/DD-BookCall/Component.tsx +535 -573
  14. package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
  15. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
  16. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
  17. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
  18. package/templates/blocks/DD-Contact/Component.tsx +434 -439
  19. package/templates/blocks/DD-Contact/config.ts +8 -3
  20. package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
  21. package/templates/blocks/DD-FeatCat/config.ts +201 -204
  22. package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
  23. package/templates/blocks/DD-Hero/Component.tsx +297 -317
  24. package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
  25. package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
  26. package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
  27. package/templates/blocks/DD-Pricing/Component.tsx +372 -380
  28. package/templates/blocks/DD-Process/Component.tsx +149 -155
  29. package/templates/blocks/DD-Section/Component.tsx +266 -327
  30. package/templates/blocks/DD-Services/Component.tsx +176 -188
  31. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
  32. package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
  33. package/templates/blocks/DD-StackCards/Component.tsx +137 -160
  34. package/templates/blocks/DD-Team/Component.tsx +247 -265
  35. package/templates/blocks/DD-TechStack/Component.tsx +57 -72
  36. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  37. package/templates/blocks/DD-Testimonials/config.ts +66 -66
  38. package/templates/blocks/DD-Work/Component.tsx +315 -328
  39. package/templates/blocks/DD-Work-B/Component.tsx +294 -320
  40. package/templates/collections/Testimonials/README.md +11 -0
  41. package/templates/collections/Testimonials/Testimonials.ts +161 -0
  42. package/templates/collections/Testimonials/manifest.json +55 -0
  43. package/templates/components/RichText/README.md +13 -0
  44. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  45. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  46. package/templates/components/RichText/converter/index.tsx +18 -0
  47. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  48. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  49. package/templates/components/RichText/index.tsx +35 -0
  50. package/templates/components/RichText/manifest.json +80 -0
@@ -1,317 +1,307 @@
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({
38
- images,
39
- speed,
40
- initialOffset,
41
- priority = false,
42
- }: ScrollColumnProps) {
43
- const colRef = useRef<HTMLDivElement>(null);
44
- const posRef = useRef(initialOffset);
45
- const rafRef = useRef<number | null>(null);
46
- const [mounted, setMounted] = useState(false);
47
-
48
- const resolveSrc = (item: ColumnImage): string | null => {
49
- if (item.image && typeof item.image === "object") {
50
- const media = item.image as Media;
51
- const url = media.url;
52
-
53
- if (!url) return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null;
54
-
55
- return isLocalImageSrc(url) ? url : null;
56
- }
57
-
58
- return isLocalImageSrc(item.imageUrl) ? item.imageUrl : null;
59
- };
60
-
61
- const validImages = useMemo(() => {
62
- return (images ?? []).filter((item) => resolveSrc(item) !== null);
63
- }, [images]);
64
-
65
- useEffect(() => {
66
- setMounted(true);
67
- }, []);
68
-
69
- useEffect(() => {
70
- if (!mounted) return;
71
-
72
- const el = colRef.current;
73
- if (!el) return;
74
-
75
- const animate = () => {
76
- posRef.current -= speed;
77
-
78
- const totalHeight = el.scrollHeight / 2;
79
-
80
- if (totalHeight > 0 && Math.abs(posRef.current) >= totalHeight) {
81
- posRef.current = 0;
82
- }
83
-
84
- el.style.transform = `translateY(${posRef.current}px)`;
85
- rafRef.current = requestAnimationFrame(animate);
86
- };
87
-
88
- const timer = window.setTimeout(() => {
89
- rafRef.current = requestAnimationFrame(animate);
90
- }, 1200);
91
-
92
- return () => {
93
- window.clearTimeout(timer);
94
-
95
- if (rafRef.current) {
96
- cancelAnimationFrame(rafRef.current);
97
- rafRef.current = null;
98
- }
99
- };
100
- }, [mounted, speed]);
101
-
102
- if (validImages.length === 0) return null;
103
-
104
- const renderImages = mounted ? [...validImages, ...validImages] : validImages;
105
-
106
- return (
107
- <div className="flex flex-col gap-4 will-change-transform" ref={colRef}>
108
- {renderImages.map((item, i) => {
109
- const src = resolveSrc(item);
110
- if (!src) return null;
111
-
112
- const isClone = i >= validImages.length;
113
- const shouldPrioritize = Boolean(priority) && !isClone;
114
-
115
- return (
116
- <div
117
- key={`${item.id ?? "image"}-${i}`}
118
- className="relative aspect-4/3 w-full shrink-0 overflow-hidden rounded-lg border border-white/6"
119
- >
120
- <Image
121
- src={src}
122
- alt={item.alt ?? ""}
123
- fill
124
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
125
- className="object-cover object-top"
126
- priority={shouldPrioritize}
127
- loading="eager"
128
- // loading={shouldPrioritize ? 'eager' : 'lazy'}
129
- fetchPriority={shouldPrioritize ? "high" : "auto"}
130
- />
131
- </div>
132
- );
133
- })}
134
- </div>
135
- );
136
- }
137
-
138
- export const DDShowcaseGrid: React.FC<DDShowcaseGridProps> = ({
139
- columns,
140
- height = 520,
141
- columnHeight = 3200,
142
- sectionId,
143
- customCSS,
144
- priority = false,
145
- stickToTop = false,
146
- fadeOutOnScroll = false,
147
- }) => {
148
- const sectionHeight = height ?? 520;
149
- const colHeight = columnHeight ?? 3200;
150
- const shouldStick = Boolean(stickToTop);
151
- const shouldFade = Boolean(fadeOutOnScroll);
152
- const columnItems = (columns ?? []) as ShowcaseColumn[];
153
- const wrapperRef = useRef<HTMLDivElement | null>(null);
154
- const stickyRef = useRef<HTMLDivElement | null>(null);
155
- const fadeDistance = Math.max(1, sectionHeight);
156
-
157
- useEffect(() => {
158
- if (!shouldFade) {
159
- if (stickyRef.current) stickyRef.current.style.opacity = "1";
160
- return;
161
- }
162
-
163
- let rafId = 0;
164
-
165
- const updateOpacity = () => {
166
- if (rafId) window.cancelAnimationFrame(rafId);
167
-
168
- rafId = window.requestAnimationFrame(() => {
169
- const wrapper = wrapperRef.current;
170
- const sticky = stickyRef.current;
171
-
172
- if (!wrapper || !sticky) return;
173
-
174
- const rect = wrapper.getBoundingClientRect();
175
- const progress = Math.min(1, Math.max(0, -rect.top / fadeDistance));
176
-
177
- sticky.style.opacity = String(1 - progress);
178
- });
179
- };
180
-
181
- updateOpacity();
182
-
183
- window.addEventListener("scroll", updateOpacity, { passive: true });
184
- window.addEventListener("resize", updateOpacity);
185
- window.addEventListener("locomotive:scroll", updateOpacity);
186
- window.addEventListener("locomotive:resize", updateOpacity);
187
-
188
- return () => {
189
- window.removeEventListener("scroll", updateOpacity);
190
- window.removeEventListener("resize", updateOpacity);
191
- window.removeEventListener("locomotive:scroll", updateOpacity);
192
- window.removeEventListener("locomotive:resize", updateOpacity);
193
-
194
- if (rafId) {
195
- window.cancelAnimationFrame(rafId);
196
- }
197
- };
198
- }, [fadeDistance, shouldFade]);
199
-
200
- return (
201
- <>
202
- <style>{`
203
- .studio-showcase-grid {
204
- --sg-perspective: 900px;
205
- --sg-perspective-origin: 50% 40%;
206
- --sg-transform: rotateX(52deg) rotateZ(-42deg);
207
- --sg-top: -160%;
208
- --sg-bottom: -90%;
209
- --sg-sides: -30%;
210
- }
211
-
212
- @media (max-width: 1023px) {
213
- .studio-showcase-grid {
214
- --sg-perspective-origin: 50% 40%;
215
- --sg-transform: rotateX(52deg) rotateZ(-42deg);
216
- --sg-top: -90%;
217
- --sg-bottom: -30%;
218
- --sg-sides: -50%;
219
- max-height: 420px;
220
- }
221
- }
222
-
223
- @media (max-width: 639px) {
224
- .studio-showcase-grid {
225
- --sg-perspective-origin: 50% 40%;
226
- --sg-transform: rotateX(52deg) rotateZ(-42deg);
227
- --sg-top: -60%;
228
- --sg-bottom: 0%;
229
- --sg-sides: -90%;
230
- max-height: 300px;
231
- }
232
- }
233
- `}</style>
234
-
235
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
236
-
237
- <div
238
- data-content-type="ddShowcaseGrid"
239
- ref={wrapperRef}
240
- className={cn(
241
- "relative overflow-clip",
242
- shouldStick || shouldFade ? "z-0" : "",
243
- )}
244
- style={{
245
- height:
246
- shouldStick || shouldFade
247
- ? `${sectionHeight + fadeDistance}px`
248
- : undefined,
249
- }}
250
- >
251
- <div
252
- ref={stickyRef}
253
- className={cn(
254
- "relative will-change-opacity",
255
- shouldStick || shouldFade ? "sticky top-0" : "",
256
- )}
257
- >
258
- <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" />
259
- <div
260
- id={sectionId ?? undefined}
261
- className="studio-showcase-grid relative z-0! overflow-hidden"
262
- style={{ height: `${sectionHeight}px` }}
263
- >
264
- <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]" />
265
- <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]" />
266
- <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]" />
267
- <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]" />
268
-
269
- <div
270
- className="absolute inset-0"
271
- style={{
272
- perspective: "var(--sg-perspective)",
273
- perspectiveOrigin: "var(--sg-perspective-origin)",
274
- }}
275
- >
276
- <div
277
- style={{
278
- transform: "var(--sg-transform)",
279
- transformOrigin: "center center",
280
- position: "absolute",
281
- top: "var(--sg-top)",
282
- left: "var(--sg-sides)",
283
- right: "var(--sg-sides)",
284
- bottom: "var(--sg-bottom)",
285
- display: "flex",
286
- gap: "16px",
287
- alignItems: "flex-start",
288
- }}
289
- >
290
- {columnItems.map((col, colIndex) => (
291
- <div
292
- key={col.id ?? colIndex}
293
- className="studio-showcase-col"
294
- style={{
295
- flex: "1",
296
- overflow: "hidden",
297
- height: `${colHeight}px`,
298
- }}
299
- >
300
- <ScrollColumn
301
- images={col.images ?? []}
302
- speed={col.speed ?? 0.5}
303
- initialOffset={col.initialOffset ?? 0}
304
- priority={Boolean(priority)}
305
- />
306
- </div>
307
- ))}
308
- </div>
309
- </div>
310
- </div>
311
- </div>
312
- </div>
313
- </>
314
- );
315
- };
316
-
317
- 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 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
+ #BlockName-DDShowcaseGrid
254
+ <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]" />
255
+ <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]" />
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