@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 ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@voila.dev/ui-landing",
3
+ "version": "1.1.9",
4
+ "type": "module",
5
+ "description": "Your marketing site, from the same system as your product.",
6
+ "license": "MIT",
7
+ "homepage": "https://ui.voila.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/voila-voila-dev/ui.git",
11
+ "directory": "packages/ui-landing"
12
+ },
13
+ "keywords": [
14
+ "react",
15
+ "landing-page",
16
+ "marketing",
17
+ "sections",
18
+ "tailwindcss"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "!src/**/*.test.ts",
26
+ "!src/**/*.test.tsx",
27
+ "!src/**/*.stories.ts",
28
+ "!src/**/*.stories.tsx"
29
+ ],
30
+ "sideEffects": [
31
+ "**/*.css"
32
+ ],
33
+ "exports": {
34
+ "./styles.css": "./src/styles.css",
35
+ "./styles/landing.css": "./src/styles/landing.css",
36
+ "./lib/tones": "./src/lib/tones.ts",
37
+ "./section-variants": "./src/components/section-variants.ts",
38
+ "./container-variants": "./src/components/container-variants.ts",
39
+ "./heading-variants": "./src/components/heading-variants.ts",
40
+ "./text-variants": "./src/components/text-variants.ts",
41
+ "./components/*": "./src/components/*.tsx"
42
+ },
43
+ "imports": {
44
+ "#/*": "./src/*"
45
+ },
46
+ "scripts": {
47
+ "check": "biome check",
48
+ "check-types": "tsc --noEmit",
49
+ "format": "biome format",
50
+ "lint": "biome lint",
51
+ "test": "vitest run"
52
+ },
53
+ "dependencies": {
54
+ "@base-ui/react": "^1.4.1",
55
+ "@phosphor-icons/react": "2.1.10",
56
+ "@voila.dev/ui": "0.0.0"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^19.0.0",
60
+ "react-dom": "^19.0.0",
61
+ "tailwindcss": "^4.0.0"
62
+ }
63
+ }
@@ -0,0 +1,200 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { ArrowUpRightIcon } from "@phosphor-icons/react";
4
+ import { cn } from "@voila.dev/ui/lib/utils";
5
+
6
+ import { ArticleTags } from "#/components/article-tags.tsx";
7
+
8
+ /**
9
+ * Blog article card.
10
+ * Compose: Root (anchor) > Frame > Image | ImageFallback + Content (Tags,
11
+ * Title, Description, Meta > MetaItem…/Arrow).
12
+ */
13
+
14
+ /** The clickable wrapper — an anchor by default; pass `render` for a router Link. */
15
+ function Root({ className, render, ...props }: useRender.ComponentProps<"a">) {
16
+ return useRender({
17
+ defaultTagName: "a",
18
+ props: mergeProps<"a">(
19
+ {
20
+ className: cn("group block", className),
21
+ },
22
+ props,
23
+ ),
24
+ render,
25
+ state: {
26
+ slot: "article-card",
27
+ },
28
+ });
29
+ }
30
+
31
+ /** The card surface (Astro `Card` default/none/lift as `article`). */
32
+ function Frame({ className, ...props }: React.ComponentProps<"article">) {
33
+ return (
34
+ <article
35
+ data-slot="article-card-frame"
36
+ className={cn(
37
+ "rounded-lg border border-border bg-card text-card-foreground transition-all duration-200 hover:-translate-y-1 hover:shadow-lg",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ interface ArticleCardImageProps extends React.ComponentProps<"img"> {
46
+ alt: string;
47
+ }
48
+
49
+ function Image({ className, alt, ...props }: ArticleCardImageProps) {
50
+ return (
51
+ <div
52
+ data-slot="article-card-image"
53
+ className="relative aspect-video overflow-hidden rounded-t-lg"
54
+ >
55
+ <img
56
+ alt={alt}
57
+ loading="lazy"
58
+ className={cn(
59
+ "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105",
60
+ className,
61
+ )}
62
+ {...props}
63
+ />
64
+ <div className="absolute inset-0 bg-linear-to-t from-black/20 to-transparent" />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ /** Gradient placeholder shown when the article has no cover image. */
70
+ function ImageFallback({
71
+ className,
72
+ children,
73
+ ...props
74
+ }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="article-card-image-fallback"
78
+ className={cn(
79
+ "relative aspect-video overflow-hidden rounded-t-lg bg-linear-to-br from-provider/20 to-organization/20",
80
+ className,
81
+ )}
82
+ {...props}
83
+ >
84
+ <div className="absolute inset-0 flex items-center justify-center">
85
+ <span className="text-6xl opacity-30">{children ?? "📝"}</span>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function Content({ className, ...props }: React.ComponentProps<"div">) {
92
+ return (
93
+ <div
94
+ data-slot="article-card-content"
95
+ className={cn("p-5", className)}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ function Tags({ className, ...props }: React.ComponentProps<"div">) {
102
+ return <ArticleTags.Root className={cn("mb-3", className)} {...props} />;
103
+ }
104
+
105
+ function Title({ className, ...props }: React.ComponentProps<"h3">) {
106
+ return (
107
+ <h3
108
+ data-slot="article-card-title"
109
+ className={cn(
110
+ "mb-2 line-clamp-2 text-lg font-semibold transition-colors group-hover:text-provider",
111
+ className,
112
+ )}
113
+ {...props}
114
+ />
115
+ );
116
+ }
117
+
118
+ function Description({ className, ...props }: React.ComponentProps<"p">) {
119
+ return (
120
+ <p
121
+ data-slot="article-card-description"
122
+ className={cn(
123
+ "mb-4 line-clamp-2 text-sm text-muted-foreground",
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+
131
+ function Meta({ className, ...props }: React.ComponentProps<"div">) {
132
+ return (
133
+ <div
134
+ data-slot="article-card-meta"
135
+ className={cn(
136
+ "flex items-center justify-between text-xs text-muted-foreground",
137
+ className,
138
+ )}
139
+ {...props}
140
+ />
141
+ );
142
+ }
143
+
144
+ function MetaItems({ className, ...props }: React.ComponentProps<"div">) {
145
+ return (
146
+ <div
147
+ data-slot="article-card-meta-items"
148
+ className={cn("flex items-center gap-3", className)}
149
+ {...props}
150
+ />
151
+ );
152
+ }
153
+
154
+ /** Date / reading-time entry — icon child + text. */
155
+ function MetaItem({ className, ...props }: React.ComponentProps<"span">) {
156
+ return (
157
+ <span
158
+ data-slot="article-card-meta-item"
159
+ className={cn(
160
+ "flex items-center gap-1 [&_svg]:h-3.5 [&_svg]:w-3.5",
161
+ className,
162
+ )}
163
+ {...props}
164
+ />
165
+ );
166
+ }
167
+
168
+ function Arrow({
169
+ className,
170
+ ...props
171
+ }: React.ComponentProps<typeof ArrowUpRightIcon>) {
172
+ return (
173
+ <ArrowUpRightIcon
174
+ data-slot="article-card-arrow"
175
+ className={cn(
176
+ "h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5",
177
+ className,
178
+ )}
179
+ {...props}
180
+ />
181
+ );
182
+ }
183
+
184
+ export const ArticleCard = {
185
+ Root,
186
+ Frame,
187
+ Image,
188
+ ImageFallback,
189
+ Content,
190
+ Tags,
191
+ Title,
192
+ Description,
193
+ Meta,
194
+ MetaItems,
195
+ MetaItem,
196
+ Arrow,
197
+ };
198
+
199
+ export type { ArticleCardImageProps };
200
+ export { ArticleTags };
@@ -0,0 +1,31 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+
3
+ /** Tag chip row for blog articles. */
4
+
5
+ function Root({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="article-tags"
9
+ className={cn("flex flex-wrap gap-2", className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function Tag({ className, ...props }: React.ComponentProps<"span">) {
16
+ return (
17
+ <span
18
+ data-slot="article-tag"
19
+ className={cn(
20
+ "inline-flex items-center rounded-full bg-provider/10 px-2.5 py-0.5 text-xs font-medium text-provider transition-colors",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ export const ArticleTags = {
29
+ Root,
30
+ Tag,
31
+ };
@@ -0,0 +1,197 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+
3
+ import { accentOrangeBlobClass, brandGradientClass } from "#/lib/tones.ts";
4
+
5
+ /**
6
+ * Bento grid with a featured gradient tile. Compose: Root > FeaturedItem (FeaturedContent >
7
+ * FeaturedIcon/FeaturedLabel/FeaturedTitle + FeaturedDescription) + Item
8
+ * (ItemIcon + ItemBody > ItemTitle/ItemDescription).
9
+ */
10
+
11
+ function Root({ className, ...props }: React.ComponentProps<"div">) {
12
+ return (
13
+ <div
14
+ data-slot="bento-grid"
15
+ className={cn("grid gap-6 lg:grid-cols-3", className)}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ /** The gradient hero tile — spans two rows on desktop, owns the blur blobs. */
22
+ function FeaturedItem({
23
+ className,
24
+ children,
25
+ ...props
26
+ }: React.ComponentProps<"article">) {
27
+ return (
28
+ <article
29
+ data-slot="bento-featured-item"
30
+ className={cn(
31
+ "animate-fade-up relative flex flex-col justify-between overflow-hidden rounded-2xl p-8 text-primary-foreground shadow-xl lg:row-span-2",
32
+ brandGradientClass,
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ <div className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-white/10 blur-2xl" />
38
+ <div
39
+ className={cn(
40
+ "pointer-events-none absolute -bottom-20 -left-10 h-56 w-56 rounded-full blur-3xl",
41
+ accentOrangeBlobClass,
42
+ )}
43
+ />
44
+ {children}
45
+ </article>
46
+ );
47
+ }
48
+
49
+ function FeaturedContent({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="bento-featured-content"
53
+ className={cn("relative", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function FeaturedIcon({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="bento-featured-icon"
63
+ className={cn(
64
+ "mb-6 inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-white/15 backdrop-blur [&_svg]:h-7 [&_svg]:w-7",
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function FeaturedLabel({ className, ...props }: React.ComponentProps<"p">) {
73
+ return (
74
+ <p
75
+ data-slot="bento-featured-label"
76
+ className={cn(
77
+ "mb-2 text-xs font-semibold uppercase tracking-[0.2em] text-white/70",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function FeaturedTitle({ className, ...props }: React.ComponentProps<"h3">) {
86
+ return (
87
+ <h3
88
+ data-slot="bento-featured-title"
89
+ className={cn(
90
+ "font-heading text-5xl font-bold tracking-tight",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function FeaturedDescription({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<"p">) {
102
+ return (
103
+ <p
104
+ data-slot="bento-featured-description"
105
+ className={cn(
106
+ "relative mt-6 text-base leading-relaxed text-white/90",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ interface BentoGridItemProps extends React.ComponentProps<"article"> {
115
+ /** Spans two columns on desktop (the last tile of an odd set). */
116
+ wide?: boolean;
117
+ }
118
+
119
+ function Item({ wide = false, className, ...props }: BentoGridItemProps) {
120
+ return (
121
+ <article
122
+ data-slot="bento-item"
123
+ className={cn(
124
+ "animate-fade-up group rounded-2xl border border-border bg-card p-6 shadow-sm transition-all hover:-translate-y-1 hover:border-primary/20 hover:shadow-md lg:p-8",
125
+ wide && "lg:col-span-2",
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ /** Icon tile + text laid out side by side. */
134
+ function ItemLayout({ className, ...props }: React.ComponentProps<"div">) {
135
+ return (
136
+ <div
137
+ data-slot="bento-item-layout"
138
+ className={cn("flex items-start gap-4", className)}
139
+ {...props}
140
+ />
141
+ );
142
+ }
143
+
144
+ function ItemIcon({ className, ...props }: React.ComponentProps<"div">) {
145
+ return (
146
+ <div
147
+ data-slot="bento-item-icon"
148
+ className={cn(
149
+ "flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary/15 [&_svg]:h-6 [&_svg]:w-6",
150
+ className,
151
+ )}
152
+ {...props}
153
+ />
154
+ );
155
+ }
156
+
157
+ function ItemBody({ className, ...props }: React.ComponentProps<"div">) {
158
+ return <div data-slot="bento-item-body" className={className} {...props} />;
159
+ }
160
+
161
+ function ItemTitle({ className, ...props }: React.ComponentProps<"h3">) {
162
+ return (
163
+ <h3
164
+ data-slot="bento-item-title"
165
+ className={cn("mb-2 font-heading text-xl font-semibold", className)}
166
+ {...props}
167
+ />
168
+ );
169
+ }
170
+
171
+ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
172
+ return (
173
+ <p
174
+ data-slot="bento-item-description"
175
+ className={cn("text-sm leading-relaxed text-muted-foreground", className)}
176
+ {...props}
177
+ />
178
+ );
179
+ }
180
+
181
+ export const BentoGrid = {
182
+ Root,
183
+ FeaturedItem,
184
+ FeaturedContent,
185
+ FeaturedIcon,
186
+ FeaturedLabel,
187
+ FeaturedTitle,
188
+ FeaturedDescription,
189
+ Item,
190
+ ItemLayout,
191
+ ItemIcon,
192
+ ItemBody,
193
+ ItemTitle,
194
+ ItemDescription,
195
+ };
196
+
197
+ export type { BentoGridItemProps };
@@ -0,0 +1,209 @@
1
+ import { CheckCircleIcon, XCircleIcon } from "@phosphor-icons/react";
2
+ import { cn } from "@voila.dev/ui/lib/utils";
3
+ import { createContext, useContext } from "react";
4
+
5
+ import { Container } from "#/components/container.tsx";
6
+ import { Section, type SectionProps } from "#/components/section.tsx";
7
+ import { type Tone, tonePanelClass, toneTextClass } from "#/lib/tones.ts";
8
+
9
+ /**
10
+ * Before/after benefits split (crossed-out list vs checked list + tag chips +
11
+ * illustration).
12
+ * Compose: Root (tone) > Content (Eyebrow, Heading, Panels > Panel >
13
+ * PanelTitle + PanelList > PanelItem, TagList > Tag, actions) + Media.
14
+ */
15
+
16
+ const ComparisonToneContext = createContext<Tone>("primary");
17
+
18
+ type ComparisonPanelVariant = "without" | "with";
19
+
20
+ const ComparisonPanelContext = createContext<ComparisonPanelVariant>("with");
21
+
22
+ interface ComparisonSectionRootProps extends SectionProps {
23
+ tone?: Tone;
24
+ }
25
+
26
+ function Root({
27
+ tone = "primary",
28
+ spacing = "lg",
29
+ background,
30
+ className,
31
+ children,
32
+ ...props
33
+ }: ComparisonSectionRootProps) {
34
+ return (
35
+ <ComparisonToneContext.Provider value={tone}>
36
+ {/* overflow-hidden: the Media illustrations carry decorative blur blobs
37
+ that would otherwise create a few pixels of horizontal scroll. */}
38
+ <Section
39
+ spacing={spacing}
40
+ background={background}
41
+ className={cn("overflow-hidden", className)}
42
+ {...props}
43
+ >
44
+ <Container>
45
+ <div className="grid items-center gap-12 lg:grid-cols-2">
46
+ {children}
47
+ </div>
48
+ </Container>
49
+ </Section>
50
+ </ComparisonToneContext.Provider>
51
+ );
52
+ }
53
+
54
+ function Content({ className, ...props }: React.ComponentProps<"div">) {
55
+ return (
56
+ <div
57
+ data-slot="comparison-content"
58
+ className={cn("min-w-0", className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function Panels({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="comparison-panels"
68
+ className={cn("mb-6 space-y-6", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ interface ComparisonPanelProps extends React.ComponentProps<"div"> {
75
+ variant: ComparisonPanelVariant;
76
+ }
77
+
78
+ function Panel({ variant, className, ...props }: ComparisonPanelProps) {
79
+ const tone = useContext(ComparisonToneContext);
80
+
81
+ return (
82
+ <ComparisonPanelContext.Provider value={variant}>
83
+ <div
84
+ data-slot="comparison-panel"
85
+ className={cn(
86
+ "animate-fade-up rounded-2xl border p-5",
87
+ variant === "without"
88
+ ? "border-border bg-muted/50"
89
+ : tonePanelClass[tone],
90
+ className,
91
+ )}
92
+ {...props}
93
+ />
94
+ </ComparisonPanelContext.Provider>
95
+ );
96
+ }
97
+
98
+ function PanelTitle({ className, ...props }: React.ComponentProps<"p">) {
99
+ const tone = useContext(ComparisonToneContext);
100
+ const variant = useContext(ComparisonPanelContext);
101
+
102
+ return (
103
+ <p
104
+ data-slot="comparison-panel-title"
105
+ className={cn(
106
+ "mb-3 text-sm font-semibold uppercase tracking-wide",
107
+ variant === "without" ? "text-muted-foreground" : toneTextClass[tone],
108
+ className,
109
+ )}
110
+ {...props}
111
+ />
112
+ );
113
+ }
114
+
115
+ function PanelList({ className, ...props }: React.ComponentProps<"ul">) {
116
+ return (
117
+ <ul
118
+ data-slot="comparison-panel-list"
119
+ className={cn("space-y-3", className)}
120
+ {...props}
121
+ />
122
+ );
123
+ }
124
+
125
+ interface ComparisonPanelItemProps extends React.ComponentProps<"li"> {
126
+ /** Replaces the default XCircle/CheckCircle mark. */
127
+ icon?: React.ReactNode;
128
+ }
129
+
130
+ function PanelItem({
131
+ icon,
132
+ className,
133
+ children,
134
+ ...props
135
+ }: ComparisonPanelItemProps) {
136
+ const variant = useContext(ComparisonPanelContext);
137
+
138
+ return (
139
+ <li
140
+ data-slot="comparison-panel-item"
141
+ className={cn("flex animate-fade-up items-start gap-3", className)}
142
+ {...props}
143
+ >
144
+ {icon ??
145
+ (variant === "without" ? (
146
+ <XCircleIcon className="mt-0.5 h-5 w-5 shrink-0 text-destructive" />
147
+ ) : (
148
+ <CheckCircleIcon className="mt-0.5 h-5 w-5 shrink-0 text-success" />
149
+ ))}
150
+ <span className={cn(variant === "without" && "text-muted-foreground")}>
151
+ {children}
152
+ </span>
153
+ </li>
154
+ );
155
+ }
156
+
157
+ function TagList({ className, ...props }: React.ComponentProps<"div">) {
158
+ return (
159
+ <div
160
+ data-slot="comparison-tag-list"
161
+ className={cn("mb-8 flex flex-wrap gap-2", className)}
162
+ {...props}
163
+ />
164
+ );
165
+ }
166
+
167
+ function Tag({ className, ...props }: React.ComponentProps<"span">) {
168
+ return (
169
+ <span
170
+ data-slot="comparison-tag"
171
+ className={cn(
172
+ "rounded-full border border-border bg-background px-3 py-1 text-sm",
173
+ className,
174
+ )}
175
+ {...props}
176
+ />
177
+ );
178
+ }
179
+
180
+ /** Illustration column — hidden below `lg`. */
181
+ function Media({ className, ...props }: React.ComponentProps<"div">) {
182
+ return (
183
+ <div
184
+ data-slot="comparison-media"
185
+ className={cn("hidden lg:block", className)}
186
+ {...props}
187
+ />
188
+ );
189
+ }
190
+
191
+ export const ComparisonSection = {
192
+ Root,
193
+ Content,
194
+ Panels,
195
+ Panel,
196
+ PanelTitle,
197
+ PanelList,
198
+ PanelItem,
199
+ TagList,
200
+ Tag,
201
+ Media,
202
+ };
203
+
204
+ export type {
205
+ ComparisonPanelItemProps,
206
+ ComparisonPanelProps,
207
+ ComparisonPanelVariant,
208
+ ComparisonSectionRootProps,
209
+ };