@olwiba/ui 0.0.28

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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/index.d.ts +762 -0
  4. package/dist/index.js +2102 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +88 -0
  7. package/src/app/AppShell.tsx +365 -0
  8. package/src/app/AuthSection.tsx +141 -0
  9. package/src/app/EmptyState.tsx +34 -0
  10. package/src/app/ErrorPage.tsx +78 -0
  11. package/src/app/UpgradePrompt.tsx +203 -0
  12. package/src/blog/PostCard.tsx +66 -0
  13. package/src/blog/PostList.tsx +27 -0
  14. package/src/components/ConfirmDialog.tsx +43 -0
  15. package/src/components/ContextMenu.tsx +86 -0
  16. package/src/components/DevBanner.tsx +22 -0
  17. package/src/components/Dock.tsx +94 -0
  18. package/src/components/FeatureCard.tsx +45 -0
  19. package/src/components/GlassCard.tsx +30 -0
  20. package/src/components/ImageCard.tsx +60 -0
  21. package/src/components/PageHeader.tsx +101 -0
  22. package/src/components/PricingCard.tsx +90 -0
  23. package/src/components/RegisterHotkeys.tsx +43 -0
  24. package/src/components/RootErrorFallback.tsx +29 -0
  25. package/src/components/Spinner.tsx +14 -0
  26. package/src/components/Spotlight.tsx +104 -0
  27. package/src/components/StatCard.tsx +44 -0
  28. package/src/components/Suspensed.tsx +17 -0
  29. package/src/components/TestimonialCard.tsx +64 -0
  30. package/src/components/ThemeColorUpdater.tsx +23 -0
  31. package/src/components/ThemeSwitchMinimal.tsx +28 -0
  32. package/src/components/VersionBanner.tsx +38 -0
  33. package/src/context/OlwibaUIContext.tsx +58 -0
  34. package/src/hooks/use-confirm.ts +64 -0
  35. package/src/hooks/use-controlled-open.ts +33 -0
  36. package/src/hooks/use-copy-to-clipboard.ts +20 -0
  37. package/src/hooks/use-debounce.ts +14 -0
  38. package/src/hooks/use-intersection-observer.ts +25 -0
  39. package/src/hooks/use-local-storage.ts +31 -0
  40. package/src/hooks/use-media-query.ts +21 -0
  41. package/src/hooks/use-mounted.ts +11 -0
  42. package/src/hooks/use-pagination.ts +31 -0
  43. package/src/hooks/use-scrolled-past.ts +27 -0
  44. package/src/index.ts +113 -0
  45. package/src/lib/utils.ts +6 -0
  46. package/src/marketing/ContactSection.tsx +110 -0
  47. package/src/marketing/CtaSection.tsx +74 -0
  48. package/src/marketing/FaqSection.tsx +44 -0
  49. package/src/marketing/FeaturesSection.tsx +42 -0
  50. package/src/marketing/Footer.tsx +87 -0
  51. package/src/marketing/HeroSection.tsx +112 -0
  52. package/src/marketing/LogoStrip.tsx +94 -0
  53. package/src/marketing/Navbar.tsx +142 -0
  54. package/src/marketing/NewsletterSection.tsx +64 -0
  55. package/src/marketing/PricingSection.tsx +120 -0
  56. package/src/marketing/SectionTitle.tsx +30 -0
  57. package/src/marketing/StatsSection.tsx +63 -0
  58. package/src/marketing/TeamSection.tsx +110 -0
  59. package/src/marketing/TestimonialsSection.tsx +56 -0
  60. package/src/motion/CountUp.tsx +64 -0
  61. package/src/motion/FadeIn.tsx +69 -0
  62. package/src/motion/PageTransition.tsx +49 -0
  63. package/src/motion/StaggerChildren.tsx +74 -0
  64. package/src/overlays/Overlay.tsx +88 -0
  65. package/src/overlays/Underlay.tsx +114 -0
  66. package/src/primitives/Badge.tsx +11 -0
  67. package/src/primitives/Button.tsx +16 -0
  68. package/src/primitives/Card.tsx +22 -0
  69. package/src/primitives/Checkbox.tsx +17 -0
  70. package/src/primitives/Input.tsx +16 -0
  71. package/src/primitives/Switch.tsx +16 -0
  72. package/src/primitives/Textarea.tsx +16 -0
  73. package/src/primitives/index.ts +7 -0
  74. package/src/types/external-packages.d.ts +79 -0
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { ArrowRight, Sparkles } from 'lucide-react';
4
+ import { Button } from '@olwiba/cn';
5
+ import { FadeIn } from '../motion/FadeIn';
6
+ import type { AppShellRenderLink } from '../app/AppShell';
7
+
8
+ export interface CtaSectionProps {
9
+ heading: string;
10
+ description: string;
11
+ primaryCta: { label: string; href: string };
12
+ secondaryCta?: { label: string; href: string };
13
+ footnote?: string;
14
+ renderLink?: AppShellRenderLink;
15
+ }
16
+
17
+ const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
18
+ <a href={href} className={className}>{children}</a>
19
+ );
20
+
21
+ export function CtaSection({
22
+ heading,
23
+ description,
24
+ primaryCta,
25
+ secondaryCta,
26
+ footnote,
27
+ renderLink = defaultRenderLink,
28
+ }: CtaSectionProps) {
29
+ return (
30
+ <section className="overflow-hidden rounded-2xl border bg-card">
31
+ <div className="relative px-6 py-16 sm:px-10 sm:py-24">
32
+ <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--primary)/0.15),transparent_70%)]" />
33
+ <FadeIn direction="up">
34
+ <div className="relative mx-auto max-w-2xl text-center">
35
+ <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
36
+ <Sparkles className="size-5" />
37
+ </div>
38
+ <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
39
+ {heading}
40
+ </h2>
41
+ <p className="mx-auto mt-4 max-w-lg text-pretty text-muted-foreground">
42
+ {description}
43
+ </p>
44
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
45
+ {renderLink({
46
+ href: primaryCta.href,
47
+ children: (
48
+ <Button size="lg">
49
+ {primaryCta.label}
50
+ <ArrowRight className="ml-2 size-4" />
51
+ </Button>
52
+ ),
53
+ })}
54
+ {secondaryCta &&
55
+ renderLink({
56
+ href: secondaryCta.href,
57
+ children: (
58
+ <Button size="lg" variant="outline">
59
+ {secondaryCta.label}
60
+ </Button>
61
+ ),
62
+ })}
63
+ </div>
64
+ {footnote && (
65
+ <p className="mt-4 text-sm text-muted-foreground">
66
+ {footnote}
67
+ </p>
68
+ )}
69
+ </div>
70
+ </FadeIn>
71
+ </div>
72
+ </section>
73
+ );
74
+ }
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@olwiba/cn';
4
+ import { SectionTitle } from './SectionTitle';
5
+ import { FadeIn } from '../motion/FadeIn';
6
+
7
+ export interface FaqSectionProps {
8
+ title?: string;
9
+ description?: string;
10
+ badge?: string;
11
+ items: Array<{ question: string; answer: string }>;
12
+ }
13
+
14
+ export function FaqSection({
15
+ title = 'Frequently asked questions',
16
+ description = 'Everything you need to know before getting started.',
17
+ badge = 'FAQ',
18
+ items,
19
+ }: FaqSectionProps) {
20
+ return (
21
+ <section className="overflow-hidden rounded-2xl border bg-card">
22
+ <div className="px-6 py-14 sm:px-10 sm:py-20">
23
+ <div className="mx-auto max-w-3xl">
24
+ <SectionTitle title={title} description={description} badge={badge} />
25
+
26
+ <FadeIn direction="up">
27
+ <Accordion type="single" collapsible className="mt-10 w-full">
28
+ {items.map((item, i) => (
29
+ <AccordionItem key={i} value={`item-${i}`}>
30
+ <AccordionTrigger className="text-left text-sm font-medium">
31
+ {item.question}
32
+ </AccordionTrigger>
33
+ <AccordionContent className="text-sm text-muted-foreground">
34
+ {item.answer}
35
+ </AccordionContent>
36
+ </AccordionItem>
37
+ ))}
38
+ </Accordion>
39
+ </FadeIn>
40
+ </div>
41
+ </div>
42
+ </section>
43
+ );
44
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import type { LucideIcon } from 'lucide-react';
4
+ import { FeatureCard } from '../components/FeatureCard';
5
+ import { SectionTitle } from './SectionTitle';
6
+ import { StaggerChildren } from '../motion/StaggerChildren';
7
+
8
+ export interface FeaturesSectionProps {
9
+ title?: string;
10
+ description?: string;
11
+ badge?: string;
12
+ features: Array<{ icon: LucideIcon; title: string; description: string; href?: string }>;
13
+ }
14
+
15
+ export function FeaturesSection({
16
+ title = 'Everything you need to ship fast',
17
+ description = 'A complete system of components, blocks, and hooks designed to work together - and get out of your way.',
18
+ badge = 'Features',
19
+ features,
20
+ }: FeaturesSectionProps) {
21
+ return (
22
+ <section className="overflow-hidden rounded-2xl border bg-card">
23
+ <div className="px-6 py-14 sm:px-10 sm:py-20">
24
+ <div className="mx-auto max-w-4xl">
25
+ <SectionTitle title={title} description={description} badge={badge} />
26
+
27
+ <StaggerChildren className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
28
+ {features.map((feature) => (
29
+ <FeatureCard
30
+ key={feature.title}
31
+ icon={feature.icon}
32
+ title={feature.title}
33
+ description={feature.description}
34
+ href={feature.href}
35
+ />
36
+ ))}
37
+ </StaggerChildren>
38
+ </div>
39
+ </div>
40
+ </section>
41
+ );
42
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import type { AppShellRenderLink } from '../app/AppShell';
5
+
6
+ export interface FooterProps {
7
+ brand: { name: string; logo?: ReactNode; href?: string };
8
+ navLinks?: Array<{ label: string; href: string }>;
9
+ socialLinks?: Array<{ label: string; href: string; icon: ReactNode }>;
10
+ legal?: string;
11
+ renderLink?: AppShellRenderLink;
12
+ }
13
+
14
+ const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
15
+ <a href={href} className={className}>{children}</a>
16
+ );
17
+
18
+ export function Footer({
19
+ brand,
20
+ navLinks,
21
+ socialLinks,
22
+ legal,
23
+ renderLink = defaultRenderLink,
24
+ }: FooterProps) {
25
+ const brandHref = brand.href ?? '/';
26
+ const copyrightText = legal ?? `\u00A9 ${new Date().getFullYear()} ${brand.name}. All rights reserved.`;
27
+
28
+ return (
29
+ <footer className="bg-gray-900">
30
+ <div className="mx-auto max-w-7xl overflow-hidden px-6 py-20 sm:py-24 lg:px-8">
31
+ {/* Brand */}
32
+ <div className="flex justify-center">
33
+ {renderLink({
34
+ href: brandHref,
35
+ className: 'flex items-center gap-2',
36
+ children: (
37
+ <>
38
+ {brand.logo && (
39
+ <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10 text-white">
40
+ {brand.logo}
41
+ </span>
42
+ )}
43
+ <span className="text-lg font-semibold text-white">{brand.name}</span>
44
+ </>
45
+ ),
46
+ })}
47
+ </div>
48
+
49
+ {/* Nav links */}
50
+ {navLinks && navLinks.length > 0 && (
51
+ <nav className="mt-10 flex flex-wrap justify-center gap-x-12 gap-y-3" aria-label="Footer">
52
+ {navLinks.map(({ label, href }) => (
53
+ <div key={label}>
54
+ {renderLink({
55
+ href,
56
+ className: 'text-sm/6 text-gray-400 hover:text-white transition-colors',
57
+ children: label,
58
+ })}
59
+ </div>
60
+ ))}
61
+ </nav>
62
+ )}
63
+
64
+ {/* Social icons */}
65
+ {socialLinks && socialLinks.length > 0 && (
66
+ <div className="mt-10 flex justify-center gap-x-10">
67
+ {socialLinks.map(({ label, href, icon }) => (
68
+ <a
69
+ key={label}
70
+ href={href}
71
+ aria-label={label}
72
+ className="text-gray-400 hover:text-gray-300 transition-colors"
73
+ >
74
+ {icon}
75
+ </a>
76
+ ))}
77
+ </div>
78
+ )}
79
+
80
+ {/* Copyright */}
81
+ <p className="mt-10 text-center text-sm/6 text-gray-500">
82
+ {copyrightText}
83
+ </p>
84
+ </div>
85
+ </footer>
86
+ );
87
+ }
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ArrowRight } from 'lucide-react';
5
+ import { Avatar, AvatarFallback, AvatarImage, Badge, Button } from '@olwiba/cn';
6
+ import { FadeIn } from '../motion/FadeIn';
7
+ import type { AppShellRenderLink } from '../app/AppShell';
8
+
9
+ export interface HeroSectionProps {
10
+ heading: string;
11
+ badge?: string;
12
+ description: string;
13
+ primaryCta: { label: string; href: string };
14
+ secondaryCta?: { label: string; href: string };
15
+ heroImage: React.ReactNode;
16
+ avatarUrls?: string[];
17
+ socialProofText?: string;
18
+ renderLink?: AppShellRenderLink;
19
+ }
20
+
21
+ const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
22
+ <a href={href} className={className}>{children}</a>
23
+ );
24
+
25
+ export function HeroSection({
26
+ heading,
27
+ badge,
28
+ description,
29
+ primaryCta,
30
+ secondaryCta,
31
+ heroImage,
32
+ avatarUrls,
33
+ socialProofText,
34
+ renderLink = defaultRenderLink,
35
+ }: HeroSectionProps) {
36
+ return (
37
+ <section className="overflow-hidden">
38
+ <div className="px-6 py-14 sm:px-10 sm:py-20">
39
+ <div className="mx-auto grid max-w-6xl items-center gap-10 lg:grid-cols-2 lg:gap-16">
40
+ {/* Left - text content */}
41
+ <FadeIn direction="left">
42
+ <div className="flex flex-col gap-6">
43
+ {badge && (
44
+ <div>
45
+ <Badge variant="secondary">{badge}</Badge>
46
+ </div>
47
+ )}
48
+ <h1 className="text-balance text-4xl font-semibold tracking-tight sm:text-5xl lg:text-6xl">
49
+ {heading}
50
+ </h1>
51
+ <p className="max-w-lg text-pretty text-lg text-muted-foreground">
52
+ {description}
53
+ </p>
54
+ <div className="flex flex-wrap items-center gap-3">
55
+ {renderLink({
56
+ href: primaryCta.href,
57
+ children: (
58
+ <Button size="lg">
59
+ {primaryCta.label}
60
+ <ArrowRight className="ml-2 size-4" />
61
+ </Button>
62
+ ),
63
+ })}
64
+ {secondaryCta &&
65
+ renderLink({
66
+ href: secondaryCta.href,
67
+ children: (
68
+ <Button size="lg" variant="outline">
69
+ {secondaryCta.label}
70
+ </Button>
71
+ ),
72
+ })}
73
+ </div>
74
+ </div>
75
+ </FadeIn>
76
+
77
+ {/* Right - hero image */}
78
+ <FadeIn direction="right" delay={200}>
79
+ <div className="overflow-hidden rounded-2xl">
80
+ {heroImage}
81
+ </div>
82
+ </FadeIn>
83
+ </div>
84
+
85
+ {/* Avatar social proof row */}
86
+ {(avatarUrls?.length || socialProofText) && (
87
+ <FadeIn direction="up" delay={400}>
88
+ <div className="mx-auto mt-12 flex max-w-6xl items-center gap-4">
89
+ {avatarUrls && avatarUrls.length > 0 && (
90
+ <div className="flex -space-x-3">
91
+ {avatarUrls.map((url, i) => (
92
+ <Avatar key={i} className="size-9 border-2 border-background">
93
+ <AvatarImage src={url} alt="" />
94
+ <AvatarFallback className="text-xs">
95
+ {String.fromCharCode(65 + (i % 26))}
96
+ </AvatarFallback>
97
+ </Avatar>
98
+ ))}
99
+ </div>
100
+ )}
101
+ {socialProofText && (
102
+ <p className="text-sm font-medium text-muted-foreground">
103
+ {socialProofText}
104
+ </p>
105
+ )}
106
+ </div>
107
+ </FadeIn>
108
+ )}
109
+ </div>
110
+ </section>
111
+ );
112
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { FadeIn } from '../motion/FadeIn';
4
+
5
+ export interface LogoStripProps {
6
+ logos: Array<{ src: string; darkSrc?: string; alt: string; href?: string }>;
7
+ label?: string;
8
+ speed?: 'slow' | 'normal' | 'fast';
9
+ }
10
+
11
+ const speedDuration = {
12
+ slow: '60s',
13
+ normal: '40s',
14
+ fast: '25s',
15
+ } as const;
16
+
17
+ export function LogoStrip({
18
+ logos,
19
+ label,
20
+ speed = 'normal',
21
+ }: LogoStripProps) {
22
+ const duration = speedDuration[speed];
23
+
24
+ return (
25
+ <FadeIn direction="up">
26
+ <section className="overflow-hidden py-8">
27
+ {label && (
28
+ <p className="mb-6 text-center text-sm text-muted-foreground">{label}</p>
29
+ )}
30
+ <div
31
+ className="relative"
32
+ style={{
33
+ maskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
34
+ WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
35
+ }}
36
+ >
37
+ <div
38
+ className="flex w-max gap-12"
39
+ style={{
40
+ animation: `logo-strip-scroll ${duration} linear infinite`,
41
+ }}
42
+ >
43
+ {/* Render logos twice for seamless loop */}
44
+ {[...logos, ...logos].map((logo, i) => {
45
+ const img = logo.darkSrc ? (
46
+ <>
47
+ <img
48
+ src={logo.src}
49
+ alt={logo.alt}
50
+ className="h-8 w-auto object-contain opacity-60 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:hidden"
51
+ />
52
+ <img
53
+ src={logo.darkSrc}
54
+ alt={logo.alt}
55
+ className="hidden h-8 w-auto object-contain opacity-60 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:block"
56
+ />
57
+ </>
58
+ ) : (
59
+ <img
60
+ src={logo.src}
61
+ alt={logo.alt}
62
+ className="h-8 w-auto object-contain opacity-60 grayscale transition-all hover:opacity-100 hover:grayscale-0"
63
+ />
64
+ );
65
+
66
+ return logo.href ? (
67
+ <a
68
+ key={`${logo.alt}-${i}`}
69
+ href={logo.href}
70
+ target="_blank"
71
+ rel="noopener noreferrer"
72
+ className="flex shrink-0 items-center"
73
+ >
74
+ {img}
75
+ </a>
76
+ ) : (
77
+ <div key={`${logo.alt}-${i}`} className="flex shrink-0 items-center">
78
+ {img}
79
+ </div>
80
+ );
81
+ })}
82
+ </div>
83
+ </div>
84
+ </section>
85
+
86
+ <style>{`
87
+ @keyframes logo-strip-scroll {
88
+ from { transform: translateX(0); }
89
+ to { transform: translateX(-50%); }
90
+ }
91
+ `}</style>
92
+ </FadeIn>
93
+ );
94
+ }
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Menu, X } from 'lucide-react';
5
+ import { Button, Separator, Sheet, SheetContent, SheetTrigger } from '@olwiba/cn';
6
+ import type { AppShellRenderLink } from '../app/AppShell';
7
+ import { ThemeSwitchMinimal } from '../components/ThemeSwitchMinimal';
8
+
9
+ export interface NavbarProps {
10
+ brand: { name: string; logo?: React.ReactNode; href?: string };
11
+ navLinks: Array<{ label: string; href: string }>;
12
+ cta?: {
13
+ primary?: { label: string; href: string };
14
+ secondary?: { label: string; href: string };
15
+ };
16
+ showThemeToggle?: boolean;
17
+ renderLink?: AppShellRenderLink;
18
+ }
19
+
20
+ const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
21
+ <a href={href} className={className}>{children}</a>
22
+ );
23
+
24
+ export function Navbar({
25
+ brand,
26
+ navLinks,
27
+ cta,
28
+ showThemeToggle = false,
29
+ renderLink = defaultRenderLink,
30
+ }: NavbarProps) {
31
+ const [open, setOpen] = React.useState(false);
32
+ const brandHref = brand.href ?? '/';
33
+
34
+ return (
35
+ <section className="overflow-hidden rounded-2xl border bg-card">
36
+ <nav className="grid h-16 grid-cols-[1fr_auto_1fr] items-center px-6">
37
+ {/* Brand — left column */}
38
+ {renderLink({
39
+ href: brandHref,
40
+ children: (
41
+ <span className="flex items-center gap-2 font-semibold">
42
+ {brand.logo && (
43
+ <span className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
44
+ {brand.logo}
45
+ </span>
46
+ )}
47
+ <span>{brand.name}</span>
48
+ </span>
49
+ ),
50
+ })}
51
+
52
+ {/* Desktop nav links — center column, truly centered */}
53
+ <div className="hidden items-center gap-1 md:flex">
54
+ {navLinks.map((link) => (
55
+ <span key={link.label}>
56
+ {renderLink({
57
+ href: link.href,
58
+ className: 'rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground',
59
+ children: link.label,
60
+ })}
61
+ </span>
62
+ ))}
63
+ </div>
64
+
65
+ {/* Desktop CTAs — right column, right-aligned */}
66
+ <div className="hidden items-center justify-end gap-3 md:flex">
67
+ {cta?.secondary &&
68
+ renderLink({
69
+ href: cta.secondary.href,
70
+ children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
71
+ })}
72
+ {cta?.primary &&
73
+ renderLink({
74
+ href: cta.primary.href,
75
+ children: <Button size="sm">{cta.primary.label}</Button>,
76
+ })}
77
+ {showThemeToggle && <ThemeSwitchMinimal />}
78
+ </div>
79
+
80
+ {/* Mobile drawer — right column on small screens */}
81
+ <Sheet open={open} onOpenChange={setOpen}>
82
+ <SheetTrigger asChild>
83
+ <Button variant="ghost" size="icon" className="justify-self-end md:hidden">
84
+ <Menu className="size-5" />
85
+ <span className="sr-only">Open menu</span>
86
+ </Button>
87
+ </SheetTrigger>
88
+ <SheetContent side="right" className="w-72">
89
+ <div className="flex items-center justify-between pb-4">
90
+ <span onClick={() => setOpen(false)} className="cursor-pointer">
91
+ {renderLink({
92
+ href: brandHref,
93
+ children: (
94
+ <span className="flex items-center gap-2 font-semibold">
95
+ {brand.logo && (
96
+ <span className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
97
+ {brand.logo}
98
+ </span>
99
+ )}
100
+ <span>{brand.name}</span>
101
+ </span>
102
+ ),
103
+ })}
104
+ </span>
105
+ <div className="flex items-center gap-1">
106
+ {showThemeToggle && <ThemeSwitchMinimal />}
107
+ <Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
108
+ <X className="size-4" />
109
+ <span className="sr-only">Close menu</span>
110
+ </Button>
111
+ </div>
112
+ </div>
113
+ <Separator />
114
+ <nav className="mt-4 flex flex-col gap-1">
115
+ {navLinks.map((link) => (
116
+ <span key={link.label} onClick={() => setOpen(false)}>
117
+ {renderLink({
118
+ href: link.href,
119
+ className: 'block rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground',
120
+ children: link.label,
121
+ })}
122
+ </span>
123
+ ))}
124
+ </nav>
125
+ <div className="mt-6 flex flex-col gap-2">
126
+ {cta?.secondary &&
127
+ renderLink({
128
+ href: cta.secondary.href,
129
+ children: <Button variant="outline" className="w-full">{cta.secondary.label}</Button>,
130
+ })}
131
+ {cta?.primary &&
132
+ renderLink({
133
+ href: cta.primary.href,
134
+ children: <Button className="w-full">{cta.primary.label}</Button>,
135
+ })}
136
+ </div>
137
+ </SheetContent>
138
+ </Sheet>
139
+ </nav>
140
+ </section>
141
+ );
142
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ArrowRight, Mail } from 'lucide-react';
5
+ import { Badge, Button, Input } from '@olwiba/cn';
6
+
7
+ export function NewsletterSection() {
8
+ const [email, setEmail] = React.useState('');
9
+ const [submitted, setSubmitted] = React.useState(false);
10
+
11
+ function handleSubmit(e: React.FormEvent) {
12
+ e.preventDefault();
13
+ if (!email) return;
14
+ setSubmitted(true);
15
+ }
16
+
17
+ return (
18
+ <section className="overflow-hidden rounded-2xl border bg-card">
19
+ <div className="relative px-6 py-14 sm:px-10 sm:py-20">
20
+ <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.1),transparent_60%)]" />
21
+ <div className="relative mx-auto max-w-xl text-center">
22
+ <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
23
+ <Mail className="size-5" />
24
+ </div>
25
+
26
+ <Badge variant="secondary" className="mb-3">Newsletter</Badge>
27
+ <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
28
+ Stay in the loop
29
+ </h2>
30
+ <p className="mx-auto mt-4 max-w-sm text-pretty text-muted-foreground">
31
+ New components, release notes, and product tips — straight to your inbox. No spam, unsubscribe any time.
32
+ </p>
33
+
34
+ {submitted ? (
35
+ <div className="mt-8 rounded-2xl border bg-muted/50 px-6 py-5 text-sm">
36
+ <span className="font-medium">You're on the list.</span>{' '}
37
+ <span className="text-muted-foreground">We'll be in touch soon.</span>
38
+ </div>
39
+ ) : (
40
+ <form onSubmit={handleSubmit} className="mt-8 flex flex-col gap-3 sm:flex-row">
41
+ <Input
42
+ type="email"
43
+ placeholder="you@company.com"
44
+ value={email}
45
+ onChange={(e) => setEmail(e.target.value)}
46
+ required
47
+ className="flex-1"
48
+ />
49
+ <Button type="submit">
50
+ Subscribe
51
+ <ArrowRight className="ml-2 size-4" />
52
+ </Button>
53
+ </form>
54
+ )}
55
+
56
+ <p className="mt-4 text-xs text-muted-foreground">
57
+ By subscribing you agree to our{' '}
58
+ <a href="#" className="underline underline-offset-4 hover:text-foreground">Privacy Policy</a>.
59
+ </p>
60
+ </div>
61
+ </div>
62
+ </section>
63
+ );
64
+ }