346 lines
11 KiB
Markdown
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`
|