From d974bb626770304b1f87f3ef479794f1fe9c0f39 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Sat, 9 May 2026 14:17:47 +0200 Subject: [PATCH] feat(generators): capture core-ui as verbatim template files 28 .hbs files mirror packages/core-ui verbatim: atoms (Button, Input, Label + stories + tests), molecules (FormField), organisms/templates (empty barrels), lib/utils, styles/globals.css, plus all root config files (package.json, tsconfig.json, vitest.config.ts, eslint.config.js, turbo.json, AGENTS.md). Co-Authored-By: Claude Sonnet 4.6 --- .../templates/core-package/ui/AGENTS.md.hbs | 67 +++++++++++++++++++ .../core-package/ui/eslint.config.js.hbs | 3 + .../core-package/ui/package.json.hbs | 33 +++++++++ .../src/atoms/button/button.stories.tsx.hbs | 38 +++++++++++ .../ui/src/atoms/button/button.test.tsx.hbs | 42 ++++++++++++ .../ui/src/atoms/button/button.tsx.hbs | 41 ++++++++++++ .../ui/src/atoms/button/index.ts.hbs | 1 + .../core-package/ui/src/atoms/index.ts.hbs | 3 + .../ui/src/atoms/input/index.ts.hbs | 1 + .../ui/src/atoms/input/input.stories.tsx.hbs | 19 ++++++ .../ui/src/atoms/input/input.test.tsx.hbs | 31 +++++++++ .../ui/src/atoms/input/input.tsx.hbs | 21 ++++++ .../ui/src/atoms/label/index.ts.hbs | 1 + .../ui/src/atoms/label/label.test.tsx.hbs | 16 +++++ .../ui/src/atoms/label/label.tsx.hbs | 20 ++++++ .../core-package/ui/src/index.ts.hbs | 5 ++ .../core-package/ui/src/lib/utils.ts.hbs | 6 ++ .../form-field/form-field.stories.tsx.hbs | 31 +++++++++ .../form-field/form-field.test.tsx.hbs | 33 +++++++++ .../molecules/form-field/form-field.tsx.hbs | 31 +++++++++ .../ui/src/molecules/form-field/index.ts.hbs | 1 + .../ui/src/molecules/index.ts.hbs | 1 + .../ui/src/organisms/index.ts.hbs | 1 + .../ui/src/styles/globals.css.hbs | 26 +++++++ .../ui/src/templates/index.ts.hbs | 1 + .../core-package/ui/tsconfig.json.hbs | 13 ++++ .../templates/core-package/ui/turbo.json.hbs | 4 ++ .../core-package/ui/vitest.config.ts.hbs | 9 +++ 28 files changed, 499 insertions(+) create mode 100644 turbo/generators/templates/core-package/ui/AGENTS.md.hbs create mode 100644 turbo/generators/templates/core-package/ui/eslint.config.js.hbs create mode 100644 turbo/generators/templates/core-package/ui/package.json.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/button/button.stories.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/button/button.test.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/button/button.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/button/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/input/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/input/input.stories.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/input/input.test.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/input/input.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/label/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/label/label.test.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/atoms/label/label.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/lib/utils.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/molecules/form-field/form-field.stories.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/molecules/form-field/form-field.test.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/molecules/form-field/form-field.tsx.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/molecules/form-field/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/styles/globals.css.hbs create mode 100644 turbo/generators/templates/core-package/ui/src/templates/index.ts.hbs create mode 100644 turbo/generators/templates/core-package/ui/tsconfig.json.hbs create mode 100644 turbo/generators/templates/core-package/ui/turbo.json.hbs create mode 100644 turbo/generators/templates/core-package/ui/vitest.config.ts.hbs diff --git a/turbo/generators/templates/core-package/ui/AGENTS.md.hbs b/turbo/generators/templates/core-package/ui/AGENTS.md.hbs new file mode 100644 index 0000000..a984ec8 --- /dev/null +++ b/turbo/generators/templates/core-package/ui/AGENTS.md.hbs @@ -0,0 +1,67 @@ +# AGENTS.md — core-ui + +Design-system primitives organized by Atomic Design. Only **generic components** (atoms, molecules, generic organisms) live here. Feature-specific components live in their respective feature packages. + +## Responsibilities + +- **Atoms** — Single HTML elements: Button, Input, Label, Card +- **Molecules** — Combinations: FormField (Label + Input), Badge, Avatar +- **Generic Organisms** — Reusable complex layouts: Modal, Tabs, NavigationMenu, CommandPalette +- **Templates** — Page layouts: AuthLayout, DashboardLayout, LandingLayout +- **Utilities** — `cn()` for className merging, design tokens, Tailwind config + +## Feature-specific boundary + +Feature-specific organisms (e.g., `ArticleCard`, `ArticleList`, `HeaderNavMenu`) live in their feature's `ui/` folder, NOT here. This boundary keeps `core-ui` framework-agnostic and reusable. + +## Must NOT import + +- Any feature package (`@repo/auth`, `@repo/blog`, etc.) +- Any app package +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc` + +> Note: `@repo/core-trpc` is an optional package scaffolded via `pnpm turbo gen core-package trpc`. If not present, this constraint still applies to any future installation. + +## Public exports + +From `package.json`: +- `.` — all atoms, molecules, organisms, templates + +Example usage: +```typescript +import { Button, Input, Label } from "@repo/core-ui"; +import { FormField } from "@repo/core-ui"; +import { Modal, Tabs } from "@repo/core-ui"; +``` + +## Test conventions + +- Tests colocated: `src/atoms/button/button.tsx` → `src/atoms/button/button.test.tsx` +- Vitest environment: `jsdom` (React component testing) +- Alias: `@/` resolves to `src/` +- Run: `pnpm test --filter @repo/core-ui` +- Storybook stories colocated: `src/atoms/button/button.stories.tsx` + +## Structure + +``` +src/ + atoms/ + {name}/ + {name}.tsx # Component + {name}.test.tsx # Tests + {name}.stories.tsx # Storybook story + index.ts # Barrel export + molecules/ + {name}/ + ... + organisms/ + {name}/ + ... + templates/ + {name}/ + ... + lib/ + utils.ts # cn() and helpers + index.ts # Re-exports everything +``` diff --git a/turbo/generators/templates/core-package/ui/eslint.config.js.hbs b/turbo/generators/templates/core-package/ui/eslint.config.js.hbs new file mode 100644 index 0000000..7440d8f --- /dev/null +++ b/turbo/generators/templates/core-package/ui/eslint.config.js.hbs @@ -0,0 +1,3 @@ +import baseConfig from "@repo/core-eslint/base"; + +export default baseConfig; diff --git a/turbo/generators/templates/core-package/ui/package.json.hbs b/turbo/generators/templates/core-package/ui/package.json.hbs new file mode 100644 index 0000000..e9354e2 --- /dev/null +++ b/turbo/generators/templates/core-package/ui/package.json.hbs @@ -0,0 +1,33 @@ +{ + "name": "@repo/core-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./styles/globals.css": "./src/styles/globals.css" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "clsx": "^2.1.1", + "react": "^19.0.0", + "tailwind-merge": "^3.0.0" + }, + "devDependencies": { + "@repo/core-eslint": "workspace:*", + "@repo/core-testing": "workspace:*", + "@repo/core-typescript": "workspace:*", + "@storybook/react": "^8.6.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.0", + "@types/react": "^19.0.0", + "jsdom": "^25.0.0", + "vitest": "^3.0.0" + } +} diff --git a/turbo/generators/templates/core-package/ui/src/atoms/button/button.stories.tsx.hbs b/turbo/generators/templates/core-package/ui/src/atoms/button/button.stories.tsx.hbs new file mode 100644 index 0000000..094736a --- /dev/null +++ b/turbo/generators/templates/core-package/ui/src/atoms/button/button.stories.tsx.hbs @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "./button"; + +const meta = { + title: "Atoms/Button", + component: Button, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["default", "secondary", "destructive", "outline", "ghost"], + }, + size: { control: "select", options: ["sm", "default", "lg"] }, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { children: "Button", variant: "default" }, +}; + +export const Secondary: Story = { + args: { children: "Secondary", variant: "secondary" }, +}; + +export const Destructive: Story = { + args: { children: "Destructive", variant: "destructive" }, +}; + +export const Outline: Story = { + args: { children: "Outline", variant: "outline" }, +}; + +export const Ghost: Story = { + args: { children: "Ghost", variant: "ghost" }, +}; diff --git a/turbo/generators/templates/core-package/ui/src/atoms/button/button.test.tsx.hbs b/turbo/generators/templates/core-package/ui/src/atoms/button/button.test.tsx.hbs new file mode 100644 index 0000000..bcf664f --- /dev/null +++ b/turbo/generators/templates/core-package/ui/src/atoms/button/button.test.tsx.hbs @@ -0,0 +1,42 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderWithProviders } from "@repo/core-testing/react"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Button } from "./button"; + +describe("Button", () => { + it("renders children inside a button", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument(); + }); + + it("calls onClick when activated", async () => { + const handleClick = vi.fn(); + renderWithProviders(); + await userEvent.click(screen.getByRole("button", { name: "Go" })); + expect(handleClick).toHaveBeenCalledOnce(); + }); + + it("applies the destructive variant class", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toHaveClass(/destructive/); + }); + + it("applies the lg size class", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toHaveClass(/h-11/); + }); + + it("disabled prop sets the attribute and prevents click handler", async () => { + const handleClick = vi.fn(); + renderWithProviders( + , + ); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + await userEvent.click(button); + expect(handleClick).not.toHaveBeenCalled(); + }); +}); diff --git a/turbo/generators/templates/core-package/ui/src/atoms/button/button.tsx.hbs b/turbo/generators/templates/core-package/ui/src/atoms/button/button.tsx.hbs new file mode 100644 index 0000000..38564ed --- /dev/null +++ b/turbo/generators/templates/core-package/ui/src/atoms/button/button.tsx.hbs @@ -0,0 +1,41 @@ +import { forwardRef, type ButtonHTMLAttributes } from "react"; +import { cn } from "../../lib/utils"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: "default" | "secondary" | "destructive" | "outline" | "ghost"; + size?: "sm" | "default" | "lg"; +} + +const variantStyles: Record, string> = { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", +}; + +const sizeStyles: Record, string> = { + sm: "h-9 px-3 text-sm", + default: "h-10 px-4 py-2", + lg: "h-11 px-8 text-lg", +}; + +export const Button = forwardRef( + ({ className, variant = "default", size = "default", ...props }, ref) => { + return ( +