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,249 +1,251 @@
1
- 'use client'
2
-
3
- import React, { useState, useCallback, useEffect } from 'react'
4
- import Link from 'next/link'
5
- import Image from 'next/image'
6
- import { ChevronLeft, ChevronRight } from 'lucide-react'
7
- import { cn } from '@/utilities/ui'
8
- import useEmblaCarousel from 'embla-carousel-react'
9
-
10
- type MediaObj = { url?: string | null; alt?: string | null }
11
-
12
- function isValidImageSrc(src?: string | null): src is string {
13
- return Boolean(src && src.startsWith('/api/media/file/'))
14
- }
15
-
16
- type CarouselItemA = {
17
- id?: string | null
18
- name: string
19
- category?: string | null
20
- badge?: string | null
21
- image?: MediaObj | string | null
22
- imageFallbackUrl?: string | null
23
- description?: string | null
24
- href?: string | null
25
- features?: Array<{ id?: string | null; label?: string | null }> | null
26
- }
27
-
28
- export type DDCarouselAProps = {
29
- blockType: 'DD-Carousel-A'
30
- blockName?: string | null
31
- eyebrow?: string | null
32
- heading?: string | null
33
- subtext?: string | null
34
- viewAllLabel?: string | null
35
- viewAllHref?: string | null
36
- items?: CarouselItemA[] | null
37
- sectionId?: string | null
38
- customCSS?: string | null
39
- }
40
-
41
- export const DDCarouselA: React.FC<DDCarouselAProps> = ({
42
- eyebrow,
43
- heading,
44
- subtext,
45
- viewAllLabel = 'View All',
46
- viewAllHref,
47
- items: itemsProp,
48
- sectionId,
49
- customCSS,
50
- }) => {
51
- const items = itemsProp ?? []
52
- const [emblaRef, emblaApi] = useEmblaCarousel({
53
- align: 'start',
54
- slidesToScroll: 1,
55
- containScroll: 'trimSnaps',
56
- dragFree: false,
57
- })
58
- const [activeIdx, setActiveIdx] = useState(0)
59
- const [canPrev, setCanPrev] = useState(false)
60
- const [canNext, setCanNext] = useState(true)
61
-
62
- const onSelect = useCallback(() => {
63
- if (!emblaApi) return
64
- setActiveIdx(emblaApi.selectedScrollSnap())
65
- setCanPrev(emblaApi.canScrollPrev())
66
- setCanNext(emblaApi.canScrollNext())
67
- }, [emblaApi])
68
-
69
- useEffect(() => {
70
- if (!emblaApi) return
71
- onSelect()
72
- emblaApi.on('select', onSelect)
73
- emblaApi.on('reInit', onSelect)
74
- return () => {
75
- emblaApi.off('select', onSelect)
76
- emblaApi.off('reInit', onSelect)
77
- }
78
- }, [emblaApi, onSelect])
79
-
80
- const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi])
81
- const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi])
82
- const scrollTo = useCallback((i: number) => emblaApi?.scrollTo(i), [emblaApi])
83
-
84
- const snapCount = emblaApi?.scrollSnapList().length ?? 0
85
-
86
- if (!items.length) return null
87
-
88
- return (
89
- <section id={sectionId ?? undefined} className="py-20 overflow-hidden">
90
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
91
- <div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
92
- {/* Header */}
93
- <div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-4 px-0 md:px-4">
94
- <div>
95
- {eyebrow && (
96
- <div className="text-lime-500 text-xs font-bold uppercase tracking-widest mb-2">
97
- {eyebrow}
98
- </div>
99
- )}
100
- {heading && (
101
- <h2 className="text-4xl lg:text-5xl font-bold text-stone-900 dark:text-stone-50 leading-tight">
102
- {heading}
103
- </h2>
104
- )}
105
- {subtext && <p className="text-stone-400 mt-3 text-base max-w-xl">{subtext}</p>}
106
- </div>
107
- <div className="flex items-center gap-3 shrink-0">
108
- <button
109
- onClick={scrollPrev}
110
- disabled={!canPrev}
111
- aria-label="Previous"
112
- className="w-11 h-11 flex items-center justify-center rounded-full border-2 border-white/30 text-stone-900 dark:text-stone-50 hover:border-white hover:bg-stone-50 hover:text-stone-900 transition-all disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
113
- >
114
- <ChevronLeft className="w-5 h-5" />
115
- </button>
116
- <button
117
- onClick={scrollNext}
118
- disabled={!canNext}
119
- aria-label="Next"
120
- className="w-11 h-11 flex items-center justify-center rounded-full bg-lime-500 text-stone-900 hover:bg-lime-400 transition-all disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
121
- >
122
- <ChevronRight className="w-5 h-5" />
123
- </button>
124
- {viewAllHref && (
125
- <Link
126
- href={viewAllHref}
127
- className="ml-2 text-sm font-semibold text-stone-600 dark:text-stone-300 hover:text-lime-500 transition-colors whitespace-nowrap"
128
- >
129
- {viewAllLabel} →
130
- </Link>
131
- )}
132
- </div>
133
- </div>
134
- {/* Embla viewport */}
135
- <div
136
- ref={emblaRef}
137
- className="overflow-visible select-none cursor-grab active:cursor-grabbing"
138
- >
139
- <div className="flex gap-6 py-4">
140
- {items.map((item, i) => {
141
- const candidateImgSrc =
142
- item.image && typeof item.image === 'object' && (item.image as MediaObj).url
143
- ? (item.image as MediaObj).url!
144
- : (item.imageFallbackUrl ?? '')
145
- const imgSrc = isValidImageSrc(candidateImgSrc) ? candidateImgSrc : ''
146
-
147
- return (
148
- <div
149
- key={item.id ?? i}
150
- className="flex-none w-[calc(100%-0px)] sm:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] min-w-70"
151
- >
152
- <Link href={item.href ?? '#'} className="block group h-full" draggable={false}>
153
- <div className="border border-border rounded-2xl overflow-hidden hover:-translate-y-2 hover:outline-3 hover:outline-lime-500/30 hover:outline-solid hover:outline-offset-2 transition-all duration-300 hover:shadow-xl hover:border-lime-500/30 h-full flex flex-col">
154
- {/* Image */}
155
- <div className="relative h-52 overflow-hidden bg-stone-900 shrink-0 ">
156
- {imgSrc ? (
157
- <Image
158
- src={imgSrc}
159
- alt={item.name}
160
- fill
161
- sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
162
- className="w-full h-full object-cover object-top group-hover:scale-105 transition-transform duration-500"
163
- draggable={false}
164
- />
165
- ) : (
166
- <div className="w-full h-full bg-linear-to-br from-violet-950 to-indigo-950" />
167
- )}
168
-
169
- {item.badge && (
170
- <div className="absolute top-3 left-3 bg-lime-500 text-stone-900 text-xs font-bold px-3 py-1 rounded-full z-10">
171
- {item.badge}
172
- </div>
173
- )}
174
-
175
- <div className="absolute inset-0 bg-linear-to-t from-stone-900/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
176
-
177
- <div className="absolute bottom-3 left-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
178
- <span className="text-stone-900 dark:text-stone-50 text-sm font-semibold">
179
- View Details →
180
- </span>
181
- </div>
182
- </div>
183
-
184
- {/* Content */}
185
- <div className="p-5 flex flex-col flex-1">
186
- {item.category && (
187
- <div className="text-lime-500 text-xs font-semibold uppercase tracking-widest mb-1">
188
- {item.category}
189
- </div>
190
- )}
191
-
192
- <h3 className="text-stone-900 dark:text-stone-50 font-bold text-base leading-snug mb-2 line-clamp-2">
193
- {item.name}
194
- </h3>
195
-
196
- {item.description && (
197
- <div className="grid grid-rows-[0fr] group-hover:grid-rows-[1fr] transition-[grid-template-rows,margin] duration-300 ease-out mb-0 group-hover:mb-4">
198
- <p className="min-h-0 overflow-hidden text-stone-400 text-sm leading-relaxed line-clamp-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
199
- {item.description}
200
- </p>
201
- </div>
202
- )}
203
-
204
- {item.features && item.features.length > 0 && (
205
- <div className="flex flex-wrap gap-1.5 mt-auto">
206
- {item.features.slice(0, 3).map((f, fi) => (
207
- <span
208
- key={f.id ?? fi}
209
- className="text-xs bg-lime-500/10 text-lime-400 px-2.5 py-1 rounded-full"
210
- >
211
- {f.label}
212
- </span>
213
- ))}
214
- </div>
215
- )}
216
- </div>
217
- </div>
218
- </Link>
219
- </div>
220
- )
221
- })}
222
- </div>
223
- </div>
224
- {/* Dot indicators */}
225
- {snapCount > 1 && (
226
- <div className="flex justify-center gap-2 mt-0">
227
- {Array.from({ length: snapCount }).map((_, i) => (
228
- <button
229
- key={i}
230
- onClick={() => scrollTo(i)}
231
- aria-label={`Go to slide ${i + 1}`}
232
- className="flex h-11 w-11 items-center justify-center rounded-full cursor-pointer"
233
- >
234
- <span
235
- className={cn(
236
- 'block rounded-full transition-all duration-300',
237
- i === activeIdx
238
- ? 'h-2.5 w-8 bg-lime-500'
239
- : 'h-2.5 w-2.5 bg-stone-600 hover:bg-stone-400',
240
- )}
241
- />
242
- </button>
243
- ))}
244
- </div>
245
- )}
246
- </div>
247
- </section>
248
- )
249
- }
1
+ 'use client'
2
+
3
+ import React, { useState, useCallback, useEffect } from 'react'
4
+ import Link from 'next/link'
5
+ import Image from 'next/image'
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
7
+ import { cn } from '@/utilities/ui'
8
+ import useEmblaCarousel from 'embla-carousel-react'
9
+
10
+ type MediaObj = { url?: string | null; alt?: string | null }
11
+
12
+ function isValidImageSrc(src?: string | null): src is string {
13
+ return Boolean(src && src.startsWith('/api/media/file/'))
14
+ }
15
+
16
+ type CarouselItemA = {
17
+ id?: string | null
18
+ name: string
19
+ category?: string | null
20
+ badge?: string | null
21
+ image?: MediaObj | string | null
22
+ imageFallbackUrl?: string | null
23
+ description?: string | null
24
+ href?: string | null
25
+ features?: Array<{ id?: string | null; label?: string | null }> | null
26
+ }
27
+
28
+ export type DDCarouselAProps = {
29
+ blockType: 'DD-Carousel-A'
30
+ blockName?: string | null
31
+ eyebrow?: string | null
32
+ heading?: string | null
33
+ subtext?: string | null
34
+ viewAllLabel?: string | null
35
+ viewAllHref?: string | null
36
+ items?: CarouselItemA[] | null
37
+ sectionId?: string | null
38
+ customCSS?: string | null
39
+ }
40
+
41
+ export const DDCarouselA: React.FC<DDCarouselAProps> = ({
42
+ eyebrow,
43
+ heading,
44
+ subtext,
45
+ viewAllLabel = 'View All',
46
+ viewAllHref,
47
+ items: itemsProp,
48
+ sectionId,
49
+ customCSS,
50
+ }) => {
51
+ const items = itemsProp ?? []
52
+ const [emblaRef, emblaApi] = useEmblaCarousel({
53
+ align: 'start',
54
+ slidesToScroll: 1,
55
+ containScroll: 'trimSnaps',
56
+ dragFree: false,
57
+ })
58
+ const [activeIdx, setActiveIdx] = useState(0)
59
+ const [canPrev, setCanPrev] = useState(false)
60
+ const [canNext, setCanNext] = useState(true)
61
+
62
+ const onSelect = useCallback(() => {
63
+ if (!emblaApi) return
64
+ setActiveIdx(emblaApi.selectedScrollSnap())
65
+ setCanPrev(emblaApi.canScrollPrev())
66
+ setCanNext(emblaApi.canScrollNext())
67
+ }, [emblaApi])
68
+
69
+ useEffect(() => {
70
+ if (!emblaApi) return
71
+ onSelect()
72
+ emblaApi.on('select', onSelect)
73
+ emblaApi.on('reInit', onSelect)
74
+ return () => {
75
+ emblaApi.off('select', onSelect)
76
+ emblaApi.off('reInit', onSelect)
77
+ }
78
+ }, [emblaApi, onSelect])
79
+
80
+ const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi])
81
+ const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi])
82
+ const scrollTo = useCallback((i: number) => emblaApi?.scrollTo(i), [emblaApi])
83
+
84
+ const snapCount = emblaApi?.scrollSnapList().length ?? 0
85
+
86
+ if (!items.length) return null
87
+
88
+ return (
89
+ <section
90
+ id={sectionId ?? undefined}
91
+ className="relative left-0 mx-auto w-screen max-w-none overflow-hidden py-20"
92
+ >
93
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
94
+ <div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
95
+ <div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-4 px-0 md:px-4">
96
+ <div>
97
+ {eyebrow && (
98
+ <div className="text-purple-600 dark:text-lime-500 text-xs font-bold uppercase tracking-widest mb-2">
99
+ {eyebrow}
100
+ </div>
101
+ )}
102
+ {heading && (
103
+ <h2 className="text-4xl lg:text-5xl font-bold text-stone-900 dark:text-stone-50 leading-tight">
104
+ {heading}
105
+ </h2>
106
+ )}
107
+ {subtext && <p className="text-stone-400 mt-3 text-base max-w-xl">{subtext}</p>}
108
+ </div>
109
+ <div className="flex items-center gap-3 shrink-0">
110
+ <button
111
+ onClick={scrollPrev}
112
+ disabled={!canPrev}
113
+ aria-label="Previous"
114
+ className="w-11 h-11 flex items-center justify-center rounded-full border-2 border-white/30 text-stone-900 dark:text-stone-50 hover:border-white hover:bg-stone-50 hover:text-stone-900 transition-all disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
115
+ >
116
+ <ChevronLeft className="w-5 h-5" />
117
+ </button>
118
+ <button
119
+ onClick={scrollNext}
120
+ disabled={!canNext}
121
+ aria-label="Next"
122
+ className="w-11 h-11 flex items-center justify-center rounded-full bg-purple-600 text-stone-50 hover:bg-purple-500 dark:bg-lime-500 dark:text-stone-900 dark:hover:bg-lime-400 transition-all disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
123
+ >
124
+ <ChevronRight className="w-5 h-5" />
125
+ </button>
126
+ {viewAllHref && (
127
+ <Link
128
+ href={viewAllHref}
129
+ className="ml-2 text-sm font-semibold text-stone-600 dark:text-stone-300 hover:text-purple-600 dark:hover:text-lime-500 transition-colors whitespace-nowrap"
130
+ >
131
+ {viewAllLabel} →
132
+ </Link>
133
+ )}
134
+ </div>
135
+ </div>
136
+ {/* Embla viewport */}
137
+ <div
138
+ ref={emblaRef}
139
+ className="overflow-visible select-none cursor-grab active:cursor-grabbing"
140
+ >
141
+ <div className="flex gap-6 py-4">
142
+ {items.map((item, i) => {
143
+ const candidateImgSrc =
144
+ item.image && typeof item.image === 'object' && (item.image as MediaObj).url
145
+ ? (item.image as MediaObj).url!
146
+ : (item.imageFallbackUrl ?? '')
147
+ const imgSrc = isValidImageSrc(candidateImgSrc) ? candidateImgSrc : ''
148
+
149
+ return (
150
+ <div
151
+ key={item.id ?? i}
152
+ className="flex-none w-[calc(100%-0px)] sm:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] min-w-70"
153
+ >
154
+ <Link href={item.href ?? '#'} className="block group h-full" draggable={false}>
155
+ <div className="border border-border rounded-2xl overflow-hidden hover:-translate-y-2 hover:outline-3 hover:outline-purple-500/30 dark:hover:outline-lime-500/30 hover:outline-solid hover:outline-offset-2 transition-all duration-300 hover:shadow-xl hover:border-purple-500/30 dark:hover:border-lime-500/30 h-full flex flex-col">
156
+ {/* Image */}
157
+ <div className="relative h-52 overflow-hidden bg-stone-900 shrink-0 ">
158
+ {imgSrc ? (
159
+ <Image
160
+ src={imgSrc}
161
+ alt={item.name}
162
+ fill
163
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
164
+ className="w-full h-full object-cover object-top group-hover:scale-105 transition-transform duration-500"
165
+ draggable={false}
166
+ />
167
+ ) : (
168
+ <div className="w-full h-full bg-linear-to-br from-violet-950 to-indigo-950" />
169
+ )}
170
+
171
+ {item.badge && (
172
+ <div className="absolute top-3 left-3 bg-purple-600 text-stone-50 dark:bg-lime-500 dark:text-stone-900 text-xs font-bold px-3 py-1 rounded-full z-10">
173
+ {item.badge}
174
+ </div>
175
+ )}
176
+
177
+ <div className="absolute inset-0 bg-linear-to-t from-stone-900/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
178
+
179
+ <div className="absolute bottom-3 left-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
180
+ <span className="text-stone-900 dark:text-stone-50 text-sm font-semibold">
181
+ View Details →
182
+ </span>
183
+ </div>
184
+ </div>
185
+
186
+ {/* Content */}
187
+ <div className="p-5 flex flex-col flex-1">
188
+ {item.category && (
189
+ <div className="text-purple-600 dark:text-lime-500 text-xs font-semibold uppercase tracking-widest mb-1">
190
+ {item.category}
191
+ </div>
192
+ )}
193
+
194
+ <h3 className="text-stone-900 dark:text-stone-50 font-bold text-base leading-snug mb-2 line-clamp-2">
195
+ {item.name}
196
+ </h3>
197
+
198
+ {item.description && (
199
+ <div className="grid grid-rows-[0fr] group-hover:grid-rows-[1fr] transition-[grid-template-rows,margin] duration-300 ease-out mb-0 group-hover:mb-4">
200
+ <p className="min-h-0 overflow-hidden text-stone-400 text-sm leading-relaxed line-clamp-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
201
+ {item.description}
202
+ </p>
203
+ </div>
204
+ )}
205
+
206
+ {item.features && item.features.length > 0 && (
207
+ <div className="flex flex-wrap gap-1.5 mt-auto">
208
+ {item.features.slice(0, 3).map((f, fi) => (
209
+ <span
210
+ key={f.id ?? fi}
211
+ className="text-xs bg-purple-500/10 text-purple-600 dark:bg-lime-500/10 dark:text-lime-400 px-2.5 py-1 rounded-full"
212
+ >
213
+ {f.label}
214
+ </span>
215
+ ))}
216
+ </div>
217
+ )}
218
+ </div>
219
+ </div>
220
+ </Link>
221
+ </div>
222
+ )
223
+ })}
224
+ </div>
225
+ </div>
226
+ {/* Dot indicators */}
227
+ {snapCount > 1 && (
228
+ <div className="flex justify-center gap-2 mt-0">
229
+ {Array.from({ length: snapCount }).map((_, i) => (
230
+ <button
231
+ key={i}
232
+ onClick={() => scrollTo(i)}
233
+ aria-label={`Go to slide ${i + 1}`}
234
+ className="flex h-11 w-11 items-center justify-center rounded-full cursor-pointer"
235
+ >
236
+ <span
237
+ className={cn(
238
+ 'block rounded-full transition-all duration-300',
239
+ i === activeIdx
240
+ ? 'h-2.5 w-8 bg-purple-600 dark:bg-lime-500'
241
+ : 'h-2.5 w-2.5 bg-stone-600 hover:bg-stone-400',
242
+ )}
243
+ />
244
+ </button>
245
+ ))}
246
+ </div>
247
+ )}
248
+ </div>
249
+ </section>
250
+ )
251
+ }