@olwiba/ui 0.1.4 → 0.1.7
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/dist/index.d.ts +246 -8
- package/dist/index.js +1126 -197
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/app/AuthSection.tsx +23 -1
- package/src/blog/ChangelogCard.tsx +73 -15
- package/src/components/BrandColorSwitchMinimal.tsx +172 -0
- package/src/components/FlowBracket.tsx +166 -0
- package/src/components/FlowConnector.tsx +135 -0
- package/src/components/PricingCard.tsx +7 -3
- package/src/components/ThemeSwitchMinimal.tsx +7 -7
- package/src/index.ts +16 -2
- package/src/marketing/ComparisonSection.tsx +109 -0
- package/src/marketing/ContactSection.tsx +74 -33
- package/src/marketing/CtaCardSection.tsx +95 -0
- package/src/marketing/FeatureMarqueeSection.tsx +120 -0
- package/src/marketing/Footer.tsx +2 -6
- package/src/marketing/GroupedFeaturesSection.tsx +71 -0
- package/src/marketing/Navbar.tsx +2 -10
- package/src/marketing/NewsletterSection.tsx +7 -6
- package/src/marketing/PricingSection.tsx +66 -39
- package/src/marketing/QualificationSection.tsx +86 -0
- package/src/marketing/StepsSection.tsx +97 -0
- package/src/marketing/TeamSection.tsx +50 -47
- package/src/marketing/TechStackSection.tsx +83 -0
- package/src/motion/AnimatedPill.tsx +71 -0
- package/src/motion/CountdownTimer.tsx +81 -0
package/src/marketing/Footer.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export function Footer({
|
|
|
26
26
|
const copyrightText = legal ?? `\u00A9 ${new Date().getFullYear()} ${brand.name}. All rights reserved.`;
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<footer className="overflow-hidden rounded-2xl border bg-
|
|
29
|
+
<footer className="overflow-hidden rounded-2xl border bg-card text-foreground">
|
|
30
30
|
<div className="mx-auto max-w-7xl px-6 py-20 sm:py-24 lg:px-8">
|
|
31
31
|
{/* Brand */}
|
|
32
32
|
<div className="flex justify-center">
|
|
@@ -35,11 +35,7 @@ export function Footer({
|
|
|
35
35
|
className: 'flex items-center gap-2',
|
|
36
36
|
children: (
|
|
37
37
|
<>
|
|
38
|
-
{brand.logo
|
|
39
|
-
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-foreground/10 text-foreground">
|
|
40
|
-
{brand.logo}
|
|
41
|
-
</span>
|
|
42
|
-
)}
|
|
38
|
+
{brand.logo}
|
|
43
39
|
<span className="text-lg font-semibold text-foreground">{brand.name}</span>
|
|
44
40
|
</>
|
|
45
41
|
),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
|
+
import { FeatureCard } from '../components/FeatureCard';
|
|
6
|
+
import { SectionTitle } from './SectionTitle';
|
|
7
|
+
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
8
|
+
import { FadeIn } from '../motion/FadeIn';
|
|
9
|
+
|
|
10
|
+
export interface GroupedFeatureGroup {
|
|
11
|
+
label: string;
|
|
12
|
+
features: Array<{ icon: LucideIcon; title: string; description: string; href?: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GroupedFeaturesSectionProps {
|
|
16
|
+
badge?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
groups: GroupedFeatureGroup[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function GroupedFeaturesSection({
|
|
23
|
+
badge = 'Features',
|
|
24
|
+
title = 'Everything you need to ship',
|
|
25
|
+
description,
|
|
26
|
+
groups,
|
|
27
|
+
}: GroupedFeaturesSectionProps) {
|
|
28
|
+
const mode = useUIVariant();
|
|
29
|
+
const sectionClasses = cn(
|
|
30
|
+
'overflow-hidden bg-card',
|
|
31
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
32
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
33
|
+
!mode && 'rounded-2xl border',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section className={sectionClasses}>
|
|
38
|
+
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
39
|
+
<div className="mx-auto max-w-4xl">
|
|
40
|
+
<SectionTitle title={title} description={description} badge={badge} />
|
|
41
|
+
|
|
42
|
+
<div className="mt-14 space-y-14">
|
|
43
|
+
{groups.map((group, i) => (
|
|
44
|
+
<div key={group.label}>
|
|
45
|
+
<FadeIn direction="up" delay={i * 60}>
|
|
46
|
+
<div className="mb-6 flex items-center gap-3">
|
|
47
|
+
<span className="text-sm font-semibold tracking-wide text-muted-foreground uppercase">
|
|
48
|
+
{group.label}
|
|
49
|
+
</span>
|
|
50
|
+
<div className="h-px flex-1 bg-border" />
|
|
51
|
+
</div>
|
|
52
|
+
</FadeIn>
|
|
53
|
+
<StaggerChildren className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
54
|
+
{group.features.map((feature) => (
|
|
55
|
+
<FeatureCard
|
|
56
|
+
key={feature.title}
|
|
57
|
+
icon={feature.icon}
|
|
58
|
+
title={feature.title}
|
|
59
|
+
description={feature.description}
|
|
60
|
+
href={feature.href}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</StaggerChildren>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/marketing/Navbar.tsx
CHANGED
|
@@ -40,11 +40,7 @@ export function Navbar({
|
|
|
40
40
|
href: brandHref,
|
|
41
41
|
children: (
|
|
42
42
|
<span className="flex items-center gap-2 font-semibold">
|
|
43
|
-
{brand.logo
|
|
44
|
-
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
|
45
|
-
{brand.logo}
|
|
46
|
-
</span>
|
|
47
|
-
)}
|
|
43
|
+
{brand.logo}
|
|
48
44
|
<span>{brand.name}</span>
|
|
49
45
|
</span>
|
|
50
46
|
),
|
|
@@ -100,11 +96,7 @@ export function Navbar({
|
|
|
100
96
|
href: brandHref,
|
|
101
97
|
children: (
|
|
102
98
|
<span className="flex items-center gap-2 font-semibold">
|
|
103
|
-
{brand.logo
|
|
104
|
-
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
105
|
-
{brand.logo}
|
|
106
|
-
</span>
|
|
107
|
-
)}
|
|
99
|
+
{brand.logo}
|
|
108
100
|
<span>{brand.name}</span>
|
|
109
101
|
</span>
|
|
110
102
|
),
|
|
@@ -57,13 +57,14 @@ export function NewsletterSection({
|
|
|
57
57
|
<div className="relative px-6 py-14 sm:px-10 sm:py-20">
|
|
58
58
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.1),transparent_60%)]" />
|
|
59
59
|
<div className="relative mx-auto max-w-xl text-center">
|
|
60
|
-
<div className="mb-4
|
|
61
|
-
<
|
|
60
|
+
<div className="mb-4 flex items-center justify-center gap-2">
|
|
61
|
+
<div className="inline-flex h-8 w-8 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
62
|
+
<Mail className="size-4" />
|
|
63
|
+
</div>
|
|
64
|
+
<Badge variant="secondary">
|
|
65
|
+
{badge}
|
|
66
|
+
</Badge>
|
|
62
67
|
</div>
|
|
63
|
-
|
|
64
|
-
<Badge variant="secondary" className="mb-3">
|
|
65
|
-
{badge}
|
|
66
|
-
</Badge>
|
|
67
68
|
<h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">{title}</h2>
|
|
68
69
|
<p className="mx-auto mt-4 max-w-sm text-pretty text-muted-foreground">{description}</p>
|
|
69
70
|
|
|
@@ -4,6 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { Badge, Button, cn, useUIVariant } from '@olwiba/cn';
|
|
5
5
|
import { PricingCard, type PricingFeature } from '../components/PricingCard';
|
|
6
6
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
7
|
+
import { CountdownTimer } from '../motion/CountdownTimer';
|
|
7
8
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
8
9
|
|
|
9
10
|
export interface PricingPlan {
|
|
@@ -13,18 +14,25 @@ export interface PricingPlan {
|
|
|
13
14
|
description: string;
|
|
14
15
|
cta: string;
|
|
15
16
|
highlighted?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
16
18
|
features: PricingFeature[];
|
|
19
|
+
/** Overrides computed price display (e.g. "$?" for community/reach-out tiers). */
|
|
20
|
+
priceDisplay?: string;
|
|
21
|
+
/** Overrides computed period display. */
|
|
22
|
+
periodDisplay?: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export interface PricingSectionProps {
|
|
20
26
|
title?: string;
|
|
21
27
|
description?: string;
|
|
22
|
-
badge?:
|
|
28
|
+
badge?: React.ReactNode;
|
|
23
29
|
plans: PricingPlan[];
|
|
24
30
|
saveBadge?: string;
|
|
25
31
|
isAuthenticated?: boolean;
|
|
26
32
|
renderLink?: AppShellRenderLink;
|
|
27
33
|
footnote?: string;
|
|
34
|
+
mode?: 'subscription' | 'one-time';
|
|
35
|
+
foundingDeadline?: string;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
|
|
@@ -40,15 +48,18 @@ export function PricingSection({
|
|
|
40
48
|
isAuthenticated,
|
|
41
49
|
renderLink = defaultRenderLink,
|
|
42
50
|
footnote,
|
|
51
|
+
mode = 'subscription',
|
|
52
|
+
foundingDeadline,
|
|
43
53
|
}: PricingSectionProps) {
|
|
44
54
|
const [annual, setAnnual] = React.useState(false);
|
|
45
|
-
const
|
|
55
|
+
const isOneTime = mode === 'one-time';
|
|
56
|
+
const uiMode = useUIVariant();
|
|
46
57
|
const sectionClasses = cn(
|
|
47
58
|
'overflow-hidden bg-card',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
!
|
|
51
|
-
)
|
|
59
|
+
uiMode === 'smooth' && 'rounded-3xl border',
|
|
60
|
+
uiMode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
61
|
+
!uiMode && 'rounded-2xl border',
|
|
62
|
+
);
|
|
52
63
|
|
|
53
64
|
return (
|
|
54
65
|
<section className={sectionClasses}>
|
|
@@ -57,7 +68,9 @@ export function PricingSection({
|
|
|
57
68
|
{/* Header */}
|
|
58
69
|
<div className="text-center">
|
|
59
70
|
{badge && (
|
|
60
|
-
|
|
71
|
+
typeof badge === 'string'
|
|
72
|
+
? <Badge variant="secondary" className="mb-4">{badge}</Badge>
|
|
73
|
+
: <div className="mb-4">{badge}</div>
|
|
61
74
|
)}
|
|
62
75
|
<h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
63
76
|
{title}
|
|
@@ -68,51 +81,65 @@ export function PricingSection({
|
|
|
68
81
|
</p>
|
|
69
82
|
)}
|
|
70
83
|
|
|
71
|
-
{/* Billing toggle */}
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
{/* Billing toggle — subscription mode only */}
|
|
85
|
+
{!isOneTime && (
|
|
86
|
+
<div className="mt-6 inline-flex items-center gap-3 rounded-full border bg-muted p-1">
|
|
87
|
+
<Button
|
|
88
|
+
variant="ghost"
|
|
89
|
+
size="sm"
|
|
90
|
+
onClick={() => setAnnual(false)}
|
|
91
|
+
className={cn(
|
|
92
|
+
'rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
|
|
93
|
+
!annual ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
Monthly
|
|
97
|
+
</Button>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="sm"
|
|
101
|
+
onClick={() => setAnnual(true)}
|
|
102
|
+
className={cn(
|
|
103
|
+
'flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
|
|
104
|
+
annual ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
Annual
|
|
108
|
+
{saveBadge && (
|
|
109
|
+
<Badge variant="secondary" className="text-xs">{saveBadge}</Badge>
|
|
110
|
+
)}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
99
114
|
</div>
|
|
100
115
|
|
|
101
116
|
{/* Plan cards */}
|
|
102
117
|
<StaggerChildren className="mt-10 grid gap-4 lg:grid-cols-3">
|
|
103
118
|
{plans.map((plan) => {
|
|
104
|
-
const
|
|
119
|
+
const rawPrice = isOneTime ? plan.monthly : (annual ? plan.annual : plan.monthly);
|
|
120
|
+
const price = plan.priceDisplay ?? `$${rawPrice}`;
|
|
121
|
+
const period = plan.periodDisplay ?? (isOneTime ? 'one-time' : (rawPrice > 0 ? '/mo' : ''));
|
|
122
|
+
const badge = plan.highlighted && foundingDeadline
|
|
123
|
+
? (
|
|
124
|
+
<span className="inline-flex items-center rounded-full border bg-secondary px-2.5 py-0.5 text-xs font-semibold text-secondary-foreground">
|
|
125
|
+
<CountdownTimer deadline={foundingDeadline} compact />
|
|
126
|
+
</span>
|
|
127
|
+
)
|
|
128
|
+
: plan.highlighted
|
|
129
|
+
? 'Founding member'
|
|
130
|
+
: undefined;
|
|
105
131
|
return (
|
|
106
132
|
<PricingCard
|
|
107
133
|
key={plan.name}
|
|
108
134
|
name={plan.name}
|
|
109
|
-
price={
|
|
110
|
-
period={
|
|
135
|
+
price={price}
|
|
136
|
+
period={period}
|
|
111
137
|
description={plan.description}
|
|
112
138
|
features={plan.features}
|
|
113
139
|
cta={plan.cta}
|
|
114
140
|
highlighted={plan.highlighted}
|
|
115
|
-
|
|
141
|
+
disabled={plan.disabled}
|
|
142
|
+
badge={badge}
|
|
116
143
|
/>
|
|
117
144
|
);
|
|
118
145
|
})}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
4
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
|
+
import { SectionTitle } from './SectionTitle';
|
|
6
|
+
import { FadeIn } from '../motion/FadeIn';
|
|
7
|
+
|
|
8
|
+
export interface QualificationItem {
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QualificationColumn {
|
|
13
|
+
heading: string;
|
|
14
|
+
variant: 'positive' | 'negative';
|
|
15
|
+
items: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QualificationSectionProps {
|
|
19
|
+
badge?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
columns: [QualificationColumn, QualificationColumn];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function QualificationSection({
|
|
26
|
+
badge = 'Who it is for',
|
|
27
|
+
title = 'Is this for you?',
|
|
28
|
+
description,
|
|
29
|
+
columns,
|
|
30
|
+
}: QualificationSectionProps) {
|
|
31
|
+
const mode = useUIVariant();
|
|
32
|
+
const sectionClasses = cn(
|
|
33
|
+
'overflow-hidden bg-card',
|
|
34
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
35
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
36
|
+
!mode && 'rounded-2xl border',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section className={sectionClasses}>
|
|
41
|
+
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
42
|
+
<div className="mx-auto max-w-4xl">
|
|
43
|
+
<SectionTitle badge={badge} title={title} description={description} />
|
|
44
|
+
|
|
45
|
+
<FadeIn direction="up" className="mt-12 grid gap-6 sm:grid-cols-2">
|
|
46
|
+
{columns.map((col) => {
|
|
47
|
+
const isPositive = col.variant === 'positive';
|
|
48
|
+
const Icon = isPositive ? Check : X;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={col.heading}
|
|
53
|
+
className={cn(
|
|
54
|
+
'flex flex-col gap-4 rounded-2xl border p-6',
|
|
55
|
+
isPositive ? 'border-border bg-muted/30' : 'border-border bg-muted/10',
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<h3 className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
59
|
+
{col.heading}
|
|
60
|
+
</h3>
|
|
61
|
+
<ul className="flex flex-col gap-3">
|
|
62
|
+
{col.items.map((item) => (
|
|
63
|
+
<li key={item} className="flex items-start gap-3">
|
|
64
|
+
<span
|
|
65
|
+
className={cn(
|
|
66
|
+
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full',
|
|
67
|
+
isPositive
|
|
68
|
+
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
|
|
69
|
+
: 'bg-rose-500/15 text-rose-600 dark:text-rose-400',
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<Icon className="size-3" />
|
|
73
|
+
</span>
|
|
74
|
+
<span className="text-sm leading-relaxed text-foreground/80">{item}</span>
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</FadeIn>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
4
|
+
import { SectionTitle } from './SectionTitle';
|
|
5
|
+
import { FadeIn } from '../motion/FadeIn';
|
|
6
|
+
|
|
7
|
+
export interface StepItem {
|
|
8
|
+
emoji?: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StepsSectionProps {
|
|
14
|
+
badge?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
steps: StepItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function StepsSection({
|
|
21
|
+
badge = 'How it works',
|
|
22
|
+
title = 'From zero to shipped',
|
|
23
|
+
description,
|
|
24
|
+
steps,
|
|
25
|
+
}: StepsSectionProps) {
|
|
26
|
+
const mode = useUIVariant();
|
|
27
|
+
const sectionClasses = cn(
|
|
28
|
+
'overflow-hidden bg-card',
|
|
29
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
30
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
31
|
+
!mode && 'rounded-2xl border',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section className={sectionClasses}>
|
|
36
|
+
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
37
|
+
<div className="mx-auto max-w-4xl">
|
|
38
|
+
<SectionTitle badge={badge} title={title} description={description} />
|
|
39
|
+
|
|
40
|
+
<div className="mt-12">
|
|
41
|
+
{/* Desktop: horizontal row */}
|
|
42
|
+
<div className="hidden sm:grid" style={{ gridTemplateColumns: `repeat(${steps.length}, 1fr)` }}>
|
|
43
|
+
{steps.map((step, i) => (
|
|
44
|
+
<FadeIn key={step.title} direction="up" delay={i * 80}>
|
|
45
|
+
<div className="relative flex flex-col items-center text-center px-4">
|
|
46
|
+
{/* Dashed connector to next step */}
|
|
47
|
+
{i < steps.length - 1 && (
|
|
48
|
+
<div
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className="absolute top-6 left-1/2 w-full border-t border-dashed border-border"
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
{/* Emoji badge (or fallback number) */}
|
|
54
|
+
<div className="relative z-10 mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted text-2xl">
|
|
55
|
+
{step.emoji ?? (
|
|
56
|
+
<span className="text-sm font-semibold text-primary">{i + 1}</span>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
<h3 className="font-semibold text-foreground">{step.title}</h3>
|
|
60
|
+
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{step.description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</FadeIn>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Mobile: vertical timeline */}
|
|
67
|
+
<div className="flex flex-col gap-0 sm:hidden">
|
|
68
|
+
{steps.map((step, i) => (
|
|
69
|
+
<FadeIn key={step.title} direction="up" delay={i * 80}>
|
|
70
|
+
<div className="relative flex gap-4 pb-8 last:pb-0">
|
|
71
|
+
{/* Vertical connector */}
|
|
72
|
+
{i < steps.length - 1 && (
|
|
73
|
+
<div
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
className="absolute left-[23px] top-14 bottom-0 w-px border-l border-dashed border-border"
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
{/* Emoji badge (or fallback number) */}
|
|
79
|
+
<div className="relative z-10 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-muted text-2xl">
|
|
80
|
+
{step.emoji ?? (
|
|
81
|
+
<span className="text-sm font-semibold text-primary">{i + 1}</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="pt-3">
|
|
85
|
+
<h3 className="font-semibold text-foreground">{step.title}</h3>
|
|
86
|
+
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{step.description}</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</FadeIn>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -3,7 +3,20 @@
|
|
|
3
3
|
import { Github, Linkedin, Twitter } from 'lucide-react';
|
|
4
4
|
import { Avatar, AvatarFallback, AvatarImage, Badge } from '@olwiba/cn';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export interface TeamMember {
|
|
7
|
+
name: string;
|
|
8
|
+
role: string;
|
|
9
|
+
bio: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
initials: string;
|
|
12
|
+
social?: {
|
|
13
|
+
twitter?: string | null;
|
|
14
|
+
github?: string | null;
|
|
15
|
+
linkedin?: string | null;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultTeam: TeamMember[] = [
|
|
7
20
|
{
|
|
8
21
|
name: 'Olivia Reed',
|
|
9
22
|
role: 'Co-Founder & CEO',
|
|
@@ -28,49 +41,37 @@ const team = [
|
|
|
28
41
|
initials: 'PN',
|
|
29
42
|
social: { twitter: '#', github: null, linkedin: '#' },
|
|
30
43
|
},
|
|
31
|
-
{
|
|
32
|
-
name: 'Daniel Frost',
|
|
33
|
-
role: 'Senior Engineer',
|
|
34
|
-
bio: 'Accessibility and performance specialist. If it doesn\'t work with a keyboard, it\'s broken.',
|
|
35
|
-
avatar: 'https://ui.shadcn.com/avatars/04.png',
|
|
36
|
-
initials: 'DF',
|
|
37
|
-
social: { twitter: null, github: '#', linkedin: '#' },
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: 'Aisha Okafor',
|
|
41
|
-
role: 'Developer Relations',
|
|
42
|
-
bio: 'Writes docs, builds examples, and makes sure every engineer can ship their first block in under an hour.',
|
|
43
|
-
avatar: 'https://ui.shadcn.com/avatars/05.png',
|
|
44
|
-
initials: 'AO',
|
|
45
|
-
social: { twitter: '#', github: '#', linkedin: '#' },
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'Tom Halvorsen',
|
|
49
|
-
role: 'Growth',
|
|
50
|
-
bio: 'Turns happy users into loud advocates. Runs the community and keeps the changelog sharp.',
|
|
51
|
-
avatar: 'https://ui.shadcn.com/avatars/06.png',
|
|
52
|
-
initials: 'TH',
|
|
53
|
-
social: { twitter: '#', github: null, linkedin: '#' },
|
|
54
|
-
},
|
|
55
44
|
];
|
|
56
45
|
|
|
57
|
-
export
|
|
46
|
+
export interface TeamSectionProps {
|
|
47
|
+
members?: TeamMember[];
|
|
48
|
+
badge?: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function TeamSection({
|
|
54
|
+
members = defaultTeam,
|
|
55
|
+
badge = 'Team',
|
|
56
|
+
title = 'The people behind Olwiba',
|
|
57
|
+
description = 'A small team with deep experience in design systems, open source, and developer tooling.',
|
|
58
|
+
}: TeamSectionProps) {
|
|
58
59
|
return (
|
|
59
60
|
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
60
61
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
61
62
|
<div className="mx-auto max-w-5xl">
|
|
62
63
|
<div className="text-center">
|
|
63
|
-
<Badge variant="secondary" className="mb-4">
|
|
64
|
+
<Badge variant="secondary" className="mb-4">{badge}</Badge>
|
|
64
65
|
<h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
65
|
-
|
|
66
|
+
{title}
|
|
66
67
|
</h2>
|
|
67
68
|
<p className="mx-auto mt-4 max-w-xl text-pretty text-muted-foreground">
|
|
68
|
-
|
|
69
|
+
{description}
|
|
69
70
|
</p>
|
|
70
71
|
</div>
|
|
71
72
|
|
|
72
73
|
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
73
|
-
{
|
|
74
|
+
{members.map((member) => (
|
|
74
75
|
<div key={member.name} className="rounded-2xl border bg-muted/40 p-5">
|
|
75
76
|
<div className="flex items-start gap-4">
|
|
76
77
|
<Avatar className="size-12 rounded-xl">
|
|
@@ -83,23 +84,25 @@ export function TeamSection() {
|
|
|
83
84
|
</div>
|
|
84
85
|
</div>
|
|
85
86
|
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">{member.bio}</p>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<Twitter className="
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
87
|
+
{member.social && (
|
|
88
|
+
<div className="mt-4 flex items-center gap-2">
|
|
89
|
+
{member.social.twitter && (
|
|
90
|
+
<a href={member.social.twitter} aria-label="Twitter" className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
|
|
91
|
+
<Twitter className="size-3.5" />
|
|
92
|
+
</a>
|
|
93
|
+
)}
|
|
94
|
+
{member.social.github && (
|
|
95
|
+
<a href={member.social.github} aria-label="GitHub" className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
|
|
96
|
+
<Github className="size-3.5" />
|
|
97
|
+
</a>
|
|
98
|
+
)}
|
|
99
|
+
{member.social.linkedin && (
|
|
100
|
+
<a href={member.social.linkedin} aria-label="LinkedIn" className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
|
|
101
|
+
<Linkedin className="size-3.5" />
|
|
102
|
+
</a>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
103
106
|
</div>
|
|
104
107
|
))}
|
|
105
108
|
</div>
|