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

579 lines
14 KiB
Markdown

# 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
```bash
mkdir -p components/atoms/buttons
```
### Create `components/atoms/buttons/Button.types.ts`
```typescript
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`
```typescript
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`
```typescript
export { default as Button } from "./Button";
export type { ButtonProps } from "./Button.types";
```
### Test it
```tsx
// 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
```bash
mkdir -p components/atoms/inputs
```
### Create `components/atoms/inputs/Input.types.ts`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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
```bash
mkdir -p components/atoms/typography
```
### Create `components/atoms/typography/Typography.types.ts`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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`
```typescript
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
```bash
mkdir -p components/atoms/badges
```
### Create `components/atoms/badges/Badge.types.ts`
```typescript
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`
```typescript
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`
```typescript
export { default as Badge } from "./Badge";
export type { BadgeProps } from "./Badge.types";
```
---
## Step 5: Create Divider Atom (5 minutes)
### Create directory
```bash
mkdir -p components/atoms/dividers
```
### Create `components/atoms/dividers/Divider.tsx`
```typescript
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`
```typescript
// 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)
```tsx
// 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)
```tsx
// components/CartProduct.tsx
import { Button } from "@/components/atoms";
<Button variant="primary" size="sm" icon="fa-cart-plus">
Mua
</Button>;
```
### Before (inline)
```tsx
// 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)
```tsx
// 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
```bash
# 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.