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

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 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:

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:

  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

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/ -- 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