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>
579 lines
14 KiB
Markdown
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.
|