@olwiba/ui 0.1.5 → 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 +2 -2
- 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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
|
+
import { SectionTitle } from './SectionTitle';
|
|
6
|
+
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
7
|
+
|
|
8
|
+
export interface TechStackItem {
|
|
9
|
+
icon: LucideIcon;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TechStackSectionProps {
|
|
16
|
+
badge?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
items: TechStackItem[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function TechStackSection({
|
|
23
|
+
badge = 'Built on',
|
|
24
|
+
title = 'The stack',
|
|
25
|
+
description,
|
|
26
|
+
items,
|
|
27
|
+
}: TechStackSectionProps) {
|
|
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 badge={badge} title={title} description={description} />
|
|
41
|
+
|
|
42
|
+
<StaggerChildren className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
43
|
+
{items.map((item) => {
|
|
44
|
+
const Icon = item.icon;
|
|
45
|
+
const card = (
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
'group flex flex-col gap-3 border bg-background p-5 transition-all duration-200',
|
|
49
|
+
mode === 'smooth'
|
|
50
|
+
? 'rounded-2xl hover:-translate-y-0.5 hover:shadow-md'
|
|
51
|
+
: 'rounded-xl hover:-translate-y-0.5 hover:shadow-md',
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
'flex h-9 w-9 items-center justify-center bg-muted text-muted-foreground transition-colors group-hover:bg-primary/10 group-hover:text-primary',
|
|
57
|
+
mode === 'smooth' ? 'rounded-xl' : 'rounded-lg',
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<Icon className="size-4" />
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<p className="text-sm font-semibold leading-tight">{item.name}</p>
|
|
64
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{item.description}</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (item.href) {
|
|
70
|
+
return (
|
|
71
|
+
<a key={item.name} href={item.href} className="block">
|
|
72
|
+
{card}
|
|
73
|
+
</a>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return <div key={item.name}>{card}</div>;
|
|
77
|
+
})}
|
|
78
|
+
</StaggerChildren>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
|
+
|
|
6
|
+
export interface AnimatedPillProps {
|
|
7
|
+
labels: string[];
|
|
8
|
+
interval?: number;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AnimatedPill({ labels, interval = 3800, className }: AnimatedPillProps) {
|
|
13
|
+
const pillRef = useRef<HTMLSpanElement>(null);
|
|
14
|
+
const textRef = useRef<HTMLSpanElement>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const pill = pillRef.current;
|
|
18
|
+
const text = textRef.current;
|
|
19
|
+
if (!pill || !text || labels.length < 2) return;
|
|
20
|
+
|
|
21
|
+
pill.style.width = pill.offsetWidth + 'px';
|
|
22
|
+
pill.dataset.w = String(pill.offsetWidth);
|
|
23
|
+
|
|
24
|
+
let idx = 0;
|
|
25
|
+
|
|
26
|
+
const cycle = () => {
|
|
27
|
+
idx = (idx + 1) % labels.length;
|
|
28
|
+
|
|
29
|
+
text.style.transition = 'opacity 140ms ease';
|
|
30
|
+
text.style.opacity = '0';
|
|
31
|
+
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
text.textContent = labels[idx] ?? '';
|
|
34
|
+
|
|
35
|
+
const oldW = parseFloat(pill.dataset.w ?? '0');
|
|
36
|
+
pill.style.transition = 'none';
|
|
37
|
+
pill.style.width = 'auto';
|
|
38
|
+
const newW = pill.offsetWidth;
|
|
39
|
+
|
|
40
|
+
pill.style.width = oldW + 'px';
|
|
41
|
+
void pill.offsetWidth;
|
|
42
|
+
|
|
43
|
+
pill.style.transition = 'width 280ms cubic-bezier(0.4, 0, 0.2, 1)';
|
|
44
|
+
pill.style.width = newW + 'px';
|
|
45
|
+
pill.dataset.w = String(newW);
|
|
46
|
+
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
text.style.transition = 'opacity 140ms ease';
|
|
49
|
+
text.style.opacity = '1';
|
|
50
|
+
}, 260);
|
|
51
|
+
}, 150);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const id = setInterval(cycle, interval);
|
|
55
|
+
return () => clearInterval(id);
|
|
56
|
+
}, [labels, interval]);
|
|
57
|
+
|
|
58
|
+
if (!labels.length) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<span
|
|
62
|
+
ref={pillRef}
|
|
63
|
+
className={cn(
|
|
64
|
+
'inline-flex items-center overflow-hidden whitespace-nowrap rounded-full border bg-secondary px-2.5 py-0.5 text-xs font-semibold text-secondary-foreground',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<span ref={textRef}>{labels[0]}</span>
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
|
+
|
|
6
|
+
interface TimeLeft {
|
|
7
|
+
days: number;
|
|
8
|
+
hours: number;
|
|
9
|
+
minutes: number;
|
|
10
|
+
seconds: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getTimeLeft(deadline: string): TimeLeft | null {
|
|
14
|
+
const diff = new Date(deadline).getTime() - Date.now();
|
|
15
|
+
if (diff <= 0) return null;
|
|
16
|
+
return {
|
|
17
|
+
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
|
|
18
|
+
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
|
|
19
|
+
minutes: Math.floor((diff / 1000 / 60) % 60),
|
|
20
|
+
seconds: Math.floor((diff / 1000) % 60),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CountdownTimerProps {
|
|
25
|
+
deadline: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Compact inline mode — renders as "29d · 11h · 22m", no seconds, no label. For use inside badges. */
|
|
29
|
+
compact?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CountdownTimer({
|
|
33
|
+
deadline,
|
|
34
|
+
label = 'Founding price closes in',
|
|
35
|
+
className,
|
|
36
|
+
compact = false,
|
|
37
|
+
}: CountdownTimerProps) {
|
|
38
|
+
const [timeLeft, setTimeLeft] = React.useState<TimeLeft | null>(() => getTimeLeft(deadline));
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
setTimeLeft(getTimeLeft(deadline));
|
|
42
|
+
const id = setInterval(() => setTimeLeft(getTimeLeft(deadline)), 1000);
|
|
43
|
+
return () => clearInterval(id);
|
|
44
|
+
}, [deadline]);
|
|
45
|
+
|
|
46
|
+
if (!timeLeft) return null;
|
|
47
|
+
|
|
48
|
+
if (compact) {
|
|
49
|
+
return (
|
|
50
|
+
<span className={cn('font-mono tabular-nums', className)}>
|
|
51
|
+
{timeLeft.days}d · {String(timeLeft.hours).padStart(2, '0')}h · {String(timeLeft.minutes).padStart(2, '0')}m · {String(timeLeft.seconds).padStart(2, '0')}s
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const units = [
|
|
57
|
+
{ value: timeLeft.days, unit: 'd' },
|
|
58
|
+
{ value: timeLeft.hours, unit: 'h' },
|
|
59
|
+
{ value: timeLeft.minutes, unit: 'm' },
|
|
60
|
+
{ value: timeLeft.seconds, unit: 's' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn('flex items-center justify-center gap-2 text-sm', className)}>
|
|
65
|
+
<span className="text-muted-foreground">{label}</span>
|
|
66
|
+
<span className="flex items-center gap-1 font-mono font-semibold tabular-nums">
|
|
67
|
+
{units.map(({ value, unit }, i) => (
|
|
68
|
+
<React.Fragment key={unit}>
|
|
69
|
+
<span>
|
|
70
|
+
{String(value).padStart(2, '0')}
|
|
71
|
+
<span className="font-normal text-muted-foreground">{unit}</span>
|
|
72
|
+
</span>
|
|
73
|
+
{i < units.length - 1 && (
|
|
74
|
+
<span className="text-muted-foreground/50">·</span>
|
|
75
|
+
)}
|
|
76
|
+
</React.Fragment>
|
|
77
|
+
))}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|