@ugurdemirel/landcraft 0.1.0
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/README.md +132 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +4521 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5979 -0
- package/package.json +67 -0
- package/src/components/Badge.tsx +68 -0
- package/src/components/Blog.tsx +243 -0
- package/src/components/Button.tsx +94 -0
- package/src/components/CTA.tsx +155 -0
- package/src/components/Card.tsx +80 -0
- package/src/components/Container.tsx +86 -0
- package/src/components/FAQ.tsx +163 -0
- package/src/components/Features.tsx +153 -0
- package/src/components/Footer.tsx +145 -0
- package/src/components/Hero.tsx +169 -0
- package/src/components/LogoCloud.tsx +100 -0
- package/src/components/MegaMenu.tsx +468 -0
- package/src/components/Navbar.tsx +183 -0
- package/src/components/Newsletter.tsx +180 -0
- package/src/components/Pricing.tsx +336 -0
- package/src/components/Prose.tsx +55 -0
- package/src/components/Section.tsx +106 -0
- package/src/components/Stats.tsx +181 -0
- package/src/components/Testimonials.tsx +205 -0
- package/src/icons.tsx +290 -0
- package/src/index.ts +111 -0
- package/src/styles/index.css +87 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/contrast.ts +107 -0
- package/src/utils/useTokenForeground.ts +38 -0
- package/tailwind.preset.cjs +121 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../utils/cn";
|
|
3
|
+
import { useTokenForeground } from "../utils/useTokenForeground";
|
|
4
|
+
|
|
5
|
+
export interface HeroMeta {
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HeroProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
+
variant?: "split" | "centered" | "statement";
|
|
12
|
+
eyebrow?: ReactNode;
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
primaryAction?: ReactNode;
|
|
16
|
+
secondaryAction?: ReactNode;
|
|
17
|
+
media?: ReactNode;
|
|
18
|
+
/** Small trust/status row shown under the copy or CTAs. */
|
|
19
|
+
meta?: HeroMeta[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const titleBase =
|
|
23
|
+
"font-display font-semibold tracking-[-0.03em] text-balance text-foreground";
|
|
24
|
+
|
|
25
|
+
export const Hero = forwardRef<HTMLElement, HeroProps>(
|
|
26
|
+
(
|
|
27
|
+
{ className, variant = "split", eyebrow, title, description, primaryAction, secondaryAction, media, meta, id, ...props },
|
|
28
|
+
ref,
|
|
29
|
+
) => {
|
|
30
|
+
const isCentered = variant === "centered";
|
|
31
|
+
const isStatement = variant === "statement";
|
|
32
|
+
// Statement paints its own background from --color-secondary, so the
|
|
33
|
+
// readable text color must be derived from THAT token, not the brand.
|
|
34
|
+
const fg = useTokenForeground(isStatement ? "--color-secondary" : "--color-primary");
|
|
35
|
+
|
|
36
|
+
const container =
|
|
37
|
+
"relative mx-auto flex w-full max-w-6xl flex-col items-center px-5 sm:px-8";
|
|
38
|
+
|
|
39
|
+
const eyebrowNode =
|
|
40
|
+
typeof eyebrow === "string" ? (
|
|
41
|
+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">{eyebrow}</p>
|
|
42
|
+
) : eyebrow ? (
|
|
43
|
+
<div>{eyebrow}</div>
|
|
44
|
+
) : null;
|
|
45
|
+
|
|
46
|
+
const actionsNode = primaryAction || secondaryAction ? (
|
|
47
|
+
<div className="flex flex-wrap items-center gap-4">{primaryAction}{secondaryAction}</div>
|
|
48
|
+
) : null;
|
|
49
|
+
|
|
50
|
+
/** Split — editorial grid: huge type left, media right, meta rule under. */
|
|
51
|
+
if (variant === "split") {
|
|
52
|
+
return (
|
|
53
|
+
<section
|
|
54
|
+
ref={ref}
|
|
55
|
+
id={id}
|
|
56
|
+
className={cn("relative w-full border-b border-border bg-background", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<div className={cn(container, "items-start gap-14 pt-20 pb-20 sm:pt-28 sm:pb-28 lg:grid lg:grid-cols-[1.05fr_0.95fr] lg:gap-20")}>
|
|
60
|
+
<div className="max-w-xl">
|
|
61
|
+
{eyebrowNode}
|
|
62
|
+
<h1 className={cn(titleBase, "mt-6 text-5xl leading-[1.03] sm:text-6xl lg:text-7xl")}>
|
|
63
|
+
{title}
|
|
64
|
+
</h1>
|
|
65
|
+
{description ? (
|
|
66
|
+
<p className="mt-7 max-w-md text-pretty leading-relaxed text-muted-foreground sm:text-lg">
|
|
67
|
+
{description}
|
|
68
|
+
</p>
|
|
69
|
+
) : null}
|
|
70
|
+
{actionsNode ? <div className="mt-10">{actionsNode}</div> : null}
|
|
71
|
+
{meta ? (
|
|
72
|
+
<ul className="mt-14 flex flex-wrap items-center gap-x-8 gap-y-3 border-t border-border pt-6">
|
|
73
|
+
{meta.map((m) => (
|
|
74
|
+
<li key={m.label} className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
75
|
+
{m.icon ? <span className="text-foreground/35">{m.icon}</span> : null}
|
|
76
|
+
{m.label}
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
{media ? <div className="w-full self-center">{media}</div> : null}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Centered — understated, one statement, generous white space. */
|
|
89
|
+
if (isCentered) {
|
|
90
|
+
return (
|
|
91
|
+
<section
|
|
92
|
+
ref={ref}
|
|
93
|
+
id={id}
|
|
94
|
+
className={cn("relative w-full border-b border-border bg-background", className)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
<div className={cn(container, "pt-24 pb-20 text-center sm:pt-32 sm:pb-24")}>
|
|
98
|
+
{eyebrowNode}
|
|
99
|
+
<h1 className={cn(titleBase, "mt-7 max-w-4xl text-5xl leading-[1.04] sm:text-6xl lg:text-7xl")}>
|
|
100
|
+
{title}
|
|
101
|
+
</h1>
|
|
102
|
+
{description ? (
|
|
103
|
+
<p className="mx-auto mt-7 max-w-xl text-pretty leading-relaxed text-muted-foreground sm:text-lg">
|
|
104
|
+
{description}
|
|
105
|
+
</p>
|
|
106
|
+
) : null}
|
|
107
|
+
{actionsNode ? <div className="mt-10 justify-center">{actionsNode}</div> : null}
|
|
108
|
+
{meta ? (
|
|
109
|
+
<ul className="mt-14 flex flex-wrap items-center justify-center gap-x-9 gap-y-3">
|
|
110
|
+
{meta.map((m, i) => (
|
|
111
|
+
<li
|
|
112
|
+
key={m.label}
|
|
113
|
+
className={cn(
|
|
114
|
+
"flex items-center gap-2 text-sm text-muted-foreground",
|
|
115
|
+
i > 0 && "sm:border-l sm:border-border sm:pl-9",
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{m.icon ? <span className="text-foreground/35">{m.icon}</span> : null}
|
|
119
|
+
{m.label}
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
) : null}
|
|
124
|
+
{media ? <div className="mt-16 w-full">{media}</div> : null}
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Statement — full-bleed ink band. No eyebrow, pure message. */
|
|
131
|
+
return (
|
|
132
|
+
<section
|
|
133
|
+
ref={ref}
|
|
134
|
+
id={id}
|
|
135
|
+
className={cn("relative w-full overflow-hidden bg-secondary", className)}
|
|
136
|
+
style={{ color: fg }}
|
|
137
|
+
{...props}
|
|
138
|
+
>
|
|
139
|
+
<div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.07] mix-blend-overlay" />
|
|
140
|
+
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/10" />
|
|
141
|
+
<div className={cn(container, "pt-28 pb-24 text-center sm:pt-36 sm:pb-32")}>
|
|
142
|
+
<h1
|
|
143
|
+
className="max-w-5xl font-display text-[2.9rem] font-semibold leading-[1.02] tracking-[-0.04em] text-balance sm:text-7xl lg:text-8xl"
|
|
144
|
+
style={{ color: fg }}
|
|
145
|
+
>
|
|
146
|
+
{title}
|
|
147
|
+
</h1>
|
|
148
|
+
{description ? (
|
|
149
|
+
<p className="mx-auto mt-8 max-w-xl text-pretty leading-relaxed sm:text-lg" style={{ color: fg, opacity: 0.72 }}>
|
|
150
|
+
{description}
|
|
151
|
+
</p>
|
|
152
|
+
) : null}
|
|
153
|
+
{actionsNode ? <div className="mt-11 justify-center">{actionsNode}</div> : null}
|
|
154
|
+
{meta ? (
|
|
155
|
+
<ul className="mt-16 flex flex-wrap items-center justify-center gap-x-10 gap-y-3">
|
|
156
|
+
{meta.map((m) => (
|
|
157
|
+
<li key={m.label} className="flex items-center gap-2 text-sm font-medium" style={{ color: fg, opacity: 0.72 }}>
|
|
158
|
+
{m.icon ? <span>{m.icon}</span> : null}
|
|
159
|
+
{m.label}
|
|
160
|
+
</li>
|
|
161
|
+
))}
|
|
162
|
+
</ul>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
Hero.displayName = "Hero";
|
|
@@ -0,0 +1,100 @@
|
|
|
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";
|