@ugurdemirel/landcraft 0.1.3 → 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 +38 -21
- package/dist/index.js +2276 -1956
- 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} +100 -127
- 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,65 +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
|
-
/** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
|
|
53
|
-
languageSwitcher?: ReactNode;
|
|
54
|
-
sticky?: boolean;
|
|
55
|
-
/** 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. */
|
|
56
|
-
LinkComponent?: ElementType;
|
|
57
|
-
}
|
|
26
|
+
const inverse = "text-on-secondary";
|
|
27
|
+
const light = "text-foreground";
|
|
58
28
|
|
|
59
|
-
const
|
|
60
|
-
classic:
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
},
|
|
63
81
|
};
|
|
64
82
|
|
|
65
83
|
export const MegaMenu = ({
|
|
@@ -85,8 +103,7 @@ export const MegaMenu = ({
|
|
|
85
103
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
86
104
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
87
105
|
const [mobileExpanded, setMobileExpanded] = useState<number | null>(null);
|
|
88
|
-
|
|
89
|
-
const inverse = variant === "inverse";
|
|
106
|
+
const s = styles[variant];
|
|
90
107
|
|
|
91
108
|
const closePanels = () => setOpenIndex(null);
|
|
92
109
|
const closeAll = () => {
|
|
@@ -95,27 +112,8 @@ export const MegaMenu = ({
|
|
|
95
112
|
setMobileExpanded(null);
|
|
96
113
|
};
|
|
97
114
|
|
|
98
|
-
const panelBackground = inverse
|
|
99
|
-
? "border-white/10 bg-[#161616]"
|
|
100
|
-
: "border-border bg-surface shadow-raised";
|
|
101
|
-
|
|
102
115
|
const panelId = (i: number) => `${ids}-panel-${i}`;
|
|
103
116
|
|
|
104
|
-
const shell = cn(
|
|
105
|
-
"relative w-full transition-colors duration-200",
|
|
106
|
-
sticky && "sticky top-0 z-50",
|
|
107
|
-
variant === "classic" &&
|
|
108
|
-
"border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
109
|
-
variant === "inverse" && "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const bar = cn(
|
|
113
|
-
"relative flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200",
|
|
114
|
-
variant === "floating" &&
|
|
115
|
-
"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",
|
|
116
|
-
variant !== "floating" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
117
|
-
);
|
|
118
|
-
|
|
119
117
|
const logoNode =
|
|
120
118
|
logo ??
|
|
121
119
|
(logoSrc ? (
|
|
@@ -129,26 +127,15 @@ export const MegaMenu = ({
|
|
|
129
127
|
const brandNode = logoNode ? (
|
|
130
128
|
<span className="flex items-center">{logoNode}</span>
|
|
131
129
|
) : (
|
|
132
|
-
<span
|
|
133
|
-
className={cn(
|
|
134
|
-
"flex items-center gap-2 font-display text-[17px] font-bold tracking-tight",
|
|
135
|
-
inverse ? "text-on-secondary" : "text-foreground",
|
|
136
|
-
)}
|
|
137
|
-
>
|
|
138
|
-
<span
|
|
139
|
-
aria-hidden
|
|
140
|
-
className={cn(
|
|
141
|
-
"inline-block h-2.5 w-2.5 rounded-sm",
|
|
142
|
-
inverse ? "bg-on-secondary" : "bg-foreground",
|
|
143
|
-
)}
|
|
144
|
-
/>
|
|
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)} />
|
|
145
132
|
{brand}
|
|
146
133
|
</span>
|
|
147
134
|
);
|
|
148
135
|
|
|
149
136
|
const triggerClasses = cn(
|
|
150
137
|
"flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150",
|
|
151
|
-
|
|
138
|
+
s.trigger,
|
|
152
139
|
);
|
|
153
140
|
|
|
154
141
|
const renderColumns = (item: MegaMenuItem) => (
|
|
@@ -164,7 +151,7 @@ export const MegaMenu = ({
|
|
|
164
151
|
<h4
|
|
165
152
|
className={cn(
|
|
166
153
|
"text-xs font-semibold uppercase tracking-[0.14em]",
|
|
167
|
-
|
|
154
|
+
s.columnTitle,
|
|
168
155
|
)}
|
|
169
156
|
>
|
|
170
157
|
{column.title}
|
|
@@ -178,9 +165,7 @@ export const MegaMenu = ({
|
|
|
178
165
|
onClick={closeAll}
|
|
179
166
|
className={cn(
|
|
180
167
|
"-mx-2 block rounded-md px-2 py-2 transition-colors duration-150",
|
|
181
|
-
|
|
182
|
-
? "hover:bg-white/10"
|
|
183
|
-
: "hover:bg-surface-strong",
|
|
168
|
+
s.columnLink,
|
|
184
169
|
)}
|
|
185
170
|
>
|
|
186
171
|
<span className="block text-sm font-medium text-foreground">
|
|
@@ -190,7 +175,7 @@ export const MegaMenu = ({
|
|
|
190
175
|
<span
|
|
191
176
|
className={cn(
|
|
192
177
|
"mt-0.5 block text-[13px] leading-snug",
|
|
193
|
-
|
|
178
|
+
s.linkDescription,
|
|
194
179
|
)}
|
|
195
180
|
>
|
|
196
181
|
{link.description}
|
|
@@ -257,11 +242,17 @@ export const MegaMenu = ({
|
|
|
257
242
|
|
|
258
243
|
return (
|
|
259
244
|
<header
|
|
260
|
-
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
|
+
)}
|
|
261
252
|
onKeyDown={handleKeyDown}
|
|
262
253
|
{...props}
|
|
263
254
|
>
|
|
264
|
-
<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">
|
|
265
256
|
<Link href={brandHref} className="shrink-0">
|
|
266
257
|
{brandNode}
|
|
267
258
|
</Link>
|
|
@@ -280,13 +271,7 @@ export const MegaMenu = ({
|
|
|
280
271
|
aria-controls={panelId(i)}
|
|
281
272
|
onClick={() => setOpenIndex(open ? null : i)}
|
|
282
273
|
onMouseEnter={() => setOpenIndex(i)}
|
|
283
|
-
className={cn(
|
|
284
|
-
triggerClasses,
|
|
285
|
-
open &&
|
|
286
|
-
(inverse
|
|
287
|
-
? "bg-white/10 text-on-secondary"
|
|
288
|
-
: "bg-surface text-foreground"),
|
|
289
|
-
)}
|
|
274
|
+
className={cn(triggerClasses, open && s.triggerOpen)}
|
|
290
275
|
>
|
|
291
276
|
<span className="flex items-center gap-1.5">
|
|
292
277
|
{item.label}
|
|
@@ -294,9 +279,7 @@ export const MegaMenu = ({
|
|
|
294
279
|
<span
|
|
295
280
|
className={cn(
|
|
296
281
|
"rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide",
|
|
297
|
-
|
|
298
|
-
? "bg-white/15 text-on-secondary"
|
|
299
|
-
: "bg-primary-soft text-primary",
|
|
282
|
+
s.badge,
|
|
300
283
|
)}
|
|
301
284
|
>
|
|
302
285
|
{item.badge}
|
|
@@ -314,7 +297,7 @@ export const MegaMenu = ({
|
|
|
314
297
|
<Link href={item.href ?? "#"} className={triggerClasses}>
|
|
315
298
|
{item.label}
|
|
316
299
|
{item.badge ? (
|
|
317
|
-
<span className="rounded-full
|
|
300
|
+
<span className={cn("rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide", s.badge)}>
|
|
318
301
|
{item.badge}
|
|
319
302
|
</span>
|
|
320
303
|
) : null}
|
|
@@ -343,14 +326,13 @@ export const MegaMenu = ({
|
|
|
343
326
|
className={cn(
|
|
344
327
|
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md lg:hidden",
|
|
345
328
|
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
346
|
-
|
|
329
|
+
s.mobileButton,
|
|
347
330
|
)}
|
|
348
331
|
>
|
|
349
332
|
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
350
333
|
</button>
|
|
351
334
|
</nav>
|
|
352
335
|
|
|
353
|
-
{/* Desktop mega panels */}
|
|
354
336
|
{items.map((item, i) => {
|
|
355
337
|
if (!item.columns?.length && !item.featured) return null;
|
|
356
338
|
const open = openIndex === i;
|
|
@@ -372,7 +354,7 @@ export const MegaMenu = ({
|
|
|
372
354
|
<div
|
|
373
355
|
className={cn(
|
|
374
356
|
"overflow-hidden rounded-xl border p-6 md:p-8",
|
|
375
|
-
|
|
357
|
+
s.panel,
|
|
376
358
|
item.featured ? "lg:grid lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-12" : "",
|
|
377
359
|
)}
|
|
378
360
|
>
|
|
@@ -387,16 +369,11 @@ export const MegaMenu = ({
|
|
|
387
369
|
);
|
|
388
370
|
})}
|
|
389
371
|
|
|
390
|
-
{/* Mobile accordion panel */}
|
|
391
372
|
<div
|
|
392
373
|
className={cn(
|
|
393
374
|
"grid transition-[grid-template-rows,opacity] duration-200 lg:hidden",
|
|
394
375
|
mobileOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
395
|
-
|
|
396
|
-
? "border-t border-white/10 bg-[#101010]"
|
|
397
|
-
: variant === "floating"
|
|
398
|
-
? "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface"
|
|
399
|
-
: "border-t border-border bg-background",
|
|
376
|
+
s.mobilePanel,
|
|
400
377
|
)}
|
|
401
378
|
>
|
|
402
379
|
<div className="min-h-0 overflow-hidden">
|
|
@@ -412,15 +389,13 @@ export const MegaMenu = ({
|
|
|
412
389
|
}
|
|
413
390
|
className={cn(
|
|
414
391
|
"flex w-full items-center justify-between rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
415
|
-
|
|
416
|
-
? "text-on-secondary hover:bg-white/10"
|
|
417
|
-
: "text-foreground hover:bg-surface",
|
|
392
|
+
s.mobileButton,
|
|
418
393
|
)}
|
|
419
394
|
>
|
|
420
395
|
<span className="flex items-center gap-2">
|
|
421
396
|
{item.label}
|
|
422
397
|
{item.badge ? (
|
|
423
|
-
<span className="rounded-full
|
|
398
|
+
<span className={cn("rounded-full px-1.5 py-0.5 text-[10px] font-semibold tracking-wide", s.badge)}>
|
|
424
399
|
{item.badge}
|
|
425
400
|
</span>
|
|
426
401
|
) : null}
|
|
@@ -457,9 +432,7 @@ export const MegaMenu = ({
|
|
|
457
432
|
onClick={() => setMobileOpen(false)}
|
|
458
433
|
className={cn(
|
|
459
434
|
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
460
|
-
|
|
461
|
-
? "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary"
|
|
462
|
-
: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
435
|
+
s.mobileLink,
|
|
463
436
|
)}
|
|
464
437
|
>
|
|
465
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