@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
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
-
import { FAQ } from "./FAQ";
|
|
4
|
-
|
|
5
|
-
const items = [
|
|
6
|
-
{ question: "How do I install it?", answer: "Run pnpm add." },
|
|
7
|
-
{ question: "How do I theme it?", answer: "Set --color-primary." },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
describe("FAQ", () => {
|
|
11
|
-
it("renders all questions and the accordion option by default", () => {
|
|
12
|
-
render(<FAQ items={items} />);
|
|
13
|
-
expect(screen.getByText("How do I install it?")).toBeInTheDocument();
|
|
14
|
-
expect(screen.getByText("How do I theme it?")).toBeInTheDocument();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("expands an answer when a question is activated", () => {
|
|
18
|
-
render(<FAQ items={items} />);
|
|
19
|
-
const q = screen.getByRole("button", { name: "How do I install it?" });
|
|
20
|
-
expect(q).toHaveAttribute("aria-expanded", "false");
|
|
21
|
-
|
|
22
|
-
fireEvent.click(q);
|
|
23
|
-
expect(q).toHaveAttribute("aria-expanded", "true");
|
|
24
|
-
expect(screen.getByText("Run pnpm add.")).toBeInTheDocument();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("closes the first item when another is opened (single open by default)", () => {
|
|
28
|
-
render(<FAQ items={items} />);
|
|
29
|
-
fireEvent.click(screen.getByRole("button", { name: "How do I install it?" }));
|
|
30
|
-
fireEvent.click(screen.getByRole("button", { name: "How do I theme it?" }));
|
|
31
|
-
|
|
32
|
-
// Only the second question's answer is visible.
|
|
33
|
-
expect(screen.getByText("Set --color-primary.")).toBeInTheDocument();
|
|
34
|
-
expect(screen.queryByText("Run pnpm add.")).not.toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("keeps multiple items open when allowMultiple is set", () => {
|
|
38
|
-
render(<FAQ items={items} allowMultiple />);
|
|
39
|
-
fireEvent.click(screen.getByRole("button", { name: "How do I install it?" }));
|
|
40
|
-
fireEvent.click(screen.getByRole("button", { name: "How do I theme it?" }));
|
|
41
|
-
|
|
42
|
-
expect(screen.getByText("Run pnpm add.")).toBeInTheDocument();
|
|
43
|
-
expect(screen.getByText("Set --color-primary.")).toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("renders the split and cards options", () => {
|
|
47
|
-
const { rerender } = render(<FAQ items={items} option="split" />);
|
|
48
|
-
expect(screen.getByText("How do I install it?")).toBeInTheDocument();
|
|
49
|
-
|
|
50
|
-
rerender(<FAQ items={items} option="cards" />);
|
|
51
|
-
expect(screen.getByText("How do I install it?")).toBeInTheDocument();
|
|
52
|
-
expect(screen.getByText("Run pnpm add.")).toBeInTheDocument();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { FAQProps } from "./types";
|
|
2
|
-
import { FaqCards } from "./variants/Cards";
|
|
3
|
-
import { FaqAccordion } from "./variants/Accordion";
|
|
4
|
-
import { FaqSplit } from "./variants/Split";
|
|
5
|
-
|
|
6
|
-
export const FAQ = ({
|
|
7
|
-
className,
|
|
8
|
-
items,
|
|
9
|
-
option = "accordion",
|
|
10
|
-
allowMultiple = false,
|
|
11
|
-
defaultOpen = [],
|
|
12
|
-
eyebrow,
|
|
13
|
-
title,
|
|
14
|
-
description,
|
|
15
|
-
...props
|
|
16
|
-
}: FAQProps) => {
|
|
17
|
-
if (option === "cards") {
|
|
18
|
-
return <FaqCards className={className} items={items} eyebrow={eyebrow} {...props} />;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (option === "split") {
|
|
22
|
-
return (
|
|
23
|
-
<FaqSplit
|
|
24
|
-
className={className}
|
|
25
|
-
items={items}
|
|
26
|
-
allowMultiple={allowMultiple}
|
|
27
|
-
defaultOpen={defaultOpen}
|
|
28
|
-
eyebrow={eyebrow}
|
|
29
|
-
title={title}
|
|
30
|
-
description={description}
|
|
31
|
-
{...props}
|
|
32
|
-
/>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// accordion
|
|
37
|
-
return (
|
|
38
|
-
<div className="mx-auto w-full max-w-3xl">
|
|
39
|
-
<FaqAccordion
|
|
40
|
-
className={className}
|
|
41
|
-
items={items}
|
|
42
|
-
allowMultiple={allowMultiple}
|
|
43
|
-
defaultOpen={defaultOpen}
|
|
44
|
-
/>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
FAQ.displayName = "FAQ";
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { cn } from "../../utils/cn";
|
|
4
|
-
import { Plus } from "../../icons";
|
|
5
|
-
import type { FaqItem } from "./types";
|
|
6
|
-
|
|
7
|
-
export function useFaqState(allowMultiple: boolean, defaultOpen: number[]) {
|
|
8
|
-
const [openSet, setOpenSet] = useState<Set<number>>(
|
|
9
|
-
() => new Set(allowMultiple ? defaultOpen : defaultOpen.slice(0, 1)),
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
const toggle = (index: number) => {
|
|
13
|
-
setOpenSet((prev) => {
|
|
14
|
-
const next = new Set(prev);
|
|
15
|
-
if (next.has(index)) {
|
|
16
|
-
next.delete(index);
|
|
17
|
-
} else {
|
|
18
|
-
if (!allowMultiple) next.clear();
|
|
19
|
-
next.add(index);
|
|
20
|
-
}
|
|
21
|
-
return next;
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
return { openSet, toggle, isOpen: (i: number) => openSet.has(i) };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function FaqItemRow({
|
|
29
|
-
item,
|
|
30
|
-
open,
|
|
31
|
-
onToggle,
|
|
32
|
-
style,
|
|
33
|
-
}: {
|
|
34
|
-
item: FaqItem;
|
|
35
|
-
open: boolean;
|
|
36
|
-
onToggle: () => void;
|
|
37
|
-
style: "boxed" | "bare";
|
|
38
|
-
}) {
|
|
39
|
-
return (
|
|
40
|
-
<div>
|
|
41
|
-
<h3>
|
|
42
|
-
<button
|
|
43
|
-
type="button"
|
|
44
|
-
aria-expanded={open}
|
|
45
|
-
onClick={onToggle}
|
|
46
|
-
className={
|
|
47
|
-
style === "boxed"
|
|
48
|
-
? "flex w-full cursor-pointer items-center justify-between gap-4 px-6 py-5 text-left text-[15px] font-medium text-foreground transition-colors duration-150 hover:bg-surface-strong/60 focus-visible:outline-none"
|
|
49
|
-
: "flex w-full cursor-pointer items-center justify-between gap-4 py-5 text-left text-base font-semibold text-foreground transition-colors duration-150 focus-visible:outline-none"
|
|
50
|
-
}
|
|
51
|
-
>
|
|
52
|
-
{item.question}
|
|
53
|
-
<Plus
|
|
54
|
-
className={cn(
|
|
55
|
-
"h-4.5 w-4.5 shrink-0 text-foreground/45 transition-transform duration-200 sm:h-5 sm:w-5",
|
|
56
|
-
open && "rotate-45 text-primary",
|
|
57
|
-
)}
|
|
58
|
-
/>
|
|
59
|
-
</button>
|
|
60
|
-
</h3>
|
|
61
|
-
{open ? (
|
|
62
|
-
<p className={style === "boxed" ? "px-6 pb-6 text-sm leading-7 text-muted-foreground" : "pb-6 text-sm leading-7 text-muted-foreground"}>
|
|
63
|
-
{item.answer}
|
|
64
|
-
</p>
|
|
65
|
-
) : null}
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function FaqEyebrow({ label }: { label: string }) {
|
|
71
|
-
return (
|
|
72
|
-
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{label}</p>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
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";
|
|
@@ -1,40 +0,0 @@
|
|
|
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";
|
|
@@ -1,44 +0,0 @@
|
|
|
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";
|
|
@@ -1,44 +0,0 @@
|
|
|
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";
|
|
@@ -1,14 +0,0 @@
|
|
|
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";
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { FeatureCard } from "./FeatureCard";
|
|
4
|
-
import { FeatureGrid } from "./FeatureGrid";
|
|
5
|
-
|
|
6
|
-
const features = [
|
|
7
|
-
{ icon: <span data-testid="icon-1">⚡</span>, title: "Fast", description: "Go live fast." },
|
|
8
|
-
{ title: "No description title" },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
describe("FeatureCard", () => {
|
|
12
|
-
it("renders title, description and icon", () => {
|
|
13
|
-
render(<FeatureCard icon={<span data-testid="ic" />} title="Fast" description="Go live fast." />);
|
|
14
|
-
expect(screen.getByText("Fast")).toBeInTheDocument();
|
|
15
|
-
expect(screen.getByText("Go live fast.")).toBeInTheDocument();
|
|
16
|
-
expect(screen.getByTestId("ic")).toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("applies a large heading when large", () => {
|
|
20
|
-
render(<FeatureCard title="Big" large />);
|
|
21
|
-
expect(screen.getByText("Big")).toHaveClass("text-2xl");
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("FeatureGrid", () => {
|
|
26
|
-
it("renders all features in columns mode", () => {
|
|
27
|
-
render(<FeatureGrid features={features} option="columns" />);
|
|
28
|
-
expect(screen.getByText("Fast")).toBeInTheDocument();
|
|
29
|
-
expect(screen.getByTestId("icon-1")).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("renders features in bento mode", () => {
|
|
33
|
-
render(<FeatureGrid features={features} option="bento" />);
|
|
34
|
-
expect(screen.getByText("Fast")).toBeInTheDocument();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("renders features as editorial rows with numbering", () => {
|
|
38
|
-
render(<FeatureGrid features={features} option="editorialRows" />);
|
|
39
|
-
expect(screen.getByText("Fast")).toBeInTheDocument();
|
|
40
|
-
// Editorial rows number features starting at 01.
|
|
41
|
-
expect(screen.getByText("01")).toBeInTheDocument();
|
|
42
|
-
expect(screen.getByText("02")).toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("applies the requested column count in columns mode", () => {
|
|
46
|
-
const { container } = render(<FeatureGrid features={features} option="columns" columns={2} />);
|
|
47
|
-
expect(container.querySelector("div.sm\\:grid-cols-2")).not.toBeNull();
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
];
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
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";
|
|
@@ -1,21 +0,0 @@
|
|
|
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";
|
|
@@ -1,38 +0,0 @@
|
|
|
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";
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Footer } from "./Footer";
|
|
4
|
-
|
|
5
|
-
const columns = [
|
|
6
|
-
{
|
|
7
|
-
title: "Product",
|
|
8
|
-
links: [
|
|
9
|
-
{ label: "Features", href: "#features" },
|
|
10
|
-
{ label: "Pricing", href: "#pricing" },
|
|
11
|
-
],
|
|
12
|
-
},
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
describe("Footer", () => {
|
|
16
|
-
it("renders the brand, column headings and links (classic by default)", () => {
|
|
17
|
-
render(<Footer brand="Acurio" columns={columns} />);
|
|
18
|
-
expect(screen.getByText("Acurio")).toBeInTheDocument();
|
|
19
|
-
expect(screen.getByText("Product")).toBeInTheDocument();
|
|
20
|
-
expect(screen.getByRole("link", { name: "Features" })).toHaveAttribute(
|
|
21
|
-
"href",
|
|
22
|
-
"#features",
|
|
23
|
-
);
|
|
24
|
-
expect(screen.getByRole("link", { name: "Pricing" })).toBeInTheDocument();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("renders a description and the bottom copy", () => {
|
|
28
|
-
render(<Footer brand="Acurio" columns={columns} description="A kit." bottom="Made with care" />);
|
|
29
|
-
expect(screen.getByText("A kit.")).toBeInTheDocument();
|
|
30
|
-
expect(screen.getByText("Made with care")).toBeInTheDocument();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("renders the minimal option", () => {
|
|
34
|
-
render(<Footer brand="Acurio" columns={columns} option="minimal" />);
|
|
35
|
-
expect(screen.getByRole("link", { name: "Features" })).toBeInTheDocument();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renders the editorial option with a large wordmark", () => {
|
|
39
|
-
const { container } = render(<Footer brand="Acurio" columns={columns} option="editorial" />);
|
|
40
|
-
expect(screen.getByText("Acurio")).toBeInTheDocument();
|
|
41
|
-
// Editorial uses a large display wordmark.
|
|
42
|
-
expect(container.querySelector("span.text-5xl")).toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("renders a footer element tag", () => {
|
|
46
|
-
const { container } = render(<Footer brand="Acurio" columns={columns} />);
|
|
47
|
-
expect(container.querySelector("footer")).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
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";
|