From 38f00dc7560ceed331ef5caa8839fc293a258599 Mon Sep 17 00:00:00 2001 From: Pedro Ramalho Date: Wed, 4 Mar 2026 01:03:15 -0300 Subject: [PATCH 1/4] Add UI components and styling utilities Introduce a set of reusable UI primitives and utilities: Button (with variants), Card (with variants), Container (with variants), SectionTitle (with variants), Navbar, Footer, FileExplorer/FileItem (with variants), ScrollIndicator (animated with framer-motion), and a central ui index export. Add a cn utility that composes clsx + tailwind-merge for safe Tailwind class merging. Update package.json to include class-variance-authority, clsx, framer-motion and tailwind-merge required by the new components. --- lp-code/package-lock.json | 84 +++++++++++++++++++ lp-code/package.json | 4 + lp-code/src/components/ui/button/Button.tsx | 23 +++++ .../components/ui/button/button.variants.ts | 23 +++++ lp-code/src/components/ui/card/Card.tsx | 40 +++++++++ .../src/components/ui/card/card.variants.ts | 19 +++++ .../src/components/ui/container/Container.tsx | 16 ++++ .../ui/container/container.variants.ts | 5 ++ .../ui/file-explorer/FileExplorer.tsx | 18 ++++ .../components/ui/file-explorer/FileItem.tsx | 22 +++++ .../ui/file-explorer/fileItem.variants.ts | 16 ++++ lp-code/src/components/ui/footer/Footer.tsx | 41 +++++++++ lp-code/src/components/ui/index.ts | 7 ++ lp-code/src/components/ui/navbar/NavBar.tsx | 25 ++++++ .../ui/scroll-indicator/ScrollIndicator.tsx | 55 ++++++++++++ .../scrollIndicator.variants.ts | 28 +++++++ .../ui/section-title/SectionTitle.tsx | 21 +++++ .../ui/section-title/sectionTitle.variants.ts | 3 + lp-code/src/lib/utils/cn.ts | 6 ++ 19 files changed, 456 insertions(+) create mode 100644 lp-code/src/components/ui/button/Button.tsx create mode 100644 lp-code/src/components/ui/button/button.variants.ts create mode 100644 lp-code/src/components/ui/card/Card.tsx create mode 100644 lp-code/src/components/ui/card/card.variants.ts create mode 100644 lp-code/src/components/ui/container/Container.tsx create mode 100644 lp-code/src/components/ui/container/container.variants.ts create mode 100644 lp-code/src/components/ui/file-explorer/FileExplorer.tsx create mode 100644 lp-code/src/components/ui/file-explorer/FileItem.tsx create mode 100644 lp-code/src/components/ui/file-explorer/fileItem.variants.ts create mode 100644 lp-code/src/components/ui/footer/Footer.tsx create mode 100644 lp-code/src/components/ui/index.ts create mode 100644 lp-code/src/components/ui/navbar/NavBar.tsx create mode 100644 lp-code/src/components/ui/scroll-indicator/ScrollIndicator.tsx create mode 100644 lp-code/src/components/ui/scroll-indicator/scrollIndicator.variants.ts create mode 100644 lp-code/src/components/ui/section-title/SectionTitle.tsx create mode 100644 lp-code/src/components/ui/section-title/sectionTitle.variants.ts create mode 100644 lp-code/src/lib/utils/cn.ts diff --git a/lp-code/package-lock.json b/lp-code/package-lock.json index c4981bb..c5fb6b7 100644 --- a/lp-code/package-lock.json +++ b/lp-code/package-lock.json @@ -10,8 +10,12 @@ "dependencies": { "@gsap/react": "^2.1.2", "@tailwindcss/vite": "^4.1.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.34.5", "react": "^19.2.0", "react-dom": "^19.2.0", + "tailwind-merge": "^3.5.0", "tailwindcss": "^4.1.18" }, "devDependencies": { @@ -2137,6 +2141,27 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2585,6 +2610,33 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.34.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.5.tgz", + "integrity": "sha512-Z2dQ+o7BsfpJI3+u0SQUNCrN+ajCKJen1blC4rCHx1Ta2EOHs+xKJegLT2aaD9iSMbU3OoX+WabQXkloUbZmJQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.5", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3146,6 +3198,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.34.5", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.5.tgz", + "integrity": "sha512-k33CsnxO2K3gBRMUZT+vPmc4Utlb5menKdG0RyVNLtlqRaaJPRWlE9fXl8NTtfZ5z3G8TDvqSu0MENLqSTaHZA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3350,6 +3417,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3495,6 +3563,16 @@ "node": ">=8" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -3543,6 +3621,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/lp-code/package.json b/lp-code/package.json index 6230f31..2a1ddd2 100644 --- a/lp-code/package.json +++ b/lp-code/package.json @@ -12,8 +12,12 @@ "dependencies": { "@gsap/react": "^2.1.2", "@tailwindcss/vite": "^4.1.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.34.5", "react": "^19.2.0", "react-dom": "^19.2.0", + "tailwind-merge": "^3.5.0", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/lp-code/src/components/ui/button/Button.tsx b/lp-code/src/components/ui/button/Button.tsx new file mode 100644 index 0000000..b97c3ff --- /dev/null +++ b/lp-code/src/components/ui/button/Button.tsx @@ -0,0 +1,23 @@ +import { forwardRef } from "react" +import type { ButtonHTMLAttributes } from "react" +import type { VariantProps } from "class-variance-authority" +import { buttonVariants } from "./button.variants" +import { cn } from "../../../lib/utils/cn" + +type ButtonProps = + ButtonHTMLAttributes & + VariantProps + +export const Button = forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( + ) + + const btn = screen.getByRole("button") + + expect(btn).toBeInTheDocument() + expect(btn).toHaveTextContent("Click") + }) + + /** + * Testa se a variante primária é aplicada por padrão + */ + it("applies primary variant by default", () => { + render() + + const btn = screen.getByRole("button") + + expect(btn.className).toContain("bg-blue-600") + }) + + /** + * Testa se a variante secundária é aplicada quando especificada + */ + it("applies secondary variant", () => { + render() + + const btn = screen.getByRole("button") + + expect(btn.className).toContain("bg-gray-200") + }) + + /** + * Testa se o evento onClick é disparado corretamente + */ + it("fires onClick", async () => { + const user = userEvent.setup() + const handleClick = vi.fn() + + render() + + const btn = screen.getByRole("button") + + await user.click(btn) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) + +}) \ No newline at end of file From 56005ca55f69d9297e47611dd087e2604b209f84 Mon Sep 17 00:00:00 2001 From: Pedro Ramalho Date: Tue, 10 Mar 2026 13:35:36 -0300 Subject: [PATCH 4/4] Add unit tests for UI components Add Vitest + React Testing Library unit tests for several UI components: Card, Container, FileExplorer, FileItem, Footer, NavBar, ScrollIndicator, and SectionTitle. Each test file checks basic rendering and common behaviors (children/prop rendering, role queries, presence of expected text, and container class application) to improve coverage and catch regressions. --- lp-code/src/components/ui/card/Card.test.tsx | 31 +++++++++++++++++ .../ui/container/Container.test.tsx | 33 +++++++++++++++++++ .../ui/file-explorer/FileExplorer.test.tsx | 19 +++++++++++ .../ui/file-explorer/FileItem.test.tsx | 16 +++++++++ .../src/components/ui/footer/Footer.test.tsx | 18 ++++++++++ .../src/components/ui/navbar/NavBar.test.tsx | 18 ++++++++++ .../scroll-indicator/ScrollIndicator.test.tsx | 16 +++++++++ .../ui/section-title/SectionTitle.test.tsx | 30 +++++++++++++++++ 8 files changed, 181 insertions(+) create mode 100644 lp-code/src/components/ui/card/Card.test.tsx create mode 100644 lp-code/src/components/ui/container/Container.test.tsx create mode 100644 lp-code/src/components/ui/file-explorer/FileExplorer.test.tsx create mode 100644 lp-code/src/components/ui/file-explorer/FileItem.test.tsx create mode 100644 lp-code/src/components/ui/footer/Footer.test.tsx create mode 100644 lp-code/src/components/ui/navbar/NavBar.test.tsx create mode 100644 lp-code/src/components/ui/scroll-indicator/ScrollIndicator.test.tsx create mode 100644 lp-code/src/components/ui/section-title/SectionTitle.test.tsx diff --git a/lp-code/src/components/ui/card/Card.test.tsx b/lp-code/src/components/ui/card/Card.test.tsx new file mode 100644 index 0000000..beac794 --- /dev/null +++ b/lp-code/src/components/ui/card/Card.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { Card } from "./Card" + +describe("Card component", () => { + + /** + * Verifica se o Card renderiza corretamente + */ + it("renders card component", () => { + render(Content) + + const card = screen.getByText("Content") + + expect(card).toBeInTheDocument() + }) + + /** + * Verifica se children são renderizados + */ + it("renders children correctly", () => { + render( + +

Card text

+
+ ) + + expect(screen.getByText("Card text")).toBeInTheDocument() + }) + +}) \ No newline at end of file diff --git a/lp-code/src/components/ui/container/Container.test.tsx b/lp-code/src/components/ui/container/Container.test.tsx new file mode 100644 index 0000000..366da85 --- /dev/null +++ b/lp-code/src/components/ui/container/Container.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { Container } from "./Container" + +describe("Container component", () => { + + /** + * Verifica se o container renderiza corretamente + */ + it("renders container", () => { + render( + +
Content
+
+ ) + + expect(screen.getByText("Content")).toBeInTheDocument() + }) + + /** + * Verifica se aplica classes de layout + */ + it("applies container classes", () => { + const { container } = render( + +
Test
+
+ ) + + expect(container.firstChild).toHaveClass("mx-auto") + }) + +}) \ No newline at end of file diff --git a/lp-code/src/components/ui/file-explorer/FileExplorer.test.tsx b/lp-code/src/components/ui/file-explorer/FileExplorer.test.tsx new file mode 100644 index 0000000..62d7375 --- /dev/null +++ b/lp-code/src/components/ui/file-explorer/FileExplorer.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { FileExplorer } from "./FileExplorer" + +describe("FileExplorer component", () => { + + /** + * Verifica se os arquivos são renderizados + */ + it("renders file explorer items", () => { + render() + + expect(screen.getByText("src")).toBeInTheDocument() + expect(screen.getByText("components")).toBeInTheDocument() + expect(screen.getByText("App.tsx")).toBeInTheDocument() + expect(screen.getByText("package.json")).toBeInTheDocument() + }) + +}) \ No newline at end of file diff --git a/lp-code/src/components/ui/file-explorer/FileItem.test.tsx b/lp-code/src/components/ui/file-explorer/FileItem.test.tsx new file mode 100644 index 0000000..3836ce8 --- /dev/null +++ b/lp-code/src/components/ui/file-explorer/FileItem.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { FileItem } from "./FileItem" + +describe("FileItem component", () => { + + /** + * Testa se o item de arquivo renderiza o nome + */ + it("renders file name", () => { + render() + + expect(screen.getByText("index.tsx")).toBeInTheDocument() + }) + +}) \ No newline at end of file diff --git a/lp-code/src/components/ui/footer/Footer.test.tsx b/lp-code/src/components/ui/footer/Footer.test.tsx new file mode 100644 index 0000000..4d4331b --- /dev/null +++ b/lp-code/src/components/ui/footer/Footer.test.tsx @@ -0,0 +1,18 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { Footer } from "./Footer" + +describe("Footer component", () => { + + /** + * Verifica se o footer renderiza + */ + it("renders footer", () => { + render(