feat(#1): Set up monorepo scaffold with pnpm workspaces + TurboRepo
Implements the foundational project structure including: - pnpm workspaces configuration - TurboRepo for build orchestration - NestJS 11.1.12 API (apps/api) - Next.js 16.1.6 web app (apps/web) - Shared packages (config, shared, ui) - TypeScript strict mode configuration - ESLint + Prettier setup - Vitest for unit testing (19 passing tests) Fixes #1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
42
packages/ui/package.json
Normal file
42
packages/ui/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@mosaic/ui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./components/*": {
|
||||
"types": "./dist/components/*.d.ts",
|
||||
"import": "./dist/components/*.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mosaic/config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"jsdom": "^26.0.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
}
|
||||
31
packages/ui/src/components/Button.test.tsx
Normal file
31
packages/ui/src/components/Button.test.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { Button } from "./Button.js";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("Button", () => {
|
||||
it("should render children", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByRole("button")).toHaveTextContent("Click me");
|
||||
});
|
||||
|
||||
it("should apply variant styles", () => {
|
||||
render(<Button variant="danger">Delete</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.className).toContain("bg-red-600");
|
||||
});
|
||||
|
||||
it("should apply size styles", () => {
|
||||
render(<Button size="lg">Large Button</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.className).toContain("px-6");
|
||||
});
|
||||
|
||||
it("should pass through additional props", () => {
|
||||
render(<Button disabled>Disabled</Button>);
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
39
packages/ui/src/components/Button.tsx
Normal file
39
packages/ui/src/components/Button.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "danger";
|
||||
size?: "sm" | "md" | "lg";
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
children,
|
||||
className = "",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const baseStyles = "inline-flex items-center justify-center font-medium rounded-md";
|
||||
|
||||
const variantStyles = {
|
||||
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700",
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "px-3 py-1.5 text-sm",
|
||||
md: "px-4 py-2 text-base",
|
||||
lg: "px-6 py-3 text-lg",
|
||||
};
|
||||
|
||||
const combinedClassName = [baseStyles, variantStyles[variant], sizeStyles[size], className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<button className={combinedClassName} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
2
packages/ui/src/index.ts
Normal file
2
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Button } from "./components/Button.js";
|
||||
export type { ButtonProps } from "./components/Button.js";
|
||||
11
packages/ui/tsconfig.json
Normal file
11
packages/ui/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@mosaic/config/typescript/library",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.tsx"]
|
||||
}
|
||||
15
packages/ui/vitest.config.ts
Normal file
15
packages/ui/vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["node_modules/", "dist/"],
|
||||
},
|
||||
setupFiles: ["./vitest.setup.ts"],
|
||||
},
|
||||
});
|
||||
1
packages/ui/vitest.setup.ts
Normal file
1
packages/ui/vitest.setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
Reference in New Issue
Block a user