@voila.dev/ui-landing 1.1.9
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/package.json +63 -0
- package/src/components/article-card.tsx +200 -0
- package/src/components/article-tags.tsx +31 -0
- package/src/components/bento-grid.tsx +197 -0
- package/src/components/comparison-section.tsx +209 -0
- package/src/components/contact-cards.tsx +49 -0
- package/src/components/container-variants.ts +27 -0
- package/src/components/container.tsx +34 -0
- package/src/components/cta-banner.tsx +93 -0
- package/src/components/eyebrow.tsx +99 -0
- package/src/components/feature-grid.tsx +162 -0
- package/src/components/heading-variants.ts +41 -0
- package/src/components/heading.tsx +47 -0
- package/src/components/landing-hero.tsx +144 -0
- package/src/components/logo-marquee.tsx +139 -0
- package/src/components/numbered-cards.tsx +148 -0
- package/src/components/page-header.tsx +56 -0
- package/src/components/prose-article.tsx +37 -0
- package/src/components/section-intro.tsx +82 -0
- package/src/components/section-variants.ts +47 -0
- package/src/components/section.tsx +45 -0
- package/src/components/site-footer.tsx +190 -0
- package/src/components/site-header.tsx +254 -0
- package/src/components/stats-row.tsx +69 -0
- package/src/components/step-tracks.tsx +235 -0
- package/src/components/testimonial-grid.tsx +182 -0
- package/src/components/text-variants.ts +65 -0
- package/src/components/text.tsx +45 -0
- package/src/lib/tones.ts +85 -0
- package/src/styles/landing.css +20 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
4
|
+
|
|
5
|
+
import { Container } from "#/components/container.tsx";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Partner/brand logo band, either an animated marquee or a static wrap. The
|
|
9
|
+
* marquee keyframes live in `@voila.dev/ui-landing/styles/landing.css`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function Root({ className, children, ...props }: React.ComponentProps<"div">) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
data-slot="logo-marquee"
|
|
16
|
+
className={cn("border-y border-border bg-muted/30 py-8", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<Container>{children}</Container>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Title({ className, ...props }: React.ComponentProps<"p">) {
|
|
25
|
+
return (
|
|
26
|
+
<p
|
|
27
|
+
data-slot="logo-marquee-title"
|
|
28
|
+
className={cn(
|
|
29
|
+
"mb-6 text-center text-sm font-medium uppercase tracking-wider text-muted-foreground",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Clips the track and fades its cut-off edges into the band background. */
|
|
38
|
+
function Viewport({
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<"div">) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
data-slot="logo-marquee-viewport"
|
|
46
|
+
className={cn("relative overflow-hidden", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<div className="pointer-events-none absolute left-0 top-0 z-10 h-full w-20 bg-linear-to-r from-muted/30 to-transparent" />
|
|
50
|
+
<div className="pointer-events-none absolute right-0 top-0 z-10 h-full w-20 bg-linear-to-l from-muted/30 to-transparent" />
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface LogoMarqueeTrackProps extends React.ComponentProps<"div"> {
|
|
57
|
+
/** Seconds for one loop of the marquee. */
|
|
58
|
+
duration?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auto-scrolling track. Children are rendered twice (second pass
|
|
63
|
+
* `aria-hidden`, laid out flat via `display: contents`) so the -50%
|
|
64
|
+
* translation loops seamlessly. Pauses on hover.
|
|
65
|
+
*/
|
|
66
|
+
function Track({
|
|
67
|
+
duration = 30,
|
|
68
|
+
className,
|
|
69
|
+
style,
|
|
70
|
+
children,
|
|
71
|
+
...props
|
|
72
|
+
}: LogoMarqueeTrackProps) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="logo-marquee-track"
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex animate-[landing-marquee_30s_linear_infinite] items-center gap-12 hover:[animation-play-state:paused]",
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
style={{ animationDuration: `${duration}s`, ...style }}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
<div aria-hidden="true" className="contents">
|
|
85
|
+
{children}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Non-animated alternative: centered wrapping row of logos. */
|
|
92
|
+
function StaticTrack({ className, ...props }: React.ComponentProps<"div">) {
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
data-slot="logo-marquee-static-track"
|
|
96
|
+
className={cn(
|
|
97
|
+
"flex flex-wrap items-center justify-center gap-12",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One logo — renders an `img` by default; pass `render` for custom markup. */
|
|
106
|
+
function Item({
|
|
107
|
+
className,
|
|
108
|
+
render,
|
|
109
|
+
...props
|
|
110
|
+
}: useRender.ComponentProps<"img">) {
|
|
111
|
+
return useRender({
|
|
112
|
+
defaultTagName: "img",
|
|
113
|
+
props: mergeProps<"img">(
|
|
114
|
+
{
|
|
115
|
+
className: cn(
|
|
116
|
+
"h-12 w-auto shrink-0 object-contain opacity-60 grayscale transition-all duration-300 hover:opacity-100 hover:grayscale-0",
|
|
117
|
+
className,
|
|
118
|
+
),
|
|
119
|
+
loading: "lazy",
|
|
120
|
+
},
|
|
121
|
+
props,
|
|
122
|
+
),
|
|
123
|
+
render,
|
|
124
|
+
state: {
|
|
125
|
+
slot: "logo-marquee-item",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const LogoMarquee = {
|
|
131
|
+
Root,
|
|
132
|
+
Title,
|
|
133
|
+
Viewport,
|
|
134
|
+
Track,
|
|
135
|
+
StaticTrack,
|
|
136
|
+
Item,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type { LogoMarqueeTrackProps };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "@voila.dev/ui/cva";
|
|
2
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Tone,
|
|
7
|
+
toneTextClass,
|
|
8
|
+
toneTintBackgroundClass,
|
|
9
|
+
} from "#/lib/tones.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Grid of numbered step cards ("Étape 1/2/3"). Compose: Root (tone, columns) > Card >
|
|
13
|
+
* CardHeader (CardIcon + CardLabel) + CardTitle + CardDescription.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const NumberedCardsToneContext = createContext<Tone>("primary");
|
|
17
|
+
|
|
18
|
+
const numberedCardsVariants = cva({
|
|
19
|
+
base: "grid gap-6",
|
|
20
|
+
variants: {
|
|
21
|
+
columns: {
|
|
22
|
+
"2": "md:grid-cols-2",
|
|
23
|
+
"3": "md:grid-cols-3",
|
|
24
|
+
"4": "md:grid-cols-2 lg:grid-cols-4",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
columns: "3",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type NumberedCardsVariants = VariantProps<typeof numberedCardsVariants>;
|
|
33
|
+
|
|
34
|
+
const numberedCardsColumnsOptions = [
|
|
35
|
+
"2",
|
|
36
|
+
"3",
|
|
37
|
+
"4",
|
|
38
|
+
] as const satisfies readonly NonNullable<NumberedCardsVariants["columns"]>[];
|
|
39
|
+
|
|
40
|
+
interface NumberedCardsRootProps
|
|
41
|
+
extends React.ComponentProps<"div">,
|
|
42
|
+
NumberedCardsVariants {
|
|
43
|
+
tone?: Tone;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Root({
|
|
47
|
+
tone = "primary",
|
|
48
|
+
columns,
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: NumberedCardsRootProps) {
|
|
52
|
+
return (
|
|
53
|
+
<NumberedCardsToneContext.Provider value={tone}>
|
|
54
|
+
<div
|
|
55
|
+
data-slot="numbered-cards"
|
|
56
|
+
className={cn(numberedCardsVariants({ columns }), className)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
</NumberedCardsToneContext.Provider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
data-slot="numbered-cards-card"
|
|
67
|
+
className={cn(
|
|
68
|
+
"animate-fade-up relative rounded-2xl border border-border bg-card p-8",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
data-slot="numbered-cards-card-header"
|
|
80
|
+
className={cn("mb-4 flex items-center gap-3", className)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function CardIcon({ className, ...props }: React.ComponentProps<"span">) {
|
|
87
|
+
const tone = useContext(NumberedCardsToneContext);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<span
|
|
91
|
+
data-slot="numbered-cards-card-icon"
|
|
92
|
+
className={cn(
|
|
93
|
+
"flex h-12 w-12 items-center justify-center rounded-xl [&_svg]:h-6 [&_svg]:w-6",
|
|
94
|
+
toneTintBackgroundClass[tone],
|
|
95
|
+
toneTextClass[tone],
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The "Étape N" label next to the icon. */
|
|
104
|
+
function CardLabel({ className, ...props }: React.ComponentProps<"span">) {
|
|
105
|
+
return (
|
|
106
|
+
<span
|
|
107
|
+
data-slot="numbered-cards-card-label"
|
|
108
|
+
className={cn("text-sm font-semibold text-muted-foreground", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
|
115
|
+
return (
|
|
116
|
+
<h3
|
|
117
|
+
data-slot="numbered-cards-card-title"
|
|
118
|
+
className={cn(
|
|
119
|
+
"mb-2 font-heading text-xl font-bold tracking-tight text-foreground",
|
|
120
|
+
className,
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
128
|
+
return (
|
|
129
|
+
<p
|
|
130
|
+
data-slot="numbered-cards-card-description"
|
|
131
|
+
className={cn("text-sm leading-relaxed text-muted-foreground", className)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const NumberedCards = {
|
|
138
|
+
Root,
|
|
139
|
+
Card,
|
|
140
|
+
CardHeader,
|
|
141
|
+
CardIcon,
|
|
142
|
+
CardLabel,
|
|
143
|
+
CardTitle,
|
|
144
|
+
CardDescription,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type { NumberedCardsRootProps, NumberedCardsVariants };
|
|
148
|
+
export { numberedCardsColumnsOptions, numberedCardsVariants };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
2
|
+
import { Container, type ContainerProps } from "#/components/container.tsx";
|
|
3
|
+
import { Heading, type HeadingProps } from "#/components/heading.tsx";
|
|
4
|
+
import { Section, type SectionProps } from "#/components/section.tsx";
|
|
5
|
+
import { Text, type TextProps } from "#/components/text.tsx";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Page-opening header band (title + lead on a gradient background), as on the
|
|
9
|
+
* contact/legal/blog pages of the source design. Compose: Root > Title + Lead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface PageHeaderRootProps extends SectionProps {
|
|
13
|
+
containerSize?: ContainerProps["size"];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Root({
|
|
17
|
+
spacing = "md",
|
|
18
|
+
background = "gradient-primary",
|
|
19
|
+
containerSize = "md",
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}: PageHeaderRootProps) {
|
|
24
|
+
return (
|
|
25
|
+
<Section
|
|
26
|
+
spacing={spacing}
|
|
27
|
+
background={background}
|
|
28
|
+
className={className}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<Container size={containerSize}>{children}</Container>
|
|
32
|
+
</Section>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Title({ className, ...props }: HeadingProps) {
|
|
37
|
+
return <Heading level="h1" className={cn("mb-4", className)} {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Lead({ className, ...props }: TextProps) {
|
|
41
|
+
return (
|
|
42
|
+
<Text
|
|
43
|
+
variant="lead"
|
|
44
|
+
className={cn("text-muted-foreground", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const PageHeader = {
|
|
51
|
+
Root,
|
|
52
|
+
Title,
|
|
53
|
+
Lead,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type { PageHeaderRootProps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Long-form prose wrapper (blog articles, legal pages). Requires
|
|
7
|
+
* `@plugin "@tailwindcss/typography"` in the consuming app's stylesheet.
|
|
8
|
+
*/
|
|
9
|
+
function ProseArticle({
|
|
10
|
+
className,
|
|
11
|
+
render,
|
|
12
|
+
...props
|
|
13
|
+
}: useRender.ComponentProps<"article">) {
|
|
14
|
+
return useRender({
|
|
15
|
+
defaultTagName: "article",
|
|
16
|
+
props: mergeProps<"article">(
|
|
17
|
+
{
|
|
18
|
+
className: cn(
|
|
19
|
+
"prose prose-lg md:prose-xl mx-auto max-w-none",
|
|
20
|
+
"prose-blockquote:border-l-provider prose-blockquote:bg-muted/50 prose-blockquote:py-1 prose-blockquote:pl-6 prose-blockquote:not-italic",
|
|
21
|
+
"prose-p:text-muted-foreground",
|
|
22
|
+
"prose-a:text-provider",
|
|
23
|
+
"prose-strong:text-foreground",
|
|
24
|
+
"prose-img:rounded-xl",
|
|
25
|
+
className,
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
props,
|
|
29
|
+
),
|
|
30
|
+
render,
|
|
31
|
+
state: {
|
|
32
|
+
slot: "prose-article",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { ProseArticle };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "@voila.dev/ui/cva";
|
|
2
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
3
|
+
|
|
4
|
+
import { Heading, type HeadingProps } from "#/components/heading.tsx";
|
|
5
|
+
import { Text, type TextProps } from "#/components/text.tsx";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Centered intro block opening a section: Eyebrow + title + lead. Factors out
|
|
9
|
+
* the header markup most sections repeat.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const sectionIntroVariants = cva({
|
|
13
|
+
base: "mx-auto text-center",
|
|
14
|
+
variants: {
|
|
15
|
+
width: {
|
|
16
|
+
md: "max-w-2xl",
|
|
17
|
+
lg: "max-w-3xl",
|
|
18
|
+
},
|
|
19
|
+
spacing: {
|
|
20
|
+
md: "mb-12",
|
|
21
|
+
lg: "mb-16",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
width: "md",
|
|
26
|
+
spacing: "lg",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type SectionIntroVariants = VariantProps<typeof sectionIntroVariants>;
|
|
31
|
+
|
|
32
|
+
const sectionIntroWidthOptions = [
|
|
33
|
+
"md",
|
|
34
|
+
"lg",
|
|
35
|
+
] as const satisfies readonly NonNullable<SectionIntroVariants["width"]>[];
|
|
36
|
+
|
|
37
|
+
const sectionIntroSpacingOptions = [
|
|
38
|
+
"md",
|
|
39
|
+
"lg",
|
|
40
|
+
] as const satisfies readonly NonNullable<SectionIntroVariants["spacing"]>[];
|
|
41
|
+
|
|
42
|
+
interface SectionIntroRootProps
|
|
43
|
+
extends React.ComponentProps<"div">,
|
|
44
|
+
SectionIntroVariants {}
|
|
45
|
+
|
|
46
|
+
function Root({ width, spacing, className, ...props }: SectionIntroRootProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="section-intro"
|
|
50
|
+
className={cn(sectionIntroVariants({ width, spacing }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Title({ className, ...props }: HeadingProps) {
|
|
57
|
+
return (
|
|
58
|
+
<Heading
|
|
59
|
+
level="h2"
|
|
60
|
+
align="center"
|
|
61
|
+
className={cn("mb-4", className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Description(props: TextProps) {
|
|
68
|
+
return <Text variant="lead" align="center" {...props} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const SectionIntro = {
|
|
72
|
+
Root,
|
|
73
|
+
Title,
|
|
74
|
+
Description,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type { SectionIntroRootProps, SectionIntroVariants };
|
|
78
|
+
export {
|
|
79
|
+
sectionIntroSpacingOptions,
|
|
80
|
+
sectionIntroVariants,
|
|
81
|
+
sectionIntroWidthOptions,
|
|
82
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "@voila.dev/ui/cva";
|
|
2
|
+
|
|
3
|
+
export const sectionVariants = cva({
|
|
4
|
+
base: "relative",
|
|
5
|
+
variants: {
|
|
6
|
+
spacing: {
|
|
7
|
+
none: "",
|
|
8
|
+
sm: "py-12 md:py-16",
|
|
9
|
+
md: "py-16 md:py-24",
|
|
10
|
+
lg: "py-24 md:py-32",
|
|
11
|
+
xl: "py-32 md:py-40",
|
|
12
|
+
},
|
|
13
|
+
background: {
|
|
14
|
+
default: "bg-background",
|
|
15
|
+
muted: "bg-muted",
|
|
16
|
+
provider: "bg-provider text-primary-foreground",
|
|
17
|
+
gradient: "bg-gradient-to-br from-background via-background to-muted",
|
|
18
|
+
"gradient-primary":
|
|
19
|
+
"bg-gradient-to-br from-primary/5 via-background to-primary/10",
|
|
20
|
+
"gradient-provider":
|
|
21
|
+
"bg-gradient-to-br from-provider/5 via-background to-provider/10",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
spacing: "md",
|
|
26
|
+
background: "default",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type SectionVariants = VariantProps<typeof sectionVariants>;
|
|
31
|
+
|
|
32
|
+
export const sectionSpacingOptions = [
|
|
33
|
+
"none",
|
|
34
|
+
"sm",
|
|
35
|
+
"md",
|
|
36
|
+
"lg",
|
|
37
|
+
"xl",
|
|
38
|
+
] as const satisfies readonly NonNullable<SectionVariants["spacing"]>[];
|
|
39
|
+
|
|
40
|
+
export const sectionBackgroundOptions = [
|
|
41
|
+
"default",
|
|
42
|
+
"muted",
|
|
43
|
+
"provider",
|
|
44
|
+
"gradient",
|
|
45
|
+
"gradient-primary",
|
|
46
|
+
"gradient-provider",
|
|
47
|
+
] as const satisfies readonly NonNullable<SectionVariants["background"]>[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "@voila.dev/ui/lib/utils";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type SectionVariants,
|
|
7
|
+
sectionVariants,
|
|
8
|
+
} from "#/components/section-variants.ts";
|
|
9
|
+
|
|
10
|
+
type SectionProps = useRender.ComponentProps<"section"> & SectionVariants;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Full-width landing band — vertical rhythm + background treatment. Marketing
|
|
14
|
+
* scale (`py-24`…): not interchangeable with the app-shell `Section` from
|
|
15
|
+
* `@voila.dev/ui`. Renders a `section`; pass `render` to swap the tag.
|
|
16
|
+
*/
|
|
17
|
+
function Section({
|
|
18
|
+
className,
|
|
19
|
+
render,
|
|
20
|
+
spacing,
|
|
21
|
+
background,
|
|
22
|
+
...props
|
|
23
|
+
}: SectionProps) {
|
|
24
|
+
return useRender({
|
|
25
|
+
defaultTagName: "section",
|
|
26
|
+
props: mergeProps<"section">(
|
|
27
|
+
{
|
|
28
|
+
className: cn(sectionVariants({ spacing, background }), className),
|
|
29
|
+
},
|
|
30
|
+
props,
|
|
31
|
+
),
|
|
32
|
+
render,
|
|
33
|
+
state: {
|
|
34
|
+
slot: "landing-section",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
type SectionVariants,
|
|
41
|
+
sectionBackgroundOptions,
|
|
42
|
+
sectionSpacingOptions,
|
|
43
|
+
sectionVariants,
|
|
44
|
+
} from "#/components/section-variants.ts";
|
|
45
|
+
export { Section, type SectionProps };
|