blocks-dusted 0.1.0

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/bin/index.js +8 -0
  4. package/package.json +40 -0
  5. package/src/cli.js +79 -0
  6. package/src/commands/add.js +327 -0
  7. package/src/commands/doctor.js +21 -0
  8. package/src/commands/list.js +18 -0
  9. package/src/installers/checkRequirements.js +101 -0
  10. package/src/installers/copyTemplateFiles.js +79 -0
  11. package/src/installers/installDependencies.js +69 -0
  12. package/src/installers/patchPayloadBlocksArray.js +122 -0
  13. package/src/installers/patchRenderBlocks.js +110 -0
  14. package/src/installers/runGenerators.js +3 -0
  15. package/src/installers/writeInstalledBlockReadme.js +117 -0
  16. package/src/registry/listTemplates.js +17 -0
  17. package/src/registry/loadTemplateManifest.js +20 -0
  18. package/src/registry/validateTemplateManifest.js +69 -0
  19. package/src/utils/logger.js +8 -0
  20. package/src/utils/packageManager.js +42 -0
  21. package/src/utils/paths.js +7 -0
  22. package/src/utils/project.js +122 -0
  23. package/src/utils/strings.js +7 -0
  24. package/templates/blocks/DD-BookCall/Component.tsx +573 -0
  25. package/templates/blocks/DD-BookCall/config.ts +239 -0
  26. package/templates/blocks/DD-BookCall/manifest.json +112 -0
  27. package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
  28. package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
  29. package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
  30. package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
  31. package/templates/blocks/DD-Carousel-A/config.ts +163 -0
  32. package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
  33. package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
  34. package/templates/blocks/DD-Carousel-B/config.ts +176 -0
  35. package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
  36. package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
  37. package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
  38. package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
  39. package/templates/blocks/DD-Contact/Component.tsx +439 -0
  40. package/templates/blocks/DD-Contact/config.ts +217 -0
  41. package/templates/blocks/DD-Contact/manifest.json +107 -0
  42. package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
  43. package/templates/blocks/DD-FeatCat/config.ts +204 -0
  44. package/templates/blocks/DD-FeatCat/manifest.json +87 -0
  45. package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
  46. package/templates/blocks/DD-FeatStrip/config.ts +134 -0
  47. package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
  48. package/templates/blocks/DD-Hero/Component.tsx +317 -0
  49. package/templates/blocks/DD-Hero/config.ts +267 -0
  50. package/templates/blocks/DD-Hero/manifest.json +92 -0
  51. package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
  52. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
  53. package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
  54. package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
  55. package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
  56. package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
  57. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
  58. package/templates/blocks/DD-Pricing/Component.tsx +380 -0
  59. package/templates/blocks/DD-Pricing/config.ts +215 -0
  60. package/templates/blocks/DD-Pricing/manifest.json +78 -0
  61. package/templates/blocks/DD-Process/Component.tsx +155 -0
  62. package/templates/blocks/DD-Process/config.ts +150 -0
  63. package/templates/blocks/DD-Process/manifest.json +82 -0
  64. package/templates/blocks/DD-Section/Component.tsx +327 -0
  65. package/templates/blocks/DD-Section/config.ts +214 -0
  66. package/templates/blocks/DD-Section/manifest.json +85 -0
  67. package/templates/blocks/DD-Services/Component.tsx +188 -0
  68. package/templates/blocks/DD-Services/config.ts +149 -0
  69. package/templates/blocks/DD-Services/manifest.json +95 -0
  70. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
  71. package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
  72. package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
  73. package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
  74. package/templates/blocks/DD-Slider-A/config.ts +118 -0
  75. package/templates/blocks/DD-Slider-A/manifest.json +86 -0
  76. package/templates/blocks/DD-StackCards/Component.tsx +496 -0
  77. package/templates/blocks/DD-StackCards/config.ts +399 -0
  78. package/templates/blocks/DD-StackCards/manifest.json +87 -0
  79. package/templates/blocks/DD-Team/Component.tsx +265 -0
  80. package/templates/blocks/DD-Team/config.ts +173 -0
  81. package/templates/blocks/DD-Team/manifest.json +82 -0
  82. package/templates/blocks/DD-TechStack/Component.tsx +189 -0
  83. package/templates/blocks/DD-TechStack/config.ts +153 -0
  84. package/templates/blocks/DD-TechStack/manifest.json +118 -0
  85. package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
  86. package/templates/blocks/DD-Testimonials/config.ts +120 -0
  87. package/templates/blocks/DD-Testimonials/manifest.json +87 -0
  88. package/templates/blocks/DD-Work/Component.tsx +328 -0
  89. package/templates/blocks/DD-Work/config.ts +165 -0
  90. package/templates/blocks/DD-Work/manifest.json +87 -0
  91. package/templates/blocks/DD-Work-B/Component.tsx +320 -0
  92. package/templates/blocks/DD-Work-B/config.ts +165 -0
  93. package/templates/blocks/DD-Work-B/manifest.json +75 -0
  94. package/templates/shared/components/ui/anims/fade.tsx +83 -0
  95. package/templates/shared/components/ui/button.tsx +233 -0
  96. package/templates/shared/components/ui/input.tsx +41 -0
  97. package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
  98. package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
  99. package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
  100. package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
  101. package/templates/shared/utilities/canUseDOM.ts +1 -0
  102. package/templates/shared/utilities/getURL.ts +31 -0
@@ -0,0 +1,87 @@
1
+ {
2
+ "type": "block",
3
+ "name": "DD-FeatCat",
4
+ "label": "Studio Feat Cat Block",
5
+ "sourceName": "StudioFeatCatBlock",
6
+ "portableName": "DD-FeatCat",
7
+ "slug": "DD-FeatCat",
8
+ "version": "0.0.1",
9
+ "source": {
10
+ "repo": "Hadizainal/DesignsDustedv3",
11
+ "originalPath": "src/blocks/StudioFeatCatBlock",
12
+ "originalSlug": "studioFeatCatBlock",
13
+ "originalConfigExport": "StudioFeatCatBlock",
14
+ "originalComponentExport": "StudioFeatCatBlock",
15
+ "originalInterfaceName": "StudioFeatCatBlock"
16
+ },
17
+ "files": [
18
+ {
19
+ "from": "Component.tsx",
20
+ "to": "src/blocks/DD-FeatCat/Component.tsx"
21
+ },
22
+ {
23
+ "from": "config.ts",
24
+ "to": "src/blocks/DD-FeatCat/config.ts"
25
+ }
26
+ ],
27
+ "dependencies": [
28
+ {
29
+ "name": "framer-motion"
30
+ },
31
+ {
32
+ "name": "lucide-react"
33
+ }
34
+ ],
35
+ "sharedFiles": [],
36
+ "requiredProjectFiles": [
37
+ {
38
+ "path": "src/utilities/ui.ts",
39
+ "reason": "Block uses the cn utility from @/utilities/ui."
40
+ }
41
+ ],
42
+ "optionalProjectFiles": [],
43
+ "collectionRegistration": {
44
+ "enabled": true,
45
+ "collections": [
46
+ "Pages",
47
+ "Posts"
48
+ ]
49
+ },
50
+ "renderBlocksRegistration": {
51
+ "enabled": true
52
+ },
53
+ "renderBlocksBehavior": {
54
+ "wrapperless": false
55
+ },
56
+ "pageRouteIntegration": {
57
+ "required": false,
58
+ "manualSteps": []
59
+ },
60
+ "manual": [
61
+ "Register DDFeatCat in Pages/Posts blocks arrays if automatic registration was skipped.",
62
+ "Register DD-FeatCat in RenderBlocks if automatic registration was skipped.",
63
+ "Run payload generate:types after schema changes.",
64
+ "Run payload generate:importmap if admin/component imports changed."
65
+ ],
66
+ "manualSteps": [
67
+ "Register DDFeatCat in Pages/Posts blocks arrays if automatic registration was skipped.",
68
+ "Register DD-FeatCat in RenderBlocks if automatic registration was skipped.",
69
+ "Run payload generate:types after schema changes.",
70
+ "Run payload generate:importmap if admin/component imports changed."
71
+ ],
72
+ "patchTargets": {
73
+ "blockConfigRegistration": {
74
+ "description": "Installer can add import { DDFeatCat } from '@/blocks/DD-FeatCat/config' and add DDFeatCat to clear Pages/Posts blocks arrays."
75
+ },
76
+ "renderBlocksRegistration": {
77
+ "description": "Installer can register DD-FeatCat in src/blocks/**/RenderBlocks.tsx when the blockComponents map is clear."
78
+ }
79
+ },
80
+ "notes": [
81
+ "Converted from DesignsDustedv3 StudioFeatCatBlock.",
82
+ "Generated Payload type imports are replaced with portable fallback types when needed.",
83
+ "Installer must not overwrite existing target files."
84
+ ],
85
+ "exportName": "DDFeatCat",
86
+ "interfaceName": "DDFeatCat"
87
+ }
@@ -0,0 +1,615 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ "use client";
3
+
4
+ import { useState, useEffect } from "react";
5
+ import Link from "next/link";
6
+ import Image from "next/image";
7
+ import dynamic from "next/dynamic";
8
+ import { ArrowRight } from "lucide-react";
9
+
10
+ const DotLottieReact = dynamic(
11
+ () => import("@lottiefiles/dotlottie-react").then((m) => m.DotLottieReact),
12
+ { ssr: false },
13
+ );
14
+
15
+ /* ── Label color maps ─────────────────────────────────────────── */
16
+
17
+ const labelColorMap: Record<string, string> = {
18
+ indigo: "text-indigo-400",
19
+ emerald: "text-emerald-400",
20
+ amber: "text-amber-400",
21
+ rose: "text-rose-400",
22
+ violet: "text-violet-400",
23
+ sky: "text-sky-400",
24
+ };
25
+
26
+ const exploreColorMap: Record<
27
+ string,
28
+ { base: string; hover: string; glow: string }
29
+ > = {
30
+ indigo: {
31
+ base: "text-indigo-300",
32
+ hover: "group-hover:text-indigo-400",
33
+ glow: "rgba(99,102,241,0.08)",
34
+ },
35
+ emerald: {
36
+ base: "text-emerald-300",
37
+ hover: "group-hover:text-emerald-400",
38
+ glow: "rgba(16,185,129,0.08)",
39
+ },
40
+ amber: {
41
+ base: "text-amber-300",
42
+ hover: "group-hover:text-amber-400",
43
+ glow: "rgba(245,158,11,0.08)",
44
+ },
45
+ rose: {
46
+ base: "text-rose-300",
47
+ hover: "group-hover:text-rose-400",
48
+ glow: "rgba(244,63,94,0.08)",
49
+ },
50
+ violet: {
51
+ base: "text-violet-300",
52
+ hover: "group-hover:text-violet-400",
53
+ glow: "rgba(167,139,250,0.08)",
54
+ },
55
+ sky: {
56
+ base: "text-sky-300",
57
+ hover: "group-hover:text-sky-400",
58
+ glow: "rgba(56,189,248,0.08)",
59
+ },
60
+ };
61
+
62
+ /* ── Isometric SVG Icons ─────────────────────────────────────── */
63
+
64
+ function UIBlocksIcon({ hovered }: { hovered: boolean }) {
65
+ return (
66
+ <svg
67
+ viewBox="0 0 120 90"
68
+ fill="none"
69
+ xmlns="http://www.w3.org/2000/svg"
70
+ className="w-full h-full"
71
+ style={{
72
+ transition: "filter 0.4s ease",
73
+ filter: hovered ? "drop-shadow(0 0 12px rgba(99,102,241,0.7))" : "none",
74
+ }}
75
+ >
76
+ <g
77
+ style={{
78
+ transform: hovered ? "translateY(-4px)" : "translateY(0)",
79
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
80
+ }}
81
+ >
82
+ <polygon
83
+ points="60,4 108,28 60,52 12,28"
84
+ fill={hovered ? "#312e81" : "#1e1b4b"}
85
+ stroke={hovered ? "#818cf8" : "#4f46e5"}
86
+ strokeWidth="1.2"
87
+ />
88
+ <polygon
89
+ points="12,28 12,58 60,82 60,52"
90
+ fill={hovered ? "#1e1b4b" : "#161430"}
91
+ stroke={hovered ? "#6366f1" : "#3730a3"}
92
+ strokeWidth="1.2"
93
+ />
94
+ <polygon
95
+ points="108,28 108,58 60,82 60,52"
96
+ fill={hovered ? "#2e2a6e" : "#1a1740"}
97
+ stroke={hovered ? "#818cf8" : "#4338ca"}
98
+ strokeWidth="1.2"
99
+ />
100
+ <line
101
+ x1="60"
102
+ y1="4"
103
+ x2="60"
104
+ y2="52"
105
+ stroke={hovered ? "#818cf8" : "#4f46e5"}
106
+ strokeWidth="0.6"
107
+ strokeDasharray="3 3"
108
+ />
109
+ <line
110
+ x1="36"
111
+ y1="16"
112
+ x2="84"
113
+ y2="40"
114
+ stroke={hovered ? "#818cf8" : "#4f46e5"}
115
+ strokeWidth="0.6"
116
+ strokeDasharray="3 3"
117
+ />
118
+ <line
119
+ x1="84"
120
+ y1="16"
121
+ x2="36"
122
+ y2="40"
123
+ stroke={hovered ? "#818cf8" : "#4f46e5"}
124
+ strokeWidth="0.6"
125
+ strokeDasharray="3 3"
126
+ />
127
+ <polygon
128
+ points="60,14 78,23 60,32 42,23"
129
+ fill={hovered ? "#4f46e5" : "#312e81"}
130
+ stroke={hovered ? "#a5b4fc" : "#6366f1"}
131
+ strokeWidth="1"
132
+ />
133
+ <polygon
134
+ points="42,23 42,33 60,42 60,32"
135
+ fill={hovered ? "#3730a3" : "#1e1b4b"}
136
+ stroke={hovered ? "#818cf8" : "#4338ca"}
137
+ strokeWidth="1"
138
+ />
139
+ <polygon
140
+ points="78,23 78,33 60,42 60,32"
141
+ fill={hovered ? "#4338ca" : "#272466"}
142
+ stroke={hovered ? "#a5b4fc" : "#6366f1"}
143
+ strokeWidth="1"
144
+ />
145
+ </g>
146
+ </svg>
147
+ );
148
+ }
149
+
150
+ function TemplatesIcon({ hovered }: { hovered: boolean }) {
151
+ return (
152
+ <svg
153
+ viewBox="0 0 120 90"
154
+ fill="none"
155
+ xmlns="http://www.w3.org/2000/svg"
156
+ className="w-full h-full"
157
+ style={{
158
+ transition: "filter 0.4s ease",
159
+ filter: hovered ? "drop-shadow(0 0 12px rgba(16,185,129,0.6))" : "none",
160
+ }}
161
+ >
162
+ <g
163
+ style={{
164
+ transform: hovered ? "translateY(-4px)" : "translateY(0)",
165
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
166
+ }}
167
+ >
168
+ <polygon
169
+ points="60,6 104,30 60,54 16,30"
170
+ fill={hovered ? "#064e3b" : "#022c22"}
171
+ stroke={hovered ? "#34d399" : "#059669"}
172
+ strokeWidth="1.2"
173
+ />
174
+ <polygon
175
+ points="16,30 16,60 60,84 60,54"
176
+ fill={hovered ? "#022c22" : "#011a15"}
177
+ stroke={hovered ? "#10b981" : "#047857"}
178
+ strokeWidth="1.2"
179
+ />
180
+ <polygon
181
+ points="104,30 104,60 60,84 60,54"
182
+ fill={hovered ? "#043927" : "#011a15"}
183
+ stroke={hovered ? "#34d399" : "#059669"}
184
+ strokeWidth="1.2"
185
+ />
186
+ <line
187
+ x1="30"
188
+ y1="30"
189
+ x2="60"
190
+ y2="16"
191
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
192
+ strokeWidth="1"
193
+ opacity="0.7"
194
+ />
195
+ <line
196
+ x1="30"
197
+ y1="36"
198
+ x2="60"
199
+ y2="22"
200
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
201
+ strokeWidth="0.8"
202
+ opacity="0.5"
203
+ />
204
+ <line
205
+ x1="30"
206
+ y1="42"
207
+ x2="52"
208
+ y2="30"
209
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
210
+ strokeWidth="0.8"
211
+ opacity="0.4"
212
+ />
213
+ <line
214
+ x1="72"
215
+ y1="22"
216
+ x2="96"
217
+ y2="34"
218
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
219
+ strokeWidth="1"
220
+ opacity="0.7"
221
+ />
222
+ <line
223
+ x1="72"
224
+ y1="28"
225
+ x2="90"
226
+ y2="37"
227
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
228
+ strokeWidth="0.8"
229
+ opacity="0.5"
230
+ />
231
+ <line
232
+ x1="72"
233
+ y1="34"
234
+ x2="84"
235
+ y2="40"
236
+ stroke={hovered ? "#6ee7b7" : "#34d399"}
237
+ strokeWidth="0.8"
238
+ opacity="0.4"
239
+ />
240
+ <line
241
+ x1="66"
242
+ y1="12"
243
+ x2="66"
244
+ y2="50"
245
+ stroke={hovered ? "#34d399" : "#059669"}
246
+ strokeWidth="0.8"
247
+ strokeDasharray="2 2"
248
+ />
249
+ </g>
250
+ </svg>
251
+ );
252
+ }
253
+
254
+ function UIKitIcon({ hovered }: { hovered: boolean }) {
255
+ return (
256
+ <svg
257
+ viewBox="0 0 120 90"
258
+ fill="none"
259
+ xmlns="http://www.w3.org/2000/svg"
260
+ className="w-full h-full"
261
+ style={{
262
+ transition: "filter 0.4s ease",
263
+ filter: hovered ? "drop-shadow(0 0 12px rgba(245,158,11,0.6))" : "none",
264
+ }}
265
+ >
266
+ <g
267
+ style={{
268
+ transform: hovered ? "translateY(-4px)" : "translateY(0)",
269
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
270
+ }}
271
+ >
272
+ <polygon
273
+ points="60,42 100,62 60,82 20,62"
274
+ fill={hovered ? "#451a03" : "#27100a"}
275
+ stroke={hovered ? "#f59e0b" : "#b45309"}
276
+ strokeWidth="1.2"
277
+ />
278
+ <polygon
279
+ points="20,52 20,62 60,82 60,72"
280
+ fill={hovered ? "#27100a" : "#1a0a05"}
281
+ stroke={hovered ? "#d97706" : "#92400e"}
282
+ strokeWidth="1"
283
+ />
284
+ <polygon
285
+ points="100,52 100,62 60,82 60,72"
286
+ fill={hovered ? "#3b1a06" : "#1a0a05"}
287
+ stroke={hovered ? "#f59e0b" : "#b45309"}
288
+ strokeWidth="1"
289
+ />
290
+ <polygon
291
+ points="60,26 100,46 60,66 20,46"
292
+ fill={hovered ? "#78350f" : "#451a03"}
293
+ stroke={hovered ? "#fbbf24" : "#d97706"}
294
+ strokeWidth="1.2"
295
+ />
296
+ <polygon
297
+ points="20,36 20,46 60,66 60,56"
298
+ fill={hovered ? "#451a03" : "#27100a"}
299
+ stroke={hovered ? "#f59e0b" : "#b45309"}
300
+ strokeWidth="1"
301
+ />
302
+ <polygon
303
+ points="100,36 100,46 60,66 60,56"
304
+ fill={hovered ? "#5c2a07" : "#2d1108"}
305
+ stroke={hovered ? "#fbbf24" : "#d97706"}
306
+ strokeWidth="1"
307
+ />
308
+ <polygon
309
+ points="60,10 100,30 60,50 20,30"
310
+ fill={hovered ? "#92400e" : "#78350f"}
311
+ stroke={hovered ? "#fcd34d" : "#fbbf24"}
312
+ strokeWidth="1.2"
313
+ />
314
+ <polygon
315
+ points="20,20 20,30 60,50 60,40"
316
+ fill={hovered ? "#78350f" : "#451a03"}
317
+ stroke={hovered ? "#fbbf24" : "#d97706"}
318
+ strokeWidth="1"
319
+ />
320
+ <polygon
321
+ points="100,20 100,30 60,50 60,40"
322
+ fill={hovered ? "#7c3a0e" : "#4a2008"}
323
+ stroke={hovered ? "#fcd34d" : "#fbbf24"}
324
+ strokeWidth="1"
325
+ />
326
+ <circle
327
+ cx="44"
328
+ cy="28"
329
+ r="2.5"
330
+ fill={hovered ? "#fcd34d" : "#fbbf24"}
331
+ opacity="0.9"
332
+ />
333
+ <circle
334
+ cx="60"
335
+ cy="20"
336
+ r="2.5"
337
+ fill={hovered ? "#fcd34d" : "#fbbf24"}
338
+ opacity="0.9"
339
+ />
340
+ <circle
341
+ cx="76"
342
+ cy="28"
343
+ r="2.5"
344
+ fill={hovered ? "#fcd34d" : "#fbbf24"}
345
+ opacity="0.9"
346
+ />
347
+ </g>
348
+ </svg>
349
+ );
350
+ }
351
+
352
+ const iconMap: Record<string, React.FC<{ hovered: boolean }>> = {
353
+ uiBlocks: UIBlocksIcon,
354
+ templates: TemplatesIcon,
355
+ uiKit: UIKitIcon,
356
+ };
357
+
358
+ /* ── Icon renderer ───────────────────────────────────────────── */
359
+
360
+ type MediaLike = {
361
+ url?: string | null;
362
+ mimeType?: string | null;
363
+ filename?: string | null;
364
+ };
365
+
366
+ function isLocalMediaSrc(src?: string | null): src is string {
367
+ return typeof src === "string" && src.startsWith("/api/media/file/");
368
+ }
369
+
370
+ function CardIconMedia({
371
+ media,
372
+ hovered,
373
+ }: {
374
+ media: MediaLike;
375
+ hovered: boolean;
376
+ }) {
377
+ const url = media.url ?? "";
378
+ const safeUrl = isLocalMediaSrc(url) ? url : "";
379
+ const mime = media.mimeType ?? "";
380
+ const filename = media.filename ?? "";
381
+ const isLottie =
382
+ mime === "application/json" ||
383
+ filename.endsWith(".lottie") ||
384
+ (mime === "application/octet-stream" && filename.endsWith(".lottie"));
385
+ const isDotLottie = filename.endsWith(".lottie");
386
+ const isJson = mime === "application/json" || filename.endsWith(".json");
387
+
388
+ if (!safeUrl) return null;
389
+
390
+ if (isDotLottie || isJson) {
391
+ return (
392
+ <DotLottieReact
393
+ src={safeUrl}
394
+ loop
395
+ autoplay
396
+ style={{
397
+ width: "100%",
398
+ height: "100%",
399
+ transform: hovered ? "scale(1.08)" : "scale(1)",
400
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
401
+ }}
402
+ />
403
+ );
404
+ }
405
+
406
+ // SVG or raster image
407
+ return (
408
+ <Image
409
+ src={safeUrl}
410
+ alt=""
411
+ fill
412
+ className="object-contain"
413
+ style={{
414
+ transform: hovered ? "scale(1.08)" : "scale(1)",
415
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
416
+ }}
417
+ />
418
+ );
419
+ }
420
+
421
+ /* ── Card ────────────────────────────────────────────────────── */
422
+
423
+ type CardData = { [key: string]: any };
424
+
425
+ function FeatureCard({
426
+ card,
427
+ index,
428
+ isLast,
429
+ totalCards,
430
+ isMobile,
431
+ }: {
432
+ card: CardData;
433
+ index: number;
434
+ isLast: boolean;
435
+ totalCards: number;
436
+ isMobile: boolean;
437
+ }) {
438
+ const [hovered, setHovered] = useState(false);
439
+ const colorKey = card.labelColor || "indigo";
440
+ const colors = exploreColorMap[colorKey] || exploreColorMap.indigo;
441
+ const labelClass = labelColorMap[colorKey] || labelColorMap.indigo;
442
+ const FallbackIcon = iconMap[card.iconType || "uiBlocks"] || UIBlocksIcon;
443
+ const iconMedia =
444
+ card.iconMedia &&
445
+ typeof card.iconMedia === "object" &&
446
+ (card.iconMedia as MediaLike).url
447
+ ? (card.iconMedia as MediaLike)
448
+ : null;
449
+
450
+ // On mobile, last card is full-width if total is odd
451
+ const isFullWidthOnMobile = isMobile && totalCards % 2 !== 0 && isLast;
452
+
453
+ let borderStyle: React.CSSProperties = {};
454
+ if (isMobile) {
455
+ const isEvenPosition = index % 2 === 0;
456
+ const isSecondRowOrLater = index >= 2;
457
+ borderStyle = {
458
+ borderRight:
459
+ isEvenPosition && index + 1 < totalCards && !isFullWidthOnMobile
460
+ ? "1px solid rgba(255,255,255,0.07)"
461
+ : "none",
462
+ borderTop: isSecondRowOrLater
463
+ ? "1px solid rgba(255,255,255,0.07)"
464
+ : "none",
465
+ };
466
+ } else {
467
+ borderStyle = {
468
+ borderRight: !isLast ? "1px solid rgba(255,255,255,0.07)" : "none",
469
+ };
470
+ }
471
+
472
+ const cardClass = `group relative flex flex-col sm:flex-row items-start gap-4 sm:gap-6 px-5 py-7 sm:px-8 sm:py-10${card.href ? " cursor-pointer" : ""}${isFullWidthOnMobile ? "col-span-1" : ""}`;
473
+
474
+ const innerContent = (
475
+ <>
476
+ {/* Hover bg glow */}
477
+ <div
478
+ className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
479
+ style={{
480
+ background: `radial-gradient(ellipse at 30% 50%, ${colors.glow} 0%, transparent 70%)`,
481
+ }}
482
+ />
483
+
484
+ {/* Icon */}
485
+ <div
486
+ className="relative shrink-0 w-14 h-11 sm:w-20 sm:h-16"
487
+ style={
488
+ !iconMedia
489
+ ? {
490
+ transform: hovered ? "scale(1.08)" : "scale(1)",
491
+ transition: "transform 0.4s cubic-bezier(0.34,1.56,0.64,1)",
492
+ }
493
+ : undefined
494
+ }
495
+ >
496
+ {iconMedia ? (
497
+ <CardIconMedia media={iconMedia} hovered={hovered} />
498
+ ) : (
499
+ <FallbackIcon hovered={hovered} />
500
+ )}
501
+ </div>
502
+
503
+ {/* Text */}
504
+ <div className="relative flex flex-col gap-2 pt-1">
505
+ <span
506
+ className={`text-sm font-bold tracking-[0.15em] uppercase ${labelClass} transition-all duration-300`}
507
+ style={{ letterSpacing: hovered ? "0.2em" : "0.15em" }}
508
+ >
509
+ {card.label}
510
+ </span>
511
+ <p className="text-sm md:text-md text-stone-700 dark:text-stone-400 leading-relaxed transition-colors duration-300">
512
+ {card.description}
513
+ </p>
514
+ {card.href && (
515
+ <span
516
+ className={`inline-flex items-center gap-1 text-sm font-medium mt-4 transition-all duration-300 ${colors.base} ${colors.hover}`}
517
+ >
518
+ Explore
519
+ <ArrowRight
520
+ size={14}
521
+ style={{
522
+ transform: hovered ? "translateX(4px)" : "translateX(0)",
523
+ transition: "transform 0.3s ease",
524
+ }}
525
+ />
526
+ </span>
527
+ )}
528
+ </div>
529
+ </>
530
+ );
531
+
532
+ if (card.href) {
533
+ return (
534
+ <Link
535
+ href={card.href}
536
+ className={cardClass}
537
+ style={borderStyle}
538
+ onMouseEnter={() => setHovered(true)}
539
+ onMouseLeave={() => setHovered(false)}
540
+ >
541
+ {innerContent}
542
+ </Link>
543
+ );
544
+ }
545
+
546
+ return (
547
+ <div
548
+ className={cardClass}
549
+ style={borderStyle}
550
+ onMouseEnter={() => setHovered(true)}
551
+ onMouseLeave={() => setHovered(false)}
552
+ >
553
+ {innerContent}
554
+ </div>
555
+ );
556
+ }
557
+
558
+ /* ── Block ───────────────────────────────────────────────────── */
559
+
560
+ export type DDFeatStripProps = Record<string, any>;
561
+
562
+ export const DDFeatStrip: React.FC<DDFeatStripProps> = ({
563
+ cards,
564
+ containerClass,
565
+ sectionId,
566
+ customCSS,
567
+ }) => {
568
+ if (!cards || cards.length === 0) return null;
569
+
570
+ const cardItems = cards as CardData[];
571
+ const [isMobile, setIsMobile] = useState(false);
572
+
573
+ useEffect(() => {
574
+ const check = () => setIsMobile(window.innerWidth < 768);
575
+ check();
576
+ window.addEventListener("resize", check);
577
+ return () => window.removeEventListener("resize", check);
578
+ }, []);
579
+
580
+ const totalCards = cardItems.length;
581
+
582
+ return (
583
+ <>
584
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
585
+
586
+ <div id={sectionId ?? undefined} className="py-24">
587
+ <div
588
+ className={`w-full bg-stone-100 dark:bg-stone-950 border-t border-b border-white/6 ${containerClass || ""}`}
589
+ >
590
+ <div
591
+ className="grid grid-cols-1"
592
+ style={
593
+ !isMobile
594
+ ? {
595
+ gridTemplateColumns: `repeat(${totalCards}, minmax(0, 1fr))`,
596
+ }
597
+ : undefined
598
+ }
599
+ >
600
+ {cardItems.map((card, i) => (
601
+ <FeatureCard
602
+ key={card.id ?? i}
603
+ card={card}
604
+ index={i}
605
+ isLast={i === totalCards - 1}
606
+ totalCards={totalCards}
607
+ isMobile={isMobile}
608
+ />
609
+ ))}
610
+ </div>
611
+ </div>
612
+ </div>
613
+ </>
614
+ );
615
+ };