@ugurdemirel/landcraft 0.1.2 → 0.1.4
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/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +41 -22
- package/dist/index.js +2261 -1937
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -0
- package/package.json +1 -1
- package/src/components/Blog/BlogCard.tsx +12 -0
- package/src/components/Blog/BlogSection.tsx +95 -0
- package/src/components/Blog/index.ts +8 -0
- package/src/components/Blog/parts.tsx +38 -0
- package/src/components/Blog/types.ts +42 -0
- package/src/components/Blog/variants/Card.tsx +62 -0
- package/src/components/Blog/variants/Row.tsx +41 -0
- package/src/components/CTA/CTA.tsx +12 -0
- package/src/components/CTA/index.ts +2 -0
- package/src/components/CTA/parts.tsx +26 -0
- package/src/components/CTA/types.ts +12 -0
- package/src/components/CTA/variants/Inverse.tsx +50 -0
- package/src/components/CTA/variants/Panel.tsx +60 -0
- package/src/components/CTA/variants/Surface.tsx +34 -0
- package/src/components/FAQ/FAQ.tsx +48 -0
- package/src/components/FAQ/index.ts +2 -0
- package/src/components/FAQ/parts.tsx +74 -0
- package/src/components/FAQ/types.ts +18 -0
- package/src/components/FAQ/variants/Accordion.tsx +46 -0
- package/src/components/FAQ/variants/Cards.tsx +40 -0
- package/src/components/FAQ/variants/Split.tsx +44 -0
- package/src/components/Features/FeatureCard.tsx +44 -0
- package/src/components/Features/FeatureGrid.tsx +14 -0
- package/src/components/Features/index.ts +3 -0
- package/src/components/Features/parts.ts +15 -0
- package/src/components/Features/types.ts +25 -0
- package/src/components/Features/variants/Bento.tsx +32 -0
- package/src/components/Features/variants/Columns.tsx +21 -0
- package/src/components/Features/variants/EditorialRows.tsx +38 -0
- package/src/components/Footer/Footer.tsx +11 -0
- package/src/components/Footer/index.ts +2 -0
- package/src/components/Footer/parts.tsx +101 -0
- package/src/components/Footer/types.ts +32 -0
- package/src/components/Footer/variants/Classic.tsx +59 -0
- package/src/components/Footer/variants/Editorial.tsx +47 -0
- package/src/components/Footer/variants/Minimal.tsx +33 -0
- package/src/components/Hero/Hero.tsx +12 -0
- package/src/components/Hero/index.ts +2 -0
- package/src/components/Hero/parts.tsx +29 -0
- package/src/components/Hero/types.ts +20 -0
- package/src/components/Hero/variants/Centered.tsx +54 -0
- package/src/components/Hero/variants/Split.tsx +53 -0
- package/src/components/Hero/variants/Statement.tsx +60 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +37 -0
- package/src/components/LanguageSwitcher/index.ts +6 -0
- package/src/components/LanguageSwitcher/parts.tsx +144 -0
- package/src/components/LanguageSwitcher/types.ts +40 -0
- package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +23 -0
- package/src/components/LanguageSwitcher/variants/Dropdown.tsx +88 -0
- package/src/components/LanguageSwitcher/variants/Modal.tsx +56 -0
- package/src/components/LogoCloud/LogoCloud.tsx +14 -0
- package/src/components/LogoCloud/index.ts +2 -0
- package/src/components/LogoCloud/parts.tsx +32 -0
- package/src/components/LogoCloud/types.ts +16 -0
- package/src/components/LogoCloud/variants/Marquee.tsx +30 -0
- package/src/components/LogoCloud/variants/Quiet.tsx +26 -0
- package/src/components/LogoCloud/variants/Strip.tsx +25 -0
- package/src/components/{MegaMenu.tsx → MegaMenu/MegaMenu.tsx} +108 -126
- package/src/components/MegaMenu/index.ts +9 -0
- package/src/components/MegaMenu/types.ts +56 -0
- package/src/components/Modal.tsx +1 -0
- package/src/components/Navbar/Navbar.tsx +185 -0
- package/src/components/Navbar/index.ts +2 -0
- package/src/components/Navbar/types.ts +30 -0
- package/src/components/Newsletter/Newsletter.tsx +12 -0
- package/src/components/Newsletter/index.ts +2 -0
- package/src/components/Newsletter/parts.tsx +23 -0
- package/src/components/Newsletter/types.ts +12 -0
- package/src/components/Newsletter/useNewsletterForm.ts +27 -0
- package/src/components/Newsletter/variants/Card.tsx +62 -0
- package/src/components/Newsletter/variants/Inline.tsx +54 -0
- package/src/components/Newsletter/variants/Underline.tsx +52 -0
- package/src/components/Pricing/Pricing.tsx +37 -0
- package/src/components/Pricing/index.ts +2 -0
- package/src/components/Pricing/parts.tsx +132 -0
- package/src/components/Pricing/types.ts +28 -0
- package/src/components/Pricing/variants/Bento.tsx +72 -0
- package/src/components/Pricing/variants/Cards.tsx +79 -0
- package/src/components/Pricing/variants/Compact.tsx +69 -0
- package/src/components/Stats/Stats.tsx +16 -0
- package/src/components/Stats/index.ts +2 -0
- package/src/components/Stats/parts.tsx +29 -0
- package/src/components/Stats/types.ts +23 -0
- package/src/components/Stats/variants/Cells.tsx +34 -0
- package/src/components/Stats/variants/Editorial.tsx +41 -0
- package/src/components/Stats/variants/Hairline.tsx +31 -0
- package/src/components/Stats/variants/Ticker.tsx +36 -0
- package/src/components/Testimonials/TestimonialCard.tsx +61 -0
- package/src/components/Testimonials/Testimonials.tsx +14 -0
- package/src/components/Testimonials/index.ts +3 -0
- package/src/components/Testimonials/parts.ts +7 -0
- package/src/components/Testimonials/types.ts +24 -0
- package/src/components/Testimonials/variants/Carousel.tsx +94 -0
- package/src/components/Testimonials/variants/Grid.tsx +19 -0
- package/src/components/Testimonials/variants/Marquee.tsx +28 -0
- package/src/utils/useTokenForeground.ts +1 -0
- package/src/components/Blog.tsx +0 -251
- package/src/components/CTA.tsx +0 -155
- package/src/components/FAQ.tsx +0 -163
- package/src/components/Features.tsx +0 -153
- package/src/components/Footer.tsx +0 -185
- package/src/components/Hero.tsx +0 -169
- package/src/components/LanguageSwitcher.tsx +0 -223
- package/src/components/LogoCloud.tsx +0 -100
- package/src/components/Navbar.tsx +0 -195
- package/src/components/Newsletter.tsx +0 -180
- package/src/components/Pricing.tsx +0 -336
- package/src/components/Stats.tsx +0 -181
- package/src/components/Testimonials.tsx +0 -205
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
export type LogoCloudOption = "quiet" | "marquee" | "strip";
|
|
4
|
+
|
|
5
|
+
export interface LogoItem {
|
|
6
|
+
name: string;
|
|
7
|
+
src?: string;
|
|
8
|
+
href?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LogoCloudProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
logos: LogoItem[];
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Visual option: quiet list · infinite marquee · hairline strip */
|
|
15
|
+
option?: LogoCloudOption;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { LogoMark, LogoCloudTitle } from "../parts";
|
|
4
|
+
import type { LogoItem } from "../types";
|
|
5
|
+
|
|
6
|
+
interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
logos: LogoItem[];
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LogoCloudMarquee = forwardRef<HTMLDivElement, MarqueeProps>(
|
|
12
|
+
({ className, logos, title, ...props }, ref) => {
|
|
13
|
+
const doubled = [...logos, ...logos];
|
|
14
|
+
return (
|
|
15
|
+
<div ref={ref} className={cn("w-full overflow-hidden", className)} {...props}>
|
|
16
|
+
<LogoCloudTitle title={title} />
|
|
17
|
+
<div className="mask-fade-x">
|
|
18
|
+
<div className="flex w-max animate-marquee gap-14 pr-14 motion-reduce:animate-none">
|
|
19
|
+
{doubled.map((logo, i) => (
|
|
20
|
+
<div key={`${logo.name}-${i}`} className="group shrink-0">
|
|
21
|
+
<LogoMark logo={logo} />
|
|
22
|
+
</div>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
LogoCloudMarquee.displayName = "LogoCloudMarquee";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { LogoMark, LogoCloudTitle } from "../parts";
|
|
4
|
+
import type { LogoItem } from "../types";
|
|
5
|
+
|
|
6
|
+
interface QuietProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
logos: LogoItem[];
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** quiet — unbounded spread of wordmarks. */
|
|
12
|
+
export const LogoCloudQuiet = forwardRef<HTMLDivElement, QuietProps>(
|
|
13
|
+
({ className, logos, title, ...props }, ref) => (
|
|
14
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
15
|
+
<LogoCloudTitle title={title} />
|
|
16
|
+
<ul className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
|
|
17
|
+
{logos.map((logo) => (
|
|
18
|
+
<li key={logo.name} className="group">
|
|
19
|
+
<LogoMark logo={logo} />
|
|
20
|
+
</li>
|
|
21
|
+
))}
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
LogoCloudQuiet.displayName = "LogoCloudQuiet";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { LogoMark, LogoCloudTitle } from "../parts";
|
|
4
|
+
import type { LogoItem } from "../types";
|
|
5
|
+
|
|
6
|
+
interface StripProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
logos: LogoItem[];
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LogoCloudStrip = forwardRef<HTMLDivElement, StripProps>(
|
|
12
|
+
({ className, logos, title, ...props }, ref) => (
|
|
13
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
14
|
+
<LogoCloudTitle title={title} />
|
|
15
|
+
<ul className="flex flex-wrap items-center justify-center gap-x-10 gap-y-4 rounded-2xl border border-border bg-surface px-6 py-7 sm:gap-x-14">
|
|
16
|
+
{logos.map((logo) => (
|
|
17
|
+
<li key={logo.name} className="group">
|
|
18
|
+
<LogoMark logo={logo} />
|
|
19
|
+
</li>
|
|
20
|
+
))}
|
|
21
|
+
</ul>
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
LogoCloudStrip.displayName = "LogoCloudStrip";
|
|
@@ -1,63 +1,83 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
"use client";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
import { cn } from "../../utils/cn";
|
|
4
|
+
import { getContrastText } from "../../utils/contrast";
|
|
5
|
+
import { ChevronDown, Menu, X } from "../../icons";
|
|
6
|
+
import type { MegaMenuProps, MegaMenuVariant, MegaMenuItem } from "./types";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
interface MegaMenuStyle {
|
|
9
|
+
shell: string;
|
|
10
|
+
bar: string;
|
|
11
|
+
header: string;
|
|
12
|
+
trigger: string;
|
|
13
|
+
triggerOpen: string;
|
|
14
|
+
badge: string;
|
|
15
|
+
panel: string;
|
|
16
|
+
columnTitle: string;
|
|
17
|
+
columnLink: string;
|
|
18
|
+
linkDescription: string;
|
|
19
|
+
mobilePanel: string;
|
|
20
|
+
mobileButton: string;
|
|
21
|
+
mobileLink: string;
|
|
22
|
+
brandText: string;
|
|
23
|
+
brandDot: string;
|
|
10
24
|
}
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
/** Component used to render brand, trigger and panel links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
54
|
-
LinkComponent?: ElementType;
|
|
55
|
-
}
|
|
26
|
+
const inverse = "text-on-secondary";
|
|
27
|
+
const light = "text-foreground";
|
|
56
28
|
|
|
57
|
-
const
|
|
58
|
-
classic:
|
|
59
|
-
|
|
60
|
-
|
|
29
|
+
const styles: Record<MegaMenuVariant, MegaMenuStyle> = {
|
|
30
|
+
classic: {
|
|
31
|
+
shell: "border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
32
|
+
bar: "mx-auto max-w-6xl px-5 sm:px-8",
|
|
33
|
+
header: "px-0",
|
|
34
|
+
trigger: "text-muted-foreground hover:text-foreground hover:bg-surface",
|
|
35
|
+
triggerOpen: "bg-surface text-foreground",
|
|
36
|
+
badge: "bg-primary-soft text-primary",
|
|
37
|
+
panel: "border-border bg-surface shadow-raised",
|
|
38
|
+
columnTitle: "text-muted-foreground/70",
|
|
39
|
+
columnLink: "hover:bg-surface-strong",
|
|
40
|
+
linkDescription: "text-muted-foreground",
|
|
41
|
+
mobilePanel: "border-t border-border bg-background",
|
|
42
|
+
mobileButton: `${light} hover:bg-surface`,
|
|
43
|
+
mobileLink: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
44
|
+
brandText: light,
|
|
45
|
+
brandDot: "bg-foreground",
|
|
46
|
+
},
|
|
47
|
+
floating: {
|
|
48
|
+
shell: "px-0",
|
|
49
|
+
bar: "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",
|
|
50
|
+
header: "px-3 sm:px-6",
|
|
51
|
+
trigger: "text-muted-foreground hover:text-foreground hover:bg-surface",
|
|
52
|
+
triggerOpen: "bg-surface text-foreground",
|
|
53
|
+
badge: "bg-primary-soft text-primary",
|
|
54
|
+
panel: "border-border bg-surface shadow-raised",
|
|
55
|
+
columnTitle: "text-muted-foreground/70",
|
|
56
|
+
columnLink: "hover:bg-surface-strong",
|
|
57
|
+
linkDescription: "text-muted-foreground",
|
|
58
|
+
mobilePanel: "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface",
|
|
59
|
+
mobileButton: `${light} hover:bg-surface`,
|
|
60
|
+
mobileLink: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
61
|
+
brandText: light,
|
|
62
|
+
brandDot: "bg-foreground",
|
|
63
|
+
},
|
|
64
|
+
inverse: {
|
|
65
|
+
shell: "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
66
|
+
bar: "mx-auto max-w-6xl px-5 sm:px-8",
|
|
67
|
+
header: "px-0",
|
|
68
|
+
trigger: "text-on-secondary/60 hover:text-on-secondary hover:bg-white/10",
|
|
69
|
+
triggerOpen: "bg-white/10 text-on-secondary",
|
|
70
|
+
badge: "bg-white/15 text-on-secondary",
|
|
71
|
+
panel: "border-white/10 bg-[#161616]",
|
|
72
|
+
columnTitle: "text-on-secondary/45",
|
|
73
|
+
columnLink: "hover:bg-white/10",
|
|
74
|
+
linkDescription: "text-on-secondary/50",
|
|
75
|
+
mobilePanel: "border-t border-white/10 bg-[#101010]",
|
|
76
|
+
mobileButton: `${inverse} hover:bg-white/10`,
|
|
77
|
+
mobileLink: "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary",
|
|
78
|
+
brandText: inverse,
|
|
79
|
+
brandDot: "bg-on-secondary",
|
|
80
|
+
},
|
|
61
81
|
};
|
|
62
82
|
|
|
63
83
|
export const MegaMenu = ({
|
|
@@ -72,6 +92,7 @@ export const MegaMenu = ({
|
|
|
72
92
|
items,
|
|
73
93
|
actions,
|
|
74
94
|
cta,
|
|
95
|
+
languageSwitcher,
|
|
75
96
|
sticky = true,
|
|
76
97
|
LinkComponent,
|
|
77
98
|
onKeyDown,
|
|
@@ -82,8 +103,7 @@ export const MegaMenu = ({
|
|
|
82
103
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
83
104
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
84
105
|
const [mobileExpanded, setMobileExpanded] = useState<number | null>(null);
|
|
85
|
-
|
|
86
|
-
const inverse = variant === "inverse";
|
|
106
|
+
const s = styles[variant];
|
|
87
107
|
|
|
88
108
|
const closePanels = () => setOpenIndex(null);
|
|
89
109
|
const closeAll = () => {
|
|
@@ -92,27 +112,8 @@ export const MegaMenu = ({
|
|
|
92
112
|
setMobileExpanded(null);
|
|
93
113
|
};
|
|
94
114
|
|
|
95
|
-
const panelBackground = inverse
|
|
96
|
-
? "border-white/10 bg-[#161616]"
|
|
97
|
-
: "border-border bg-surface shadow-raised";
|
|
98
|
-
|
|
99
115
|
const panelId = (i: number) => `${ids}-panel-${i}`;
|
|
100
116
|
|
|
101
|
-
const shell = cn(
|
|
102
|
-
"relative w-full transition-colors duration-200",
|
|
103
|
-
sticky && "sticky top-0 z-50",
|
|
104
|
-
variant === "classic" &&
|
|
105
|
-
"border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
106
|
-
variant === "inverse" && "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const bar = cn(
|
|
110
|
-
"relative flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200",
|
|
111
|
-
variant === "floating" &&
|
|
112
|
-
"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",
|
|
113
|
-
variant !== "floating" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
114
|
-
);
|
|
115
|
-
|
|
116
117
|
const logoNode =
|
|
117
118
|
logo ??
|
|
118
119
|
(logoSrc ? (
|
|
@@ -126,26 +127,15 @@ export const MegaMenu = ({
|
|
|
126
127
|
const brandNode = logoNode ? (
|
|
127
128
|
<span className="flex items-center">{logoNode}</span>
|
|
128
129
|
) : (
|
|
129
|
-
<span
|
|
130
|
-
className={cn(
|
|
131
|
-
"flex items-center gap-2 font-display text-[17px] font-bold tracking-tight",
|
|
132
|
-
inverse ? "text-on-secondary" : "text-foreground",
|
|
133
|
-
)}
|
|
134
|
-
>
|
|
135
|
-
<span
|
|
136
|
-
aria-hidden
|
|
137
|
-
className={cn(
|
|
138
|
-
"inline-block h-2.5 w-2.5 rounded-sm",
|
|
139
|
-
inverse ? "bg-on-secondary" : "bg-foreground",
|
|
140
|
-
)}
|
|
141
|
-
/>
|
|
130
|
+
<span className={cn("flex items-center gap-2 font-display text-[17px] font-bold tracking-tight", s.brandText)}>
|
|
131
|
+
<span aria-hidden className={cn("inline-block h-2.5 w-2.5 rounded-sm", s.brandDot)} />
|
|
142
132
|
{brand}
|
|
143
133
|
</span>
|
|
144
134
|
);
|
|
145
135
|
|
|
146
136
|
const triggerClasses = cn(
|
|
147
137
|
"flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150",
|
|
148
|
-
|
|
138
|
+
s.trigger,
|
|
149
139
|
);
|
|
150
140
|
|
|
151
141
|
const renderColumns = (item: MegaMenuItem) => (
|
|
@@ -161,7 +151,7 @@ export const MegaMenu = ({
|
|
|
161
151
|
<h4
|
|
162
152
|
className={cn(
|
|
163
153
|
"text-xs font-semibold uppercase tracking-[0.14em]",
|
|
164
|
-
|
|
154
|
+
s.columnTitle,
|
|
165
155
|
)}
|
|
166
156
|
>
|
|
167
157
|
{column.title}
|
|
@@ -175,9 +165,7 @@ export const MegaMenu = ({
|
|
|
175
165
|
onClick={closeAll}
|
|
176
166
|
className={cn(
|
|
177
167
|
"-mx-2 block rounded-md px-2 py-2 transition-colors duration-150",
|
|
178
|
-
|
|
179
|
-
? "hover:bg-white/10"
|
|
180
|
-
: "hover:bg-surface-strong",
|
|
168
|
+
s.columnLink,
|
|
181
169
|
)}
|
|
182
170
|
>
|
|
183
171
|
<span className="block text-sm font-medium text-foreground">
|
|
@@ -187,7 +175,7 @@ export const MegaMenu = ({
|
|
|
187
175
|
<span
|
|
188
176
|
className={cn(
|
|
189
177
|
"mt-0.5 block text-[13px] leading-snug",
|
|
190
|
-
|
|
178
|
+
s.linkDescription,
|
|
191
179
|
)}
|
|
192
180
|
>
|
|
193
181
|
{link.description}
|
|
@@ -254,11 +242,17 @@ export const MegaMenu = ({
|
|
|
254
242
|
|
|
255
243
|
return (
|
|
256
244
|
<header
|
|
257
|
-
className={cn(
|
|
245
|
+
className={cn(
|
|
246
|
+
"relative w-full transition-colors duration-200",
|
|
247
|
+
sticky && "sticky top-0 z-50",
|
|
248
|
+
s.shell,
|
|
249
|
+
s.header,
|
|
250
|
+
className,
|
|
251
|
+
)}
|
|
258
252
|
onKeyDown={handleKeyDown}
|
|
259
253
|
{...props}
|
|
260
254
|
>
|
|
261
|
-
<nav className={bar} onMouseLeave={closePanels} aria-label="Mega menu">
|
|
255
|
+
<nav className={cn("relative flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200", s.bar)} onMouseLeave={closePanels} aria-label="Mega menu">
|
|
262
256
|
<Link href={brandHref} className="shrink-0">
|
|
263
257
|
{brandNode}
|
|
264
258
|
</Link>
|
|
@@ -277,13 +271,7 @@ export const MegaMenu = ({
|
|
|
277
271
|
aria-controls={panelId(i)}
|
|
278
272
|
onClick={() => setOpenIndex(open ? null : i)}
|
|
279
273
|
onMouseEnter={() => setOpenIndex(i)}
|
|
280
|
-
className={cn(
|
|
281
|
-
triggerClasses,
|
|
282
|
-
open &&
|
|
283
|
-
(inverse
|
|
284
|
-
? "bg-white/10 text-on-secondary"
|
|
285
|
-
: "bg-surface text-foreground"),
|
|
286
|
-
)}
|
|
274
|
+
className={cn(triggerClasses, open && s.triggerOpen)}
|
|
287
275
|
>
|
|
288
276
|
<span className="flex items-center gap-1.5">
|
|
289
277
|
{item.label}
|
|
@@ -291,9 +279,7 @@ export const MegaMenu = ({
|
|
|
291
279
|
<span
|
|
292
280
|
className={cn(
|
|
293
281
|
"rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide",
|
|
294
|
-
|
|
295
|
-
? "bg-white/15 text-on-secondary"
|
|
296
|
-
: "bg-primary-soft text-primary",
|
|
282
|
+
s.badge,
|
|
297
283
|
)}
|
|
298
284
|
>
|
|
299
285
|
{item.badge}
|
|
@@ -311,7 +297,7 @@ export const MegaMenu = ({
|
|
|
311
297
|
<Link href={item.href ?? "#"} className={triggerClasses}>
|
|
312
298
|
{item.label}
|
|
313
299
|
{item.badge ? (
|
|
314
|
-
<span className="rounded-full
|
|
300
|
+
<span className={cn("rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide", s.badge)}>
|
|
315
301
|
{item.badge}
|
|
316
302
|
</span>
|
|
317
303
|
) : null}
|
|
@@ -323,10 +309,15 @@ export const MegaMenu = ({
|
|
|
323
309
|
</ul>
|
|
324
310
|
|
|
325
311
|
<div className="hidden items-center gap-3 lg:flex">
|
|
312
|
+
{languageSwitcher}
|
|
326
313
|
{actions}
|
|
327
314
|
{cta}
|
|
328
315
|
</div>
|
|
329
316
|
|
|
317
|
+
{languageSwitcher ? (
|
|
318
|
+
<div className="flex items-center lg:hidden">{languageSwitcher}</div>
|
|
319
|
+
) : null}
|
|
320
|
+
|
|
330
321
|
<button
|
|
331
322
|
type="button"
|
|
332
323
|
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
|
@@ -335,14 +326,13 @@ export const MegaMenu = ({
|
|
|
335
326
|
className={cn(
|
|
336
327
|
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md lg:hidden",
|
|
337
328
|
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
338
|
-
|
|
329
|
+
s.mobileButton,
|
|
339
330
|
)}
|
|
340
331
|
>
|
|
341
332
|
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
342
333
|
</button>
|
|
343
334
|
</nav>
|
|
344
335
|
|
|
345
|
-
{/* Desktop mega panels */}
|
|
346
336
|
{items.map((item, i) => {
|
|
347
337
|
if (!item.columns?.length && !item.featured) return null;
|
|
348
338
|
const open = openIndex === i;
|
|
@@ -354,7 +344,8 @@ export const MegaMenu = ({
|
|
|
354
344
|
aria-label={`${item.label} menu`}
|
|
355
345
|
onMouseEnter={() => setOpenIndex(i)}
|
|
356
346
|
className={cn(
|
|
357
|
-
"absolute
|
|
347
|
+
"absolute top-full z-50 pt-2 transition-all duration-150",
|
|
348
|
+
variant === "floating" ? "inset-x-0 mx-auto w-full max-w-5xl" : "inset-x-0",
|
|
358
349
|
open
|
|
359
350
|
? "visible translate-y-0 opacity-100"
|
|
360
351
|
: "pointer-events-none invisible -translate-y-1 opacity-0",
|
|
@@ -363,7 +354,7 @@ export const MegaMenu = ({
|
|
|
363
354
|
<div
|
|
364
355
|
className={cn(
|
|
365
356
|
"overflow-hidden rounded-xl border p-6 md:p-8",
|
|
366
|
-
|
|
357
|
+
s.panel,
|
|
367
358
|
item.featured ? "lg:grid lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-12" : "",
|
|
368
359
|
)}
|
|
369
360
|
>
|
|
@@ -378,16 +369,11 @@ export const MegaMenu = ({
|
|
|
378
369
|
);
|
|
379
370
|
})}
|
|
380
371
|
|
|
381
|
-
{/* Mobile accordion panel */}
|
|
382
372
|
<div
|
|
383
373
|
className={cn(
|
|
384
374
|
"grid transition-[grid-template-rows,opacity] duration-200 lg:hidden",
|
|
385
375
|
mobileOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
386
|
-
|
|
387
|
-
? "border-t border-white/10 bg-[#101010]"
|
|
388
|
-
: variant === "floating"
|
|
389
|
-
? "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface"
|
|
390
|
-
: "border-t border-border bg-background",
|
|
376
|
+
s.mobilePanel,
|
|
391
377
|
)}
|
|
392
378
|
>
|
|
393
379
|
<div className="min-h-0 overflow-hidden">
|
|
@@ -403,15 +389,13 @@ export const MegaMenu = ({
|
|
|
403
389
|
}
|
|
404
390
|
className={cn(
|
|
405
391
|
"flex w-full items-center justify-between rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
406
|
-
|
|
407
|
-
? "text-on-secondary hover:bg-white/10"
|
|
408
|
-
: "text-foreground hover:bg-surface",
|
|
392
|
+
s.mobileButton,
|
|
409
393
|
)}
|
|
410
394
|
>
|
|
411
395
|
<span className="flex items-center gap-2">
|
|
412
396
|
{item.label}
|
|
413
397
|
{item.badge ? (
|
|
414
|
-
<span className="rounded-full
|
|
398
|
+
<span className={cn("rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide", s.badge)}>
|
|
415
399
|
{item.badge}
|
|
416
400
|
</span>
|
|
417
401
|
) : null}
|
|
@@ -448,9 +432,7 @@ export const MegaMenu = ({
|
|
|
448
432
|
onClick={() => setMobileOpen(false)}
|
|
449
433
|
className={cn(
|
|
450
434
|
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
451
|
-
|
|
452
|
-
? "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary"
|
|
453
|
-
: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
435
|
+
s.mobileLink,
|
|
454
436
|
)}
|
|
455
437
|
>
|
|
456
438
|
{item.label}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface MegaMenuLink {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MegaMenuColumn {
|
|
10
|
+
title?: string;
|
|
11
|
+
links: MegaMenuLink[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MegaMenuFeatured {
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
href: string;
|
|
18
|
+
cta?: string;
|
|
19
|
+
/** Background color for the card; glyph text color is computed for contrast. */
|
|
20
|
+
accent?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MegaMenuItem {
|
|
24
|
+
label: string;
|
|
25
|
+
href?: string;
|
|
26
|
+
/** When set, the item becomes a trigger that opens a mega panel. */
|
|
27
|
+
columns?: MegaMenuColumn[];
|
|
28
|
+
/** Optional highlighted card pinned to the right side of the panel. */
|
|
29
|
+
featured?: MegaMenuFeatured;
|
|
30
|
+
/** Small pill rendered next to the trigger label. */
|
|
31
|
+
badge?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type MegaMenuVariant = "classic" | "floating" | "inverse";
|
|
35
|
+
|
|
36
|
+
export interface MegaMenuProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
37
|
+
variant?: MegaMenuVariant;
|
|
38
|
+
/** Wordmark rendered when no logo is supplied. */
|
|
39
|
+
brand?: ReactNode;
|
|
40
|
+
/** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
|
|
41
|
+
logo?: ReactNode;
|
|
42
|
+
/** Quick logo image source. */
|
|
43
|
+
logoSrc?: string;
|
|
44
|
+
logoAlt?: string;
|
|
45
|
+
logoClassName?: string;
|
|
46
|
+
brandHref?: string;
|
|
47
|
+
items: MegaMenuItem[];
|
|
48
|
+
/** Rendered at the right side of the bar (desktop) and inside the mobile panel. */
|
|
49
|
+
actions?: ReactNode;
|
|
50
|
+
cta?: ReactNode;
|
|
51
|
+
/** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
|
|
52
|
+
languageSwitcher?: ReactNode;
|
|
53
|
+
sticky?: boolean;
|
|
54
|
+
/** Component used to render brand, trigger and panel links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
55
|
+
LinkComponent?: ElementType;
|
|
56
|
+
}
|
package/src/components/Modal.tsx
CHANGED