@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,190 @@
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
+ * Marketing site footer.
9
+ * Compose: Root > Columns (Brand > BrandDescription + SocialLinks >
10
+ * SocialLink…, Column > ColumnTitle + ColumnList > ColumnLink…) + Bottom >
11
+ * BottomText…
12
+ */
13
+
14
+ function Root({
15
+ className,
16
+ children,
17
+ ...props
18
+ }: React.ComponentProps<"footer">) {
19
+ return (
20
+ <footer
21
+ data-slot="site-footer"
22
+ className={cn("border-t border-border bg-muted/30", className)}
23
+ {...props}
24
+ >
25
+ <Container>{children}</Container>
26
+ </footer>
27
+ );
28
+ }
29
+
30
+ function Columns({ className, ...props }: React.ComponentProps<"div">) {
31
+ return (
32
+ <div
33
+ data-slot="site-footer-columns"
34
+ className={cn(
35
+ "grid gap-8 py-12 md:grid-cols-2 md:py-16 lg:grid-cols-6",
36
+ className,
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ /** Brand column — logo + description + social links; spans two columns. */
44
+ function Brand({ className, ...props }: React.ComponentProps<"div">) {
45
+ return (
46
+ <div
47
+ data-slot="site-footer-brand"
48
+ className={cn("lg:col-span-2", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function BrandDescription({ className, ...props }: React.ComponentProps<"p">) {
55
+ return (
56
+ <p
57
+ data-slot="site-footer-brand-description"
58
+ className={cn("mb-6 max-w-sm text-sm text-muted-foreground", className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function SocialLinks({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="site-footer-social-links"
68
+ className={cn("flex items-center gap-4", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ interface SiteFooterSocialLinkProps extends useRender.ComponentProps<"a"> {
75
+ "aria-label": string;
76
+ }
77
+
78
+ function SocialLink({
79
+ className,
80
+ render,
81
+ ...props
82
+ }: SiteFooterSocialLinkProps) {
83
+ return useRender({
84
+ defaultTagName: "a",
85
+ props: mergeProps<"a">(
86
+ {
87
+ className: cn(
88
+ "text-muted-foreground transition-colors hover:text-foreground [&_svg]:h-5 [&_svg]:w-5",
89
+ className,
90
+ ),
91
+ },
92
+ props,
93
+ ),
94
+ render,
95
+ state: {
96
+ slot: "site-footer-social-link",
97
+ },
98
+ });
99
+ }
100
+
101
+ function Column({ className, ...props }: React.ComponentProps<"div">) {
102
+ return (
103
+ <div data-slot="site-footer-column" className={className} {...props} />
104
+ );
105
+ }
106
+
107
+ function ColumnTitle({ className, ...props }: React.ComponentProps<"h3">) {
108
+ return (
109
+ <h3
110
+ data-slot="site-footer-column-title"
111
+ className={cn("mb-4 text-sm font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function ColumnList({ className, ...props }: React.ComponentProps<"ul">) {
118
+ return (
119
+ <ul
120
+ data-slot="site-footer-column-list"
121
+ className={cn("space-y-3", className)}
122
+ {...props}
123
+ />
124
+ );
125
+ }
126
+
127
+ function ColumnLink({
128
+ className,
129
+ render,
130
+ ...props
131
+ }: useRender.ComponentProps<"a">) {
132
+ const anchor = useRender({
133
+ defaultTagName: "a",
134
+ props: mergeProps<"a">(
135
+ {
136
+ className: cn(
137
+ "text-sm text-muted-foreground transition-colors hover:text-foreground",
138
+ className,
139
+ ),
140
+ },
141
+ props,
142
+ ),
143
+ render,
144
+ state: {
145
+ slot: "site-footer-column-link",
146
+ },
147
+ });
148
+
149
+ return <li>{anchor}</li>;
150
+ }
151
+
152
+ function Bottom({ className, ...props }: React.ComponentProps<"div">) {
153
+ return (
154
+ <div
155
+ data-slot="site-footer-bottom"
156
+ className={cn(
157
+ "flex flex-col items-center justify-between gap-4 border-t border-border py-6 md:flex-row",
158
+ className,
159
+ )}
160
+ {...props}
161
+ />
162
+ );
163
+ }
164
+
165
+ function BottomText({ className, ...props }: React.ComponentProps<"p">) {
166
+ return (
167
+ <p
168
+ data-slot="site-footer-bottom-text"
169
+ className={cn("text-sm text-muted-foreground", className)}
170
+ {...props}
171
+ />
172
+ );
173
+ }
174
+
175
+ export const SiteFooter = {
176
+ Root,
177
+ Columns,
178
+ Brand,
179
+ BrandDescription,
180
+ SocialLinks,
181
+ SocialLink,
182
+ Column,
183
+ ColumnTitle,
184
+ ColumnList,
185
+ ColumnLink,
186
+ Bottom,
187
+ BottomText,
188
+ };
189
+
190
+ export type { SiteFooterSocialLinkProps };
@@ -0,0 +1,254 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { ListIcon, XIcon } from "@phosphor-icons/react";
4
+ import { cn } from "@voila.dev/ui/lib/utils";
5
+ import { createContext, useContext, useState } from "react";
6
+
7
+ import { Container } from "#/components/container.tsx";
8
+
9
+ /**
10
+ * Sticky marketing site header. Compose: Root > Bar (Brand + Nav > NavList >
11
+ * NavItem… + Actions + MobileToggle) + MobileMenu > MobileNavItem… +
12
+ * MobileActions. The mobile menu server-renders closed and hydrates.
13
+ */
14
+
15
+ interface SiteHeaderContextValue {
16
+ open: boolean;
17
+ toggle: () => void;
18
+ }
19
+
20
+ const SiteHeaderContext = createContext<SiteHeaderContextValue>({
21
+ open: false,
22
+ toggle: () => {},
23
+ });
24
+
25
+ interface SiteHeaderRootProps extends React.ComponentProps<"header"> {
26
+ defaultOpen?: boolean;
27
+ }
28
+
29
+ function Root({
30
+ defaultOpen = false,
31
+ className,
32
+ ...props
33
+ }: SiteHeaderRootProps) {
34
+ const [open, setOpen] = useState(defaultOpen);
35
+
36
+ return (
37
+ <SiteHeaderContext.Provider
38
+ value={{ open, toggle: () => setOpen((value) => !value) }}
39
+ >
40
+ <header
41
+ data-slot="site-header"
42
+ className={cn(
43
+ "sticky top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-lg",
44
+ className,
45
+ )}
46
+ {...props}
47
+ />
48
+ </SiteHeaderContext.Provider>
49
+ );
50
+ }
51
+
52
+ interface SiteHeaderBarProps extends React.ComponentProps<"nav"> {
53
+ "aria-label"?: string;
54
+ }
55
+
56
+ function Bar({
57
+ className,
58
+ children,
59
+ "aria-label": ariaLabel = "Main",
60
+ ...props
61
+ }: SiteHeaderBarProps) {
62
+ return (
63
+ <Container>
64
+ <nav
65
+ data-slot="site-header-bar"
66
+ aria-label={ariaLabel}
67
+ className={cn("flex h-16 items-center justify-between", className)}
68
+ {...props}
69
+ >
70
+ {children}
71
+ </nav>
72
+ </Container>
73
+ );
74
+ }
75
+
76
+ /** Logo slot — an anchor by default; pass `render` for a router Link. */
77
+ function Brand({ className, render, ...props }: useRender.ComponentProps<"a">) {
78
+ return useRender({
79
+ defaultTagName: "a",
80
+ props: mergeProps<"a">(
81
+ {
82
+ className: cn("flex items-center", className),
83
+ },
84
+ props,
85
+ ),
86
+ render,
87
+ state: {
88
+ slot: "site-header-brand",
89
+ },
90
+ });
91
+ }
92
+
93
+ /** Desktop-only nav area: link list + call-to-action cluster. */
94
+ function Nav({ className, ...props }: React.ComponentProps<"div">) {
95
+ return (
96
+ <div
97
+ data-slot="site-header-nav"
98
+ className={cn("hidden items-center gap-8 md:flex", className)}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function NavList({ className, ...props }: React.ComponentProps<"ul">) {
105
+ return (
106
+ <ul
107
+ data-slot="site-header-nav-list"
108
+ className={cn("flex items-center gap-6", className)}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function NavItem({
115
+ className,
116
+ render,
117
+ ...props
118
+ }: useRender.ComponentProps<"a">) {
119
+ const anchor = useRender({
120
+ defaultTagName: "a",
121
+ props: mergeProps<"a">(
122
+ {
123
+ className: cn(
124
+ "text-sm font-medium text-muted-foreground transition-colors hover:text-foreground",
125
+ className,
126
+ ),
127
+ },
128
+ props,
129
+ ),
130
+ render,
131
+ state: {
132
+ slot: "site-header-nav-item",
133
+ },
134
+ });
135
+
136
+ return <li>{anchor}</li>;
137
+ }
138
+
139
+ function Actions({ className, ...props }: React.ComponentProps<"div">) {
140
+ return (
141
+ <div
142
+ data-slot="site-header-actions"
143
+ className={cn("flex items-center gap-3", className)}
144
+ {...props}
145
+ />
146
+ );
147
+ }
148
+
149
+ interface SiteHeaderMobileToggleProps extends React.ComponentProps<"button"> {
150
+ "aria-label": string;
151
+ }
152
+
153
+ function MobileToggle({ className, ...props }: SiteHeaderMobileToggleProps) {
154
+ const { open, toggle } = useContext(SiteHeaderContext);
155
+
156
+ return (
157
+ <button
158
+ type="button"
159
+ data-slot="site-header-mobile-toggle"
160
+ aria-expanded={open}
161
+ onClick={toggle}
162
+ className={cn(
163
+ "flex h-10 w-10 items-center justify-center rounded-md hover:bg-accent md:hidden",
164
+ className,
165
+ )}
166
+ {...props}
167
+ >
168
+ {open ? <XIcon className="h-6 w-6" /> : <ListIcon className="h-6 w-6" />}
169
+ </button>
170
+ );
171
+ }
172
+
173
+ function MobileMenu({
174
+ className,
175
+ children,
176
+ ...props
177
+ }: React.ComponentProps<"div">) {
178
+ const { open } = useContext(SiteHeaderContext);
179
+
180
+ return (
181
+ <div
182
+ data-slot="site-header-mobile-menu"
183
+ className={cn(
184
+ "border-t border-border bg-background md:hidden",
185
+ !open && "hidden",
186
+ className,
187
+ )}
188
+ {...props}
189
+ >
190
+ <Container>
191
+ <ul className="flex flex-col gap-2 py-4">{children}</ul>
192
+ </Container>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function MobileNavItem({
198
+ className,
199
+ render,
200
+ ...props
201
+ }: useRender.ComponentProps<"a">) {
202
+ const anchor = useRender({
203
+ defaultTagName: "a",
204
+ props: mergeProps<"a">(
205
+ {
206
+ className: cn(
207
+ "block rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
208
+ className,
209
+ ),
210
+ },
211
+ props,
212
+ ),
213
+ render,
214
+ state: {
215
+ slot: "site-header-mobile-nav-item",
216
+ },
217
+ });
218
+
219
+ return <li>{anchor}</li>;
220
+ }
221
+
222
+ /** Call-to-action cluster closing the mobile menu. */
223
+ function MobileActions({ className, ...props }: React.ComponentProps<"li">) {
224
+ return (
225
+ <li
226
+ data-slot="site-header-mobile-actions"
227
+ className={cn(
228
+ "mt-2 flex flex-col gap-2 border-t border-border pt-4",
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+
236
+ export const SiteHeader = {
237
+ Root,
238
+ Bar,
239
+ Brand,
240
+ Nav,
241
+ NavList,
242
+ NavItem,
243
+ Actions,
244
+ MobileToggle,
245
+ MobileMenu,
246
+ MobileNavItem,
247
+ MobileActions,
248
+ };
249
+
250
+ export type {
251
+ SiteHeaderBarProps,
252
+ SiteHeaderMobileToggleProps,
253
+ SiteHeaderRootProps,
254
+ };
@@ -0,0 +1,69 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+
3
+ /**
4
+ * Horizontal row of value/label stats separated by hairline dividers, the kind
5
+ * that usually closes a hero.
6
+ */
7
+
8
+ interface StatsRowRootProps extends React.ComponentProps<"div"> {
9
+ /** Adds the top hairline + spacing used when the row closes a hero. */
10
+ bordered?: boolean;
11
+ }
12
+
13
+ function Root({ bordered = true, className, ...props }: StatsRowRootProps) {
14
+ return (
15
+ <div
16
+ data-slot="stats-row"
17
+ className={cn(
18
+ "flex items-center gap-6 sm:gap-8",
19
+ bordered && "mt-10 border-t border-border/50 pt-8",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function Item({ className, ...props }: React.ComponentProps<"div">) {
28
+ return <div data-slot="stats-row-item" className={className} {...props} />;
29
+ }
30
+
31
+ function Value({ className, ...props }: React.ComponentProps<"p">) {
32
+ return (
33
+ <p
34
+ data-slot="stats-row-value"
35
+ className={cn("text-3xl font-bold text-foreground", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function Label({ className, ...props }: React.ComponentProps<"p">) {
42
+ return (
43
+ <p
44
+ data-slot="stats-row-label"
45
+ className={cn("text-sm text-muted-foreground", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function Divider({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="stats-row-divider"
55
+ className={cn("h-12 w-px bg-border", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ export const StatsRow = {
62
+ Root,
63
+ Item,
64
+ Value,
65
+ Label,
66
+ Divider,
67
+ };
68
+
69
+ export type { StatsRowRootProps };