blocks-dusted 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/src/installers/patchRenderBlocks.js +1 -1
  6. package/src/registry/validateTemplateManifest.js +2 -1
  7. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  8. package/templates/blocks/BannerDD790432/config.ts +54 -0
  9. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  10. package/templates/blocks/Button47/Component.tsx +124 -0
  11. package/templates/blocks/Button47/config.ts +75 -0
  12. package/templates/blocks/Button47/link.ts +22 -0
  13. package/templates/blocks/Button47/manifest.json +105 -0
  14. package/templates/blocks/Button47/styles.module.scss +75 -0
  15. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  16. package/templates/blocks/DD-Accordion/config.ts +71 -59
  17. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  18. package/templates/blocks/DD-AnimText/config.ts +296 -295
  19. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  20. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  21. package/templates/blocks/DD-BookCall/Component.tsx +10 -9
  22. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  23. package/templates/blocks/DD-Card/Component.tsx +4 -4
  24. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
  25. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  28. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  29. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  30. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  31. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  32. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  33. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  34. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  35. package/templates/blocks/DD-Contact/Component.tsx +433 -434
  36. package/templates/blocks/DD-Counter/Component.tsx +18 -13
  37. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  38. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  39. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  40. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  41. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  42. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  43. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  44. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  45. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  46. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  47. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  48. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  49. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  50. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  51. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  52. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  53. package/templates/blocks/DD-Hero/Component.tsx +16 -40
  54. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  55. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  56. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  57. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  58. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
  59. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  60. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  61. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  62. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  63. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  64. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  65. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  66. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
  67. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  68. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  69. package/templates/blocks/DD-Masonry/config.ts +475 -475
  70. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  71. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  72. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  73. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  74. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  75. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  76. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  77. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  78. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  79. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  80. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  81. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  82. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  83. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  84. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  85. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  86. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  87. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  88. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  89. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  90. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  91. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  92. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  95. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  96. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  97. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  98. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  99. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  100. package/templates/blocks/DD-Pricing/Component.tsx +368 -372
  101. package/templates/blocks/DD-Pricing/config.ts +215 -215
  102. package/templates/blocks/DD-Process/Component.tsx +147 -149
  103. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  104. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  105. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  106. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  107. package/templates/blocks/DD-Section/Component.tsx +265 -266
  108. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  109. package/templates/blocks/DD-Services/Component.tsx +176 -176
  110. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  111. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  112. package/templates/blocks/DD-StackCards/Component.tsx +474 -473
  113. package/templates/blocks/DD-Team/Component.tsx +247 -247
  114. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  115. package/templates/blocks/DD-TechStack/Component.tsx +161 -168
  116. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  117. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  118. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  119. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  120. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  121. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  122. package/templates/blocks/DD-Work/Component.tsx +315 -315
  123. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  124. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  125. package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
  126. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
  127. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  128. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
  129. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  130. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  131. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
  132. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  133. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  134. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  135. package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
  136. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  137. package/templates/blocks/Form/Component.tsx +163 -0
  138. package/templates/blocks/Form/Country/index.tsx +65 -0
  139. package/templates/blocks/Form/Country/options.ts +982 -0
  140. package/templates/blocks/Form/Email/index.tsx +38 -0
  141. package/templates/blocks/Form/Error/index.tsx +15 -0
  142. package/templates/blocks/Form/Message/index.tsx +13 -0
  143. package/templates/blocks/Form/Number/index.tsx +36 -0
  144. package/templates/blocks/Form/Select/index.tsx +63 -0
  145. package/templates/blocks/Form/State/index.tsx +64 -0
  146. package/templates/blocks/Form/State/options.ts +52 -0
  147. package/templates/blocks/Form/Text/index.tsx +32 -0
  148. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  149. package/templates/blocks/Form/Width/index.tsx +13 -0
  150. package/templates/blocks/Form/config.ts +51 -0
  151. package/templates/blocks/Form/fields.tsx +21 -0
  152. package/templates/blocks/Form/manifest.json +168 -0
  153. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  154. package/templates/blocks/MediaBlock/config.ts +14 -0
  155. package/templates/blocks/MediaBlock/manifest.json +92 -0
  156. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  157. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  158. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  159. package/templates/blocks/rawHtml/Component.tsx +25 -0
  160. package/templates/blocks/rawHtml/config.ts +56 -0
  161. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -1,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
-
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
+ }