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 —
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 "