@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,235 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import { createContext, useContext } from "react";
3
+
4
+ import {
5
+ type Tone,
6
+ toneConnectorLineClass,
7
+ toneHoverBorderClass,
8
+ toneSolidClass,
9
+ toneTextClass,
10
+ toneTintBackgroundClass,
11
+ } from "#/lib/tones.ts";
12
+
13
+ /**
14
+ * Parallel tone-colored step tracks with a vertical connector line and
15
+ * numbered icon tiles. Compose: Root > Track (tone) >
16
+ * Header (HeaderIcon + HeaderText > HeaderTitle/HeaderSubtitle) +
17
+ * Steps > Step (StepIcon number + Body > BodyTitle/BodyDescription).
18
+ */
19
+
20
+ const StepTracksToneContext = createContext<Tone>("primary");
21
+
22
+ function Root({ className, ...props }: React.ComponentProps<"div">) {
23
+ return (
24
+ <div
25
+ data-slot="step-tracks"
26
+ className={cn("grid gap-16 lg:grid-cols-2 lg:gap-12", className)}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ interface StepTracksTrackProps extends React.ComponentProps<"div"> {
33
+ tone?: Tone;
34
+ }
35
+
36
+ function Track({
37
+ tone = "primary",
38
+ className,
39
+ ...props
40
+ }: StepTracksTrackProps) {
41
+ return (
42
+ <StepTracksToneContext.Provider value={tone}>
43
+ <div
44
+ data-slot="step-tracks-track"
45
+ className={cn("animate-fade-up", className)}
46
+ {...props}
47
+ />
48
+ </StepTracksToneContext.Provider>
49
+ );
50
+ }
51
+
52
+ function Header({ className, ...props }: React.ComponentProps<"div">) {
53
+ return (
54
+ <div
55
+ data-slot="step-tracks-header"
56
+ className={cn("mb-8 flex items-center gap-4", className)}
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ function HeaderIcon({ className, ...props }: React.ComponentProps<"div">) {
63
+ const tone = useContext(StepTracksToneContext);
64
+
65
+ return (
66
+ <div
67
+ data-slot="step-tracks-header-icon"
68
+ className={cn(
69
+ "flex h-12 w-12 shrink-0 items-center justify-center rounded-xl [&_svg]:h-6 [&_svg]:w-6",
70
+ toneTintBackgroundClass[tone],
71
+ toneTextClass[tone],
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function HeaderText({ className, ...props }: React.ComponentProps<"div">) {
80
+ return (
81
+ <div data-slot="step-tracks-header-text" className={className} {...props} />
82
+ );
83
+ }
84
+
85
+ function HeaderTitle({ className, ...props }: React.ComponentProps<"h3">) {
86
+ const tone = useContext(StepTracksToneContext);
87
+
88
+ return (
89
+ <h3
90
+ data-slot="step-tracks-header-title"
91
+ className={cn("text-xl font-semibold", toneTextClass[tone], className)}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ function HeaderSubtitle({ className, ...props }: React.ComponentProps<"p">) {
98
+ return (
99
+ <p
100
+ data-slot="step-tracks-header-subtitle"
101
+ className={cn("text-sm text-muted-foreground", className)}
102
+ {...props}
103
+ />
104
+ );
105
+ }
106
+
107
+ /** Stepper wrapper — owns the vertical connector line behind the icon tiles. */
108
+ function Steps({ className, children, ...props }: React.ComponentProps<"div">) {
109
+ const tone = useContext(StepTracksToneContext);
110
+
111
+ return (
112
+ <div
113
+ data-slot="step-tracks-steps"
114
+ className={cn("relative", className)}
115
+ {...props}
116
+ >
117
+ <div
118
+ className={cn(
119
+ "absolute bottom-10 left-7 top-2 w-px",
120
+ toneConnectorLineClass[tone],
121
+ )}
122
+ />
123
+ <div className="grid gap-6">{children}</div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function Step({ className, ...props }: React.ComponentProps<"div">) {
129
+ return (
130
+ <div
131
+ data-slot="step-tracks-step"
132
+ className={cn("animate-fade-up relative flex gap-5", className)}
133
+ {...props}
134
+ />
135
+ );
136
+ }
137
+
138
+ interface StepTracksStepIconProps extends React.ComponentProps<"div"> {
139
+ number: number;
140
+ }
141
+
142
+ function StepIcon({
143
+ number,
144
+ className,
145
+ children,
146
+ ...props
147
+ }: StepTracksStepIconProps) {
148
+ const tone = useContext(StepTracksToneContext);
149
+
150
+ return (
151
+ <div
152
+ data-slot="step-tracks-step-icon"
153
+ className={cn(
154
+ "relative z-10 h-14 w-14 shrink-0 rounded-xl bg-background",
155
+ className,
156
+ )}
157
+ {...props}
158
+ >
159
+ <div
160
+ className={cn(
161
+ "flex h-full w-full items-center justify-center rounded-xl [&_svg]:h-6 [&_svg]:w-6",
162
+ toneTintBackgroundClass[tone],
163
+ toneTextClass[tone],
164
+ )}
165
+ >
166
+ {children}
167
+ </div>
168
+ <span
169
+ className={cn(
170
+ "absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold",
171
+ toneSolidClass[tone],
172
+ )}
173
+ >
174
+ {number}
175
+ </span>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ function Body({ className, ...props }: React.ComponentProps<"div">) {
181
+ const tone = useContext(StepTracksToneContext);
182
+
183
+ return (
184
+ <div
185
+ data-slot="step-tracks-step-body"
186
+ className={cn(
187
+ "flex-1 rounded-2xl border border-border bg-card p-6 shadow-sm transition-all hover:shadow-md",
188
+ toneHoverBorderClass[tone],
189
+ className,
190
+ )}
191
+ {...props}
192
+ />
193
+ );
194
+ }
195
+
196
+ function BodyTitle({ className, ...props }: React.ComponentProps<"h4">) {
197
+ return (
198
+ <h4
199
+ data-slot="step-tracks-step-title"
200
+ className={cn("mb-2 text-lg font-semibold", className)}
201
+ {...props}
202
+ />
203
+ );
204
+ }
205
+
206
+ function BodyDescription({ className, ...props }: React.ComponentProps<"p">) {
207
+ return (
208
+ <p
209
+ data-slot="step-tracks-step-description"
210
+ className={cn(
211
+ "text-muted-foreground [&_strong]:font-semibold [&_strong]:text-foreground",
212
+ className,
213
+ )}
214
+ {...props}
215
+ />
216
+ );
217
+ }
218
+
219
+ export const StepTracks = {
220
+ Root,
221
+ Track,
222
+ Header,
223
+ HeaderIcon,
224
+ HeaderText,
225
+ HeaderTitle,
226
+ HeaderSubtitle,
227
+ Steps,
228
+ Step,
229
+ StepIcon,
230
+ Body,
231
+ BodyTitle,
232
+ BodyDescription,
233
+ };
234
+
235
+ export type { StepTracksStepIconProps, StepTracksTrackProps };
@@ -0,0 +1,182 @@
1
+ import { QuotesIcon, StarIcon } from "@phosphor-icons/react";
2
+ import { cva, type VariantProps } from "@voila.dev/ui/cva";
3
+ import { cn } from "@voila.dev/ui/lib/utils";
4
+
5
+ import { accentOrangeStarClass, accentOrangeTintClass } from "#/lib/tones.ts";
6
+
7
+ /**
8
+ * Testimonial figure cards (quote, avatar initial, star rating). Compose: Root > Item >
9
+ * QuoteIcon + Quote + Footer (Avatar + Author > AuthorName/AuthorRole +
10
+ * Rating).
11
+ */
12
+
13
+ function Root({ className, ...props }: React.ComponentProps<"div">) {
14
+ return (
15
+ <div
16
+ data-slot="testimonial-grid"
17
+ className={cn("grid gap-6 md:grid-cols-3", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function Item({ className, ...props }: React.ComponentProps<"figure">) {
24
+ return (
25
+ <figure
26
+ data-slot="testimonial-item"
27
+ className={cn(
28
+ "animate-fade-up flex h-full min-w-0 flex-col rounded-2xl border border-border bg-card p-6 shadow-sm transition-all hover:-translate-y-1 hover:shadow-md lg:p-8",
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function QuoteIcon({
37
+ className,
38
+ ...props
39
+ }: React.ComponentProps<typeof QuotesIcon>) {
40
+ return (
41
+ <QuotesIcon
42
+ data-slot="testimonial-quote-icon"
43
+ className={cn("mb-4 h-8 w-8 text-primary/30", className)}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function Quote({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<"blockquote">) {
54
+ return (
55
+ <blockquote
56
+ data-slot="testimonial-quote"
57
+ className={cn("flex-1 text-foreground", className)}
58
+ {...props}
59
+ >
60
+ <p className="leading-relaxed">{children}</p>
61
+ </blockquote>
62
+ );
63
+ }
64
+
65
+ function Footer({ className, ...props }: React.ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ data-slot="testimonial-footer"
69
+ className={cn(
70
+ "mt-6 flex items-center gap-3 border-t border-border/60 pt-5",
71
+ className,
72
+ )}
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ const testimonialAvatarVariants = cva({
79
+ base: "flex h-11 w-11 shrink-0 items-center justify-center rounded-full font-heading text-sm font-bold",
80
+ variants: {
81
+ accent: {
82
+ primary: "bg-primary/10 text-primary",
83
+ orange: accentOrangeTintClass,
84
+ },
85
+ },
86
+ defaultVariants: {
87
+ accent: "primary",
88
+ },
89
+ });
90
+
91
+ type TestimonialAvatarVariants = VariantProps<typeof testimonialAvatarVariants>;
92
+
93
+ const testimonialAvatarAccentOptions = [
94
+ "primary",
95
+ "orange",
96
+ ] as const satisfies readonly NonNullable<
97
+ TestimonialAvatarVariants["accent"]
98
+ >[];
99
+
100
+ interface TestimonialAvatarProps
101
+ extends React.ComponentProps<"span">,
102
+ TestimonialAvatarVariants {}
103
+
104
+ /** Initial-letter avatar disc. */
105
+ function Avatar({ accent, className, ...props }: TestimonialAvatarProps) {
106
+ return (
107
+ <span
108
+ data-slot="testimonial-avatar"
109
+ className={cn(testimonialAvatarVariants({ accent }), className)}
110
+ {...props}
111
+ />
112
+ );
113
+ }
114
+
115
+ function Author({ className, ...props }: React.ComponentProps<"figcaption">) {
116
+ return (
117
+ <figcaption
118
+ data-slot="testimonial-author"
119
+ className={cn("min-w-0", className)}
120
+ {...props}
121
+ />
122
+ );
123
+ }
124
+
125
+ function AuthorName({ className, ...props }: React.ComponentProps<"p">) {
126
+ return (
127
+ <p
128
+ data-slot="testimonial-author-name"
129
+ className={cn("text-sm font-semibold text-foreground", className)}
130
+ {...props}
131
+ />
132
+ );
133
+ }
134
+
135
+ function AuthorRole({ className, ...props }: React.ComponentProps<"p">) {
136
+ return (
137
+ <p
138
+ data-slot="testimonial-author-role"
139
+ className={cn("text-xs text-muted-foreground", className)}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ interface TestimonialRatingProps extends React.ComponentProps<"div"> {
146
+ count?: number;
147
+ }
148
+
149
+ function Rating({ count = 5, className, ...props }: TestimonialRatingProps) {
150
+ return (
151
+ <div
152
+ data-slot="testimonial-rating"
153
+ className={cn("ml-auto flex shrink-0 gap-0.5", className)}
154
+ {...props}
155
+ >
156
+ {Array.from({ length: count }, (_, index) => (
157
+ <StarIcon
158
+ // Stars are a static decorative sequence — the index is the identity.
159
+ key={index}
160
+ weight="fill"
161
+ className={cn("h-3.5 w-3.5", accentOrangeStarClass)}
162
+ />
163
+ ))}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ export const TestimonialGrid = {
169
+ Root,
170
+ Item,
171
+ QuoteIcon,
172
+ Quote,
173
+ Footer,
174
+ Avatar,
175
+ Author,
176
+ AuthorName,
177
+ AuthorRole,
178
+ Rating,
179
+ };
180
+
181
+ export type { TestimonialAvatarProps, TestimonialRatingProps };
182
+ export { testimonialAvatarAccentOptions, testimonialAvatarVariants };
@@ -0,0 +1,65 @@
1
+ import { cva, type VariantProps } from "@voila.dev/ui/cva";
2
+
3
+ export const textVariants = cva({
4
+ base: "leading-relaxed",
5
+ variants: {
6
+ variant: {
7
+ body: "text-foreground",
8
+ muted: "text-muted-foreground",
9
+ lead: "text-xl text-muted-foreground",
10
+ },
11
+ size: {
12
+ xs: "text-xs",
13
+ sm: "text-sm",
14
+ base: "text-base",
15
+ lg: "text-lg",
16
+ xl: "text-xl",
17
+ },
18
+ align: {
19
+ left: "text-left",
20
+ center: "text-center",
21
+ right: "text-right",
22
+ },
23
+ weight: {
24
+ normal: "font-normal",
25
+ medium: "font-medium",
26
+ semibold: "font-semibold",
27
+ bold: "font-bold",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "body",
32
+ size: "base",
33
+ align: "left",
34
+ weight: "normal",
35
+ },
36
+ });
37
+
38
+ export type TextVariants = VariantProps<typeof textVariants>;
39
+
40
+ export const textVariantOptions = [
41
+ "body",
42
+ "muted",
43
+ "lead",
44
+ ] as const satisfies readonly NonNullable<TextVariants["variant"]>[];
45
+
46
+ export const textSizeOptions = [
47
+ "xs",
48
+ "sm",
49
+ "base",
50
+ "lg",
51
+ "xl",
52
+ ] as const satisfies readonly NonNullable<TextVariants["size"]>[];
53
+
54
+ export const textAlignOptions = [
55
+ "left",
56
+ "center",
57
+ "right",
58
+ ] as const satisfies readonly NonNullable<TextVariants["align"]>[];
59
+
60
+ export const textWeightOptions = [
61
+ "normal",
62
+ "medium",
63
+ "semibold",
64
+ "bold",
65
+ ] as const satisfies readonly NonNullable<TextVariants["weight"]>[];
@@ -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 { type TextVariants, textVariants } from "#/components/text-variants.ts";
6
+
7
+ type TextProps = useRender.ComponentProps<"p"> & TextVariants;
8
+
9
+ /** Marketing copy block — renders a `p`; pass `render` for `span`/`div`. */
10
+ function Text({
11
+ className,
12
+ render,
13
+ variant,
14
+ size,
15
+ align,
16
+ weight,
17
+ ...props
18
+ }: TextProps) {
19
+ return useRender({
20
+ defaultTagName: "p",
21
+ props: mergeProps<"p">(
22
+ {
23
+ className: cn(
24
+ textVariants({ variant, size, align, weight }),
25
+ className,
26
+ ),
27
+ },
28
+ props,
29
+ ),
30
+ render,
31
+ state: {
32
+ slot: "landing-text",
33
+ },
34
+ });
35
+ }
36
+
37
+ export {
38
+ type TextVariants,
39
+ textAlignOptions,
40
+ textSizeOptions,
41
+ textVariantOptions,
42
+ textVariants,
43
+ textWeightOptions,
44
+ } from "#/components/text-variants.ts";
45
+ export { Text, type TextProps };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared "tone" axis for landing sections. A tone maps the brand roles from
3
+ * `@voila.dev/ui-tokens` onto the coordinated class sets marketing sections
4
+ * need (tinted chip, solid badge, connector line…). Class strings are full
5
+ * literals — never composed at runtime — so Tailwind's scanner sees them.
6
+ */
7
+
8
+ export const toneOptions = [
9
+ "primary",
10
+ "provider",
11
+ "organization",
12
+ "destructive",
13
+ "muted",
14
+ ] as const;
15
+
16
+ export type Tone = (typeof toneOptions)[number];
17
+
18
+ export const toneTextClass: Record<Tone, string> = {
19
+ primary: "text-primary",
20
+ provider: "text-provider",
21
+ organization: "text-organization",
22
+ destructive: "text-destructive",
23
+ muted: "text-muted-foreground",
24
+ };
25
+
26
+ export const toneTintBackgroundClass: Record<Tone, string> = {
27
+ primary: "bg-primary/10",
28
+ provider: "bg-provider/10",
29
+ organization: "bg-organization/10",
30
+ destructive: "bg-destructive/10",
31
+ muted: "bg-muted",
32
+ };
33
+
34
+ export const toneSolidBackgroundClass: Record<Tone, string> = {
35
+ primary: "bg-primary",
36
+ provider: "bg-provider",
37
+ organization: "bg-organization",
38
+ destructive: "bg-destructive",
39
+ muted: "bg-muted-foreground",
40
+ };
41
+
42
+ export const toneSolidClass: Record<Tone, string> = {
43
+ primary: "bg-primary text-primary-foreground",
44
+ provider: "bg-provider text-provider-foreground",
45
+ organization: "bg-organization text-organization-foreground",
46
+ destructive: "bg-destructive text-white",
47
+ muted: "bg-muted text-muted-foreground",
48
+ };
49
+
50
+ export const toneConnectorLineClass: Record<Tone, string> = {
51
+ primary: "bg-gradient-to-b from-primary via-primary/40 to-primary/10",
52
+ provider: "bg-gradient-to-b from-provider via-provider/40 to-provider/10",
53
+ organization:
54
+ "bg-gradient-to-b from-organization via-organization/40 to-organization/10",
55
+ destructive:
56
+ "bg-gradient-to-b from-destructive via-destructive/40 to-destructive/10",
57
+ muted: "bg-gradient-to-b from-border via-border/40 to-border/10",
58
+ };
59
+
60
+ export const toneHoverBorderClass: Record<Tone, string> = {
61
+ primary: "hover:border-primary/40",
62
+ provider: "hover:border-provider/40",
63
+ organization: "hover:border-organization/40",
64
+ destructive: "hover:border-destructive/40",
65
+ muted: "hover:border-border",
66
+ };
67
+
68
+ export const tonePanelClass: Record<Tone, string> = {
69
+ primary: "border-primary/20 bg-primary/5",
70
+ provider: "border-provider/20 bg-provider/5",
71
+ organization: "border-organization/20 bg-organization/5",
72
+ destructive: "border-destructive/20 bg-destructive/5",
73
+ muted: "border-border bg-muted/50",
74
+ };
75
+
76
+ /*
77
+ * Accent literals carried over from the Astro marketing site for pixel
78
+ * parity. TODO: promote to named tokens in @voila.dev/ui-tokens once the
79
+ * rebuild has shipped (they are close to, but not equal to, the brand roles).
80
+ */
81
+
82
+ export const accentOrangeTintClass = "bg-[#FA8424]/10 text-[#FA8424]";
83
+ export const accentOrangeStarClass = "fill-[#FA8424] text-[#FA8424]";
84
+ export const accentOrangeBlobClass = "bg-[#FA8424]/20";
85
+ export const brandGradientClass = "bg-gradient-to-br from-primary to-[#3b5bff]";
@@ -0,0 +1,20 @@
1
+ @import "../styles.css";
2
+
3
+ /*
4
+ * Side-effect styles for @voila.dev/ui-landing. Import once from the
5
+ * consuming app's global stylesheet, alongside an `@source` directive
6
+ * pointing at this package's `src` so Tailwind emits the classes the
7
+ * components use.
8
+ */
9
+
10
+ /* Seamless logo marquee: the track renders its children twice, so sliding by
11
+ * half its width loops without a visible jump. Used via the arbitrary utility
12
+ * `animate-[landing-marquee_30s_linear_infinite]` in LogoMarquee. */
13
+ @keyframes landing-marquee {
14
+ from {
15
+ transform: translateX(0);
16
+ }
17
+ to {
18
+ transform: translateX(-50%);
19
+ }
20
+ }
package/src/styles.css ADDED
@@ -0,0 +1,10 @@
1
+ /*
2
+ * Tailwind must scan this package or none of its classes are generated — the
3
+ * components ship as .tsx source, not as compiled CSS.
4
+ *
5
+ * `@source` resolves relative to the file that declares it, so this works
6
+ * wherever the package ends up: node_modules in an app, a symlink in a
7
+ * workspace. Consumers import this file and add nothing of their own.
8
+ */
9
+ @source "./";
10
+ @source not "./**/*.test.*";