blocks-dusted 0.1.12 → 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 (148) hide show
  1. package/package.json +1 -1
  2. package/src/installers/patchRenderBlocks.js +1 -1
  3. package/src/registry/validateTemplateManifest.js +2 -1
  4. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  5. package/templates/blocks/BannerDD790432/config.ts +54 -0
  6. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  7. package/templates/blocks/Button47/Component.tsx +124 -0
  8. package/templates/blocks/Button47/config.ts +75 -0
  9. package/templates/blocks/Button47/link.ts +22 -0
  10. package/templates/blocks/Button47/manifest.json +105 -0
  11. package/templates/blocks/Button47/styles.module.scss +75 -0
  12. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  13. package/templates/blocks/DD-Accordion/config.ts +71 -59
  14. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  15. package/templates/blocks/DD-AnimText/config.ts +296 -295
  16. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  17. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  18. package/templates/blocks/DD-BookCall/Component.tsx +535 -535
  19. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  20. package/templates/blocks/DD-Card/Component.tsx +4 -4
  21. package/templates/blocks/DD-CardTemplate01/Component.tsx +139 -140
  22. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  23. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  24. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  25. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  28. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  29. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  30. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  31. package/templates/blocks/DD-Contact/Component.tsx +433 -435
  32. package/templates/blocks/DD-Counter/Component.tsx +11 -6
  33. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  34. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  35. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  36. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  37. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  38. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  39. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  40. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  41. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  42. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  43. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  44. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  45. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  46. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  47. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  48. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  49. package/templates/blocks/DD-Hero/Component.tsx +296 -297
  50. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  51. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  52. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  53. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  54. package/templates/blocks/DD-HorizontalScroll/Component.tsx +937 -936
  55. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  56. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  57. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  58. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  59. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  60. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  61. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -545
  62. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  63. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  64. package/templates/blocks/DD-Masonry/config.ts +475 -475
  65. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  66. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  67. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  68. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  69. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  70. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  71. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  72. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  73. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  74. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  75. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  76. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  77. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  78. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  79. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  80. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  81. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  82. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  83. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  84. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  85. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  86. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  87. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  88. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  89. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  90. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  91. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  92. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  95. package/templates/blocks/DD-Pricing/Component.tsx +2 -3
  96. package/templates/blocks/DD-Pricing/config.ts +215 -215
  97. package/templates/blocks/DD-Process/Component.tsx +2 -2
  98. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  99. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  100. package/templates/blocks/DD-Section/Component.tsx +265 -266
  101. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  102. package/templates/blocks/DD-Services/Component.tsx +176 -176
  103. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  104. package/templates/blocks/DD-Slider-A/Component.tsx +2 -2
  105. package/templates/blocks/DD-StackCards/Component.tsx +172 -171
  106. package/templates/blocks/DD-Team/Component.tsx +247 -247
  107. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  108. package/templates/blocks/DD-TechStack/Component.tsx +173 -174
  109. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  110. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  111. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  112. package/templates/blocks/DD-Testimonials/Component.tsx +139 -139
  113. package/templates/blocks/DD-Thanks/Component.tsx +1 -1
  114. package/templates/blocks/DD-Work/Component.tsx +318 -318
  115. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  116. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  117. package/templates/blocks/DDHG-HighlightArc/Component.tsx +6 -3
  118. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +45 -8
  119. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +1 -3
  120. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +9 -22
  121. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  122. package/templates/blocks/DDHG-TeamCards/Component.tsx +6 -35
  123. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  124. package/templates/blocks/Form/Component.tsx +163 -0
  125. package/templates/blocks/Form/Country/index.tsx +65 -0
  126. package/templates/blocks/Form/Country/options.ts +982 -0
  127. package/templates/blocks/Form/Email/index.tsx +38 -0
  128. package/templates/blocks/Form/Error/index.tsx +15 -0
  129. package/templates/blocks/Form/Message/index.tsx +13 -0
  130. package/templates/blocks/Form/Number/index.tsx +36 -0
  131. package/templates/blocks/Form/Select/index.tsx +63 -0
  132. package/templates/blocks/Form/State/index.tsx +64 -0
  133. package/templates/blocks/Form/State/options.ts +52 -0
  134. package/templates/blocks/Form/Text/index.tsx +32 -0
  135. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  136. package/templates/blocks/Form/Width/index.tsx +13 -0
  137. package/templates/blocks/Form/config.ts +51 -0
  138. package/templates/blocks/Form/fields.tsx +21 -0
  139. package/templates/blocks/Form/manifest.json +168 -0
  140. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  141. package/templates/blocks/MediaBlock/config.ts +14 -0
  142. package/templates/blocks/MediaBlock/manifest.json +92 -0
  143. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  144. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  145. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  146. package/templates/blocks/rawHtml/Component.tsx +25 -0
  147. package/templates/blocks/rawHtml/config.ts +56 -0
  148. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -1,585 +1,584 @@
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<string, { base: string; hover: string; glow: string }> = {
27
- indigo: {
28
- base: 'text-indigo-300',
29
- hover: 'group-hover:text-indigo-400',
30
- glow: 'rgba(99,102,241,0.08)',
31
- },
32
- emerald: {
33
- base: 'text-emerald-300',
34
- hover: 'group-hover:text-emerald-400',
35
- glow: 'rgba(16,185,129,0.08)',
36
- },
37
- amber: {
38
- base: 'text-amber-300',
39
- hover: 'group-hover:text-amber-400',
40
- glow: 'rgba(245,158,11,0.08)',
41
- },
42
- rose: {
43
- base: 'text-rose-300',
44
- hover: 'group-hover:text-rose-400',
45
- glow: 'rgba(244,63,94,0.08)',
46
- },
47
- violet: {
48
- base: 'text-violet-300',
49
- hover: 'group-hover:text-violet-400',
50
- glow: 'rgba(167,139,250,0.08)',
51
- },
52
- sky: {
53
- base: 'text-sky-300',
54
- hover: 'group-hover:text-sky-400',
55
- glow: 'rgba(56,189,248,0.08)',
56
- },
57
- }
58
-
59
- /* ── Isometric SVG Icons ─────────────────────────────────────── */
60
-
61
- function UIBlocksIcon({ hovered }: { hovered: boolean }) {
62
- return (
63
- <svg
64
- viewBox="0 0 120 90"
65
- fill="none"
66
- xmlns="http://www.w3.org/2000/svg"
67
- className="w-full h-full"
68
- style={{
69
- transition: 'filter 0.4s ease',
70
- filter: hovered ? 'drop-shadow(0 0 12px rgba(99,102,241,0.7))' : 'none',
71
- }}
72
- >
73
- <g
74
- style={{
75
- transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
76
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
77
- }}
78
- >
79
- <polygon
80
- points="60,4 108,28 60,52 12,28"
81
- fill={hovered ? '#312e81' : '#1e1b4b'}
82
- stroke={hovered ? '#818cf8' : '#4f46e5'}
83
- strokeWidth="1.2"
84
- />
85
- <polygon
86
- points="12,28 12,58 60,82 60,52"
87
- fill={hovered ? '#1e1b4b' : '#161430'}
88
- stroke={hovered ? '#6366f1' : '#3730a3'}
89
- strokeWidth="1.2"
90
- />
91
- <polygon
92
- points="108,28 108,58 60,82 60,52"
93
- fill={hovered ? '#2e2a6e' : '#1a1740'}
94
- stroke={hovered ? '#818cf8' : '#4338ca'}
95
- strokeWidth="1.2"
96
- />
97
- <line
98
- x1="60"
99
- y1="4"
100
- x2="60"
101
- y2="52"
102
- stroke={hovered ? '#818cf8' : '#4f46e5'}
103
- strokeWidth="0.6"
104
- strokeDasharray="3 3"
105
- />
106
- <line
107
- x1="36"
108
- y1="16"
109
- x2="84"
110
- y2="40"
111
- stroke={hovered ? '#818cf8' : '#4f46e5'}
112
- strokeWidth="0.6"
113
- strokeDasharray="3 3"
114
- />
115
- <line
116
- x1="84"
117
- y1="16"
118
- x2="36"
119
- y2="40"
120
- stroke={hovered ? '#818cf8' : '#4f46e5'}
121
- strokeWidth="0.6"
122
- strokeDasharray="3 3"
123
- />
124
- <polygon
125
- points="60,14 78,23 60,32 42,23"
126
- fill={hovered ? '#4f46e5' : '#312e81'}
127
- stroke={hovered ? '#a5b4fc' : '#6366f1'}
128
- strokeWidth="1"
129
- />
130
- <polygon
131
- points="42,23 42,33 60,42 60,32"
132
- fill={hovered ? '#3730a3' : '#1e1b4b'}
133
- stroke={hovered ? '#818cf8' : '#4338ca'}
134
- strokeWidth="1"
135
- />
136
- <polygon
137
- points="78,23 78,33 60,42 60,32"
138
- fill={hovered ? '#4338ca' : '#272466'}
139
- stroke={hovered ? '#a5b4fc' : '#6366f1'}
140
- strokeWidth="1"
141
- />
142
- </g>
143
- </svg>
144
- )
145
- }
146
-
147
- function TemplatesIcon({ hovered }: { hovered: boolean }) {
148
- return (
149
- <svg
150
- viewBox="0 0 120 90"
151
- fill="none"
152
- xmlns="http://www.w3.org/2000/svg"
153
- className="w-full h-full"
154
- style={{
155
- transition: 'filter 0.4s ease',
156
- filter: hovered ? 'drop-shadow(0 0 12px rgba(16,185,129,0.6))' : 'none',
157
- }}
158
- >
159
- <g
160
- style={{
161
- transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
162
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
163
- }}
164
- >
165
- <polygon
166
- points="60,6 104,30 60,54 16,30"
167
- fill={hovered ? '#064e3b' : '#022c22'}
168
- stroke={hovered ? '#34d399' : '#059669'}
169
- strokeWidth="1.2"
170
- />
171
- <polygon
172
- points="16,30 16,60 60,84 60,54"
173
- fill={hovered ? '#022c22' : '#011a15'}
174
- stroke={hovered ? '#10b981' : '#047857'}
175
- strokeWidth="1.2"
176
- />
177
- <polygon
178
- points="104,30 104,60 60,84 60,54"
179
- fill={hovered ? '#043927' : '#011a15'}
180
- stroke={hovered ? '#34d399' : '#059669'}
181
- strokeWidth="1.2"
182
- />
183
- <line
184
- x1="30"
185
- y1="30"
186
- x2="60"
187
- y2="16"
188
- stroke={hovered ? '#6ee7b7' : '#34d399'}
189
- strokeWidth="1"
190
- opacity="0.7"
191
- />
192
- <line
193
- x1="30"
194
- y1="36"
195
- x2="60"
196
- y2="22"
197
- stroke={hovered ? '#6ee7b7' : '#34d399'}
198
- strokeWidth="0.8"
199
- opacity="0.5"
200
- />
201
- <line
202
- x1="30"
203
- y1="42"
204
- x2="52"
205
- y2="30"
206
- stroke={hovered ? '#6ee7b7' : '#34d399'}
207
- strokeWidth="0.8"
208
- opacity="0.4"
209
- />
210
- <line
211
- x1="72"
212
- y1="22"
213
- x2="96"
214
- y2="34"
215
- stroke={hovered ? '#6ee7b7' : '#34d399'}
216
- strokeWidth="1"
217
- opacity="0.7"
218
- />
219
- <line
220
- x1="72"
221
- y1="28"
222
- x2="90"
223
- y2="37"
224
- stroke={hovered ? '#6ee7b7' : '#34d399'}
225
- strokeWidth="0.8"
226
- opacity="0.5"
227
- />
228
- <line
229
- x1="72"
230
- y1="34"
231
- x2="84"
232
- y2="40"
233
- stroke={hovered ? '#6ee7b7' : '#34d399'}
234
- strokeWidth="0.8"
235
- opacity="0.4"
236
- />
237
- <line
238
- x1="66"
239
- y1="12"
240
- x2="66"
241
- y2="50"
242
- stroke={hovered ? '#34d399' : '#059669'}
243
- strokeWidth="0.8"
244
- strokeDasharray="2 2"
245
- />
246
- </g>
247
- </svg>
248
- )
249
- }
250
-
251
- function UIKitIcon({ hovered }: { hovered: boolean }) {
252
- return (
253
- <svg
254
- viewBox="0 0 120 90"
255
- fill="none"
256
- xmlns="http://www.w3.org/2000/svg"
257
- className="w-full h-full"
258
- style={{
259
- transition: 'filter 0.4s ease',
260
- filter: hovered ? 'drop-shadow(0 0 12px rgba(245,158,11,0.6))' : 'none',
261
- }}
262
- >
263
- <g
264
- style={{
265
- transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
266
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
267
- }}
268
- >
269
- <polygon
270
- points="60,42 100,62 60,82 20,62"
271
- fill={hovered ? '#451a03' : '#27100a'}
272
- stroke={hovered ? '#f59e0b' : '#b45309'}
273
- strokeWidth="1.2"
274
- />
275
- <polygon
276
- points="20,52 20,62 60,82 60,72"
277
- fill={hovered ? '#27100a' : '#1a0a05'}
278
- stroke={hovered ? '#d97706' : '#92400e'}
279
- strokeWidth="1"
280
- />
281
- <polygon
282
- points="100,52 100,62 60,82 60,72"
283
- fill={hovered ? '#3b1a06' : '#1a0a05'}
284
- stroke={hovered ? '#f59e0b' : '#b45309'}
285
- strokeWidth="1"
286
- />
287
- <polygon
288
- points="60,26 100,46 60,66 20,46"
289
- fill={hovered ? '#78350f' : '#451a03'}
290
- stroke={hovered ? '#fbbf24' : '#d97706'}
291
- strokeWidth="1.2"
292
- />
293
- <polygon
294
- points="20,36 20,46 60,66 60,56"
295
- fill={hovered ? '#451a03' : '#27100a'}
296
- stroke={hovered ? '#f59e0b' : '#b45309'}
297
- strokeWidth="1"
298
- />
299
- <polygon
300
- points="100,36 100,46 60,66 60,56"
301
- fill={hovered ? '#5c2a07' : '#2d1108'}
302
- stroke={hovered ? '#fbbf24' : '#d97706'}
303
- strokeWidth="1"
304
- />
305
- <polygon
306
- points="60,10 100,30 60,50 20,30"
307
- fill={hovered ? '#92400e' : '#78350f'}
308
- stroke={hovered ? '#fcd34d' : '#fbbf24'}
309
- strokeWidth="1.2"
310
- />
311
- <polygon
312
- points="20,20 20,30 60,50 60,40"
313
- fill={hovered ? '#78350f' : '#451a03'}
314
- stroke={hovered ? '#fbbf24' : '#d97706'}
315
- strokeWidth="1"
316
- />
317
- <polygon
318
- points="100,20 100,30 60,50 60,40"
319
- fill={hovered ? '#7c3a0e' : '#4a2008'}
320
- stroke={hovered ? '#fcd34d' : '#fbbf24'}
321
- strokeWidth="1"
322
- />
323
- <circle cx="44" cy="28" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
324
- <circle cx="60" cy="20" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
325
- <circle cx="76" cy="28" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
326
- </g>
327
- </svg>
328
- )
329
- }
330
-
331
- const iconMap: Record<string, React.FC<{ hovered: boolean }>> = {
332
- uiBlocks: UIBlocksIcon,
333
- templates: TemplatesIcon,
334
- uiKit: UIKitIcon,
335
- }
336
-
337
- /* ── Icon renderer ───────────────────────────────────────────── */
338
-
339
- type MediaLike = {
340
- url?: string | null
341
- mimeType?: string | null
342
- filename?: string | null
343
- }
344
-
345
- function isLocalMediaSrc(src?: string | null): src is string {
346
- return typeof src === 'string' && src.startsWith('/api/media/file/')
347
- }
348
-
349
- function CardIconMedia({ media, hovered }: { media: MediaLike; hovered: boolean }) {
350
- const url = media.url ?? ''
351
- const safeUrl = isLocalMediaSrc(url) ? url : ''
352
- const mime = media.mimeType ?? ''
353
- const filename = media.filename ?? ''
354
- const isLottie =
355
- mime === 'application/json' ||
356
- filename.endsWith('.lottie') ||
357
- (mime === 'application/octet-stream' && filename.endsWith('.lottie'))
358
- const isDotLottie = filename.endsWith('.lottie')
359
- const isJson = mime === 'application/json' || filename.endsWith('.json')
360
-
361
- if (!safeUrl) return null
362
-
363
- if (isDotLottie || isJson) {
364
- return (
365
- <DotLottieReact
366
- src={safeUrl}
367
- loop
368
- autoplay
369
- style={{
370
- width: '100%',
371
- height: '100%',
372
- transform: hovered ? 'scale(1.08)' : 'scale(1)',
373
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
374
- }}
375
- />
376
- )
377
- }
378
-
379
- // SVG or raster image
380
- return (
381
- <Image
382
- src={safeUrl}
383
- alt=""
384
- fill
385
- className="object-contain"
386
- style={{
387
- transform: hovered ? 'scale(1.08)' : 'scale(1)',
388
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
389
- }}
390
- />
391
- )
392
- }
393
-
394
- /* ── Card ────────────────────────────────────────────────────── */
395
-
396
- type CardData = { [key: string]: any }
397
-
398
- function FeatureCard({
399
- card,
400
- index,
401
- isLast,
402
- totalCards,
403
- isMobile,
404
- }: {
405
- card: CardData
406
- index: number
407
- isLast: boolean
408
- totalCards: number
409
- isMobile: boolean
410
- }) {
411
- const [hovered, setHovered] = useState(false)
412
- const colorKey = card.labelColor || 'indigo'
413
- const colors = exploreColorMap[colorKey] || exploreColorMap.indigo
414
- const labelClass = labelColorMap[colorKey] || labelColorMap.indigo
415
- const FallbackIcon = iconMap[card.iconType || 'uiBlocks'] || UIBlocksIcon
416
- const iconMedia =
417
- card.iconMedia && typeof card.iconMedia === 'object' && (card.iconMedia as MediaLike).url
418
- ? (card.iconMedia as MediaLike)
419
- : null
420
-
421
- // On mobile, last card is full-width if total is odd
422
- const isFullWidthOnMobile = isMobile && totalCards % 2 !== 0 && isLast
423
-
424
- let borderStyle: React.CSSProperties = {}
425
- if (isMobile) {
426
- const isEvenPosition = index % 2 === 0
427
- const isSecondRowOrLater = index >= 2
428
- borderStyle = {
429
- borderRight:
430
- isEvenPosition && index + 1 < totalCards && !isFullWidthOnMobile
431
- ? '1px solid rgba(255,255,255,0.07)'
432
- : 'none',
433
- borderTop: isSecondRowOrLater ? '1px solid rgba(255,255,255,0.07)' : 'none',
434
- }
435
- } else {
436
- borderStyle = {
437
- borderRight: !isLast ? '1px solid rgba(255,255,255,0.07)' : 'none',
438
- }
439
- }
440
-
441
- 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' : ''}`
442
-
443
- const innerContent = (
444
- <>
445
- {/* Hover bg glow */}
446
- <div
447
- className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
448
- style={{
449
- background: `radial-gradient(ellipse at 30% 50%, ${colors.glow} 0%, transparent 70%)`,
450
- }}
451
- />
452
-
453
- {/* Icon */}
454
- <div
455
- className="relative shrink-0 w-14 h-11 sm:w-20 sm:h-16"
456
- style={
457
- !iconMedia
458
- ? {
459
- transform: hovered ? 'scale(1.08)' : 'scale(1)',
460
- transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
461
- }
462
- : undefined
463
- }
464
- >
465
- {iconMedia ? (
466
- <CardIconMedia media={iconMedia} hovered={hovered} />
467
- ) : (
468
- <FallbackIcon hovered={hovered} />
469
- )}
470
- </div>
471
-
472
- {/* Text */}
473
- <div className="relative flex flex-col gap-2 pt-1">
474
- <span
475
- className={`text-sm font-bold tracking-[0.15em] uppercase ${labelClass} transition-all duration-300`}
476
- style={{ letterSpacing: hovered ? '0.2em' : '0.15em' }}
477
- >
478
- {card.label}
479
- </span>
480
- <p className="text-sm md:text-md text-stone-700 dark:text-stone-400 leading-relaxed transition-colors duration-300">
481
- {card.description}
482
- </p>
483
- {card.href && (
484
- <span
485
- className={`inline-flex items-center gap-1 text-sm font-medium mt-4 transition-all duration-300 ${colors.base} ${colors.hover}`}
486
- >
487
- Explore
488
- <ArrowRight
489
- size={14}
490
- style={{
491
- transform: hovered ? 'translateX(4px)' : 'translateX(0)',
492
- transition: 'transform 0.3s ease',
493
- }}
494
- />
495
- </span>
496
- )}
497
- </div>
498
- </>
499
- )
500
-
501
- if (card.href) {
502
- return (
503
- <Link
504
- href={card.href}
505
- className={cardClass}
506
- style={borderStyle}
507
- onMouseEnter={() => setHovered(true)}
508
- onMouseLeave={() => setHovered(false)}
509
- >
510
- {innerContent}
511
- </Link>
512
- )
513
- }
514
-
515
- return (
516
- <div
517
- className={cardClass}
518
- style={borderStyle}
519
- onMouseEnter={() => setHovered(true)}
520
- onMouseLeave={() => setHovered(false)}
521
- >
522
- {innerContent}
523
- </div>
524
- )
525
- }
526
-
527
- /* ── Block ───────────────────────────────────────────────────── */
528
-
529
- export type DDFeatStripProps = Record<string, any>
530
-
531
- export const DDFeatStrip: React.FC<DDFeatStripProps> = ({
532
- cards,
533
- containerClass,
534
- sectionId,
535
- customCSS,
536
- }) => {
537
- if (!cards || cards.length === 0) return null
538
-
539
- const cardItems = cards as CardData[]
540
- const [isMobile, setIsMobile] = useState(false)
541
-
542
- useEffect(() => {
543
- const check = () => setIsMobile(window.innerWidth < 768)
544
- check()
545
- window.addEventListener('resize', check)
546
- return () => window.removeEventListener('resize', check)
547
- }, [])
548
-
549
- const totalCards = cardItems.length
550
-
551
- return (
552
- <>
553
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
554
-
555
- <div id={sectionId ?? undefined} className="py-24">
556
- #BlockName-DDFeatStrip
557
- <div
558
- className={`w-full bg-stone-100 dark:bg-stone-950 border-t border-b border-white/6 ${containerClass || ''}`}
559
- >
560
- <div
561
- className="grid grid-cols-1"
562
- style={
563
- !isMobile
564
- ? {
565
- gridTemplateColumns: `repeat(${totalCards}, minmax(0, 1fr))`,
566
- }
567
- : undefined
568
- }
569
- >
570
- {cardItems.map((card, i) => (
571
- <FeatureCard
572
- key={card.id ?? i}
573
- card={card}
574
- index={i}
575
- isLast={i === totalCards - 1}
576
- totalCards={totalCards}
577
- isMobile={isMobile}
578
- />
579
- ))}
580
- </div>
581
- </div>
582
- </div>
583
- </>
584
- )
585
- }
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<string, { base: string; hover: string; glow: string }> = {
27
+ indigo: {
28
+ base: 'text-indigo-300',
29
+ hover: 'group-hover:text-indigo-400',
30
+ glow: 'rgba(99,102,241,0.08)',
31
+ },
32
+ emerald: {
33
+ base: 'text-emerald-300',
34
+ hover: 'group-hover:text-emerald-400',
35
+ glow: 'rgba(16,185,129,0.08)',
36
+ },
37
+ amber: {
38
+ base: 'text-amber-300',
39
+ hover: 'group-hover:text-amber-400',
40
+ glow: 'rgba(245,158,11,0.08)',
41
+ },
42
+ rose: {
43
+ base: 'text-rose-300',
44
+ hover: 'group-hover:text-rose-400',
45
+ glow: 'rgba(244,63,94,0.08)',
46
+ },
47
+ violet: {
48
+ base: 'text-violet-300',
49
+ hover: 'group-hover:text-violet-400',
50
+ glow: 'rgba(167,139,250,0.08)',
51
+ },
52
+ sky: {
53
+ base: 'text-sky-300',
54
+ hover: 'group-hover:text-sky-400',
55
+ glow: 'rgba(56,189,248,0.08)',
56
+ },
57
+ }
58
+
59
+ /* ── Isometric SVG Icons ─────────────────────────────────────── */
60
+
61
+ function UIBlocksIcon({ hovered }: { hovered: boolean }) {
62
+ return (
63
+ <svg
64
+ viewBox="0 0 120 90"
65
+ fill="none"
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ className="w-full h-full"
68
+ style={{
69
+ transition: 'filter 0.4s ease',
70
+ filter: hovered ? 'drop-shadow(0 0 12px rgba(99,102,241,0.7))' : 'none',
71
+ }}
72
+ >
73
+ <g
74
+ style={{
75
+ transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
76
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
77
+ }}
78
+ >
79
+ <polygon
80
+ points="60,4 108,28 60,52 12,28"
81
+ fill={hovered ? '#312e81' : '#1e1b4b'}
82
+ stroke={hovered ? '#818cf8' : '#4f46e5'}
83
+ strokeWidth="1.2"
84
+ />
85
+ <polygon
86
+ points="12,28 12,58 60,82 60,52"
87
+ fill={hovered ? '#1e1b4b' : '#161430'}
88
+ stroke={hovered ? '#6366f1' : '#3730a3'}
89
+ strokeWidth="1.2"
90
+ />
91
+ <polygon
92
+ points="108,28 108,58 60,82 60,52"
93
+ fill={hovered ? '#2e2a6e' : '#1a1740'}
94
+ stroke={hovered ? '#818cf8' : '#4338ca'}
95
+ strokeWidth="1.2"
96
+ />
97
+ <line
98
+ x1="60"
99
+ y1="4"
100
+ x2="60"
101
+ y2="52"
102
+ stroke={hovered ? '#818cf8' : '#4f46e5'}
103
+ strokeWidth="0.6"
104
+ strokeDasharray="3 3"
105
+ />
106
+ <line
107
+ x1="36"
108
+ y1="16"
109
+ x2="84"
110
+ y2="40"
111
+ stroke={hovered ? '#818cf8' : '#4f46e5'}
112
+ strokeWidth="0.6"
113
+ strokeDasharray="3 3"
114
+ />
115
+ <line
116
+ x1="84"
117
+ y1="16"
118
+ x2="36"
119
+ y2="40"
120
+ stroke={hovered ? '#818cf8' : '#4f46e5'}
121
+ strokeWidth="0.6"
122
+ strokeDasharray="3 3"
123
+ />
124
+ <polygon
125
+ points="60,14 78,23 60,32 42,23"
126
+ fill={hovered ? '#4f46e5' : '#312e81'}
127
+ stroke={hovered ? '#a5b4fc' : '#6366f1'}
128
+ strokeWidth="1"
129
+ />
130
+ <polygon
131
+ points="42,23 42,33 60,42 60,32"
132
+ fill={hovered ? '#3730a3' : '#1e1b4b'}
133
+ stroke={hovered ? '#818cf8' : '#4338ca'}
134
+ strokeWidth="1"
135
+ />
136
+ <polygon
137
+ points="78,23 78,33 60,42 60,32"
138
+ fill={hovered ? '#4338ca' : '#272466'}
139
+ stroke={hovered ? '#a5b4fc' : '#6366f1'}
140
+ strokeWidth="1"
141
+ />
142
+ </g>
143
+ </svg>
144
+ )
145
+ }
146
+
147
+ function TemplatesIcon({ hovered }: { hovered: boolean }) {
148
+ return (
149
+ <svg
150
+ viewBox="0 0 120 90"
151
+ fill="none"
152
+ xmlns="http://www.w3.org/2000/svg"
153
+ className="w-full h-full"
154
+ style={{
155
+ transition: 'filter 0.4s ease',
156
+ filter: hovered ? 'drop-shadow(0 0 12px rgba(16,185,129,0.6))' : 'none',
157
+ }}
158
+ >
159
+ <g
160
+ style={{
161
+ transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
162
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
163
+ }}
164
+ >
165
+ <polygon
166
+ points="60,6 104,30 60,54 16,30"
167
+ fill={hovered ? '#064e3b' : '#022c22'}
168
+ stroke={hovered ? '#34d399' : '#059669'}
169
+ strokeWidth="1.2"
170
+ />
171
+ <polygon
172
+ points="16,30 16,60 60,84 60,54"
173
+ fill={hovered ? '#022c22' : '#011a15'}
174
+ stroke={hovered ? '#10b981' : '#047857'}
175
+ strokeWidth="1.2"
176
+ />
177
+ <polygon
178
+ points="104,30 104,60 60,84 60,54"
179
+ fill={hovered ? '#043927' : '#011a15'}
180
+ stroke={hovered ? '#34d399' : '#059669'}
181
+ strokeWidth="1.2"
182
+ />
183
+ <line
184
+ x1="30"
185
+ y1="30"
186
+ x2="60"
187
+ y2="16"
188
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
189
+ strokeWidth="1"
190
+ opacity="0.7"
191
+ />
192
+ <line
193
+ x1="30"
194
+ y1="36"
195
+ x2="60"
196
+ y2="22"
197
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
198
+ strokeWidth="0.8"
199
+ opacity="0.5"
200
+ />
201
+ <line
202
+ x1="30"
203
+ y1="42"
204
+ x2="52"
205
+ y2="30"
206
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
207
+ strokeWidth="0.8"
208
+ opacity="0.4"
209
+ />
210
+ <line
211
+ x1="72"
212
+ y1="22"
213
+ x2="96"
214
+ y2="34"
215
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
216
+ strokeWidth="1"
217
+ opacity="0.7"
218
+ />
219
+ <line
220
+ x1="72"
221
+ y1="28"
222
+ x2="90"
223
+ y2="37"
224
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
225
+ strokeWidth="0.8"
226
+ opacity="0.5"
227
+ />
228
+ <line
229
+ x1="72"
230
+ y1="34"
231
+ x2="84"
232
+ y2="40"
233
+ stroke={hovered ? '#6ee7b7' : '#34d399'}
234
+ strokeWidth="0.8"
235
+ opacity="0.4"
236
+ />
237
+ <line
238
+ x1="66"
239
+ y1="12"
240
+ x2="66"
241
+ y2="50"
242
+ stroke={hovered ? '#34d399' : '#059669'}
243
+ strokeWidth="0.8"
244
+ strokeDasharray="2 2"
245
+ />
246
+ </g>
247
+ </svg>
248
+ )
249
+ }
250
+
251
+ function UIKitIcon({ hovered }: { hovered: boolean }) {
252
+ return (
253
+ <svg
254
+ viewBox="0 0 120 90"
255
+ fill="none"
256
+ xmlns="http://www.w3.org/2000/svg"
257
+ className="w-full h-full"
258
+ style={{
259
+ transition: 'filter 0.4s ease',
260
+ filter: hovered ? 'drop-shadow(0 0 12px rgba(245,158,11,0.6))' : 'none',
261
+ }}
262
+ >
263
+ <g
264
+ style={{
265
+ transform: hovered ? 'translateY(-4px)' : 'translateY(0)',
266
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
267
+ }}
268
+ >
269
+ <polygon
270
+ points="60,42 100,62 60,82 20,62"
271
+ fill={hovered ? '#451a03' : '#27100a'}
272
+ stroke={hovered ? '#f59e0b' : '#b45309'}
273
+ strokeWidth="1.2"
274
+ />
275
+ <polygon
276
+ points="20,52 20,62 60,82 60,72"
277
+ fill={hovered ? '#27100a' : '#1a0a05'}
278
+ stroke={hovered ? '#d97706' : '#92400e'}
279
+ strokeWidth="1"
280
+ />
281
+ <polygon
282
+ points="100,52 100,62 60,82 60,72"
283
+ fill={hovered ? '#3b1a06' : '#1a0a05'}
284
+ stroke={hovered ? '#f59e0b' : '#b45309'}
285
+ strokeWidth="1"
286
+ />
287
+ <polygon
288
+ points="60,26 100,46 60,66 20,46"
289
+ fill={hovered ? '#78350f' : '#451a03'}
290
+ stroke={hovered ? '#fbbf24' : '#d97706'}
291
+ strokeWidth="1.2"
292
+ />
293
+ <polygon
294
+ points="20,36 20,46 60,66 60,56"
295
+ fill={hovered ? '#451a03' : '#27100a'}
296
+ stroke={hovered ? '#f59e0b' : '#b45309'}
297
+ strokeWidth="1"
298
+ />
299
+ <polygon
300
+ points="100,36 100,46 60,66 60,56"
301
+ fill={hovered ? '#5c2a07' : '#2d1108'}
302
+ stroke={hovered ? '#fbbf24' : '#d97706'}
303
+ strokeWidth="1"
304
+ />
305
+ <polygon
306
+ points="60,10 100,30 60,50 20,30"
307
+ fill={hovered ? '#92400e' : '#78350f'}
308
+ stroke={hovered ? '#fcd34d' : '#fbbf24'}
309
+ strokeWidth="1.2"
310
+ />
311
+ <polygon
312
+ points="20,20 20,30 60,50 60,40"
313
+ fill={hovered ? '#78350f' : '#451a03'}
314
+ stroke={hovered ? '#fbbf24' : '#d97706'}
315
+ strokeWidth="1"
316
+ />
317
+ <polygon
318
+ points="100,20 100,30 60,50 60,40"
319
+ fill={hovered ? '#7c3a0e' : '#4a2008'}
320
+ stroke={hovered ? '#fcd34d' : '#fbbf24'}
321
+ strokeWidth="1"
322
+ />
323
+ <circle cx="44" cy="28" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
324
+ <circle cx="60" cy="20" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
325
+ <circle cx="76" cy="28" r="2.5" fill={hovered ? '#fcd34d' : '#fbbf24'} opacity="0.9" />
326
+ </g>
327
+ </svg>
328
+ )
329
+ }
330
+
331
+ const iconMap: Record<string, React.FC<{ hovered: boolean }>> = {
332
+ uiBlocks: UIBlocksIcon,
333
+ templates: TemplatesIcon,
334
+ uiKit: UIKitIcon,
335
+ }
336
+
337
+ /* ── Icon renderer ───────────────────────────────────────────── */
338
+
339
+ type MediaLike = {
340
+ url?: string | null
341
+ mimeType?: string | null
342
+ filename?: string | null
343
+ }
344
+
345
+ function isLocalMediaSrc(src?: string | null): src is string {
346
+ return typeof src === 'string' && src.startsWith('/api/media/file/')
347
+ }
348
+
349
+ function CardIconMedia({ media, hovered }: { media: MediaLike; hovered: boolean }) {
350
+ const url = media.url ?? ''
351
+ const safeUrl = isLocalMediaSrc(url) ? url : ''
352
+ const mime = media.mimeType ?? ''
353
+ const filename = media.filename ?? ''
354
+ const isLottie =
355
+ mime === 'application/json' ||
356
+ filename.endsWith('.lottie') ||
357
+ (mime === 'application/octet-stream' && filename.endsWith('.lottie'))
358
+ const isDotLottie = filename.endsWith('.lottie')
359
+ const isJson = mime === 'application/json' || filename.endsWith('.json')
360
+
361
+ if (!safeUrl) return null
362
+
363
+ if (isDotLottie || isJson) {
364
+ return (
365
+ <DotLottieReact
366
+ src={safeUrl}
367
+ loop
368
+ autoplay
369
+ style={{
370
+ width: '100%',
371
+ height: '100%',
372
+ transform: hovered ? 'scale(1.08)' : 'scale(1)',
373
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
374
+ }}
375
+ />
376
+ )
377
+ }
378
+
379
+ // SVG or raster image
380
+ return (
381
+ <Image
382
+ src={safeUrl}
383
+ alt=""
384
+ fill
385
+ className="object-contain"
386
+ style={{
387
+ transform: hovered ? 'scale(1.08)' : 'scale(1)',
388
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
389
+ }}
390
+ />
391
+ )
392
+ }
393
+
394
+ /* ── Card ────────────────────────────────────────────────────── */
395
+
396
+ type CardData = { [key: string]: any }
397
+
398
+ function FeatureCard({
399
+ card,
400
+ index,
401
+ isLast,
402
+ totalCards,
403
+ isMobile,
404
+ }: {
405
+ card: CardData
406
+ index: number
407
+ isLast: boolean
408
+ totalCards: number
409
+ isMobile: boolean
410
+ }) {
411
+ const [hovered, setHovered] = useState(false)
412
+ const colorKey = card.labelColor || 'indigo'
413
+ const colors = exploreColorMap[colorKey] || exploreColorMap.indigo
414
+ const labelClass = labelColorMap[colorKey] || labelColorMap.indigo
415
+ const FallbackIcon = iconMap[card.iconType || 'uiBlocks'] || UIBlocksIcon
416
+ const iconMedia =
417
+ card.iconMedia && typeof card.iconMedia === 'object' && (card.iconMedia as MediaLike).url
418
+ ? (card.iconMedia as MediaLike)
419
+ : null
420
+
421
+ // On mobile, last card is full-width if total is odd
422
+ const isFullWidthOnMobile = isMobile && totalCards % 2 !== 0 && isLast
423
+
424
+ let borderStyle: React.CSSProperties = {}
425
+ if (isMobile) {
426
+ const isEvenPosition = index % 2 === 0
427
+ const isSecondRowOrLater = index >= 2
428
+ borderStyle = {
429
+ borderRight:
430
+ isEvenPosition && index + 1 < totalCards && !isFullWidthOnMobile
431
+ ? '1px solid rgba(255,255,255,0.07)'
432
+ : 'none',
433
+ borderTop: isSecondRowOrLater ? '1px solid rgba(255,255,255,0.07)' : 'none',
434
+ }
435
+ } else {
436
+ borderStyle = {
437
+ borderRight: !isLast ? '1px solid rgba(255,255,255,0.07)' : 'none',
438
+ }
439
+ }
440
+
441
+ 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' : ''}`
442
+
443
+ const innerContent = (
444
+ <>
445
+ {/* Hover bg glow */}
446
+ <div
447
+ className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
448
+ style={{
449
+ background: `radial-gradient(ellipse at 30% 50%, ${colors.glow} 0%, transparent 70%)`,
450
+ }}
451
+ />
452
+
453
+ {/* Icon */}
454
+ <div
455
+ className="relative shrink-0 w-14 h-11 sm:w-20 sm:h-16"
456
+ style={
457
+ !iconMedia
458
+ ? {
459
+ transform: hovered ? 'scale(1.08)' : 'scale(1)',
460
+ transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',
461
+ }
462
+ : undefined
463
+ }
464
+ >
465
+ {iconMedia ? (
466
+ <CardIconMedia media={iconMedia} hovered={hovered} />
467
+ ) : (
468
+ <FallbackIcon hovered={hovered} />
469
+ )}
470
+ </div>
471
+
472
+ {/* Text */}
473
+ <div className="relative flex flex-col gap-2 pt-1">
474
+ <span
475
+ className={`text-sm font-bold tracking-[0.15em] uppercase ${labelClass} transition-all duration-300`}
476
+ style={{ letterSpacing: hovered ? '0.2em' : '0.15em' }}
477
+ >
478
+ {card.label}
479
+ </span>
480
+ <p className="text-sm md:text-md text-stone-700 dark:text-stone-400 leading-relaxed transition-colors duration-300">
481
+ {card.description}
482
+ </p>
483
+ {card.href && (
484
+ <span
485
+ className={`inline-flex items-center gap-1 text-sm font-medium mt-4 transition-all duration-300 ${colors.base} ${colors.hover}`}
486
+ >
487
+ Explore
488
+ <ArrowRight
489
+ size={14}
490
+ style={{
491
+ transform: hovered ? 'translateX(4px)' : 'translateX(0)',
492
+ transition: 'transform 0.3s ease',
493
+ }}
494
+ />
495
+ </span>
496
+ )}
497
+ </div>
498
+ </>
499
+ )
500
+
501
+ if (card.href) {
502
+ return (
503
+ <Link
504
+ href={card.href}
505
+ className={cardClass}
506
+ style={borderStyle}
507
+ onMouseEnter={() => setHovered(true)}
508
+ onMouseLeave={() => setHovered(false)}
509
+ >
510
+ {innerContent}
511
+ </Link>
512
+ )
513
+ }
514
+
515
+ return (
516
+ <div
517
+ className={cardClass}
518
+ style={borderStyle}
519
+ onMouseEnter={() => setHovered(true)}
520
+ onMouseLeave={() => setHovered(false)}
521
+ >
522
+ {innerContent}
523
+ </div>
524
+ )
525
+ }
526
+
527
+ /* ── Block ───────────────────────────────────────────────────── */
528
+
529
+ export type DDFeatStripProps = Record<string, any>
530
+
531
+ export const DDFeatStrip: React.FC<DDFeatStripProps> = ({
532
+ cards,
533
+ containerClass,
534
+ sectionId,
535
+ customCSS,
536
+ }) => {
537
+ if (!cards || cards.length === 0) return null
538
+
539
+ const cardItems = cards as CardData[]
540
+ const [isMobile, setIsMobile] = useState(false)
541
+
542
+ useEffect(() => {
543
+ const check = () => setIsMobile(window.innerWidth < 768)
544
+ check()
545
+ window.addEventListener('resize', check)
546
+ return () => window.removeEventListener('resize', check)
547
+ }, [])
548
+
549
+ const totalCards = cardItems.length
550
+
551
+ return (
552
+ <>
553
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
554
+
555
+ <div id={sectionId ?? undefined} className="py-4">
556
+ <div
557
+ className={`w-full bg-stone-100 dark:bg-stone-950 border-t border-b border-white/6 ${containerClass || ''}`}
558
+ >
559
+ <div
560
+ className="grid grid-cols-1"
561
+ style={
562
+ !isMobile
563
+ ? {
564
+ gridTemplateColumns: `repeat(${totalCards}, minmax(0, 1fr))`,
565
+ }
566
+ : undefined
567
+ }
568
+ >
569
+ {cardItems.map((card, i) => (
570
+ <FeatureCard
571
+ key={card.id ?? i}
572
+ card={card}
573
+ index={i}
574
+ isLast={i === totalCards - 1}
575
+ totalCards={totalCards}
576
+ isMobile={isMobile}
577
+ />
578
+ ))}
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </>
583
+ )
584
+ }