Shadcn UI คืออะไร
Shadcn UI เป็น Component Library แนวใหม่ที่ไม่ได้ติดตั้งเป็น npm package แต่ Copy Source Code ลงในโปรเจคโดยตรง ทำให้ Developer มี Full Control สามารถปรับแต่ง Component ได้ตามต้องการ สร้างบน Radix UI Primitives (Accessible) และ Tailwind CSS (Styling)
การใช้ Automation Scripts ร่วมกับ Shadcn UI ช่วยเพิ่มประสิทธิภาพในการ Setup Project, Generate Components หลายตัวพร้อมกัน, Customize Theme อัตโนมัติ และสร้าง Documentation จาก Components
เนื้อหาเกี่ยวข้อง — ทำความเข้าใจ LLM Inference vLLM Event Driven Design —
Automation Script — Setup และ Generate Components
#!/bin/bash
# shadcn-setup.sh — Automation Script สำหรับ Shadcn UI Setup
# ใช้กับ Next.js + TypeScript + Tailwind CSS
set -e
PROJECT_NAME=
echo "=== Setting up Shadcn UI Project: $PROJECT_NAME ==="
# 1. สร้าง Next.js Project
npx create-next-app@latest "$PROJECT_NAME" \
--typescript --tailwind --eslint --app \
--src-dir --import-alias "@/*" --no-git
cd "$PROJECT_NAME"
# 2. Initialize Shadcn UI
npx shadcn-ui@latest init -y \
--style default \
--base-color slate \
--css-variables
# 3. ติดตั้ง Essential Components
COMPONENTS=(
"button"
"input"
"label"
"card"
"dialog"
"dropdown-menu"
"table"
"tabs"
"toast"
"form"
"select"
"badge"
"alert"
"avatar"
"separator"
"skeleton"
"sheet"
"command"
"popover"
"tooltip"
)
echo "Installing components..."
for comp in ""; do
echo " Adding: $comp"
npx shadcn-ui@latest add "$comp" -y 2>/dev/null || echo " Skipped: $comp"
done
# 4. ติดตั้ง Dependencies เพิ่มเติม
npm install lucide-react @tanstack/react-table date-fns zod \
@hookform/resolvers react-hook-form next-themes
# 5. สร้าง Theme Provider
mkdir -p src/components
cat > src/components/theme-provider.tsx << 'THEME_EOF'
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return {children}
}
THEME_EOF
# 6. สร้าง Toaster Component
cat > src/components/toaster.tsx << 'TOAST_EOF'
"use client"
import { Toaster as SonnerToaster } from "sonner"
export function Toaster() {
return
}
TOAST_EOF
echo ""
echo "=== Setup Complete ==="
echo "Components installed: "
echo "Run: cd $PROJECT_NAME && npm run dev"
Python Script — Component Generator
# shadcn_generator.py — Generate Custom Components จาก Template
import os
import json
import subprocess
from pathlib import Path
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ComponentSpec:
name: str
description: str
props: dict
shadcn_deps: List[str]
template: str = "default"
class ShadcnComponentGenerator:
"""Generate Custom Components บน Shadcn UI"""
def __init__(self, project_path):
self.project_path = Path(project_path)
self.components_dir = self.project_path / "src" / "components"
self.ui_dir = self.components_dir / "ui"
def ensure_shadcn_deps(self, deps: List[str]):
"""ติดตั้ง Shadcn Components ที่จำเป็น"""
for dep in deps:
dep_file = self.ui_dir / f"{dep}.tsx"
if not dep_file.exists():
print(f" Installing shadcn component: {dep}")
subprocess.run(
["npx", "shadcn-ui@latest", "add", dep, "-y"],
cwd=str(self.project_path),
capture_output=True,
)
def generate_data_table(self, name="data-table", columns=None):
"""Generate Data Table Component"""
self.ensure_shadcn_deps(["table", "button", "input",
"dropdown-menu", "badge"])
component_code = '''
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Table, TableBody, TableCell, TableHead,
TableHeader, TableRow,
} from "@/components/ui/table"
import {
DropdownMenu, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
interface DataTableProps
{
columns: ColumnDef[]
data: TData[]
searchKey?: string
searchPlaceholder?: string
}
export function DataTable({
columns, data, searchKey, searchPlaceholder = "Search...",
}: DataTableProps) {
const [sorting, setSorting] = React.useState([])
const [columnFilters, setColumnFilters] =
React.useState([])
const [columnVisibility, setColumnVisibility] =
React.useState({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data, columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: { sorting, columnFilters, columnVisibility, rowSelection },
})
return (
{searchKey && (
table.getColumn(searchKey)
?.setFilterValue(e.target.value)}
className="max-w-sm"
/>
)}
{table.getAllColumns()
.filter((col) => col.getCanHide())
.map((col) => (
col.toggleVisibility(!!v)}
>
{col.id}
))}
{table.getHeaderGroups().map((hg) => (
{hg.headers.map((h) => (
{h.isPlaceholder ? null :
flexRender(h.column.columnDef.header, h.getContext())}
))}
))}
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
))}
))
) : (
No results.
)}
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
)
}
'''
output_path = self.components_dir / f"{name}.tsx"
output_path.write_text(component_code.strip(), encoding="utf-8")
print(f"Generated: {output_path}")
def generate_component_index(self):
"""สร้าง Index File สำหรับ Export ทุก Components"""
ui_files = sorted(self.ui_dir.glob("*.tsx"))
exports = []
for f in ui_files:
name = f.stem
exports.append(f'export * from "./ui/{name}"')
index_path = self.components_dir / "index.ts"
index_path.write_text("\n".join(exports), encoding="utf-8")
print(f"Generated index: {index_path} ({len(exports)} exports)")
def audit_accessibility(self):
"""ตรวจสอบ Accessibility ของ Components"""
issues = []
for f in self.ui_dir.glob("*.tsx"):
content = f.read_text(encoding="utf-8", errors="ignore")
# Check for aria labels
if "