blocks-dusted 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) 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/HeaderDD02/MobileBottomNav/index.tsx +545 -0
  44. package/templates/components/HeaderDD02/README.md +45 -0
  45. package/templates/components/HeaderDD02/RowLabel.tsx +33 -0
  46. package/templates/components/HeaderDD02/config.ts +302 -0
  47. package/templates/components/HeaderDD02/index.tsx +517 -0
  48. package/templates/components/HeaderDD02/manifest.json +123 -0
  49. package/templates/components/RichText/README.md +13 -0
  50. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  51. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  52. package/templates/components/RichText/converter/index.tsx +18 -0
  53. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  54. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  55. package/templates/components/RichText/index.tsx +35 -0
  56. package/templates/components/RichText/manifest.json +80 -0
@@ -1,127 +1,118 @@
1
- "use client";
1
+ 'use client'
2
2
 
3
- import React, { useEffect, useMemo, useRef } from "react";
4
- import Link from "next/link";
5
- import { motion, type Variants } from "framer-motion";
6
- import { cn } from "@/utilities/ui";
3
+ import React, { useEffect, useMemo, useRef } from 'react'
4
+ import Link from 'next/link'
5
+ import { motion, type Variants } from 'framer-motion'
6
+ import { cn } from '@/utilities/ui'
7
7
 
8
8
  type MediaLike = {
9
- url?: string | null;
10
- alt?: string | null;
11
- mimeType?: string | null;
12
- };
9
+ url?: string | null
10
+ alt?: string | null
11
+ mimeType?: string | null
12
+ }
13
13
 
14
14
  type ShowcaseItem = {
15
- id?: string | null;
16
- title?: string | null;
17
- category?: string | null;
18
- framework?: string | null;
19
- description?: string | null;
20
- href?: string | null;
21
- linkLabel?: string | null;
22
- free?: boolean | null;
23
- priceLabel?: string | null;
24
- aspectRatio?: "16/9" | "3/2" | "1/1" | "2/3" | string | null;
25
- image?: number | string | MediaLike | null;
26
- hoverVideo?: number | string | MediaLike | null;
27
- posterFallbackUrl?: string | null;
28
- videoFallbackUrl?: string | null;
29
- };
15
+ id?: string | null
16
+ title?: string | null
17
+ category?: string | null
18
+ framework?: string | null
19
+ description?: string | null
20
+ href?: string | null
21
+ linkLabel?: string | null
22
+ free?: boolean | null
23
+ priceLabel?: string | null
24
+ aspectRatio?: '16/9' | '3/2' | '1/1' | '2/3' | string | null
25
+ image?: number | string | MediaLike | null
26
+ hoverVideo?: number | string | MediaLike | null
27
+ posterFallbackUrl?: string | null
28
+ videoFallbackUrl?: string | null
29
+ }
30
30
 
31
31
  type DDMasonaryMediaProps = {
32
- eyebrow?: string | null;
33
- headlineMain?: string | null;
34
- headlineAccent?: string | null;
35
- subtext?: string | null;
36
- items?: ShowcaseItem[] | null;
37
- columnsDesktop?: number | null;
38
- columnsTablet?: number | null;
39
- gapClass?: string | null;
40
- containerClassName?: string | null;
41
- sectionClassName?: string | null;
42
- sectionId?: string | null;
43
- customCSS?: string | null;
44
- showCta?: boolean | null;
45
- ctaAlign?: "left" | "center" | "right" | null;
46
- ctaDisplayMode?: "iconWithLabel" | "iconOnly" | "labelOnly" | null;
47
- ctaIconPosition?: "before" | "after" | null;
48
- ctaButtonText?: string | null;
49
- ctaButtonStyle?: string | null;
50
- ctaSize?: "small" | "medium" | "large" | null;
51
- ctaIcon?: string | null;
52
- ctaLink?: any | null;
53
- ctaContainerClass?: string | null;
54
- };
55
-
56
- const getMediaUrl = (
57
- media?: number | string | MediaLike | null,
58
- fallback?: string | null,
59
- ) => {
60
- if (media && typeof media === "object" && typeof media.url === "string")
61
- return media.url;
62
- if (typeof media === "string") return media;
63
- return fallback || "";
64
- };
65
-
66
- const getMediaAlt = (
67
- media?: number | string | MediaLike | null,
68
- fallback = "",
69
- ) => {
70
- if (media && typeof media === "object" && typeof media.alt === "string")
71
- return media.alt;
72
- return fallback;
73
- };
32
+ eyebrow?: string | null
33
+ headlineMain?: string | null
34
+ headlineAccent?: string | null
35
+ subtext?: string | null
36
+ items?: ShowcaseItem[] | null
37
+ columnsDesktop?: number | null
38
+ columnsTablet?: number | null
39
+ gapClass?: string | null
40
+ containerClassName?: string | null
41
+ sectionClassName?: string | null
42
+ sectionId?: string | null
43
+ customCSS?: string | null
44
+ showCta?: boolean | null
45
+ ctaAlign?: 'left' | 'center' | 'right' | null
46
+ ctaDisplayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly' | null
47
+ ctaIconPosition?: 'before' | 'after' | null
48
+ ctaButtonText?: string | null
49
+ ctaButtonStyle?: string | null
50
+ ctaSize?: 'small' | 'medium' | 'large' | null
51
+ ctaIcon?: string | null
52
+ ctaLink?: any | null
53
+ ctaContainerClass?: string | null
54
+ }
55
+
56
+ const getMediaUrl = (media?: number | string | MediaLike | null, fallback?: string | null) => {
57
+ if (media && typeof media === 'object' && typeof media.url === 'string') return media.url
58
+ if (typeof media === 'string') return media
59
+ return fallback || ''
60
+ }
61
+
62
+ const getMediaAlt = (media?: number | string | MediaLike | null, fallback = '') => {
63
+ if (media && typeof media === 'object' && typeof media.alt === 'string') return media.alt
64
+ return fallback
65
+ }
74
66
 
75
67
  const getMediaMimeType = (media?: number | string | MediaLike | null) => {
76
- if (media && typeof media === "object" && typeof media.mimeType === "string")
77
- return media.mimeType;
78
- return "";
79
- };
68
+ if (media && typeof media === 'object' && typeof media.mimeType === 'string')
69
+ return media.mimeType
70
+ return ''
71
+ }
80
72
 
81
73
  const getFileExtension = (url: string) => {
82
- const clean = url.split("?")[0]?.split("#")[0] || "";
83
- return clean.split(".").pop()?.toLowerCase() || "";
84
- };
74
+ const clean = url.split('?')[0]?.split('#')[0] || ''
75
+ return clean.split('.').pop()?.toLowerCase() || ''
76
+ }
85
77
 
86
78
  const isVideoMedia = (url: string, mimeType?: string | null) => {
87
- const ext = getFileExtension(url);
79
+ const ext = getFileExtension(url)
88
80
 
89
81
  return Boolean(
90
- mimeType?.startsWith("video/") ||
91
- ext === "webm" ||
92
- ext === "mp4" ||
93
- ext === "mov" ||
94
- ext === "m4v",
95
- );
96
- };
97
-
98
- const isWideCard = (ratio?: string | null) =>
99
- ratio === "16/9" || ratio === "3/2";
100
- const isSquareCard = (ratio?: string | null) => ratio === "1/1";
82
+ mimeType?.startsWith('video/') ||
83
+ ext === 'webm' ||
84
+ ext === 'mp4' ||
85
+ ext === 'mov' ||
86
+ ext === 'm4v',
87
+ )
88
+ }
89
+
90
+ const isWideCard = (ratio?: string | null) => ratio === '16/9' || ratio === '3/2'
91
+ const isSquareCard = (ratio?: string | null) => ratio === '1/1'
101
92
 
102
93
  const getGapPx = (gapClass?: string | null) => {
103
- if (!gapClass) return 20;
104
- if (gapClass.includes("gap-0")) return 0;
105
- if (gapClass.includes("gap-1")) return 4;
106
- if (gapClass.includes("gap-2")) return 8;
107
- if (gapClass.includes("gap-3")) return 12;
108
- if (gapClass.includes("gap-4")) return 16;
109
- if (gapClass.includes("gap-5")) return 20;
110
- if (gapClass.includes("gap-6")) return 24;
111
- if (gapClass.includes("gap-8")) return 32;
112
- if (gapClass.includes("gap-10")) return 40;
113
- return 20;
114
- };
94
+ if (!gapClass) return 20
95
+ if (gapClass.includes('gap-0')) return 0
96
+ if (gapClass.includes('gap-1')) return 4
97
+ if (gapClass.includes('gap-2')) return 8
98
+ if (gapClass.includes('gap-3')) return 12
99
+ if (gapClass.includes('gap-4')) return 16
100
+ if (gapClass.includes('gap-5')) return 20
101
+ if (gapClass.includes('gap-6')) return 24
102
+ if (gapClass.includes('gap-8')) return 32
103
+ if (gapClass.includes('gap-10')) return 40
104
+ return 20
105
+ }
115
106
 
116
107
  const parseAspectRatio = (ratio?: string | null) => {
117
- if (!ratio || !ratio.includes("/")) return "1 / 1";
108
+ if (!ratio || !ratio.includes('/')) return '1 / 1'
118
109
 
119
- const [rawWidth, rawHeight] = ratio.split("/").map(Number);
120
- const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1;
121
- const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1;
110
+ const [rawWidth, rawHeight] = ratio.split('/').map(Number)
111
+ const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1
112
+ const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1
122
113
 
123
- return `${width} / ${height}`;
124
- };
114
+ return `${width} / ${height}`
115
+ }
125
116
 
126
117
  const containerVariants: Variants = {
127
118
  hidden: {},
@@ -131,78 +122,67 @@ const containerVariants: Variants = {
131
122
  delayChildren: 0.06,
132
123
  },
133
124
  },
134
- };
125
+ }
135
126
 
136
127
  const itemVariants: Variants = {
137
128
  hidden: {
138
129
  opacity: 0,
139
130
  y: 22,
140
- filter: "blur(6px)",
131
+ filter: 'blur(6px)',
141
132
  },
142
133
  show: {
143
134
  opacity: 1,
144
135
  y: 0,
145
- filter: "none",
136
+ filter: 'none',
146
137
  transition: {
147
138
  duration: 0.45,
148
- ease: "easeOut",
139
+ ease: 'easeOut',
149
140
  },
150
141
  },
151
- };
152
-
153
- function StudioMasonaryCard({
154
- item,
155
- index,
156
- }: {
157
- item: ShowcaseItem;
158
- index: number;
159
- }) {
160
- const videoRef = useRef<HTMLVideoElement | null>(null);
161
-
162
- const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl);
163
- const imageAlt = getMediaAlt(item.image, item.title || "Studio preview");
164
-
165
- const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl);
166
- const hoverMimeType = getMediaMimeType(item.hoverVideo);
167
- const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType);
168
- const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo);
169
-
170
- const href = item.href || "#";
171
- const aspectRatio = parseAspectRatio(item.aspectRatio || "16/9");
172
- const wide = isWideCard(item.aspectRatio);
173
- const square = isSquareCard(item.aspectRatio);
174
- const badgeLabel =
175
- item.category || (item.free ? "Free" : item.priceLabel || "");
142
+ }
143
+
144
+ function StudioMasonaryCard({ item, index }: { item: ShowcaseItem; index: number }) {
145
+ const videoRef = useRef<HTMLVideoElement | null>(null)
146
+
147
+ const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl)
148
+ const imageAlt = getMediaAlt(item.image, item.title || 'Studio preview')
149
+
150
+ const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl)
151
+ const hoverMimeType = getMediaMimeType(item.hoverVideo)
152
+ const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType)
153
+ const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo)
154
+
155
+ const href = item.href || '#'
156
+ const aspectRatio = parseAspectRatio(item.aspectRatio || '16/9')
157
+ const wide = isWideCard(item.aspectRatio)
158
+ const square = isSquareCard(item.aspectRatio)
159
+ const badgeLabel = item.category || (item.free ? 'Free' : item.priceLabel || '')
176
160
 
177
161
  const handleMouseEnter = () => {
178
- const video = videoRef.current;
179
- if (!video || !hoverIsVideo) return;
162
+ const video = videoRef.current
163
+ if (!video || !hoverIsVideo) return
180
164
 
181
- if (video.readyState === 0) video.load();
165
+ if (video.readyState === 0) video.load()
182
166
 
183
- video.currentTime = 0;
184
- const playPromise = video.play();
167
+ video.currentTime = 0
168
+ const playPromise = video.play()
185
169
 
186
- if (playPromise && typeof playPromise.catch === "function") {
187
- playPromise.catch(() => undefined);
170
+ if (playPromise && typeof playPromise.catch === 'function') {
171
+ playPromise.catch(() => undefined)
188
172
  }
189
- };
173
+ }
190
174
 
191
175
  const handleMouseLeave = () => {
192
- const video = videoRef.current;
193
- if (!video) return;
176
+ const video = videoRef.current
177
+ if (!video) return
194
178
 
195
- video.pause();
196
- video.currentTime = 0;
197
- };
179
+ video.pause()
180
+ video.currentTime = 0
181
+ }
198
182
 
199
183
  return (
200
184
  <article
201
- className={cn(
202
- "studio-masonry-item card-item",
203
- wide && "is-wide",
204
- square && "is-square",
205
- )}
185
+ className={cn('studio-masonry-item card-item', wide && 'is-wide', square && 'is-square')}
206
186
  data-index={index}
207
187
  onMouseEnter={handleMouseEnter}
208
188
  onMouseLeave={handleMouseLeave}
@@ -211,15 +191,11 @@ function StudioMasonaryCard({
211
191
  <article
212
192
  data-theme="dark"
213
193
  className="masonary-article-card outline-2 outline-stone-500/50 outline-solid group relative h-full w-full overflow-clip rounded-[3rem]! border border-border bg-[#55526b85]! backdrop-blur-xl! shadow-2xl transition-all duration-300 hover:cursor-pointer hover:border-lime-500 hover:shadow-2xl"
214
- style={
215
- { cornerShape: "squircle", aspectRatio } as React.CSSProperties
216
- }
194
+ style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
217
195
  >
218
196
  <div
219
197
  className="card-media relative w-full overflow-hidden rounded-[3rem]! border border-border bg-linear-to-br from-violet-950 to-indigo-950 shadow-2xl!"
220
- style={
221
- { cornerShape: "squircle", aspectRatio } as React.CSSProperties
222
- }
198
+ style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
223
199
  >
224
200
  {imageUrl ? (
225
201
  <img
@@ -228,10 +204,10 @@ function StudioMasonaryCard({
228
204
  loading="lazy"
229
205
  decoding="async"
230
206
  className={cn(
231
- "absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! transition-all duration-500 group-hover:scale-105",
232
- hoverUrl && "group-hover:opacity-0",
207
+ 'absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! transition-all duration-500 group-hover:scale-105',
208
+ hoverUrl && 'group-hover:opacity-0',
233
209
  )}
234
- style={{ cornerShape: "squircle" } as React.CSSProperties}
210
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
235
211
  />
236
212
  ) : (
237
213
  <div className="absolute inset-0 flex items-center justify-center bg-linear-to-br from-violet-950 to-indigo-950 text-xs text-stone-500">
@@ -248,7 +224,7 @@ function StudioMasonaryCard({
248
224
  playsInline
249
225
  preload="metadata"
250
226
  className="absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
251
- style={{ cornerShape: "squircle" } as React.CSSProperties}
227
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
252
228
  />
253
229
  )}
254
230
 
@@ -259,7 +235,7 @@ function StudioMasonaryCard({
259
235
  loading="lazy"
260
236
  decoding="async"
261
237
  className="absolute inset-0 h-full w-full object-cover object-top !rounded-4xl opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
262
- style={{ cornerShape: "squircle" } as React.CSSProperties}
238
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
263
239
  />
264
240
  )}
265
241
 
@@ -267,7 +243,7 @@ function StudioMasonaryCard({
267
243
  className="pointer-events-none absolute inset-0 z-10 transition-opacity duration-300 group-hover:opacity-90"
268
244
  style={{
269
245
  background:
270
- "linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)",
246
+ 'linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)',
271
247
  }}
272
248
  />
273
249
 
@@ -276,7 +252,7 @@ function StudioMasonaryCard({
276
252
  <div className="flex items-start justify-start">
277
253
  <span
278
254
  className="inline-flex rounded-full border border-lime-500/80 bg-lime-500/15 px-2 py-1 text-[11.5px] font-medium tracking-widest text-stone-50 backdrop-blur-md sm:px-2 sm:py-1"
279
- style={{ boxShadow: "0 0 24px rgba(132,204,22,0.35)" }}
255
+ style={{ boxShadow: '0 0 24px rgba(132,204,22,0.35)' }}
280
256
  >
281
257
  {badgeLabel}
282
258
  </span>
@@ -288,7 +264,7 @@ function StudioMasonaryCard({
288
264
  {item.title && (
289
265
  <h3
290
266
  className="max-w-[86%] text-lg font-semibold tracking-[0.08em] text-stone-50"
291
- style={{ textShadow: "0 4px 16px rgba(0,0,0,0.9)" }}
267
+ style={{ textShadow: '0 4px 16px rgba(0,0,0,0.9)' }}
292
268
  >
293
269
  {item.title}
294
270
  </h3>
@@ -298,96 +274,96 @@ function StudioMasonaryCard({
298
274
  </article>
299
275
  </Link>
300
276
  </article>
301
- );
277
+ )
302
278
  }
303
279
 
304
280
  export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
305
- eyebrow = "Featured systems",
306
- headlineMain = "Components, blocks, and templates",
307
- headlineAccent = "built to feel alive",
308
- subtext = "A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.",
281
+ eyebrow = 'Featured systems',
282
+ headlineMain = 'Components, blocks, and templates',
283
+ headlineAccent = 'built to feel alive',
284
+ subtext = 'A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.',
309
285
  items = [],
310
286
  columnsDesktop = 12,
311
287
  columnsTablet = 2,
312
- gapClass = "gap-5",
313
- containerClassName = "max-w-7xl mx-auto px-4 md:px-6",
314
- sectionClassName = "py-24",
315
- sectionId = "studio-masonary-media",
288
+ gapClass = 'gap-5',
289
+ containerClassName = 'max-w-7xl mx-auto px-4 md:px-6',
290
+ sectionClassName = 'py-24',
291
+ sectionId = 'studio-masonary-media',
316
292
  customCSS,
317
293
  showCta = false,
318
- ctaAlign = "center",
319
- ctaDisplayMode = "iconWithLabel",
320
- ctaIconPosition = "before",
294
+ ctaAlign = 'center',
295
+ ctaDisplayMode = 'iconWithLabel',
296
+ ctaIconPosition = 'before',
321
297
  ctaButtonText,
322
- ctaButtonStyle = "primary",
323
- ctaSize = "medium",
298
+ ctaButtonStyle = 'primary',
299
+ ctaSize = 'medium',
324
300
  ctaIcon,
325
301
  ctaLink,
326
302
  ctaContainerClass,
327
303
  }) => {
328
- const gridRef = useRef<HTMLDivElement | null>(null);
329
- const isotopeRef = useRef<any>(null);
304
+ const gridRef = useRef<HTMLDivElement | null>(null)
305
+ const isotopeRef = useRef<any>(null)
330
306
 
331
- const safeSectionId = sectionId || "studio-masonary-media";
332
- const gap = useMemo(() => getGapPx(gapClass), [gapClass]);
333
- const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6);
334
- const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3);
307
+ const safeSectionId = sectionId || 'studio-masonary-media'
308
+ const gap = useMemo(() => getGapPx(gapClass), [gapClass])
309
+ const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6)
310
+ const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3)
335
311
 
336
312
  useEffect(() => {
337
- if (!gridRef.current || !items || items.length === 0) return;
313
+ if (!gridRef.current || !items || items.length === 0) return
338
314
 
339
- let destroyed = false;
315
+ let destroyed = false
340
316
 
341
317
  const init = async () => {
342
- const Isotope = (await import("isotope-layout")).default;
343
- const imagesLoaded = (await import("imagesloaded")).default;
318
+ const Isotope = (await import('isotope-layout')).default
319
+ const imagesLoaded = (await import('imagesloaded')).default
344
320
 
345
- if (!gridRef.current || destroyed) return;
321
+ if (!gridRef.current || destroyed) return
346
322
 
347
323
  const iso = new Isotope(gridRef.current, {
348
- itemSelector: ".studio-masonry-item",
324
+ itemSelector: '.studio-masonry-item',
349
325
  percentPosition: true,
350
- transitionDuration: "0.35s",
326
+ transitionDuration: '0.35s',
351
327
  masonry: {
352
- columnWidth: ".studio-masonry-sizer",
353
- gutter: ".studio-masonry-gutter",
328
+ columnWidth: '.studio-masonry-sizer',
329
+ gutter: '.studio-masonry-gutter',
354
330
  fitWidth: false,
355
331
  },
356
- });
332
+ })
357
333
 
358
- isotopeRef.current = iso;
334
+ isotopeRef.current = iso
359
335
 
360
336
  imagesLoaded(gridRef.current, () => {
361
- if (!destroyed) iso.layout();
362
- });
337
+ if (!destroyed) iso.layout()
338
+ })
363
339
 
364
340
  window.setTimeout(() => {
365
- if (!destroyed) iso.layout();
366
- }, 250);
367
- };
341
+ if (!destroyed) iso.layout()
342
+ }, 250)
343
+ }
368
344
 
369
- init();
345
+ init()
370
346
 
371
347
  const handleResize = () => {
372
- isotopeRef.current?.layout();
373
- };
348
+ isotopeRef.current?.layout()
349
+ }
374
350
 
375
- window.addEventListener("resize", handleResize);
351
+ window.addEventListener('resize', handleResize)
376
352
 
377
353
  return () => {
378
- destroyed = true;
379
- window.removeEventListener("resize", handleResize);
380
- isotopeRef.current?.destroy();
381
- isotopeRef.current = null;
382
- };
383
- }, [items]);
354
+ destroyed = true
355
+ window.removeEventListener('resize', handleResize)
356
+ isotopeRef.current?.destroy()
357
+ isotopeRef.current = null
358
+ }
359
+ }, [items])
384
360
 
385
361
  useEffect(() => {
386
- isotopeRef.current?.reloadItems();
387
- isotopeRef.current?.layout();
388
- }, [items, columnsDesktop, columnsTablet, gap]);
362
+ isotopeRef.current?.reloadItems()
363
+ isotopeRef.current?.layout()
364
+ }, [items, columnsDesktop, columnsTablet, gap])
389
365
 
390
- if (!items || items.length === 0) return null;
366
+ if (!items || items.length === 0) return null
391
367
 
392
368
  return (
393
369
  <>
@@ -487,11 +463,9 @@ export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
487
463
 
488
464
  {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
489
465
 
490
- <section
491
- id={safeSectionId}
492
- className={cn("studio-masonary-media", sectionClassName)}
493
- >
466
+ <section id={safeSectionId} className={cn('studio-masonary-media', sectionClassName)}>
494
467
  <div className={cn(containerClassName)}>
468
+ #BlockName-DDMasonaryMedia
495
469
  <motion.div
496
470
  variants={containerVariants}
497
471
  initial="hidden"
@@ -546,26 +520,26 @@ export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
546
520
  {showCta && (
547
521
  <div
548
522
  className={cn(
549
- "mt-10 flex",
550
- ctaAlign === "left" && "justify-start",
551
- ctaAlign === "center" && "justify-center",
552
- ctaAlign === "right" && "justify-end",
523
+ 'mt-10 flex',
524
+ ctaAlign === 'left' && 'justify-start',
525
+ ctaAlign === 'center' && 'justify-center',
526
+ ctaAlign === 'right' && 'justify-end',
553
527
  ctaContainerClass,
554
528
  )}
555
529
  >
556
530
  <a
557
- href={(ctaLink as any)?.url ?? "#"}
531
+ href={(ctaLink as any)?.url ?? '#'}
558
532
  className="inline-flex items-center gap-1.5 bg-indigo-700 text-stone-50 text-xs font-semibold px-3 py-1.5 rounded-lg group-hover:bg-indigo-600 group-hover:scale-[1.05] group-hover:shadow-xl transition-all duration-150 whitespace-nowrap"
559
- style={{ cornerShape: "squircle" } as React.CSSProperties}
533
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
560
534
  >
561
535
  {/* TODO: Reconnect ButtonWithIcon if this project provides it. */}
562
- {ctaButtonText ?? (ctaLink as any)?.label ?? "Learn more"}
536
+ {ctaButtonText ?? (ctaLink as any)?.label ?? 'Learn more'}
563
537
  </a>
564
538
  </div>
565
539
  )}
566
540
  </section>
567
541
  </>
568
- );
569
- };
542
+ )
543
+ }
570
544
 
571
- export default DDMasonaryMedia;
545
+ export default DDMasonaryMedia
@@ -151,17 +151,16 @@ export const DDMasonaryMedia: Block = {
151
151
  }
152
152
  ]
153
153
  },
154
- {
155
- name: 'image',
156
- type: 'upload',
157
- relationTo: 'media',
158
- label: 'Static Preview Image',
154
+ {
155
+ name: 'image',
156
+ type: 'upload',
157
+ relationTo: 'media',
158
+ label: 'Static Preview Image',
159
159
  required: false,
160
- admin: {
161
- description:
162
- 'Optional static image shown by default before hover. Use the fallback URL below for clean installs without media records.'
163
- }
164
- },
160
+ admin: {
161
+ description: 'Static image shown by default before hover.'
162
+ }
163
+ },
165
164
  {
166
165
  name: 'hoverVideo',
167
166
  type: 'upload',
@@ -372,10 +371,10 @@ export const DDMasonaryMedia: Block = {
372
371
  ]
373
372
  }
374
373
  ]
375
- },
376
- {
377
- name: 'ctaContainerClass',
378
- type: 'text',
374
+ },
375
+ {
376
+ name: 'ctaContainerClass',
377
+ type: 'text',
379
378
  label: 'Extra Container Class',
380
379
  admin: {
381
380
  condition: (_, siblingData) => siblingData?.showCta,