@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.
Files changed (116) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +38 -21
  4. package/dist/index.js +2276 -1956
  5. package/dist/index.js.map +1 -1
  6. package/dist/styles.css +3 -0
  7. package/package.json +1 -1
  8. package/src/components/Blog/BlogCard.tsx +12 -0
  9. package/src/components/Blog/BlogSection.tsx +95 -0
  10. package/src/components/Blog/index.ts +8 -0
  11. package/src/components/Blog/parts.tsx +38 -0
  12. package/src/components/Blog/types.ts +42 -0
  13. package/src/components/Blog/variants/Card.tsx +62 -0
  14. package/src/components/Blog/variants/Row.tsx +41 -0
  15. package/src/components/CTA/CTA.tsx +12 -0
  16. package/src/components/CTA/index.ts +2 -0
  17. package/src/components/CTA/parts.tsx +26 -0
  18. package/src/components/CTA/types.ts +12 -0
  19. package/src/components/CTA/variants/Inverse.tsx +50 -0
  20. package/src/components/CTA/variants/Panel.tsx +60 -0
  21. package/src/components/CTA/variants/Surface.tsx +34 -0
  22. package/src/components/FAQ/FAQ.tsx +48 -0
  23. package/src/components/FAQ/index.ts +2 -0
  24. package/src/components/FAQ/parts.tsx +74 -0
  25. package/src/components/FAQ/types.ts +18 -0
  26. package/src/components/FAQ/variants/Accordion.tsx +46 -0
  27. package/src/components/FAQ/variants/Cards.tsx +40 -0
  28. package/src/components/FAQ/variants/Split.tsx +44 -0
  29. package/src/components/Features/FeatureCard.tsx +44 -0
  30. package/src/components/Features/FeatureGrid.tsx +14 -0
  31. package/src/components/Features/index.ts +3 -0
  32. package/src/components/Features/parts.ts +15 -0
  33. package/src/components/Features/types.ts +25 -0
  34. package/src/components/Features/variants/Bento.tsx +32 -0
  35. package/src/components/Features/variants/Columns.tsx +21 -0
  36. package/src/components/Features/variants/EditorialRows.tsx +38 -0
  37. package/src/components/Footer/Footer.tsx +11 -0
  38. package/src/components/Footer/index.ts +2 -0
  39. package/src/components/Footer/parts.tsx +101 -0
  40. package/src/components/Footer/types.ts +32 -0
  41. package/src/components/Footer/variants/Classic.tsx +59 -0
  42. package/src/components/Footer/variants/Editorial.tsx +47 -0
  43. package/src/components/Footer/variants/Minimal.tsx +33 -0
  44. package/src/components/Hero/Hero.tsx +12 -0
  45. package/src/components/Hero/index.ts +2 -0
  46. package/src/components/Hero/parts.tsx +29 -0
  47. package/src/components/Hero/types.ts +20 -0
  48. package/src/components/Hero/variants/Centered.tsx +54 -0
  49. package/src/components/Hero/variants/Split.tsx +53 -0
  50. package/src/components/Hero/variants/Statement.tsx +60 -0
  51. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +37 -0
  52. package/src/components/LanguageSwitcher/index.ts +6 -0
  53. package/src/components/LanguageSwitcher/parts.tsx +144 -0
  54. package/src/components/LanguageSwitcher/types.ts +40 -0
  55. package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +23 -0
  56. package/src/components/LanguageSwitcher/variants/Dropdown.tsx +88 -0
  57. package/src/components/LanguageSwitcher/variants/Modal.tsx +56 -0
  58. package/src/components/LogoCloud/LogoCloud.tsx +14 -0
  59. package/src/components/LogoCloud/index.ts +2 -0
  60. package/src/components/LogoCloud/parts.tsx +32 -0
  61. package/src/components/LogoCloud/types.ts +16 -0
  62. package/src/components/LogoCloud/variants/Marquee.tsx +30 -0
  63. package/src/components/LogoCloud/variants/Quiet.tsx +26 -0
  64. package/src/components/LogoCloud/variants/Strip.tsx +25 -0
  65. package/src/components/{MegaMenu.tsx → MegaMenu/MegaMenu.tsx} +100 -127
  66. package/src/components/MegaMenu/index.ts +9 -0
  67. package/src/components/MegaMenu/types.ts +56 -0
  68. package/src/components/Modal.tsx +1 -0
  69. package/src/components/Navbar/Navbar.tsx +185 -0
  70. package/src/components/Navbar/index.ts +2 -0
  71. package/src/components/Navbar/types.ts +30 -0
  72. package/src/components/Newsletter/Newsletter.tsx +12 -0
  73. package/src/components/Newsletter/index.ts +2 -0
  74. package/src/components/Newsletter/parts.tsx +23 -0
  75. package/src/components/Newsletter/types.ts +12 -0
  76. package/src/components/Newsletter/useNewsletterForm.ts +27 -0
  77. package/src/components/Newsletter/variants/Card.tsx +62 -0
  78. package/src/components/Newsletter/variants/Inline.tsx +54 -0
  79. package/src/components/Newsletter/variants/Underline.tsx +52 -0
  80. package/src/components/Pricing/Pricing.tsx +37 -0
  81. package/src/components/Pricing/index.ts +2 -0
  82. package/src/components/Pricing/parts.tsx +132 -0
  83. package/src/components/Pricing/types.ts +28 -0
  84. package/src/components/Pricing/variants/Bento.tsx +72 -0
  85. package/src/components/Pricing/variants/Cards.tsx +79 -0
  86. package/src/components/Pricing/variants/Compact.tsx +69 -0
  87. package/src/components/Stats/Stats.tsx +16 -0
  88. package/src/components/Stats/index.ts +2 -0
  89. package/src/components/Stats/parts.tsx +29 -0
  90. package/src/components/Stats/types.ts +23 -0
  91. package/src/components/Stats/variants/Cells.tsx +34 -0
  92. package/src/components/Stats/variants/Editorial.tsx +41 -0
  93. package/src/components/Stats/variants/Hairline.tsx +31 -0
  94. package/src/components/Stats/variants/Ticker.tsx +36 -0
  95. package/src/components/Testimonials/TestimonialCard.tsx +61 -0
  96. package/src/components/Testimonials/Testimonials.tsx +14 -0
  97. package/src/components/Testimonials/index.ts +3 -0
  98. package/src/components/Testimonials/parts.ts +7 -0
  99. package/src/components/Testimonials/types.ts +24 -0
  100. package/src/components/Testimonials/variants/Carousel.tsx +94 -0
  101. package/src/components/Testimonials/variants/Grid.tsx +19 -0
  102. package/src/components/Testimonials/variants/Marquee.tsx +28 -0
  103. package/src/utils/useTokenForeground.ts +1 -0
  104. package/src/components/Blog.tsx +0 -251
  105. package/src/components/CTA.tsx +0 -155
  106. package/src/components/FAQ.tsx +0 -163
  107. package/src/components/Features.tsx +0 -153
  108. package/src/components/Footer.tsx +0 -185
  109. package/src/components/Hero.tsx +0 -169
  110. package/src/components/LanguageSwitcher.tsx +0 -223
  111. package/src/components/LogoCloud.tsx +0 -100
  112. package/src/components/Navbar.tsx +0 -195
  113. package/src/components/Newsletter.tsx +0 -180
  114. package/src/components/Pricing.tsx +0 -336
  115. package/src/components/Stats.tsx +0 -181
  116. 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
- import { useId, useState, type ElementType, 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";
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
- export interface MegaMenuLink {
7
- label: string;
8
- href: string;
9
- description?: string;
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
- 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
- /** 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 triggerStyles = {
60
- classic: "text-muted-foreground hover:text-foreground hover:bg-surface",
61
- floating: "text-muted-foreground hover:text-foreground hover:bg-surface",
62
- inverse: "text-on-secondary/60 hover:text-on-secondary hover:bg-white/10",
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
- triggerStyles[variant],
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
- inverse ? "text-on-secondary/45" : "text-muted-foreground/70",
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
- inverse
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
- inverse ? "text-on-secondary/50" : "text-muted-foreground",
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(shell, variant === "floating" && "px-3 sm:px-6", className)}
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
- inverse
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 bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
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
- inverse ? "text-on-secondary hover:bg-white/10" : "text-foreground hover:bg-surface",
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
- panelBackground,
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
- inverse
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
- inverse
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 bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
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
- inverse
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,9 @@
1
+ export { MegaMenu } from "./MegaMenu";
2
+ export type {
3
+ MegaMenuProps,
4
+ MegaMenuVariant,
5
+ MegaMenuItem,
6
+ MegaMenuColumn,
7
+ MegaMenuLink,
8
+ MegaMenuFeatured,
9
+ } from "./types";
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { forwardRef, useEffect, useId, useRef, type HTMLAttributes, type ReactNode } from "react";
2
3
  import { cn } from "../utils/cn";
3
4
  import { X } from "../icons";