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.
- package/package.json +1 -1
- package/src/installers/patchRenderBlocks.js +1 -1
- package/src/registry/validateTemplateManifest.js +2 -1
- package/templates/blocks/BannerDD790432/Component.tsx +63 -0
- package/templates/blocks/BannerDD790432/config.ts +54 -0
- package/templates/blocks/BannerDD790432/manifest.json +80 -0
- package/templates/blocks/Button47/Component.tsx +124 -0
- package/templates/blocks/Button47/config.ts +75 -0
- package/templates/blocks/Button47/link.ts +22 -0
- package/templates/blocks/Button47/manifest.json +105 -0
- package/templates/blocks/Button47/styles.module.scss +75 -0
- package/templates/blocks/DD-Accordion/Component.tsx +55 -36
- package/templates/blocks/DD-Accordion/config.ts +71 -59
- package/templates/blocks/DD-AnimText/Component.tsx +50 -32
- package/templates/blocks/DD-AnimText/config.ts +296 -295
- package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
- package/templates/blocks/DD-Banner/Component.tsx +54 -51
- package/templates/blocks/DD-BookCall/Component.tsx +535 -535
- package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
- package/templates/blocks/DD-Card/Component.tsx +4 -4
- package/templates/blocks/DD-CardTemplate01/Component.tsx +139 -140
- package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
- package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
- package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
- package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
- package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
- package/templates/blocks/DD-Contact/Component.tsx +433 -435
- package/templates/blocks/DD-Counter/Component.tsx +11 -6
- package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
- package/templates/blocks/DD-DeliCard/config.ts +186 -0
- package/templates/blocks/DD-DeliCard/manifest.json +94 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
- package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
- package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
- package/templates/blocks/DD-FeatProd/config.ts +173 -0
- package/templates/blocks/DD-FeatProd/manifest.json +93 -0
- package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
- package/templates/blocks/DD-FeatServ/config.ts +213 -0
- package/templates/blocks/DD-FeatServ/manifest.json +113 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
- package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
- package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
- package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
- package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
- package/templates/blocks/DD-Hero/Component.tsx +296 -297
- package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
- package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
- package/templates/blocks/DD-HeroPlas/config.ts +33 -0
- package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +937 -936
- package/templates/blocks/DD-Icon/Component.tsx +46 -81
- package/templates/blocks/DD-IconButton/Component.tsx +13 -20
- package/templates/blocks/DD-IdTag/Component.tsx +22 -21
- package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
- package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
- package/templates/blocks/DD-Lottie/Component.tsx +11 -3
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -545
- package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
- package/templates/blocks/DD-Masonry/Component.tsx +654 -644
- package/templates/blocks/DD-Masonry/config.ts +475 -475
- package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
- package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
- package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
- package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
- package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
- package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
- package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
- package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
- package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
- package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
- package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
- package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
- package/templates/blocks/DD-NineCanvas/config.ts +61 -0
- package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
- package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
- package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
- package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
- package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
- package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
- package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
- package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
- package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
- package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
- package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
- package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
- package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
- package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
- package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
- package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
- package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
- package/templates/blocks/DD-Pricing/Component.tsx +2 -3
- package/templates/blocks/DD-Pricing/config.ts +215 -215
- package/templates/blocks/DD-Process/Component.tsx +2 -2
- package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
- package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
- package/templates/blocks/DD-Section/Component.tsx +265 -266
- package/templates/blocks/DD-Separator/Component.tsx +66 -66
- package/templates/blocks/DD-Services/Component.tsx +176 -176
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
- package/templates/blocks/DD-Slider-A/Component.tsx +2 -2
- package/templates/blocks/DD-StackCards/Component.tsx +172 -171
- package/templates/blocks/DD-Team/Component.tsx +247 -247
- package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
- package/templates/blocks/DD-TechStack/Component.tsx +173 -174
- package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
- package/templates/blocks/DD-Testim/Component.tsx +247 -247
- package/templates/blocks/DD-Testim2/Component.tsx +265 -256
- package/templates/blocks/DD-Testimonials/Component.tsx +139 -139
- package/templates/blocks/DD-Thanks/Component.tsx +1 -1
- package/templates/blocks/DD-Work/Component.tsx +318 -318
- package/templates/blocks/DD-Work-B/Component.tsx +294 -294
- package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +6 -3
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +45 -8
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +1 -3
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +9 -22
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
- package/templates/blocks/DDHG-TeamCards/Component.tsx +6 -35
- package/templates/blocks/Form/Checkbox/index.tsx +45 -0
- package/templates/blocks/Form/Component.tsx +163 -0
- package/templates/blocks/Form/Country/index.tsx +65 -0
- package/templates/blocks/Form/Country/options.ts +982 -0
- package/templates/blocks/Form/Email/index.tsx +38 -0
- package/templates/blocks/Form/Error/index.tsx +15 -0
- package/templates/blocks/Form/Message/index.tsx +13 -0
- package/templates/blocks/Form/Number/index.tsx +36 -0
- package/templates/blocks/Form/Select/index.tsx +63 -0
- package/templates/blocks/Form/State/index.tsx +64 -0
- package/templates/blocks/Form/State/options.ts +52 -0
- package/templates/blocks/Form/Text/index.tsx +32 -0
- package/templates/blocks/Form/Textarea/index.tsx +40 -0
- package/templates/blocks/Form/Width/index.tsx +13 -0
- package/templates/blocks/Form/config.ts +51 -0
- package/templates/blocks/Form/fields.tsx +21 -0
- package/templates/blocks/Form/manifest.json +168 -0
- package/templates/blocks/MediaBlock/Component.tsx +67 -0
- package/templates/blocks/MediaBlock/config.ts +14 -0
- package/templates/blocks/MediaBlock/manifest.json +92 -0
- package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
- package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
- package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
- package/templates/blocks/rawHtml/Component.tsx +25 -0
- package/templates/blocks/rawHtml/config.ts +56 -0
- package/templates/blocks/rawHtml/manifest.json +80 -0
|
@@ -1,545 +1,544 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useMemo, useRef } from 'react'
|
|
4
|
-
import Link from 'next/link'
|
|
5
|
-
import { motion, type Variants } from 'framer-motion'
|
|
6
|
-
import { cn } from '@/utilities/ui'
|
|
7
|
-
|
|
8
|
-
type MediaLike = {
|
|
9
|
-
url?: string | null
|
|
10
|
-
alt?: string | null
|
|
11
|
-
mimeType?: string | null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type ShowcaseItem = {
|
|
15
|
-
id?: string | null
|
|
16
|
-
title?: string | null
|
|
17
|
-
category?: string | null
|
|
18
|
-
framework?: string | null
|
|
19
|
-
description?: string | null
|
|
20
|
-
href?: string | null
|
|
21
|
-
linkLabel?: string | null
|
|
22
|
-
free?: boolean | null
|
|
23
|
-
priceLabel?: string | null
|
|
24
|
-
aspectRatio?: '16/9' | '3/2' | '1/1' | '2/3' | string | null
|
|
25
|
-
image?: number | string | MediaLike | null
|
|
26
|
-
hoverVideo?: number | string | MediaLike | null
|
|
27
|
-
posterFallbackUrl?: string | null
|
|
28
|
-
videoFallbackUrl?: string | null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type DDMasonaryMediaProps = {
|
|
32
|
-
eyebrow?: string | null
|
|
33
|
-
headlineMain?: string | null
|
|
34
|
-
headlineAccent?: string | null
|
|
35
|
-
subtext?: string | null
|
|
36
|
-
items?: ShowcaseItem[] | null
|
|
37
|
-
columnsDesktop?: number | null
|
|
38
|
-
columnsTablet?: number | null
|
|
39
|
-
gapClass?: string | null
|
|
40
|
-
containerClassName?: string | null
|
|
41
|
-
sectionClassName?: string | null
|
|
42
|
-
sectionId?: string | null
|
|
43
|
-
customCSS?: string | null
|
|
44
|
-
showCta?: boolean | null
|
|
45
|
-
ctaAlign?: 'left' | 'center' | 'right' | null
|
|
46
|
-
ctaDisplayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly' | null
|
|
47
|
-
ctaIconPosition?: 'before' | 'after' | null
|
|
48
|
-
ctaButtonText?: string | null
|
|
49
|
-
ctaButtonStyle?: string | null
|
|
50
|
-
ctaSize?: 'small' | 'medium' | 'large' | null
|
|
51
|
-
ctaIcon?: string | null
|
|
52
|
-
ctaLink?: any | null
|
|
53
|
-
ctaContainerClass?: string | null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const getMediaUrl = (media?: number | string | MediaLike | null, fallback?: string | null) => {
|
|
57
|
-
if (media && typeof media === 'object' && typeof media.url === 'string') return media.url
|
|
58
|
-
if (typeof media === 'string') return media
|
|
59
|
-
return fallback || ''
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const getMediaAlt = (media?: number | string | MediaLike | null, fallback = '') => {
|
|
63
|
-
if (media && typeof media === 'object' && typeof media.alt === 'string') return media.alt
|
|
64
|
-
return fallback
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const getMediaMimeType = (media?: number | string | MediaLike | null) => {
|
|
68
|
-
if (media && typeof media === 'object' && typeof media.mimeType === 'string')
|
|
69
|
-
return media.mimeType
|
|
70
|
-
return ''
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const getFileExtension = (url: string) => {
|
|
74
|
-
const clean = url.split('?')[0]?.split('#')[0] || ''
|
|
75
|
-
return clean.split('.').pop()?.toLowerCase() || ''
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const isVideoMedia = (url: string, mimeType?: string | null) => {
|
|
79
|
-
const ext = getFileExtension(url)
|
|
80
|
-
|
|
81
|
-
return Boolean(
|
|
82
|
-
mimeType?.startsWith('video/') ||
|
|
83
|
-
ext === 'webm' ||
|
|
84
|
-
ext === 'mp4' ||
|
|
85
|
-
ext === 'mov' ||
|
|
86
|
-
ext === 'm4v',
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const isWideCard = (ratio?: string | null) => ratio === '16/9' || ratio === '3/2'
|
|
91
|
-
const isSquareCard = (ratio?: string | null) => ratio === '1/1'
|
|
92
|
-
|
|
93
|
-
const getGapPx = (gapClass?: string | null) => {
|
|
94
|
-
if (!gapClass) return 20
|
|
95
|
-
if (gapClass.includes('gap-0')) return 0
|
|
96
|
-
if (gapClass.includes('gap-1')) return 4
|
|
97
|
-
if (gapClass.includes('gap-2')) return 8
|
|
98
|
-
if (gapClass.includes('gap-3')) return 12
|
|
99
|
-
if (gapClass.includes('gap-4')) return 16
|
|
100
|
-
if (gapClass.includes('gap-5')) return 20
|
|
101
|
-
if (gapClass.includes('gap-6')) return 24
|
|
102
|
-
if (gapClass.includes('gap-8')) return 32
|
|
103
|
-
if (gapClass.includes('gap-10')) return 40
|
|
104
|
-
return 20
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const parseAspectRatio = (ratio?: string | null) => {
|
|
108
|
-
if (!ratio || !ratio.includes('/')) return '1 / 1'
|
|
109
|
-
|
|
110
|
-
const [rawWidth, rawHeight] = ratio.split('/').map(Number)
|
|
111
|
-
const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1
|
|
112
|
-
const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1
|
|
113
|
-
|
|
114
|
-
return `${width} / ${height}`
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const containerVariants: Variants = {
|
|
118
|
-
hidden: {},
|
|
119
|
-
show: {
|
|
120
|
-
transition: {
|
|
121
|
-
staggerChildren: 0.08,
|
|
122
|
-
delayChildren: 0.06,
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const itemVariants: Variants = {
|
|
128
|
-
hidden: {
|
|
129
|
-
opacity: 0,
|
|
130
|
-
y: 22,
|
|
131
|
-
filter: 'blur(6px)',
|
|
132
|
-
},
|
|
133
|
-
show: {
|
|
134
|
-
opacity: 1,
|
|
135
|
-
y: 0,
|
|
136
|
-
filter: 'none',
|
|
137
|
-
transition: {
|
|
138
|
-
duration: 0.45,
|
|
139
|
-
ease: 'easeOut',
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function StudioMasonaryCard({ item, index }: { item: ShowcaseItem; index: number }) {
|
|
145
|
-
const videoRef = useRef<HTMLVideoElement | null>(null)
|
|
146
|
-
|
|
147
|
-
const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl)
|
|
148
|
-
const imageAlt = getMediaAlt(item.image, item.title || 'Studio preview')
|
|
149
|
-
|
|
150
|
-
const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl)
|
|
151
|
-
const hoverMimeType = getMediaMimeType(item.hoverVideo)
|
|
152
|
-
const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType)
|
|
153
|
-
const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo)
|
|
154
|
-
|
|
155
|
-
const href = item.href || '#'
|
|
156
|
-
const aspectRatio = parseAspectRatio(item.aspectRatio || '16/9')
|
|
157
|
-
const wide = isWideCard(item.aspectRatio)
|
|
158
|
-
const square = isSquareCard(item.aspectRatio)
|
|
159
|
-
const badgeLabel = item.category || (item.free ? 'Free' : item.priceLabel || '')
|
|
160
|
-
|
|
161
|
-
const handleMouseEnter = () => {
|
|
162
|
-
const video = videoRef.current
|
|
163
|
-
if (!video || !hoverIsVideo) return
|
|
164
|
-
|
|
165
|
-
if (video.readyState === 0) video.load()
|
|
166
|
-
|
|
167
|
-
video.currentTime = 0
|
|
168
|
-
const playPromise = video.play()
|
|
169
|
-
|
|
170
|
-
if (playPromise && typeof playPromise.catch === 'function') {
|
|
171
|
-
playPromise.catch(() => undefined)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const handleMouseLeave = () => {
|
|
176
|
-
const video = videoRef.current
|
|
177
|
-
if (!video) return
|
|
178
|
-
|
|
179
|
-
video.pause()
|
|
180
|
-
video.currentTime = 0
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return (
|
|
184
|
-
<article
|
|
185
|
-
className={cn('studio-masonry-item card-item', wide && 'is-wide', square && 'is-square')}
|
|
186
|
-
data-index={index}
|
|
187
|
-
onMouseEnter={handleMouseEnter}
|
|
188
|
-
onMouseLeave={handleMouseLeave}
|
|
189
|
-
>
|
|
190
|
-
<Link href={href} className="block h-full focus-visible:outline-none">
|
|
191
|
-
<article
|
|
192
|
-
data-theme="dark"
|
|
193
|
-
className="masonary-article-card outline-2 outline-stone-500/50 outline-solid group relative h-full w-full overflow-clip rounded-[3rem]! border border-border bg-[#55526b85]! backdrop-blur-xl! shadow-2xl transition-all duration-300 hover:cursor-pointer hover:border-lime-500 hover:shadow-2xl"
|
|
194
|
-
style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
|
|
195
|
-
>
|
|
196
|
-
<div
|
|
197
|
-
className="card-media relative w-full overflow-hidden rounded-[3rem]! border border-border bg-linear-to-br from-violet-950 to-indigo-950 shadow-2xl!"
|
|
198
|
-
style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
|
|
199
|
-
>
|
|
200
|
-
{imageUrl ? (
|
|
201
|
-
<img
|
|
202
|
-
src={imageUrl}
|
|
203
|
-
alt={imageAlt}
|
|
204
|
-
loading="lazy"
|
|
205
|
-
decoding="async"
|
|
206
|
-
className={cn(
|
|
207
|
-
'absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! transition-all duration-500 group-hover:scale-105',
|
|
208
|
-
hoverUrl && 'group-hover:opacity-0',
|
|
209
|
-
)}
|
|
210
|
-
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
211
|
-
/>
|
|
212
|
-
) : (
|
|
213
|
-
<div className="absolute inset-0 flex items-center justify-center bg-linear-to-br from-violet-950 to-indigo-950 text-xs text-stone-500">
|
|
214
|
-
Preview image
|
|
215
|
-
</div>
|
|
216
|
-
)}
|
|
217
|
-
|
|
218
|
-
{hoverUrl && hoverIsVideo && (
|
|
219
|
-
<video
|
|
220
|
-
ref={videoRef}
|
|
221
|
-
src={hoverUrl}
|
|
222
|
-
muted
|
|
223
|
-
loop
|
|
224
|
-
playsInline
|
|
225
|
-
preload="metadata"
|
|
226
|
-
className="absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
|
|
227
|
-
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
228
|
-
/>
|
|
229
|
-
)}
|
|
230
|
-
|
|
231
|
-
{hoverUrl && hoverIsImage && (
|
|
232
|
-
<img
|
|
233
|
-
src={hoverUrl}
|
|
234
|
-
alt=""
|
|
235
|
-
loading="lazy"
|
|
236
|
-
decoding="async"
|
|
237
|
-
className="absolute inset-0 h-full w-full object-cover object-top !rounded-4xl opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
|
|
238
|
-
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
239
|
-
/>
|
|
240
|
-
)}
|
|
241
|
-
|
|
242
|
-
<div
|
|
243
|
-
className="pointer-events-none absolute inset-0 z-10 transition-opacity duration-300 group-hover:opacity-90"
|
|
244
|
-
style={{
|
|
245
|
-
background:
|
|
246
|
-
'linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)',
|
|
247
|
-
}}
|
|
248
|
-
/>
|
|
249
|
-
|
|
250
|
-
<div className="pointer-events-none absolute inset-0 z-20 flex flex-col justify-between p-4 sm:p-4 md:p-4">
|
|
251
|
-
{badgeLabel ? (
|
|
252
|
-
<div className="flex items-start justify-start">
|
|
253
|
-
<span
|
|
254
|
-
className="inline-flex rounded-full border border-lime-500/80 bg-lime-500/15 px-2 py-1 text-[11.5px] font-medium tracking-widest text-stone-50 backdrop-blur-md sm:px-2 sm:py-1"
|
|
255
|
-
style={{ boxShadow: '0 0 24px rgba(132,204,22,0.35)' }}
|
|
256
|
-
>
|
|
257
|
-
{badgeLabel}
|
|
258
|
-
</span>
|
|
259
|
-
</div>
|
|
260
|
-
) : (
|
|
261
|
-
<span />
|
|
262
|
-
)}
|
|
263
|
-
|
|
264
|
-
{item.title && (
|
|
265
|
-
<h3
|
|
266
|
-
className="max-w-[86%] text-lg font-semibold tracking-[0.08em] text-stone-50"
|
|
267
|
-
style={{ textShadow: '0 4px 16px rgba(0,0,0,0.9)' }}
|
|
268
|
-
>
|
|
269
|
-
{item.title}
|
|
270
|
-
</h3>
|
|
271
|
-
)}
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
</article>
|
|
275
|
-
</Link>
|
|
276
|
-
</article>
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
|
|
281
|
-
eyebrow = 'Featured systems',
|
|
282
|
-
headlineMain = 'Components, blocks, and templates',
|
|
283
|
-
headlineAccent = 'built to feel alive',
|
|
284
|
-
subtext = 'A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.',
|
|
285
|
-
items = [],
|
|
286
|
-
columnsDesktop = 12,
|
|
287
|
-
columnsTablet = 2,
|
|
288
|
-
gapClass = 'gap-5',
|
|
289
|
-
containerClassName = 'max-w-7xl mx-auto px-4 md:px-6',
|
|
290
|
-
sectionClassName = 'py-
|
|
291
|
-
sectionId = 'studio-masonary-media',
|
|
292
|
-
customCSS,
|
|
293
|
-
showCta = false,
|
|
294
|
-
ctaAlign = 'center',
|
|
295
|
-
ctaDisplayMode = 'iconWithLabel',
|
|
296
|
-
ctaIconPosition = 'before',
|
|
297
|
-
ctaButtonText,
|
|
298
|
-
ctaButtonStyle = 'primary',
|
|
299
|
-
ctaSize = 'medium',
|
|
300
|
-
ctaIcon,
|
|
301
|
-
ctaLink,
|
|
302
|
-
ctaContainerClass,
|
|
303
|
-
}) => {
|
|
304
|
-
const gridRef = useRef<HTMLDivElement | null>(null)
|
|
305
|
-
const isotopeRef = useRef<any>(null)
|
|
306
|
-
|
|
307
|
-
const safeSectionId = sectionId || 'studio-masonary-media'
|
|
308
|
-
const gap = useMemo(() => getGapPx(gapClass), [gapClass])
|
|
309
|
-
const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6)
|
|
310
|
-
const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3)
|
|
311
|
-
|
|
312
|
-
useEffect(() => {
|
|
313
|
-
if (!gridRef.current || !items || items.length === 0) return
|
|
314
|
-
|
|
315
|
-
let destroyed = false
|
|
316
|
-
|
|
317
|
-
const init = async () => {
|
|
318
|
-
const Isotope = (await import('isotope-layout')).default
|
|
319
|
-
const imagesLoaded = (await import('imagesloaded')).default
|
|
320
|
-
|
|
321
|
-
if (!gridRef.current || destroyed) return
|
|
322
|
-
|
|
323
|
-
const iso = new Isotope(gridRef.current, {
|
|
324
|
-
itemSelector: '.studio-masonry-item',
|
|
325
|
-
percentPosition: true,
|
|
326
|
-
transitionDuration: '0.35s',
|
|
327
|
-
masonry: {
|
|
328
|
-
columnWidth: '.studio-masonry-sizer',
|
|
329
|
-
gutter: '.studio-masonry-gutter',
|
|
330
|
-
fitWidth: false,
|
|
331
|
-
},
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
isotopeRef.current = iso
|
|
335
|
-
|
|
336
|
-
imagesLoaded(gridRef.current, () => {
|
|
337
|
-
if (!destroyed) iso.layout()
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
window.setTimeout(() => {
|
|
341
|
-
if (!destroyed) iso.layout()
|
|
342
|
-
}, 250)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
init()
|
|
346
|
-
|
|
347
|
-
const handleResize = () => {
|
|
348
|
-
isotopeRef.current?.layout()
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
window.addEventListener('resize', handleResize)
|
|
352
|
-
|
|
353
|
-
return () => {
|
|
354
|
-
destroyed = true
|
|
355
|
-
window.removeEventListener('resize', handleResize)
|
|
356
|
-
isotopeRef.current?.destroy()
|
|
357
|
-
isotopeRef.current = null
|
|
358
|
-
}
|
|
359
|
-
}, [items])
|
|
360
|
-
|
|
361
|
-
useEffect(() => {
|
|
362
|
-
isotopeRef.current?.reloadItems()
|
|
363
|
-
isotopeRef.current?.layout()
|
|
364
|
-
}, [items, columnsDesktop, columnsTablet, gap])
|
|
365
|
-
|
|
366
|
-
if (!items || items.length === 0) return null
|
|
367
|
-
|
|
368
|
-
return (
|
|
369
|
-
<>
|
|
370
|
-
<style
|
|
371
|
-
dangerouslySetInnerHTML={{
|
|
372
|
-
__html: `
|
|
373
|
-
#${safeSectionId} .studio-masonry-grid {
|
|
374
|
-
--studio-masonry-gap: ${gap}px;
|
|
375
|
-
--studio-masonry-cols: 1;
|
|
376
|
-
position: relative;
|
|
377
|
-
width: 100%;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
#${safeSectionId} .studio-masonry-sizer,
|
|
381
|
-
#${safeSectionId} .studio-masonry-item {
|
|
382
|
-
width: 100%;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
#${safeSectionId} .studio-masonry-gutter {
|
|
386
|
-
width: var(--studio-masonry-gap);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
#${safeSectionId} .studio-masonry-item {
|
|
390
|
-
margin-bottom: var(--studio-masonry-gap);
|
|
391
|
-
will-change: transform;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
@media (max-width: 639px) {
|
|
395
|
-
#${safeSectionId} .studio-masonry-grid {
|
|
396
|
-
--studio-masonry-cols: 2;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
#${safeSectionId} .studio-masonry-sizer,
|
|
400
|
-
#${safeSectionId} .studio-masonry-item.is-square {
|
|
401
|
-
width: calc(
|
|
402
|
-
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
403
|
-
var(--studio-masonry-cols)
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
#${safeSectionId} .studio-masonry-item:not(.is-square) {
|
|
408
|
-
width: 100%;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
#${safeSectionId} .studio-masonry-item.is-square .card-media {
|
|
412
|
-
aspect-ratio: 1 / 1 !important;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
#${safeSectionId} .studio-masonry-item:not(.is-square) .card-media {
|
|
416
|
-
aspect-ratio: 16 / 9 !important;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
@media (min-width: 640px) {
|
|
421
|
-
#${safeSectionId} .studio-masonry-grid {
|
|
422
|
-
--studio-masonry-cols: ${tabletColumns};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
#${safeSectionId} .studio-masonry-sizer,
|
|
426
|
-
#${safeSectionId} .studio-masonry-item {
|
|
427
|
-
width: calc(
|
|
428
|
-
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
429
|
-
var(--studio-masonry-cols)
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
#${safeSectionId} .studio-masonry-item.is-wide {
|
|
434
|
-
width: calc(
|
|
435
|
-
(
|
|
436
|
-
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
437
|
-
var(--studio-masonry-cols)
|
|
438
|
-
) *
|
|
439
|
-
2 + var(--studio-masonry-gap)
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
@media (min-width: 1024px) {
|
|
445
|
-
#${safeSectionId} .studio-masonry-grid {
|
|
446
|
-
--studio-masonry-cols: ${desktopColumns};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
@media (min-width: 640px) and (max-width: 1023px) {
|
|
451
|
-
#${safeSectionId} .studio-masonry-item.is-wide {
|
|
452
|
-
width: 100%;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
#${safeSectionId} .card-media {
|
|
457
|
-
min-height: 10rem;
|
|
458
|
-
scale: 1;
|
|
459
|
-
}
|
|
460
|
-
`,
|
|
461
|
-
}}
|
|
462
|
-
/>
|
|
463
|
-
|
|
464
|
-
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
465
|
-
|
|
466
|
-
<section id={safeSectionId} className={cn('studio-masonary-media', sectionClassName)}>
|
|
467
|
-
<div className={cn(containerClassName)}>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
<div className="studio-masonry-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
'
|
|
524
|
-
ctaAlign === '
|
|
525
|
-
ctaAlign === '
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
export default DDMasonaryMedia
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { motion, type Variants } from 'framer-motion'
|
|
6
|
+
import { cn } from '@/utilities/ui'
|
|
7
|
+
|
|
8
|
+
type MediaLike = {
|
|
9
|
+
url?: string | null
|
|
10
|
+
alt?: string | null
|
|
11
|
+
mimeType?: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ShowcaseItem = {
|
|
15
|
+
id?: string | null
|
|
16
|
+
title?: string | null
|
|
17
|
+
category?: string | null
|
|
18
|
+
framework?: string | null
|
|
19
|
+
description?: string | null
|
|
20
|
+
href?: string | null
|
|
21
|
+
linkLabel?: string | null
|
|
22
|
+
free?: boolean | null
|
|
23
|
+
priceLabel?: string | null
|
|
24
|
+
aspectRatio?: '16/9' | '3/2' | '1/1' | '2/3' | string | null
|
|
25
|
+
image?: number | string | MediaLike | null
|
|
26
|
+
hoverVideo?: number | string | MediaLike | null
|
|
27
|
+
posterFallbackUrl?: string | null
|
|
28
|
+
videoFallbackUrl?: string | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type DDMasonaryMediaProps = {
|
|
32
|
+
eyebrow?: string | null
|
|
33
|
+
headlineMain?: string | null
|
|
34
|
+
headlineAccent?: string | null
|
|
35
|
+
subtext?: string | null
|
|
36
|
+
items?: ShowcaseItem[] | null
|
|
37
|
+
columnsDesktop?: number | null
|
|
38
|
+
columnsTablet?: number | null
|
|
39
|
+
gapClass?: string | null
|
|
40
|
+
containerClassName?: string | null
|
|
41
|
+
sectionClassName?: string | null
|
|
42
|
+
sectionId?: string | null
|
|
43
|
+
customCSS?: string | null
|
|
44
|
+
showCta?: boolean | null
|
|
45
|
+
ctaAlign?: 'left' | 'center' | 'right' | null
|
|
46
|
+
ctaDisplayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly' | null
|
|
47
|
+
ctaIconPosition?: 'before' | 'after' | null
|
|
48
|
+
ctaButtonText?: string | null
|
|
49
|
+
ctaButtonStyle?: string | null
|
|
50
|
+
ctaSize?: 'small' | 'medium' | 'large' | null
|
|
51
|
+
ctaIcon?: string | null
|
|
52
|
+
ctaLink?: any | null
|
|
53
|
+
ctaContainerClass?: string | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const getMediaUrl = (media?: number | string | MediaLike | null, fallback?: string | null) => {
|
|
57
|
+
if (media && typeof media === 'object' && typeof media.url === 'string') return media.url
|
|
58
|
+
if (typeof media === 'string') return media
|
|
59
|
+
return fallback || ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const getMediaAlt = (media?: number | string | MediaLike | null, fallback = '') => {
|
|
63
|
+
if (media && typeof media === 'object' && typeof media.alt === 'string') return media.alt
|
|
64
|
+
return fallback
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const getMediaMimeType = (media?: number | string | MediaLike | null) => {
|
|
68
|
+
if (media && typeof media === 'object' && typeof media.mimeType === 'string')
|
|
69
|
+
return media.mimeType
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const getFileExtension = (url: string) => {
|
|
74
|
+
const clean = url.split('?')[0]?.split('#')[0] || ''
|
|
75
|
+
return clean.split('.').pop()?.toLowerCase() || ''
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isVideoMedia = (url: string, mimeType?: string | null) => {
|
|
79
|
+
const ext = getFileExtension(url)
|
|
80
|
+
|
|
81
|
+
return Boolean(
|
|
82
|
+
mimeType?.startsWith('video/') ||
|
|
83
|
+
ext === 'webm' ||
|
|
84
|
+
ext === 'mp4' ||
|
|
85
|
+
ext === 'mov' ||
|
|
86
|
+
ext === 'm4v',
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isWideCard = (ratio?: string | null) => ratio === '16/9' || ratio === '3/2'
|
|
91
|
+
const isSquareCard = (ratio?: string | null) => ratio === '1/1'
|
|
92
|
+
|
|
93
|
+
const getGapPx = (gapClass?: string | null) => {
|
|
94
|
+
if (!gapClass) return 20
|
|
95
|
+
if (gapClass.includes('gap-0')) return 0
|
|
96
|
+
if (gapClass.includes('gap-1')) return 4
|
|
97
|
+
if (gapClass.includes('gap-2')) return 8
|
|
98
|
+
if (gapClass.includes('gap-3')) return 12
|
|
99
|
+
if (gapClass.includes('gap-4')) return 16
|
|
100
|
+
if (gapClass.includes('gap-5')) return 20
|
|
101
|
+
if (gapClass.includes('gap-6')) return 24
|
|
102
|
+
if (gapClass.includes('gap-8')) return 32
|
|
103
|
+
if (gapClass.includes('gap-10')) return 40
|
|
104
|
+
return 20
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parseAspectRatio = (ratio?: string | null) => {
|
|
108
|
+
if (!ratio || !ratio.includes('/')) return '1 / 1'
|
|
109
|
+
|
|
110
|
+
const [rawWidth, rawHeight] = ratio.split('/').map(Number)
|
|
111
|
+
const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1
|
|
112
|
+
const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1
|
|
113
|
+
|
|
114
|
+
return `${width} / ${height}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const containerVariants: Variants = {
|
|
118
|
+
hidden: {},
|
|
119
|
+
show: {
|
|
120
|
+
transition: {
|
|
121
|
+
staggerChildren: 0.08,
|
|
122
|
+
delayChildren: 0.06,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const itemVariants: Variants = {
|
|
128
|
+
hidden: {
|
|
129
|
+
opacity: 0,
|
|
130
|
+
y: 22,
|
|
131
|
+
filter: 'blur(6px)',
|
|
132
|
+
},
|
|
133
|
+
show: {
|
|
134
|
+
opacity: 1,
|
|
135
|
+
y: 0,
|
|
136
|
+
filter: 'none',
|
|
137
|
+
transition: {
|
|
138
|
+
duration: 0.45,
|
|
139
|
+
ease: 'easeOut',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function StudioMasonaryCard({ item, index }: { item: ShowcaseItem; index: number }) {
|
|
145
|
+
const videoRef = useRef<HTMLVideoElement | null>(null)
|
|
146
|
+
|
|
147
|
+
const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl)
|
|
148
|
+
const imageAlt = getMediaAlt(item.image, item.title || 'Studio preview')
|
|
149
|
+
|
|
150
|
+
const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl)
|
|
151
|
+
const hoverMimeType = getMediaMimeType(item.hoverVideo)
|
|
152
|
+
const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType)
|
|
153
|
+
const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo)
|
|
154
|
+
|
|
155
|
+
const href = item.href || '#'
|
|
156
|
+
const aspectRatio = parseAspectRatio(item.aspectRatio || '16/9')
|
|
157
|
+
const wide = isWideCard(item.aspectRatio)
|
|
158
|
+
const square = isSquareCard(item.aspectRatio)
|
|
159
|
+
const badgeLabel = item.category || (item.free ? 'Free' : item.priceLabel || '')
|
|
160
|
+
|
|
161
|
+
const handleMouseEnter = () => {
|
|
162
|
+
const video = videoRef.current
|
|
163
|
+
if (!video || !hoverIsVideo) return
|
|
164
|
+
|
|
165
|
+
if (video.readyState === 0) video.load()
|
|
166
|
+
|
|
167
|
+
video.currentTime = 0
|
|
168
|
+
const playPromise = video.play()
|
|
169
|
+
|
|
170
|
+
if (playPromise && typeof playPromise.catch === 'function') {
|
|
171
|
+
playPromise.catch(() => undefined)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleMouseLeave = () => {
|
|
176
|
+
const video = videoRef.current
|
|
177
|
+
if (!video) return
|
|
178
|
+
|
|
179
|
+
video.pause()
|
|
180
|
+
video.currentTime = 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<article
|
|
185
|
+
className={cn('studio-masonry-item card-item', wide && 'is-wide', square && 'is-square')}
|
|
186
|
+
data-index={index}
|
|
187
|
+
onMouseEnter={handleMouseEnter}
|
|
188
|
+
onMouseLeave={handleMouseLeave}
|
|
189
|
+
>
|
|
190
|
+
<Link href={href} className="block h-full focus-visible:outline-none">
|
|
191
|
+
<article
|
|
192
|
+
data-theme="dark"
|
|
193
|
+
className="masonary-article-card outline-2 outline-stone-500/50 outline-solid group relative h-full w-full overflow-clip rounded-[3rem]! border border-border bg-[#55526b85]! backdrop-blur-xl! shadow-2xl transition-all duration-300 hover:cursor-pointer hover:border-lime-500 hover:shadow-2xl"
|
|
194
|
+
style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
|
|
195
|
+
>
|
|
196
|
+
<div
|
|
197
|
+
className="card-media relative w-full overflow-hidden rounded-[3rem]! border border-border bg-linear-to-br from-violet-950 to-indigo-950 shadow-2xl!"
|
|
198
|
+
style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
|
|
199
|
+
>
|
|
200
|
+
{imageUrl ? (
|
|
201
|
+
<img
|
|
202
|
+
src={imageUrl}
|
|
203
|
+
alt={imageAlt}
|
|
204
|
+
loading="lazy"
|
|
205
|
+
decoding="async"
|
|
206
|
+
className={cn(
|
|
207
|
+
'absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! transition-all duration-500 group-hover:scale-105',
|
|
208
|
+
hoverUrl && 'group-hover:opacity-0',
|
|
209
|
+
)}
|
|
210
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
<div className="absolute inset-0 flex items-center justify-center bg-linear-to-br from-violet-950 to-indigo-950 text-xs text-stone-500">
|
|
214
|
+
Preview image
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{hoverUrl && hoverIsVideo && (
|
|
219
|
+
<video
|
|
220
|
+
ref={videoRef}
|
|
221
|
+
src={hoverUrl}
|
|
222
|
+
muted
|
|
223
|
+
loop
|
|
224
|
+
playsInline
|
|
225
|
+
preload="metadata"
|
|
226
|
+
className="absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
|
|
227
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{hoverUrl && hoverIsImage && (
|
|
232
|
+
<img
|
|
233
|
+
src={hoverUrl}
|
|
234
|
+
alt=""
|
|
235
|
+
loading="lazy"
|
|
236
|
+
decoding="async"
|
|
237
|
+
className="absolute inset-0 h-full w-full object-cover object-top !rounded-4xl opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
|
|
238
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<div
|
|
243
|
+
className="pointer-events-none absolute inset-0 z-10 transition-opacity duration-300 group-hover:opacity-90"
|
|
244
|
+
style={{
|
|
245
|
+
background:
|
|
246
|
+
'linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)',
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
|
|
250
|
+
<div className="pointer-events-none absolute inset-0 z-20 flex flex-col justify-between p-4 sm:p-4 md:p-4">
|
|
251
|
+
{badgeLabel ? (
|
|
252
|
+
<div className="flex items-start justify-start">
|
|
253
|
+
<span
|
|
254
|
+
className="inline-flex rounded-full border border-lime-500/80 bg-lime-500/15 px-2 py-1 text-[11.5px] font-medium tracking-widest text-stone-50 backdrop-blur-md sm:px-2 sm:py-1"
|
|
255
|
+
style={{ boxShadow: '0 0 24px rgba(132,204,22,0.35)' }}
|
|
256
|
+
>
|
|
257
|
+
{badgeLabel}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
) : (
|
|
261
|
+
<span />
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{item.title && (
|
|
265
|
+
<h3
|
|
266
|
+
className="max-w-[86%] text-lg font-semibold tracking-[0.08em] text-stone-50"
|
|
267
|
+
style={{ textShadow: '0 4px 16px rgba(0,0,0,0.9)' }}
|
|
268
|
+
>
|
|
269
|
+
{item.title}
|
|
270
|
+
</h3>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</article>
|
|
275
|
+
</Link>
|
|
276
|
+
</article>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
|
|
281
|
+
eyebrow = 'Featured systems',
|
|
282
|
+
headlineMain = 'Components, blocks, and templates',
|
|
283
|
+
headlineAccent = 'built to feel alive',
|
|
284
|
+
subtext = 'A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.',
|
|
285
|
+
items = [],
|
|
286
|
+
columnsDesktop = 12,
|
|
287
|
+
columnsTablet = 2,
|
|
288
|
+
gapClass = 'gap-5',
|
|
289
|
+
containerClassName = 'max-w-7xl mx-auto px-4 md:px-6',
|
|
290
|
+
sectionClassName = 'py-4',
|
|
291
|
+
sectionId = 'studio-masonary-media',
|
|
292
|
+
customCSS,
|
|
293
|
+
showCta = false,
|
|
294
|
+
ctaAlign = 'center',
|
|
295
|
+
ctaDisplayMode = 'iconWithLabel',
|
|
296
|
+
ctaIconPosition = 'before',
|
|
297
|
+
ctaButtonText,
|
|
298
|
+
ctaButtonStyle = 'primary',
|
|
299
|
+
ctaSize = 'medium',
|
|
300
|
+
ctaIcon,
|
|
301
|
+
ctaLink,
|
|
302
|
+
ctaContainerClass,
|
|
303
|
+
}) => {
|
|
304
|
+
const gridRef = useRef<HTMLDivElement | null>(null)
|
|
305
|
+
const isotopeRef = useRef<any>(null)
|
|
306
|
+
|
|
307
|
+
const safeSectionId = sectionId || 'studio-masonary-media'
|
|
308
|
+
const gap = useMemo(() => getGapPx(gapClass), [gapClass])
|
|
309
|
+
const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6)
|
|
310
|
+
const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3)
|
|
311
|
+
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (!gridRef.current || !items || items.length === 0) return
|
|
314
|
+
|
|
315
|
+
let destroyed = false
|
|
316
|
+
|
|
317
|
+
const init = async () => {
|
|
318
|
+
const Isotope = (await import('isotope-layout')).default
|
|
319
|
+
const imagesLoaded = (await import('imagesloaded')).default
|
|
320
|
+
|
|
321
|
+
if (!gridRef.current || destroyed) return
|
|
322
|
+
|
|
323
|
+
const iso = new Isotope(gridRef.current, {
|
|
324
|
+
itemSelector: '.studio-masonry-item',
|
|
325
|
+
percentPosition: true,
|
|
326
|
+
transitionDuration: '0.35s',
|
|
327
|
+
masonry: {
|
|
328
|
+
columnWidth: '.studio-masonry-sizer',
|
|
329
|
+
gutter: '.studio-masonry-gutter',
|
|
330
|
+
fitWidth: false,
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
isotopeRef.current = iso
|
|
335
|
+
|
|
336
|
+
imagesLoaded(gridRef.current, () => {
|
|
337
|
+
if (!destroyed) iso.layout()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
window.setTimeout(() => {
|
|
341
|
+
if (!destroyed) iso.layout()
|
|
342
|
+
}, 250)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
init()
|
|
346
|
+
|
|
347
|
+
const handleResize = () => {
|
|
348
|
+
isotopeRef.current?.layout()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
window.addEventListener('resize', handleResize)
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
destroyed = true
|
|
355
|
+
window.removeEventListener('resize', handleResize)
|
|
356
|
+
isotopeRef.current?.destroy()
|
|
357
|
+
isotopeRef.current = null
|
|
358
|
+
}
|
|
359
|
+
}, [items])
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
isotopeRef.current?.reloadItems()
|
|
363
|
+
isotopeRef.current?.layout()
|
|
364
|
+
}, [items, columnsDesktop, columnsTablet, gap])
|
|
365
|
+
|
|
366
|
+
if (!items || items.length === 0) return null
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<>
|
|
370
|
+
<style
|
|
371
|
+
dangerouslySetInnerHTML={{
|
|
372
|
+
__html: `
|
|
373
|
+
#${safeSectionId} .studio-masonry-grid {
|
|
374
|
+
--studio-masonry-gap: ${gap}px;
|
|
375
|
+
--studio-masonry-cols: 1;
|
|
376
|
+
position: relative;
|
|
377
|
+
width: 100%;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#${safeSectionId} .studio-masonry-sizer,
|
|
381
|
+
#${safeSectionId} .studio-masonry-item {
|
|
382
|
+
width: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#${safeSectionId} .studio-masonry-gutter {
|
|
386
|
+
width: var(--studio-masonry-gap);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#${safeSectionId} .studio-masonry-item {
|
|
390
|
+
margin-bottom: var(--studio-masonry-gap);
|
|
391
|
+
will-change: transform;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@media (max-width: 639px) {
|
|
395
|
+
#${safeSectionId} .studio-masonry-grid {
|
|
396
|
+
--studio-masonry-cols: 2;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#${safeSectionId} .studio-masonry-sizer,
|
|
400
|
+
#${safeSectionId} .studio-masonry-item.is-square {
|
|
401
|
+
width: calc(
|
|
402
|
+
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
403
|
+
var(--studio-masonry-cols)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#${safeSectionId} .studio-masonry-item:not(.is-square) {
|
|
408
|
+
width: 100%;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#${safeSectionId} .studio-masonry-item.is-square .card-media {
|
|
412
|
+
aspect-ratio: 1 / 1 !important;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#${safeSectionId} .studio-masonry-item:not(.is-square) .card-media {
|
|
416
|
+
aspect-ratio: 16 / 9 !important;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@media (min-width: 640px) {
|
|
421
|
+
#${safeSectionId} .studio-masonry-grid {
|
|
422
|
+
--studio-masonry-cols: ${tabletColumns};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#${safeSectionId} .studio-masonry-sizer,
|
|
426
|
+
#${safeSectionId} .studio-masonry-item {
|
|
427
|
+
width: calc(
|
|
428
|
+
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
429
|
+
var(--studio-masonry-cols)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#${safeSectionId} .studio-masonry-item.is-wide {
|
|
434
|
+
width: calc(
|
|
435
|
+
(
|
|
436
|
+
(100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
|
|
437
|
+
var(--studio-masonry-cols)
|
|
438
|
+
) *
|
|
439
|
+
2 + var(--studio-masonry-gap)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@media (min-width: 1024px) {
|
|
445
|
+
#${safeSectionId} .studio-masonry-grid {
|
|
446
|
+
--studio-masonry-cols: ${desktopColumns};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@media (min-width: 640px) and (max-width: 1023px) {
|
|
451
|
+
#${safeSectionId} .studio-masonry-item.is-wide {
|
|
452
|
+
width: 100%;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#${safeSectionId} .card-media {
|
|
457
|
+
min-height: 10rem;
|
|
458
|
+
scale: 1;
|
|
459
|
+
}
|
|
460
|
+
`,
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
|
|
464
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
465
|
+
|
|
466
|
+
<section id={safeSectionId} className={cn('studio-masonary-media', sectionClassName)}>
|
|
467
|
+
<div className={cn(containerClassName)}>
|
|
468
|
+
<motion.div
|
|
469
|
+
variants={containerVariants}
|
|
470
|
+
initial="hidden"
|
|
471
|
+
whileInView="show"
|
|
472
|
+
viewport={{ once: false, amount: 0.2 }}
|
|
473
|
+
className="mb-14 flex flex-col justify-between gap-6 overflow-hidden lg:flex-row lg:items-end"
|
|
474
|
+
>
|
|
475
|
+
<motion.div variants={itemVariants}>
|
|
476
|
+
{eyebrow && (
|
|
477
|
+
<p className="top-caption mb-3 text-[11.5px] font-semibold uppercase tracking-widest text-stone-400">
|
|
478
|
+
{eyebrow}
|
|
479
|
+
</p>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
{(headlineMain || headlineAccent) && (
|
|
483
|
+
<h2 className="text-3d text-4xl font-bold leading-tight text-stone-900 dark:text-stone-50 md:text-5xl">
|
|
484
|
+
{headlineMain}
|
|
485
|
+
|
|
486
|
+
{headlineAccent && (
|
|
487
|
+
<>
|
|
488
|
+
<br />
|
|
489
|
+
<span className="text-stone-400">{headlineAccent}</span>
|
|
490
|
+
</>
|
|
491
|
+
)}
|
|
492
|
+
</h2>
|
|
493
|
+
)}
|
|
494
|
+
</motion.div>
|
|
495
|
+
|
|
496
|
+
{subtext && (
|
|
497
|
+
<motion.p
|
|
498
|
+
variants={itemVariants}
|
|
499
|
+
className="max-w-sm text-base leading-relaxed text-stone-400 dark:text-stone-300 lg:text-right"
|
|
500
|
+
>
|
|
501
|
+
{subtext}
|
|
502
|
+
</motion.p>
|
|
503
|
+
)}
|
|
504
|
+
</motion.div>
|
|
505
|
+
|
|
506
|
+
<div ref={gridRef} className="studio-masonry-grid">
|
|
507
|
+
<div className="studio-masonry-sizer" />
|
|
508
|
+
<div className="studio-masonry-gutter" />
|
|
509
|
+
|
|
510
|
+
{items.map((item, index) => (
|
|
511
|
+
<StudioMasonaryCard
|
|
512
|
+
key={item.id ?? `${item.title}-${index}`}
|
|
513
|
+
item={item}
|
|
514
|
+
index={index}
|
|
515
|
+
/>
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
{showCta && (
|
|
520
|
+
<div
|
|
521
|
+
className={cn(
|
|
522
|
+
'mt-10 flex',
|
|
523
|
+
ctaAlign === 'left' && 'justify-start',
|
|
524
|
+
ctaAlign === 'center' && 'justify-center',
|
|
525
|
+
ctaAlign === 'right' && 'justify-end',
|
|
526
|
+
ctaContainerClass,
|
|
527
|
+
)}
|
|
528
|
+
>
|
|
529
|
+
<a
|
|
530
|
+
href={(ctaLink as any)?.url ?? '#'}
|
|
531
|
+
className="inline-flex items-center gap-1.5 bg-indigo-700 text-stone-50 text-xs font-semibold px-3 py-1.5 rounded-lg group-hover:bg-indigo-600 group-hover:scale-[1.05] group-hover:shadow-xl transition-all duration-150 whitespace-nowrap"
|
|
532
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
533
|
+
>
|
|
534
|
+
{/* TODO: Reconnect ButtonWithIcon if this project provides it. */}
|
|
535
|
+
{ctaButtonText ?? (ctaLink as any)?.label ?? 'Learn more'}
|
|
536
|
+
</a>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
</section>
|
|
540
|
+
</>
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export default DDMasonaryMedia
|