@ugurdemirel/landcraft 0.1.5 → 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/package.json +4 -4
- package/src/components/Badge.test.tsx +0 -55
- package/src/components/Badge.tsx +0 -68
- package/src/components/Blog/Blog.test.tsx +0 -64
- 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.test.tsx +0 -91
- package/src/components/Button.tsx +0 -113
- package/src/components/CTA/CTA.test.tsx +0 -53
- 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.test.tsx +0 -49
- package/src/components/Card.tsx +0 -80
- package/src/components/Container.test.tsx +0 -52
- package/src/components/Container.tsx +0 -86
- package/src/components/FAQ/FAQ.test.tsx +0 -54
- 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/Features.test.tsx +0 -49
- 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.test.tsx +0 -49
- 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.test.tsx +0 -71
- 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.test.tsx +0 -64
- 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.test.tsx +0 -35
- 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.test.tsx +0 -89
- 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.test.tsx +0 -80
- package/src/components/Modal.tsx +0 -125
- package/src/components/Navbar/Navbar.test.tsx +0 -91
- 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.test.tsx +0 -45
- 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.test.tsx +0 -67
- 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.test.tsx +0 -49
- package/src/components/Prose.tsx +0 -55
- package/src/components/Section.test.tsx +0 -51
- package/src/components/Section.tsx +0 -106
- package/src/components/Slot.test.tsx +0 -45
- package/src/components/Slot.tsx +0 -78
- package/src/components/Stats/Stats.test.tsx +0 -47
- 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.test.tsx +0 -48
- 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/test/setup.ts +0 -7
- package/src/utils/cn.test.ts +0 -21
- package/src/utils/cn.ts +0 -6
- package/src/utils/contrast.test.ts +0 -71
- package/src/utils/contrast.ts +0 -107
- package/src/utils/iso2Flag.test.ts +0 -26
- package/src/utils/iso2Flag.ts +0 -26
- package/src/utils/useTokenForeground.test.ts +0 -50
- package/src/utils/useTokenForeground.ts +0 -39
- /package/{src/styles → dist}/theme.css +0 -0
|
@@ -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,67 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
-
import { Pricing } from "./Pricing";
|
|
4
|
-
|
|
5
|
-
const plans = [
|
|
6
|
-
{
|
|
7
|
-
name: "Starter",
|
|
8
|
-
monthly: 9,
|
|
9
|
-
yearly: 90,
|
|
10
|
-
features: ["1 project"],
|
|
11
|
-
cta: "Get started",
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
name: "Pro",
|
|
15
|
-
monthly: 29,
|
|
16
|
-
yearly: 290,
|
|
17
|
-
highlighted: true,
|
|
18
|
-
features: ["Unlimited projects"],
|
|
19
|
-
cta: "Go Pro",
|
|
20
|
-
},
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
describe("Pricing", () => {
|
|
24
|
-
it("renders plan names, prices and the cards option by default", () => {
|
|
25
|
-
render(<Pricing plans={plans} />);
|
|
26
|
-
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
27
|
-
expect(screen.getByText("Pro")).toBeInTheDocument();
|
|
28
|
-
// Monthly price for the starter plan is $9 (default billing = monthly).
|
|
29
|
-
expect(screen.getByText("$9")).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("switches between monthly and yearly billing via the toggle", () => {
|
|
33
|
-
render(<Pricing plans={[plans[0]]} />);
|
|
34
|
-
expect(screen.getByText("$9")).toBeInTheDocument();
|
|
35
|
-
|
|
36
|
-
fireEvent.click(screen.getByRole("switch"));
|
|
37
|
-
expect(screen.getByText("$90")).toBeInTheDocument();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("renders the compact option", () => {
|
|
41
|
-
render(<Pricing plans={plans} option="compact" />);
|
|
42
|
-
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("renders the bento option", () => {
|
|
46
|
-
render(<Pricing plans={plans} option="bento" />);
|
|
47
|
-
expect(screen.getByText("Starter")).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("calls onSelect with the plan and current billing when a plan button is pressed", () => {
|
|
51
|
-
const onSelect = vi.fn();
|
|
52
|
-
render(<Pricing plans={[plans[0]]} onSelect={onSelect} />);
|
|
53
|
-
fireEvent.click(screen.getByRole("button", { name: "Get started" }));
|
|
54
|
-
expect(onSelect).toHaveBeenCalledWith(plans[0], "monthly");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("renders a price label for custom (null) pricing", () => {
|
|
58
|
-
render(
|
|
59
|
-
<Pricing
|
|
60
|
-
plans={[
|
|
61
|
-
{ name: "Enterprise", monthly: null, yearly: null, features: [], cta: "Contact" },
|
|
62
|
-
]}
|
|
63
|
-
/>,
|
|
64
|
-
);
|
|
65
|
-
expect(screen.getByText("Custom")).toBeInTheDocument();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -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
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { getContrastText } from "../../../utils/contrast";
|
|
3
|
-
import { PriceValue, PlanButton } from "../parts";
|
|
4
|
-
import { Check } from "../../../icons";
|
|
5
|
-
import type { PricingVariantProps } from "../types";
|
|
6
|
-
|
|
7
|
-
interface BentoProps extends PricingVariantProps {
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const PricingBento = ({ className, plans, billing, onSelect, ...props }: BentoProps) => {
|
|
12
|
-
return (
|
|
13
|
-
<div className={cn("w-full", className)} {...props}>
|
|
14
|
-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
15
|
-
{plans.map((plan) => {
|
|
16
|
-
const isHighlighted = plan.highlighted && !plan.customColor;
|
|
17
|
-
const bg = plan.customColor || (isHighlighted ? "rgb(var(--color-primary))" : undefined);
|
|
18
|
-
const fg = plan.customColor
|
|
19
|
-
? getContrastText(plan.customColor, "#111111", "#ffffff")
|
|
20
|
-
: isHighlighted
|
|
21
|
-
? "rgb(var(--color-on-primary))"
|
|
22
|
-
: undefined;
|
|
23
|
-
return (
|
|
24
|
-
<div
|
|
25
|
-
key={plan.name}
|
|
26
|
-
className={cn(
|
|
27
|
-
"flex h-full flex-col rounded-xl p-8",
|
|
28
|
-
bg
|
|
29
|
-
? "lg:col-span-2"
|
|
30
|
-
: "border border-border bg-surface",
|
|
31
|
-
)}
|
|
32
|
-
style={bg ? { backgroundColor: bg, color: fg } : undefined}
|
|
33
|
-
>
|
|
34
|
-
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
35
|
-
<div>
|
|
36
|
-
<h3 className={cn("font-display text-xl font-semibold tracking-tight", bg ? "text-current" : "text-foreground")}>
|
|
37
|
-
{plan.name}
|
|
38
|
-
</h3>
|
|
39
|
-
{plan.description ? (
|
|
40
|
-
<p className={cn("mt-1.5 text-sm", bg ? undefined : "text-muted-foreground")}>
|
|
41
|
-
{plan.description}
|
|
42
|
-
</p>
|
|
43
|
-
) : null}
|
|
44
|
-
</div>
|
|
45
|
-
<div className={cn("font-display", bg ? "text-current" : "text-foreground")}>
|
|
46
|
-
<PriceValue value={billing === "monthly" ? plan.monthly : plan.yearly} billing={billing} dim={!bg} />
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
<ul
|
|
50
|
-
className={cn(
|
|
51
|
-
"mt-7 grid grid-cols-1 gap-x-8 gap-y-2.5 text-sm sm:grid-cols-2",
|
|
52
|
-
bg ? "lg:max-w-2xl" : undefined,
|
|
53
|
-
)}
|
|
54
|
-
>
|
|
55
|
-
{plan.features.map((feature) => (
|
|
56
|
-
<li key={feature} className={cn("flex items-start gap-2.5", bg ? "text-current" : "text-muted-foreground")}>
|
|
57
|
-
<Check className={cn("mt-0.5 h-4 w-4 shrink-0", bg ? "text-current" : "text-primary")} />
|
|
58
|
-
{feature}
|
|
59
|
-
</li>
|
|
60
|
-
))}
|
|
61
|
-
</ul>
|
|
62
|
-
<div className="mt-8">
|
|
63
|
-
<PlanButton plan={plan} billing={billing} onSelect={onSelect} dark={!bg} />
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
PricingBento.displayName = "PricingBento";
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { getContrastText } from "../../../utils/contrast";
|
|
3
|
-
import { PriceValue, BillingToggle, PlanButton } from "../parts";
|
|
4
|
-
import { Check } from "../../../icons";
|
|
5
|
-
import type { PricingVariantProps } from "../types";
|
|
6
|
-
|
|
7
|
-
interface CardsProps extends PricingVariantProps {
|
|
8
|
-
className?: string;
|
|
9
|
-
onBillingChange: (billing: "monthly" | "yearly") => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const PricingCards = ({ className, plans, billing, onSelect, onBillingChange, ...props }: CardsProps) => {
|
|
13
|
-
return (
|
|
14
|
-
<div className={cn("w-full", className)} {...props}>
|
|
15
|
-
<BillingToggle billing={billing} onChange={onBillingChange} />
|
|
16
|
-
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
|
|
17
|
-
{plans.map((plan) => {
|
|
18
|
-
const isHighlighted = plan.highlighted && !plan.customColor;
|
|
19
|
-
const bg = plan.customColor || (isHighlighted ? "rgb(var(--color-primary))" : undefined);
|
|
20
|
-
const fg = plan.customColor
|
|
21
|
-
? getContrastText(plan.customColor, "#111111", "#ffffff")
|
|
22
|
-
: isHighlighted
|
|
23
|
-
? "rgb(var(--color-on-primary))"
|
|
24
|
-
: undefined;
|
|
25
|
-
return (
|
|
26
|
-
<div
|
|
27
|
-
key={plan.name}
|
|
28
|
-
className={cn(
|
|
29
|
-
"relative flex flex-col rounded-xl p-7",
|
|
30
|
-
bg ? "shadow-raised" : "border border-border bg-surface",
|
|
31
|
-
!bg && isHighlighted && "border-primary",
|
|
32
|
-
)}
|
|
33
|
-
style={bg ? { backgroundColor: bg, color: fg } : undefined}
|
|
34
|
-
>
|
|
35
|
-
{isHighlighted ? (
|
|
36
|
-
<span
|
|
37
|
-
className={cn(
|
|
38
|
-
"absolute -top-3 left-7 rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider",
|
|
39
|
-
!bg && "bg-primary text-on-primary",
|
|
40
|
-
)}
|
|
41
|
-
style={
|
|
42
|
-
bg
|
|
43
|
-
? { backgroundColor: "rgb(var(--color-on-primary))", color: "rgb(var(--color-primary))" }
|
|
44
|
-
: undefined
|
|
45
|
-
}
|
|
46
|
-
>
|
|
47
|
-
Most popular
|
|
48
|
-
</span>
|
|
49
|
-
) : null}
|
|
50
|
-
<h3 className={cn("font-display text-lg font-semibold tracking-tight", bg ? "text-current" : "text-foreground")}>
|
|
51
|
-
{plan.name}
|
|
52
|
-
</h3>
|
|
53
|
-
{plan.description ? (
|
|
54
|
-
<p className={cn("mt-1.5 text-sm", bg ? undefined : "text-muted-foreground")}>
|
|
55
|
-
{plan.description}
|
|
56
|
-
</p>
|
|
57
|
-
) : null}
|
|
58
|
-
<div className={cn("mt-6 flex items-baseline font-display", bg ? "text-current" : "text-foreground")}>
|
|
59
|
-
<PriceValue value={billing === "monthly" ? plan.monthly : plan.yearly} billing={billing} dim={!bg} />
|
|
60
|
-
</div>
|
|
61
|
-
<ul className={cn("mt-7 flex-1 space-y-2.5 text-sm", bg ? "text-current" : "text-muted-foreground")}>
|
|
62
|
-
{plan.features.map((feature) => (
|
|
63
|
-
<li key={feature} className="flex items-start gap-2.5">
|
|
64
|
-
<Check className={cn("mt-0.5 h-4 w-4 shrink-0", bg ? "text-current" : "text-primary")} />
|
|
65
|
-
{feature}
|
|
66
|
-
</li>
|
|
67
|
-
))}
|
|
68
|
-
</ul>
|
|
69
|
-
<div className="mt-8">
|
|
70
|
-
<PlanButton plan={plan} billing={billing} onSelect={onSelect} dark={!bg} />
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
};
|
|
79
|
-
PricingCards.displayName = "PricingCards";
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { PlanButton } from "../parts";
|
|
3
|
-
import { Check } from "../../../icons";
|
|
4
|
-
import type { PricingVariantProps } from "../types";
|
|
5
|
-
|
|
6
|
-
interface CompactProps extends PricingVariantProps {
|
|
7
|
-
className?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const PricingCompact = ({ className, plans, billing, onSelect, ...props }: CompactProps) => {
|
|
11
|
-
const price = (v: number | null) => (v === null ? "Custom" : `$${v}`);
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<div className={cn("w-full", className)} {...props}>
|
|
15
|
-
<div className="divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface">
|
|
16
|
-
{plans.map((plan) => {
|
|
17
|
-
const value = billing === "monthly" ? plan.monthly : plan.yearly;
|
|
18
|
-
const isHighlighted = plan.highlighted && !plan.customColor;
|
|
19
|
-
const bg = plan.customColor ? "rgb(var(--color-surface))" : isHighlighted ? "rgb(var(--color-primary-soft))" : undefined;
|
|
20
|
-
const fg = plan.customColor ? plan.customColor : isHighlighted ? "rgb(var(--color-primary))" : undefined;
|
|
21
|
-
return (
|
|
22
|
-
<div
|
|
23
|
-
key={plan.name}
|
|
24
|
-
className={cn(
|
|
25
|
-
"group flex flex-col gap-5 p-5 transition-colors duration-200 hover:bg-surface-strong sm:flex-row sm:items-center sm:justify-between",
|
|
26
|
-
isHighlighted && "bg-primary-soft/60",
|
|
27
|
-
)}
|
|
28
|
-
style={bg ? { backgroundColor: bg } : undefined}
|
|
29
|
-
>
|
|
30
|
-
<div className="min-w-0">
|
|
31
|
-
<div className="flex items-center gap-2.5">
|
|
32
|
-
<h3 className="font-display text-lg font-semibold tracking-tight text-foreground">
|
|
33
|
-
{plan.name}
|
|
34
|
-
</h3>
|
|
35
|
-
{isHighlighted ? (
|
|
36
|
-
<span className="rounded-full bg-primary px-2.5 py-0.5 text-[11px] font-semibold text-on-primary uppercase tracking-wider">
|
|
37
|
-
Popular
|
|
38
|
-
</span>
|
|
39
|
-
) : null}
|
|
40
|
-
</div>
|
|
41
|
-
{plan.description ? (
|
|
42
|
-
<p className="mt-1 truncate text-sm text-muted-foreground">{plan.description}</p>
|
|
43
|
-
) : null}
|
|
44
|
-
</div>
|
|
45
|
-
<div className="flex items-center gap-6">
|
|
46
|
-
<ul className="hidden items-center gap-4 lg:flex">
|
|
47
|
-
{plan.features.slice(0, 3).map((f) => (
|
|
48
|
-
<li key={f} className="flex items-center gap-1.5 text-sm text-muted-foreground" style={{ color: fg }}>
|
|
49
|
-
<Check className="h-3.5 w-3.5 text-current" />
|
|
50
|
-
{f}
|
|
51
|
-
</li>
|
|
52
|
-
))}
|
|
53
|
-
</ul>
|
|
54
|
-
<div className="font-display text-2xl font-semibold tracking-tight text-foreground">
|
|
55
|
-
{price(value)}
|
|
56
|
-
{value !== null ? (
|
|
57
|
-
<span className="text-sm font-normal text-muted-foreground">/{billing === "monthly" ? "mo" : "yr"}</span>
|
|
58
|
-
) : null}
|
|
59
|
-
</div>
|
|
60
|
-
<PlanButton plan={plan} billing={billing} onSelect={onSelect} />
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
})}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
};
|
|
69
|
-
PricingCompact.displayName = "PricingCompact";
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Prose, ProseLead } from "./Prose";
|
|
4
|
-
|
|
5
|
-
describe("Prose", () => {
|
|
6
|
-
it("renders an article element with prose classes", () => {
|
|
7
|
-
render(<Prose>Text</Prose>);
|
|
8
|
-
const article = screen.getByText("Text");
|
|
9
|
-
expect(article.tagName).toBe("ARTICLE");
|
|
10
|
-
expect(article).toHaveClass("prose");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("applies the requested size", () => {
|
|
14
|
-
render(<Prose size="xl">Text</Prose>);
|
|
15
|
-
expect(screen.getByText("Text")).toHaveClass("prose-xl");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("caps width by default and removes it when wide", () => {
|
|
19
|
-
render(<Prose>Capped</Prose>);
|
|
20
|
-
expect(screen.getByText("Capped")).toHaveClass("max-w-3xl");
|
|
21
|
-
|
|
22
|
-
render(<Prose wide>Wide</Prose>);
|
|
23
|
-
expect(screen.getByText("Wide")).not.toHaveClass("max-w-3xl");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("renders children", () => {
|
|
27
|
-
render(
|
|
28
|
-
<Prose>
|
|
29
|
-
<h1>Heading</h1>
|
|
30
|
-
</Prose>,
|
|
31
|
-
);
|
|
32
|
-
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("renders raw HTML via the html prop instead of children", () => {
|
|
36
|
-
render(<Prose html="<h2>CMS content</h2><p>A paragraph.</p>" />);
|
|
37
|
-
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("CMS content");
|
|
38
|
-
expect(screen.getByText("A paragraph.")).toBeInTheDocument();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("ProseLead", () => {
|
|
43
|
-
it("renders a lead-styled paragraph", () => {
|
|
44
|
-
render(<ProseLead>Intro</ProseLead>);
|
|
45
|
-
const p = screen.getByText("Intro");
|
|
46
|
-
expect(p.tagName).toBe("P");
|
|
47
|
-
expect(p).toHaveClass("lead");
|
|
48
|
-
});
|
|
49
|
-
});
|