@ugurdemirel/landcraft 0.1.0
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/README.md +132 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +4521 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5979 -0
- package/package.json +67 -0
- package/src/components/Badge.tsx +68 -0
- package/src/components/Blog.tsx +243 -0
- package/src/components/Button.tsx +94 -0
- package/src/components/CTA.tsx +155 -0
- package/src/components/Card.tsx +80 -0
- package/src/components/Container.tsx +86 -0
- package/src/components/FAQ.tsx +163 -0
- package/src/components/Features.tsx +153 -0
- package/src/components/Footer.tsx +145 -0
- package/src/components/Hero.tsx +169 -0
- package/src/components/LogoCloud.tsx +100 -0
- package/src/components/MegaMenu.tsx +468 -0
- package/src/components/Navbar.tsx +183 -0
- package/src/components/Newsletter.tsx +180 -0
- package/src/components/Pricing.tsx +336 -0
- package/src/components/Prose.tsx +55 -0
- package/src/components/Section.tsx +106 -0
- package/src/components/Stats.tsx +181 -0
- package/src/components/Testimonials.tsx +205 -0
- package/src/icons.tsx +290 -0
- package/src/index.ts +111 -0
- package/src/styles/index.css +87 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/contrast.ts +107 -0
- package/src/utils/useTokenForeground.ts +38 -0
- package/tailwind.preset.cjs +121 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { useId, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../utils/cn";
|
|
3
|
+
import { getContrastText } from "../utils/contrast";
|
|
4
|
+
import { ChevronDown, Menu, X } from "../icons";
|
|
5
|
+
|
|
6
|
+
export interface MegaMenuLink {
|
|
7
|
+
label: string;
|
|
8
|
+
href: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MegaMenuColumn {
|
|
13
|
+
title?: string;
|
|
14
|
+
links: MegaMenuLink[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MegaMenuFeatured {
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
href: string;
|
|
21
|
+
cta?: string;
|
|
22
|
+
/** Background color for the card; glyph text color is computed for contrast. */
|
|
23
|
+
accent?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MegaMenuItem {
|
|
27
|
+
label: string;
|
|
28
|
+
href?: string;
|
|
29
|
+
/** When set, the item becomes a trigger that opens a mega panel. */
|
|
30
|
+
columns?: MegaMenuColumn[];
|
|
31
|
+
/** Optional highlighted card pinned to the right side of the panel. */
|
|
32
|
+
featured?: MegaMenuFeatured;
|
|
33
|
+
/** Small pill rendered next to the trigger label. */
|
|
34
|
+
badge?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MegaMenuProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
38
|
+
variant?: "classic" | "floating" | "inverse";
|
|
39
|
+
/** Wordmark rendered when no logo is supplied. */
|
|
40
|
+
brand?: ReactNode;
|
|
41
|
+
/** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
|
|
42
|
+
logo?: ReactNode;
|
|
43
|
+
/** Quick logo image source. */
|
|
44
|
+
logoSrc?: string;
|
|
45
|
+
logoAlt?: string;
|
|
46
|
+
logoClassName?: string;
|
|
47
|
+
brandHref?: string;
|
|
48
|
+
items: MegaMenuItem[];
|
|
49
|
+
/** Rendered at the right side of the bar (desktop) and inside the mobile panel. */
|
|
50
|
+
actions?: ReactNode;
|
|
51
|
+
cta?: ReactNode;
|
|
52
|
+
sticky?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const triggerStyles = {
|
|
56
|
+
classic: "text-muted-foreground hover:text-foreground hover:bg-surface",
|
|
57
|
+
floating: "text-muted-foreground hover:text-foreground hover:bg-surface",
|
|
58
|
+
inverse: "text-on-secondary/60 hover:text-on-secondary hover:bg-white/10",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const MegaMenu = ({
|
|
62
|
+
className,
|
|
63
|
+
variant = "classic",
|
|
64
|
+
brand,
|
|
65
|
+
logo,
|
|
66
|
+
logoSrc,
|
|
67
|
+
logoAlt,
|
|
68
|
+
logoClassName,
|
|
69
|
+
brandHref = "#",
|
|
70
|
+
items,
|
|
71
|
+
actions,
|
|
72
|
+
cta,
|
|
73
|
+
sticky = true,
|
|
74
|
+
onKeyDown,
|
|
75
|
+
...props
|
|
76
|
+
}: MegaMenuProps) => {
|
|
77
|
+
const ids = useId();
|
|
78
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
79
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
80
|
+
const [mobileExpanded, setMobileExpanded] = useState<number | null>(null);
|
|
81
|
+
|
|
82
|
+
const inverse = variant === "inverse";
|
|
83
|
+
|
|
84
|
+
const closePanels = () => setOpenIndex(null);
|
|
85
|
+
const closeAll = () => {
|
|
86
|
+
setOpenIndex(null);
|
|
87
|
+
setMobileOpen(false);
|
|
88
|
+
setMobileExpanded(null);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const panelBackground = inverse
|
|
92
|
+
? "border-white/10 bg-[#161616]"
|
|
93
|
+
: "border-border bg-surface shadow-raised";
|
|
94
|
+
|
|
95
|
+
const panelId = (i: number) => `${ids}-panel-${i}`;
|
|
96
|
+
|
|
97
|
+
const shell = cn(
|
|
98
|
+
"relative w-full transition-colors duration-200",
|
|
99
|
+
sticky && "sticky top-0 z-50",
|
|
100
|
+
variant === "classic" &&
|
|
101
|
+
"border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
102
|
+
variant === "inverse" && "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const bar = cn(
|
|
106
|
+
"relative flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200",
|
|
107
|
+
variant === "floating" &&
|
|
108
|
+
"mx-auto mt-3 max-w-5xl rounded-xl border border-border bg-surface/90 px-5 shadow-soft backdrop-blur-md supports-[backdrop-filter]:bg-surface/70",
|
|
109
|
+
variant !== "floating" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const logoNode =
|
|
113
|
+
logo ??
|
|
114
|
+
(logoSrc ? (
|
|
115
|
+
<img
|
|
116
|
+
src={logoSrc}
|
|
117
|
+
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
118
|
+
className={cn("h-7 w-auto", logoClassName)}
|
|
119
|
+
/>
|
|
120
|
+
) : null);
|
|
121
|
+
|
|
122
|
+
const brandNode = logoNode ? (
|
|
123
|
+
<span className="flex items-center">{logoNode}</span>
|
|
124
|
+
) : (
|
|
125
|
+
<span
|
|
126
|
+
className={cn(
|
|
127
|
+
"flex items-center gap-2 font-display text-[17px] font-bold tracking-tight",
|
|
128
|
+
inverse ? "text-on-secondary" : "text-foreground",
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<span
|
|
132
|
+
aria-hidden
|
|
133
|
+
className={cn(
|
|
134
|
+
"inline-block h-2.5 w-2.5 rounded-sm",
|
|
135
|
+
inverse ? "bg-on-secondary" : "bg-foreground",
|
|
136
|
+
)}
|
|
137
|
+
/>
|
|
138
|
+
{brand}
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const triggerClasses = cn(
|
|
143
|
+
"flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150",
|
|
144
|
+
triggerStyles[variant],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const renderColumns = (item: MegaMenuItem) => (
|
|
148
|
+
<div
|
|
149
|
+
className={cn(
|
|
150
|
+
"grid gap-x-10 gap-y-8 sm:grid-cols-2",
|
|
151
|
+
item.featured ? "" : "lg:grid-cols-3",
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
{item.columns?.map((column, ci) => (
|
|
155
|
+
<div key={column.title ?? ci}>
|
|
156
|
+
{column.title ? (
|
|
157
|
+
<h4
|
|
158
|
+
className={cn(
|
|
159
|
+
"text-xs font-semibold uppercase tracking-[0.14em]",
|
|
160
|
+
inverse ? "text-on-secondary/45" : "text-muted-foreground/70",
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{column.title}
|
|
164
|
+
</h4>
|
|
165
|
+
) : null}
|
|
166
|
+
<ul className={cn(column.title && "mt-3", "space-y-0.5")}>
|
|
167
|
+
{column.links.map((link) => (
|
|
168
|
+
<li key={link.href}>
|
|
169
|
+
<a
|
|
170
|
+
href={link.href}
|
|
171
|
+
onClick={closeAll}
|
|
172
|
+
className={cn(
|
|
173
|
+
"-mx-2 block rounded-md px-2 py-2 transition-colors duration-150",
|
|
174
|
+
inverse
|
|
175
|
+
? "hover:bg-white/10"
|
|
176
|
+
: "hover:bg-surface-strong",
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
<span className="block text-sm font-medium text-foreground">
|
|
180
|
+
{link.label}
|
|
181
|
+
</span>
|
|
182
|
+
{link.description ? (
|
|
183
|
+
<span
|
|
184
|
+
className={cn(
|
|
185
|
+
"mt-0.5 block text-[13px] leading-snug",
|
|
186
|
+
inverse ? "text-on-secondary/50" : "text-muted-foreground",
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{link.description}
|
|
190
|
+
</span>
|
|
191
|
+
) : null}
|
|
192
|
+
</a>
|
|
193
|
+
</li>
|
|
194
|
+
))}
|
|
195
|
+
</ul>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const renderFeatured = (item: MegaMenuItem, mobile = false) => {
|
|
202
|
+
const featured = item.featured;
|
|
203
|
+
if (!featured) return null;
|
|
204
|
+
const featuredStyle = featured.accent
|
|
205
|
+
? { backgroundColor: featured.accent, color: getContrastText(featured.accent) }
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<a
|
|
210
|
+
href={featured.href}
|
|
211
|
+
onClick={closeAll}
|
|
212
|
+
className={cn(
|
|
213
|
+
"group relative block overflow-hidden rounded-lg p-6 transition-transform duration-150 hover:-translate-y-0.5",
|
|
214
|
+
mobile && "mt-8",
|
|
215
|
+
!featured.accent && "bg-primary text-on-primary",
|
|
216
|
+
)}
|
|
217
|
+
style={featuredStyle}
|
|
218
|
+
>
|
|
219
|
+
<div className="flex h-full flex-col justify-between gap-4">
|
|
220
|
+
<div>
|
|
221
|
+
<h4 className="font-display text-base font-semibold tracking-tight">
|
|
222
|
+
{featured.title}
|
|
223
|
+
</h4>
|
|
224
|
+
{featured.description ? (
|
|
225
|
+
<p className="mt-1.5 text-[13px] leading-relaxed opacity-80">
|
|
226
|
+
{featured.description}
|
|
227
|
+
</p>
|
|
228
|
+
) : null}
|
|
229
|
+
</div>
|
|
230
|
+
{featured.cta ? (
|
|
231
|
+
<span className="text-sm font-semibold underline decoration-1 underline-offset-4">
|
|
232
|
+
{featured.cta} →
|
|
233
|
+
</span>
|
|
234
|
+
) : null}
|
|
235
|
+
</div>
|
|
236
|
+
<div
|
|
237
|
+
aria-hidden
|
|
238
|
+
className="pointer-events-none absolute -right-8 -top-8 h-28 w-28 rounded-full opacity-20 blur-2xl bg-black/40"
|
|
239
|
+
/>
|
|
240
|
+
</a>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
245
|
+
if (event.key === "Escape") {
|
|
246
|
+
closeAll();
|
|
247
|
+
}
|
|
248
|
+
onKeyDown?.(event);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<header
|
|
253
|
+
className={cn(shell, variant === "floating" && "px-3 sm:px-6", className)}
|
|
254
|
+
onKeyDown={handleKeyDown}
|
|
255
|
+
{...props}
|
|
256
|
+
>
|
|
257
|
+
<nav className={bar} onMouseLeave={closePanels} aria-label="Mega menu">
|
|
258
|
+
<a href={brandHref} className="shrink-0">
|
|
259
|
+
{brandNode}
|
|
260
|
+
</a>
|
|
261
|
+
|
|
262
|
+
<ul className="hidden items-center gap-1 lg:flex">
|
|
263
|
+
{items.map((item, i) => {
|
|
264
|
+
const hasPanel = Boolean(item.columns?.length || item.featured);
|
|
265
|
+
const open = openIndex === i;
|
|
266
|
+
return (
|
|
267
|
+
<li key={item.label} className="relative">
|
|
268
|
+
{hasPanel ? (
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
aria-haspopup="true"
|
|
272
|
+
aria-expanded={open}
|
|
273
|
+
aria-controls={panelId(i)}
|
|
274
|
+
onClick={() => setOpenIndex(open ? null : i)}
|
|
275
|
+
onMouseEnter={() => setOpenIndex(i)}
|
|
276
|
+
className={cn(
|
|
277
|
+
triggerClasses,
|
|
278
|
+
open &&
|
|
279
|
+
(inverse
|
|
280
|
+
? "bg-white/10 text-on-secondary"
|
|
281
|
+
: "bg-surface text-foreground"),
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
<span className="flex items-center gap-1.5">
|
|
285
|
+
{item.label}
|
|
286
|
+
{item.badge ? (
|
|
287
|
+
<span
|
|
288
|
+
className={cn(
|
|
289
|
+
"rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide",
|
|
290
|
+
inverse
|
|
291
|
+
? "bg-white/15 text-on-secondary"
|
|
292
|
+
: "bg-primary-soft text-primary",
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
{item.badge}
|
|
296
|
+
</span>
|
|
297
|
+
) : null}
|
|
298
|
+
</span>
|
|
299
|
+
<ChevronDown
|
|
300
|
+
className={cn(
|
|
301
|
+
"h-4 w-4 shrink-0 transition-transform duration-150",
|
|
302
|
+
open && "rotate-180",
|
|
303
|
+
)}
|
|
304
|
+
/>
|
|
305
|
+
</button>
|
|
306
|
+
) : (
|
|
307
|
+
<a href={item.href ?? "#"} className={triggerClasses}>
|
|
308
|
+
{item.label}
|
|
309
|
+
{item.badge ? (
|
|
310
|
+
<span className="rounded-full bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
|
|
311
|
+
{item.badge}
|
|
312
|
+
</span>
|
|
313
|
+
) : null}
|
|
314
|
+
</a>
|
|
315
|
+
)}
|
|
316
|
+
</li>
|
|
317
|
+
);
|
|
318
|
+
})}
|
|
319
|
+
</ul>
|
|
320
|
+
|
|
321
|
+
<div className="hidden items-center gap-3 lg:flex">
|
|
322
|
+
{actions}
|
|
323
|
+
{cta}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
|
329
|
+
aria-expanded={mobileOpen}
|
|
330
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
331
|
+
className={cn(
|
|
332
|
+
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md lg:hidden",
|
|
333
|
+
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
334
|
+
inverse ? "text-on-secondary hover:bg-white/10" : "text-foreground hover:bg-surface",
|
|
335
|
+
)}
|
|
336
|
+
>
|
|
337
|
+
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
338
|
+
</button>
|
|
339
|
+
</nav>
|
|
340
|
+
|
|
341
|
+
{/* Desktop mega panels */}
|
|
342
|
+
{items.map((item, i) => {
|
|
343
|
+
if (!item.columns?.length && !item.featured) return null;
|
|
344
|
+
const open = openIndex === i;
|
|
345
|
+
return (
|
|
346
|
+
<div
|
|
347
|
+
key={item.label}
|
|
348
|
+
id={panelId(i)}
|
|
349
|
+
role="region"
|
|
350
|
+
aria-label={`${item.label} menu`}
|
|
351
|
+
onMouseEnter={() => setOpenIndex(i)}
|
|
352
|
+
className={cn(
|
|
353
|
+
"absolute inset-x-0 top-full z-50 pt-2 transition-all duration-150",
|
|
354
|
+
open
|
|
355
|
+
? "visible translate-y-0 opacity-100"
|
|
356
|
+
: "pointer-events-none invisible -translate-y-1 opacity-0",
|
|
357
|
+
)}
|
|
358
|
+
>
|
|
359
|
+
<div
|
|
360
|
+
className={cn(
|
|
361
|
+
"overflow-hidden rounded-xl border p-6 md:p-8",
|
|
362
|
+
panelBackground,
|
|
363
|
+
item.featured ? "lg:grid lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-12" : "",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{renderColumns(item)}
|
|
367
|
+
{item.featured && (
|
|
368
|
+
<div className={cn("mt-8 max-w-sm lg:mt-0 lg:w-64")}>
|
|
369
|
+
{renderFeatured(item)}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
})}
|
|
376
|
+
|
|
377
|
+
{/* Mobile accordion panel */}
|
|
378
|
+
<div
|
|
379
|
+
className={cn(
|
|
380
|
+
"grid transition-[grid-template-rows,opacity] duration-200 lg:hidden",
|
|
381
|
+
mobileOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
382
|
+
inverse
|
|
383
|
+
? "border-t border-white/10 bg-[#101010]"
|
|
384
|
+
: variant === "floating"
|
|
385
|
+
? "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface"
|
|
386
|
+
: "border-t border-border bg-background",
|
|
387
|
+
)}
|
|
388
|
+
>
|
|
389
|
+
<div className="min-h-0 overflow-hidden">
|
|
390
|
+
<ul className="flex flex-col py-4">
|
|
391
|
+
{items.map((item, i) =>
|
|
392
|
+
item.columns?.length || item.featured ? (
|
|
393
|
+
<li key={item.label} className="px-5">
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
aria-expanded={mobileExpanded === i}
|
|
397
|
+
onClick={() =>
|
|
398
|
+
setMobileExpanded((prev) => (prev === i ? null : i))
|
|
399
|
+
}
|
|
400
|
+
className={cn(
|
|
401
|
+
"flex w-full items-center justify-between rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
402
|
+
inverse
|
|
403
|
+
? "text-on-secondary hover:bg-white/10"
|
|
404
|
+
: "text-foreground hover:bg-surface",
|
|
405
|
+
)}
|
|
406
|
+
>
|
|
407
|
+
<span className="flex items-center gap-2">
|
|
408
|
+
{item.label}
|
|
409
|
+
{item.badge ? (
|
|
410
|
+
<span className="rounded-full bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
|
|
411
|
+
{item.badge}
|
|
412
|
+
</span>
|
|
413
|
+
) : null}
|
|
414
|
+
</span>
|
|
415
|
+
<ChevronDown
|
|
416
|
+
className={cn(
|
|
417
|
+
"h-4 w-4 shrink-0 transition-transform duration-150",
|
|
418
|
+
mobileExpanded === i && "rotate-180",
|
|
419
|
+
)}
|
|
420
|
+
/>
|
|
421
|
+
</button>
|
|
422
|
+
<div
|
|
423
|
+
className={cn(
|
|
424
|
+
"grid transition-[grid-template-rows,opacity] duration-200",
|
|
425
|
+
mobileExpanded === i
|
|
426
|
+
? "grid-rows-[1fr] opacity-100"
|
|
427
|
+
: "grid-rows-[0fr] opacity-0",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
<div className="min-h-0 overflow-hidden">
|
|
431
|
+
<div className="py-2 pl-1">
|
|
432
|
+
{renderColumns(item)}
|
|
433
|
+
{item.featured ? (
|
|
434
|
+
<div className="max-w-sm">{renderFeatured(item, true)}</div>
|
|
435
|
+
) : null}
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</li>
|
|
440
|
+
) : (
|
|
441
|
+
<li key={item.label} className="px-5">
|
|
442
|
+
<a
|
|
443
|
+
href={item.href ?? "#"}
|
|
444
|
+
onClick={() => setMobileOpen(false)}
|
|
445
|
+
className={cn(
|
|
446
|
+
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
447
|
+
inverse
|
|
448
|
+
? "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary"
|
|
449
|
+
: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
450
|
+
)}
|
|
451
|
+
>
|
|
452
|
+
{item.label}
|
|
453
|
+
</a>
|
|
454
|
+
</li>
|
|
455
|
+
),
|
|
456
|
+
)}
|
|
457
|
+
{cta ? (
|
|
458
|
+
<li className="px-8 pb-2 pt-3">{cta}</li>
|
|
459
|
+
) : actions ? (
|
|
460
|
+
<li className="mt-3 flex flex-wrap gap-3 px-8 pb-2">{actions}</li>
|
|
461
|
+
) : null}
|
|
462
|
+
</ul>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</header>
|
|
466
|
+
);
|
|
467
|
+
};
|
|
468
|
+
MegaMenu.displayName = "MegaMenu";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../utils/cn";
|
|
3
|
+
import { Menu, X } from "../icons";
|
|
4
|
+
|
|
5
|
+
export interface NavLink {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NavbarProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
+
variant?: "classic" | "floating" | "inverse";
|
|
12
|
+
/** Wordmark or fallback text rendered when no logo is supplied. */
|
|
13
|
+
brand?: ReactNode;
|
|
14
|
+
/** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
|
|
15
|
+
logo?: ReactNode;
|
|
16
|
+
/** Quick logo image source. */
|
|
17
|
+
logoSrc?: string;
|
|
18
|
+
logoAlt?: string;
|
|
19
|
+
/** Size / position tuning for logo images. Defaults to `h-7 w-auto`. */
|
|
20
|
+
logoClassName?: string;
|
|
21
|
+
brandHref?: string;
|
|
22
|
+
links: NavLink[];
|
|
23
|
+
actions?: ReactNode;
|
|
24
|
+
sticky?: boolean;
|
|
25
|
+
cta?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const linkStyles = {
|
|
29
|
+
classic: "text-muted-foreground hover:text-foreground",
|
|
30
|
+
floating: "text-muted-foreground hover:text-foreground",
|
|
31
|
+
inverse: "text-on-secondary/60 hover:text-on-secondary",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Navbar = ({
|
|
35
|
+
className,
|
|
36
|
+
variant = "classic",
|
|
37
|
+
brand,
|
|
38
|
+
logo,
|
|
39
|
+
logoSrc,
|
|
40
|
+
logoAlt,
|
|
41
|
+
logoClassName,
|
|
42
|
+
brandHref = "#",
|
|
43
|
+
links,
|
|
44
|
+
actions,
|
|
45
|
+
cta,
|
|
46
|
+
sticky = true,
|
|
47
|
+
...props
|
|
48
|
+
}: NavbarProps) => {
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const inverse = variant === "inverse";
|
|
51
|
+
|
|
52
|
+
const shell = cn(
|
|
53
|
+
"w-full transition-colors duration-200",
|
|
54
|
+
sticky && "sticky top-0 z-50",
|
|
55
|
+
variant === "classic" &&
|
|
56
|
+
"border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
57
|
+
variant === "floating" && "px-0",
|
|
58
|
+
variant === "inverse" && "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const bar = cn(
|
|
62
|
+
"flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200",
|
|
63
|
+
variant === "floating" &&
|
|
64
|
+
"mx-auto mt-3 max-w-5xl rounded-xl border border-border bg-surface/90 px-5 shadow-soft backdrop-blur-md supports-[backdrop-filter]:bg-surface/70",
|
|
65
|
+
variant === "classic" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
66
|
+
variant === "inverse" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const logoNode =
|
|
70
|
+
logo ??
|
|
71
|
+
(logoSrc ? (
|
|
72
|
+
<img
|
|
73
|
+
src={logoSrc}
|
|
74
|
+
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
75
|
+
className={cn("h-7 w-auto", logoClassName)}
|
|
76
|
+
/>
|
|
77
|
+
) : null);
|
|
78
|
+
|
|
79
|
+
const brandNode = logoNode ? (
|
|
80
|
+
<span className="flex items-center">{logoNode}</span>
|
|
81
|
+
) : (
|
|
82
|
+
<span
|
|
83
|
+
className={cn(
|
|
84
|
+
"flex items-center gap-2 font-display text-[17px] font-bold tracking-tight",
|
|
85
|
+
inverse ? "text-on-secondary" : "text-foreground",
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
<span
|
|
89
|
+
aria-hidden
|
|
90
|
+
className={cn(
|
|
91
|
+
"inline-block h-2.5 w-2.5 rounded-sm",
|
|
92
|
+
inverse ? "bg-on-secondary" : "bg-foreground",
|
|
93
|
+
)}
|
|
94
|
+
/>
|
|
95
|
+
{brand}
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<header className={cn(shell, variant === "floating" && "px-3 sm:px-6", className)} {...props}>
|
|
101
|
+
<nav className={bar} aria-label="Main navigation">
|
|
102
|
+
<a href={brandHref} className="shrink-0">
|
|
103
|
+
{brandNode}
|
|
104
|
+
</a>
|
|
105
|
+
|
|
106
|
+
<ul className="hidden items-center gap-7 md:flex">
|
|
107
|
+
{links.map((link) => (
|
|
108
|
+
<li key={link.href}>
|
|
109
|
+
<a
|
|
110
|
+
href={link.href}
|
|
111
|
+
className={cn(
|
|
112
|
+
"text-sm font-medium transition-colors duration-150",
|
|
113
|
+
linkStyles[variant],
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{link.label}
|
|
117
|
+
</a>
|
|
118
|
+
</li>
|
|
119
|
+
))}
|
|
120
|
+
</ul>
|
|
121
|
+
|
|
122
|
+
<div className="hidden items-center gap-3 md:flex">
|
|
123
|
+
{actions}
|
|
124
|
+
{cta}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
aria-label={open ? "Close menu" : "Open menu"}
|
|
130
|
+
aria-expanded={open}
|
|
131
|
+
onClick={() => setOpen((o) => !o)}
|
|
132
|
+
className={cn(
|
|
133
|
+
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md md:hidden",
|
|
134
|
+
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
135
|
+
inverse ? "text-on-secondary hover:bg-white/10" : "text-foreground hover:bg-surface",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
139
|
+
</button>
|
|
140
|
+
</nav>
|
|
141
|
+
|
|
142
|
+
{/* Mobile panel */}
|
|
143
|
+
<div
|
|
144
|
+
className={cn(
|
|
145
|
+
"grid transition-[grid-template-rows,opacity] duration-200 md:hidden",
|
|
146
|
+
open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
147
|
+
inverse
|
|
148
|
+
? "border-t border-white/10 bg-[#101010]"
|
|
149
|
+
: variant === "floating"
|
|
150
|
+
? "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface"
|
|
151
|
+
: "border-t border-border bg-background",
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<div className="min-h-0 overflow-hidden">
|
|
155
|
+
<ul className={cn("flex flex-col py-4", variant !== "floating" && "px-5")}>
|
|
156
|
+
{links.map((link) => (
|
|
157
|
+
<li key={link.href}>
|
|
158
|
+
<a
|
|
159
|
+
href={link.href}
|
|
160
|
+
onClick={() => setOpen(false)}
|
|
161
|
+
className={cn(
|
|
162
|
+
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
163
|
+
inverse
|
|
164
|
+
? "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary"
|
|
165
|
+
: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{link.label}
|
|
169
|
+
</a>
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
{cta ? (
|
|
173
|
+
<li className="mt-3 px-2 pb-2">{cta}</li>
|
|
174
|
+
) : actions ? (
|
|
175
|
+
<li className="mt-3 flex flex-wrap gap-3 px-2 pb-2">{actions}</li>
|
|
176
|
+
) : null}
|
|
177
|
+
</ul>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</header>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
Navbar.displayName = "Navbar";
|