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.
@@ -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
+ }