@ugurdemirel/landcraft 0.1.4 → 0.1.5

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 CHANGED
@@ -1679,6 +1679,9 @@
1679
1679
  .aspect-\[16\/10\] {
1680
1680
  aspect-ratio: 16/10;
1681
1681
  }
1682
+ .h-1 {
1683
+ height: var(--spacing);
1684
+ }
1682
1685
  .h-1\.5 {
1683
1686
  height: calc(var(--spacing) * 1.5);
1684
1687
  }
@@ -2268,6 +2271,12 @@
2268
2271
  .p-1 {
2269
2272
  padding: var(--spacing);
2270
2273
  }
2274
+ .p-2 {
2275
+ padding: calc(var(--spacing) * 2);
2276
+ }
2277
+ .p-4 {
2278
+ padding: calc(var(--spacing) * 4);
2279
+ }
2271
2280
  .p-5 {
2272
2281
  padding: calc(var(--spacing) * 5);
2273
2282
  }
@@ -2736,6 +2745,9 @@
2736
2745
  .text-white {
2737
2746
  color: var(--color-white);
2738
2747
  }
2748
+ .lowercase {
2749
+ text-transform: lowercase;
2750
+ }
2739
2751
  .uppercase {
2740
2752
  text-transform: uppercase;
2741
2753
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ugurdemirel/landcraft",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,14 +35,16 @@
35
35
  "build:js": "vite build",
36
36
  "build:css": "tailwindcss -i ./src/styles/index.css -o ./dist/styles.css",
37
37
  "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
38
40
  "clean": "rimraf dist",
39
41
  "prepublishOnly": "pnpm run build"
40
42
  },
41
43
  "peerDependencies": {
44
+ "@tailwindcss/typography": "^0.5.15",
42
45
  "react": ">=18.0.0",
43
46
  "react-dom": ">=18.0.0",
44
- "tailwindcss": "^4.0.0",
45
- "@tailwindcss/typography": "^0.5.15"
47
+ "tailwindcss": "^4.0.0"
46
48
  },
47
49
  "peerDependenciesMeta": {
48
50
  "@tailwindcss/typography": {
@@ -56,14 +58,19 @@
56
58
  "devDependencies": {
57
59
  "@tailwindcss/cli": "^4.0.0",
58
60
  "@tailwindcss/typography": "^0.5.15",
61
+ "@testing-library/jest-dom": "^7.0.1",
62
+ "@testing-library/react": "^16.3.2",
63
+ "@testing-library/user-event": "^14.6.5",
59
64
  "@types/node": "^22.9.0",
60
65
  "@types/react": "^18.3.12",
61
66
  "@types/react-dom": "^18.3.1",
62
67
  "@vitejs/plugin-react": "^4.3.4",
68
+ "jsdom": "^30.0.1",
63
69
  "rimraf": "^6.0.1",
64
70
  "tailwindcss": "^4.0.0",
65
71
  "typescript": "^5.6.3",
66
72
  "vite": "^5.4.11",
67
- "vite-plugin-dts": "^4.3.0"
73
+ "vite-plugin-dts": "^4.3.0",
74
+ "vitest": "^3.2.7"
68
75
  }
69
76
  }
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Badge } from "./Badge";
4
+ import { Zap } from "../icons";
5
+
6
+ describe("Badge", () => {
7
+ it("renders a span with children", () => {
8
+ render(<Badge>New</Badge>);
9
+ expect(screen.getByText("New")).toBeInTheDocument();
10
+ expect(screen.getByText("New").tagName).toBe("SPAN");
11
+ });
12
+
13
+ it("defaults to the soft variant", () => {
14
+ render(<Badge>Soft</Badge>);
15
+ expect(screen.getByText("Soft")).toHaveClass("bg-primary-soft");
16
+ });
17
+
18
+ it("applies the requested variant", () => {
19
+ render(<Badge variant="solid">Solid</Badge>);
20
+ expect(screen.getByText("Solid")).toHaveClass("bg-primary");
21
+ });
22
+
23
+ it("renders a dot indicator for the dot variant", () => {
24
+ const { container } = render(<Badge variant="dot">Online</Badge>);
25
+ // The dot is an inner decorative span.
26
+ const dots = container.querySelectorAll("span.h-1\\.5");
27
+ expect(dots.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it("renders an icon", () => {
31
+ render(
32
+ <Badge icon={<Zap data-testid="icon" />} variant="soft">
33
+ Fast
34
+ </Badge>,
35
+ );
36
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
37
+ });
38
+
39
+ it("applies sizes", () => {
40
+ render(<Badge size="sm">Small</Badge>);
41
+ expect(screen.getByText("Small")).toHaveClass("px-2.5");
42
+ });
43
+
44
+ it("sets a computed text color for customColor", () => {
45
+ render(<Badge customColor="#111111">Dark</Badge>);
46
+ const badge = screen.getByText("Dark");
47
+ expect(badge).toHaveStyle({ backgroundColor: "#111111" });
48
+ expect(badge).toHaveStyle({ color: "#ffffff" });
49
+ });
50
+
51
+ it("forwards arbitrary props like aria-label", () => {
52
+ render(<Badge aria-label="status">v1</Badge>);
53
+ expect(screen.getByLabelText("status")).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { BlogCard } from "./BlogCard";
4
+ import { BlogSection } from "./BlogSection";
5
+
6
+ const posts = [
7
+ {
8
+ title: "Token-based theming",
9
+ excerpt: "A short excerpt.",
10
+ date: "12 Aug 2026",
11
+ readTime: "5 min",
12
+ category: "Strategy",
13
+ href: "/post",
14
+ author: { name: "Aylin Demir" },
15
+ },
16
+ {
17
+ title: "Second post",
18
+ date: "1 Aug 2026",
19
+ },
20
+ ];
21
+
22
+ describe("BlogCard", () => {
23
+ it("renders a card with title, category badge and meta (card option)", () => {
24
+ render(<BlogCard post={posts[0]} />);
25
+ expect(screen.getByText("Token-based theming")).toBeInTheDocument();
26
+ expect(screen.getByText("Strategy")).toBeInTheDocument();
27
+ expect(screen.getByText("5 min")).toBeInTheDocument();
28
+ });
29
+
30
+ it("renders the row option", () => {
31
+ render(<BlogCard post={posts[0]} option="row" />);
32
+ expect(screen.getByText("Token-based theming")).toBeInTheDocument();
33
+ });
34
+
35
+ it("links to the post href", () => {
36
+ render(<BlogCard post={posts[0]} />);
37
+ expect(
38
+ screen.getByRole("link", { name: /Token-based theming/ }),
39
+ ).toHaveAttribute("href", "/post");
40
+ });
41
+ });
42
+
43
+ describe("BlogSection", () => {
44
+ it("renders the heading and posts as a card grid", () => {
45
+ render(
46
+ <BlogSection
47
+ posts={posts}
48
+ eyebrow="Blog"
49
+ title="Latest"
50
+ description="A description."
51
+ />,
52
+ );
53
+ expect(screen.getByText("Blog")).toBeInTheDocument();
54
+ expect(screen.getByText("Latest")).toBeInTheDocument();
55
+ expect(screen.getByText("Token-based theming")).toBeInTheDocument();
56
+ expect(screen.getByText("Second post")).toBeInTheDocument();
57
+ });
58
+
59
+ it("limits the number of posts with the limit prop", () => {
60
+ render(<BlogSection posts={posts} limit={1} />);
61
+ expect(screen.getByText("Token-based theming")).toBeInTheDocument();
62
+ expect(screen.queryByText("Second post")).not.toBeInTheDocument();
63
+ });
64
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Button } from "./Button";
5
+ import { ArrowRight } from "../icons";
6
+
7
+ describe("Button", () => {
8
+ it("renders a button element with children", () => {
9
+ render(<Button>Hello</Button>);
10
+ const btn = screen.getByRole("button", { name: "Hello" });
11
+ expect(btn).toBeInTheDocument();
12
+ expect(btn.tagName).toBe("BUTTON");
13
+ });
14
+
15
+ it("defaults to type=button so it never submits forms unintentionally", () => {
16
+ render(<Button>Save</Button>);
17
+ expect(screen.getByRole("button", { name: "Save" })).toHaveAttribute("type", "button");
18
+ });
19
+
20
+ it("applies the default primary variant", () => {
21
+ render(<Button>Start</Button>);
22
+ expect(screen.getByRole("button", { name: "Start" })).toHaveClass("bg-primary");
23
+ });
24
+
25
+ it("applies the requested variant class", () => {
26
+ render(<Button variant="ghost">Ghost</Button>);
27
+ expect(screen.getByRole("button", { name: "Ghost" })).toHaveClass("hover:bg-surface");
28
+ });
29
+
30
+ it("applies the link variant underline behavior", () => {
31
+ render(<Button variant="link">Link</Button>);
32
+ expect(screen.getByRole("button", { name: "Link" })).toHaveClass("hover:underline");
33
+ });
34
+
35
+ it("renders leading and trailing icons", () => {
36
+ render(
37
+ <Button iconLeft={<span data-testid="left" />} iconRight={<ArrowRight data-testid="right" />}>
38
+ Next
39
+ </Button>,
40
+ );
41
+ const btn = screen.getByRole("button", { name: "Next" });
42
+ expect(btn.querySelector('[data-testid="left"]')).toBeInTheDocument();
43
+ expect(btn.querySelector('[data-testid="right"]')).toBeInTheDocument();
44
+ });
45
+
46
+ it("applies fullWidth", () => {
47
+ render(<Button fullWidth>Wide</Button>);
48
+ expect(screen.getByRole("button", { name: "Wide" })).toHaveClass("w-full");
49
+ });
50
+
51
+ it("disables and blocks clicks when disabled", async () => {
52
+ const onClick = vi.fn();
53
+ render(
54
+ <Button disabled onClick={onClick}>
55
+ Disabled
56
+ </Button>,
57
+ );
58
+ const btn = screen.getByRole("button", { name: "Disabled" });
59
+ expect(btn).toBeDisabled();
60
+ await userEvent.click(btn);
61
+ expect(onClick).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it("sets a computed text color and background for customColor", () => {
65
+ render(<Button customColor="#111111">Custom</Button>);
66
+ const btn = screen.getByRole("button", { name: "Custom" });
67
+ expect(btn).toHaveStyle({ backgroundColor: "#111111" });
68
+ // Text is the light candidate for a very dark background.
69
+ expect(btn).toHaveStyle({ color: "#ffffff" });
70
+ });
71
+
72
+ it("forwards an onClick handler and fires it", async () => {
73
+ const onClick = vi.fn();
74
+ render(<Button onClick={onClick}>Click</Button>);
75
+ await userEvent.click(screen.getByRole("button", { name: "Click" }));
76
+ expect(onClick).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ it("applies button styles onto a single child when asChild is set", () => {
80
+ render(
81
+ <Button asChild>
82
+ <a href="#pricing">Pricing link</a>
83
+ </Button>,
84
+ );
85
+ const link = screen.getByRole("link", { name: "Pricing link" });
86
+ expect(link.tagName).toBe("A");
87
+ expect(link).toHaveClass("bg-primary");
88
+ // No wrapping <button> is rendered.
89
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
90
+ });
91
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { CTA } from "./CTA";
4
+
5
+ const base = { title: "Ready to get started?" };
6
+
7
+ describe("CTA", () => {
8
+ it("renders the title and description", () => {
9
+ render(<CTA {...base} description="Free for 14 days." />);
10
+ expect(screen.getByText("Ready to get started?")).toBeInTheDocument();
11
+ expect(screen.getByText("Free for 14 days.")).toBeInTheDocument();
12
+ });
13
+
14
+ it("renders primary and secondary actions", () => {
15
+ render(
16
+ <CTA
17
+ {...base}
18
+ action={<a href="#a">Action</a>}
19
+ secondaryAction={<a href="#b">Secondary</a>}
20
+ />,
21
+ );
22
+ expect(screen.getByRole("link", { name: "Action" })).toBeInTheDocument();
23
+ expect(screen.getByRole("link", { name: "Secondary" })).toBeInTheDocument();
24
+ });
25
+
26
+ it("renders the panel option (default) with a radial brand background", () => {
27
+ const { container } = render(<CTA {...base} option="panel" />);
28
+ const panel = container.querySelector("div[class*='rounded-']");
29
+ expect(panel).toBeInTheDocument();
30
+ });
31
+
32
+ it("renders the surface option as a paper band", () => {
33
+ const { container } = render(<CTA {...base} option="surface" />);
34
+ expect(container.querySelector("section")).toHaveClass("w-full");
35
+ });
36
+
37
+ it("renders the inverse option on a secondary surface", () => {
38
+ const { container } = render(<CTA {...base} option="inverse" />);
39
+ // The inverse option paints its band with the --color-secondary token.
40
+ const ink = container.querySelector("[style*='--color-secondary']");
41
+ expect(ink).toBeInTheDocument();
42
+ });
43
+
44
+ it("applies centered alignment", () => {
45
+ render(<CTA {...base} option="surface" align="center" />);
46
+ expect(screen.getByText("Ready to get started?")).toBeInTheDocument();
47
+ });
48
+
49
+ it("forwards an id", () => {
50
+ const { container } = render(<CTA {...base} id="cta" />);
51
+ expect(container.querySelector("section#cta")).not.toBeNull();
52
+ });
53
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ CardFooter,
10
+ } from "./Card";
11
+
12
+ describe("Card & subcomponents", () => {
13
+ it("renders an outlined card by default", () => {
14
+ render(<Card>Content</Card>);
15
+ expect(screen.getByText("Content")).toHaveClass("border-border");
16
+ });
17
+
18
+ it("applies a custom variant", () => {
19
+ render(<Card variant="elevated">Elevated</Card>);
20
+ expect(screen.getByText("Elevated")).toHaveClass("shadow-soft");
21
+ });
22
+
23
+ it("adds interactive styling when interactive", () => {
24
+ render(<Card interactive>Interactive</Card>);
25
+ expect(screen.getByText("Interactive")).toHaveClass("hover:-translate-y-0.5");
26
+ });
27
+
28
+ it("renders the full composite header/content/footer", () => {
29
+ render(
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle>Title</CardTitle>
33
+ <CardDescription>Description</CardDescription>
34
+ </CardHeader>
35
+ <CardContent>Body</CardContent>
36
+ <CardFooter>Footer</CardFooter>
37
+ </Card>,
38
+ );
39
+ expect(screen.getByText("Title")).toBeInTheDocument();
40
+ expect(screen.getByText("Description")).toBeInTheDocument();
41
+ expect(screen.getByText("Body")).toBeInTheDocument();
42
+ expect(screen.getByText("Footer")).toBeInTheDocument();
43
+ });
44
+
45
+ it("renders an icon in the header when provided", () => {
46
+ render(<CardHeader icon={<span data-testid="hdr-icon" />}>Header</CardHeader>);
47
+ expect(screen.getByTestId("hdr-icon")).toBeInTheDocument();
48
+ });
49
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Container, Stack } from "./Container";
4
+
5
+ describe("Container", () => {
6
+ it("renders a div by default", () => {
7
+ render(<Container>Body</Container>);
8
+ const el = screen.getByText("Body");
9
+ expect(el.tagName).toBe("DIV");
10
+ expect(el).toHaveClass("mx-auto");
11
+ });
12
+
13
+ it("applies a size class", () => {
14
+ render(<Container size="sm">Body</Container>);
15
+ expect(screen.getByText("Body")).toHaveClass("max-w-xl");
16
+ });
17
+
18
+ it("does not cap width for the full size", () => {
19
+ render(<Container size="full">Body</Container>);
20
+ expect(screen.getByText("Body")).not.toHaveClass("max-w-xl", "max-w-6xl");
21
+ });
22
+
23
+ it("adds gutters by default and removes them with gutters=false", () => {
24
+ render(<Container>A</Container>);
25
+ expect(screen.getByText("A")).toHaveClass("px-5");
26
+
27
+ render(<Container gutters={false}>B</Container>);
28
+ expect(screen.getByText("B")).not.toHaveClass("px-5");
29
+ });
30
+
31
+ it("renders as the requested element via `as`", () => {
32
+ render(<Container as="section">Body</Container>);
33
+ expect(screen.getByText("Body").tagName).toBe("SECTION");
34
+ });
35
+ });
36
+
37
+ describe("Stack", () => {
38
+ it("renders a vertical flex by default", () => {
39
+ render(<Stack>A</Stack>);
40
+ expect(screen.getByText("A")).toHaveClass("flex", "flex-col");
41
+ });
42
+
43
+ it("renders horizontal when horizontal", () => {
44
+ render(<Stack horizontal>A</Stack>);
45
+ expect(screen.getByText("A")).toHaveClass("flex-row");
46
+ });
47
+
48
+ it("applies a gap", () => {
49
+ render(<Stack gap={6}>A</Stack>);
50
+ expect(screen.getByText("A")).toHaveClass("gap-6");
51
+ });
52
+ });
@@ -0,0 +1,54 @@
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
+ });
@@ -0,0 +1,49 @@
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
+ });
@@ -0,0 +1,49 @@
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
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Hero } from "./Hero";
4
+
5
+ const base = {
6
+ title: "Build something great",
7
+ description: "A short description.",
8
+ eyebrow: "New release",
9
+ };
10
+
11
+ describe("Hero", () => {
12
+ it("renders the headline and description", () => {
13
+ render(<Hero {...base} />);
14
+ expect(
15
+ screen.getByRole("heading", { name: "Build something great" }),
16
+ ).toBeInTheDocument();
17
+ expect(screen.getByText("A short description.")).toBeInTheDocument();
18
+ });
19
+
20
+ it("renders an eyebrow, primary and secondary actions", () => {
21
+ render(
22
+ <Hero
23
+ {...base}
24
+ primaryAction={<a href="#a">Primary</a>}
25
+ secondaryAction={<a href="#b">Secondary</a>}
26
+ />,
27
+ );
28
+ expect(screen.getByText("New release")).toBeInTheDocument();
29
+ expect(screen.getByRole("link", { name: "Primary" })).toBeInTheDocument();
30
+ expect(screen.getByRole("link", { name: "Secondary" })).toBeInTheDocument();
31
+ });
32
+
33
+ it("renders the split variant by default with meta row and media", () => {
34
+ render(
35
+ <Hero
36
+ {...base}
37
+ media={<div data-testid="media" />}
38
+ meta={[{ label: "4.9/5", icon: <span>★</span> }]}
39
+ />,
40
+ );
41
+ expect(screen.getByTestId("media")).toBeInTheDocument();
42
+ expect(screen.getByText("4.9/5")).toBeInTheDocument();
43
+ const section = screen.getByTestId("media").closest("section");
44
+ expect(section).toHaveClass("bg-background");
45
+ });
46
+
47
+ it("renders the centered variant with centered copy", () => {
48
+ render(<Hero {...base} variant="centered" media={<div data-testid="m" />} />);
49
+ const h1 = screen.getByRole("heading", { name: "Build something great" });
50
+ expect(h1).toBeInTheDocument();
51
+ expect(screen.getByTestId("m")).toBeInTheDocument();
52
+ });
53
+
54
+ it("renders the statement variant as an ink band with a grain overlay", () => {
55
+ const { container } = render(<Hero {...base} variant="statement" />);
56
+ const section = container.querySelector("section");
57
+ expect(section).toHaveClass("bg-secondary");
58
+ // Statement paints a grain overlay div.
59
+ expect(container.querySelector(".grain")).toBeInTheDocument();
60
+ });
61
+
62
+ it("omits the meta row when no meta is supplied", () => {
63
+ render(<Hero {...base} />);
64
+ expect(screen.queryByRole("list")).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("forwards an id to the section", () => {
68
+ const { container } = render(<Hero {...base} id="home" />);
69
+ expect(container.querySelector("section#home")).not.toBeNull();
70
+ });
71
+ });