@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,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 };
|
package/src/lib/tones.ts
ADDED
|
@@ -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.*";
|