@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,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 };