eca619b79a
✨ 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>
14 KiB
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
- Start small: Just do atoms. Don't worry about molecules yet.
- Use consistently: Once Button atom exists, never write
<button>again. - Expand gradually: Add more atoms/molecules as you need them.
- Keep props simple: Atoms should have 5-10 props max.
- 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.