Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
{
"name": "forge",
"version": "1.0.0",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "playwright test"
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.460.0",
"next": "14.2.18",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.53.2",
"tailwind-merge": "^2.5.4",
"zod": "^3.23.8"
"lucide-react": "^0.468.0",
"next": "^15.1.6",
"next-themes": "^0.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.5",
"zod": "^3.24.1"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/node": "^22.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.2",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitest/coverage-v8": "^2.1.8",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-next": "^15.1.6",
"jsdom": "^25.0.1",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.16",
"typescript": "^5.6.3",
"vitest": "^2.1.5"
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
34 changes: 34 additions & 0 deletions src/app/globals-css.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";

describe("globals.css", () => {
it("defines Tailwind layers and root/dark theme variables", async () => {
const cssPath = path.resolve(process.cwd(), "src/app/globals.css");
const css = await readFile(cssPath, "utf8");

expect(css).toContain("@tailwind base;");
expect(css).toContain("@tailwind components;");
expect(css).toContain("@tailwind utilities;");

expect(css).toContain(":root {");
expect(css).toContain(".dark {");

expect(css).toContain("--background:");
expect(css).toContain("--foreground:");
expect(css).toContain("--primary:");
expect(css).toContain("--secondary:");
expect(css).toContain("--accent:");
expect(css).toContain("--destructive:");
expect(css).toContain("--chart-1:");
expect(css).toContain("--chart-5:");
});

it("applies global border and body typography utilities", async () => {
const cssPath = path.resolve(process.cwd(), "src/app/globals.css");
const css = await readFile(cssPath, "utf8");

expect(css).toContain("@apply border-border;");
expect(css).toContain("@apply bg-background text-foreground font-sans antialiased;");
});
});
69 changes: 60 additions & 9 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,66 @@
@tailwind components;
@tailwind utilities;

:root {
color-scheme: dark;
}
@layer base {
:root {
--background: 210 40% 98%;
--foreground: 222.2 47.4% 11.2%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--primary: 218.3 79.2% 53.1%;
--primary-foreground: 210 40% 98%;
--secondary: 179.1 84.1% 35.1%;
--secondary-foreground: 210 40% 98%;
--muted: 214.3 31.8% 91.4%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 37.7 92.1% 50.2%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 218.3 79.2% 53.1%;
--chart-1: 218.3 79.2% 53.1%;
--chart-2: 179.1 84.1% 35.1%;
--chart-3: 37.7 92.1% 50.2%;
--chart-4: 262.1 83.3% 57.8%;
--chart-5: 0 72.2% 50.6%;
}

html,
body {
min-height: 100%;
}
.dark {
--background: 222.2 47.4% 11.2%;
--foreground: 210 40% 98%;
--card: 222.2 47.4% 14%;
--card-foreground: 210 40% 98%;
--popover: 222.2 47.4% 14%;
--popover-foreground: 210 40% 98%;
--primary: 213.1 93.9% 67.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 173.4 80.4% 48%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 43.3 96.4% 56.3%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 91.2% 71.4%;
--destructive-foreground: 222.2 47.4% 11.2%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 213.1 93.9% 67.8%;
--chart-1: 213.1 93.9% 67.8%;
--chart-2: 173.4 80.4% 48%;
--chart-3: 43.3 96.4% 56.3%;
--chart-4: 258.3 89.5% 74.7%;
--chart-5: 0 91.2% 71.4%;
}

* {
@apply border-border;
}

body {
@apply bg-background text-foreground antialiased;
body {
@apply bg-background text-foreground font-sans antialiased;
}
}
34 changes: 34 additions & 0 deletions src/app/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from "vitest";

vi.mock("next/font/google", () => ({
Inter: () => ({ variable: "--font-inter-mock" })
}));

vi.mock("@/lib/config/env", () => ({
env: {
NEXT_PUBLIC_APP_NAME: "Forge Test App"
}
}));

import RootLayout, { metadata } from "@/app/layout";

describe("app layout", () => {
it("exports metadata with app name title", () => {
expect(metadata.title).toBe("Forge Test App");
expect(metadata.description).toBe("Forge client portal");
});

it("renders html/body shell with expected classes", () => {
const tree = RootLayout({ children: <div id="child" /> });

expect(tree.type).toBe("html");
expect(tree.props.lang).toBe("en");
expect(tree.props.className).toBe("--font-inter-mock");

const body = tree.props.children;
expect(body.type).toBe("body");
expect(body.props.className).toContain("font-sans");
expect(body.props.className).toContain("antialiased");
expect(body.props.children.props.id).toBe("child");
});
});
8 changes: 4 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import { Manrope } from "next/font/google";
import { Inter } from "next/font/google";
import "@/app/globals.css";
import { env } from "@/lib/config/env";

const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope" });
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });

export const metadata: Metadata = {
title: env.NEXT_PUBLIC_APP_NAME,
Expand All @@ -12,8 +12,8 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }): JSX.Element {
return (
<html lang="en" className={manrope.variable}>
<body className="font-sans">{children}</body>
<html lang="en" className={inter.variable}>
<body className="font-sans antialiased">{children}</body>
</html>
);
}
73 changes: 73 additions & 0 deletions src/config/__tests__/package-json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";

type PackageJson = {
name: string;
version: string;
private: boolean;
scripts: Record<string, string>;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};

const packageJsonPath = path.resolve(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageJson;

describe("package.json", () => {
it("defines expected package identity", () => {
expect(packageJson.name).toBe("forge");
expect(packageJson.version).toBe("0.1.0");
expect(packageJson.private).toBe(true);
});

it("exposes expected app scripts", () => {
expect(packageJson.scripts).toMatchObject({
dev: "next dev",
build: "next build",
start: "next start",
lint: "next lint",
test: "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
});
});

it("includes runtime dependencies needed by app and UI", () => {
const deps = packageJson.dependencies;

expect(deps).toEqual(
expect.objectContaining({
next: expect.stringMatching(/^\^/),
react: expect.stringMatching(/^\^/),
"react-dom": expect.stringMatching(/^\^/),
zod: expect.stringMatching(/^\^/),
"next-themes": expect.stringMatching(/^\^/),
"lucide-react": expect.stringMatching(/^\^/),
"class-variance-authority": expect.stringMatching(/^\^/),
clsx: expect.stringMatching(/^\^/),
"tailwind-merge": expect.stringMatching(/^\^/)
})
);
});

it("includes required testing/tooling dev dependencies", () => {
const devDeps = packageJson.devDependencies;

expect(devDeps).toEqual(
expect.objectContaining({
vitest: expect.stringMatching(/^\^/),
jsdom: expect.stringMatching(/^\^/),
"@testing-library/react": expect.stringMatching(/^\^/),
"@testing-library/jest-dom": expect.stringMatching(/^\^/),
"@vitest/coverage-v8": expect.stringMatching(/^\^/),
typescript: expect.stringMatching(/^\^/),
eslint: expect.stringMatching(/^\^/),
"eslint-config-next": expect.stringMatching(/^\^/),
tailwindcss: expect.stringMatching(/^\^/),
postcss: expect.stringMatching(/^\^/),
autoprefixer: expect.stringMatching(/^\^/)
})
);
});
});
67 changes: 67 additions & 0 deletions src/config/__tests__/tailwind-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import config from "../../../../tailwind.config";

describe("tailwind.config", () => {
it("uses class-based dark mode", () => {
expect(config.darkMode).toEqual(["class"]);
});

it("scans app/component/hook/lib source globs", () => {
expect(config.content).toEqual([
"./src/app/**/*.{ts,tsx}",
"./src/components/**/*.{ts,tsx}",
"./src/hooks/**/*.{ts,tsx}",
"./src/lib/**/*.{ts,tsx}"
]);
});

it("defines key extended color tokens", () => {
const colors = config.theme?.extend?.colors as Record<string, unknown>;

expect(colors.border).toBe("hsl(var(--border))");
expect(colors.input).toBe("hsl(var(--border))");
expect(colors.ring).toBe("hsl(var(--primary))");
expect(colors.background).toBe("hsl(var(--background))");
expect(colors.foreground).toBe("hsl(var(--foreground))");
expect(colors.primary).toEqual({
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))"
});
expect(colors.secondary).toEqual({
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))"
});
expect(colors.destructive).toEqual({
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))"
});
expect(colors["chart-1"]).toBe("hsl(var(--chart-1))");
expect(colors["chart-5"]).toBe("hsl(var(--chart-5))");
});

it("defines custom radii and font family", () => {
const extend = config.theme?.extend as {
borderRadius: Record<string, string>;
fontFamily: Record<string, string[]>;
};

expect(extend.borderRadius).toEqual({
lg: "0.5rem",
md: "0.375rem",
xl: "0.75rem"
});

expect(extend.fontFamily.sans).toEqual([
"var(--font-inter)",
"Inter",
"system-ui",
"sans-serif"
]);
});

it("registers tailwindcss-animate plugin", () => {
expect(Array.isArray(config.plugins)).toBe(true);
expect(config.plugins).toHaveLength(1);
expect(typeof config.plugins?.[0]).toBe("function");
});
});
Loading