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,495 @@
1
+ import React from "react";
2
+ import Link from "next/link";
3
+ import { Button } from "@/components/ui/button";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ type ButtonVariant =
7
+ | "default"
8
+ | "destructive"
9
+ | "outline"
10
+ | "secondary"
11
+ | "ghost"
12
+ | "link";
13
+
14
+ type ButtonSize = "default" | "sm" | "lg" | "icon";
15
+
16
+ export interface HeroWithVideoCta {
17
+ label?: string;
18
+ href?: string;
19
+ ariaLabel?: string;
20
+ variant?: ButtonVariant;
21
+ size?: ButtonSize;
22
+ className?: string;
23
+ unstyled?: boolean;
24
+ style?: React.CSSProperties;
25
+ target?: string;
26
+ rel?: string;
27
+ }
28
+
29
+ export interface HeroWithVideoTextContent {
30
+ text?: React.ReactNode;
31
+ className?: string;
32
+ }
33
+
34
+ export interface HeroWithVideoTitleContent extends HeroWithVideoTextContent {
35
+ highlight?: string;
36
+ highlightClassName?: string;
37
+ }
38
+
39
+ export interface HeroWithVideoMedia {
40
+ src?: string;
41
+ poster?: string;
42
+ title?: string;
43
+ ariaLabel?: string;
44
+ className?: string;
45
+ autoPlay?: boolean;
46
+ muted?: boolean;
47
+ loop?: boolean;
48
+ playsInline?: boolean;
49
+ controls?: boolean;
50
+ preload?: "none" | "metadata" | "auto";
51
+ }
52
+
53
+ export interface HeroWithVideoClassNames {
54
+ section?: string;
55
+ backgroundGlow?: string;
56
+ container?: string;
57
+ content?: string;
58
+ eyebrow?: string;
59
+ title?: string;
60
+ titleHighlight?: string;
61
+ description?: string;
62
+ credibility?: string;
63
+ buttons?: string;
64
+ primaryCta?: string;
65
+ secondaryCta?: string;
66
+ command?: string;
67
+ videoOuter?: string;
68
+ videoFrame?: string;
69
+ videoChrome?: string;
70
+ video?: string;
71
+ videoFallback?: string;
72
+ }
73
+
74
+ export interface HeroWithVideoProps {
75
+ id?: string;
76
+ className?: string;
77
+ eyebrow?: string | HeroWithVideoTextContent;
78
+ title?: string | HeroWithVideoTitleContent;
79
+ heading?: string | HeroWithVideoTitleContent;
80
+ description?: string | HeroWithVideoTextContent;
81
+ subheading?: string | HeroWithVideoTextContent;
82
+ credibility?: string | HeroWithVideoTextContent;
83
+ command?: string | HeroWithVideoTextContent;
84
+ primaryCta?: HeroWithVideoCta;
85
+ secondaryCta?: HeroWithVideoCta;
86
+ cta1?: HeroWithVideoCta;
87
+ cta2?: HeroWithVideoCta;
88
+ video?: HeroWithVideoMedia;
89
+ embed?: React.ReactNode;
90
+ fallback?: React.ReactNode;
91
+ previewId?: string;
92
+ ariaLabel?: string;
93
+ titleId?: string;
94
+ enableMotion?: boolean;
95
+ classNames?: HeroWithVideoClassNames;
96
+ section?: { className?: string };
97
+ container?: { className?: string };
98
+ content?: { className?: string };
99
+ eyebrowSlot?: { className?: string };
100
+ titleSlot?: { className?: string };
101
+ descriptionSlot?: { className?: string };
102
+ credibilitySlot?: { className?: string };
103
+ buttonsContainer?: { className?: string };
104
+ commandSlot?: { className?: string };
105
+ videoOuter?: { className?: string };
106
+ videoFrame?: { className?: string };
107
+ videoChrome?: { className?: string };
108
+ videoSlot?: { className?: string };
109
+ }
110
+
111
+ function normalizeTextContent<T extends HeroWithVideoTextContent>(
112
+ value: string | T | undefined,
113
+ defaults: Required<HeroWithVideoTextContent>,
114
+ ): Required<HeroWithVideoTextContent> {
115
+ if (typeof value === "string") {
116
+ return { text: value, className: defaults.className };
117
+ }
118
+
119
+ return {
120
+ text: value?.text ?? defaults.text,
121
+ className: cn(defaults.className, value?.className),
122
+ };
123
+ }
124
+
125
+ function normalizeTitleContent(
126
+ value: string | HeroWithVideoTitleContent | undefined,
127
+ defaults: Required<HeroWithVideoTitleContent>,
128
+ ): Required<HeroWithVideoTitleContent> {
129
+ if (typeof value === "string") {
130
+ return {
131
+ text: value,
132
+ className: defaults.className,
133
+ highlight: defaults.highlight,
134
+ highlightClassName: defaults.highlightClassName,
135
+ };
136
+ }
137
+
138
+ return {
139
+ text: value?.text ?? defaults.text,
140
+ className: cn(defaults.className, value?.className),
141
+ highlight: value?.highlight ?? defaults.highlight,
142
+ highlightClassName: cn(
143
+ defaults.highlightClassName,
144
+ value?.highlightClassName,
145
+ ),
146
+ };
147
+ }
148
+
149
+ function renderHighlightedTitle(
150
+ title: Required<HeroWithVideoTitleContent>,
151
+ className?: string,
152
+ ) {
153
+ if (!title.highlight || typeof title.text !== "string") {
154
+ return title.text;
155
+ }
156
+
157
+ const [before, ...rest] = title.text.split(title.highlight);
158
+
159
+ if (rest.length === 0) {
160
+ return title.text;
161
+ }
162
+
163
+ return (
164
+ <>
165
+ {before}
166
+ <span className={cn(title.highlightClassName, className)}>
167
+ {title.highlight}
168
+ </span>
169
+ {rest.join(title.highlight)}
170
+ </>
171
+ );
172
+ }
173
+
174
+ function renderCta(cta: HeroWithVideoCta | undefined) {
175
+ if (!cta?.label) {
176
+ return null;
177
+ }
178
+
179
+ return (
180
+ <Button
181
+ asChild
182
+ variant={cta.variant}
183
+ size={cta.size}
184
+ className={cta.className}
185
+ unstyled={cta.unstyled}
186
+ style={cta.style}
187
+ >
188
+ <Link
189
+ href={cta.href || "#"}
190
+ aria-label={cta.ariaLabel ?? cta.label}
191
+ target={cta.target}
192
+ rel={cta.rel ?? (cta.target === "_blank" ? "noreferrer" : undefined)}
193
+ >
194
+ {cta.label}
195
+ </Link>
196
+ </Button>
197
+ );
198
+ }
199
+
200
+ export function HeroWithVideo({
201
+ id,
202
+ className,
203
+ eyebrow,
204
+ title,
205
+ heading,
206
+ description,
207
+ subheading,
208
+ credibility,
209
+ command,
210
+ primaryCta,
211
+ secondaryCta,
212
+ cta1,
213
+ cta2,
214
+ video,
215
+ embed,
216
+ fallback,
217
+ previewId,
218
+ ariaLabel = "Video hero section",
219
+ titleId,
220
+ enableMotion = true,
221
+ classNames,
222
+ section,
223
+ container,
224
+ content,
225
+ eyebrowSlot,
226
+ titleSlot,
227
+ descriptionSlot,
228
+ credibilitySlot,
229
+ buttonsContainer,
230
+ commandSlot,
231
+ videoOuter,
232
+ videoFrame,
233
+ videoChrome,
234
+ videoSlot,
235
+ }: HeroWithVideoProps) {
236
+ const normalizedEyebrow = normalizeTextContent(eyebrow, {
237
+ text: "Product demo · Launch page · Video preview",
238
+ className:
239
+ "hidden text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground sm:block sm:text-sm",
240
+ });
241
+
242
+ const normalizedTitle = normalizeTitleContent(heading ?? title, {
243
+ text: "Show your product with a clear video-first hero.",
244
+ className:
245
+ "mx-auto max-w-5xl text-balance text-4xl font-semibold tracking-[-0.055em] text-foreground sm:text-6xl lg:text-7xl",
246
+ highlight: "video-first",
247
+ highlightClassName: "text-foreground",
248
+ });
249
+
250
+ const normalizedDescription = normalizeTextContent(subheading ?? description, {
251
+ text: "Introduce a product, service, or project with a focused headline, simple calls to action, and a large demo preview.",
252
+ className:
253
+ "mx-auto mt-5 max-w-2xl text-pretty text-base leading-7 text-muted-foreground sm:text-lg lg:text-xl",
254
+ });
255
+
256
+ const normalizedCredibility = normalizeTextContent(credibility, {
257
+ text: "Designed for landing pages, launches, and product walkthroughs.",
258
+ className: "mt-4 text-sm text-muted-foreground",
259
+ });
260
+
261
+ const normalizedCommand = normalizeTextContent(command, {
262
+ text: "Add a short setup command or product note here.",
263
+ className:
264
+ "mt-6 inline-flex max-w-full items-center rounded-full border border-border bg-muted/60 px-4 py-2 font-mono text-xs text-muted-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] sm:text-sm dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]",
265
+ });
266
+
267
+ const buttonMotion = enableMotion
268
+ ? "transition-all duration-200 hover:-translate-y-0.5"
269
+ : "transition-none hover:!translate-y-0";
270
+
271
+ const resolvedPreviewId =
272
+ previewId ?? (id ? `${id}-preview` : "video-preview");
273
+
274
+ const resolvedPrimaryCta: HeroWithVideoCta = {
275
+ label: "View demo",
276
+ href: `#${resolvedPreviewId}`,
277
+ variant: "default",
278
+ size: "lg",
279
+ unstyled: true,
280
+ ...(primaryCta ?? cta1 ?? {}),
281
+ className: cn(
282
+ "h-11 rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground shadow-lg hover:bg-primary/90",
283
+ buttonMotion,
284
+ classNames?.primaryCta,
285
+ primaryCta?.className ?? cta1?.className,
286
+ ),
287
+ };
288
+
289
+ const secondaryInput = secondaryCta ?? cta2;
290
+ const resolvedSecondaryCta: HeroWithVideoCta = {
291
+ label: "Learn more",
292
+ href: "#",
293
+ variant: "outline",
294
+ size: "lg",
295
+ unstyled: true,
296
+ ...(secondaryInput ?? {}),
297
+ className: cn(
298
+ "h-11 rounded-full border border-border bg-background/80 px-6 text-sm font-medium text-foreground hover:bg-muted",
299
+ buttonMotion,
300
+ classNames?.secondaryCta,
301
+ secondaryInput?.className,
302
+ ),
303
+ };
304
+
305
+ const videoMotion = enableMotion
306
+ ? "transition-transform duration-300 hover:-translate-y-1"
307
+ : "transition-none hover:!translate-y-0";
308
+
309
+ return (
310
+ <section
311
+ id={id}
312
+ className={cn(
313
+ "relative overflow-hidden bg-background px-4 py-20 text-foreground sm:px-6 lg:px-8 lg:py-24",
314
+ section?.className,
315
+ classNames?.section,
316
+ className,
317
+ )}
318
+ aria-label={ariaLabel}
319
+ aria-labelledby={titleId}
320
+ >
321
+ <div
322
+ className={cn(
323
+ "pointer-events-none absolute left-1/2 top-[34rem] h-[30rem] w-[72rem] -translate-x-1/2 rounded-full bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.08)_0%,rgba(0,0,0,0.03)_24%,transparent_68%)] blur-3xl dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.16)_0%,rgba(255,255,255,0.08)_24%,transparent_68%)]",
324
+ classNames?.backgroundGlow,
325
+ )}
326
+ aria-hidden="true"
327
+ />
328
+ <div
329
+ className="pointer-events-none absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-foreground/5 to-transparent"
330
+ aria-hidden="true"
331
+ />
332
+
333
+ <div
334
+ className={cn(
335
+ "relative z-10 mx-auto flex max-w-7xl flex-col items-center",
336
+ container?.className,
337
+ classNames?.container,
338
+ )}
339
+ >
340
+ <div
341
+ className={cn(
342
+ "mx-auto flex max-w-5xl flex-col items-center text-center",
343
+ content?.className,
344
+ classNames?.content,
345
+ )}
346
+ >
347
+ {normalizedEyebrow.text ? (
348
+ <p
349
+ className={cn(
350
+ normalizedEyebrow.className,
351
+ eyebrowSlot?.className,
352
+ classNames?.eyebrow,
353
+ )}
354
+ >
355
+ {normalizedEyebrow.text}
356
+ </p>
357
+ ) : null}
358
+
359
+ <h1
360
+ id={titleId}
361
+ className={cn(
362
+ "mt-5",
363
+ normalizedTitle.className,
364
+ titleSlot?.className,
365
+ classNames?.title,
366
+ )}
367
+ >
368
+ {renderHighlightedTitle(normalizedTitle, classNames?.titleHighlight)}
369
+ </h1>
370
+
371
+ {normalizedDescription.text ? (
372
+ <p
373
+ className={cn(
374
+ normalizedDescription.className,
375
+ descriptionSlot?.className,
376
+ classNames?.description,
377
+ )}
378
+ >
379
+ {normalizedDescription.text}
380
+ </p>
381
+ ) : null}
382
+
383
+ <div
384
+ className={cn(
385
+ "mt-8 flex w-full flex-col items-center justify-center gap-3 sm:w-auto sm:flex-row",
386
+ buttonsContainer?.className,
387
+ classNames?.buttons,
388
+ )}
389
+ >
390
+ {renderCta(resolvedPrimaryCta)}
391
+ {renderCta(resolvedSecondaryCta)}
392
+ </div>
393
+
394
+ {normalizedCredibility.text ? (
395
+ <p
396
+ className={cn(
397
+ normalizedCredibility.className,
398
+ credibilitySlot?.className,
399
+ classNames?.credibility,
400
+ )}
401
+ >
402
+ {normalizedCredibility.text}
403
+ </p>
404
+ ) : null}
405
+
406
+ {normalizedCommand.text ? (
407
+ <code
408
+ className={cn(
409
+ normalizedCommand.className,
410
+ commandSlot?.className,
411
+ classNames?.command,
412
+ )}
413
+ >
414
+ <span className="mr-2 text-muted-foreground/60" aria-hidden="true">
415
+ $
416
+ </span>
417
+ <span className="truncate">{normalizedCommand.text}</span>
418
+ </code>
419
+ ) : null}
420
+ </div>
421
+
422
+ <div
423
+ id={resolvedPreviewId}
424
+ className={cn(
425
+ "relative mt-12 w-full max-w-6xl sm:mt-14 lg:mt-16",
426
+ videoOuter?.className,
427
+ classNames?.videoOuter,
428
+ )}
429
+ >
430
+ <div
431
+ className={cn(
432
+ "relative overflow-hidden rounded-[1.75rem] border border-border bg-card/80 p-2 shadow-[0_28px_90px_rgba(0,0,0,0.12)] backdrop-blur dark:shadow-[0_28px_90px_rgba(0,0,0,0.72),0_0_0_1px_rgba(255,255,255,0.04)]",
433
+ videoMotion,
434
+ videoFrame?.className,
435
+ classNames?.videoFrame,
436
+ )}
437
+ >
438
+ <div
439
+ className={cn(
440
+ "flex h-10 items-center gap-2 border-b border-border px-4",
441
+ videoChrome?.className,
442
+ classNames?.videoChrome,
443
+ )}
444
+ aria-hidden="true"
445
+ >
446
+ <span className="size-2.5 rounded-full bg-foreground/24" />
447
+ <span className="size-2.5 rounded-full bg-foreground/16" />
448
+ <span className="size-2.5 rounded-full bg-foreground/10" />
449
+ <span className="ml-3 h-2 w-28 rounded-full bg-foreground/8" />
450
+ </div>
451
+
452
+ <div className="relative aspect-video overflow-hidden rounded-[1.25rem] bg-muted">
453
+ {embed ??
454
+ (video?.src ? (
455
+ <video
456
+ src={video.src}
457
+ poster={video.poster}
458
+ title={video.title ?? video.ariaLabel ?? "Hero video"}
459
+ aria-label={video.ariaLabel ?? video.title ?? "Hero video"}
460
+ className={cn(
461
+ "size-full object-cover",
462
+ video.className,
463
+ videoSlot?.className,
464
+ classNames?.video,
465
+ )}
466
+ autoPlay={video.autoPlay}
467
+ muted={video.muted ?? video.autoPlay ?? false}
468
+ loop={video.loop}
469
+ playsInline={video.playsInline ?? true}
470
+ controls={video.controls ?? true}
471
+ preload={video.preload ?? "metadata"}
472
+ />
473
+ ) : (
474
+ <div
475
+ className={cn(
476
+ "flex size-full items-center justify-center bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.28),rgba(255,255,255,0.1)_38%,rgba(255,255,255,0)_72%)] px-6 text-center dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.11),rgba(255,255,255,0.035)_38%,rgba(0,0,0,1)_72%)]",
477
+ classNames?.videoFallback,
478
+ )}
479
+ >
480
+ {fallback ?? (
481
+ <p className="max-w-md text-sm leading-6 text-muted-foreground">
482
+ Add a demo video source to show a product walkthrough,
483
+ launch preview, or selected project here.
484
+ </p>
485
+ )}
486
+ </div>
487
+ ))}
488
+ </div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ </section>
493
+ );
494
+ }
495
+