@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.
@@ -0,0 +1,49 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+
3
+ import { Heading, type HeadingProps } from "#/components/heading.tsx";
4
+ import { Text, type TextProps } from "#/components/text.tsx";
5
+
6
+ /**
7
+ * Bordered contact cards (email/phone). Compose: Root > Card > CardTitle +
8
+ * CardDescription + an action (e.g. a `@voila.dev/ui` Button rendered as a
9
+ * `mailto:`/`tel:` anchor).
10
+ */
11
+
12
+ function Root({ className, ...props }: React.ComponentProps<"div">) {
13
+ return (
14
+ <div
15
+ data-slot="contact-cards"
16
+ className={cn("grid gap-8 md:grid-cols-2", className)}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
23
+ return (
24
+ <div
25
+ data-slot="contact-card"
26
+ className={cn("rounded-2xl border border-border p-8", className)}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function CardTitle({ className, ...props }: HeadingProps) {
33
+ return (
34
+ <Heading level="h2" className={cn("mb-2 text-xl", className)} {...props} />
35
+ );
36
+ }
37
+
38
+ function CardDescription({ className, ...props }: TextProps) {
39
+ return (
40
+ <Text className={cn("mb-4 text-muted-foreground", className)} {...props} />
41
+ );
42
+ }
43
+
44
+ export const ContactCards = {
45
+ Root,
46
+ Card,
47
+ CardTitle,
48
+ CardDescription,
49
+ };
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from "@voila.dev/ui/cva";
2
+
3
+ export const containerVariants = cva({
4
+ base: "mx-auto w-full px-4 sm:px-6 lg:px-8",
5
+ variants: {
6
+ size: {
7
+ sm: "max-w-3xl",
8
+ md: "max-w-5xl",
9
+ lg: "max-w-6xl",
10
+ xl: "max-w-7xl",
11
+ full: "max-w-full",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ size: "xl",
16
+ },
17
+ });
18
+
19
+ export type ContainerVariants = VariantProps<typeof containerVariants>;
20
+
21
+ export const containerSizeOptions = [
22
+ "sm",
23
+ "md",
24
+ "lg",
25
+ "xl",
26
+ "full",
27
+ ] as const satisfies readonly NonNullable<ContainerVariants["size"]>[];
@@ -0,0 +1,34 @@
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 ContainerVariants,
7
+ containerVariants,
8
+ } from "#/components/container-variants.ts";
9
+
10
+ type ContainerProps = useRender.ComponentProps<"div"> & ContainerVariants;
11
+
12
+ /** Centered max-width column with responsive gutters. */
13
+ function Container({ className, render, size, ...props }: ContainerProps) {
14
+ return useRender({
15
+ defaultTagName: "div",
16
+ props: mergeProps<"div">(
17
+ {
18
+ className: cn(containerVariants({ size }), className),
19
+ },
20
+ props,
21
+ ),
22
+ render,
23
+ state: {
24
+ slot: "landing-container",
25
+ },
26
+ });
27
+ }
28
+
29
+ export {
30
+ type ContainerVariants,
31
+ containerSizeOptions,
32
+ containerVariants,
33
+ } from "#/components/container-variants.ts";
34
+ export { Container, type ContainerProps };
@@ -0,0 +1,93 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+
3
+ import { Container, type ContainerProps } from "#/components/container.tsx";
4
+ import { Heading, type HeadingProps } from "#/components/heading.tsx";
5
+ import { Section, type SectionProps } from "#/components/section.tsx";
6
+ import { Text, type TextProps } from "#/components/text.tsx";
7
+ import { accentOrangeBlobClass, brandGradientClass } from "#/lib/tones.ts";
8
+
9
+ /**
10
+ * Gradient call-to-action banner with decorative blur blobs. Compose: Root > Title,
11
+ * Description, Actions.
12
+ */
13
+
14
+ interface CtaBannerRootProps extends SectionProps {
15
+ containerSize?: ContainerProps["size"];
16
+ }
17
+
18
+ function Root({
19
+ spacing = "lg",
20
+ background,
21
+ containerSize = "md",
22
+ className,
23
+ children,
24
+ ...props
25
+ }: CtaBannerRootProps) {
26
+ return (
27
+ <Section
28
+ spacing={spacing}
29
+ background={background}
30
+ className={className}
31
+ {...props}
32
+ >
33
+ <Container size={containerSize}>
34
+ <div
35
+ data-slot="cta-banner"
36
+ className={cn(
37
+ "relative overflow-hidden rounded-3xl px-6 py-12 text-center text-primary-foreground shadow-xl sm:px-12 sm:py-16",
38
+ brandGradientClass,
39
+ )}
40
+ >
41
+ <div className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-white/10 blur-3xl" />
42
+ <div
43
+ className={cn(
44
+ "pointer-events-none absolute -bottom-20 -left-10 h-56 w-56 rounded-full blur-3xl",
45
+ accentOrangeBlobClass,
46
+ )}
47
+ />
48
+
49
+ <div className="relative mx-auto max-w-2xl">{children}</div>
50
+ </div>
51
+ </Container>
52
+ </Section>
53
+ );
54
+ }
55
+
56
+ function Title({ className, ...props }: HeadingProps) {
57
+ return (
58
+ <Heading
59
+ level="h2"
60
+ align="center"
61
+ className={cn("mb-4 text-white", className)}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function Description({ className, ...props }: TextProps) {
68
+ return (
69
+ <Text className={cn("mb-8 text-lg text-white/90", className)} {...props} />
70
+ );
71
+ }
72
+
73
+ function Actions({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="cta-banner-actions"
77
+ className={cn(
78
+ "flex flex-col justify-center gap-4 sm:flex-row",
79
+ className,
80
+ )}
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ export const CtaBanner = {
87
+ Root,
88
+ Title,
89
+ Description,
90
+ Actions,
91
+ };
92
+
93
+ export type { CtaBannerRootProps };
@@ -0,0 +1,99 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import { createContext, useContext } from "react";
3
+
4
+ import {
5
+ type Tone,
6
+ toneOptions,
7
+ toneSolidBackgroundClass,
8
+ toneTextClass,
9
+ toneTintBackgroundClass,
10
+ } from "#/lib/tones.ts";
11
+
12
+ /**
13
+ * The badge pill that opens most sections ("Nouvelle plateforme", "Pour les
14
+ * clubs"…): tinted rounded-full chip with an optional pulse dot or icon and a
15
+ * label in the tone color. Reproduces the inline pattern of
16
+ * the source design.
17
+ */
18
+
19
+ const EyebrowToneContext = createContext<Tone>("primary");
20
+
21
+ interface EyebrowRootProps extends React.ComponentProps<"div"> {
22
+ tone?: Tone;
23
+ }
24
+
25
+ function Root({ tone = "primary", className, ...props }: EyebrowRootProps) {
26
+ return (
27
+ <EyebrowToneContext.Provider value={tone}>
28
+ <div
29
+ data-slot="eyebrow"
30
+ className={cn(
31
+ "inline-flex items-center gap-2 rounded-full px-4 py-1.5",
32
+ toneTintBackgroundClass[tone],
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ </EyebrowToneContext.Provider>
38
+ );
39
+ }
40
+
41
+ interface EyebrowDotProps extends React.ComponentProps<"span"> {
42
+ /** The hero variant pulses; the values variant is still. */
43
+ pulse?: boolean;
44
+ }
45
+
46
+ function Dot({ pulse = false, className, ...props }: EyebrowDotProps) {
47
+ const tone = useContext(EyebrowToneContext);
48
+
49
+ return (
50
+ <span
51
+ data-slot="eyebrow-dot"
52
+ className={cn(
53
+ "h-2 w-2 rounded-full",
54
+ pulse && "animate-pulse",
55
+ toneSolidBackgroundClass[tone],
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ function Icon({ className, ...props }: React.ComponentProps<"span">) {
64
+ const tone = useContext(EyebrowToneContext);
65
+
66
+ return (
67
+ <span
68
+ data-slot="eyebrow-icon"
69
+ className={cn(
70
+ "flex items-center [&_svg]:h-4 [&_svg]:w-4",
71
+ toneTextClass[tone],
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function Label({ className, ...props }: React.ComponentProps<"span">) {
80
+ const tone = useContext(EyebrowToneContext);
81
+
82
+ return (
83
+ <span
84
+ data-slot="eyebrow-label"
85
+ className={cn("text-sm font-medium", toneTextClass[tone], className)}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ export const Eyebrow = {
92
+ Root,
93
+ Dot,
94
+ Icon,
95
+ Label,
96
+ };
97
+
98
+ export type { EyebrowDotProps, EyebrowRootProps };
99
+ export { type Tone, toneOptions };
@@ -0,0 +1,162 @@
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 icon cards (pain points, benefits, product highlights). Compose: Root (tone,
13
+ * columns) > Card > CardIcon + CardTitle + CardDescription.
14
+ */
15
+
16
+ const FeatureGridToneContext = createContext<Tone>("primary");
17
+
18
+ const featureGridVariants = 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 FeatureGridVariants = VariantProps<typeof featureGridVariants>;
33
+
34
+ const featureGridColumnsOptions = [
35
+ "2",
36
+ "3",
37
+ "4",
38
+ ] as const satisfies readonly NonNullable<FeatureGridVariants["columns"]>[];
39
+
40
+ interface FeatureGridRootProps
41
+ extends React.ComponentProps<"div">,
42
+ FeatureGridVariants {
43
+ tone?: Tone;
44
+ }
45
+
46
+ function Root({
47
+ tone = "primary",
48
+ columns,
49
+ className,
50
+ ...props
51
+ }: FeatureGridRootProps) {
52
+ return (
53
+ <FeatureGridToneContext.Provider value={tone}>
54
+ <div
55
+ data-slot="feature-grid"
56
+ className={cn(featureGridVariants({ columns }), className)}
57
+ {...props}
58
+ />
59
+ </FeatureGridToneContext.Provider>
60
+ );
61
+ }
62
+
63
+ const featureGridCardVariants = cva({
64
+ base: "h-full rounded-2xl bg-card text-card-foreground transition-all duration-200",
65
+ variants: {
66
+ variant: {
67
+ elevated: "border border-transparent p-8 shadow-lg",
68
+ outline: "border border-border p-8",
69
+ },
70
+ },
71
+ defaultVariants: {
72
+ variant: "elevated",
73
+ },
74
+ });
75
+
76
+ type FeatureGridCardVariants = VariantProps<typeof featureGridCardVariants>;
77
+
78
+ const featureGridCardVariantOptions = [
79
+ "elevated",
80
+ "outline",
81
+ ] as const satisfies readonly NonNullable<FeatureGridCardVariants["variant"]>[];
82
+
83
+ interface FeatureGridCardProps
84
+ extends React.ComponentProps<"div">,
85
+ FeatureGridCardVariants {}
86
+
87
+ function Card({ variant, className, ...props }: FeatureGridCardProps) {
88
+ return (
89
+ <div
90
+ data-slot="feature-grid-card"
91
+ className={cn(featureGridCardVariants({ variant }), className)}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ interface FeatureGridCardIconProps extends React.ComponentProps<"span"> {
98
+ tone?: Tone;
99
+ }
100
+
101
+ function CardIcon({ tone, className, ...props }: FeatureGridCardIconProps) {
102
+ const inheritedTone = useContext(FeatureGridToneContext);
103
+ const resolvedTone = tone ?? inheritedTone;
104
+
105
+ return (
106
+ <span
107
+ data-slot="feature-grid-card-icon"
108
+ className={cn(
109
+ "mb-4 flex h-12 w-12 items-center justify-center rounded-xl [&_svg]:h-6 [&_svg]:w-6",
110
+ toneTintBackgroundClass[resolvedTone],
111
+ toneTextClass[resolvedTone],
112
+ className,
113
+ )}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
120
+ return (
121
+ <h3
122
+ data-slot="feature-grid-card-title"
123
+ className={cn(
124
+ "mb-2 font-heading text-xl font-bold tracking-tight text-foreground",
125
+ className,
126
+ )}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
133
+ return (
134
+ <p
135
+ data-slot="feature-grid-card-description"
136
+ className={cn("text-sm leading-relaxed text-muted-foreground", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ export const FeatureGrid = {
143
+ Root,
144
+ Card,
145
+ CardIcon,
146
+ CardTitle,
147
+ CardDescription,
148
+ };
149
+
150
+ export type {
151
+ FeatureGridCardIconProps,
152
+ FeatureGridCardProps,
153
+ FeatureGridCardVariants,
154
+ FeatureGridRootProps,
155
+ FeatureGridVariants,
156
+ };
157
+ export {
158
+ featureGridCardVariantOptions,
159
+ featureGridCardVariants,
160
+ featureGridColumnsOptions,
161
+ featureGridVariants,
162
+ };
@@ -0,0 +1,41 @@
1
+ import { cva, type VariantProps } from "@voila.dev/ui/cva";
2
+
3
+ export const headingVariants = cva({
4
+ base: "font-heading font-bold tracking-tight text-foreground",
5
+ variants: {
6
+ level: {
7
+ h1: "text-4xl md:text-5xl lg:text-6xl",
8
+ h2: "text-3xl md:text-4xl lg:text-5xl",
9
+ h3: "text-2xl md:text-3xl",
10
+ h4: "text-xl md:text-2xl",
11
+ h5: "text-lg md:text-xl",
12
+ h6: "text-base md:text-lg",
13
+ },
14
+ align: {
15
+ left: "text-left",
16
+ center: "text-center",
17
+ right: "text-right",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ level: "h2",
22
+ align: "left",
23
+ },
24
+ });
25
+
26
+ export type HeadingVariants = VariantProps<typeof headingVariants>;
27
+
28
+ export const headingLevelOptions = [
29
+ "h1",
30
+ "h2",
31
+ "h3",
32
+ "h4",
33
+ "h5",
34
+ "h6",
35
+ ] as const satisfies readonly NonNullable<HeadingVariants["level"]>[];
36
+
37
+ export const headingAlignOptions = [
38
+ "left",
39
+ "center",
40
+ "right",
41
+ ] as const satisfies readonly NonNullable<HeadingVariants["align"]>[];
@@ -0,0 +1,47 @@
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 HeadingVariants,
7
+ headingVariants,
8
+ } from "#/components/heading-variants.ts";
9
+
10
+ type HeadingLevel = NonNullable<HeadingVariants["level"]>;
11
+
12
+ type HeadingProps = useRender.ComponentProps<"h2"> &
13
+ HeadingVariants & {
14
+ /** Sets both the rendered tag and the size scale (override the tag with `render`). */
15
+ level?: HeadingLevel;
16
+ };
17
+
18
+ /** Marketing heading — `font-heading`, responsive size scale per level. */
19
+ function Heading({
20
+ className,
21
+ render,
22
+ level = "h2",
23
+ align,
24
+ ...props
25
+ }: HeadingProps) {
26
+ return useRender({
27
+ defaultTagName: level,
28
+ props: mergeProps<"h2">(
29
+ {
30
+ className: cn(headingVariants({ level, align }), className),
31
+ },
32
+ props,
33
+ ),
34
+ render,
35
+ state: {
36
+ slot: "landing-heading",
37
+ },
38
+ });
39
+ }
40
+
41
+ export {
42
+ type HeadingVariants,
43
+ headingAlignOptions,
44
+ headingLevelOptions,
45
+ headingVariants,
46
+ } from "#/components/heading-variants.ts";
47
+ export { Heading, type HeadingLevel, type HeadingProps };
@@ -0,0 +1,144 @@
1
+ import { cva, type VariantProps } from "@voila.dev/ui/cva";
2
+ import { cn } from "@voila.dev/ui/lib/utils";
3
+
4
+ import { Container } from "#/components/container.tsx";
5
+ import { Heading, type HeadingProps } from "#/components/heading.tsx";
6
+ import { Section, type SectionProps } from "#/components/section.tsx";
7
+ import { Text, type TextProps } from "#/components/text.tsx";
8
+ import { type Tone, toneTextClass } from "#/lib/tones.ts";
9
+
10
+ /**
11
+ * Landing page hero. Split layout = content column + illustration/media
12
+ * column; centered layout for
13
+ * media-less heroes. Compose: Root > Content (Eyebrow, Title + Highlight,
14
+ * Lead, Actions, StatsRow) + Media.
15
+ */
16
+
17
+ const landingHeroLayoutVariants = cva({
18
+ base: "grid items-center gap-12",
19
+ variants: {
20
+ layout: {
21
+ split: "lg:grid-cols-2",
22
+ centered: "justify-items-center text-center",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ layout: "split",
27
+ },
28
+ });
29
+
30
+ type LandingHeroLayoutVariants = VariantProps<typeof landingHeroLayoutVariants>;
31
+
32
+ const landingHeroLayoutOptions = [
33
+ "split",
34
+ "centered",
35
+ ] as const satisfies readonly NonNullable<
36
+ LandingHeroLayoutVariants["layout"]
37
+ >[];
38
+
39
+ interface LandingHeroRootProps
40
+ extends SectionProps,
41
+ LandingHeroLayoutVariants {}
42
+
43
+ function Root({
44
+ layout,
45
+ spacing = "lg",
46
+ background,
47
+ className,
48
+ children,
49
+ ...props
50
+ }: LandingHeroRootProps) {
51
+ return (
52
+ <Section
53
+ spacing={spacing}
54
+ background={background}
55
+ className={cn("overflow-hidden", className)}
56
+ {...props}
57
+ >
58
+ <Container>
59
+ <div className={landingHeroLayoutVariants({ layout })}>{children}</div>
60
+ </Container>
61
+ </Section>
62
+ );
63
+ }
64
+
65
+ function Content({ className, ...props }: React.ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ data-slot="landing-hero-content"
69
+ className={cn("animate-fade-up max-w-2xl", className)}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ function Title({ className, ...props }: HeadingProps) {
76
+ return <Heading level="h1" className={cn("mb-6", className)} {...props} />;
77
+ }
78
+
79
+ interface LandingHeroHighlightProps extends React.ComponentProps<"span"> {
80
+ tone?: Tone;
81
+ }
82
+
83
+ /** Colored span inside the title — the multi-tone headline of the home hero. */
84
+ function Highlight({
85
+ tone = "primary",
86
+ className,
87
+ ...props
88
+ }: LandingHeroHighlightProps) {
89
+ return (
90
+ <span
91
+ data-slot="landing-hero-highlight"
92
+ className={cn(toneTextClass[tone], className)}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function Lead({ className, ...props }: TextProps) {
99
+ return (
100
+ <Text
101
+ variant="lead"
102
+ className={cn("mb-8 max-w-xl", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function Actions({ className, ...props }: React.ComponentProps<"div">) {
109
+ return (
110
+ <div
111
+ data-slot="landing-hero-actions"
112
+ className={cn("flex flex-col gap-4 sm:flex-row", className)}
113
+ {...props}
114
+ />
115
+ );
116
+ }
117
+
118
+ /** Illustration column — hidden below `lg`, like the Astro heroes. */
119
+ function Media({ className, ...props }: React.ComponentProps<"div">) {
120
+ return (
121
+ <div
122
+ data-slot="landing-hero-media"
123
+ className={cn("hidden lg:block", className)}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ export const LandingHero = {
130
+ Root,
131
+ Content,
132
+ Title,
133
+ Highlight,
134
+ Lead,
135
+ Actions,
136
+ Media,
137
+ };
138
+
139
+ export type {
140
+ LandingHeroHighlightProps,
141
+ LandingHeroLayoutVariants,
142
+ LandingHeroRootProps,
143
+ };
144
+ export { landingHeroLayoutOptions, landingHeroLayoutVariants };