@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
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { useEffect, useId, useRef, useState, type CSSProperties, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
import { iso2ToFlagEmoji } from "../utils/iso2Flag";
|
|
4
|
-
import { Modal } from "./Modal";
|
|
5
|
-
import { Check, ChevronDown, Globe } from "../icons";
|
|
6
|
-
|
|
7
|
-
export interface LanguageOption {
|
|
8
|
-
/** Language code (ISO 3166-1 alpha-2, e.g. `"tr"`) — this is the value passed to `onChange` and the flag source. */
|
|
9
|
-
code: string;
|
|
10
|
-
/** Native display name, e.g. `"Türkçe"`. */
|
|
11
|
-
label: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type LanguageSwitcherOption = "dropdown" | "modal";
|
|
15
|
-
|
|
16
|
-
export interface LanguageSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
17
|
-
/** `dropdown` opens the anchored menu; `modal` opens a full dialog via the Modal component. */
|
|
18
|
-
option?: LanguageSwitcherOption;
|
|
19
|
-
/** Languages to switch between. Required — no built-in default language list. */
|
|
20
|
-
languages: LanguageOption[];
|
|
21
|
-
/** Controlled current language code. */
|
|
22
|
-
value?: string;
|
|
23
|
-
/** Uncontrolled initial language code. Defaults to the first language. */
|
|
24
|
-
defaultValue?: string;
|
|
25
|
-
/** Called with the selected language code. */
|
|
26
|
-
onChange?: (code: string) => void;
|
|
27
|
-
/** Dropdown menu alignment relative to its trigger. Defaults to `end`. */
|
|
28
|
-
align?: "start" | "end";
|
|
29
|
-
/** Heading rendered inside the `modal` option. Optional — pass it to show a title. */
|
|
30
|
-
modalTitle?: ReactNode;
|
|
31
|
-
/** Replace the globe icon with the selected language's flag emoji (and show flags in options). */
|
|
32
|
-
showFlag?: boolean;
|
|
33
|
-
/** Adapts colors for dark surfaces (inverse Navbar, classic Footer). */
|
|
34
|
-
inverse?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const LanguageSwitcher = ({
|
|
38
|
-
className,
|
|
39
|
-
languages,
|
|
40
|
-
option = "dropdown",
|
|
41
|
-
value,
|
|
42
|
-
defaultValue,
|
|
43
|
-
onChange,
|
|
44
|
-
align = "end",
|
|
45
|
-
modalTitle,
|
|
46
|
-
showFlag = false,
|
|
47
|
-
inverse = false,
|
|
48
|
-
...props
|
|
49
|
-
}: LanguageSwitcherProps) => {
|
|
50
|
-
const listboxId = useId();
|
|
51
|
-
const anchorName = `--ls-${listboxId.replace(/:/g, "")}`;
|
|
52
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
53
|
-
const [open, setOpen] = useState(false);
|
|
54
|
-
const [internal, setInternal] = useState(defaultValue ?? languages[0]?.code ?? "");
|
|
55
|
-
const current = value ?? internal;
|
|
56
|
-
const currentLang = languages.find((l) => l.code === current) ?? languages[0];
|
|
57
|
-
|
|
58
|
-
const select = (code: string) => {
|
|
59
|
-
setInternal(code);
|
|
60
|
-
setOpen(false);
|
|
61
|
-
onChange?.(code);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const renderFlagOrGlobe = (
|
|
65
|
-
code: string,
|
|
66
|
-
{ size = "text-sm", globeClass = "text-muted-foreground" }: { size?: string; globeClass?: string } = {},
|
|
67
|
-
) => {
|
|
68
|
-
const flag = iso2ToFlagEmoji(code);
|
|
69
|
-
return flag ? (
|
|
70
|
-
<span aria-hidden className={cn(size, "leading-none")}>
|
|
71
|
-
{flag}
|
|
72
|
-
</span>
|
|
73
|
-
) : (
|
|
74
|
-
<Globe className={cn("h-4 w-4 shrink-0", globeClass)} />
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (!open || option !== "dropdown") return;
|
|
80
|
-
const onPointerDown = (event: PointerEvent) => {
|
|
81
|
-
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
|
|
82
|
-
setOpen(false);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
86
|
-
if (event.key === "Escape") setOpen(false);
|
|
87
|
-
};
|
|
88
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
89
|
-
document.addEventListener("keydown", onKeyDown);
|
|
90
|
-
return () => {
|
|
91
|
-
document.removeEventListener("pointerdown", onPointerDown);
|
|
92
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
93
|
-
};
|
|
94
|
-
}, [open, option]);
|
|
95
|
-
|
|
96
|
-
if (languages.length === 0) return null;
|
|
97
|
-
|
|
98
|
-
const trigger = (
|
|
99
|
-
<button
|
|
100
|
-
type="button"
|
|
101
|
-
aria-haspopup={option === "dropdown" ? "listbox" : "dialog"}
|
|
102
|
-
aria-expanded={open}
|
|
103
|
-
aria-controls={option === "dropdown" ? listboxId : undefined}
|
|
104
|
-
onClick={() => setOpen((o) => !o)}
|
|
105
|
-
style={{ anchorName } as CSSProperties}
|
|
106
|
-
className={cn(
|
|
107
|
-
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg border px-3 text-[13px] font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
108
|
-
inverse
|
|
109
|
-
? "border-white/10 text-on-secondary hover:bg-white/10"
|
|
110
|
-
: "border-border text-foreground hover:bg-surface",
|
|
111
|
-
)}
|
|
112
|
-
>
|
|
113
|
-
{showFlag ? (
|
|
114
|
-
renderFlagOrGlobe(currentLang.code, { globeClass: inverse ? "opacity-70" : "text-muted-foreground" })
|
|
115
|
-
) : (
|
|
116
|
-
<Globe
|
|
117
|
-
className={cn("h-4 w-4", inverse ? "opacity-70" : "text-muted-foreground")}
|
|
118
|
-
/>
|
|
119
|
-
)}
|
|
120
|
-
<span className="tracking-tight">{currentLang.label}</span>
|
|
121
|
-
<ChevronDown
|
|
122
|
-
className={cn(
|
|
123
|
-
"h-3.5 w-3.5 transition-transform duration-150",
|
|
124
|
-
open && "rotate-180",
|
|
125
|
-
inverse ? "opacity-70" : "text-muted-foreground",
|
|
126
|
-
)}
|
|
127
|
-
/>
|
|
128
|
-
</button>
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (option === "modal") {
|
|
132
|
-
return (
|
|
133
|
-
<>
|
|
134
|
-
<div ref={rootRef} className={cn("relative inline-flex", className)} {...props}>
|
|
135
|
-
{trigger}
|
|
136
|
-
</div>
|
|
137
|
-
<Modal open={open} onClose={() => setOpen(false)} title={modalTitle} size="sm">
|
|
138
|
-
<div role="listbox" aria-label="Select language" className="grid min-w-[19rem] gap-1.5">
|
|
139
|
-
{languages.map((lang) => {
|
|
140
|
-
const active = lang.code === current;
|
|
141
|
-
return (
|
|
142
|
-
<button
|
|
143
|
-
key={lang.code}
|
|
144
|
-
type="button"
|
|
145
|
-
role="option"
|
|
146
|
-
aria-selected={active}
|
|
147
|
-
onClick={() => select(lang.code)}
|
|
148
|
-
className={cn(
|
|
149
|
-
"flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border px-4 py-3 text-left transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
150
|
-
active
|
|
151
|
-
? "border-primary/40 bg-primary-soft text-foreground"
|
|
152
|
-
: "border-border text-foreground hover:bg-surface-strong",
|
|
153
|
-
)}
|
|
154
|
-
>
|
|
155
|
-
<span className="flex items-center gap-3">
|
|
156
|
-
{showFlag ? renderFlagOrGlobe(lang.code, { size: "text-base" }) : null}
|
|
157
|
-
<span className="text-sm font-medium">{lang.label}</span>
|
|
158
|
-
</span>
|
|
159
|
-
<span className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
160
|
-
{lang.code}
|
|
161
|
-
{active ? <Check className="h-4 w-4 text-primary" /> : null}
|
|
162
|
-
</span>
|
|
163
|
-
</button>
|
|
164
|
-
);
|
|
165
|
-
})}
|
|
166
|
-
</div>
|
|
167
|
-
</Modal>
|
|
168
|
-
</>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div ref={rootRef} className={cn("relative inline-flex", className)} {...props}>
|
|
174
|
-
{trigger}
|
|
175
|
-
|
|
176
|
-
<div
|
|
177
|
-
id={listboxId}
|
|
178
|
-
role="listbox"
|
|
179
|
-
aria-label="Select language"
|
|
180
|
-
style={
|
|
181
|
-
{
|
|
182
|
-
positionAnchor: anchorName,
|
|
183
|
-
top: "anchor(bottom)",
|
|
184
|
-
...(align === "start" ? { left: "anchor(left)" } : { right: "anchor(right)" }),
|
|
185
|
-
} as CSSProperties
|
|
186
|
-
}
|
|
187
|
-
className={cn(
|
|
188
|
-
"absolute top-full z-50 mt-2 min-w-[10rem] rounded-lg border border-border bg-surface p-1 shadow-raised transition-all duration-150",
|
|
189
|
-
align === "start" ? "left-0" : "right-0",
|
|
190
|
-
open
|
|
191
|
-
? "visible translate-y-0 opacity-100"
|
|
192
|
-
: "pointer-events-none invisible -translate-y-1 opacity-0",
|
|
193
|
-
)}
|
|
194
|
-
>
|
|
195
|
-
{languages.map((lang) => {
|
|
196
|
-
const active = lang.code === current;
|
|
197
|
-
return (
|
|
198
|
-
<button
|
|
199
|
-
key={lang.code}
|
|
200
|
-
type="button"
|
|
201
|
-
role="option"
|
|
202
|
-
aria-selected={active}
|
|
203
|
-
onClick={() => select(lang.code)}
|
|
204
|
-
className={cn(
|
|
205
|
-
"flex w-full cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-[13px] font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
206
|
-
active
|
|
207
|
-
? "bg-surface-strong text-foreground"
|
|
208
|
-
: "text-muted-foreground hover:bg-surface-strong hover:text-foreground",
|
|
209
|
-
)}
|
|
210
|
-
>
|
|
211
|
-
<span className="flex items-center gap-2.5">
|
|
212
|
-
{showFlag ? renderFlagOrGlobe(lang.code) : null}
|
|
213
|
-
{lang.label}
|
|
214
|
-
</span>
|
|
215
|
-
{active ? <Check className="h-3.5 w-3.5 text-primary" /> : null}
|
|
216
|
-
</button>
|
|
217
|
-
);
|
|
218
|
-
})}
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
);
|
|
222
|
-
};
|
|
223
|
-
LanguageSwitcher.displayName = "LanguageSwitcher";
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
|
|
4
|
-
export interface LogoItem {
|
|
5
|
-
name: string;
|
|
6
|
-
src?: string;
|
|
7
|
-
href?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface LogoCloudProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
-
logos: LogoItem[];
|
|
12
|
-
title?: string;
|
|
13
|
-
/** Visual option: quiet list · infinite marquee · hairline strip */
|
|
14
|
-
option?: "quiet" | "marquee" | "strip";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const LogoText = ({ name }: { name: string }) => (
|
|
18
|
-
<span className="text-lg font-semibold tracking-tight text-foreground/45 transition-colors duration-200 group-hover:text-foreground">
|
|
19
|
-
{name}
|
|
20
|
-
</span>
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const LogoMark = ({ logo }: { logo: LogoItem }) =>
|
|
24
|
-
logo.src ? (
|
|
25
|
-
<img
|
|
26
|
-
src={logo.src}
|
|
27
|
-
alt={logo.name}
|
|
28
|
-
loading="lazy"
|
|
29
|
-
className={cn(
|
|
30
|
-
"max-h-7 w-auto opacity-50 grayscale transition-all duration-200 group-hover:opacity-90 group-hover:grayscale-0",
|
|
31
|
-
"h-7",
|
|
32
|
-
)}
|
|
33
|
-
/>
|
|
34
|
-
) : (
|
|
35
|
-
<LogoText name={logo.name} />
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
export const LogoCloud = forwardRef<HTMLDivElement, LogoCloudProps>(
|
|
39
|
-
({ className, logos, title, option = "quiet", ...props }, ref) => {
|
|
40
|
-
if (option === "marquee") {
|
|
41
|
-
const doubled = [...logos, ...logos];
|
|
42
|
-
return (
|
|
43
|
-
<div ref={ref} className={cn("w-full overflow-hidden", className)} {...props}>
|
|
44
|
-
{title ? (
|
|
45
|
-
<p className="mb-8 text-center text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
46
|
-
{title}
|
|
47
|
-
</p>
|
|
48
|
-
) : null}
|
|
49
|
-
<div className="mask-fade-x">
|
|
50
|
-
<div className="flex w-max animate-marquee gap-14 pr-14 motion-reduce:animate-none">
|
|
51
|
-
{doubled.map((logo, i) => (
|
|
52
|
-
<div key={`${logo.name}-${i}`} className="group shrink-0">
|
|
53
|
-
<LogoMark logo={logo} />
|
|
54
|
-
</div>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (option === "strip") {
|
|
63
|
-
return (
|
|
64
|
-
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
65
|
-
{title ? (
|
|
66
|
-
<p className="mb-8 text-center text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
67
|
-
{title}
|
|
68
|
-
</p>
|
|
69
|
-
) : null}
|
|
70
|
-
<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">
|
|
71
|
-
{logos.map((logo) => (
|
|
72
|
-
<li key={logo.name} className="group">
|
|
73
|
-
<LogoMark logo={logo} />
|
|
74
|
-
</li>
|
|
75
|
-
))}
|
|
76
|
-
</ul>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// quiet — unbounded spread of wordmarks.
|
|
82
|
-
return (
|
|
83
|
-
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
84
|
-
{title ? (
|
|
85
|
-
<p className="mb-8 text-center text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
86
|
-
{title}
|
|
87
|
-
</p>
|
|
88
|
-
) : null}
|
|
89
|
-
<ul className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
|
|
90
|
-
{logos.map((logo) => (
|
|
91
|
-
<li key={logo.name} className="group">
|
|
92
|
-
<LogoMark logo={logo} />
|
|
93
|
-
</li>
|
|
94
|
-
))}
|
|
95
|
-
</ul>
|
|
96
|
-
</div>
|
|
97
|
-
);
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
LogoCloud.displayName = "LogoCloud";
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { useState, type ElementType, 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
|
-
cta?: ReactNode;
|
|
25
|
-
/** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
|
|
26
|
-
languageSwitcher?: ReactNode;
|
|
27
|
-
sticky?: boolean;
|
|
28
|
-
/** Component used to render the brand and nav links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
29
|
-
LinkComponent?: ElementType;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const linkStyles = {
|
|
33
|
-
classic: "text-muted-foreground hover:text-foreground",
|
|
34
|
-
floating: "text-muted-foreground hover:text-foreground",
|
|
35
|
-
inverse: "text-on-secondary/60 hover:text-on-secondary",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const Navbar = ({
|
|
39
|
-
className,
|
|
40
|
-
variant = "classic",
|
|
41
|
-
brand,
|
|
42
|
-
logo,
|
|
43
|
-
logoSrc,
|
|
44
|
-
logoAlt,
|
|
45
|
-
logoClassName,
|
|
46
|
-
brandHref = "#",
|
|
47
|
-
links,
|
|
48
|
-
actions,
|
|
49
|
-
cta,
|
|
50
|
-
languageSwitcher,
|
|
51
|
-
sticky = true,
|
|
52
|
-
LinkComponent,
|
|
53
|
-
...props
|
|
54
|
-
}: NavbarProps) => {
|
|
55
|
-
const [open, setOpen] = useState(false);
|
|
56
|
-
const inverse = variant === "inverse";
|
|
57
|
-
const Link = LinkComponent ?? "a";
|
|
58
|
-
|
|
59
|
-
const shell = cn(
|
|
60
|
-
"w-full transition-colors duration-200",
|
|
61
|
-
sticky && "sticky top-0 z-50",
|
|
62
|
-
variant === "classic" &&
|
|
63
|
-
"border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
64
|
-
variant === "floating" && "px-0",
|
|
65
|
-
variant === "inverse" && "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const bar = cn(
|
|
69
|
-
"flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200",
|
|
70
|
-
variant === "floating" &&
|
|
71
|
-
"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",
|
|
72
|
-
variant === "classic" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
73
|
-
variant === "inverse" && "mx-auto max-w-6xl px-5 sm:px-8",
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const logoNode =
|
|
77
|
-
logo ??
|
|
78
|
-
(logoSrc ? (
|
|
79
|
-
<img
|
|
80
|
-
src={logoSrc}
|
|
81
|
-
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
82
|
-
className={cn("h-7 w-auto", logoClassName)}
|
|
83
|
-
/>
|
|
84
|
-
) : null);
|
|
85
|
-
|
|
86
|
-
const brandNode = logoNode ? (
|
|
87
|
-
<span className="flex items-center">{logoNode}</span>
|
|
88
|
-
) : (
|
|
89
|
-
<span
|
|
90
|
-
className={cn(
|
|
91
|
-
"flex items-center gap-2 font-display text-[17px] font-bold tracking-tight",
|
|
92
|
-
inverse ? "text-on-secondary" : "text-foreground",
|
|
93
|
-
)}
|
|
94
|
-
>
|
|
95
|
-
<span
|
|
96
|
-
aria-hidden
|
|
97
|
-
className={cn(
|
|
98
|
-
"inline-block h-2.5 w-2.5 rounded-sm",
|
|
99
|
-
inverse ? "bg-on-secondary" : "bg-foreground",
|
|
100
|
-
)}
|
|
101
|
-
/>
|
|
102
|
-
{brand}
|
|
103
|
-
</span>
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<header className={cn(shell, variant === "floating" && "px-3 sm:px-6", className)} {...props}>
|
|
108
|
-
<nav className={bar} aria-label="Main navigation">
|
|
109
|
-
<Link href={brandHref} className="shrink-0">
|
|
110
|
-
{brandNode}
|
|
111
|
-
</Link>
|
|
112
|
-
|
|
113
|
-
<ul className="hidden items-center gap-7 md:flex">
|
|
114
|
-
{links.map((link) => (
|
|
115
|
-
<li key={link.href}>
|
|
116
|
-
<Link
|
|
117
|
-
href={link.href}
|
|
118
|
-
className={cn(
|
|
119
|
-
"text-sm font-medium transition-colors duration-150",
|
|
120
|
-
linkStyles[variant],
|
|
121
|
-
)}
|
|
122
|
-
>
|
|
123
|
-
{link.label}
|
|
124
|
-
</Link>
|
|
125
|
-
</li>
|
|
126
|
-
))}
|
|
127
|
-
</ul>
|
|
128
|
-
|
|
129
|
-
<div className="hidden items-center gap-3 md:flex">
|
|
130
|
-
{languageSwitcher}
|
|
131
|
-
{actions}
|
|
132
|
-
{cta}
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
{languageSwitcher ? (
|
|
136
|
-
<div className="flex items-center md:hidden">{languageSwitcher}</div>
|
|
137
|
-
) : null}
|
|
138
|
-
|
|
139
|
-
<button
|
|
140
|
-
type="button"
|
|
141
|
-
aria-label={open ? "Close menu" : "Open menu"}
|
|
142
|
-
aria-expanded={open}
|
|
143
|
-
onClick={() => setOpen((o) => !o)}
|
|
144
|
-
className={cn(
|
|
145
|
-
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md md:hidden",
|
|
146
|
-
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
147
|
-
inverse ? "text-on-secondary hover:bg-white/10" : "text-foreground hover:bg-surface",
|
|
148
|
-
)}
|
|
149
|
-
>
|
|
150
|
-
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
151
|
-
</button>
|
|
152
|
-
</nav>
|
|
153
|
-
|
|
154
|
-
{/* Mobile panel */}
|
|
155
|
-
<div
|
|
156
|
-
className={cn(
|
|
157
|
-
"grid transition-[grid-template-rows,opacity] duration-200 md:hidden",
|
|
158
|
-
open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
159
|
-
inverse
|
|
160
|
-
? "border-t border-white/10 bg-[#101010]"
|
|
161
|
-
: variant === "floating"
|
|
162
|
-
? "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface"
|
|
163
|
-
: "border-t border-border bg-background",
|
|
164
|
-
)}
|
|
165
|
-
>
|
|
166
|
-
<div className="min-h-0 overflow-hidden">
|
|
167
|
-
<ul className={cn("flex flex-col py-4", variant !== "floating" && "px-5")}>
|
|
168
|
-
{links.map((link) => (
|
|
169
|
-
<li key={link.href}>
|
|
170
|
-
<Link
|
|
171
|
-
href={link.href}
|
|
172
|
-
onClick={() => setOpen(false)}
|
|
173
|
-
className={cn(
|
|
174
|
-
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
175
|
-
inverse
|
|
176
|
-
? "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary"
|
|
177
|
-
: "text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
178
|
-
)}
|
|
179
|
-
>
|
|
180
|
-
{link.label}
|
|
181
|
-
</Link>
|
|
182
|
-
</li>
|
|
183
|
-
))}
|
|
184
|
-
{cta ? (
|
|
185
|
-
<li className="mt-3 px-2 pb-2">{cta}</li>
|
|
186
|
-
) : actions ? (
|
|
187
|
-
<li className="mt-3 flex flex-wrap gap-3 px-2 pb-2">{actions}</li>
|
|
188
|
-
) : null}
|
|
189
|
-
</ul>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</header>
|
|
193
|
-
);
|
|
194
|
-
};
|
|
195
|
-
Navbar.displayName = "Navbar";
|