11 KiB
@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
classNameprop for composition. UseforwardReffor DOM elements. - Molecules: Single responsibility. Minimal controlled state (e.g., open/closed). Compose atoms only. Accept
classNamefor outer container. - Organisms: Can have internal state and sub-components. Can fetch context. Self-contained sections of a page.
- Templates: Use
childrenor 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>plusvariantandsize - 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-disabledstate
FormField (Molecule)
- Props: Extends
InputPropspluslabel(string),error?(string),description?(string) - Composes: Label + Input
- Auto-generates
idfrom label text if not provided
cn() Utility
The cn() function merges Tailwind classes safely using clsx + tailwind-merge:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Usage in components:
<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:
@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:
- Install:
pnpm dlx shadcn@latest add [component]-- it lands inatoms/by default - Classify: Check the Atomic Design classification table above
- Relocate: If the component is not an atom, move it to the correct level directory
- Story: Create a
.stories.tsxfile next to the component - Export: Add to the level's
index.tsbarrel 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 storiesget-documentation-- understand existing component props, variants, and usagerun-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
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:
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:
export { Badge, type BadgeProps } from "./badge";
Step 6: Write the story
src/atoms/badge/badge.stories.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:
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/-- seeapps/storybook/AGENTS.md - Consumed by Next.js:
apps/web-next/-- seeapps/web-next/AGENTS.md - Consumed by TanStack:
apps/web-tanstack/-- seeapps/web-tanstack/AGENTS.md