Files
agentic-dev/packages/ui/AGENTS.md

346 lines
11 KiB
Markdown

# @repo/ui -- Atomic Design Component Library
## Purpose
Shared UI component library built with shadcn/ui patterns, Tailwind CSS v4, and organized by Atomic Design levels. All components have co-located Storybook stories. This package is consumed by all frontend apps (`apps/web-next`, `apps/web-tanstack`).
## Atomic Design Classification Guide
| Level | Definition | Examples | Import Rules |
|---|---|---|---|
| Atom | Single HTML element, cannot be broken down further | Button, Input, Label, Badge, Separator, Icon | Can import: `lib/`, `styles/`. NEVER import: molecules, organisms, templates |
| Molecule | 2-3 atoms composed together, single responsibility | FormField, SearchBar, Tooltip, Select, Dropdown | Can import: `atoms/`, `lib/`. NEVER import: organisms, templates |
| Organism | Complex self-contained section with multiple molecules/atoms | DataTable, Dialog, Header, Sidebar, Card, Navbar | Can import: `atoms/`, `molecules/`, `lib/`. NEVER import: templates |
| Template | Page-level layout shell, content-agnostic (uses children/slots) | DashboardLayout, AuthLayout, MarketingLayout | Can import: `atoms/`, `molecules/`, `organisms/`, `lib/` |
| Page | Template filled with real data | **LIVES IN `apps/`, NOT IN THIS PACKAGE** | N/A |
## Import Rules Table
| Level | Can import from | NEVER import from |
|---|---|---|
| Atoms | `lib/`, `hooks/`, `styles/` | `molecules/`, `organisms/`, `templates/` |
| Molecules | `atoms/`, `lib/`, `hooks/` | `organisms/`, `templates/` |
| Organisms | `atoms/`, `molecules/`, `lib/`, `hooks/` | `templates/` |
| Templates | `atoms/`, `molecules/`, `organisms/`, `lib/`, `hooks/` | (nothing above -- top level) |
## Component Rules Per Level
- **Atoms:** No margins or positioning (consumer controls layout). No internal state. No business logic. Accept `className` prop for composition. Use `forwardRef` for DOM elements.
- **Molecules:** Single responsibility. Minimal controlled state (e.g., open/closed). Compose atoms only. Accept `className` for outer container.
- **Organisms:** Can have internal state and sub-components. Can fetch context. Self-contained sections of a page.
- **Templates:** Use `children` or named slots (`sidebar`, `header`, etc.) for content injection. NEVER hard-code content or data.
## File Structure
```
packages/ui/
src/
lib/
utils.ts # cn() utility (clsx + twMerge)
styles/
globals.css # Tailwind v4 @theme tokens, @import "tailwindcss"
atoms/
button/
button.tsx # Button component (5 variants, 3 sizes)
button.stories.tsx # Storybook stories
index.ts # Barrel export
input/
input.tsx # Input component
input.stories.tsx
index.ts
label/
label.tsx # Label component
index.ts
index.ts # Barrel: re-exports all atoms
molecules/
form-field/
form-field.tsx # FormField = Label + Input + error/description
form-field.stories.tsx
index.ts
index.ts # Barrel: re-exports all molecules
organisms/
index.ts # Barrel (empty -- no organisms yet)
templates/
index.ts # Barrel (empty -- no templates yet)
index.ts # Package entry: re-exports cn + all levels
package.json
AGENTS.md
```
## Existing Components
### Button (Atom)
- **Variants:** `default`, `secondary`, `destructive`, `outline`, `ghost`
- **Sizes:** `sm` (h-9), `default` (h-10), `lg` (h-11)
- **Props:** Extends `ButtonHTMLAttributes<HTMLButtonElement>` plus `variant` and `size`
- **Uses:** `forwardRef`, `cn()` for class merging
### Input (Atom)
- **Props:** Extends `InputHTMLAttributes<HTMLInputElement>`
- **Uses:** `forwardRef`, `cn()` for class merging
- Full styling: border, focus ring, disabled state, file input support
### Label (Atom)
- **Props:** Extends `LabelHTMLAttributes<HTMLLabelElement>`
- **Uses:** `forwardRef`, `cn()` for class merging
- Handles `peer-disabled` state
### FormField (Molecule)
- **Props:** Extends `InputProps` plus `label` (string), `error?` (string), `description?` (string)
- **Composes:** Label + Input
- Auto-generates `id` from label text if not provided
## cn() Utility
The `cn()` function merges Tailwind classes safely using `clsx` + `tailwind-merge`:
```typescript
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
Usage in components:
```tsx
<button
className={cn(
"inline-flex items-center justify-center rounded-md", // base classes
variantStyles[variant], // variant classes
sizeStyles[size], // size classes
className // consumer override
)}
/>
```
`cn()` ensures that consumer-provided classes properly override component defaults (e.g., `cn("bg-red-500", "bg-blue-500")` yields `"bg-blue-500"`).
## Tailwind v4 CSS-First Configuration
This project uses **Tailwind CSS v4**, which replaces `tailwind.config.ts` with CSS-first configuration. There is no `tailwind.config.ts` file. All design tokens are defined in `src/styles/globals.css` using the `@theme` directive:
```css
@import "tailwindcss";
@theme {
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(240 10% 3.9%);
--color-primary: hsl(240 5.9% 10%);
--color-primary-foreground: hsl(0 0% 98%);
--color-secondary: hsl(240 4.8% 95.9%);
--color-destructive: hsl(0 84.2% 60.2%);
--color-border: hsl(240 5.9% 90%);
--color-input: hsl(240 5.9% 90%);
--color-ring: hsl(240 5.9% 10%);
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
```
To add new tokens, add them inside the `@theme { }` block in `globals.css`. Classes like `bg-primary`, `text-destructive`, `rounded-lg` reference these tokens automatically.
## shadcn/ui Workflow
When adding a new shadcn/ui component:
1. **Install:** `pnpm dlx shadcn@latest add [component]` -- it lands in `atoms/` by default
2. **Classify:** Check the Atomic Design classification table above
3. **Relocate:** If the component is not an atom, move it to the correct level directory
4. **Story:** Create a `.stories.tsx` file next to the component
5. **Export:** Add to the level's `index.ts` barrel file
## Storybook MCP Integration
Before creating any new UI component, query the Storybook MCP to check for existing components:
- **`list-all-documentation`** -- discover all existing components and their stories
- **`get-documentation`** -- understand existing component props, variants, and usage
- **`run-story-tests`** -- validate your new story renders correctly after creation
Storybook MCP is available at `http://localhost:6006/mcp` when Storybook is running.
## Complete Story File Template
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { MyComponent } from "./my-component";
const meta = {
title: "{Level}/{ComponentName}", // e.g., "Atoms/Button", "Molecules/FormField"
component: MyComponent,
tags: ["autodocs"],
argTypes: {
// Define controls for interactive props
variant: {
control: "select",
options: ["default", "secondary"],
},
size: {
control: "select",
options: ["sm", "default", "lg"],
},
disabled: { control: "boolean" },
},
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: "Default",
},
};
export const AnotherVariant: Story = {
args: {
children: "Another Variant",
variant: "secondary",
},
};
```
## Recipe: Adding a New Component (8 Steps)
This example adds a `Badge` atom component.
### Step 1: Check Storybook MCP for existing components
Query `list-all-documentation` to confirm no Badge component exists.
### Step 2: Classify the component
Badge is a single HTML element displaying a short label -- it is an **Atom**.
### Step 3: Create the component directory
```
src/atoms/badge/
badge.tsx
badge.stories.tsx
index.ts
```
### Step 4: Write the component
`src/atoms/badge/badge.tsx`:
```tsx
import { type HTMLAttributes } from "react";
import { cn } from "../../lib/utils";
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: "default" | "secondary" | "destructive" | "outline";
}
const variantStyles: Record<NonNullable<BadgeProps["variant"]>, string> = {
default: "bg-primary text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background text-foreground",
};
export function Badge({
className,
variant = "default",
...props
}: BadgeProps) {
return (
<span
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",
variantStyles[variant],
className
)}
{...props}
/>
);
}
```
### Step 5: Create the barrel export
`src/atoms/badge/index.ts`:
```typescript
export { Badge, type BadgeProps } from "./badge";
```
### Step 6: Write the story
`src/atoms/badge/badge.stories.tsx`:
```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Badge } from "./badge";
const meta = {
title: "Atoms/Badge",
component: Badge,
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "secondary", "destructive", "outline"],
},
},
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: "Badge" },
};
export const Secondary: Story = {
args: { children: "Secondary", variant: "secondary" },
};
export const Destructive: Story = {
args: { children: "Error", variant: "destructive" },
};
export const Outline: Story = {
args: { children: "Outline", variant: "outline" },
};
```
### Step 7: Add to atoms barrel
Edit `src/atoms/index.ts`:
```typescript
export { Button, type ButtonProps } from "./button/index";
export { Input, type InputProps } from "./input/index";
export { Label, type LabelProps } from "./label/index";
export { Badge, type BadgeProps } from "./badge/index"; // <-- add
```
### Step 8: Validate
Run `run-story-tests` via Storybook MCP to confirm the stories render correctly.
## Dependencies
| Dependency | Purpose |
|---|---|
| `react` | JSX runtime |
| `clsx` | Conditional class string builder |
| `tailwind-merge` | Intelligent Tailwind class merging (deduplication) |
| `tailwindcss` (devDep) | Tailwind CSS v4 engine |
## Cross-References
- **Storybook app:** `apps/storybook/` -- see `apps/storybook/AGENTS.md`
- **Consumed by Next.js:** `apps/web-next/` -- see `apps/web-next/AGENTS.md`
- **Consumed by TanStack:** `apps/web-tanstack/` -- see `apps/web-tanstack/AGENTS.md`