blocks-dusted 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/index.js +8 -0
- package/package.json +40 -0
- package/src/cli.js +79 -0
- package/src/commands/add.js +327 -0
- package/src/commands/doctor.js +21 -0
- package/src/commands/list.js +18 -0
- package/src/installers/checkRequirements.js +101 -0
- package/src/installers/copyTemplateFiles.js +79 -0
- package/src/installers/installDependencies.js +69 -0
- package/src/installers/patchPayloadBlocksArray.js +122 -0
- package/src/installers/patchRenderBlocks.js +110 -0
- package/src/installers/runGenerators.js +3 -0
- package/src/installers/writeInstalledBlockReadme.js +117 -0
- package/src/registry/listTemplates.js +17 -0
- package/src/registry/loadTemplateManifest.js +20 -0
- package/src/registry/validateTemplateManifest.js +69 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/packageManager.js +42 -0
- package/src/utils/paths.js +7 -0
- package/src/utils/project.js +122 -0
- package/src/utils/strings.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +573 -0
- package/templates/blocks/DD-BookCall/config.ts +239 -0
- package/templates/blocks/DD-BookCall/manifest.json +112 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
- package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
- package/templates/blocks/DD-Carousel-A/config.ts +163 -0
- package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
- package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
- package/templates/blocks/DD-Carousel-B/config.ts +176 -0
- package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
- package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
- package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
- package/templates/blocks/DD-Contact/Component.tsx +439 -0
- package/templates/blocks/DD-Contact/config.ts +217 -0
- package/templates/blocks/DD-Contact/manifest.json +107 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
- package/templates/blocks/DD-FeatCat/config.ts +204 -0
- package/templates/blocks/DD-FeatCat/manifest.json +87 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
- package/templates/blocks/DD-FeatStrip/config.ts +134 -0
- package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
- package/templates/blocks/DD-Hero/Component.tsx +317 -0
- package/templates/blocks/DD-Hero/config.ts +267 -0
- package/templates/blocks/DD-Hero/manifest.json +92 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
- package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
- package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
- package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
- package/templates/blocks/DD-Pricing/Component.tsx +380 -0
- package/templates/blocks/DD-Pricing/config.ts +215 -0
- package/templates/blocks/DD-Pricing/manifest.json +78 -0
- package/templates/blocks/DD-Process/Component.tsx +155 -0
- package/templates/blocks/DD-Process/config.ts +150 -0
- package/templates/blocks/DD-Process/manifest.json +82 -0
- package/templates/blocks/DD-Section/Component.tsx +327 -0
- package/templates/blocks/DD-Section/config.ts +214 -0
- package/templates/blocks/DD-Section/manifest.json +85 -0
- package/templates/blocks/DD-Services/Component.tsx +188 -0
- package/templates/blocks/DD-Services/config.ts +149 -0
- package/templates/blocks/DD-Services/manifest.json +95 -0
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
- package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
- package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
- package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
- package/templates/blocks/DD-Slider-A/config.ts +118 -0
- package/templates/blocks/DD-Slider-A/manifest.json +86 -0
- package/templates/blocks/DD-StackCards/Component.tsx +496 -0
- package/templates/blocks/DD-StackCards/config.ts +399 -0
- package/templates/blocks/DD-StackCards/manifest.json +87 -0
- package/templates/blocks/DD-Team/Component.tsx +265 -0
- package/templates/blocks/DD-Team/config.ts +173 -0
- package/templates/blocks/DD-Team/manifest.json +82 -0
- package/templates/blocks/DD-TechStack/Component.tsx +189 -0
- package/templates/blocks/DD-TechStack/config.ts +153 -0
- package/templates/blocks/DD-TechStack/manifest.json +118 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
- package/templates/blocks/DD-Testimonials/config.ts +120 -0
- package/templates/blocks/DD-Testimonials/manifest.json +87 -0
- package/templates/blocks/DD-Work/Component.tsx +328 -0
- package/templates/blocks/DD-Work/config.ts +165 -0
- package/templates/blocks/DD-Work/manifest.json +87 -0
- package/templates/blocks/DD-Work-B/Component.tsx +320 -0
- package/templates/blocks/DD-Work-B/config.ts +165 -0
- package/templates/blocks/DD-Work-B/manifest.json +75 -0
- package/templates/shared/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/components/ui/button.tsx +233 -0
- package/templates/shared/components/ui/input.tsx +41 -0
- package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
- package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
- package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
- package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
- package/templates/shared/utilities/canUseDOM.ts +1 -0
- package/templates/shared/utilities/getURL.ts +31 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useRef, useMemo } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { gsap } from "gsap";
|
|
6
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
7
|
+
import dynamic from "next/dynamic";
|
|
8
|
+
|
|
9
|
+
const DotLottieReact = dynamic(
|
|
10
|
+
() => import("@lottiefiles/dotlottie-react").then((m) => m.DotLottieReact),
|
|
11
|
+
{ ssr: false, loading: () => null },
|
|
12
|
+
);
|
|
13
|
+
import {
|
|
14
|
+
PencilRuler,
|
|
15
|
+
Code2,
|
|
16
|
+
Rocket,
|
|
17
|
+
TrendingUp,
|
|
18
|
+
Settings,
|
|
19
|
+
BarChart2,
|
|
20
|
+
Users,
|
|
21
|
+
Globe,
|
|
22
|
+
Zap,
|
|
23
|
+
Layers,
|
|
24
|
+
ShieldCheck,
|
|
25
|
+
Lightbulb,
|
|
26
|
+
Puzzle,
|
|
27
|
+
Star,
|
|
28
|
+
Check,
|
|
29
|
+
UserPlus,
|
|
30
|
+
Send,
|
|
31
|
+
ArrowUp,
|
|
32
|
+
Hammer,
|
|
33
|
+
} from "lucide-react";
|
|
34
|
+
import type { LucideIcon } from "lucide-react";
|
|
35
|
+
type Media = { url?: string | null; alt?: string | null; [key: string]: any };
|
|
36
|
+
|
|
37
|
+
if (typeof window !== "undefined") {
|
|
38
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Types ─────────────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
interface TagItem {
|
|
44
|
+
id?: string;
|
|
45
|
+
tag: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SlideItem {
|
|
49
|
+
id?: string;
|
|
50
|
+
phase?: string | null;
|
|
51
|
+
watermark?: string | null;
|
|
52
|
+
icon?: string | null;
|
|
53
|
+
accent?: "lime" | "purple" | null;
|
|
54
|
+
headline?: string | null;
|
|
55
|
+
sub?: string | null;
|
|
56
|
+
body?: string | null;
|
|
57
|
+
tags?: TagItem[] | null;
|
|
58
|
+
visualType?:
|
|
59
|
+
| "wireframe"
|
|
60
|
+
| "codeEditor"
|
|
61
|
+
| "pipeline"
|
|
62
|
+
| "funnel"
|
|
63
|
+
| "automation"
|
|
64
|
+
| null;
|
|
65
|
+
customMedia?: Media | string | null;
|
|
66
|
+
customMediaType?: "image" | "video" | "lottie" | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DDHorizontalScrollProps {
|
|
70
|
+
id?: string | null;
|
|
71
|
+
sectionLabel?: string | null;
|
|
72
|
+
slides?: SlideItem[] | null;
|
|
73
|
+
sectionId?: string | null;
|
|
74
|
+
enableCustomCSS?: boolean | null;
|
|
75
|
+
customCSS?: string | null;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── Accent helpers ─────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
const ACCENTS = {
|
|
82
|
+
lime: { hex: "#84cc16", rgb: "132,204,22" },
|
|
83
|
+
purple: { hex: "#9803e2", rgb: "152,3,226" },
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
type AccentKey = keyof typeof ACCENTS;
|
|
87
|
+
|
|
88
|
+
function getAccent(key?: string | null) {
|
|
89
|
+
return ACCENTS[(key as AccentKey) ?? "lime"] ?? ACCENTS.lime;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── Icon map ───────────────────────────────────────────────────── */
|
|
93
|
+
|
|
94
|
+
const ICON_MAP: Record<string, LucideIcon> = {
|
|
95
|
+
penRuler: PencilRuler,
|
|
96
|
+
code2: Code2,
|
|
97
|
+
rocket: Rocket,
|
|
98
|
+
trendingUp: TrendingUp,
|
|
99
|
+
settings: Settings,
|
|
100
|
+
barChart2: BarChart2,
|
|
101
|
+
users: Users,
|
|
102
|
+
globe: Globe,
|
|
103
|
+
zap: Zap,
|
|
104
|
+
layers: Layers,
|
|
105
|
+
shieldCheck: ShieldCheck,
|
|
106
|
+
lightbulb: Lightbulb,
|
|
107
|
+
puzzle: Puzzle,
|
|
108
|
+
star: Star,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function SlideIcon({
|
|
112
|
+
name,
|
|
113
|
+
size = 14,
|
|
114
|
+
style,
|
|
115
|
+
}: {
|
|
116
|
+
name?: string | null;
|
|
117
|
+
size?: number;
|
|
118
|
+
style?: React.CSSProperties;
|
|
119
|
+
}) {
|
|
120
|
+
const Icon = ICON_MAP[name ?? "star"] ?? Star;
|
|
121
|
+
return <Icon size={size} style={style} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── Visual panels ─────────────────────────────────────────────── */
|
|
125
|
+
|
|
126
|
+
function WireframeVisual({ accent }: { accent: string }) {
|
|
127
|
+
const { hex, rgb } = getAccent(accent);
|
|
128
|
+
return (
|
|
129
|
+
<section className="relative w-full h-full flex items-center justify-center">
|
|
130
|
+
<div className="relative w-64 h-64 scale-80 md:scale-100">
|
|
131
|
+
<div
|
|
132
|
+
className="absolute inset-0 rounded-2xl border"
|
|
133
|
+
style={{
|
|
134
|
+
borderColor: `rgba(${rgb},0.2)`,
|
|
135
|
+
background: `rgba(${rgb},0.03)`,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<div className="p-4 space-y-3">
|
|
139
|
+
<div
|
|
140
|
+
className="h-3 rounded-full w-3/4"
|
|
141
|
+
style={{ background: `rgba(${rgb},0.3)` }}
|
|
142
|
+
/>
|
|
143
|
+
<div
|
|
144
|
+
className="h-2 rounded-full w-full"
|
|
145
|
+
style={{ background: "rgba(255,255,255,0.08)" }}
|
|
146
|
+
/>
|
|
147
|
+
<div
|
|
148
|
+
className="h-2 rounded-full w-5/6"
|
|
149
|
+
style={{ background: "rgba(255,255,255,0.08)" }}
|
|
150
|
+
/>
|
|
151
|
+
<div className="grid grid-cols-2 gap-2 mt-4">
|
|
152
|
+
<div
|
|
153
|
+
className="h-16 rounded-lg"
|
|
154
|
+
style={{
|
|
155
|
+
background: `rgba(${rgb},0.08)`,
|
|
156
|
+
border: `1px solid rgba(${rgb},0.15)`,
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
<div
|
|
160
|
+
className="h-16 rounded-lg"
|
|
161
|
+
style={{
|
|
162
|
+
background: "rgba(255,255,255,0.04)",
|
|
163
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div
|
|
168
|
+
className="h-8 rounded-full mt-2"
|
|
169
|
+
style={{
|
|
170
|
+
background: `rgba(${rgb},0.2)`,
|
|
171
|
+
border: `1px solid rgba(${rgb},0.4)`,
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
{/* Cursor dot */}
|
|
177
|
+
<div
|
|
178
|
+
className="absolute -right-4 top-1/3 w-5 h-5 rounded-full border-2 flex items-center justify-center"
|
|
179
|
+
style={{ borderColor: hex, background: `rgba(${rgb},0.15)` }}
|
|
180
|
+
>
|
|
181
|
+
<div
|
|
182
|
+
className="w-1.5 h-1.5 rounded-full"
|
|
183
|
+
style={{ background: hex }}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
{/* Grid bg */}
|
|
187
|
+
<div
|
|
188
|
+
className="absolute -inset-6 pointer-events-none"
|
|
189
|
+
style={{
|
|
190
|
+
backgroundImage: `linear-gradient(rgba(${rgb},0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(${rgb},0.05) 1px, transparent 1px)`,
|
|
191
|
+
backgroundSize: "24px 24px",
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</section>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function CodeEditorVisual({ accent }: { accent: string }) {
|
|
200
|
+
const { hex, rgb } = getAccent(accent);
|
|
201
|
+
return (
|
|
202
|
+
<div className="relative w-full h-full flex items-center justify-center scale-80 md:scale-100">
|
|
203
|
+
<div
|
|
204
|
+
className="relative w-72 rounded-xl overflow-hidden"
|
|
205
|
+
style={{ background: "#0d0d0d", border: `1px solid rgba(${rgb},0.15)` }}
|
|
206
|
+
>
|
|
207
|
+
{/* Title bar */}
|
|
208
|
+
<div
|
|
209
|
+
className="flex items-center gap-2 px-4 py-3 border-b"
|
|
210
|
+
style={{ borderColor: "rgba(255,255,255,0.06)" }}
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
className="w-3 h-3 rounded-full"
|
|
214
|
+
style={{ background: "#ff5f57" }}
|
|
215
|
+
/>
|
|
216
|
+
<div
|
|
217
|
+
className="w-3 h-3 rounded-full"
|
|
218
|
+
style={{ background: "#febc2e" }}
|
|
219
|
+
/>
|
|
220
|
+
<div
|
|
221
|
+
className="w-3 h-3 rounded-full"
|
|
222
|
+
style={{ background: "#28c840" }}
|
|
223
|
+
/>
|
|
224
|
+
<span
|
|
225
|
+
className="ml-2 text-xs"
|
|
226
|
+
style={{ color: "rgba(255,255,255,0.3)", fontFamily: "monospace" }}
|
|
227
|
+
>
|
|
228
|
+
index.tsx
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
{/* Code lines */}
|
|
232
|
+
<div className="p-4 space-y-1.5 font-mono text-xs">
|
|
233
|
+
<div>
|
|
234
|
+
<span style={{ color: "#9803e2" }}>const</span>{" "}
|
|
235
|
+
<span style={{ color: hex }}>Hero</span>{" "}
|
|
236
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>=</span>{" "}
|
|
237
|
+
<span style={{ color: "#9803e2" }}>()</span>{" "}
|
|
238
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>=></span>{" "}
|
|
239
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>{"{"}</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="pl-4">
|
|
242
|
+
<span style={{ color: "#9803e2" }}>return</span>{" "}
|
|
243
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>(</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="pl-8">
|
|
246
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}><</span>
|
|
247
|
+
<span style={{ color: hex }}>section</span>{" "}
|
|
248
|
+
<span style={{ color: "#9803e2" }}>className</span>
|
|
249
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}>=</span>
|
|
250
|
+
<span style={{ color: "#febc2e" }}>"hero"</span>
|
|
251
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}>></span>
|
|
252
|
+
</div>
|
|
253
|
+
<div className="pl-12">
|
|
254
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}><</span>
|
|
255
|
+
<span style={{ color: hex }}>h1</span>
|
|
256
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}>></span>
|
|
257
|
+
<span style={{ color: "rgba(255,255,255,0.6)" }}>Convert.</span>
|
|
258
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}></</span>
|
|
259
|
+
<span style={{ color: hex }}>h1</span>
|
|
260
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}>></span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="pl-8">
|
|
263
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}></</span>
|
|
264
|
+
<span style={{ color: hex }}>section</span>
|
|
265
|
+
<span style={{ color: "rgba(255,255,255,0.3)" }}>></span>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="pl-4">
|
|
268
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>)</span>
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>{"}"}</span>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="mt-2 flex items-center gap-2">
|
|
274
|
+
<span style={{ color: "rgba(255,255,255,0.2)" }}>{"// "}</span>
|
|
275
|
+
<span
|
|
276
|
+
className="inline-block w-2 h-4 animate-pulse"
|
|
277
|
+
style={{ background: hex }}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function PipelineVisual() {
|
|
287
|
+
const steps = [
|
|
288
|
+
{ label: "Build", status: "done", Icon: Hammer },
|
|
289
|
+
{ label: "Test", status: "done", Icon: ShieldCheck },
|
|
290
|
+
{ label: "Deploy", status: "active", Icon: Rocket },
|
|
291
|
+
{ label: "Live", status: "pending", Icon: Globe },
|
|
292
|
+
];
|
|
293
|
+
return (
|
|
294
|
+
<div className="relative w-full h-full flex items-center justify-center scale-80 md:scale-100">
|
|
295
|
+
<div className="relative w-64">
|
|
296
|
+
{steps.map((step, i) => (
|
|
297
|
+
<div key={i} className="flex items-center gap-3 mb-3">
|
|
298
|
+
<div
|
|
299
|
+
className="w-9 h-9 rounded-full flex items-center justify-center shrink-0"
|
|
300
|
+
style={{
|
|
301
|
+
background:
|
|
302
|
+
step.status === "done"
|
|
303
|
+
? "rgba(132,204,22,0.15)"
|
|
304
|
+
: step.status === "active"
|
|
305
|
+
? "rgba(152,3,226,0.2)"
|
|
306
|
+
: "rgba(255,255,255,0.04)",
|
|
307
|
+
border: `1px solid ${
|
|
308
|
+
step.status === "done"
|
|
309
|
+
? "rgba(132,204,22,0.4)"
|
|
310
|
+
: step.status === "active"
|
|
311
|
+
? "rgba(152,3,226,0.5)"
|
|
312
|
+
: "rgba(255,255,255,0.1)"
|
|
313
|
+
}`,
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
<step.Icon
|
|
317
|
+
size={14}
|
|
318
|
+
style={{
|
|
319
|
+
color:
|
|
320
|
+
step.status === "done"
|
|
321
|
+
? "#84cc16"
|
|
322
|
+
: step.status === "active"
|
|
323
|
+
? "#9803e2"
|
|
324
|
+
: "rgba(255,255,255,0.2)",
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
<div className="flex-1">
|
|
329
|
+
<div className="flex items-center justify-between">
|
|
330
|
+
<span
|
|
331
|
+
className="text-sm font-semibold"
|
|
332
|
+
style={{
|
|
333
|
+
color:
|
|
334
|
+
step.status === "pending"
|
|
335
|
+
? "rgba(255,255,255,0.2)"
|
|
336
|
+
: "rgba(255,255,255,0.8)",
|
|
337
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
{step.label}
|
|
341
|
+
</span>
|
|
342
|
+
{step.status === "done" && (
|
|
343
|
+
<Check size={12} style={{ color: "#84cc16" }} />
|
|
344
|
+
)}
|
|
345
|
+
{step.status === "active" && (
|
|
346
|
+
<span
|
|
347
|
+
className="text-xs animate-pulse"
|
|
348
|
+
style={{ color: "#9803e2" }}
|
|
349
|
+
>
|
|
350
|
+
Running…
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
<div
|
|
355
|
+
className="h-1 rounded-full mt-1.5"
|
|
356
|
+
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
357
|
+
>
|
|
358
|
+
<div
|
|
359
|
+
className="h-full rounded-full"
|
|
360
|
+
style={{
|
|
361
|
+
width:
|
|
362
|
+
step.status === "done"
|
|
363
|
+
? "100%"
|
|
364
|
+
: step.status === "active"
|
|
365
|
+
? "60%"
|
|
366
|
+
: "0%",
|
|
367
|
+
background: step.status === "done" ? "#84cc16" : "#9803e2",
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
))}
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function FunnelVisual({ accent }: { accent: string }) {
|
|
380
|
+
const { hex, rgb } = getAccent(accent);
|
|
381
|
+
const rows = [
|
|
382
|
+
{ label: "Visitors", value: "24,800", pct: 100 },
|
|
383
|
+
{ label: "Engaged", value: "11,200", pct: 65 },
|
|
384
|
+
{ label: "Leads", value: "3,840", pct: 40 },
|
|
385
|
+
{ label: "Converted", value: "960", pct: 22 },
|
|
386
|
+
];
|
|
387
|
+
return (
|
|
388
|
+
<div className="relative w-full h-full flex items-center justify-center scale-80 md:scale-100">
|
|
389
|
+
<div className="relative w-64 space-y-3">
|
|
390
|
+
{rows.map((row, i) => (
|
|
391
|
+
<div key={i}>
|
|
392
|
+
<div
|
|
393
|
+
className="flex justify-between text-xs mb-1"
|
|
394
|
+
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
395
|
+
>
|
|
396
|
+
<span style={{ color: "rgba(255,255,255,0.4)" }}>
|
|
397
|
+
{row.label}
|
|
398
|
+
</span>
|
|
399
|
+
<span style={{ color: "rgba(255,255,255,0.7)" }}>
|
|
400
|
+
{row.value}
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
<div
|
|
404
|
+
className="h-2 rounded-full"
|
|
405
|
+
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
406
|
+
>
|
|
407
|
+
<div
|
|
408
|
+
className="h-full rounded-full"
|
|
409
|
+
style={{ width: `${row.pct}%`, background: hex }}
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
<div
|
|
415
|
+
className="mt-4 p-3 rounded-xl flex items-center gap-3"
|
|
416
|
+
style={{
|
|
417
|
+
background: `rgba(${rgb},0.08)`,
|
|
418
|
+
border: `1px solid rgba(${rgb},0.2)`,
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
<ArrowUp size={18} style={{ color: hex }} />
|
|
422
|
+
<div>
|
|
423
|
+
<div
|
|
424
|
+
className="text-xs font-bold"
|
|
425
|
+
style={{
|
|
426
|
+
color: hex,
|
|
427
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
+3.2× Conversion Rate
|
|
431
|
+
</div>
|
|
432
|
+
<div
|
|
433
|
+
className="text-xs"
|
|
434
|
+
style={{
|
|
435
|
+
color: "rgba(255,255,255,0.3)",
|
|
436
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
vs. industry average
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function AutomationVisual() {
|
|
449
|
+
const flows = [
|
|
450
|
+
{
|
|
451
|
+
from: "New Lead",
|
|
452
|
+
to: "CRM",
|
|
453
|
+
Icon: UserPlus,
|
|
454
|
+
color: "#9803e2",
|
|
455
|
+
rgb: "152,3,226",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
from: "CRM",
|
|
459
|
+
to: "Email Seq.",
|
|
460
|
+
Icon: Send,
|
|
461
|
+
color: "#84cc16",
|
|
462
|
+
rgb: "132,204,22",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
from: "Email Seq.",
|
|
466
|
+
to: "Sales Team",
|
|
467
|
+
Icon: Users,
|
|
468
|
+
color: "#9803e2",
|
|
469
|
+
rgb: "152,3,226",
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
return (
|
|
473
|
+
<div className="relative w-full h-full flex items-center justify-center scale-80 md:scale-100">
|
|
474
|
+
<div className="relative w-64">
|
|
475
|
+
{flows.map((flow, i) => (
|
|
476
|
+
<div key={i} className="flex items-center gap-2 mb-4">
|
|
477
|
+
<div
|
|
478
|
+
className="px-3 py-2 rounded-lg text-xs font-semibold whitespace-nowrap"
|
|
479
|
+
style={{
|
|
480
|
+
background: "rgba(255,255,255,0.05)",
|
|
481
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
482
|
+
color: "rgba(255,255,255,0.6)",
|
|
483
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
{flow.from}
|
|
487
|
+
</div>
|
|
488
|
+
<div className="flex-1 flex items-center gap-1">
|
|
489
|
+
<div
|
|
490
|
+
className="flex-1 h-px"
|
|
491
|
+
style={{ background: `rgba(${flow.rgb},0.4)` }}
|
|
492
|
+
/>
|
|
493
|
+
<div
|
|
494
|
+
className="w-6 h-6 rounded-full flex items-center justify-center"
|
|
495
|
+
style={{
|
|
496
|
+
background: `rgba(${flow.rgb},0.15)`,
|
|
497
|
+
border: `1px solid ${flow.color}`,
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
<flow.Icon size={10} style={{ color: flow.color }} />
|
|
501
|
+
</div>
|
|
502
|
+
<div
|
|
503
|
+
className="flex-1 h-px"
|
|
504
|
+
style={{ background: `rgba(${flow.rgb},0.4)` }}
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
<div
|
|
508
|
+
className="px-3 py-2 rounded-lg text-xs font-semibold whitespace-nowrap"
|
|
509
|
+
style={{
|
|
510
|
+
background: `rgba(${flow.rgb},0.1)`,
|
|
511
|
+
border: `1px solid ${flow.color}40`,
|
|
512
|
+
color: flow.color,
|
|
513
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
514
|
+
}}
|
|
515
|
+
>
|
|
516
|
+
{flow.to}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
))}
|
|
520
|
+
<div
|
|
521
|
+
className="mt-2 p-3 rounded-xl text-center"
|
|
522
|
+
style={{
|
|
523
|
+
background: "rgba(152,3,226,0.08)",
|
|
524
|
+
border: "1px solid rgba(152,3,226,0.2)",
|
|
525
|
+
}}
|
|
526
|
+
>
|
|
527
|
+
<div
|
|
528
|
+
className="text-xs font-bold flex items-center justify-center gap-1"
|
|
529
|
+
style={{
|
|
530
|
+
color: "#9803e2",
|
|
531
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
532
|
+
}}
|
|
533
|
+
>
|
|
534
|
+
<Zap size={12} />
|
|
535
|
+
Fully automated. Zero manual steps.
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const VISUAL_MAP: Record<string, React.ComponentType<{ accent: string }>> = {
|
|
544
|
+
wireframe: WireframeVisual,
|
|
545
|
+
codeEditor: CodeEditorVisual,
|
|
546
|
+
pipeline: PipelineVisual as React.ComponentType<{ accent: string }>,
|
|
547
|
+
funnel: FunnelVisual,
|
|
548
|
+
automation: AutomationVisual as React.ComponentType<{ accent: string }>,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/* ── Slide panel ────────────────────────────────────────────────── */
|
|
552
|
+
|
|
553
|
+
interface SlidePanelProps {
|
|
554
|
+
slide: SlideItem;
|
|
555
|
+
index: number;
|
|
556
|
+
total: number;
|
|
557
|
+
panelRef: (el: HTMLDivElement | null) => void;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function isLocalMediaSrc(src?: string | null): src is string {
|
|
561
|
+
return typeof src === "string" && src.startsWith("/api/media/file/");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function SlidePanel({ slide, index, total, panelRef }: SlidePanelProps) {
|
|
565
|
+
const { hex, rgb } = getAccent(slide.accent);
|
|
566
|
+
const Visual = VISUAL_MAP[slide.visualType ?? "wireframe"] ?? WireframeVisual;
|
|
567
|
+
|
|
568
|
+
// Extract media URL safely
|
|
569
|
+
const mediaUrl: string =
|
|
570
|
+
typeof slide.customMedia === "string"
|
|
571
|
+
? slide.customMedia
|
|
572
|
+
: (slide.customMedia?.url ?? "");
|
|
573
|
+
const safeMediaUrl = isLocalMediaSrc(mediaUrl) ? mediaUrl : "";
|
|
574
|
+
const mediaAlt =
|
|
575
|
+
slide.customMedia && typeof slide.customMedia !== "string"
|
|
576
|
+
? (slide.customMedia.alt ?? "")
|
|
577
|
+
: "";
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
<div
|
|
581
|
+
ref={panelRef}
|
|
582
|
+
className={`shs-slide shs-slide--${index} relative shrink-0 h-full flex items-center overflow-hidden`}
|
|
583
|
+
style={{ width: "100vw" }}
|
|
584
|
+
>
|
|
585
|
+
{/* Watermark number */}
|
|
586
|
+
<div
|
|
587
|
+
className="absolute top-1/2 -translate-y-1/2 right-16 font-black select-none pointer-events-none text-stone-200/40 dark:text-stone-200/5"
|
|
588
|
+
style={{
|
|
589
|
+
fontSize: "clamp(120px, 18vw, 220px)",
|
|
590
|
+
|
|
591
|
+
lineHeight: 1,
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
{slide.watermark}
|
|
595
|
+
{/* {String(index + 1).padStart(2, '0')} */}
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
{/* Vertical divider */}
|
|
599
|
+
{index < total - 1 && (
|
|
600
|
+
<div
|
|
601
|
+
className="absolute right-0 top-1/4 bottom-1/4 w-px"
|
|
602
|
+
style={{
|
|
603
|
+
background:
|
|
604
|
+
"linear-gradient(to bottom, transparent, rgba(255,255,255,0.08), transparent)",
|
|
605
|
+
}}
|
|
606
|
+
/>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
<div className="relative z-10 w-full max-w-6xl mx-auto px-6 md:px-16 grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-20 items-center">
|
|
610
|
+
{/* ── Left: content ── */}
|
|
611
|
+
<div className="space-y-2 md:space-y-7">
|
|
612
|
+
{/* Phase badge */}
|
|
613
|
+
<div className="anim-el flex items-center gap-3">
|
|
614
|
+
<div
|
|
615
|
+
className="w-7 h-7 md:w-8 md:h-8 rounded-full flex items-center justify-center"
|
|
616
|
+
style={{
|
|
617
|
+
background: `rgba(${rgb},0.12)`,
|
|
618
|
+
border: `1px solid rgba(${rgb},0.3)`,
|
|
619
|
+
}}
|
|
620
|
+
>
|
|
621
|
+
<SlideIcon name={slide.icon} size={14} style={{ color: hex }} />
|
|
622
|
+
</div>
|
|
623
|
+
<span
|
|
624
|
+
className="text-xs scale-90 origin-top-left font-black tracking-widest uppercase"
|
|
625
|
+
style={{
|
|
626
|
+
color: hex,
|
|
627
|
+
}}
|
|
628
|
+
>
|
|
629
|
+
{slide.phase}
|
|
630
|
+
</span>
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
{/* Headline */}
|
|
634
|
+
<h2
|
|
635
|
+
className="anim-el font-extrabold leading-none tracking-tight text-stone-900 dark:text-stone-50"
|
|
636
|
+
style={{
|
|
637
|
+
fontSize: "clamp(42px, 5vw, 72px)",
|
|
638
|
+
|
|
639
|
+
whiteSpace: "pre-line",
|
|
640
|
+
}}
|
|
641
|
+
>
|
|
642
|
+
{" "}
|
|
643
|
+
<span className="relative">
|
|
644
|
+
{(slide.headline ?? "").replace(/\\n/g, "\n")}
|
|
645
|
+
</span>
|
|
646
|
+
</h2>
|
|
647
|
+
|
|
648
|
+
{/* Sub */}
|
|
649
|
+
{slide.sub && (
|
|
650
|
+
<p
|
|
651
|
+
className="anim-el text-sm md:text-md font-semibold"
|
|
652
|
+
style={{
|
|
653
|
+
color: hex,
|
|
654
|
+
}}
|
|
655
|
+
>
|
|
656
|
+
{" "}
|
|
657
|
+
<span className="relative">{slide.sub}</span>
|
|
658
|
+
</p>
|
|
659
|
+
)}
|
|
660
|
+
|
|
661
|
+
{/* Body */}
|
|
662
|
+
{slide.body && (
|
|
663
|
+
<p
|
|
664
|
+
className="anim-el text-sm md:text-md leading-relaxed max-w-md text-stone-600 dark:text-stone-200"
|
|
665
|
+
// style={{
|
|
666
|
+
// fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
667
|
+
// }}
|
|
668
|
+
>
|
|
669
|
+
{" "}
|
|
670
|
+
<span className="relative"> {slide.body}</span>
|
|
671
|
+
</p>
|
|
672
|
+
)}
|
|
673
|
+
|
|
674
|
+
{/* Tags */}
|
|
675
|
+
{slide.tags && slide.tags.length > 0 && (
|
|
676
|
+
<div className="anim-el flex flex-wrap gap-2 scale-80 md:scale-100 origin-bottom-left">
|
|
677
|
+
{slide.tags.map((t, ti) => (
|
|
678
|
+
<span
|
|
679
|
+
key={t.id ?? ti}
|
|
680
|
+
className="px-3 py-1 rounded-full text-xs font-semibold bg-lines"
|
|
681
|
+
style={{
|
|
682
|
+
background: `rgba(${rgb},0.08)`,
|
|
683
|
+
border: `1px solid rgba(${rgb},0.2)`,
|
|
684
|
+
color: hex,
|
|
685
|
+
fontFamily: "'Plus Jakarta Sans', sans-serif",
|
|
686
|
+
}}
|
|
687
|
+
>
|
|
688
|
+
{t.tag}
|
|
689
|
+
</span>
|
|
690
|
+
))}
|
|
691
|
+
</div>
|
|
692
|
+
)}
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
{/* ── Right: visual ── */}
|
|
696
|
+
<div
|
|
697
|
+
className="anim-el relative h-56 md:h-80 rounded-2xl overflow-hidden flex items-center justify-center"
|
|
698
|
+
style={{
|
|
699
|
+
background: `linear-gradient(135deg, rgba(${rgb},0.15) 0%, rgba(0,0,0,0) 100%)`,
|
|
700
|
+
border: `1px solid rgba(${rgb},0.5)`,
|
|
701
|
+
}}
|
|
702
|
+
>
|
|
703
|
+
{/* Glow */}
|
|
704
|
+
<div
|
|
705
|
+
className="absolute inset-0 pointer-events-none"
|
|
706
|
+
style={{
|
|
707
|
+
background: `radial-gradient(ellipse at 50% 50%, rgba(${rgb},0.08) 0%, transparent 70%)`,
|
|
708
|
+
}}
|
|
709
|
+
/>
|
|
710
|
+
<div className="relative z-10 w-full h-full">
|
|
711
|
+
{slide.customMedia && safeMediaUrl ? (
|
|
712
|
+
<>
|
|
713
|
+
{slide.customMediaType === "image" && (
|
|
714
|
+
<div className="relative w-full h-full bg-dots">
|
|
715
|
+
<Image
|
|
716
|
+
src={safeMediaUrl}
|
|
717
|
+
alt={mediaAlt}
|
|
718
|
+
fill
|
|
719
|
+
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
720
|
+
className="object-cover"
|
|
721
|
+
priority={index === 0}
|
|
722
|
+
quality={90}
|
|
723
|
+
/>
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
726
|
+
{slide.customMediaType === "video" && (
|
|
727
|
+
<video
|
|
728
|
+
src={safeMediaUrl}
|
|
729
|
+
autoPlay
|
|
730
|
+
loop
|
|
731
|
+
muted
|
|
732
|
+
playsInline
|
|
733
|
+
preload="metadata"
|
|
734
|
+
className="w-full h-full object-cover bg-dots"
|
|
735
|
+
aria-label={mediaAlt || "Video content"}
|
|
736
|
+
/>
|
|
737
|
+
)}
|
|
738
|
+
{slide.customMediaType === "lottie" && (
|
|
739
|
+
<DotLottieReact
|
|
740
|
+
src={safeMediaUrl}
|
|
741
|
+
loop
|
|
742
|
+
autoplay
|
|
743
|
+
className="w-full h-full object-cover bg-dots"
|
|
744
|
+
/>
|
|
745
|
+
)}
|
|
746
|
+
</>
|
|
747
|
+
) : (
|
|
748
|
+
<Visual accent={slide.accent ?? "lime"} />
|
|
749
|
+
)}
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ── Main component ─────────────────────────────────────────────── */
|
|
758
|
+
|
|
759
|
+
export function DDHorizontalScroll(props: DDHorizontalScrollProps) {
|
|
760
|
+
const { sectionLabel, slides, sectionId, enableCustomCSS, customCSS } = props;
|
|
761
|
+
|
|
762
|
+
const outerRef = useRef<HTMLElement>(null);
|
|
763
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
764
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
765
|
+
const slideRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
766
|
+
const activeSlideRef = useRef<number>(-1);
|
|
767
|
+
|
|
768
|
+
// Memoize normalized slides to avoid unnecessary re-renders
|
|
769
|
+
const normalSlides: SlideItem[] = useMemo(() => slides ?? [], [slides]);
|
|
770
|
+
const slideCount = normalSlides.length;
|
|
771
|
+
|
|
772
|
+
/* ── GSAP horizontal scroll + pin + snap ── */
|
|
773
|
+
useLayoutEffect(() => {
|
|
774
|
+
if (typeof window === "undefined") return;
|
|
775
|
+
if (slideCount === 0) return;
|
|
776
|
+
|
|
777
|
+
// Store ctx outside the timer so cleanup always reaches it
|
|
778
|
+
let ctx: ReturnType<typeof gsap.context> | null = null;
|
|
779
|
+
let initTimer: ReturnType<typeof setTimeout>;
|
|
780
|
+
let resizeTimer: ReturnType<typeof setTimeout>;
|
|
781
|
+
let rafId: number;
|
|
782
|
+
|
|
783
|
+
const init = () => {
|
|
784
|
+
const outerEl = outerRef.current;
|
|
785
|
+
const wrapper = wrapperRef.current;
|
|
786
|
+
const track = trackRef.current;
|
|
787
|
+
if (!outerEl || !wrapper || !track) return;
|
|
788
|
+
|
|
789
|
+
// Kill any stale ScrollTriggers on this element (e.g. from Strict-Mode double mount)
|
|
790
|
+
ScrollTrigger.getAll()
|
|
791
|
+
.filter((st) => st.vars.trigger === outerEl)
|
|
792
|
+
.forEach((st) => st.kill());
|
|
793
|
+
|
|
794
|
+
ctx = gsap.context(() => {
|
|
795
|
+
ScrollTrigger.create({
|
|
796
|
+
trigger: outerEl,
|
|
797
|
+
start: "top top",
|
|
798
|
+
end: () => `+=${(slideCount - 1) * window.innerWidth}`,
|
|
799
|
+
scrub: 0.5,
|
|
800
|
+
invalidateOnRefresh: true,
|
|
801
|
+
onUpdate: (self) => {
|
|
802
|
+
gsap.set(track, {
|
|
803
|
+
x: -(slideCount - 1) * window.innerWidth * self.progress,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const newActive = Math.round(self.progress * (slideCount - 1));
|
|
807
|
+
if (newActive !== activeSlideRef.current) {
|
|
808
|
+
activeSlideRef.current = newActive;
|
|
809
|
+
animateSlideIn(slideRefs.current[newActive]);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}, outerEl);
|
|
814
|
+
|
|
815
|
+
// Animate first slide in on mount
|
|
816
|
+
animateSlideIn(slideRefs.current[1]);
|
|
817
|
+
activeSlideRef.current = 1;
|
|
818
|
+
|
|
819
|
+
// Refresh so ScrollTrigger measures the fully-painted layout
|
|
820
|
+
ScrollTrigger.refresh();
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// Double rAF inside a timeout: waits for SSR hydration, LocomotiveScroll init
|
|
824
|
+
// and web-font paint before measuring. 200 ms covers the loco 100 ms delay too.
|
|
825
|
+
initTimer = setTimeout(() => {
|
|
826
|
+
rafId = requestAnimationFrame(() => {
|
|
827
|
+
requestAnimationFrame(init);
|
|
828
|
+
});
|
|
829
|
+
}, 200);
|
|
830
|
+
|
|
831
|
+
// Debounced resize — recalculates pin length and slide positions
|
|
832
|
+
const onResize = () => {
|
|
833
|
+
clearTimeout(resizeTimer);
|
|
834
|
+
resizeTimer = setTimeout(() => {
|
|
835
|
+
ScrollTrigger.refresh(true);
|
|
836
|
+
}, 150);
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
window.addEventListener("resize", onResize);
|
|
840
|
+
// Also respond to locomotive:resize dispatched by PageClient
|
|
841
|
+
window.addEventListener("locomotive:resize", onResize);
|
|
842
|
+
|
|
843
|
+
return () => {
|
|
844
|
+
clearTimeout(initTimer);
|
|
845
|
+
clearTimeout(resizeTimer);
|
|
846
|
+
cancelAnimationFrame(rafId);
|
|
847
|
+
window.removeEventListener("resize", onResize);
|
|
848
|
+
window.removeEventListener("locomotive:resize", onResize);
|
|
849
|
+
ctx?.revert();
|
|
850
|
+
ctx = null;
|
|
851
|
+
};
|
|
852
|
+
}, [slideCount, sectionId]);
|
|
853
|
+
|
|
854
|
+
// offset next block
|
|
855
|
+
const getOffset = () => {
|
|
856
|
+
if (typeof window === "undefined") return 25;
|
|
857
|
+
|
|
858
|
+
if (window.matchMedia("(max-width: 420px)").matches) return 550;
|
|
859
|
+
if (window.matchMedia("(max-width: 768px)").matches) return 300;
|
|
860
|
+
|
|
861
|
+
return 25;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const offset = getOffset();
|
|
865
|
+
return (
|
|
866
|
+
<section
|
|
867
|
+
ref={outerRef}
|
|
868
|
+
id={sectionId ?? undefined}
|
|
869
|
+
className="studio-hscrollblock relative z-30 top-0"
|
|
870
|
+
// !!Note: First & Last child slides set to opacity 0 to rectify scroll. offset for next block entry. Use svh to offset and FirstSlide_width(!vw)
|
|
871
|
+
style={{
|
|
872
|
+
height: `calc((${Math.max(0, slideCount + 2)} * 100svh + 100svh) - ${offset}svh)`,
|
|
873
|
+
}}
|
|
874
|
+
>
|
|
875
|
+
<div
|
|
876
|
+
ref={wrapperRef}
|
|
877
|
+
className="shs-wrapper sticky overflow-hidden top-0"
|
|
878
|
+
style={{
|
|
879
|
+
height: "100svh",
|
|
880
|
+
}}
|
|
881
|
+
>
|
|
882
|
+
{/* Custom CSS */}
|
|
883
|
+
{enableCustomCSS && customCSS && (
|
|
884
|
+
<style
|
|
885
|
+
dangerouslySetInnerHTML={{
|
|
886
|
+
__html: sectionId
|
|
887
|
+
? customCSS.replace(/:host/g, `#${sectionId}`)
|
|
888
|
+
: customCSS.replace(/:host/g, ".shs-wrapper"),
|
|
889
|
+
}}
|
|
890
|
+
/>
|
|
891
|
+
)}
|
|
892
|
+
|
|
893
|
+
{/* Subtle grid overlay */}
|
|
894
|
+
{/* <div
|
|
895
|
+
className="absolute inset-0 pointer-events-none z-0"
|
|
896
|
+
style={{
|
|
897
|
+
backgroundImage: `
|
|
898
|
+
linear-gradient(rgba(132,204,22,0.02) 1px, transparent 1px),
|
|
899
|
+
linear-gradient(90deg, rgba(132,204,22,0.02) 1px, transparent 1px)
|
|
900
|
+
`,
|
|
901
|
+
backgroundSize: '60px 60px'
|
|
902
|
+
}}
|
|
903
|
+
/> */}
|
|
904
|
+
|
|
905
|
+
{/* Section eyebrow label */}
|
|
906
|
+
{sectionLabel && (
|
|
907
|
+
<div className="section-label absolute top-16 left-4 z-20 flex items-center gap-3">
|
|
908
|
+
<div className="w-6 h-px" style={{ background: "#84cc16" }} />
|
|
909
|
+
<span
|
|
910
|
+
className="text-[11.5px] font-semibold tracking-wide uppercase"
|
|
911
|
+
style={{
|
|
912
|
+
color: "#84cc16",
|
|
913
|
+
}}
|
|
914
|
+
>
|
|
915
|
+
{sectionLabel}
|
|
916
|
+
</span>
|
|
917
|
+
</div>
|
|
918
|
+
)}
|
|
919
|
+
|
|
920
|
+
{/* Slide dot counter */}
|
|
921
|
+
<div className="slide-counter absolute top-16 right-6 z-20 flex items-center gap-2">
|
|
922
|
+
{normalSlides.map((_, i) => (
|
|
923
|
+
<div
|
|
924
|
+
key={i}
|
|
925
|
+
className="w-1.5 h-1.5 rounded-full hidden"
|
|
926
|
+
style={{ background: "rgba(255,255,255,0.2)" }}
|
|
927
|
+
/>
|
|
928
|
+
))}
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
{/* Horizontal track */}
|
|
932
|
+
<div
|
|
933
|
+
ref={trackRef}
|
|
934
|
+
className="absolute top-0 left-0 flex h-full"
|
|
935
|
+
style={{ willChange: "transform" }}
|
|
936
|
+
>
|
|
937
|
+
{normalSlides.map((slide, i) => (
|
|
938
|
+
<SlidePanel
|
|
939
|
+
key={slide.id ?? i}
|
|
940
|
+
slide={slide}
|
|
941
|
+
index={i}
|
|
942
|
+
total={normalSlides.length - 1}
|
|
943
|
+
panelRef={(el) => {
|
|
944
|
+
slideRefs.current[i] = el;
|
|
945
|
+
}}
|
|
946
|
+
/>
|
|
947
|
+
))}
|
|
948
|
+
</div>
|
|
949
|
+
|
|
950
|
+
{/* Scroll hint */}
|
|
951
|
+
{/* <div
|
|
952
|
+
className="scroll-hint absolute bottom-16 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-2"
|
|
953
|
+
style={{ color: 'rgba(255,255,255,0.2)' }}
|
|
954
|
+
>
|
|
955
|
+
<span
|
|
956
|
+
className="text-xs tracking-widest uppercase"
|
|
957
|
+
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
958
|
+
>
|
|
959
|
+
Scroll to explore
|
|
960
|
+
</span>
|
|
961
|
+
<div className="flex gap-1">
|
|
962
|
+
{[0, 1, 2].map((d) => (
|
|
963
|
+
<div
|
|
964
|
+
key={d}
|
|
965
|
+
className="w-1 h-1 rounded-full animate-bounce"
|
|
966
|
+
style={{ background: 'rgba(255,255,255,0.3)', animationDelay: `${d * 0.15}s` }}
|
|
967
|
+
/>
|
|
968
|
+
))}
|
|
969
|
+
</div>
|
|
970
|
+
</div> */}
|
|
971
|
+
</div>
|
|
972
|
+
</section>
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* ── Entrance animation helper ──────────────────────────────────── */
|
|
977
|
+
|
|
978
|
+
function animateSlideIn(el: HTMLDivElement | null) {
|
|
979
|
+
if (!el) return;
|
|
980
|
+
const animEls = el.querySelectorAll<HTMLElement>(".anim-el");
|
|
981
|
+
if (!animEls.length) return;
|
|
982
|
+
gsap.fromTo(
|
|
983
|
+
animEls,
|
|
984
|
+
{ opacity: 1, y: 0 },
|
|
985
|
+
{
|
|
986
|
+
opacity: 1,
|
|
987
|
+
y: 0,
|
|
988
|
+
stagger: 0.07,
|
|
989
|
+
duration: 0.55,
|
|
990
|
+
ease: "power4.out",
|
|
991
|
+
overwrite: true,
|
|
992
|
+
clearProps: "transform",
|
|
993
|
+
},
|
|
994
|
+
);
|
|
995
|
+
}
|