@ugurdemirel/landcraft 0.1.4 → 0.1.9
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/styles.css +12 -0
- package/package.json +14 -7
- package/src/components/Badge.tsx +0 -68
- package/src/components/Blog/BlogCard.tsx +0 -12
- package/src/components/Blog/BlogSection.tsx +0 -95
- package/src/components/Blog/index.ts +0 -8
- package/src/components/Blog/parts.tsx +0 -38
- package/src/components/Blog/types.ts +0 -42
- package/src/components/Blog/variants/Card.tsx +0 -62
- package/src/components/Blog/variants/Row.tsx +0 -41
- package/src/components/Button.tsx +0 -113
- package/src/components/CTA/CTA.tsx +0 -12
- package/src/components/CTA/index.ts +0 -2
- package/src/components/CTA/parts.tsx +0 -26
- package/src/components/CTA/types.ts +0 -12
- package/src/components/CTA/variants/Inverse.tsx +0 -50
- package/src/components/CTA/variants/Panel.tsx +0 -60
- package/src/components/CTA/variants/Surface.tsx +0 -34
- package/src/components/Card.tsx +0 -80
- package/src/components/Container.tsx +0 -86
- package/src/components/FAQ/FAQ.tsx +0 -48
- package/src/components/FAQ/index.ts +0 -2
- package/src/components/FAQ/parts.tsx +0 -74
- package/src/components/FAQ/types.ts +0 -18
- package/src/components/FAQ/variants/Accordion.tsx +0 -46
- package/src/components/FAQ/variants/Cards.tsx +0 -40
- package/src/components/FAQ/variants/Split.tsx +0 -44
- package/src/components/Features/FeatureCard.tsx +0 -44
- package/src/components/Features/FeatureGrid.tsx +0 -14
- package/src/components/Features/index.ts +0 -3
- package/src/components/Features/parts.ts +0 -15
- package/src/components/Features/types.ts +0 -25
- package/src/components/Features/variants/Bento.tsx +0 -32
- package/src/components/Features/variants/Columns.tsx +0 -21
- package/src/components/Features/variants/EditorialRows.tsx +0 -38
- package/src/components/Footer/Footer.tsx +0 -11
- package/src/components/Footer/index.ts +0 -2
- package/src/components/Footer/parts.tsx +0 -101
- package/src/components/Footer/types.ts +0 -32
- package/src/components/Footer/variants/Classic.tsx +0 -59
- package/src/components/Footer/variants/Editorial.tsx +0 -47
- package/src/components/Footer/variants/Minimal.tsx +0 -33
- package/src/components/Hero/Hero.tsx +0 -12
- package/src/components/Hero/index.ts +0 -2
- package/src/components/Hero/parts.tsx +0 -29
- package/src/components/Hero/types.ts +0 -20
- package/src/components/Hero/variants/Centered.tsx +0 -54
- package/src/components/Hero/variants/Split.tsx +0 -53
- package/src/components/Hero/variants/Statement.tsx +0 -60
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +0 -37
- package/src/components/LanguageSwitcher/index.ts +0 -6
- package/src/components/LanguageSwitcher/parts.tsx +0 -144
- package/src/components/LanguageSwitcher/types.ts +0 -40
- package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +0 -23
- package/src/components/LanguageSwitcher/variants/Dropdown.tsx +0 -88
- package/src/components/LanguageSwitcher/variants/Modal.tsx +0 -56
- package/src/components/LogoCloud/LogoCloud.tsx +0 -14
- package/src/components/LogoCloud/index.ts +0 -2
- package/src/components/LogoCloud/parts.tsx +0 -32
- package/src/components/LogoCloud/types.ts +0 -16
- package/src/components/LogoCloud/variants/Marquee.tsx +0 -30
- package/src/components/LogoCloud/variants/Quiet.tsx +0 -26
- package/src/components/LogoCloud/variants/Strip.tsx +0 -25
- package/src/components/MegaMenu/MegaMenu.tsx +0 -454
- package/src/components/MegaMenu/index.ts +0 -9
- package/src/components/MegaMenu/types.ts +0 -56
- package/src/components/Modal.tsx +0 -125
- package/src/components/Navbar/Navbar.tsx +0 -185
- package/src/components/Navbar/index.ts +0 -2
- package/src/components/Navbar/types.ts +0 -30
- package/src/components/Newsletter/Newsletter.tsx +0 -12
- package/src/components/Newsletter/index.ts +0 -2
- package/src/components/Newsletter/parts.tsx +0 -23
- package/src/components/Newsletter/types.ts +0 -12
- package/src/components/Newsletter/useNewsletterForm.ts +0 -27
- package/src/components/Newsletter/variants/Card.tsx +0 -62
- package/src/components/Newsletter/variants/Inline.tsx +0 -54
- package/src/components/Newsletter/variants/Underline.tsx +0 -52
- package/src/components/Pricing/Pricing.tsx +0 -37
- package/src/components/Pricing/index.ts +0 -2
- package/src/components/Pricing/parts.tsx +0 -132
- package/src/components/Pricing/types.ts +0 -28
- package/src/components/Pricing/variants/Bento.tsx +0 -72
- package/src/components/Pricing/variants/Cards.tsx +0 -79
- package/src/components/Pricing/variants/Compact.tsx +0 -69
- package/src/components/Prose.tsx +0 -55
- package/src/components/Section.tsx +0 -106
- package/src/components/Slot.tsx +0 -78
- package/src/components/Stats/Stats.tsx +0 -16
- package/src/components/Stats/index.ts +0 -2
- package/src/components/Stats/parts.tsx +0 -29
- package/src/components/Stats/types.ts +0 -23
- package/src/components/Stats/variants/Cells.tsx +0 -34
- package/src/components/Stats/variants/Editorial.tsx +0 -41
- package/src/components/Stats/variants/Hairline.tsx +0 -31
- package/src/components/Stats/variants/Ticker.tsx +0 -36
- package/src/components/Testimonials/TestimonialCard.tsx +0 -61
- package/src/components/Testimonials/Testimonials.tsx +0 -14
- package/src/components/Testimonials/index.ts +0 -3
- package/src/components/Testimonials/parts.ts +0 -7
- package/src/components/Testimonials/types.ts +0 -24
- package/src/components/Testimonials/variants/Carousel.tsx +0 -94
- package/src/components/Testimonials/variants/Grid.tsx +0 -19
- package/src/components/Testimonials/variants/Marquee.tsx +0 -28
- package/src/icons.tsx +0 -290
- package/src/index.ts +0 -120
- package/src/styles/index.css +0 -18
- package/src/utils/cn.ts +0 -6
- package/src/utils/contrast.ts +0 -107
- package/src/utils/iso2Flag.ts +0 -26
- package/src/utils/useTokenForeground.ts +0 -39
- /package/{src/styles → dist}/theme.css +0 -0
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { cn } from "../../utils/cn";
|
|
4
|
-
import { Menu, X } from "../../icons";
|
|
5
|
-
import type { NavbarProps, NavbarVariant } from "./types";
|
|
6
|
-
|
|
7
|
-
interface NavbarStyle {
|
|
8
|
-
shell: string;
|
|
9
|
-
bar: string;
|
|
10
|
-
header: string;
|
|
11
|
-
link: string;
|
|
12
|
-
mobilePanel: string;
|
|
13
|
-
mobileLink: string;
|
|
14
|
-
menuButton: string;
|
|
15
|
-
brandText: string;
|
|
16
|
-
brandDot: string;
|
|
17
|
-
desktopList: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const inverse = "text-on-secondary";
|
|
21
|
-
const light = "text-foreground";
|
|
22
|
-
|
|
23
|
-
const styles: Record<NavbarVariant, NavbarStyle> = {
|
|
24
|
-
classic: {
|
|
25
|
-
shell: "border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
|
|
26
|
-
bar: "mx-auto max-w-6xl px-5 sm:px-8",
|
|
27
|
-
header: "px-0",
|
|
28
|
-
link: "text-muted-foreground hover:text-foreground",
|
|
29
|
-
mobilePanel: "border-t border-border bg-background",
|
|
30
|
-
mobileLink:
|
|
31
|
-
"text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
32
|
-
menuButton: `${light} hover:bg-surface`,
|
|
33
|
-
brandText: light,
|
|
34
|
-
brandDot: "bg-foreground",
|
|
35
|
-
desktopList: "hidden items-center gap-7 md:flex",
|
|
36
|
-
},
|
|
37
|
-
floating: {
|
|
38
|
-
shell: "px-0",
|
|
39
|
-
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",
|
|
40
|
-
header: "px-3 sm:px-6",
|
|
41
|
-
link: "text-muted-foreground hover:text-foreground",
|
|
42
|
-
mobilePanel:
|
|
43
|
-
"mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface",
|
|
44
|
-
mobileLink:
|
|
45
|
-
"text-muted-foreground hover:bg-surface hover:text-foreground",
|
|
46
|
-
menuButton: `${light} hover:bg-surface`,
|
|
47
|
-
brandText: light,
|
|
48
|
-
brandDot: "bg-foreground",
|
|
49
|
-
desktopList: "hidden items-center gap-7 md:flex",
|
|
50
|
-
},
|
|
51
|
-
inverse: {
|
|
52
|
-
shell: "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
|
|
53
|
-
bar: "mx-auto max-w-6xl px-5 sm:px-8",
|
|
54
|
-
header: "px-0",
|
|
55
|
-
link: "text-on-secondary/60 hover:text-on-secondary",
|
|
56
|
-
mobilePanel: "border-t border-white/10 bg-[#101010]",
|
|
57
|
-
mobileLink:
|
|
58
|
-
"text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary",
|
|
59
|
-
menuButton: `${inverse} hover:bg-white/10`,
|
|
60
|
-
brandText: inverse,
|
|
61
|
-
brandDot: "bg-on-secondary",
|
|
62
|
-
desktopList: "hidden items-center gap-7 md:flex",
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const Navbar = ({
|
|
67
|
-
className,
|
|
68
|
-
variant = "classic",
|
|
69
|
-
brand,
|
|
70
|
-
logo,
|
|
71
|
-
logoSrc,
|
|
72
|
-
logoAlt,
|
|
73
|
-
logoClassName,
|
|
74
|
-
brandHref = "#",
|
|
75
|
-
links,
|
|
76
|
-
actions,
|
|
77
|
-
cta,
|
|
78
|
-
languageSwitcher,
|
|
79
|
-
sticky = true,
|
|
80
|
-
LinkComponent,
|
|
81
|
-
...props
|
|
82
|
-
}: NavbarProps) => {
|
|
83
|
-
const [open, setOpen] = useState(false);
|
|
84
|
-
const Link = LinkComponent ?? "a";
|
|
85
|
-
const s = styles[variant];
|
|
86
|
-
|
|
87
|
-
const logoNode =
|
|
88
|
-
logo ??
|
|
89
|
-
(logoSrc ? (
|
|
90
|
-
<img
|
|
91
|
-
src={logoSrc}
|
|
92
|
-
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
93
|
-
className={cn("h-7 w-auto", logoClassName)}
|
|
94
|
-
/>
|
|
95
|
-
) : null);
|
|
96
|
-
|
|
97
|
-
const brandNode = logoNode ? (
|
|
98
|
-
<span className="flex items-center">{logoNode}</span>
|
|
99
|
-
) : (
|
|
100
|
-
<span className={cn("flex items-center gap-2 font-display text-[17px] font-bold tracking-tight", s.brandText)}>
|
|
101
|
-
<span aria-hidden className={cn("inline-block h-2.5 w-2.5 rounded-sm", s.brandDot)} />
|
|
102
|
-
{brand}
|
|
103
|
-
</span>
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<header className={cn("w-full transition-colors duration-200", sticky && "sticky top-0 z-50", s.shell, s.header, className)} {...props}>
|
|
108
|
-
<nav className={cn("flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200", s.bar)} aria-label="Main navigation">
|
|
109
|
-
<Link href={brandHref} className="shrink-0">
|
|
110
|
-
{brandNode}
|
|
111
|
-
</Link>
|
|
112
|
-
|
|
113
|
-
<ul className={s.desktopList}>
|
|
114
|
-
{links.map((link) => (
|
|
115
|
-
<li key={link.href}>
|
|
116
|
-
<Link
|
|
117
|
-
href={link.href}
|
|
118
|
-
className={cn("text-sm font-medium transition-colors duration-150", s.link)}
|
|
119
|
-
>
|
|
120
|
-
{link.label}
|
|
121
|
-
</Link>
|
|
122
|
-
</li>
|
|
123
|
-
))}
|
|
124
|
-
</ul>
|
|
125
|
-
|
|
126
|
-
<div className="hidden items-center gap-3 md:flex">
|
|
127
|
-
{languageSwitcher}
|
|
128
|
-
{actions}
|
|
129
|
-
{cta}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
{languageSwitcher ? (
|
|
133
|
-
<div className="flex items-center md:hidden">{languageSwitcher}</div>
|
|
134
|
-
) : null}
|
|
135
|
-
|
|
136
|
-
<button
|
|
137
|
-
type="button"
|
|
138
|
-
aria-label={open ? "Close menu" : "Open menu"}
|
|
139
|
-
aria-expanded={open}
|
|
140
|
-
onClick={() => setOpen((o) => !o)}
|
|
141
|
-
className={cn(
|
|
142
|
-
"inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md md:hidden",
|
|
143
|
-
"transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
144
|
-
s.menuButton,
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
147
|
-
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
148
|
-
</button>
|
|
149
|
-
</nav>
|
|
150
|
-
|
|
151
|
-
<div
|
|
152
|
-
className={cn(
|
|
153
|
-
"grid transition-[grid-template-rows,opacity] duration-200 md:hidden",
|
|
154
|
-
open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
155
|
-
s.mobilePanel,
|
|
156
|
-
)}
|
|
157
|
-
>
|
|
158
|
-
<div className="min-h-0 overflow-hidden">
|
|
159
|
-
<ul className={cn("flex flex-col py-4", variant !== "floating" && "px-5")}>
|
|
160
|
-
{links.map((link) => (
|
|
161
|
-
<li key={link.href}>
|
|
162
|
-
<Link
|
|
163
|
-
href={link.href}
|
|
164
|
-
onClick={() => setOpen(false)}
|
|
165
|
-
className={cn(
|
|
166
|
-
"block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
|
|
167
|
-
s.mobileLink,
|
|
168
|
-
)}
|
|
169
|
-
>
|
|
170
|
-
{link.label}
|
|
171
|
-
</Link>
|
|
172
|
-
</li>
|
|
173
|
-
))}
|
|
174
|
-
{cta ? (
|
|
175
|
-
<li className="mt-3 px-2 pb-2">{cta}</li>
|
|
176
|
-
) : actions ? (
|
|
177
|
-
<li className="mt-3 flex flex-wrap gap-3 px-2 pb-2">{actions}</li>
|
|
178
|
-
) : null}
|
|
179
|
-
</ul>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</header>
|
|
183
|
-
);
|
|
184
|
-
};
|
|
185
|
-
Navbar.displayName = "Navbar";
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface NavLink {
|
|
4
|
-
label: string;
|
|
5
|
-
href: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type NavbarVariant = "classic" | "floating" | "inverse";
|
|
9
|
-
|
|
10
|
-
export interface NavbarProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
-
variant?: NavbarVariant;
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { NewsletterProps } from "./types";
|
|
2
|
-
import { NewsletterInline } from "./variants/Inline";
|
|
3
|
-
import { NewsletterCard } from "./variants/Card";
|
|
4
|
-
import { NewsletterUnderline } from "./variants/Underline";
|
|
5
|
-
|
|
6
|
-
export const Newsletter = (props: NewsletterProps) => {
|
|
7
|
-
const { option = "inline", ...rest } = props;
|
|
8
|
-
if (option === "card") return <NewsletterCard {...rest} />;
|
|
9
|
-
if (option === "underline") return <NewsletterUnderline {...rest} />;
|
|
10
|
-
return <NewsletterInline {...rest} />;
|
|
11
|
-
};
|
|
12
|
-
Newsletter.displayName = "Newsletter";
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Check } from "../../icons";
|
|
2
|
-
|
|
3
|
-
export const inputBase =
|
|
4
|
-
"w-full rounded-lg border border-border bg-surface px-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background";
|
|
5
|
-
|
|
6
|
-
export function NewsletterSuccess({ message }: { message: string }) {
|
|
7
|
-
return (
|
|
8
|
-
<p className="flex items-center gap-2 rounded-lg border border-accent/40 bg-accent/10 px-4 py-3.5 text-sm font-medium text-accent" role="status">
|
|
9
|
-
<Check className="h-4 w-4" />
|
|
10
|
-
{message}
|
|
11
|
-
</p>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function NewsletterNote({ error, note }: { error: string | null; note?: string }) {
|
|
16
|
-
if (error) {
|
|
17
|
-
return <p className="mt-2 text-sm font-medium text-red-500" role="alert">{error}</p>;
|
|
18
|
-
}
|
|
19
|
-
if (note) {
|
|
20
|
-
return <p className="mt-3 text-xs leading-5 text-muted-foreground">{note}</p>;
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes } from "react";
|
|
2
|
-
|
|
3
|
-
export type NewsletterOption = "inline" | "card" | "underline";
|
|
4
|
-
|
|
5
|
-
export interface NewsletterProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
6
|
-
option?: NewsletterOption;
|
|
7
|
-
placeholder?: string;
|
|
8
|
-
buttonLabel?: string;
|
|
9
|
-
successMessage?: string;
|
|
10
|
-
note?: string;
|
|
11
|
-
onSubmit?: (email: string) => void;
|
|
12
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useState, type FormEvent } from "react";
|
|
3
|
-
|
|
4
|
-
export function useNewsletterForm(onSubmit?: (email: string) => void) {
|
|
5
|
-
const [email, setEmail] = useState("");
|
|
6
|
-
const [done, setDone] = useState(false);
|
|
7
|
-
const [error, setError] = useState<string | null>(null);
|
|
8
|
-
|
|
9
|
-
const handleSubmit = (event: FormEvent) => {
|
|
10
|
-
event.preventDefault();
|
|
11
|
-
const value = email.trim();
|
|
12
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
13
|
-
setError("Please enter a valid email.");
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
setError(null);
|
|
17
|
-
setDone(true);
|
|
18
|
-
onSubmit?.(value);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const updateEmail = (value: string) => {
|
|
22
|
-
setEmail(value);
|
|
23
|
-
if (error) setError(null);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
return { email, done, error, handleSubmit, updateEmail };
|
|
27
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { ArrowRight, Mail } from "../../../icons";
|
|
4
|
-
import { useNewsletterForm } from "../useNewsletterForm";
|
|
5
|
-
import { inputBase, NewsletterSuccess, NewsletterNote } from "../parts";
|
|
6
|
-
import type { NewsletterProps } from "../types";
|
|
7
|
-
|
|
8
|
-
interface CardProps extends Omit<NewsletterProps, "option"> {}
|
|
9
|
-
|
|
10
|
-
export function NewsletterCard({
|
|
11
|
-
className,
|
|
12
|
-
placeholder = "you@company.com",
|
|
13
|
-
buttonLabel = "Join",
|
|
14
|
-
successMessage = "You’re on the list!",
|
|
15
|
-
note,
|
|
16
|
-
onSubmit,
|
|
17
|
-
...props
|
|
18
|
-
}: CardProps) {
|
|
19
|
-
const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
|
|
23
|
-
{done ? (
|
|
24
|
-
<NewsletterSuccess message={successMessage} />
|
|
25
|
-
) : (
|
|
26
|
-
<div className="rounded-xl border border-border bg-surface p-7">
|
|
27
|
-
<div className="mb-5 flex items-center gap-3">
|
|
28
|
-
<span className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-primary-soft text-primary">
|
|
29
|
-
<Mail className="h-4 w-4" />
|
|
30
|
-
</span>
|
|
31
|
-
<p className="font-display text-lg font-semibold tracking-tight text-foreground">
|
|
32
|
-
Monthly digest
|
|
33
|
-
</p>
|
|
34
|
-
</div>
|
|
35
|
-
<label htmlFor="newsletter-email" className="sr-only">
|
|
36
|
-
Your email address
|
|
37
|
-
</label>
|
|
38
|
-
<div className="flex flex-col gap-3 sm:flex-row">
|
|
39
|
-
<input
|
|
40
|
-
id="newsletter-email"
|
|
41
|
-
type="email"
|
|
42
|
-
autoComplete="email"
|
|
43
|
-
value={email}
|
|
44
|
-
onChange={(e) => updateEmail(e.target.value)}
|
|
45
|
-
placeholder={placeholder}
|
|
46
|
-
className={cn(inputBase, "h-11")}
|
|
47
|
-
/>
|
|
48
|
-
<button
|
|
49
|
-
type="submit"
|
|
50
|
-
className="inline-flex h-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-5 text-sm font-semibold text-on-primary transition-colors duration-200 hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
51
|
-
>
|
|
52
|
-
{buttonLabel}
|
|
53
|
-
<ArrowRight className="h-4 w-4 opacity-60" />
|
|
54
|
-
</button>
|
|
55
|
-
</div>
|
|
56
|
-
<NewsletterNote error={error} note={note} />
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
</form>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
NewsletterCard.displayName = "NewsletterCard";
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { ArrowRight } from "../../../icons";
|
|
4
|
-
import { useNewsletterForm } from "../useNewsletterForm";
|
|
5
|
-
import { inputBase, NewsletterSuccess, NewsletterNote } from "../parts";
|
|
6
|
-
import type { NewsletterProps } from "../types";
|
|
7
|
-
|
|
8
|
-
interface InlineProps extends Omit<NewsletterProps, "option"> {}
|
|
9
|
-
|
|
10
|
-
export function NewsletterInline({
|
|
11
|
-
className,
|
|
12
|
-
placeholder = "you@company.com",
|
|
13
|
-
buttonLabel = "Join",
|
|
14
|
-
successMessage = "You’re on the list!",
|
|
15
|
-
note,
|
|
16
|
-
onSubmit,
|
|
17
|
-
...props
|
|
18
|
-
}: InlineProps) {
|
|
19
|
-
const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
|
|
23
|
-
{done ? (
|
|
24
|
-
<NewsletterSuccess message={successMessage} />
|
|
25
|
-
) : (
|
|
26
|
-
<>
|
|
27
|
-
<label htmlFor="newsletter-email" className="sr-only">
|
|
28
|
-
Your email address
|
|
29
|
-
</label>
|
|
30
|
-
<div className="flex flex-col gap-3 sm:flex-row">
|
|
31
|
-
<input
|
|
32
|
-
id="newsletter-email"
|
|
33
|
-
type="email"
|
|
34
|
-
autoComplete="email"
|
|
35
|
-
value={email}
|
|
36
|
-
onChange={(e) => updateEmail(e.target.value)}
|
|
37
|
-
placeholder={placeholder}
|
|
38
|
-
className={cn(inputBase, "h-11")}
|
|
39
|
-
/>
|
|
40
|
-
<button
|
|
41
|
-
type="submit"
|
|
42
|
-
className="inline-flex h-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-5 text-sm font-semibold text-on-primary transition-colors duration-200 hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
43
|
-
>
|
|
44
|
-
{buttonLabel}
|
|
45
|
-
<ArrowRight className="h-4 w-4 opacity-60" />
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
<NewsletterNote error={error} note={note} />
|
|
49
|
-
</>
|
|
50
|
-
)}
|
|
51
|
-
</form>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
NewsletterInline.displayName = "NewsletterInline";
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { ArrowRight } from "../../../icons";
|
|
4
|
-
import { useNewsletterForm } from "../useNewsletterForm";
|
|
5
|
-
import { NewsletterSuccess, NewsletterNote } from "../parts";
|
|
6
|
-
import type { NewsletterProps } from "../types";
|
|
7
|
-
|
|
8
|
-
interface UnderlineProps extends Omit<NewsletterProps, "option"> {}
|
|
9
|
-
|
|
10
|
-
export function NewsletterUnderline({
|
|
11
|
-
className,
|
|
12
|
-
placeholder = "you@company.com",
|
|
13
|
-
buttonLabel = "Join",
|
|
14
|
-
successMessage = "You’re on the list!",
|
|
15
|
-
note,
|
|
16
|
-
onSubmit,
|
|
17
|
-
...props
|
|
18
|
-
}: UnderlineProps) {
|
|
19
|
-
const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
|
|
23
|
-
{done ? (
|
|
24
|
-
<NewsletterSuccess message={successMessage} />
|
|
25
|
-
) : (
|
|
26
|
-
<div className="flex items-end gap-3 border-b border-foreground/20 pb-3 transition-colors duration-200 focus-within:border-foreground">
|
|
27
|
-
<label htmlFor="newsletter-email" className="sr-only">
|
|
28
|
-
Your email address
|
|
29
|
-
</label>
|
|
30
|
-
<input
|
|
31
|
-
id="newsletter-email"
|
|
32
|
-
type="email"
|
|
33
|
-
autoComplete="email"
|
|
34
|
-
value={email}
|
|
35
|
-
onChange={(e) => updateEmail(e.target.value)}
|
|
36
|
-
placeholder={placeholder}
|
|
37
|
-
className="w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
|
|
38
|
-
/>
|
|
39
|
-
<button
|
|
40
|
-
type="submit"
|
|
41
|
-
className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 text-sm font-semibold text-foreground transition-colors duration-150 hover:text-primary"
|
|
42
|
-
>
|
|
43
|
-
{buttonLabel}
|
|
44
|
-
<ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
|
|
45
|
-
</button>
|
|
46
|
-
</div>
|
|
47
|
-
)}
|
|
48
|
-
<NewsletterNote error={error} note={note} />
|
|
49
|
-
</form>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
NewsletterUnderline.displayName = "NewsletterUnderline";
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import type { PricingProps } from "./types";
|
|
4
|
-
import { PricingCards } from "./variants/Cards";
|
|
5
|
-
import { PricingBento } from "./variants/Bento";
|
|
6
|
-
import { PricingCompact } from "./variants/Compact";
|
|
7
|
-
|
|
8
|
-
export const Pricing = ({
|
|
9
|
-
className,
|
|
10
|
-
plans,
|
|
11
|
-
option = "cards",
|
|
12
|
-
defaultBilling = "monthly",
|
|
13
|
-
onSelect,
|
|
14
|
-
...props
|
|
15
|
-
}: PricingProps) => {
|
|
16
|
-
const [billing, setBilling] = useState<"monthly" | "yearly">(defaultBilling);
|
|
17
|
-
|
|
18
|
-
if (option === "compact") {
|
|
19
|
-
return <PricingCompact className={className} plans={plans} billing={billing} onSelect={onSelect} {...props} />;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (option === "bento") {
|
|
23
|
-
return <PricingBento className={className} plans={plans} billing={billing} onSelect={onSelect} {...props} />;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<PricingCards
|
|
28
|
-
className={className}
|
|
29
|
-
plans={plans}
|
|
30
|
-
billing={billing}
|
|
31
|
-
onSelect={onSelect}
|
|
32
|
-
onBillingChange={setBilling}
|
|
33
|
-
{...props}
|
|
34
|
-
/>
|
|
35
|
-
);
|
|
36
|
-
};
|
|
37
|
-
Pricing.displayName = "Pricing";
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { cn } from "../../utils/cn";
|
|
3
|
-
import { getContrastText } from "../../utils/contrast";
|
|
4
|
-
import { useTokenForeground } from "../../utils/useTokenForeground";
|
|
5
|
-
import { ArrowRight } from "../../icons";
|
|
6
|
-
import type { Plan } from "./types";
|
|
7
|
-
|
|
8
|
-
export function PriceValue({
|
|
9
|
-
value,
|
|
10
|
-
billing,
|
|
11
|
-
dim,
|
|
12
|
-
}: {
|
|
13
|
-
value: number | null;
|
|
14
|
-
billing: "monthly" | "yearly";
|
|
15
|
-
/** False when rendered on a colored background where opacity would kill contrast. */
|
|
16
|
-
dim?: boolean;
|
|
17
|
-
}) {
|
|
18
|
-
if (value === null) {
|
|
19
|
-
return <span>Custom</span>;
|
|
20
|
-
}
|
|
21
|
-
return (
|
|
22
|
-
<>
|
|
23
|
-
<span className="text-[2.6rem] font-semibold tracking-tight sm:text-[3rem]">${value}</span>
|
|
24
|
-
<span className={cn("text-sm font-normal", dim && "opacity-60")}>
|
|
25
|
-
/{billing === "monthly" ? "mo" : "yr"}
|
|
26
|
-
</span>
|
|
27
|
-
</>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function BillingToggle({
|
|
32
|
-
billing,
|
|
33
|
-
onChange,
|
|
34
|
-
dark,
|
|
35
|
-
}: {
|
|
36
|
-
billing: "monthly" | "yearly";
|
|
37
|
-
onChange: (b: "monthly" | "yearly") => void;
|
|
38
|
-
dark?: boolean;
|
|
39
|
-
}) {
|
|
40
|
-
// The accent token is a background color, but its shipped "on-accent"
|
|
41
|
-
// pairing isn't guaranteed to be contrast-safe (e.g. white on amber).
|
|
42
|
-
// Derive the badge text color from the actual accent value instead.
|
|
43
|
-
const accentText = useTokenForeground("--color-accent");
|
|
44
|
-
return (
|
|
45
|
-
<div className="mb-10 flex items-center justify-center gap-4">
|
|
46
|
-
<span
|
|
47
|
-
className={cn(
|
|
48
|
-
"text-sm font-medium transition-colors",
|
|
49
|
-
billing === "monthly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
|
|
50
|
-
)}
|
|
51
|
-
>
|
|
52
|
-
Monthly
|
|
53
|
-
</span>
|
|
54
|
-
<button
|
|
55
|
-
type="button"
|
|
56
|
-
role="switch"
|
|
57
|
-
aria-checked={billing === "yearly"}
|
|
58
|
-
aria-label="Billing period"
|
|
59
|
-
onClick={() => onChange(billing === "monthly" ? "yearly" : "monthly")}
|
|
60
|
-
className={cn(
|
|
61
|
-
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200",
|
|
62
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
63
|
-
billing === "yearly" ? "bg-primary" : "bg-border",
|
|
64
|
-
)}
|
|
65
|
-
>
|
|
66
|
-
<span
|
|
67
|
-
className={cn(
|
|
68
|
-
"inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200",
|
|
69
|
-
billing === "yearly" ? "translate-x-6" : "translate-x-1",
|
|
70
|
-
)}
|
|
71
|
-
/>
|
|
72
|
-
</button>
|
|
73
|
-
<span
|
|
74
|
-
className={cn(
|
|
75
|
-
"flex items-center gap-2 text-sm font-medium transition-colors",
|
|
76
|
-
billing === "yearly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
|
|
77
|
-
)}
|
|
78
|
-
>
|
|
79
|
-
Yearly
|
|
80
|
-
<span
|
|
81
|
-
className="rounded-full bg-accent px-2 py-0.5 text-xs font-semibold"
|
|
82
|
-
style={{ color: accentText }}
|
|
83
|
-
>
|
|
84
|
-
−2 ay
|
|
85
|
-
</span>
|
|
86
|
-
</span>
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function PlanButton({
|
|
92
|
-
plan,
|
|
93
|
-
billing,
|
|
94
|
-
onSelect,
|
|
95
|
-
dark,
|
|
96
|
-
}: {
|
|
97
|
-
plan: Plan;
|
|
98
|
-
billing: "monthly" | "yearly";
|
|
99
|
-
onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
|
|
100
|
-
dark?: boolean;
|
|
101
|
-
}) {
|
|
102
|
-
const isHighlighted = plan.highlighted && !plan.customColor;
|
|
103
|
-
const fg = plan.customColor ? getContrastText(plan.customColor, "#111111", "#ffffff") : undefined;
|
|
104
|
-
return (
|
|
105
|
-
<button
|
|
106
|
-
type="button"
|
|
107
|
-
onClick={() => onSelect?.(plan, billing)}
|
|
108
|
-
className={cn(
|
|
109
|
-
"inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-lg px-5 text-sm font-semibold transition-colors duration-200",
|
|
110
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
111
|
-
isHighlighted || plan.customColor
|
|
112
|
-
? "hover:brightness-95"
|
|
113
|
-
: dark
|
|
114
|
-
? "bg-white text-black hover:bg-white/90"
|
|
115
|
-
: "border border-border text-foreground hover:bg-surface",
|
|
116
|
-
)}
|
|
117
|
-
style={
|
|
118
|
-
plan.customColor
|
|
119
|
-
? { backgroundColor: plan.customColor, color: fg }
|
|
120
|
-
: isHighlighted
|
|
121
|
-
? {
|
|
122
|
-
backgroundColor: "rgb(var(--color-primary))",
|
|
123
|
-
color: "rgb(var(--color-on-primary))",
|
|
124
|
-
}
|
|
125
|
-
: undefined
|
|
126
|
-
}
|
|
127
|
-
>
|
|
128
|
-
{plan.cta || "Start"}
|
|
129
|
-
<ArrowRight className="h-4 w-4 opacity-60" />
|
|
130
|
-
</button>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes } from "react";
|
|
2
|
-
|
|
3
|
-
export type PricingOption = "cards" | "bento" | "compact";
|
|
4
|
-
|
|
5
|
-
export interface Plan {
|
|
6
|
-
name: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
monthly: number | null;
|
|
9
|
-
yearly: number | null;
|
|
10
|
-
features: string[];
|
|
11
|
-
cta?: string;
|
|
12
|
-
href?: string;
|
|
13
|
-
highlighted?: boolean;
|
|
14
|
-
customColor?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PricingProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
18
|
-
plans: Plan[];
|
|
19
|
-
option?: PricingOption;
|
|
20
|
-
defaultBilling?: "monthly" | "yearly";
|
|
21
|
-
onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface PricingVariantProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
25
|
-
plans: Plan[];
|
|
26
|
-
billing: "monthly" | "yearly";
|
|
27
|
-
onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
|
|
28
|
-
}
|