Trái tim của Monorepo chính là thư mục packages/ui. Đây là nơi chứa Design System của riêng tổ chức bạn.
1. Nguyên tắc Atomic Design
Chúng ta chia UI thành các tầng:
- Atoms: Button, Input, Badge, Icon. (Không logic).
- Molecules: SearchBar (Input + Icon), FormField (Label + Input + Error).
- Organisms: DataTable, Header, Sidebar.
2. Implementation: Atomic Components
Sau khi đã có Tokens, việc cài đặt Component trở nên cực kỳ "Clean". Chúng ta chỉ việc mapping tokens vào Tailwind Classes.
2.1. Button Component (Atom)
Button là component phức tạp nhất vì có nhiều biến thể (Variant) và trạng thái (State).
Yêu cầu kỹ thuật:
- Sử dụng
bg-primary,bg-destructivecho các variant. - Sử dụng
ringtoken cho trạng thái:focus-visible(Accessibility). - Hỗ trợ
disabledstate (dùngopacity-50).
// packages/ui/components/button.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../lib/utils"
const buttonVariants = cva(
// Base classes: Flexbox, Focus Ring using Token, Disabled state
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }Live Preview (HTML Render):
Note: Playground bên dưới hiển thị kết quả HTML sau khi React render. Mã nguồn thực tế bạn sử dụng là React Component ở trên.
2.2. Card Component (Molecule)
Card sử dụng các token bg-card, text-card-foreground và border để tạo khối nội dung tách biệt.
// packages/ui/components/card.tsx
import * as React from "react"
import { cn } from "../lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
)
Card.displayName = "Card"
// ... (CardHeader, CardTitle, CardContent implementation)
export { Card }Live Preview (HTML Render):
2.3. Badge Component (Atom)
Badge cực kỳ quan trọng cho Admin Dashboard để hiển thị trạng thái (Status): Pending, Paid, Failed.
// packages/ui/components/badge.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }Live Preview (HTML Render):
2.4. Form Inputs (Atom/Molecule)
Input và Textarea dùng chung token border-input và ring để tạo focus state đồng nhất.
// packages/ui/components/input.tsx
import * as React from "react"
import { cn } from "../lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }Live Preview (HTML Render):
2.5. Data Table (Organism)
Table là thành phần quan trọng nhất của Admin Dashboard.
// packages/ui/components/table.tsx
// (Simplified structure)
import * as React from "react"
import { cn } from "../lib/utils"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
))
Table.displayName = "Table"
// ... Header, Body, Row, Cell implementations using border-b tokenLive Preview (HTML Render):
2.6. Avatar Component (Atom)
Hiển thị ảnh đại diện người dùng, tự động fallback về chữ cái đầu nếu ảnh lỗi.
// packages/ui/components/avatar.tsx
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "../lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
// ... Image and Fallback componentsLive Preview (HTML Render):
2.7. Dropdown Menu (Molecule)
Menu thả xuống cho User actions hoặc Table options.
// packages/ui/components/dropdown.tsx
// Wrapper around @radix-ui/react-dropdown-menu
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
// Implementation details omitted for brevityLive Preview (HTML Render):
2.8. Dialog / Modal (Organism)
Sử dụng cho các form tạo mới nhanh hoặc xác nhận xóa.
// packages/ui/components/dialog.tsx
import * as DialogPrimitive from "@radix-ui/react-dialog"
// ...Live Preview (HTML Render):
2.9. Tabs (Molecule)
Chuyển đổi giữa các view (VD: Account / Password settings).
// packages/ui/components/tabs.tsx
import * as TabsPrimitive from "@radix-ui/react-tabs"
// ...Live Preview (HTML Render):
2.10. Select Component (Molecule)
Component chọn giá trị từ danh sách, thay thế cho native select để có UI đồng nhất.
// packages/ui/components/select.tsx
import * as SelectPrimitive from "@radix-ui/react-select"
// ...Live Preview (HTML Render):
2.11. Checkbox Component (Atom)
Sử dụng cho các lựa chọn boolean hoặc chọn nhiều item trong danh sách.
// packages/ui/components/checkbox.tsx
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { cn } from "../lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><polyline points="20 6 9 17 4 12"></polyline></svg>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayNameLive Preview (HTML Render):
2.12. Radio Group (Molecule)
Sử dụng khi người dùng chỉ được chọn 1 sự lựa chọn trong danh sách.
// packages/ui/components/radio-group.tsx
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
// ...Live Preview (HTML Render):
2.13. Tooltip (Atom)
Hiển thị thông tin bổ sung khi hover vào element.
// packages/ui/components/tooltip.tsx
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
// ...Live Preview (HTML Render):
2.14. Alert (Molecule)
Hiển thị các thông báo quan trọng cho người dùng.
// packages/ui/components/alert.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)Live Preview (HTML Render):
3. Cách sử dụng ở App
Tại apps/store/page.tsx, developer chỉ cần import component đã được đóng gói.
import { Button } from "@repo/ui/button";4. Lợi ích
- Consistency: Mọi nút bấm trên Store và Admin đều giống hệt nhau về padding, animation và focus state.
- Dark Mode Ready: Nhờ dùng Token (
bg-card,bg-primary), các component này tự động đẹp khi chuyển sang Dark Mode mà không cần sửa code.