Files
frontend/QUICK_START_ATOMIC.md
T
Thanh Quy - wolf eca619b79a feat: Implement Atomic Design Phase 1 - Atoms Foundation
 Created complete atoms library (21 components):
- Buttons: Button, IconButton with variants (primary, secondary, danger, ghost)
- Inputs: TextInput, SearchInput, Textarea with validation support
- Typography: Heading (h1-h6), Text (variants), Caption
- Badges: Badge (5 variants), PriceBadge (formatted pricing)
- Dividers: Horizontal/vertical separators

🎯 Updated existing components to use atoms:
- CartProduct: Now uses Button, Text, Caption atoms
- ReviewModal: Now uses Button, Textarea, Heading, Text atoms

📚 Added comprehensive documentation:
- components/atoms/ATOMS.md: Complete atoms reference guide
- Usage examples, theming, import patterns, accessibility

🏗️ Architecture improvements:
- Foundation for molecules/organisms
- Type-safe components with full TypeScript support
- Consistent theming via CSS variables
- Barrel exports for clean imports

 Verified:
- npm run build: Success (✓ Compiled successfully)
- npm run format: All files formatted
- npm run lint: No new errors in atoms

Next: Phase 2 - Create molecules (ProductCard, FormField, SearchBar, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:40:07 +07:00

14 KiB

Quick Start: Atomic Design Implementation

TL;DR: Follow this to start building components the atomic way. Do Step 1 first (creates foundation), then everything else uses it.


Step 1: Create Button Atom (30 minutes)

This is the most important component. Everything else will use it.

Create directory

mkdir -p components/atoms/buttons

Create components/atoms/buttons/Button.types.ts

import { ButtonHTMLAttributes } from "react";

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "danger" | "ghost";
  size?: "sm" | "md" | "lg";
  icon?: string; // FontAwesome class like "fa-solid fa-cart-plus"
  iconPosition?: "left" | "right";
  disabled?: boolean;
  children: React.ReactNode;
  className?: string;
}

Create components/atoms/buttons/Button.tsx

import type { ButtonProps } from "./Button.types";

export default function Button({
  variant = "primary",
  size = "md",
  icon,
  iconPosition = "left",
  disabled = false,
  children,
  className = "",
  ...props
}: ButtonProps) {
  const baseStyles =
    "font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5";

  const variants = {
    primary: "bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-95",
    secondary: "border border-(--color-border) hover:bg-(--color-border-light) active:scale-95",
    danger: "bg-red-500 text-white hover:bg-red-600 active:scale-95",
    ghost: "bg-transparent hover:bg-(--color-border-light) active:scale-95",
  };

  const sizes = {
    sm: "px-3 py-1.5 text-xs",
    md: "px-4 py-2 text-sm",
    lg: "px-6 py-3 text-base",
  };

  const iconClasses = `text-sm shrink-0`;

  return (
    <button
      className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
      disabled={disabled}
      {...props}
    >
      {icon && iconPosition === "left" && <i className={`fa-solid ${icon} ${iconClasses}`}></i>}
      {children}
      {icon && iconPosition === "right" && <i className={`fa-solid ${icon} ${iconClasses}`}></i>}
    </button>
  );
}

Create components/atoms/buttons/index.ts

export { default as Button } from "./Button";
export type { ButtonProps } from "./Button.types";

Test it

// Try in any page
import { Button } from "@/components/atoms/buttons";

<Button variant="primary" size="sm" icon="fa-cart-plus">Mua</Button>
<Button variant="secondary">Quay lại</Button>
<Button variant="danger" size="lg">Xóa</Button>

Step 2: Create TextInput Atom (20 minutes)

Create directory

mkdir -p components/atoms/inputs

Create components/atoms/inputs/Input.types.ts

import { InputHTMLAttributes } from "react";

export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
  icon?: string; // FontAwesome class
  onIconClick?: () => void;
  className?: string;
}

Create components/atoms/inputs/TextInput.tsx

import type { TextInputProps } from "./Input.types";

export default function TextInput({
  label,
  error,
  icon,
  onIconClick,
  className = "",
  ...props
}: TextInputProps) {
  return (
    <div className="w-full">
      {label && (
        <label className="mb-1.5 block text-sm font-medium text-(--color-text-secondary)">
          {label}
        </label>
      )}
      <div className="relative">
        <input
          className={`w-full rounded-lg border border-(--color-border) bg-transparent px-3 py-2 text-sm transition-colors placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20 focus:outline-none ${error ? "border-red-500" : ""} ${className}`}
          {...props}
        />
        {icon && (
          <button
            type="button"
            onClick={onIconClick}
            className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted) hover:text-(--color-primary)"
          >
            <i className={`fa-solid ${icon}`}></i>
          </button>
        )}
      </div>
      {error && <p className="mt-1 text-xs text-red-500">{error}</p>}
    </div>
  );
}

Create components/atoms/inputs/SearchInput.tsx

import { InputHTMLAttributes } from "react";

export default function SearchInput({
  value,
  onChange,
  onClear,
  ...props
}: InputHTMLAttributes<HTMLInputElement> & {
  onClear?: () => void;
}) {
  return (
    <div className="relative w-full">
      <i className="fa-solid fa-magnifying-glass pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-(--color-text-muted)"></i>
      <input
        type="text"
        value={value}
        onChange={onChange}
        className="w-full rounded-lg border border-(--color-border) bg-transparent py-2 pr-9 pl-9 text-sm transition-all duration-150 placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20 focus:outline-none"
        {...props}
      />
      {value && onClear && (
        <button
          type="button"
          onClick={onClear}
          className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted) hover:text-(--color-primary)"
        >
          <i className="fa-solid fa-xmark text-sm"></i>
        </button>
      )}
    </div>
  );
}

Create components/atoms/inputs/index.ts

export { default as TextInput } from "./TextInput";
export { default as SearchInput } from "./SearchInput";
export type { TextInputProps } from "./Input.types";

Step 3: Create Typography Atoms (45 minutes)

Create directory

mkdir -p components/atoms/typography

Create components/atoms/typography/Typography.types.ts

import { HTMLAttributes } from "react";

export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
  level?: 1 | 2 | 3 | 4 | 5 | 6;
  children: React.ReactNode;
}

export interface TextProps extends HTMLAttributes<HTMLParagraphElement> {
  variant?: "body1" | "body2" | "caption" | "label";
  children: React.ReactNode;
}

export interface CaptionProps extends HTMLAttributes<HTMLSpanElement> {
  children: React.ReactNode;
}

Create components/atoms/typography/Heading.tsx

import type { HeadingProps } from "./Typography.types";

export default function Heading({
  level = 2,
  children,
  className = "",
  ...props
}: HeadingProps) {
  const sizes = {
    1: "text-3xl font-bold",
    2: "text-2xl font-bold",
    3: "text-xl font-bold",
    4: "text-lg font-semibold",
    5: "text-base font-semibold",
    6: "text-sm font-semibold",
  };

  const Tag = `h${level}` as keyof JSX.IntrinsicElements;

  return (
    <Tag
      className={`text-foreground ${sizes[level]} ${className}`}
      {...props}
    >
      {children}
    </Tag>
  );
}

Create components/atoms/typography/Text.tsx

import type { TextProps } from "./Typography.types";

export default function Text({
  variant = "body1",
  children,
  className = "",
  ...props
}: TextProps) {
  const variants = {
    body1: "text-base text-(--color-text-primary)",
    body2: "text-sm text-(--color-text-secondary)",
    caption: "text-xs text-(--color-text-muted)",
    label: "text-sm font-medium text-(--color-text-secondary)",
  };

  return (
    <p className={`${variants[variant]} ${className}`} {...props}>
      {children}
    </p>
  );
}

Create components/atoms/typography/Caption.tsx

import type { CaptionProps } from "./Typography.types";

export default function Caption({
  children,
  className = "",
  ...props
}: CaptionProps) {
  return (
    <span className={`text-xs text-(--color-text-muted) ${className}`} {...props}>
      {children}
    </span>
  );
}

Create components/atoms/typography/index.ts

export { default as Heading } from "./Heading";
export { default as Text } from "./Text";
export { default as Caption } from "./Caption";
export type { HeadingProps, TextProps, CaptionProps } from "./Typography.types";

Step 4: Create Badge Atom (15 minutes)

Create directory

mkdir -p components/atoms/badges

Create components/atoms/badges/Badge.types.ts

import { HTMLAttributes } from "react";

export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
  variant?: "primary" | "secondary" | "success" | "danger" | "warning";
  size?: "sm" | "md";
  children: React.ReactNode;
}

Create components/atoms/badges/Badge.tsx

import type { BadgeProps } from "./Badge.types";

export default function Badge({
  variant = "primary",
  size = "md",
  children,
  className = "",
  ...props
}: BadgeProps) {
  const variants = {
    primary: "bg-(--color-primary) text-white",
    secondary: "bg-(--color-border-light) text-(--color-text-secondary)",
    success: "bg-green-100 text-green-700",
    danger: "bg-red-100 text-red-700",
    warning: "bg-yellow-100 text-yellow-700",
  };

  const sizes = {
    sm: "px-2 py-1 text-xs",
    md: "px-3 py-1.5 text-sm",
  };

  return (
    <span
      className={`inline-flex items-center justify-center rounded-full font-semibold ${variants[variant]} ${sizes[size]} ${className}`}
      {...props}
    >
      {children}
    </span>
  );
}

Create components/atoms/badges/index.ts

export { default as Badge } from "./Badge";
export type { BadgeProps } from "./Badge.types";

Step 5: Create Divider Atom (5 minutes)

Create directory

mkdir -p components/atoms/dividers

Create components/atoms/dividers/Divider.tsx

import { HTMLAttributes } from "react";

export interface DividerProps extends HTMLAttributes<HTMLHRElement> {
  orientation?: "horizontal" | "vertical";
}

export default function Divider({
  orientation = "horizontal",
  className = "",
  ...props
}: DividerProps) {
  return (
    <hr
      className={`border-(--color-border) ${
        orientation === "horizontal"
          ? "border-t"
          : "border-l h-full"
      } ${className}`}
      {...props}
    />
  );
}

Step 6: Create Atoms Index (10 minutes)

Create components/atoms/index.ts

// Buttons
export { Button } from "./buttons";
export type { ButtonProps } from "./buttons";

// Inputs
export { TextInput, SearchInput } from "./inputs";
export type { TextInputProps } from "./inputs";

// Typography
export { Heading, Text, Caption } from "./typography";
export type { HeadingProps, TextProps, CaptionProps } from "./typography";

// Badges
export { Badge } from "./badges";
export type { BadgeProps } from "./badges";

// Dividers
export { Divider } from "./dividers";
export type { DividerProps } from "./dividers";

Step 7: Replace Existing Code (Optional, Gradual)

Now you have atoms! Start using them:

Before (inline)

// components/CartProduct.tsx
<button
  onClick={onBuy}
  className="flex cursor-pointer items-center gap-1.5 rounded-lg border-none bg-(--color-primary) px-3 py-1.5 text-xs font-semibold whitespace-nowrap text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95"
>
  <i className="fa-solid fa-cart-plus"></i>
  Mua
</button>

After (using atom)

// components/CartProduct.tsx
import { Button } from "@/components/atoms";

<Button variant="primary" size="sm" icon="fa-cart-plus">
  Mua
</Button>;

Before (inline)

// app/(main)/page.tsx
<h2 className="text-foreground text-xl font-bold">
  {activeCategoryLabel}
</h2>
<p className="text-muted-foreground mt-0.5 text-sm">
  {filteredProducts.length} món
</p>

After (using atoms)

// app/(main)/page.tsx
import { Heading, Text } from "@/components/atoms";

<Heading level={2}>{activeCategoryLabel}</Heading>
<Text variant="body2">{filteredProducts.length} món</Text>

Step 8: Verify Everything Works

# Check for TypeScript errors
npm run lint

# Format code
npm run format

# Run dev server
npm run dev

# Visit http://localhost:3000 and check styling

Next: Create Molecules

Once atoms are solid, create molecules that use atoms:

components/molecules/
├── cards/
│   └── ProductCard.tsx  (uses Button, Text, Badge atoms)
├── form-groups/
│   └── FormField.tsx    (uses TextInput, Text atoms)
├── ratings/
│   └── RatingInput.tsx  (uses Button, Caption atoms)
└── search-bar/
    └── SearchBar.tsx    (uses SearchInput, Button atoms)

Each molecule = 2-3 atoms combined + simple state


Checklist

  • Created components/atoms/buttons/
  • Created components/atoms/inputs/
  • Created components/atoms/typography/
  • Created components/atoms/badges/
  • Created components/atoms/dividers/
  • Created components/atoms/index.ts (barrel export)
  • Updated at least 1 component to use atoms
  • Run npm run lint - no errors
  • Run npm run format - code formatted
  • Test in browser - styling looks correct

💡 Tips

  1. Start small: Just do atoms. Don't worry about molecules yet.
  2. Use consistently: Once Button atom exists, never write <button> again.
  3. Expand gradually: Add more atoms/molecules as you need them.
  4. Keep props simple: Atoms should have 5-10 props max.
  5. Test variants: Make sure all variants (primary, secondary, etc.) look good.

🚀 You're Ready!

You now have the foundation. Everything else builds on these atoms. Nice work!

Next: Once atoms are comfortable, read the full OPTIMIZATION_PLAN.md for molecules and organisms.