@olwiba/ui 0.2.18 → 0.2.20
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 +114 -17
- package/dist/index.js +230 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/marketing/ComparisonSection.tsx +6 -8
- package/src/marketing/ContactSection.tsx +6 -8
- package/src/marketing/CtaSection.tsx +7 -7
- package/src/marketing/FaqSection.tsx +6 -8
- package/src/marketing/FeatureMarqueeSection.tsx +6 -9
- package/src/marketing/FeaturesSection.tsx +6 -8
- package/src/marketing/GroupedFeaturesSection.tsx +5 -8
- package/src/marketing/HeroSection.tsx +35 -11
- package/src/marketing/NewsletterSection.tsx +6 -8
- package/src/marketing/PricingSection.tsx +12 -8
- package/src/marketing/QualificationSection.tsx +6 -8
- package/src/marketing/StatsSection.tsx +6 -8
- package/src/marketing/StepsSection.tsx +6 -8
- package/src/marketing/TeamSection.tsx +6 -8
- package/src/marketing/TechStackSection.tsx +6 -6
- package/src/marketing/TestimonialsSection.tsx +5 -8
- package/src/marketing/section-surface.tsx +88 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -83,6 +83,12 @@ export { UpdateBanner, type UpdateBannerProps } from './app/UpdateBanner';
|
|
|
83
83
|
// ─── Marketing — page sections ────────────────────────────────────────────────
|
|
84
84
|
export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
|
|
85
85
|
export { marketingSectionSpacing, type MarketingSectionSpacing } from './marketing/section-spacing';
|
|
86
|
+
export {
|
|
87
|
+
MarketingSurfaceProvider,
|
|
88
|
+
useMarketingSurface,
|
|
89
|
+
useSectionSurface,
|
|
90
|
+
type MarketingSurface,
|
|
91
|
+
} from './marketing/section-surface';
|
|
86
92
|
export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
|
|
87
93
|
export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
|
|
88
94
|
export { GroupedFeaturesSection, type GroupedFeaturesSectionProps, type GroupedFeatureGroup } from './marketing/GroupedFeaturesSection';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { cn
|
|
3
|
+
import { cn } from '@olwiba/cn';
|
|
4
4
|
import { SectionTitle } from './SectionTitle';
|
|
5
5
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
6
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
6
7
|
|
|
7
8
|
export interface ComparisonColumn {
|
|
8
9
|
label: string;
|
|
@@ -17,6 +18,8 @@ export interface ComparisonSectionProps {
|
|
|
17
18
|
title?: string;
|
|
18
19
|
description?: string;
|
|
19
20
|
columns: ComparisonColumn[];
|
|
21
|
+
/** How the section sits on the page. @default 'card' */
|
|
22
|
+
surface?: MarketingSurface;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function ComparisonSection({
|
|
@@ -24,14 +27,9 @@ export function ComparisonSection({
|
|
|
24
27
|
title = 'The math is simple',
|
|
25
28
|
description,
|
|
26
29
|
columns,
|
|
30
|
+
surface,
|
|
27
31
|
}: ComparisonSectionProps) {
|
|
28
|
-
const
|
|
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
|
-
);
|
|
32
|
+
const sectionClasses = useSectionSurface(surface);
|
|
35
33
|
|
|
36
34
|
return (
|
|
37
35
|
<section className={sectionClasses}>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ChevronDown, Mail, MessageSquare, Send, type LucideIcon } from 'lucide-react';
|
|
5
|
-
import { cn, Label
|
|
5
|
+
import { cn, Label } from '@olwiba/cn';
|
|
6
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
6
7
|
import { Badge } from '../primitives/Badge';
|
|
7
8
|
import { Button } from '../primitives/Button';
|
|
8
9
|
import { Input } from '../primitives/Input';
|
|
@@ -24,6 +25,8 @@ export interface ContactSectionProps {
|
|
|
24
25
|
successDescription?: string;
|
|
25
26
|
sendAnotherLabel?: string;
|
|
26
27
|
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
28
|
+
/** How the section sits on the page. @default 'card' */
|
|
29
|
+
surface?: MarketingSurface;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const PHONE_PREFIXES = [
|
|
@@ -55,15 +58,10 @@ export function ContactSection({
|
|
|
55
58
|
successDescription = defaults.successDescription,
|
|
56
59
|
sendAnotherLabel = defaults.sendAnotherLabel,
|
|
57
60
|
onSubmit,
|
|
61
|
+
surface,
|
|
58
62
|
}: ContactSectionProps = {}) {
|
|
59
63
|
const [submitted, setSubmitted] = React.useState(false);
|
|
60
|
-
const
|
|
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
|
-
);
|
|
64
|
+
const sectionClasses = useSectionSurface(surface);
|
|
67
65
|
|
|
68
66
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
69
67
|
e.preventDefault();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ArrowRight, Rocket, Sparkles } from 'lucide-react';
|
|
5
5
|
import { cn, useUIVariant } from '@olwiba/cn';
|
|
6
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
6
7
|
import { Button } from '../primitives/Button';
|
|
7
8
|
import { FadeIn } from '../motion/FadeIn';
|
|
8
9
|
import { useIntersectionObserver } from '../hooks/use-intersection-observer';
|
|
@@ -24,6 +25,8 @@ export interface CtaSectionProps {
|
|
|
24
25
|
/** (showcase) Watermark icon rendered behind the content. @default <Rocket /> */
|
|
25
26
|
icon?: React.ReactNode;
|
|
26
27
|
renderLink?: AppShellRenderLink;
|
|
28
|
+
/** How the section sits on the page. @default 'card' */
|
|
29
|
+
surface?: MarketingSurface;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
|
|
@@ -102,14 +105,11 @@ export function CtaSection(props: CtaSectionProps) {
|
|
|
102
105
|
footnote,
|
|
103
106
|
variant = 'default',
|
|
104
107
|
renderLink = defaultRenderLink,
|
|
108
|
+
surface,
|
|
105
109
|
} = props;
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
110
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
111
|
-
!mode && 'rounded-2xl border',
|
|
112
|
-
)
|
|
110
|
+
// Still read directly: `mode` also drives the badge icon's rounding below.
|
|
111
|
+
const mode = useUIVariant();
|
|
112
|
+
const sectionClasses = useSectionSurface(surface);
|
|
113
113
|
|
|
114
114
|
if (variant === 'showcase') {
|
|
115
115
|
return <ShowcaseCta {...props} sectionClasses={sectionClasses} />;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger
|
|
3
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@olwiba/cn';
|
|
4
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
4
5
|
import { SectionTitle } from './SectionTitle';
|
|
5
6
|
import { FadeIn } from '../motion/FadeIn';
|
|
6
7
|
|
|
@@ -9,6 +10,8 @@ export interface FaqSectionProps {
|
|
|
9
10
|
description?: string;
|
|
10
11
|
badge?: string;
|
|
11
12
|
items: Array<{ question: string; answer: string }>;
|
|
13
|
+
/** How the section sits on the page. @default 'card' */
|
|
14
|
+
surface?: MarketingSurface;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function FaqSection({
|
|
@@ -16,14 +19,9 @@ export function FaqSection({
|
|
|
16
19
|
description = 'Everything you need to know before getting started.',
|
|
17
20
|
badge = 'FAQ',
|
|
18
21
|
items,
|
|
22
|
+
surface,
|
|
19
23
|
}: FaqSectionProps) {
|
|
20
|
-
const
|
|
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
|
-
)
|
|
24
|
+
const sectionClasses = useSectionSurface(surface);
|
|
27
25
|
return (
|
|
28
26
|
<section className={sectionClasses}>
|
|
29
27
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import { cn
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
5
6
|
import { SectionTitle } from './SectionTitle';
|
|
6
7
|
import { FadeIn } from '../motion/FadeIn';
|
|
7
8
|
|
|
@@ -22,6 +23,8 @@ export interface FeatureMarqueeSectionProps {
|
|
|
22
23
|
rows: FeatureMarqueeRow[];
|
|
23
24
|
speed?: 'slow' | 'normal' | 'fast';
|
|
24
25
|
pauseOnHover?: boolean;
|
|
26
|
+
/** How the section sits on the page. @default 'card' */
|
|
27
|
+
surface?: MarketingSurface;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const speedDuration = {
|
|
@@ -37,16 +40,10 @@ export function FeatureMarqueeSection({
|
|
|
37
40
|
rows,
|
|
38
41
|
speed = 'normal',
|
|
39
42
|
pauseOnHover = true,
|
|
43
|
+
surface,
|
|
40
44
|
}: FeatureMarqueeSectionProps) {
|
|
41
|
-
const mode = useUIVariant();
|
|
42
45
|
const duration = speedDuration[speed];
|
|
43
|
-
|
|
44
|
-
const sectionClasses = cn(
|
|
45
|
-
'overflow-hidden bg-card',
|
|
46
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
47
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
48
|
-
!mode && 'rounded-2xl border',
|
|
49
|
-
);
|
|
46
|
+
const sectionClasses = useSectionSurface(surface);
|
|
50
47
|
|
|
51
48
|
return (
|
|
52
49
|
<section className={sectionClasses}>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import { cn
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
5
|
import { FeatureCard } from '../components/FeatureCard';
|
|
6
6
|
import { Carousel } from '../mechanics/Carousel';
|
|
7
7
|
import { SectionTitle } from './SectionTitle';
|
|
8
8
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
9
9
|
import { FadeIn } from '../motion/FadeIn';
|
|
10
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
10
11
|
|
|
11
12
|
export interface FeaturesSectionProps {
|
|
12
13
|
title?: string;
|
|
@@ -15,6 +16,8 @@ export interface FeaturesSectionProps {
|
|
|
15
16
|
features: Array<{ icon: LucideIcon; title: string; description: string; href?: string }>;
|
|
16
17
|
/** How the feature cards are arranged. @default 'grid' */
|
|
17
18
|
layout?: 'grid' | 'carousel';
|
|
19
|
+
/** How the section sits on the page. @default 'card' */
|
|
20
|
+
surface?: MarketingSurface;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export function FeaturesSection({
|
|
@@ -23,14 +26,9 @@ export function FeaturesSection({
|
|
|
23
26
|
badge = 'Features',
|
|
24
27
|
features,
|
|
25
28
|
layout = 'grid',
|
|
29
|
+
surface,
|
|
26
30
|
}: FeaturesSectionProps) {
|
|
27
|
-
const
|
|
28
|
-
const sectionClasses = cn(
|
|
29
|
-
'overflow-hidden bg-card',
|
|
30
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
31
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
32
|
-
!mode && 'rounded-2xl border',
|
|
33
|
-
)
|
|
31
|
+
const sectionClasses = useSectionSurface(surface);
|
|
34
32
|
const cards = features.map((feature) => (
|
|
35
33
|
<FeatureCard
|
|
36
34
|
key={feature.title}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
4
|
import { FeatureCard } from '../components/FeatureCard';
|
|
6
5
|
import { SectionTitle } from './SectionTitle';
|
|
7
6
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
8
7
|
import { FadeIn } from '../motion/FadeIn';
|
|
8
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
9
9
|
|
|
10
10
|
export interface GroupedFeatureGroup {
|
|
11
11
|
label: string;
|
|
@@ -17,6 +17,8 @@ export interface GroupedFeaturesSectionProps {
|
|
|
17
17
|
title?: string;
|
|
18
18
|
description?: string;
|
|
19
19
|
groups: GroupedFeatureGroup[];
|
|
20
|
+
/** How the section sits on the page. @default 'card' */
|
|
21
|
+
surface?: MarketingSurface;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function GroupedFeaturesSection({
|
|
@@ -24,14 +26,9 @@ export function GroupedFeaturesSection({
|
|
|
24
26
|
title = 'Everything you need to ship',
|
|
25
27
|
description,
|
|
26
28
|
groups,
|
|
29
|
+
surface,
|
|
27
30
|
}: GroupedFeaturesSectionProps) {
|
|
28
|
-
const
|
|
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
|
-
);
|
|
31
|
+
const sectionClasses = useSectionSurface(surface);
|
|
35
32
|
|
|
36
33
|
return (
|
|
37
34
|
<section className={sectionClasses}>
|
|
@@ -14,8 +14,21 @@ export interface HeroSectionProps {
|
|
|
14
14
|
heading: string;
|
|
15
15
|
badge?: string;
|
|
16
16
|
description: string;
|
|
17
|
-
|
|
17
|
+
/** Ignored when `primarySlot` is given. */
|
|
18
|
+
primaryCta?: { label: string; href: string };
|
|
18
19
|
secondaryCta?: { label: string; href: string };
|
|
20
|
+
/**
|
|
21
|
+
* Replaces the primary button with your own control — a search field, a
|
|
22
|
+
* short form, an email capture.
|
|
23
|
+
*
|
|
24
|
+
* A hero whose first action *is* the product converts better than one that
|
|
25
|
+
* links to a page where the product starts. `secondaryCta` still renders
|
|
26
|
+
* beside it, so "…or see pricing" survives.
|
|
27
|
+
*
|
|
28
|
+
* Rendered full-width above the secondary CTA on small screens, since a
|
|
29
|
+
* field sharing a row with a button gets squeezed to nothing on a phone.
|
|
30
|
+
*/
|
|
31
|
+
primarySlot?: React.ReactNode;
|
|
19
32
|
heroImage?: React.ReactNode;
|
|
20
33
|
media?: 'image' | 'phone' | 'none';
|
|
21
34
|
phoneSize?: 'sm' | 'md' | 'lg';
|
|
@@ -34,6 +47,7 @@ export function HeroSection({
|
|
|
34
47
|
description,
|
|
35
48
|
primaryCta,
|
|
36
49
|
secondaryCta,
|
|
50
|
+
primarySlot,
|
|
37
51
|
heroImage,
|
|
38
52
|
media = 'image',
|
|
39
53
|
phoneSize = 'lg',
|
|
@@ -60,16 +74,26 @@ export function HeroSection({
|
|
|
60
74
|
<p className="max-w-lg text-pretty text-lg text-muted-foreground">
|
|
61
75
|
{description}
|
|
62
76
|
</p>
|
|
63
|
-
<div
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
'flex gap-3',
|
|
80
|
+
primarySlot
|
|
81
|
+
? 'flex-col items-stretch sm:flex-row sm:items-center'
|
|
82
|
+
: 'flex-wrap items-center',
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{primarySlot ?? (
|
|
86
|
+
primaryCta &&
|
|
87
|
+
renderLink({
|
|
88
|
+
href: primaryCta.href,
|
|
89
|
+
children: (
|
|
90
|
+
<Button size="lg">
|
|
91
|
+
{primaryCta.label}
|
|
92
|
+
<ArrowRight className="ml-2 size-4" />
|
|
93
|
+
</Button>
|
|
94
|
+
),
|
|
95
|
+
})
|
|
96
|
+
)}
|
|
73
97
|
{secondaryCta &&
|
|
74
98
|
renderLink({
|
|
75
99
|
href: secondaryCta.href,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ArrowRight, Mail } from 'lucide-react';
|
|
5
|
-
import { cn
|
|
5
|
+
import { cn } from '@olwiba/cn';
|
|
6
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
6
7
|
import { Badge } from '../primitives/Badge';
|
|
7
8
|
import { Button } from '../primitives/Button';
|
|
8
9
|
import { Input } from '../primitives/Input';
|
|
@@ -18,6 +19,8 @@ export interface NewsletterSectionProps {
|
|
|
18
19
|
successTitle?: string;
|
|
19
20
|
successDescription?: string;
|
|
20
21
|
onSubscribe?: (email: string) => void | Promise<void>;
|
|
22
|
+
/** How the section sits on the page. @default 'card' */
|
|
23
|
+
surface?: MarketingSurface;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
const defaults = {
|
|
@@ -44,16 +47,11 @@ export function NewsletterSection({
|
|
|
44
47
|
successTitle = defaults.successTitle,
|
|
45
48
|
successDescription = defaults.successDescription,
|
|
46
49
|
onSubscribe,
|
|
50
|
+
surface,
|
|
47
51
|
}: NewsletterSectionProps = {}) {
|
|
48
52
|
const [email, setEmail] = React.useState('');
|
|
49
53
|
const [submitted, setSubmitted] = React.useState(false);
|
|
50
|
-
const
|
|
51
|
-
const sectionClasses = cn(
|
|
52
|
-
'overflow-hidden bg-card',
|
|
53
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
54
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
55
|
-
!mode && 'rounded-2xl border',
|
|
56
|
-
);
|
|
54
|
+
const sectionClasses = useSectionSurface(surface);
|
|
57
55
|
|
|
58
56
|
async function handleSubmit(e: React.FormEvent) {
|
|
59
57
|
e.preventDefault();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { Badge, Button, cn
|
|
4
|
+
import { Badge, Button, cn } from '@olwiba/cn';
|
|
5
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
5
6
|
import { PricingCard, type PricingCardProps, type PricingFeature } from '../components/PricingCard';
|
|
6
7
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
7
8
|
import { CountdownTimer } from '../motion/CountdownTimer';
|
|
@@ -147,6 +148,14 @@ export interface PricingSectionProps {
|
|
|
147
148
|
* mode.
|
|
148
149
|
*/
|
|
149
150
|
priceEffect?: PricingCardProps['priceEffect'];
|
|
151
|
+
/**
|
|
152
|
+
* How the section sits on the page. @default 'card'
|
|
153
|
+
*
|
|
154
|
+
* Named `surface` rather than following the `mode` convention the other
|
|
155
|
+
* sections use, because `mode` on this component already means
|
|
156
|
+
* subscription-vs-one-time.
|
|
157
|
+
*/
|
|
158
|
+
surface?: MarketingSurface;
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
|
|
@@ -172,6 +181,7 @@ export function PricingSection({
|
|
|
172
181
|
onSelectPlan,
|
|
173
182
|
pendingPlanName,
|
|
174
183
|
priceEffect,
|
|
184
|
+
surface,
|
|
175
185
|
}: PricingSectionProps) {
|
|
176
186
|
const [annual, setAnnual] = React.useState(false);
|
|
177
187
|
const isOneTime = mode === 'one-time';
|
|
@@ -190,13 +200,7 @@ export function PricingSection({
|
|
|
190
200
|
);
|
|
191
201
|
// A single cadence is just a label for the price — nothing to switch between.
|
|
192
202
|
const showToggle = useCadences ? cadences!.length > 1 : !isOneTime;
|
|
193
|
-
const
|
|
194
|
-
const sectionClasses = cn(
|
|
195
|
-
'overflow-hidden bg-card',
|
|
196
|
-
uiMode === 'smooth' && 'rounded-3xl border',
|
|
197
|
-
uiMode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
198
|
-
!uiMode && 'rounded-2xl border',
|
|
199
|
-
);
|
|
203
|
+
const sectionClasses = useSectionSurface(surface);
|
|
200
204
|
|
|
201
205
|
return (
|
|
202
206
|
<section className={sectionClasses}>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Check, X } from 'lucide-react';
|
|
4
|
-
import { cn
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
5
|
import { SectionTitle } from './SectionTitle';
|
|
6
6
|
import { FadeIn } from '../motion/FadeIn';
|
|
7
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
7
8
|
|
|
8
9
|
export interface QualificationItem {
|
|
9
10
|
text: string;
|
|
@@ -20,6 +21,8 @@ export interface QualificationSectionProps {
|
|
|
20
21
|
title?: string;
|
|
21
22
|
description?: string;
|
|
22
23
|
columns: [QualificationColumn, QualificationColumn];
|
|
24
|
+
/** How the section sits on the page. @default 'card' */
|
|
25
|
+
surface?: MarketingSurface;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export function QualificationSection({
|
|
@@ -27,14 +30,9 @@ export function QualificationSection({
|
|
|
27
30
|
title = 'Is this for you?',
|
|
28
31
|
description,
|
|
29
32
|
columns,
|
|
33
|
+
surface,
|
|
30
34
|
}: QualificationSectionProps) {
|
|
31
|
-
const
|
|
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
|
-
);
|
|
35
|
+
const sectionClasses = useSectionSurface(surface);
|
|
38
36
|
|
|
39
37
|
return (
|
|
40
38
|
<section className={sectionClasses}>
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Separator
|
|
3
|
+
import { Separator } from '@olwiba/cn';
|
|
4
4
|
import { SectionTitle } from './SectionTitle';
|
|
5
5
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
6
6
|
import { CountUp } from '../motion/CountUp';
|
|
7
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
7
8
|
|
|
8
9
|
export interface StatsSectionProps {
|
|
9
10
|
title?: string;
|
|
10
11
|
description?: string;
|
|
11
12
|
badge?: string;
|
|
12
13
|
stats: Array<{ value: string; label: string; description?: string }>;
|
|
14
|
+
/** How the section sits on the page. @default 'card' */
|
|
15
|
+
surface?: MarketingSurface;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
function parseStatValue(value: string) {
|
|
@@ -32,14 +35,9 @@ export function StatsSection({
|
|
|
32
35
|
description,
|
|
33
36
|
badge = 'By the numbers',
|
|
34
37
|
stats,
|
|
38
|
+
surface,
|
|
35
39
|
}: StatsSectionProps) {
|
|
36
|
-
const
|
|
37
|
-
const sectionClasses = cn(
|
|
38
|
-
'overflow-hidden bg-card',
|
|
39
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
40
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
41
|
-
!mode && 'rounded-2xl border',
|
|
42
|
-
)
|
|
40
|
+
const sectionClasses = useSectionSurface(surface);
|
|
43
41
|
return (
|
|
44
42
|
<section className={sectionClasses}>
|
|
45
43
|
<div className="relative px-6 py-14 sm:px-10 sm:py-20">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { cn
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
5
6
|
import { SectionTitle } from './SectionTitle';
|
|
6
7
|
import { FadeIn } from '../motion/FadeIn';
|
|
7
8
|
|
|
@@ -26,6 +27,8 @@ export interface StepsSectionProps {
|
|
|
26
27
|
variant?: 'default' | 'timeline';
|
|
27
28
|
/** Timeline only — step groups with optional interstitial content; overrides `steps`. Numbering continues across groups. */
|
|
28
29
|
groups?: StepGroup[];
|
|
30
|
+
/** How the section sits on the page. @default 'card' */
|
|
31
|
+
surface?: MarketingSurface;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function TimelineGroup({
|
|
@@ -88,14 +91,9 @@ export function StepsSection({
|
|
|
88
91
|
steps,
|
|
89
92
|
variant = 'default',
|
|
90
93
|
groups,
|
|
94
|
+
surface,
|
|
91
95
|
}: StepsSectionProps) {
|
|
92
|
-
const
|
|
93
|
-
const sectionClasses = cn(
|
|
94
|
-
'overflow-hidden bg-card',
|
|
95
|
-
mode === 'smooth' && 'rounded-3xl border',
|
|
96
|
-
mode === 'playful' && 'rounded-2xl border-primary/25 border',
|
|
97
|
-
!mode && 'rounded-2xl border',
|
|
98
|
-
);
|
|
96
|
+
const sectionClasses = useSectionSurface(surface);
|
|
99
97
|
|
|
100
98
|
if (variant === 'timeline') {
|
|
101
99
|
const resolvedGroups: StepGroup[] = groups ?? (steps ? [{ steps }] : []);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Github, Linkedin, Twitter } from 'lucide-react';
|
|
4
|
-
import { Avatar, AvatarFallback, AvatarImage
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@olwiba/cn';
|
|
5
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
5
6
|
import { Badge } from '../primitives/Badge';
|
|
6
7
|
|
|
7
8
|
export interface TeamMember {
|
|
@@ -49,6 +50,8 @@ export interface TeamSectionProps {
|
|
|
49
50
|
badge?: string;
|
|
50
51
|
title?: string;
|
|
51
52
|
description?: string;
|
|
53
|
+
/** How the section sits on the page. @default 'card' */
|
|
54
|
+
surface?: MarketingSurface;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function TeamSection({
|
|
@@ -56,14 +59,9 @@ export function TeamSection({
|
|
|
56
59
|
badge = 'Team',
|
|
57
60
|
title = 'The people behind Olwiba',
|
|
58
61
|
description = 'A small team with deep experience in design systems, open source, and developer tooling.',
|
|
62
|
+
surface,
|
|
59
63
|
}: TeamSectionProps) {
|
|
60
|
-
const
|
|
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
|
-
);
|
|
64
|
+
const sectionClasses = useSectionSurface(surface);
|
|
67
65
|
return (
|
|
68
66
|
<section className={sectionClasses}>
|
|
69
67
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
@@ -4,6 +4,7 @@ import type { LucideIcon } from 'lucide-react';
|
|
|
4
4
|
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
5
|
import { SectionTitle } from './SectionTitle';
|
|
6
6
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
7
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
7
8
|
|
|
8
9
|
export interface TechStackItem {
|
|
9
10
|
icon: LucideIcon;
|
|
@@ -17,6 +18,8 @@ export interface TechStackSectionProps {
|
|
|
17
18
|
title?: string;
|
|
18
19
|
description?: string;
|
|
19
20
|
items: TechStackItem[];
|
|
21
|
+
/** How the section sits on the page. @default 'card' */
|
|
22
|
+
surface?: MarketingSurface;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function TechStackSection({
|
|
@@ -24,14 +27,11 @@ export function TechStackSection({
|
|
|
24
27
|
title = 'The stack',
|
|
25
28
|
description,
|
|
26
29
|
items,
|
|
30
|
+
surface,
|
|
27
31
|
}: TechStackSectionProps) {
|
|
32
|
+
// Still read directly: `mode` also drives the per-item tile rounding below.
|
|
28
33
|
const mode = useUIVariant();
|
|
29
|
-
const sectionClasses =
|
|
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
|
-
);
|
|
34
|
+
const sectionClasses = useSectionSurface(surface);
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<section className={sectionClasses}>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { cn, useUIVariant } from '@olwiba/cn';
|
|
4
3
|
import { TestimonialCard } from '../components/TestimonialCard';
|
|
5
4
|
import { SectionTitle } from './SectionTitle';
|
|
6
5
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
6
|
+
import { useSectionSurface, type MarketingSurface } from './section-surface';
|
|
7
7
|
|
|
8
8
|
export interface TestimonialsSectionProps {
|
|
9
9
|
title?: string;
|
|
@@ -18,6 +18,8 @@ export interface TestimonialsSectionProps {
|
|
|
18
18
|
initials?: string;
|
|
19
19
|
rating?: number;
|
|
20
20
|
}>;
|
|
21
|
+
/** How the section sits on the page. @default 'card' */
|
|
22
|
+
surface?: MarketingSurface;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function TestimonialsSection({
|
|
@@ -25,14 +27,9 @@ export function TestimonialsSection({
|
|
|
25
27
|
description = "Here's what engineers and product teams say after using Olwiba in production.",
|
|
26
28
|
badge = 'Testimonials',
|
|
27
29
|
testimonials,
|
|
30
|
+
surface,
|
|
28
31
|
}: TestimonialsSectionProps) {
|
|
29
|
-
const
|
|
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
|
-
)
|
|
32
|
+
const sectionClasses = useSectionSurface(surface);
|
|
36
33
|
return (
|
|
37
34
|
<section className={sectionClasses}>
|
|
38
35
|
<div className="px-6 py-14 sm:px-10 sm:py-20">
|