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,687 @@
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 interface FeaturedProjectShowcaseCta {
18
+ label?: string;
19
+ href?: string;
20
+ ariaLabel?: string;
21
+ variant?: ButtonVariant;
22
+ size?: ButtonSize;
23
+ className?: string;
24
+ unstyled?: boolean;
25
+ style?: React.CSSProperties;
26
+ target?: string;
27
+ rel?: string;
28
+ }
29
+
30
+ export interface FeaturedProjectShowcaseTag {
31
+ label: string;
32
+ className?: string;
33
+ }
34
+
35
+ export interface FeaturedProjectShowcaseFeature {
36
+ title?: string;
37
+ description: React.ReactNode;
38
+ className?: string;
39
+ }
40
+
41
+ export interface FeaturedProjectShowcaseMetaItem {
42
+ label: string;
43
+ value: React.ReactNode;
44
+ className?: string;
45
+ }
46
+
47
+ export interface FeaturedProjectShowcaseBreakdownItem {
48
+ label: string;
49
+ description: React.ReactNode;
50
+ className?: string;
51
+ }
52
+
53
+ export interface FeaturedProjectShowcaseMediaConfig {
54
+ type?: "image" | "video" | "terminal" | "custom";
55
+ src?: string;
56
+ alt?: string;
57
+ title?: string;
58
+ ariaLabel?: string;
59
+ poster?: string;
60
+ autoPlay?: boolean;
61
+ muted?: boolean;
62
+ loop?: boolean;
63
+ playsInline?: boolean;
64
+ controls?: boolean;
65
+ preload?: "none" | "metadata" | "auto";
66
+ commands?: string[];
67
+ output?: string[];
68
+ caption?: React.ReactNode;
69
+ className?: string;
70
+ content?: React.ReactNode;
71
+ }
72
+
73
+ export interface FeaturedProjectShowcaseClassNames {
74
+ section?: string;
75
+ backgroundGlow?: string;
76
+ container?: string;
77
+ contentGrid?: string;
78
+ content?: string;
79
+ eyebrow?: string;
80
+ title?: string;
81
+ description?: string;
82
+ tags?: string;
83
+ tag?: string;
84
+ features?: string;
85
+ feature?: string;
86
+ featureMarker?: string;
87
+ featureTitle?: string;
88
+ featureDescription?: string;
89
+ breakdown?: string;
90
+ breakdownItem?: string;
91
+ breakdownLabel?: string;
92
+ breakdownDescription?: string;
93
+ meta?: string;
94
+ metaItem?: string;
95
+ metaLabel?: string;
96
+ metaValue?: string;
97
+ buttons?: string;
98
+ primaryCta?: string;
99
+ secondaryCta?: 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
+ image?: string;
109
+ video?: string;
110
+ }
111
+
112
+ export interface FeaturedProjectShowcaseProps {
113
+ id?: string;
114
+ className?: string;
115
+ eyebrow?: React.ReactNode;
116
+ title?: React.ReactNode;
117
+ description?: React.ReactNode;
118
+ tags?: Array<string | FeaturedProjectShowcaseTag>;
119
+ features?: Array<string | FeaturedProjectShowcaseFeature>;
120
+ breakdown?: FeaturedProjectShowcaseBreakdownItem[];
121
+ meta?: FeaturedProjectShowcaseMetaItem[];
122
+ primaryCta?: FeaturedProjectShowcaseCta;
123
+ secondaryCta?: FeaturedProjectShowcaseCta;
124
+ media?: React.ReactNode | FeaturedProjectShowcaseMediaConfig | false;
125
+ mediaId?: string;
126
+ detailsId?: string;
127
+ ariaLabel?: string;
128
+ titleId?: string;
129
+ enableMotion?: boolean;
130
+ classNames?: FeaturedProjectShowcaseClassNames;
131
+ section?: { className?: string };
132
+ container?: { className?: string };
133
+ content?: { className?: string };
134
+ mediaSlot?: { className?: string };
135
+ }
136
+
137
+ const defaultTags = [
138
+ "Product",
139
+ "Frontend",
140
+ "Launch",
141
+ "Case study",
142
+ "Design system",
143
+ "Workflow",
144
+ ];
145
+
146
+ const defaultFeatures: FeaturedProjectShowcaseFeature[] = [
147
+ { description: "Clear project positioning with room for technical details." },
148
+ { description: "Flexible media area for screenshots, video, terminal output, or custom content." },
149
+ { description: "Optional tags, metadata, and calls to action." },
150
+ { description: "Works for products, tools, portfolios, and case studies." },
151
+ ];
152
+
153
+ const defaultBreakdown: FeaturedProjectShowcaseBreakdownItem[] = [
154
+ {
155
+ label: "What it is",
156
+ description: "A focused project highlight with enough context to understand the work.",
157
+ },
158
+ {
159
+ label: "What it shows",
160
+ description: "Key features, implementation notes, supporting details, and proof points.",
161
+ },
162
+ {
163
+ label: "How to use it",
164
+ description: "Wrap it with project-specific copy, links, media, and metadata.",
165
+ },
166
+ ];
167
+
168
+ const defaultMeta: FeaturedProjectShowcaseMetaItem[] = [
169
+ { label: "Type", value: "Featured project" },
170
+ { label: "Focus", value: "Product story" },
171
+ { label: "Status", value: "Demo content" },
172
+ ];
173
+
174
+ const defaultMedia: FeaturedProjectShowcaseMediaConfig = {
175
+ type: "terminal",
176
+ title: "Project preview",
177
+ ariaLabel: "Terminal-style preview showing example project notes",
178
+ commands: [
179
+ "open project-brief.md",
180
+ "review design-system-notes.md",
181
+ "ship polished-preview",
182
+ ],
183
+ output: [
184
+ "✔ summary, tags, and project metadata ready",
185
+ "✔ feature bullets and implementation notes added",
186
+ "✔ media area available for screenshots or video",
187
+ ],
188
+ caption: "Use the media area for a screenshot, video, terminal preview, or custom visual.",
189
+ };
190
+
191
+ function isMediaConfig(
192
+ media: FeaturedProjectShowcaseProps["media"],
193
+ ): media is FeaturedProjectShowcaseMediaConfig {
194
+ return Boolean(
195
+ media &&
196
+ typeof media === "object" &&
197
+ !React.isValidElement(media) &&
198
+ ("type" in media || "src" in media || "content" in media),
199
+ );
200
+ }
201
+
202
+ function normalizeTag(tag: string | FeaturedProjectShowcaseTag) {
203
+ return typeof tag === "string" ? { label: tag } : tag;
204
+ }
205
+
206
+ function normalizeFeature(feature: string | FeaturedProjectShowcaseFeature) {
207
+ return typeof feature === "string" ? { description: feature } : feature;
208
+ }
209
+
210
+ function renderCta(cta: FeaturedProjectShowcaseCta | undefined) {
211
+ if (!cta?.label) {
212
+ return null;
213
+ }
214
+
215
+ return (
216
+ <Button
217
+ asChild
218
+ variant={cta.variant}
219
+ size={cta.size}
220
+ className={cta.className}
221
+ unstyled={cta.unstyled}
222
+ style={cta.style}
223
+ >
224
+ <Link
225
+ href={cta.href || "#"}
226
+ aria-label={cta.ariaLabel ?? cta.label}
227
+ target={cta.target}
228
+ rel={cta.rel ?? (cta.target === "_blank" ? "noreferrer" : undefined)}
229
+ >
230
+ {cta.label}
231
+ </Link>
232
+ </Button>
233
+ );
234
+ }
235
+
236
+ function renderMediaContent(
237
+ media: FeaturedProjectShowcaseMediaConfig,
238
+ classNames?: FeaturedProjectShowcaseClassNames,
239
+ ) {
240
+ if (media.content) {
241
+ return media.content;
242
+ }
243
+
244
+ if (media.type === "image" && media.src) {
245
+ return (
246
+ <div
247
+ className={cn(
248
+ "relative min-h-[24rem] overflow-hidden",
249
+ media.className,
250
+ classNames?.image,
251
+ )}
252
+ >
253
+ <Image
254
+ src={media.src}
255
+ alt={media.alt ?? media.title ?? "Featured project preview"}
256
+ fill
257
+ sizes="(max-width: 1024px) 100vw, 50vw"
258
+ className="object-cover"
259
+ unoptimized
260
+ />
261
+ </div>
262
+ );
263
+ }
264
+
265
+ if (media.type === "video" && media.src) {
266
+ return (
267
+ <video
268
+ src={media.src}
269
+ poster={media.poster}
270
+ title={media.title ?? media.ariaLabel ?? "Featured project video"}
271
+ aria-label={media.ariaLabel ?? media.title ?? "Featured project video"}
272
+ className={cn("size-full object-cover", media.className, classNames?.video)}
273
+ autoPlay={media.autoPlay}
274
+ muted={media.muted ?? media.autoPlay ?? false}
275
+ loop={media.loop}
276
+ playsInline={media.playsInline ?? true}
277
+ controls={media.controls ?? true}
278
+ preload={media.preload ?? "metadata"}
279
+ />
280
+ );
281
+ }
282
+
283
+ return (
284
+ <div
285
+ className={cn(
286
+ "flex min-h-[24rem] flex-col justify-between bg-muted p-5 font-mono text-xs text-muted-foreground sm:p-6",
287
+
288
+ media.className,
289
+ classNames?.terminal,
290
+ )}
291
+ role="img"
292
+ aria-label={media.ariaLabel ?? media.title ?? "Terminal preview"}
293
+ >
294
+ <div className="space-y-3">
295
+ {(media.commands ?? defaultMedia.commands ?? []).map((command) => (
296
+ <p key={command} className={cn("break-all", classNames?.terminalLine)}>
297
+ <span className="mr-2 text-muted-foreground/60" aria-hidden="true">
298
+
299
+ $
300
+ </span>
301
+ <span>{command}</span>
302
+ </p>
303
+ ))}
304
+ </div>
305
+
306
+ <div className="mt-8 space-y-2 border-t border-border pt-5">
307
+
308
+ {(media.output ?? defaultMedia.output ?? []).map((line) => (
309
+ <p key={line} className={cn("text-muted-foreground", classNames?.terminalOutput)}>
310
+
311
+ {line}
312
+ </p>
313
+ ))}
314
+ </div>
315
+ </div>
316
+ );
317
+ }
318
+
319
+ function renderMedia(
320
+ media: FeaturedProjectShowcaseProps["media"],
321
+ classNames?: FeaturedProjectShowcaseClassNames,
322
+ ) {
323
+ if (media === false) {
324
+ return null;
325
+ }
326
+
327
+ if (media !== undefined && !isMediaConfig(media)) {
328
+ return media;
329
+ }
330
+
331
+ const resolvedMedia = isMediaConfig(media) ? media : defaultMedia;
332
+
333
+ return (
334
+ <>
335
+ {renderMediaContent(resolvedMedia, classNames)}
336
+ {resolvedMedia.caption ? (
337
+ <p className={cn("px-5 py-4 text-sm text-muted-foreground", classNames?.mediaCaption)}>
338
+
339
+ {resolvedMedia.caption}
340
+ </p>
341
+ ) : null}
342
+ </>
343
+ );
344
+ }
345
+
346
+ export function FeaturedProjectShowcase({
347
+ id,
348
+ className,
349
+ eyebrow = "Featured project",
350
+ title = "Featured project",
351
+ description =
352
+ "Highlight one project with a short summary, key features, supporting details, and an optional visual preview.",
353
+ tags = defaultTags,
354
+ features = defaultFeatures,
355
+ breakdown = defaultBreakdown,
356
+ meta = defaultMeta,
357
+ primaryCta,
358
+ secondaryCta,
359
+ media,
360
+ mediaId,
361
+ detailsId,
362
+ ariaLabel = "Featured project showcase section",
363
+ titleId,
364
+ enableMotion = true,
365
+ classNames,
366
+ section,
367
+ container,
368
+ content,
369
+ mediaSlot,
370
+ }: FeaturedProjectShowcaseProps) {
371
+ const motionClasses = enableMotion
372
+ ? "transition-all duration-200 hover:-translate-y-0.5"
373
+ : "transition-none hover:!translate-y-0";
374
+
375
+ const resolvedMediaId = mediaId ?? (id ? `${id}-media` : "featured-project-media");
376
+ const resolvedDetailsId = detailsId ?? (id ? `${id}-details` : "featured-project-details");
377
+
378
+ const resolvedPrimaryCta: FeaturedProjectShowcaseCta = {
379
+ label: "View project",
380
+ href: `#${resolvedMediaId}`,
381
+ variant: "default",
382
+ size: "lg",
383
+ unstyled: true,
384
+ ...(primaryCta ?? {}),
385
+ className: cn(
386
+ "h-11 rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground shadow-lg hover:bg-primary/90",
387
+
388
+ motionClasses,
389
+ classNames?.primaryCta,
390
+ primaryCta?.className,
391
+ ),
392
+ };
393
+
394
+ const resolvedSecondaryCta: FeaturedProjectShowcaseCta = {
395
+ label: "Read notes",
396
+ href: `#${resolvedDetailsId}`,
397
+ variant: "outline",
398
+ size: "lg",
399
+ unstyled: true,
400
+ ...(secondaryCta ?? {}),
401
+ className: cn(
402
+ "h-11 rounded-full border border-border bg-background/80 px-6 text-sm font-medium text-foreground hover:bg-muted",
403
+
404
+ motionClasses,
405
+ classNames?.secondaryCta,
406
+ secondaryCta?.className,
407
+ ),
408
+ };
409
+
410
+ const mediaContent = renderMedia(media, classNames);
411
+
412
+ return (
413
+ <section
414
+ id={id}
415
+ className={cn(
416
+ "relative overflow-hidden bg-background px-4 py-20 text-foreground sm:px-6 lg:px-8 lg:py-24",
417
+
418
+ section?.className,
419
+ classNames?.section,
420
+ className,
421
+ )}
422
+ aria-label={ariaLabel}
423
+ aria-labelledby={titleId}
424
+ >
425
+ <div
426
+ className={cn(
427
+ "pointer-events-none absolute right-[-18rem] top-24 h-[34rem] w-[34rem] rounded-full bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.08),rgba(0,0,0,0.03)_36%,transparent_70%)] blur-3xl dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.12),rgba(255,255,255,0.04)_36%,transparent_70%)]",
428
+
429
+ classNames?.backgroundGlow,
430
+ )}
431
+ aria-hidden="true"
432
+ />
433
+
434
+ <div
435
+ className={cn(
436
+ "relative z-10 mx-auto max-w-7xl",
437
+ container?.className,
438
+ classNames?.container,
439
+ )}
440
+ >
441
+ <div
442
+ className={cn(
443
+ "grid gap-8 lg:grid-cols-[minmax(0,0.92fr)_minmax(26rem,1.08fr)] lg:items-stretch",
444
+ classNames?.contentGrid,
445
+ )}
446
+ >
447
+ <div
448
+ className={cn(
449
+ "flex flex-col 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.55)] sm:p-8 lg:p-10",
450
+
451
+ content?.className,
452
+ classNames?.content,
453
+ )}
454
+ >
455
+ {eyebrow ? (
456
+ <p
457
+ className={cn(
458
+ "text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground",
459
+
460
+ classNames?.eyebrow,
461
+ )}
462
+ >
463
+ {eyebrow}
464
+ </p>
465
+ ) : null}
466
+
467
+ <h2
468
+ id={titleId}
469
+ className={cn(
470
+ "mt-5 text-balance text-4xl font-semibold tracking-[-0.055em] text-foreground sm:text-5xl lg:text-6xl",
471
+
472
+ classNames?.title,
473
+ )}
474
+ >
475
+ {title}
476
+ </h2>
477
+
478
+ {description ? (
479
+ <p
480
+ className={cn(
481
+ "mt-5 max-w-2xl text-pretty text-base leading-7 text-muted-foreground sm:text-lg",
482
+
483
+ classNames?.description,
484
+ )}
485
+ >
486
+ {description}
487
+ </p>
488
+ ) : null}
489
+
490
+ {tags.length > 0 ? (
491
+ <div className={cn("mt-7 flex flex-wrap gap-2", classNames?.tags)}>
492
+ {tags.map((tag) => {
493
+ const normalizedTag = normalizeTag(tag);
494
+
495
+ return (
496
+ <span
497
+ key={normalizedTag.label}
498
+ className={cn(
499
+ "rounded-full border border-border bg-muted/60 px-3 py-1 text-xs font-medium text-muted-foreground",
500
+
501
+ classNames?.tag,
502
+ normalizedTag.className,
503
+ )}
504
+ >
505
+ {normalizedTag.label}
506
+ </span>
507
+ );
508
+ })}
509
+ </div>
510
+ ) : null}
511
+
512
+ {features.length > 0 ? (
513
+ <ul className={cn("mt-8 space-y-3", classNames?.features)}>
514
+ {features.map((feature, index) => {
515
+ const normalizedFeature = normalizeFeature(feature);
516
+
517
+ return (
518
+ <li
519
+ key={`${normalizedFeature.title ?? "feature"}-${index}`}
520
+ className={cn(
521
+ "flex gap-3 text-sm leading-6 text-muted-foreground",
522
+
523
+ classNames?.feature,
524
+ normalizedFeature.className,
525
+ )}
526
+ >
527
+ <span
528
+ className={cn(
529
+ "mt-2 size-1.5 shrink-0 rounded-full bg-foreground/50",
530
+
531
+ classNames?.featureMarker,
532
+ )}
533
+ aria-hidden="true"
534
+ />
535
+ <span>
536
+ {normalizedFeature.title ? (
537
+ <span
538
+ className={cn(
539
+ "mr-1 font-medium text-foreground",
540
+
541
+ classNames?.featureTitle,
542
+ )}
543
+ >
544
+ {normalizedFeature.title}:
545
+ </span>
546
+ ) : null}
547
+ <span className={classNames?.featureDescription}>
548
+ {normalizedFeature.description}
549
+ </span>
550
+ </span>
551
+ </li>
552
+ );
553
+ })}
554
+ </ul>
555
+ ) : null}
556
+
557
+ {breakdown.length > 0 ? (
558
+ <div
559
+ id={resolvedDetailsId}
560
+ className={cn(
561
+ "mt-8 grid gap-3 border-t border-border pt-6 sm:grid-cols-3",
562
+
563
+ classNames?.breakdown,
564
+ )}
565
+ >
566
+ {breakdown.map((item) => (
567
+ <div
568
+ key={item.label}
569
+ className={cn(
570
+ "rounded-2xl border border-border bg-muted/40 p-4",
571
+
572
+ classNames?.breakdownItem,
573
+ item.className,
574
+ )}
575
+ >
576
+ <p
577
+ className={cn(
578
+ "text-sm font-medium text-foreground",
579
+
580
+ classNames?.breakdownLabel,
581
+ )}
582
+ >
583
+ {item.label}
584
+ </p>
585
+ <p
586
+ className={cn(
587
+ "mt-2 text-sm leading-6 text-muted-foreground",
588
+
589
+ classNames?.breakdownDescription,
590
+ )}
591
+ >
592
+ {item.description}
593
+ </p>
594
+ </div>
595
+ ))}
596
+ </div>
597
+ ) : null}
598
+
599
+ <div
600
+ className={cn(
601
+ "mt-8 flex flex-col gap-3 sm:flex-row",
602
+ classNames?.buttons,
603
+ )}
604
+ >
605
+ {renderCta(resolvedPrimaryCta)}
606
+ {renderCta(resolvedSecondaryCta)}
607
+ </div>
608
+
609
+ {meta.length > 0 ? (
610
+ <dl
611
+ className={cn(
612
+ "mt-auto grid gap-4 border-t border-border pt-6 sm:grid-cols-3 lg:mt-10",
613
+
614
+ classNames?.meta,
615
+ )}
616
+ >
617
+ {meta.map((item) => (
618
+ <div
619
+ key={item.label}
620
+ className={cn("min-w-0", classNames?.metaItem, item.className)}
621
+ >
622
+ <dt
623
+ className={cn(
624
+ "text-xs uppercase tracking-[0.2em] text-muted-foreground",
625
+
626
+ classNames?.metaLabel,
627
+ )}
628
+ >
629
+ {item.label}
630
+ </dt>
631
+ <dd
632
+ className={cn(
633
+ "mt-2 truncate text-sm font-medium text-foreground/80",
634
+
635
+ classNames?.metaValue,
636
+ )}
637
+ >
638
+ {item.value}
639
+ </dd>
640
+ </div>
641
+ ))}
642
+ </dl>
643
+ ) : null}
644
+ </div>
645
+
646
+ {mediaContent ? (
647
+ <div
648
+ id={resolvedMediaId}
649
+ className={cn(
650
+ "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.62)]",
651
+
652
+ enableMotion && "transition-transform duration-300 hover:-translate-y-1",
653
+ !enableMotion && "transition-none hover:!translate-y-0",
654
+ mediaSlot?.className,
655
+ classNames?.mediaOuter,
656
+ )}
657
+ >
658
+ <div
659
+ className={cn(
660
+ "flex h-10 items-center gap-2 border-b border-border px-4",
661
+
662
+ classNames?.mediaChrome,
663
+ )}
664
+ aria-hidden="true"
665
+ >
666
+ <span className="size-2.5 rounded-full bg-foreground/24" />
667
+ <span className="size-2.5 rounded-full bg-foreground/16" />
668
+ <span className="size-2.5 rounded-full bg-foreground/10" />
669
+ <span className="ml-3 h-2 w-28 rounded-full bg-foreground/8" />
670
+
671
+ </div>
672
+ <div
673
+ className={cn(
674
+ "overflow-hidden rounded-[1.45rem] bg-muted",
675
+
676
+ classNames?.mediaFrame,
677
+ )}
678
+ >
679
+ <div className={classNames?.mediaContent}>{mediaContent}</div>
680
+ </div>
681
+ </div>
682
+ ) : null}
683
+ </div>
684
+ </div>
685
+ </section>
686
+ );
687
+ }