nextworks 0.2.0-alpha.16 → 0.2.0-alpha.18
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/README.md +0 -4
- package/dist/cli_manifests/blocks_manifest.json +10 -2
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +3 -0
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +11 -1
- package/dist/kits/blocks/components/sections/CommandShowcase.tsx +517 -0
- package/dist/kits/blocks/components/sections/FeaturedProjectShowcase.tsx +687 -0
- package/dist/kits/blocks/components/sections/HeroWithVideo.tsx +495 -0
- package/dist/kits/blocks/components/sections/ProjectDeepDive.tsx +805 -0
- package/dist/kits/blocks/components/sections/SelectedWorkRail.tsx +485 -0
- package/dist/kits/blocks/package-deps.json +3 -3
- package/dist/kits/blocks/tsconfig.json +0 -2
- package/dist/kits/blocks/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
type ButtonVariant =
|
|
8
|
+
| "default"
|
|
9
|
+
| "destructive"
|
|
10
|
+
| "outline"
|
|
11
|
+
| "secondary"
|
|
12
|
+
| "ghost"
|
|
13
|
+
| "link";
|
|
14
|
+
|
|
15
|
+
type ButtonSize = "default" | "sm" | "lg" | "icon";
|
|
16
|
+
|
|
17
|
+
export type ProjectDeepDiveSectionKey =
|
|
18
|
+
| "problem"
|
|
19
|
+
| "solution"
|
|
20
|
+
| "highlights"
|
|
21
|
+
| "result"
|
|
22
|
+
| "architecture";
|
|
23
|
+
|
|
24
|
+
export interface ProjectDeepDiveCta {
|
|
25
|
+
label?: string;
|
|
26
|
+
href?: string;
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
variant?: ButtonVariant;
|
|
29
|
+
size?: ButtonSize;
|
|
30
|
+
className?: string;
|
|
31
|
+
unstyled?: boolean;
|
|
32
|
+
style?: React.CSSProperties;
|
|
33
|
+
target?: string;
|
|
34
|
+
rel?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProjectDeepDiveItem {
|
|
38
|
+
title?: React.ReactNode;
|
|
39
|
+
description: React.ReactNode;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProjectDeepDiveSectionConfig {
|
|
44
|
+
label?: React.ReactNode;
|
|
45
|
+
title?: React.ReactNode;
|
|
46
|
+
description?: React.ReactNode;
|
|
47
|
+
items?: Array<string | ProjectDeepDiveItem>;
|
|
48
|
+
ariaLabel?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ProjectDeepDiveMediaConfig {
|
|
53
|
+
type?: "image" | "video" | "terminal" | "custom";
|
|
54
|
+
src?: string;
|
|
55
|
+
alt?: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
ariaLabel?: string;
|
|
58
|
+
poster?: string;
|
|
59
|
+
autoPlay?: boolean;
|
|
60
|
+
muted?: boolean;
|
|
61
|
+
loop?: boolean;
|
|
62
|
+
playsInline?: boolean;
|
|
63
|
+
controls?: boolean;
|
|
64
|
+
preload?: "none" | "metadata" | "auto";
|
|
65
|
+
commands?: string[];
|
|
66
|
+
output?: string[];
|
|
67
|
+
caption?: React.ReactNode;
|
|
68
|
+
content?: React.ReactNode;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ProjectDeepDiveClassNames {
|
|
73
|
+
section?: string;
|
|
74
|
+
backgroundGlow?: string;
|
|
75
|
+
container?: string;
|
|
76
|
+
header?: string;
|
|
77
|
+
eyebrow?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
cardsGrid?: string;
|
|
81
|
+
card?: string;
|
|
82
|
+
cardLabel?: string;
|
|
83
|
+
cardTitle?: string;
|
|
84
|
+
cardDescription?: string;
|
|
85
|
+
itemList?: string;
|
|
86
|
+
item?: string;
|
|
87
|
+
itemMarker?: string;
|
|
88
|
+
itemTitle?: string;
|
|
89
|
+
itemDescription?: string;
|
|
90
|
+
supportGrid?: string;
|
|
91
|
+
architectureCard?: string;
|
|
92
|
+
architectureLabel?: string;
|
|
93
|
+
architectureTitle?: string;
|
|
94
|
+
architectureDescription?: string;
|
|
95
|
+
architectureList?: string;
|
|
96
|
+
architectureItem?: string;
|
|
97
|
+
architectureItemMarker?: string;
|
|
98
|
+
architectureItemTitle?: string;
|
|
99
|
+
architectureItemDescription?: string;
|
|
100
|
+
mediaOuter?: string;
|
|
101
|
+
mediaFrame?: string;
|
|
102
|
+
mediaChrome?: string;
|
|
103
|
+
mediaContent?: string;
|
|
104
|
+
mediaCaption?: string;
|
|
105
|
+
terminal?: string;
|
|
106
|
+
terminalLine?: string;
|
|
107
|
+
terminalOutput?: string;
|
|
108
|
+
buttons?: string;
|
|
109
|
+
cta?: string;
|
|
110
|
+
image?: string;
|
|
111
|
+
video?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ProjectDeepDiveProps {
|
|
115
|
+
id?: string;
|
|
116
|
+
className?: string;
|
|
117
|
+
eyebrow?: React.ReactNode;
|
|
118
|
+
title?: React.ReactNode;
|
|
119
|
+
description?: React.ReactNode;
|
|
120
|
+
sections?: Partial<Record<ProjectDeepDiveSectionKey, ProjectDeepDiveSectionConfig>>;
|
|
121
|
+
problem?: ProjectDeepDiveSectionConfig;
|
|
122
|
+
solution?: ProjectDeepDiveSectionConfig;
|
|
123
|
+
highlights?: ProjectDeepDiveSectionConfig;
|
|
124
|
+
technicalHighlights?: ProjectDeepDiveSectionConfig;
|
|
125
|
+
result?: ProjectDeepDiveSectionConfig;
|
|
126
|
+
architecture?: ProjectDeepDiveSectionConfig;
|
|
127
|
+
architectureNotes?: ProjectDeepDiveSectionConfig;
|
|
128
|
+
media?: React.ReactNode | ProjectDeepDiveMediaConfig | false;
|
|
129
|
+
cta?: ProjectDeepDiveCta;
|
|
130
|
+
ariaLabel?: string;
|
|
131
|
+
titleId?: string;
|
|
132
|
+
enableMotion?: boolean;
|
|
133
|
+
classNames?: ProjectDeepDiveClassNames;
|
|
134
|
+
section?: { className?: string };
|
|
135
|
+
container?: { className?: string };
|
|
136
|
+
header?: { className?: string };
|
|
137
|
+
cards?: { className?: string };
|
|
138
|
+
mediaSlot?: { className?: string };
|
|
139
|
+
supportSlot?: { className?: string };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const defaultSections: Record<
|
|
143
|
+
Exclude<ProjectDeepDiveSectionKey, "architecture">,
|
|
144
|
+
ProjectDeepDiveSectionConfig
|
|
145
|
+
> = {
|
|
146
|
+
problem: {
|
|
147
|
+
label: "Problem",
|
|
148
|
+
title: "Important project details can be hard to scan.",
|
|
149
|
+
description:
|
|
150
|
+
"A normal card or screenshot often does not explain the context, constraints, and decisions behind the work.",
|
|
151
|
+
items: [
|
|
152
|
+
"Short summaries can miss the reason a project mattered.",
|
|
153
|
+
"Technical choices are easier to trust when they are shown clearly.",
|
|
154
|
+
],
|
|
155
|
+
ariaLabel: "Problem overview",
|
|
156
|
+
},
|
|
157
|
+
solution: {
|
|
158
|
+
label: "Solution",
|
|
159
|
+
title: "Tell the story with a structured case-study layout.",
|
|
160
|
+
description:
|
|
161
|
+
"Use clear sections for the problem, approach, technical highlights, and outcome so visitors can understand the work quickly.",
|
|
162
|
+
items: [
|
|
163
|
+
"Each block can be replaced with project-specific copy and evidence.",
|
|
164
|
+
"The layout works for products, services, internal tools, and client projects.",
|
|
165
|
+
],
|
|
166
|
+
ariaLabel: "Solution overview",
|
|
167
|
+
},
|
|
168
|
+
highlights: {
|
|
169
|
+
label: "Technical highlights",
|
|
170
|
+
title: "Show the decisions that shaped the build.",
|
|
171
|
+
description:
|
|
172
|
+
"Use this area for architecture notes, workflow details, integration choices, or implementation tradeoffs.",
|
|
173
|
+
items: [
|
|
174
|
+
"Clear problem and solution framing.",
|
|
175
|
+
"Room for implementation details and constraints.",
|
|
176
|
+
"Optional architecture notes or media preview.",
|
|
177
|
+
"Useful for product work, tools, client projects, and launches.",
|
|
178
|
+
],
|
|
179
|
+
ariaLabel: "Technical highlights overview",
|
|
180
|
+
},
|
|
181
|
+
result: {
|
|
182
|
+
label: "Result",
|
|
183
|
+
title: "Visitors get the context behind the finished work.",
|
|
184
|
+
description:
|
|
185
|
+
"The outcome is a concise project narrative that explains what was built, why it mattered, and how it came together.",
|
|
186
|
+
items: [
|
|
187
|
+
"Good for portfolios, launch pages, case studies, and product breakdowns.",
|
|
188
|
+
"Can stay high-level for clients or become more technical for developer audiences.",
|
|
189
|
+
],
|
|
190
|
+
ariaLabel: "Result overview",
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const defaultArchitecture: ProjectDeepDiveSectionConfig = {
|
|
195
|
+
label: "Architecture notes",
|
|
196
|
+
title: "Add a simple diagram or implementation summary.",
|
|
197
|
+
description:
|
|
198
|
+
"Use this optional block for system shape, data flow, component structure, delivery process, or other technical notes.",
|
|
199
|
+
items: [
|
|
200
|
+
"Input → process → output",
|
|
201
|
+
"Planning, design, implementation, testing, and launch can be summarized here",
|
|
202
|
+
],
|
|
203
|
+
ariaLabel: "Architecture notes",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const defaultMedia: ProjectDeepDiveMediaConfig = {
|
|
207
|
+
type: "terminal",
|
|
208
|
+
title: "Case study preview",
|
|
209
|
+
ariaLabel: "Terminal-style preview showing example project milestones",
|
|
210
|
+
commands: [
|
|
211
|
+
"define project goals",
|
|
212
|
+
"map constraints and tradeoffs",
|
|
213
|
+
"document implementation notes",
|
|
214
|
+
],
|
|
215
|
+
output: [
|
|
216
|
+
"✔ problem and audience clarified",
|
|
217
|
+
"✔ solution approach documented",
|
|
218
|
+
"✔ outcome and next steps summarized",
|
|
219
|
+
],
|
|
220
|
+
caption: "Use the media area for a screenshot, diagram, or terminal walkthrough.",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
function normalizeItem(item: string | ProjectDeepDiveItem): ProjectDeepDiveItem {
|
|
224
|
+
return typeof item === "string" ? { description: item } : item;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeSection(
|
|
228
|
+
value: ProjectDeepDiveSectionConfig | undefined,
|
|
229
|
+
defaults: ProjectDeepDiveSectionConfig,
|
|
230
|
+
): Required<ProjectDeepDiveSectionConfig> {
|
|
231
|
+
return {
|
|
232
|
+
label: value?.label ?? defaults.label ?? null,
|
|
233
|
+
title: value?.title ?? defaults.title ?? null,
|
|
234
|
+
description: value?.description ?? defaults.description ?? null,
|
|
235
|
+
items: value?.items ?? defaults.items ?? [],
|
|
236
|
+
ariaLabel: value?.ariaLabel ?? defaults.ariaLabel ?? "Project deep dive section card",
|
|
237
|
+
className: value?.className ?? "",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isMediaConfig(
|
|
242
|
+
media: ProjectDeepDiveProps["media"],
|
|
243
|
+
): media is ProjectDeepDiveMediaConfig {
|
|
244
|
+
return Boolean(
|
|
245
|
+
media &&
|
|
246
|
+
typeof media === "object" &&
|
|
247
|
+
!React.isValidElement(media) &&
|
|
248
|
+
("type" in media || "src" in media || "content" in media),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderCta(cta: ProjectDeepDiveCta | undefined) {
|
|
253
|
+
if (!cta?.label) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Button
|
|
259
|
+
asChild
|
|
260
|
+
variant={cta.variant}
|
|
261
|
+
size={cta.size}
|
|
262
|
+
className={cta.className}
|
|
263
|
+
unstyled={cta.unstyled}
|
|
264
|
+
style={cta.style}
|
|
265
|
+
>
|
|
266
|
+
<Link
|
|
267
|
+
href={cta.href || "#"}
|
|
268
|
+
aria-label={cta.ariaLabel ?? cta.label}
|
|
269
|
+
target={cta.target}
|
|
270
|
+
rel={cta.rel ?? (cta.target === "_blank" ? "noreferrer" : undefined)}
|
|
271
|
+
>
|
|
272
|
+
{cta.label}
|
|
273
|
+
</Link>
|
|
274
|
+
</Button>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderSectionCard(
|
|
279
|
+
section: Required<ProjectDeepDiveSectionConfig>,
|
|
280
|
+
classNames?: ProjectDeepDiveClassNames,
|
|
281
|
+
motionClassName?: string,
|
|
282
|
+
) {
|
|
283
|
+
const normalizedItems = section.items.map(normalizeItem);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<article
|
|
287
|
+
className={cn(
|
|
288
|
+
"rounded-[1.75rem] border border-border bg-card/80 p-5 shadow-[0_18px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:shadow-[0_18px_60px_rgba(0,0,0,0.34)] sm:p-6",
|
|
289
|
+
|
|
290
|
+
motionClassName,
|
|
291
|
+
classNames?.card,
|
|
292
|
+
section.className,
|
|
293
|
+
)}
|
|
294
|
+
aria-label={section.ariaLabel}
|
|
295
|
+
>
|
|
296
|
+
{section.label ? (
|
|
297
|
+
<p
|
|
298
|
+
className={cn(
|
|
299
|
+
"text-[0.7rem] font-medium uppercase tracking-[0.24em] text-muted-foreground",
|
|
300
|
+
classNames?.cardLabel,
|
|
301
|
+
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
{section.label}
|
|
305
|
+
</p>
|
|
306
|
+
) : null}
|
|
307
|
+
|
|
308
|
+
{section.title ? (
|
|
309
|
+
<h3
|
|
310
|
+
className={cn(
|
|
311
|
+
"mt-4 text-balance text-xl font-medium tracking-[-0.04em] text-foreground sm:text-2xl",
|
|
312
|
+
classNames?.cardTitle,
|
|
313
|
+
|
|
314
|
+
)}
|
|
315
|
+
>
|
|
316
|
+
{section.title}
|
|
317
|
+
</h3>
|
|
318
|
+
) : null}
|
|
319
|
+
|
|
320
|
+
{section.description ? (
|
|
321
|
+
<p
|
|
322
|
+
className={cn(
|
|
323
|
+
"mt-3 text-sm leading-6 text-muted-foreground sm:text-[0.95rem]",
|
|
324
|
+
classNames?.cardDescription,
|
|
325
|
+
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
{section.description}
|
|
329
|
+
</p>
|
|
330
|
+
) : null}
|
|
331
|
+
|
|
332
|
+
{normalizedItems.length > 0 ? (
|
|
333
|
+
<ul className={cn("mt-5 space-y-3", classNames?.itemList)}>
|
|
334
|
+
{normalizedItems.map((item, index) => (
|
|
335
|
+
<li
|
|
336
|
+
key={`${String(item.title ?? item.description)}-${index}`}
|
|
337
|
+
className={cn("flex gap-3", classNames?.item, item.className)}
|
|
338
|
+
>
|
|
339
|
+
<span
|
|
340
|
+
className={cn(
|
|
341
|
+
"mt-2 size-1.5 shrink-0 rounded-full bg-foreground/42",
|
|
342
|
+
classNames?.itemMarker,
|
|
343
|
+
|
|
344
|
+
)}
|
|
345
|
+
aria-hidden="true"
|
|
346
|
+
/>
|
|
347
|
+
<span>
|
|
348
|
+
{item.title ? (
|
|
349
|
+
<span
|
|
350
|
+
className={cn(
|
|
351
|
+
"block text-sm font-medium text-foreground",
|
|
352
|
+
classNames?.itemTitle,
|
|
353
|
+
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
{item.title}
|
|
357
|
+
</span>
|
|
358
|
+
) : null}
|
|
359
|
+
<span
|
|
360
|
+
className={cn(
|
|
361
|
+
"block text-sm leading-6 text-muted-foreground",
|
|
362
|
+
classNames?.itemDescription,
|
|
363
|
+
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{item.description}
|
|
367
|
+
</span>
|
|
368
|
+
</span>
|
|
369
|
+
</li>
|
|
370
|
+
))}
|
|
371
|
+
</ul>
|
|
372
|
+
) : null}
|
|
373
|
+
</article>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function renderMediaContent(
|
|
378
|
+
media: ProjectDeepDiveMediaConfig,
|
|
379
|
+
classNames?: ProjectDeepDiveClassNames,
|
|
380
|
+
) {
|
|
381
|
+
if (media.content) {
|
|
382
|
+
return media.content;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (media.type === "image" && media.src) {
|
|
386
|
+
return (
|
|
387
|
+
<div
|
|
388
|
+
className={cn(
|
|
389
|
+
"relative min-h-[24rem] overflow-hidden",
|
|
390
|
+
media.className,
|
|
391
|
+
classNames?.image,
|
|
392
|
+
)}
|
|
393
|
+
>
|
|
394
|
+
<Image
|
|
395
|
+
src={media.src}
|
|
396
|
+
alt={media.alt ?? media.title ?? "Project deep dive preview"}
|
|
397
|
+
fill
|
|
398
|
+
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
399
|
+
className="object-cover"
|
|
400
|
+
unoptimized
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (media.type === "video" && media.src) {
|
|
407
|
+
return (
|
|
408
|
+
<video
|
|
409
|
+
src={media.src}
|
|
410
|
+
poster={media.poster}
|
|
411
|
+
title={media.title ?? media.ariaLabel ?? "Project deep dive video"}
|
|
412
|
+
aria-label={media.ariaLabel ?? media.title ?? "Project deep dive video"}
|
|
413
|
+
className={cn("size-full object-cover", media.className, classNames?.video)}
|
|
414
|
+
autoPlay={media.autoPlay}
|
|
415
|
+
muted={media.muted ?? media.autoPlay ?? false}
|
|
416
|
+
loop={media.loop}
|
|
417
|
+
playsInline={media.playsInline ?? true}
|
|
418
|
+
controls={media.controls ?? true}
|
|
419
|
+
preload={media.preload ?? "metadata"}
|
|
420
|
+
/>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div
|
|
426
|
+
className={cn(
|
|
427
|
+
"flex min-h-[24rem] flex-col justify-between bg-muted p-5 font-mono text-xs text-muted-foreground sm:p-6",
|
|
428
|
+
|
|
429
|
+
media.className,
|
|
430
|
+
classNames?.terminal,
|
|
431
|
+
)}
|
|
432
|
+
role="img"
|
|
433
|
+
aria-label={media.ariaLabel ?? media.title ?? "Technical preview"}
|
|
434
|
+
>
|
|
435
|
+
<div className="space-y-3">
|
|
436
|
+
{(media.commands ?? defaultMedia.commands ?? []).map((command) => (
|
|
437
|
+
<p key={command} className={cn("break-all", classNames?.terminalLine)}>
|
|
438
|
+
<span className="mr-2 text-muted-foreground/60" aria-hidden="true">
|
|
439
|
+
|
|
440
|
+
$
|
|
441
|
+
</span>
|
|
442
|
+
<span>{command}</span>
|
|
443
|
+
</p>
|
|
444
|
+
))}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div className="mt-8 space-y-2 border-t border-border pt-5">
|
|
448
|
+
|
|
449
|
+
{(media.output ?? defaultMedia.output ?? []).map((line) => (
|
|
450
|
+
<p key={line} className={cn("text-muted-foreground", classNames?.terminalOutput)}>
|
|
451
|
+
|
|
452
|
+
{line}
|
|
453
|
+
</p>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderMedia(
|
|
461
|
+
media: ProjectDeepDiveProps["media"],
|
|
462
|
+
classNames?: ProjectDeepDiveClassNames,
|
|
463
|
+
) {
|
|
464
|
+
if (media === false) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (media !== undefined && !isMediaConfig(media)) {
|
|
469
|
+
return media;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const resolvedMedia = isMediaConfig(media) ? media : defaultMedia;
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<>
|
|
476
|
+
{renderMediaContent(resolvedMedia, classNames)}
|
|
477
|
+
{resolvedMedia.caption ? (
|
|
478
|
+
<p className={cn("px-5 py-4 text-sm text-muted-foreground", classNames?.mediaCaption)}>
|
|
479
|
+
|
|
480
|
+
{resolvedMedia.caption}
|
|
481
|
+
</p>
|
|
482
|
+
) : null}
|
|
483
|
+
</>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function ProjectDeepDive({
|
|
488
|
+
id,
|
|
489
|
+
className,
|
|
490
|
+
eyebrow = "Project deep dive",
|
|
491
|
+
title = "Explain the problem, the approach, and the outcome in one place.",
|
|
492
|
+
description =
|
|
493
|
+
"Use this section to show how a project was thought through, what was built, and which technical decisions mattered.",
|
|
494
|
+
sections,
|
|
495
|
+
problem,
|
|
496
|
+
solution,
|
|
497
|
+
highlights,
|
|
498
|
+
technicalHighlights,
|
|
499
|
+
result,
|
|
500
|
+
architecture,
|
|
501
|
+
architectureNotes,
|
|
502
|
+
media,
|
|
503
|
+
cta,
|
|
504
|
+
ariaLabel = "Project deep dive section",
|
|
505
|
+
titleId,
|
|
506
|
+
enableMotion = true,
|
|
507
|
+
classNames,
|
|
508
|
+
section,
|
|
509
|
+
container,
|
|
510
|
+
header,
|
|
511
|
+
cards,
|
|
512
|
+
mediaSlot,
|
|
513
|
+
supportSlot,
|
|
514
|
+
}: ProjectDeepDiveProps) {
|
|
515
|
+
const resolvedProblem = normalizeSection(
|
|
516
|
+
problem ?? sections?.problem,
|
|
517
|
+
defaultSections.problem,
|
|
518
|
+
);
|
|
519
|
+
const resolvedSolution = normalizeSection(
|
|
520
|
+
solution ?? sections?.solution,
|
|
521
|
+
defaultSections.solution,
|
|
522
|
+
);
|
|
523
|
+
const resolvedHighlights = normalizeSection(
|
|
524
|
+
technicalHighlights ?? highlights ?? sections?.highlights,
|
|
525
|
+
defaultSections.highlights,
|
|
526
|
+
);
|
|
527
|
+
const resolvedResult = normalizeSection(
|
|
528
|
+
result ?? sections?.result,
|
|
529
|
+
defaultSections.result,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const architectureInput =
|
|
533
|
+
architectureNotes ?? architecture ?? sections?.architecture;
|
|
534
|
+
const resolvedArchitecture = architectureInput
|
|
535
|
+
? normalizeSection(architectureInput, defaultArchitecture)
|
|
536
|
+
: null;
|
|
537
|
+
|
|
538
|
+
const mediaContent = renderMedia(media, classNames);
|
|
539
|
+
const hasSupportContent = Boolean(mediaContent || resolvedArchitecture || cta?.label);
|
|
540
|
+
|
|
541
|
+
const motionClasses = enableMotion
|
|
542
|
+
? "transition-all duration-300 hover:-translate-y-1"
|
|
543
|
+
: "transition-none hover:!translate-y-0";
|
|
544
|
+
const cardMotionClasses = enableMotion
|
|
545
|
+
? "transition-transform duration-300 hover:-translate-y-1"
|
|
546
|
+
: "transition-none hover:!translate-y-0";
|
|
547
|
+
|
|
548
|
+
const resolvedCta: ProjectDeepDiveCta | undefined = cta?.label
|
|
549
|
+
? {
|
|
550
|
+
href: "#",
|
|
551
|
+
variant: "outline",
|
|
552
|
+
size: "lg",
|
|
553
|
+
unstyled: true,
|
|
554
|
+
...cta,
|
|
555
|
+
className: cn(
|
|
556
|
+
"h-11 rounded-full border border-border bg-background/80 px-6 text-sm font-medium text-foreground hover:bg-muted",
|
|
557
|
+
|
|
558
|
+
enableMotion && "transition-all duration-200 hover:-translate-y-0.5",
|
|
559
|
+
!enableMotion && "transition-none hover:!translate-y-0",
|
|
560
|
+
classNames?.cta,
|
|
561
|
+
cta.className,
|
|
562
|
+
),
|
|
563
|
+
}
|
|
564
|
+
: undefined;
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<section
|
|
568
|
+
id={id}
|
|
569
|
+
className={cn(
|
|
570
|
+
"relative overflow-hidden bg-background px-4 py-20 text-foreground sm:px-6 lg:px-8 lg:py-24",
|
|
571
|
+
|
|
572
|
+
section?.className,
|
|
573
|
+
classNames?.section,
|
|
574
|
+
className,
|
|
575
|
+
)}
|
|
576
|
+
aria-label={ariaLabel}
|
|
577
|
+
aria-labelledby={titleId}
|
|
578
|
+
>
|
|
579
|
+
<div
|
|
580
|
+
className={cn(
|
|
581
|
+
"pointer-events-none absolute right-[-16rem] top-28 h-[34rem] w-[34rem] rounded-full bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.08),rgba(0,0,0,0.03)_38%,transparent_72%)] blur-3xl dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.12),rgba(255,255,255,0.04)_38%,transparent_72%)]",
|
|
582
|
+
|
|
583
|
+
classNames?.backgroundGlow,
|
|
584
|
+
)}
|
|
585
|
+
aria-hidden="true"
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
<div
|
|
589
|
+
className={cn(
|
|
590
|
+
"relative z-10 mx-auto max-w-7xl",
|
|
591
|
+
container?.className,
|
|
592
|
+
classNames?.container,
|
|
593
|
+
)}
|
|
594
|
+
>
|
|
595
|
+
<div
|
|
596
|
+
className={cn(
|
|
597
|
+
"mx-auto max-w-3xl text-center",
|
|
598
|
+
header?.className,
|
|
599
|
+
classNames?.header,
|
|
600
|
+
)}
|
|
601
|
+
>
|
|
602
|
+
{eyebrow ? (
|
|
603
|
+
<p
|
|
604
|
+
className={cn(
|
|
605
|
+
"text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground",
|
|
606
|
+
classNames?.eyebrow,
|
|
607
|
+
|
|
608
|
+
)}
|
|
609
|
+
>
|
|
610
|
+
{eyebrow}
|
|
611
|
+
</p>
|
|
612
|
+
) : null}
|
|
613
|
+
|
|
614
|
+
<h2
|
|
615
|
+
id={titleId}
|
|
616
|
+
className={cn(
|
|
617
|
+
"mt-5 text-balance text-4xl font-semibold tracking-[-0.055em] text-foreground sm:text-5xl lg:text-6xl",
|
|
618
|
+
classNames?.title,
|
|
619
|
+
|
|
620
|
+
)}
|
|
621
|
+
>
|
|
622
|
+
{title}
|
|
623
|
+
</h2>
|
|
624
|
+
|
|
625
|
+
{description ? (
|
|
626
|
+
<p
|
|
627
|
+
className={cn(
|
|
628
|
+
"mx-auto mt-5 max-w-2xl text-pretty text-base leading-7 text-muted-foreground sm:text-lg",
|
|
629
|
+
classNames?.description,
|
|
630
|
+
|
|
631
|
+
)}
|
|
632
|
+
>
|
|
633
|
+
{description}
|
|
634
|
+
</p>
|
|
635
|
+
) : null}
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<div
|
|
639
|
+
className={cn(
|
|
640
|
+
"mt-12 grid gap-5 md:grid-cols-2 xl:grid-cols-4",
|
|
641
|
+
cards?.className,
|
|
642
|
+
classNames?.cardsGrid,
|
|
643
|
+
)}
|
|
644
|
+
>
|
|
645
|
+
{renderSectionCard(resolvedProblem, classNames, cardMotionClasses)}
|
|
646
|
+
{renderSectionCard(resolvedSolution, classNames, cardMotionClasses)}
|
|
647
|
+
{renderSectionCard(resolvedHighlights, classNames, cardMotionClasses)}
|
|
648
|
+
{renderSectionCard(resolvedResult, classNames, cardMotionClasses)}
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
{hasSupportContent ? (
|
|
652
|
+
<div
|
|
653
|
+
className={cn(
|
|
654
|
+
"mt-8 grid gap-6 lg:grid-cols-[minmax(0,1.08fr)_minmax(0,0.92fr)]",
|
|
655
|
+
supportSlot?.className,
|
|
656
|
+
classNames?.supportGrid,
|
|
657
|
+
)}
|
|
658
|
+
>
|
|
659
|
+
{mediaContent ? (
|
|
660
|
+
<div
|
|
661
|
+
className={cn(
|
|
662
|
+
"relative overflow-hidden rounded-[2rem] border border-border bg-card/80 p-2 shadow-[0_24px_90px_rgba(0,0,0,0.12)] backdrop-blur dark:shadow-[0_24px_90px_rgba(0,0,0,0.58)]",
|
|
663
|
+
|
|
664
|
+
motionClasses,
|
|
665
|
+
mediaSlot?.className,
|
|
666
|
+
classNames?.mediaOuter,
|
|
667
|
+
)}
|
|
668
|
+
>
|
|
669
|
+
<div
|
|
670
|
+
className={cn(
|
|
671
|
+
"flex h-10 items-center gap-2 border-b border-border px-4",
|
|
672
|
+
classNames?.mediaChrome,
|
|
673
|
+
|
|
674
|
+
)}
|
|
675
|
+
aria-hidden="true"
|
|
676
|
+
>
|
|
677
|
+
<span className="size-2.5 rounded-full bg-foreground/24" />
|
|
678
|
+
<span className="size-2.5 rounded-full bg-foreground/16" />
|
|
679
|
+
<span className="size-2.5 rounded-full bg-foreground/10" />
|
|
680
|
+
<span className="ml-3 h-2 w-28 rounded-full bg-foreground/8" />
|
|
681
|
+
|
|
682
|
+
</div>
|
|
683
|
+
<div
|
|
684
|
+
className={cn(
|
|
685
|
+
"overflow-hidden rounded-[1.45rem] bg-muted",
|
|
686
|
+
classNames?.mediaFrame,
|
|
687
|
+
|
|
688
|
+
)}
|
|
689
|
+
>
|
|
690
|
+
<div className={classNames?.mediaContent}>{mediaContent}</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
) : null}
|
|
694
|
+
|
|
695
|
+
{resolvedArchitecture || resolvedCta ? (
|
|
696
|
+
<div className="flex flex-col gap-6">
|
|
697
|
+
{resolvedArchitecture ? (
|
|
698
|
+
<article
|
|
699
|
+
className={cn(
|
|
700
|
+
"rounded-[2rem] border border-border bg-card/80 p-6 shadow-[0_24px_90px_rgba(0,0,0,0.12)] backdrop-blur dark:shadow-[0_24px_90px_rgba(0,0,0,0.42)] sm:p-8",
|
|
701
|
+
|
|
702
|
+
cardMotionClasses,
|
|
703
|
+
classNames?.architectureCard,
|
|
704
|
+
)}
|
|
705
|
+
aria-label={resolvedArchitecture.ariaLabel}
|
|
706
|
+
>
|
|
707
|
+
{resolvedArchitecture.label ? (
|
|
708
|
+
<p
|
|
709
|
+
className={cn(
|
|
710
|
+
"text-[0.7rem] font-medium uppercase tracking-[0.24em] text-muted-foreground",
|
|
711
|
+
classNames?.architectureLabel,
|
|
712
|
+
|
|
713
|
+
)}
|
|
714
|
+
>
|
|
715
|
+
{resolvedArchitecture.label}
|
|
716
|
+
</p>
|
|
717
|
+
) : null}
|
|
718
|
+
|
|
719
|
+
{resolvedArchitecture.title ? (
|
|
720
|
+
<h3
|
|
721
|
+
className={cn(
|
|
722
|
+
"mt-4 text-balance text-2xl font-medium tracking-[-0.04em] text-foreground sm:text-[1.75rem]",
|
|
723
|
+
classNames?.architectureTitle,
|
|
724
|
+
|
|
725
|
+
)}
|
|
726
|
+
>
|
|
727
|
+
{resolvedArchitecture.title}
|
|
728
|
+
</h3>
|
|
729
|
+
) : null}
|
|
730
|
+
|
|
731
|
+
{resolvedArchitecture.description ? (
|
|
732
|
+
<p
|
|
733
|
+
className={cn(
|
|
734
|
+
"mt-3 text-sm leading-6 text-muted-foreground sm:text-[0.95rem]",
|
|
735
|
+
classNames?.architectureDescription,
|
|
736
|
+
|
|
737
|
+
)}
|
|
738
|
+
>
|
|
739
|
+
{resolvedArchitecture.description}
|
|
740
|
+
</p>
|
|
741
|
+
) : null}
|
|
742
|
+
|
|
743
|
+
{resolvedArchitecture.items.length > 0 ? (
|
|
744
|
+
<ul className={cn("mt-5 space-y-3", classNames?.architectureList)}>
|
|
745
|
+
{resolvedArchitecture.items.map((item, index) => {
|
|
746
|
+
const normalizedItem = normalizeItem(item);
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<li
|
|
750
|
+
key={`${String(normalizedItem.title ?? normalizedItem.description)}-${index}`}
|
|
751
|
+
className={cn(
|
|
752
|
+
"flex gap-3",
|
|
753
|
+
classNames?.architectureItem,
|
|
754
|
+
normalizedItem.className,
|
|
755
|
+
)}
|
|
756
|
+
>
|
|
757
|
+
<span
|
|
758
|
+
className={cn(
|
|
759
|
+
"mt-2 size-1.5 shrink-0 rounded-full bg-foreground/42",
|
|
760
|
+
classNames?.architectureItemMarker,
|
|
761
|
+
|
|
762
|
+
)}
|
|
763
|
+
aria-hidden="true"
|
|
764
|
+
/>
|
|
765
|
+
<span>
|
|
766
|
+
{normalizedItem.title ? (
|
|
767
|
+
<span
|
|
768
|
+
className={cn(
|
|
769
|
+
"block text-sm font-medium text-foreground",
|
|
770
|
+
classNames?.architectureItemTitle,
|
|
771
|
+
|
|
772
|
+
)}
|
|
773
|
+
>
|
|
774
|
+
{normalizedItem.title}
|
|
775
|
+
</span>
|
|
776
|
+
) : null}
|
|
777
|
+
<span
|
|
778
|
+
className={cn(
|
|
779
|
+
"block text-sm leading-6 text-muted-foreground",
|
|
780
|
+
classNames?.architectureItemDescription,
|
|
781
|
+
|
|
782
|
+
)}
|
|
783
|
+
>
|
|
784
|
+
{normalizedItem.description}
|
|
785
|
+
</span>
|
|
786
|
+
</span>
|
|
787
|
+
</li>
|
|
788
|
+
);
|
|
789
|
+
})}
|
|
790
|
+
</ul>
|
|
791
|
+
) : null}
|
|
792
|
+
</article>
|
|
793
|
+
) : null}
|
|
794
|
+
|
|
795
|
+
{resolvedCta ? (
|
|
796
|
+
<div className={cn("flex", classNames?.buttons)}>{renderCta(resolvedCta)}</div>
|
|
797
|
+
) : null}
|
|
798
|
+
</div>
|
|
799
|
+
) : null}
|
|
800
|
+
</div>
|
|
801
|
+
) : null}
|
|
802
|
+
</div>
|
|
803
|
+
</section>
|
|
804
|
+
);
|
|
805
|
+
}
|