@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,18 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type FAQOption = "accordion" | "split" | "cards";
|
|
4
|
+
|
|
5
|
+
export interface FaqItem {
|
|
6
|
+
question: string;
|
|
7
|
+
answer: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FAQProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
11
|
+
items: FaqItem[];
|
|
12
|
+
option?: FAQOption;
|
|
13
|
+
allowMultiple?: boolean;
|
|
14
|
+
defaultOpen?: number[];
|
|
15
|
+
eyebrow?: ReactNode;
|
|
16
|
+
title?: ReactNode;
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { useFaqState, FaqItemRow } from "../parts";
|
|
4
|
+
import type { FAQProps } from "../types";
|
|
5
|
+
|
|
6
|
+
interface AccordionProps extends FAQProps {
|
|
7
|
+
allowMultiple?: boolean;
|
|
8
|
+
defaultOpen?: number[];
|
|
9
|
+
bare?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FaqAccordion = ({
|
|
13
|
+
className,
|
|
14
|
+
items,
|
|
15
|
+
allowMultiple = false,
|
|
16
|
+
defaultOpen = [],
|
|
17
|
+
bare = false,
|
|
18
|
+
option,
|
|
19
|
+
title,
|
|
20
|
+
eyebrow,
|
|
21
|
+
description,
|
|
22
|
+
...props
|
|
23
|
+
}: AccordionProps) => {
|
|
24
|
+
const { isOpen, toggle } = useFaqState(allowMultiple, defaultOpen);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
!bare && "divide-y divide-border rounded-2xl border border-border bg-surface",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{items.map((item, index) => (
|
|
35
|
+
<FaqItemRow
|
|
36
|
+
key={item.question}
|
|
37
|
+
item={item}
|
|
38
|
+
open={isOpen(index)}
|
|
39
|
+
onToggle={() => toggle(index)}
|
|
40
|
+
style={bare ? "bare" : "boxed"}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
FaqAccordion.displayName = "FaqAccordion";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from "../../../utils/cn";
|
|
2
|
+
import { FaqEyebrow } from "../parts";
|
|
3
|
+
import type { FAQProps } from "../types";
|
|
4
|
+
|
|
5
|
+
interface CardsProps extends FAQProps {}
|
|
6
|
+
|
|
7
|
+
export const FaqCards = ({
|
|
8
|
+
className,
|
|
9
|
+
items,
|
|
10
|
+
eyebrow,
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
option,
|
|
14
|
+
...props
|
|
15
|
+
}: CardsProps) => (
|
|
16
|
+
<div className={cn("w-full", className)} {...props}>
|
|
17
|
+
{eyebrow ? <div className="mb-10"><FaqEyebrow label={String(eyebrow)} /></div> : null}
|
|
18
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
19
|
+
{items.map((item, i) => (
|
|
20
|
+
<div
|
|
21
|
+
key={item.question}
|
|
22
|
+
className="flex flex-col rounded-xl border border-border bg-surface p-7"
|
|
23
|
+
>
|
|
24
|
+
<div className="flex items-baseline gap-4">
|
|
25
|
+
<span className="font-display text-sm font-semibold tabular-nums text-foreground/35">
|
|
26
|
+
{String(i + 1).padStart(2, "0")}
|
|
27
|
+
</span>
|
|
28
|
+
<h3 className="font-display text-lg font-semibold tracking-tight text-foreground">
|
|
29
|
+
{item.question}
|
|
30
|
+
</h3>
|
|
31
|
+
</div>
|
|
32
|
+
<p className="mt-4 border-t border-border pt-4 text-sm leading-6 text-muted-foreground">
|
|
33
|
+
{item.answer}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
FaqCards.displayName = "FaqCards";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cn } from "../../../utils/cn";
|
|
2
|
+
import { FaqEyebrow } from "../parts";
|
|
3
|
+
import { FaqAccordion } from "./Accordion";
|
|
4
|
+
import type { FAQProps } from "../types";
|
|
5
|
+
|
|
6
|
+
interface SplitProps extends FAQProps {
|
|
7
|
+
allowMultiple?: boolean;
|
|
8
|
+
defaultOpen?: number[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FaqSplit = ({
|
|
12
|
+
className,
|
|
13
|
+
items,
|
|
14
|
+
allowMultiple = false,
|
|
15
|
+
defaultOpen = [],
|
|
16
|
+
eyebrow,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
option,
|
|
20
|
+
...props
|
|
21
|
+
}: SplitProps) => (
|
|
22
|
+
<div className={cn("grid w-full gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:gap-20", className)} {...props}>
|
|
23
|
+
<div className="lg:sticky lg:top-24 lg:self-start">
|
|
24
|
+
{eyebrow ? <FaqEyebrow label={String(eyebrow)} /> : null}
|
|
25
|
+
{title ? (
|
|
26
|
+
<h2 className="mt-4 font-display text-3xl font-semibold leading-[1.08] tracking-tight text-foreground sm:text-4xl">
|
|
27
|
+
{title}
|
|
28
|
+
</h2>
|
|
29
|
+
) : null}
|
|
30
|
+
{description ? (
|
|
31
|
+
<p className="mt-4 text-pretty text-base leading-7 text-muted-foreground">{description}</p>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="divide-y divide-border">
|
|
35
|
+
<FaqAccordion
|
|
36
|
+
items={items}
|
|
37
|
+
allowMultiple={allowMultiple}
|
|
38
|
+
defaultOpen={defaultOpen}
|
|
39
|
+
bare
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
FaqSplit.displayName = "FaqSplit";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { cn } from "../../utils/cn";
|
|
3
|
+
import { getContrastText } from "../../utils/contrast";
|
|
4
|
+
import type { FeatureCardProps } from "./types";
|
|
5
|
+
|
|
6
|
+
export const FeatureCard = forwardRef<HTMLDivElement, FeatureCardProps>(
|
|
7
|
+
({ className, icon, title, description, accent, large = false, ...props }, ref) => {
|
|
8
|
+
const chipStyle = accent
|
|
9
|
+
? { backgroundColor: accent, color: getContrastText(accent, "#111111", "#ffffff") }
|
|
10
|
+
: undefined;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(
|
|
16
|
+
"group relative flex h-full flex-col rounded-xl border border-border bg-surface p-7 transition-colors duration-200 hover:border-foreground/15",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
{icon ? (
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
"mb-5 inline-flex h-10 w-10 items-center justify-center rounded-lg text-[1.05rem]",
|
|
25
|
+
!accent && "bg-primary-soft text-primary",
|
|
26
|
+
)}
|
|
27
|
+
style={chipStyle}
|
|
28
|
+
>
|
|
29
|
+
{icon}
|
|
30
|
+
</div>
|
|
31
|
+
) : null}
|
|
32
|
+
<h3 className={cn("font-display font-semibold tracking-tight text-foreground", large ? "text-2xl" : "text-lg")}>
|
|
33
|
+
{title}
|
|
34
|
+
</h3>
|
|
35
|
+
{description ? (
|
|
36
|
+
<p className={cn("mt-2.5 text-pretty leading-relaxed text-muted-foreground", large ? "text-[15px]" : "text-sm")}>
|
|
37
|
+
{description}
|
|
38
|
+
</p>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
FeatureCard.displayName = "FeatureCard";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { FeatureGridProps } from "./types";
|
|
3
|
+
import { FeatureColumns } from "./variants/Columns";
|
|
4
|
+
import { FeatureBento } from "./variants/Bento";
|
|
5
|
+
import { FeatureEditorialRows } from "./variants/EditorialRows";
|
|
6
|
+
|
|
7
|
+
export const FeatureGrid = forwardRef<HTMLDivElement, FeatureGridProps>(
|
|
8
|
+
({ option = "columns", columns = 3, ...props }, ref) => {
|
|
9
|
+
if (option === "editorialRows") return <FeatureEditorialRows ref={ref} {...props} />;
|
|
10
|
+
if (option === "bento") return <FeatureBento ref={ref} {...props} />;
|
|
11
|
+
return <FeatureColumns ref={ref} columns={columns} {...props} />;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
FeatureGrid.displayName = "FeatureGrid";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const gridCols: Record<2 | 3 | 4, string> = {
|
|
2
|
+
2: "sm:grid-cols-2",
|
|
3
|
+
3: "sm:grid-cols-2 lg:grid-cols-3",
|
|
4
|
+
4: "sm:grid-cols-2 lg:grid-cols-4",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** Asymmetric bento layout for up to 6 features. */
|
|
8
|
+
export const bentoSpans: string[] = [
|
|
9
|
+
"sm:col-span-2 lg:row-span-2",
|
|
10
|
+
"",
|
|
11
|
+
"",
|
|
12
|
+
"lg:col-span-2",
|
|
13
|
+
"",
|
|
14
|
+
"lg:col-span-2",
|
|
15
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type FeatureGridOption = "columns" | "bento" | "editorialRows";
|
|
4
|
+
|
|
5
|
+
export interface FeatureItem {
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
accent?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FeatureCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
title: ReactNode;
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
/** Arbitrary CSS color for the icon chip; glyph color computed for contrast. */
|
|
17
|
+
accent?: string;
|
|
18
|
+
large?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FeatureGridProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
22
|
+
features: FeatureItem[];
|
|
23
|
+
option?: FeatureGridOption;
|
|
24
|
+
columns?: 2 | 3 | 4;
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { FeatureCard } from "../FeatureCard";
|
|
4
|
+
import { bentoSpans } from "../parts";
|
|
5
|
+
import type { FeatureItem } from "../types";
|
|
6
|
+
|
|
7
|
+
interface BentoProps extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
features: FeatureItem[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FeatureBento = forwardRef<HTMLDivElement, BentoProps>(
|
|
12
|
+
({ className, features, ...props }, ref) => (
|
|
13
|
+
<div
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(
|
|
16
|
+
"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 lg:auto-rows-[minmax(9rem,auto)]",
|
|
17
|
+
"w-full",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
{features.map((feature, i) =>
|
|
23
|
+
feature ? (
|
|
24
|
+
<div key={i} className={cn(i < bentoSpans.length && bentoSpans[i])}>
|
|
25
|
+
<FeatureCard {...feature} large={i === 0} className="h-full" />
|
|
26
|
+
</div>
|
|
27
|
+
) : null,
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
FeatureBento.displayName = "FeatureBento";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import { FeatureCard } from "../FeatureCard";
|
|
4
|
+
import { gridCols } from "../parts";
|
|
5
|
+
import type { FeatureItem } from "../types";
|
|
6
|
+
|
|
7
|
+
interface ColumnsProps extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
features: FeatureItem[];
|
|
9
|
+
columns: 2 | 3 | 4;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FeatureColumns = forwardRef<HTMLDivElement, ColumnsProps>(
|
|
13
|
+
({ className, features, columns, ...props }, ref) => (
|
|
14
|
+
<div ref={ref} className={cn("grid w-full grid-cols-1 gap-4", gridCols[columns], className)} {...props}>
|
|
15
|
+
{features.map((feature, i) => (
|
|
16
|
+
<FeatureCard key={i} {...feature} className="h-full" />
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
FeatureColumns.displayName = "FeatureColumns";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../../utils/cn";
|
|
3
|
+
import type { FeatureItem } from "../types";
|
|
4
|
+
|
|
5
|
+
interface EditorialRowsProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
features: FeatureItem[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const FeatureEditorialRows = forwardRef<HTMLDivElement, EditorialRowsProps>(
|
|
10
|
+
({ className, features, ...props }, ref) => (
|
|
11
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
12
|
+
<div className="divide-y divide-border border-t border-b border-border">
|
|
13
|
+
{features.map((feature, i) => (
|
|
14
|
+
<div
|
|
15
|
+
key={i}
|
|
16
|
+
className="group flex flex-col gap-3 py-6 transition-colors duration-200 sm:flex-row sm:items-baseline sm:gap-10"
|
|
17
|
+
>
|
|
18
|
+
<span className="text-sm font-medium tabular-nums text-muted-foreground/60 sm:w-12">
|
|
19
|
+
{String(i + 1).padStart(2, "0")}
|
|
20
|
+
</span>
|
|
21
|
+
<div className="flex-1 sm:flex sm:items-baseline sm:justify-between sm:gap-10">
|
|
22
|
+
<h3 className="font-display text-xl font-semibold tracking-tight text-foreground sm:w-2/5">
|
|
23
|
+
{feature.title}
|
|
24
|
+
</h3>
|
|
25
|
+
<p className="mt-1.5 text-sm leading-7 text-muted-foreground sm:mt-0 sm:w-1/2">
|
|
26
|
+
{feature.description}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
{feature.icon ? (
|
|
30
|
+
<span className="hidden text-foreground/30 sm:inline-flex">{feature.icon}</span>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
FeatureEditorialRows.displayName = "FeatureEditorialRows";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FooterProps } from "./types";
|
|
2
|
+
import { FooterClassic } from "./variants/Classic";
|
|
3
|
+
import { FooterMinimal } from "./variants/Minimal";
|
|
4
|
+
import { FooterEditorial } from "./variants/Editorial";
|
|
5
|
+
|
|
6
|
+
export const Footer = ({ option = "classic", ...props }: FooterProps) => {
|
|
7
|
+
if (option === "minimal") return <FooterMinimal {...props} />;
|
|
8
|
+
if (option === "editorial") return <FooterEditorial {...props} />;
|
|
9
|
+
return <FooterClassic {...props} />;
|
|
10
|
+
};
|
|
11
|
+
Footer.displayName = "Footer";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../utils/cn";
|
|
3
|
+
import type { FooterColumn } from "./types";
|
|
4
|
+
|
|
5
|
+
export function FooterLogoNode({
|
|
6
|
+
logo,
|
|
7
|
+
logoSrc,
|
|
8
|
+
logoAlt,
|
|
9
|
+
logoClassName,
|
|
10
|
+
brand,
|
|
11
|
+
imageClassName = "h-7 w-auto",
|
|
12
|
+
}: {
|
|
13
|
+
logo?: ReactNode;
|
|
14
|
+
logoSrc?: string;
|
|
15
|
+
logoAlt?: string;
|
|
16
|
+
logoClassName?: string;
|
|
17
|
+
brand?: ReactNode;
|
|
18
|
+
imageClassName?: string;
|
|
19
|
+
}) {
|
|
20
|
+
if (logo) return logo;
|
|
21
|
+
if (logoSrc) {
|
|
22
|
+
return (
|
|
23
|
+
<img
|
|
24
|
+
src={logoSrc}
|
|
25
|
+
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
26
|
+
className={cn(imageClassName, logoClassName)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FooterWordmark({ brand }: { brand?: ReactNode }) {
|
|
34
|
+
return (
|
|
35
|
+
<span className="flex items-center gap-2 font-display text-lg font-bold tracking-tight">
|
|
36
|
+
<span aria-hidden className="inline-block h-2.5 w-2.5 rounded-sm bg-current" />
|
|
37
|
+
{brand}
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FooterEditorialWordmark({ brand }: { brand?: ReactNode }) {
|
|
43
|
+
return (
|
|
44
|
+
<span className="font-display text-5xl font-bold tracking-[-0.03em] text-foreground sm:text-7xl">
|
|
45
|
+
{brand}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function FooterColumnsBlock({
|
|
51
|
+
columns,
|
|
52
|
+
Link,
|
|
53
|
+
linkClassName,
|
|
54
|
+
}: {
|
|
55
|
+
columns: FooterColumn[];
|
|
56
|
+
Link: ElementType;
|
|
57
|
+
linkClassName: string;
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
|
61
|
+
{columns.map((column) => (
|
|
62
|
+
<div key={column.title}>
|
|
63
|
+
<h3 className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
64
|
+
{column.title}
|
|
65
|
+
</h3>
|
|
66
|
+
<ul className="mt-4 space-y-2.5">
|
|
67
|
+
{column.links.map((link) => (
|
|
68
|
+
<li key={link.href}>
|
|
69
|
+
<Link href={link.href} className={linkClassName}>
|
|
70
|
+
{link.label}
|
|
71
|
+
</Link>
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function FooterBottomBar({
|
|
82
|
+
bottom,
|
|
83
|
+
languageSwitcher,
|
|
84
|
+
brand,
|
|
85
|
+
wrapperClassName,
|
|
86
|
+
textClassName,
|
|
87
|
+
}: {
|
|
88
|
+
bottom?: ReactNode;
|
|
89
|
+
languageSwitcher?: ReactNode;
|
|
90
|
+
brand?: ReactNode;
|
|
91
|
+
wrapperClassName: string;
|
|
92
|
+
textClassName: string;
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<div className={wrapperClassName}>
|
|
96
|
+
<span className={cn(textClassName)}>{bottom}</span>
|
|
97
|
+
{languageSwitcher ? <div>{languageSwitcher}</div> : null}
|
|
98
|
+
<span className={cn(textClassName)}>© {new Date().getFullYear()} {brand}. All rights reserved.</span>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FooterLink {
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FooterColumn {
|
|
9
|
+
title: string;
|
|
10
|
+
links: FooterLink[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FooterProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
14
|
+
brand?: ReactNode;
|
|
15
|
+
/** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
|
|
16
|
+
logo?: ReactNode;
|
|
17
|
+
/** Quick logo image source. */
|
|
18
|
+
logoSrc?: string;
|
|
19
|
+
logoAlt?: string;
|
|
20
|
+
/** Size / position tuning for logo images. Defaults to `h-7 w-auto`. */
|
|
21
|
+
logoClassName?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
columns: FooterColumn[];
|
|
24
|
+
socials?: ReactNode;
|
|
25
|
+
bottom?: ReactNode;
|
|
26
|
+
/** Optional language switcher rendered in the footer's bottom bar. */
|
|
27
|
+
languageSwitcher?: ReactNode;
|
|
28
|
+
option?: "classic" | "minimal" | "editorial";
|
|
29
|
+
badge?: ReactNode;
|
|
30
|
+
/** Component used to render footer links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
31
|
+
LinkComponent?: ElementType;
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cn } from "../../../utils/cn";
|
|
2
|
+
import { Container } from "../../Container";
|
|
3
|
+
import type { FooterProps } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
FooterWordmark,
|
|
6
|
+
FooterLogoNode,
|
|
7
|
+
FooterBottomBar,
|
|
8
|
+
} from "../parts";
|
|
9
|
+
|
|
10
|
+
interface ClassicProps extends FooterProps {}
|
|
11
|
+
|
|
12
|
+
export function FooterClassic({ className, brand, logo, logoSrc, logoAlt, logoClassName, description, columns, socials, bottom, languageSwitcher, badge, LinkComponent, ...props }: ClassicProps) {
|
|
13
|
+
const Link = LinkComponent ?? "a";
|
|
14
|
+
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand }) ?? <FooterWordmark brand={brand} />;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<footer className={cn("w-full bg-secondary text-on-secondary", className)} {...props}>
|
|
18
|
+
<Container className="py-16">
|
|
19
|
+
<div className="grid grid-cols-2 gap-10 md:grid-cols-4 lg:grid-cols-6">
|
|
20
|
+
<div className="col-span-2">
|
|
21
|
+
<span style={{ color: "rgb(var(--color-on-secondary))" }}>{brandNode}</span>
|
|
22
|
+
{description ? (
|
|
23
|
+
<p className="mt-4 max-w-xs text-sm leading-6 text-on-secondary/60">{description}</p>
|
|
24
|
+
) : null}
|
|
25
|
+
{socials ? <div className="mt-6 flex items-center gap-3">{socials}</div> : null}
|
|
26
|
+
{badge ? <div className="mt-6">{badge}</div> : null}
|
|
27
|
+
</div>
|
|
28
|
+
{columns.map((column) => (
|
|
29
|
+
<div key={column.title}>
|
|
30
|
+
<h3 className="text-xs font-semibold uppercase tracking-[0.16em] text-on-secondary/50">
|
|
31
|
+
{column.title}
|
|
32
|
+
</h3>
|
|
33
|
+
<ul className="mt-4 space-y-2.5">
|
|
34
|
+
{column.links.map((link) => (
|
|
35
|
+
<li key={link.href}>
|
|
36
|
+
<Link
|
|
37
|
+
href={link.href}
|
|
38
|
+
className="text-sm text-on-secondary/70 transition-colors duration-150 hover:text-on-secondary"
|
|
39
|
+
>
|
|
40
|
+
{link.label}
|
|
41
|
+
</Link>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
<FooterBottomBar
|
|
49
|
+
bottom={bottom}
|
|
50
|
+
languageSwitcher={languageSwitcher}
|
|
51
|
+
brand={brand}
|
|
52
|
+
wrapperClassName="mt-14 flex flex-col items-center justify-between gap-4 border-t border-white/10 pt-8 text-sm text-on-secondary/50 sm:flex-row"
|
|
53
|
+
textClassName=""
|
|
54
|
+
/>
|
|
55
|
+
</Container>
|
|
56
|
+
</footer>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
FooterClassic.displayName = "FooterClassic";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cn } from "../../../utils/cn";
|
|
2
|
+
import { Container } from "../../Container";
|
|
3
|
+
import type { FooterProps } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
FooterColumnsBlock,
|
|
6
|
+
FooterEditorialWordmark,
|
|
7
|
+
FooterLogoNode,
|
|
8
|
+
FooterBottomBar,
|
|
9
|
+
} from "../parts";
|
|
10
|
+
|
|
11
|
+
interface EditorialProps extends FooterProps {}
|
|
12
|
+
|
|
13
|
+
export function FooterEditorial({ className, brand, logo, logoSrc, logoAlt, logoClassName, description, columns, socials, bottom, languageSwitcher, LinkComponent, ...props }: EditorialProps) {
|
|
14
|
+
const Link = LinkComponent ?? "a";
|
|
15
|
+
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand, imageClassName: "h-12 w-auto" }) ?? <FooterEditorialWordmark brand={brand} />;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<footer className={cn("w-full border-t border-border bg-background", className)} {...props}>
|
|
19
|
+
<Container className="py-16 sm:py-20">
|
|
20
|
+
<div className="mb-14 flex flex-col justify-between gap-8 sm:flex-row sm:items-end">
|
|
21
|
+
<div>
|
|
22
|
+
{brandNode}
|
|
23
|
+
{description ? (
|
|
24
|
+
<p className="mt-3 max-w-sm text-sm leading-6 text-muted-foreground">{description}</p>
|
|
25
|
+
) : null}
|
|
26
|
+
</div>
|
|
27
|
+
{socials ? <div className="flex items-center gap-3">{socials}</div> : null}
|
|
28
|
+
</div>
|
|
29
|
+
<div className="border-t border-border pt-12">
|
|
30
|
+
<FooterColumnsBlock
|
|
31
|
+
columns={columns}
|
|
32
|
+
Link={Link}
|
|
33
|
+
linkClassName="text-[15px] text-foreground/80 transition-colors duration-150 hover:text-foreground"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<FooterBottomBar
|
|
37
|
+
bottom={bottom}
|
|
38
|
+
languageSwitcher={languageSwitcher}
|
|
39
|
+
brand={brand}
|
|
40
|
+
wrapperClassName="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-8 text-sm text-muted-foreground sm:flex-row"
|
|
41
|
+
textClassName=""
|
|
42
|
+
/>
|
|
43
|
+
</Container>
|
|
44
|
+
</footer>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
FooterEditorial.displayName = "FooterEditorial";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cn } from "../../../utils/cn";
|
|
2
|
+
import { Container } from "../../Container";
|
|
3
|
+
import type { FooterProps } from "../types";
|
|
4
|
+
import { FooterWordmark, FooterLogoNode } from "../parts";
|
|
5
|
+
|
|
6
|
+
interface MinimalProps extends FooterProps {}
|
|
7
|
+
|
|
8
|
+
export function FooterMinimal({ className, brand, logo, logoSrc, logoAlt, logoClassName, columns, languageSwitcher, bottom, LinkComponent, ...props }: MinimalProps) {
|
|
9
|
+
const Link = LinkComponent ?? "a";
|
|
10
|
+
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand }) ?? <FooterWordmark brand={brand} />;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<footer className={cn("w-full border-t border-border bg-background", className)} {...props}>
|
|
14
|
+
<Container className="flex flex-col items-center justify-between gap-6 py-10 sm:flex-row">
|
|
15
|
+
{brandNode}
|
|
16
|
+
<ul className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
|
|
17
|
+
{columns.flatMap((c) => c.links).slice(0, 6).map((link) => (
|
|
18
|
+
<li key={link.href}>
|
|
19
|
+
<Link href={link.href} className="text-sm text-muted-foreground transition-colors duration-150 hover:text-foreground">
|
|
20
|
+
{link.label}
|
|
21
|
+
</Link>
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
<div className="flex flex-col items-center gap-3">
|
|
26
|
+
{languageSwitcher ? <div>{languageSwitcher}</div> : null}
|
|
27
|
+
<div className="text-sm text-muted-foreground">{bottom}</div>
|
|
28
|
+
</div>
|
|
29
|
+
</Container>
|
|
30
|
+
</footer>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
FooterMinimal.displayName = "FooterMinimal";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HeroProps } from "./types";
|
|
3
|
+
import { HeroSplit } from "./variants/Split";
|
|
4
|
+
import { HeroCentered } from "./variants/Centered";
|
|
5
|
+
import { HeroStatement } from "./variants/Statement";
|
|
6
|
+
|
|
7
|
+
export const Hero = forwardRef<HTMLElement, HeroProps>(({ variant = "split", ...props }, ref) => {
|
|
8
|
+
if (variant === "centered") return <HeroCentered ref={ref} {...props} />;
|
|
9
|
+
if (variant === "statement") return <HeroStatement ref={ref} {...props} />;
|
|
10
|
+
return <HeroSplit ref={ref} {...props} />;
|
|
11
|
+
});
|
|
12
|
+
Hero.displayName = "Hero";
|