@olwiba/ui 0.1.1 → 0.1.3
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 +25 -4
- package/dist/index.js +364 -129
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FeatureCard.tsx +32 -7
- package/src/components/ModeSwitchMinimal.tsx +40 -0
- package/src/components/PricingCard.tsx +29 -5
- package/src/components/TestimonialCard.tsx +30 -6
- package/src/context/OlwibaUIContext.tsx +5 -2
- package/src/index.ts +2 -0
- package/src/marketing/CarouselSection.tsx +126 -0
- package/src/marketing/CtaSection.tsx +10 -3
- package/src/marketing/FaqSection.tsx +9 -2
- package/src/marketing/FeaturesSection.tsx +9 -1
- package/src/marketing/Navbar.tsx +30 -20
- package/src/marketing/PricingSection.tsx +17 -6
- package/src/marketing/StatsSection.tsx +9 -2
- package/src/marketing/TestimonialsSection.tsx +9 -1
- package/src/motion/StaggerChildren.tsx +1 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import type { LucideIcon } from 'lucide-react';
|
|
5
5
|
import { ArrowRight } from 'lucide-react';
|
|
6
|
-
import { cn } from '@olwiba/cn';
|
|
6
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
7
7
|
|
|
8
8
|
export interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
9
|
icon: LucideIcon;
|
|
@@ -13,16 +13,28 @@ export interface FeatureCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function FeatureCard({ icon: Icon, title, description, href, className, ...props }: FeatureCardProps) {
|
|
16
|
-
const
|
|
16
|
+
const mode = useUIVariant();
|
|
17
|
+
|
|
18
|
+
const cardInner = (
|
|
17
19
|
<div
|
|
18
20
|
className={cn(
|
|
19
|
-
'group relative flex flex-col gap-4
|
|
21
|
+
'group relative flex h-full flex-col gap-4 border bg-card p-6 transition-all duration-200',
|
|
22
|
+
mode === 'playful'
|
|
23
|
+
? 'rounded-2xl rotate-[0.3deg]'
|
|
24
|
+
: mode === 'smooth'
|
|
25
|
+
? 'rounded-3xl hover:-translate-y-0.5 hover:shadow-md'
|
|
26
|
+
: 'rounded-2xl hover:-translate-y-0.5 hover:shadow-md',
|
|
20
27
|
href && 'cursor-pointer',
|
|
21
|
-
className,
|
|
28
|
+
mode !== 'playful' && className,
|
|
22
29
|
)}
|
|
23
|
-
{...props}
|
|
30
|
+
{...(mode !== 'playful' ? props : {})}
|
|
24
31
|
>
|
|
25
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
'flex h-10 w-10 items-center justify-center bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground',
|
|
35
|
+
mode === 'smooth' ? 'rounded-2xl' : 'rounded-xl',
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
26
38
|
<Icon className="size-5" />
|
|
27
39
|
</div>
|
|
28
40
|
<div className="space-y-1.5">
|
|
@@ -37,8 +49,21 @@ export function FeatureCard({ icon: Icon, title, description, href, className, .
|
|
|
37
49
|
</div>
|
|
38
50
|
);
|
|
39
51
|
|
|
52
|
+
const content =
|
|
53
|
+
mode === 'playful' ? (
|
|
54
|
+
<div className={cn('group/playful relative h-full', className)} {...props}>
|
|
55
|
+
<div
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
className="absolute inset-0 rounded-2xl bg-border transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]"
|
|
58
|
+
/>
|
|
59
|
+
{cardInner}
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
cardInner
|
|
63
|
+
);
|
|
64
|
+
|
|
40
65
|
if (href) {
|
|
41
|
-
return <a href={href} className="block">{content}</a>;
|
|
66
|
+
return <a href={href} className="block h-full">{content}</a>;
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
return content;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { LayoutGrid, Sparkles, Layers } from 'lucide-react';
|
|
5
|
+
import { Button } from '@olwiba/cn';
|
|
6
|
+
import { useOlwibaUI, type UIMode } from '../context/OlwibaUIContext';
|
|
7
|
+
|
|
8
|
+
const MODES: UIMode[] = ['default', 'playful', 'smooth'];
|
|
9
|
+
|
|
10
|
+
const MODE_ICONS: Record<UIMode, React.ReactNode> = {
|
|
11
|
+
default: <LayoutGrid className="size-4" />,
|
|
12
|
+
playful: <Sparkles className="size-4" />,
|
|
13
|
+
smooth: <Layers className="size-4" />,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MODE_LABELS: Record<UIMode, string> = {
|
|
17
|
+
default: 'Default mode',
|
|
18
|
+
playful: 'Playful mode',
|
|
19
|
+
smooth: 'Smooth mode',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ModeSwitchMinimal() {
|
|
23
|
+
const { mode, setMode } = useOlwibaUI();
|
|
24
|
+
|
|
25
|
+
const cycle = () => {
|
|
26
|
+
const idx = MODES.indexOf(mode);
|
|
27
|
+
setMode(MODES[(idx + 1) % MODES.length]);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Button variant="ghost" size="icon" onClick={cycle} className="size-8" aria-label={MODE_LABELS[mode]}>
|
|
32
|
+
<span
|
|
33
|
+
key={mode}
|
|
34
|
+
className="inline-flex size-4 items-center justify-center animate-in fade-in zoom-in-75 duration-200 motion-reduce:animate-none"
|
|
35
|
+
>
|
|
36
|
+
{MODE_ICONS[mode]}
|
|
37
|
+
</span>
|
|
38
|
+
</Button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { Check, Minus } from 'lucide-react';
|
|
5
|
-
import { Badge, Button, cn, Separator } from '@olwiba/cn';
|
|
5
|
+
import { Badge, Button, cn, Separator, useUIVariant } from '@olwiba/cn';
|
|
6
6
|
|
|
7
7
|
export interface PricingFeature {
|
|
8
8
|
label: string;
|
|
@@ -34,14 +34,21 @@ export function PricingCard({
|
|
|
34
34
|
className,
|
|
35
35
|
...props
|
|
36
36
|
}: PricingCardProps) {
|
|
37
|
-
|
|
37
|
+
const mode = useUIVariant();
|
|
38
|
+
|
|
39
|
+
const cardInner = (
|
|
38
40
|
<div
|
|
39
41
|
className={cn(
|
|
40
|
-
'relative flex flex-col
|
|
42
|
+
'relative flex flex-col border p-6',
|
|
43
|
+
mode === 'playful'
|
|
44
|
+
? 'rounded-2xl rotate-[0.3deg]'
|
|
45
|
+
: mode === 'smooth'
|
|
46
|
+
? 'rounded-3xl'
|
|
47
|
+
: 'rounded-2xl',
|
|
41
48
|
highlighted ? 'border-primary bg-primary/5 shadow-sm' : 'bg-card',
|
|
42
|
-
className,
|
|
49
|
+
mode !== 'playful' && className,
|
|
43
50
|
)}
|
|
44
|
-
{...props}
|
|
51
|
+
{...(mode !== 'playful' ? props : {})}
|
|
45
52
|
>
|
|
46
53
|
{badge && (
|
|
47
54
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
@@ -87,4 +94,21 @@ export function PricingCard({
|
|
|
87
94
|
</ul>
|
|
88
95
|
</div>
|
|
89
96
|
);
|
|
97
|
+
|
|
98
|
+
if (mode === 'playful') {
|
|
99
|
+
return (
|
|
100
|
+
<div className={cn('group/playful relative', className)} {...props}>
|
|
101
|
+
<div
|
|
102
|
+
aria-hidden="true"
|
|
103
|
+
className={cn(
|
|
104
|
+
'absolute inset-0 rounded-2xl transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]',
|
|
105
|
+
highlighted ? 'bg-primary/20' : 'bg-border',
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
108
|
+
{cardInner}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return cardInner;
|
|
90
114
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { Quote, Star } from 'lucide-react';
|
|
5
|
-
import { Avatar, AvatarFallback, AvatarImage, cn } from '@olwiba/cn';
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage, cn, useUIVariant } from '@olwiba/cn';
|
|
6
6
|
|
|
7
7
|
export interface TestimonialCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
8
|
quote: string;
|
|
@@ -25,10 +25,20 @@ export function TestimonialCard({
|
|
|
25
25
|
className,
|
|
26
26
|
...props
|
|
27
27
|
}: TestimonialCardProps) {
|
|
28
|
-
|
|
28
|
+
const mode = useUIVariant();
|
|
29
|
+
|
|
30
|
+
const cardInner = (
|
|
29
31
|
<div
|
|
30
|
-
className={cn(
|
|
31
|
-
|
|
32
|
+
className={cn(
|
|
33
|
+
'flex flex-col gap-4 border bg-card p-6',
|
|
34
|
+
mode === 'playful'
|
|
35
|
+
? 'rounded-2xl rotate-[0.3deg]'
|
|
36
|
+
: mode === 'smooth'
|
|
37
|
+
? 'rounded-3xl'
|
|
38
|
+
: 'rounded-2xl',
|
|
39
|
+
mode !== 'playful' && className,
|
|
40
|
+
)}
|
|
41
|
+
{...(mode !== 'playful' ? props : {})}
|
|
32
42
|
>
|
|
33
43
|
<Quote className="size-4 text-muted-foreground/40" />
|
|
34
44
|
{rating != null && (
|
|
@@ -48,9 +58,9 @@ export function TestimonialCard({
|
|
|
48
58
|
)}
|
|
49
59
|
<p className="flex-1 text-sm leading-relaxed">{quote}</p>
|
|
50
60
|
<div className="flex items-center gap-3">
|
|
51
|
-
<Avatar className="size-9
|
|
61
|
+
<Avatar className="size-9">
|
|
52
62
|
<AvatarImage src={avatar} alt={name} />
|
|
53
|
-
<AvatarFallback className="
|
|
63
|
+
<AvatarFallback className="text-xs">{initials ?? name.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
54
64
|
</Avatar>
|
|
55
65
|
<div>
|
|
56
66
|
<div className="text-sm font-medium leading-tight">{name}</div>
|
|
@@ -61,4 +71,18 @@ export function TestimonialCard({
|
|
|
61
71
|
</div>
|
|
62
72
|
</div>
|
|
63
73
|
);
|
|
74
|
+
|
|
75
|
+
if (mode === 'playful') {
|
|
76
|
+
return (
|
|
77
|
+
<div className={cn('group/playful relative', className)} {...props}>
|
|
78
|
+
<div
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
className="absolute inset-0 rounded-2xl bg-border transition-transform duration-200 translate-x-[5px] translate-y-[5px] -rotate-[0.5deg] group-hover/playful:-rotate-[1.5deg] group-hover/playful:translate-x-[6px] group-hover/playful:translate-y-[6px]"
|
|
81
|
+
/>
|
|
82
|
+
{cardInner}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return cardInner;
|
|
64
88
|
}
|
|
@@ -8,11 +8,13 @@ export type UIMode = 'default' | 'playful' | 'smooth';
|
|
|
8
8
|
interface OlwibaUIContextValue {
|
|
9
9
|
isMobile: boolean;
|
|
10
10
|
mode: UIMode;
|
|
11
|
+
setMode: (mode: UIMode) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const OlwibaUIContext = React.createContext<OlwibaUIContextValue>({
|
|
14
15
|
isMobile: false,
|
|
15
16
|
mode: 'default',
|
|
17
|
+
setMode: () => {},
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
export interface OlwibaUIProviderProps {
|
|
@@ -33,14 +35,15 @@ export interface OlwibaUIProviderProps {
|
|
|
33
35
|
mode?: UIMode;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode = 'default' }: OlwibaUIProviderProps) {
|
|
38
|
+
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode: initialMode = 'default' }: OlwibaUIProviderProps) {
|
|
37
39
|
const detectedMobile = useIsMobile();
|
|
38
40
|
const isMobile = isMobileProp ?? detectedMobile;
|
|
41
|
+
const [mode, setMode] = React.useState<UIMode>(initialMode);
|
|
39
42
|
|
|
40
43
|
const variant = mode !== 'default' ? (mode as UIVariant) : undefined;
|
|
41
44
|
|
|
42
45
|
return (
|
|
43
|
-
<OlwibaUIContext.Provider value={{ isMobile, mode }}>
|
|
46
|
+
<OlwibaUIContext.Provider value={{ isMobile, mode, setMode }}>
|
|
44
47
|
<UIVariantProvider mode={variant}>
|
|
45
48
|
{children}
|
|
46
49
|
</UIVariantProvider>
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
|
|
55
55
|
export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
|
|
56
56
|
export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
|
|
57
|
+
export { CarouselSection, type CarouselSectionProps } from './marketing/CarouselSection';
|
|
57
58
|
export { CtaSection, type CtaSectionProps } from './marketing/CtaSection';
|
|
58
59
|
export { PricingSection, type PricingSectionProps, type PricingPlan } from './marketing/PricingSection';
|
|
59
60
|
export { TestimonialsSection, type TestimonialsSectionProps } from './marketing/TestimonialsSection';
|
|
@@ -101,6 +102,7 @@ export { FullPageSpinner } from './components/Spinner';
|
|
|
101
102
|
export { PageHeader, type PageHeaderProps, type PageHeaderBreadcrumb, type PageHeaderBackButton } from './components/PageHeader';
|
|
102
103
|
export { Suspensed } from './components/Suspensed';
|
|
103
104
|
export { ThemeSwitchMinimal } from './components/ThemeSwitchMinimal';
|
|
105
|
+
export { ModeSwitchMinimal } from './components/ModeSwitchMinimal';
|
|
104
106
|
export { ThemeColorUpdater } from './components/ThemeColorUpdater';
|
|
105
107
|
export { VersionBanner } from './components/VersionBanner';
|
|
106
108
|
export { DevBanner, type DevBannerProps } from './components/DevBanner';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import type { LucideIcon } from 'lucide-react';
|
|
5
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
6
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
7
|
+
import { FeatureCard } from '../components/FeatureCard';
|
|
8
|
+
import { SectionTitle } from './SectionTitle';
|
|
9
|
+
import { FadeIn } from '../motion/FadeIn';
|
|
10
|
+
|
|
11
|
+
export interface CarouselSectionProps {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
badge?: string;
|
|
15
|
+
features: Array<{ icon: LucideIcon; title: string; description: string; href?: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Horizontally scrolling card carousel for feature-style content.
|
|
20
|
+
* Scroll-snap based with prev/next controls — no carousel dependency.
|
|
21
|
+
*/
|
|
22
|
+
export function CarouselSection({
|
|
23
|
+
title = 'Explore the platform',
|
|
24
|
+
description,
|
|
25
|
+
badge = 'Features',
|
|
26
|
+
features,
|
|
27
|
+
}: CarouselSectionProps) {
|
|
28
|
+
const mode = useUIVariant();
|
|
29
|
+
const trackRef = React.useRef<HTMLDivElement>(null);
|
|
30
|
+
const [canPrev, setCanPrev] = React.useState(false);
|
|
31
|
+
const [canNext, setCanNext] = React.useState(true);
|
|
32
|
+
|
|
33
|
+
const updateScrollState = React.useCallback(() => {
|
|
34
|
+
const track = trackRef.current;
|
|
35
|
+
if (!track) return;
|
|
36
|
+
setCanPrev(track.scrollLeft > 8);
|
|
37
|
+
setCanNext(track.scrollLeft < track.scrollWidth - track.clientWidth - 8);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const track = trackRef.current;
|
|
42
|
+
if (!track) return;
|
|
43
|
+
updateScrollState();
|
|
44
|
+
track.addEventListener('scroll', updateScrollState, { passive: true });
|
|
45
|
+
const observer = new ResizeObserver(updateScrollState);
|
|
46
|
+
observer.observe(track);
|
|
47
|
+
return () => {
|
|
48
|
+
track.removeEventListener('scroll', updateScrollState);
|
|
49
|
+
observer.disconnect();
|
|
50
|
+
};
|
|
51
|
+
}, [updateScrollState]);
|
|
52
|
+
|
|
53
|
+
const scrollByCard = (direction: 1 | -1) => {
|
|
54
|
+
const track = trackRef.current;
|
|
55
|
+
if (!track) return;
|
|
56
|
+
const card = track.querySelector<HTMLElement>('[data-carousel-item]');
|
|
57
|
+
const step = card ? card.offsetWidth + 24 : track.clientWidth * 0.8;
|
|
58
|
+
track.scrollBy({ left: direction * step, behavior: 'smooth' });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sectionClasses = cn(
|
|
62
|
+
'overflow-hidden bg-card',
|
|
63
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
64
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
65
|
+
!mode && 'rounded-2xl border',
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const controlClasses = cn(
|
|
69
|
+
'flex size-9 items-center justify-center border bg-background text-foreground transition-colors',
|
|
70
|
+
'hover:bg-muted disabled:opacity-40 disabled:hover:bg-background',
|
|
71
|
+
mode === 'smooth' ? 'rounded-2xl' : 'rounded-xl',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<section className={sectionClasses}>
|
|
76
|
+
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
77
|
+
<div className="mx-auto max-w-5xl">
|
|
78
|
+
<SectionTitle title={title} description={description} badge={badge} />
|
|
79
|
+
|
|
80
|
+
<FadeIn direction="up">
|
|
81
|
+
<div className="mt-10 flex items-center justify-end gap-2">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
aria-label="Previous"
|
|
85
|
+
className={controlClasses}
|
|
86
|
+
onClick={() => scrollByCard(-1)}
|
|
87
|
+
disabled={!canPrev}
|
|
88
|
+
>
|
|
89
|
+
<ChevronLeft className="size-4" />
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-label="Next"
|
|
94
|
+
className={controlClasses}
|
|
95
|
+
onClick={() => scrollByCard(1)}
|
|
96
|
+
disabled={!canNext}
|
|
97
|
+
>
|
|
98
|
+
<ChevronRight className="size-4" />
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
ref={trackRef}
|
|
104
|
+
className="mt-4 flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
105
|
+
>
|
|
106
|
+
{features.map((feature) => (
|
|
107
|
+
<div
|
|
108
|
+
key={feature.title}
|
|
109
|
+
data-carousel-item
|
|
110
|
+
className="w-[280px] shrink-0 snap-start sm:w-[320px]"
|
|
111
|
+
>
|
|
112
|
+
<FeatureCard
|
|
113
|
+
icon={feature.icon}
|
|
114
|
+
title={feature.title}
|
|
115
|
+
description={feature.description}
|
|
116
|
+
href={feature.href}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</FadeIn>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ArrowRight, Sparkles } from 'lucide-react';
|
|
4
|
-
import { Button } from '@olwiba/cn';
|
|
4
|
+
import { Button, cn, useUIVariant } from '@olwiba/cn';
|
|
5
5
|
import { FadeIn } from '../motion/FadeIn';
|
|
6
6
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
7
7
|
|
|
@@ -26,13 +26,20 @@ export function CtaSection({
|
|
|
26
26
|
footnote,
|
|
27
27
|
renderLink = defaultRenderLink,
|
|
28
28
|
}: CtaSectionProps) {
|
|
29
|
+
const mode = useUIVariant()
|
|
30
|
+
const sectionClasses = cn(
|
|
31
|
+
'overflow-hidden bg-card',
|
|
32
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
33
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
34
|
+
!mode && 'rounded-2xl border',
|
|
35
|
+
)
|
|
29
36
|
return (
|
|
30
|
-
<section className=
|
|
37
|
+
<section className={sectionClasses}>
|
|
31
38
|
<div className="relative px-6 py-16 sm:px-10 sm:py-24">
|
|
32
39
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--primary)/0.15),transparent_70%)]" />
|
|
33
40
|
<FadeIn direction="up">
|
|
34
41
|
<div className="relative mx-auto max-w-2xl text-center">
|
|
35
|
-
<div className=
|
|
42
|
+
<div className={cn('mb-4 inline-flex h-10 w-10 items-center justify-center bg-primary/10 text-primary', mode === 'smooth' ? 'rounded-3xl' : 'rounded-2xl')}>
|
|
36
43
|
<Sparkles className="size-5" />
|
|
37
44
|
</div>
|
|
38
45
|
<h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@olwiba/cn';
|
|
3
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, cn, useUIVariant } from '@olwiba/cn';
|
|
4
4
|
import { SectionTitle } from './SectionTitle';
|
|
5
5
|
import { FadeIn } from '../motion/FadeIn';
|
|
6
6
|
|
|
@@ -17,8 +17,15 @@ export function FaqSection({
|
|
|
17
17
|
badge = 'FAQ',
|
|
18
18
|
items,
|
|
19
19
|
}: FaqSectionProps) {
|
|
20
|
+
const mode = useUIVariant()
|
|
21
|
+
const sectionClasses = cn(
|
|
22
|
+
'overflow-hidden bg-card',
|
|
23
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
24
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
25
|
+
!mode && 'rounded-2xl border',
|
|
26
|
+
)
|
|
20
27
|
return (
|
|
21
|
-
<section className=
|
|
28
|
+
<section className={sectionClasses}>
|
|
22
29
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
23
30
|
<div className="mx-auto max-w-3xl">
|
|
24
31
|
<SectionTitle title={title} description={description} badge={badge} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import { cn, useUIVariant } from '@olwiba/cn';
|
|
4
5
|
import { FeatureCard } from '../components/FeatureCard';
|
|
5
6
|
import { SectionTitle } from './SectionTitle';
|
|
6
7
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
@@ -18,8 +19,15 @@ export function FeaturesSection({
|
|
|
18
19
|
badge = 'Features',
|
|
19
20
|
features,
|
|
20
21
|
}: FeaturesSectionProps) {
|
|
22
|
+
const mode = useUIVariant()
|
|
23
|
+
const sectionClasses = cn(
|
|
24
|
+
'overflow-hidden bg-card',
|
|
25
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
26
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
27
|
+
!mode && 'rounded-2xl border',
|
|
28
|
+
)
|
|
21
29
|
return (
|
|
22
|
-
<section className=
|
|
30
|
+
<section className={sectionClasses}>
|
|
23
31
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
24
32
|
<div className="mx-auto max-w-4xl">
|
|
25
33
|
<SectionTitle title={title} description={description} badge={badge} />
|
package/src/marketing/Navbar.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import * as React from 'react';
|
|
|
4
4
|
import { Menu, X } from 'lucide-react';
|
|
5
5
|
import { Button, Separator, Sheet, SheetContent, SheetTrigger } from '@olwiba/cn';
|
|
6
6
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
7
|
-
import { ThemeSwitchMinimal } from '../components/ThemeSwitchMinimal';
|
|
8
7
|
|
|
9
8
|
export interface NavbarProps {
|
|
10
9
|
brand: { name: string; logo?: React.ReactNode; href?: string };
|
|
@@ -13,7 +12,8 @@ export interface NavbarProps {
|
|
|
13
12
|
primary?: { label: string; href: string };
|
|
14
13
|
secondary?: { label: string; href: string };
|
|
15
14
|
};
|
|
16
|
-
|
|
15
|
+
/** Icon-button controls rendered right-of-nav, left-of-CTA. Pass e.g. ModeSwitchMinimal, ThemeSwitchMinimal. */
|
|
16
|
+
controls?: React.ReactNode[];
|
|
17
17
|
renderLink?: AppShellRenderLink;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -25,11 +25,12 @@ export function Navbar({
|
|
|
25
25
|
brand,
|
|
26
26
|
navLinks,
|
|
27
27
|
cta,
|
|
28
|
-
|
|
28
|
+
controls,
|
|
29
29
|
renderLink = defaultRenderLink,
|
|
30
30
|
}: NavbarProps) {
|
|
31
31
|
const [open, setOpen] = React.useState(false);
|
|
32
32
|
const brandHref = brand.href ?? '/';
|
|
33
|
+
const hasRightContent = controls?.length || cta?.primary || cta?.secondary;
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
36
|
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
@@ -49,8 +50,8 @@ export function Navbar({
|
|
|
49
50
|
),
|
|
50
51
|
})}
|
|
51
52
|
|
|
52
|
-
{/* Desktop nav links — center column,
|
|
53
|
-
<div className=
|
|
53
|
+
{/* Desktop nav links — center column when right content exists, else shift right */}
|
|
54
|
+
<div className={`hidden items-center gap-1 md:flex ${!hasRightContent ? 'col-span-2 justify-end' : ''}`}>
|
|
54
55
|
{navLinks.map((link) => (
|
|
55
56
|
<span key={link.label}>
|
|
56
57
|
{renderLink({
|
|
@@ -62,20 +63,27 @@ export function Navbar({
|
|
|
62
63
|
))}
|
|
63
64
|
</div>
|
|
64
65
|
|
|
65
|
-
{/* Desktop
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
{/* Desktop right — controls + CTA */}
|
|
67
|
+
{hasRightContent && (
|
|
68
|
+
<div className="hidden items-center justify-end gap-2 md:flex">
|
|
69
|
+
{controls?.map((control, i) => (
|
|
70
|
+
<React.Fragment key={i}>{control}</React.Fragment>
|
|
71
|
+
))}
|
|
72
|
+
{(cta?.secondary || cta?.primary) && controls?.length ? (
|
|
73
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
74
|
+
) : null}
|
|
75
|
+
{cta?.secondary &&
|
|
76
|
+
renderLink({
|
|
77
|
+
href: cta.secondary.href,
|
|
78
|
+
children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
|
|
79
|
+
})}
|
|
80
|
+
{cta?.primary &&
|
|
81
|
+
renderLink({
|
|
82
|
+
href: cta.primary.href,
|
|
83
|
+
children: <Button size="sm">{cta.primary.label}</Button>,
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
79
87
|
|
|
80
88
|
{/* Mobile drawer — right column on small screens */}
|
|
81
89
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
@@ -103,7 +111,9 @@ export function Navbar({
|
|
|
103
111
|
})}
|
|
104
112
|
</span>
|
|
105
113
|
<div className="flex items-center gap-1">
|
|
106
|
-
{
|
|
114
|
+
{controls?.map((control, i) => (
|
|
115
|
+
<React.Fragment key={i}>{control}</React.Fragment>
|
|
116
|
+
))}
|
|
107
117
|
<Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
|
|
108
118
|
<X className="size-4" />
|
|
109
119
|
<span className="sr-only">Close menu</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { Badge, cn } from '@olwiba/cn';
|
|
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
7
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
@@ -42,9 +42,16 @@ export function PricingSection({
|
|
|
42
42
|
footnote,
|
|
43
43
|
}: PricingSectionProps) {
|
|
44
44
|
const [annual, setAnnual] = React.useState(false);
|
|
45
|
+
const mode = useUIVariant()
|
|
46
|
+
const sectionClasses = cn(
|
|
47
|
+
'overflow-hidden bg-card',
|
|
48
|
+
mode === 'smooth' && 'rounded-3xl border',
|
|
49
|
+
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
50
|
+
!mode && 'rounded-2xl border',
|
|
51
|
+
)
|
|
45
52
|
|
|
46
53
|
return (
|
|
47
|
-
<section className=
|
|
54
|
+
<section className={sectionClasses}>
|
|
48
55
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
49
56
|
<div className="mx-auto max-w-5xl">
|
|
50
57
|
{/* Header */}
|
|
@@ -63,7 +70,9 @@ export function PricingSection({
|
|
|
63
70
|
|
|
64
71
|
{/* Billing toggle */}
|
|
65
72
|
<div className="mt-6 inline-flex items-center gap-3 rounded-full border bg-muted p-1">
|
|
66
|
-
<
|
|
73
|
+
<Button
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="sm"
|
|
67
76
|
onClick={() => setAnnual(false)}
|
|
68
77
|
className={cn(
|
|
69
78
|
'rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
|
|
@@ -71,8 +80,10 @@ export function PricingSection({
|
|
|
71
80
|
)}
|
|
72
81
|
>
|
|
73
82
|
Monthly
|
|
74
|
-
</
|
|
75
|
-
<
|
|
83
|
+
</Button>
|
|
84
|
+
<Button
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="sm"
|
|
76
87
|
onClick={() => setAnnual(true)}
|
|
77
88
|
className={cn(
|
|
78
89
|
'flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
|
|
@@ -83,7 +94,7 @@ export function PricingSection({
|
|
|
83
94
|
{saveBadge && (
|
|
84
95
|
<Badge variant="secondary" className="text-xs">{saveBadge}</Badge>
|
|
85
96
|
)}
|
|
86
|
-
</
|
|
97
|
+
</Button>
|
|
87
98
|
</div>
|
|
88
99
|
</div>
|
|
89
100
|
|