@olwiba/ui 0.2.19 → 0.2.21
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 +101 -16
- package/dist/index.js +253 -270
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/app/AppPageHero.tsx +8 -2
- package/src/app/AppShell.tsx +45 -36
- package/src/context/OlwibaUIContext.tsx +3 -2
- 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/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/app/AppPageHero.tsx
CHANGED
|
@@ -35,8 +35,14 @@ export function AppPageHero({
|
|
|
35
35
|
<section
|
|
36
36
|
className={cn(
|
|
37
37
|
'relative overflow-hidden rounded-3xl border bg-card/80 p-5 shadow-sm sm:p-6',
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// The glow blobs are decoration, and `::after` is the section's last
|
|
39
|
+
// child — so with everything on `z-index: auto` it paints above the
|
|
40
|
+
// header content and swallows clicks on whatever sits under it. At
|
|
41
|
+
// narrow widths the action row stacks below the copy, right into the
|
|
42
|
+
// bottom-left blob, which is how a visible button stops responding.
|
|
43
|
+
// pointer-events-none keeps them purely visual.
|
|
44
|
+
'before:pointer-events-none before:absolute before:-right-16 before:-top-16 before:size-44 before:rounded-full before:bg-primary/15 before:blur-3xl',
|
|
45
|
+
'after:pointer-events-none after:absolute after:-bottom-20 after:left-10 after:size-52 after:rounded-full after:bg-primary/10 after:blur-3xl',
|
|
40
46
|
className,
|
|
41
47
|
)}
|
|
42
48
|
>
|
package/src/app/AppShell.tsx
CHANGED
|
@@ -4,10 +4,12 @@ import type { LucideIcon } from 'lucide-react';
|
|
|
4
4
|
import { useCallback, type CSSProperties, type ReactNode } from 'react';
|
|
5
5
|
import {
|
|
6
6
|
BellIcon,
|
|
7
|
+
BirdIcon,
|
|
7
8
|
CreditCardIcon,
|
|
8
9
|
LogOutIcon,
|
|
9
10
|
MoreVerticalIcon,
|
|
10
11
|
PlusCircleIcon,
|
|
12
|
+
SettingsIcon,
|
|
11
13
|
} from 'lucide-react';
|
|
12
14
|
import {
|
|
13
15
|
Avatar,
|
|
@@ -63,6 +65,7 @@ export interface AppShellUser {
|
|
|
63
65
|
onSignOut?: () => void;
|
|
64
66
|
onBilling?: () => void;
|
|
65
67
|
onNotifications?: () => void;
|
|
68
|
+
onSettings?: () => void;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export interface AppShellBrand {
|
|
@@ -142,41 +145,35 @@ const RAIL_SQUARE =
|
|
|
142
145
|
'group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!min-w-8 group-data-[collapsible=icon]:!max-w-8 group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:transition-[width,height]';
|
|
143
146
|
|
|
144
147
|
/**
|
|
145
|
-
*
|
|
148
|
+
* A stable miniature identity artwork, instead of one grey square or a pair
|
|
149
|
+
* of initials for everybody.
|
|
146
150
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*/
|
|
151
|
-
function initialsOf(source: string): string {
|
|
152
|
-
const base = source.includes('@') ? (source.split('@')[0] ?? source) : source;
|
|
153
|
-
const words = base.split(/[\s._-]+/).filter(Boolean);
|
|
154
|
-
if (words.length >= 2) return (words[0]![0]! + words[1]![0]!).toUpperCase();
|
|
155
|
-
return (words[0] ?? base).slice(0, 2).toUpperCase();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* A stable colour per person, instead of one grey square for everybody.
|
|
160
|
-
*
|
|
161
|
-
* The hue comes from a hash of the email, so an avatar is always the same
|
|
162
|
-
* colour for the same account — recognisable at a glance in a member list, and
|
|
163
|
-
* identical across devices without storing anything.
|
|
151
|
+
* FNV-1a gives the same unsigned hash for the same name/email on every device.
|
|
152
|
+
* Different byte ranges choose three hues and the gradient angle, so nearby
|
|
153
|
+
* names do not merely receive nearby shades of the same two-colour gradient.
|
|
164
154
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* chrome rather than as a person. Saturation and lightness are fixed so the
|
|
168
|
-
* whole set sits at one weight and white text clears contrast on all of them;
|
|
169
|
-
* only the hue moves. A gradient across a small hue span gives it some depth
|
|
170
|
-
* without becoming a second brand.
|
|
155
|
+
* Lightness stays in a bounded range so the white bird remains legible. The
|
|
156
|
+
* radial highlight supplies depth without adding a fourth random colour.
|
|
171
157
|
*/
|
|
172
158
|
function identityGradient(seed: string): CSSProperties {
|
|
173
|
-
let hash =
|
|
159
|
+
let hash = 2166136261;
|
|
174
160
|
for (let i = 0; i < seed.length; i += 1) {
|
|
175
|
-
hash
|
|
161
|
+
hash ^= seed.charCodeAt(i);
|
|
162
|
+
hash = Math.imul(hash, 16777619);
|
|
176
163
|
}
|
|
177
|
-
|
|
164
|
+
hash >>>= 0;
|
|
165
|
+
|
|
166
|
+
const firstHue = hash % 360;
|
|
167
|
+
const secondHue = (firstHue + 55 + ((hash >>> 8) % 70)) % 360;
|
|
168
|
+
const thirdHue = (firstHue + 190 + ((hash >>> 16) % 110)) % 360;
|
|
169
|
+
const angleHash = Math.imul(hash ^ (hash >>> 15), 2246822519) >>> 0;
|
|
170
|
+
const angle = angleHash % 360;
|
|
171
|
+
|
|
178
172
|
return {
|
|
179
|
-
backgroundImage:
|
|
173
|
+
backgroundImage: [
|
|
174
|
+
`radial-gradient(circle at 28% 18%, oklch(0.94 0.06 ${thirdHue} / 0.78), transparent 34%)`,
|
|
175
|
+
`linear-gradient(${angle}deg, oklch(0.68 0.18 ${firstHue}), oklch(0.55 0.2 ${secondHue}) 52%, oklch(0.62 0.18 ${thirdHue}))`,
|
|
176
|
+
].join(', '),
|
|
180
177
|
color: 'oklch(0.98 0 0)',
|
|
181
178
|
};
|
|
182
179
|
}
|
|
@@ -185,8 +182,8 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
185
182
|
const { isMobile } = useSidebar();
|
|
186
183
|
const uiMode = useUIMode();
|
|
187
184
|
const avatarMode = uiMode !== 'default' ? (uiMode as 'playful' | 'smooth') : undefined;
|
|
188
|
-
const
|
|
189
|
-
const identityTint = identityGradient(
|
|
185
|
+
const identitySeed = `${user.name?.trim().toLocaleLowerCase() ?? ''}|${user.email.toLocaleLowerCase()}`;
|
|
186
|
+
const identityTint = identityGradient(identitySeed);
|
|
190
187
|
|
|
191
188
|
return (
|
|
192
189
|
<SidebarMenu>
|
|
@@ -206,8 +203,11 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
206
203
|
>
|
|
207
204
|
<Avatar mode={avatarMode} size="sm">
|
|
208
205
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
209
|
-
<AvatarFallback className="
|
|
210
|
-
|
|
206
|
+
<AvatarFallback className="text-white" style={identityTint}>
|
|
207
|
+
<BirdIcon
|
|
208
|
+
aria-hidden
|
|
209
|
+
className="size-4 drop-shadow-[0_1px_1px_rgb(0_0_0/0.3)]"
|
|
210
|
+
/>
|
|
211
211
|
</AvatarFallback>
|
|
212
212
|
</Avatar>
|
|
213
213
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
@@ -229,8 +229,11 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
229
229
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
230
230
|
<Avatar mode={avatarMode} size="sm">
|
|
231
231
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
232
|
-
<AvatarFallback className="
|
|
233
|
-
|
|
232
|
+
<AvatarFallback className="text-white" style={identityTint}>
|
|
233
|
+
<BirdIcon
|
|
234
|
+
aria-hidden
|
|
235
|
+
className="size-4 drop-shadow-[0_1px_1px_rgb(0_0_0/0.3)]"
|
|
236
|
+
/>
|
|
234
237
|
</AvatarFallback>
|
|
235
238
|
</Avatar>
|
|
236
239
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
@@ -239,7 +242,7 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
239
242
|
</div>
|
|
240
243
|
</div>
|
|
241
244
|
</DropdownMenuLabel>
|
|
242
|
-
{(user.onBilling || user.onNotifications) && (
|
|
245
|
+
{(user.onBilling || user.onNotifications || user.onSettings) && (
|
|
243
246
|
<>
|
|
244
247
|
<DropdownMenuSeparator />
|
|
245
248
|
<DropdownMenuGroup>
|
|
@@ -255,6 +258,12 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
255
258
|
Notifications
|
|
256
259
|
</DropdownMenuItem>
|
|
257
260
|
)}
|
|
261
|
+
{user.onSettings && (
|
|
262
|
+
<DropdownMenuItem onClick={user.onSettings}>
|
|
263
|
+
<SettingsIcon />
|
|
264
|
+
Settings
|
|
265
|
+
</DropdownMenuItem>
|
|
266
|
+
)}
|
|
258
267
|
</DropdownMenuGroup>
|
|
259
268
|
</>
|
|
260
269
|
)}
|
|
@@ -500,7 +509,7 @@ export function AppShell({
|
|
|
500
509
|
side={side}
|
|
501
510
|
sidebarPosition={sidebarPosition}
|
|
502
511
|
/>
|
|
503
|
-
<SidebarInset className=
|
|
512
|
+
<SidebarInset className="overflow-y-auto">
|
|
504
513
|
<ShellHeader pageTitle={pageTitle} headerStart={headerStart} headerEnd={headerEnd} />
|
|
505
514
|
<div className={contentClassName}>{children}</div>
|
|
506
515
|
</SidebarInset>
|
|
@@ -35,10 +35,11 @@ export interface OlwibaUIProviderProps {
|
|
|
35
35
|
mode?: UIMode;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode:
|
|
38
|
+
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode: modeProp }: OlwibaUIProviderProps) {
|
|
39
39
|
const detectedMobile = useIsMobile();
|
|
40
40
|
const isMobile = isMobileProp ?? detectedMobile;
|
|
41
|
-
const [
|
|
41
|
+
const [uncontrolledMode, setMode] = React.useState<UIMode>('default');
|
|
42
|
+
const mode = modeProp ?? uncontrolledMode;
|
|
42
43
|
|
|
43
44
|
const variant = mode !== 'default' ? (mode as UIVariant) : undefined;
|
|
44
45
|
|
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}>
|
|
@@ -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">
|