Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
@@ -0,0 +1,207 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Combobox,
|
||||
DataTable,
|
||||
DataTableColumnHeader,
|
||||
DataTableViewOptions,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
FilterBar,
|
||||
FilterBarActions,
|
||||
FilterBarSearch,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
dataTableSelectColumn,
|
||||
toast,
|
||||
useDataTable,
|
||||
type DataTableColumnDef,
|
||||
} from "@hm-design/ui"
|
||||
import { MoreHorizontalIcon } from "lucide-react"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { formatDate, sameDay, sampleUsers, statusLabel, type SampleUser } from "./sample-users"
|
||||
|
||||
// Data Table is a pattern: the HM Table primitive driven by TanStack Table v9 (useDataTable). Columns and data stay referentially stable
|
||||
// (module scope / useMemo). Row actions use the HM Dropdown Menu and report through Sonner `toast` (Story composition only).
|
||||
|
||||
const statusBadge = (status: SampleUser["status"]) => (
|
||||
<Badge variant={status === "active" ? "default" : status === "pending" ? "secondary" : "outline"}>{statusLabel[status]}</Badge>
|
||||
)
|
||||
|
||||
const columnLabels = { name: "이름", department: "부서", status: "상태", createdAt: "등록일" }
|
||||
|
||||
const columns: DataTableColumnDef<SampleUser>[] = [
|
||||
dataTableSelectColumn<SampleUser>(),
|
||||
{ accessorKey: "name", header: ({ column }) => <DataTableColumnHeader column={column} title="이름" /> },
|
||||
{ accessorKey: "department", header: "부서" },
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "상태",
|
||||
cell: ({ row }) => statusBadge(row.original.status),
|
||||
filterFn: "equalsString",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="등록일" />,
|
||||
cell: ({ row }) => formatDate(row.original.createdAt),
|
||||
filterFn: (row, columnId, value: Date) => sameDay(row.getValue<string>(columnId), value),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="primary" appearance="outline" size="icon-sm" aria-label={`${row.original.name} 작업`}>
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 보기`)}>보기</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => toast(`${row.original.name} 수정`)}>수정</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onSelect={() => toast.error(`${row.original.name} 삭제 요청`)}>
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function Basic({ data = sampleUsers, loading }: { data?: SampleUser[]; loading?: boolean }) {
|
||||
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
|
||||
return (
|
||||
<div className="grid w-full gap-3">
|
||||
<div className="flex justify-end">
|
||||
<DataTableViewOptions table={table} labels={columnLabels} />
|
||||
</div>
|
||||
<DataTable table={table} loading={loading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "active", label: "활성" },
|
||||
{ value: "pending", label: "대기" },
|
||||
{ value: "inactive", label: "비활성" },
|
||||
]
|
||||
const departmentOptions = [...new Set(sampleUsers.map((u) => u.department))].map((d) => ({ value: d, label: d }))
|
||||
|
||||
// Filter Bar → filter state → Data Table: search is the TanStack global filter, status / date are column filters.
|
||||
function WithFilterBar() {
|
||||
const data = useMemo(() => sampleUsers, [])
|
||||
const table = useDataTable({ columns, data, getRowId: (row) => row.id })
|
||||
const [search, setSearch] = useState("")
|
||||
const [status, setStatus] = useState("all")
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
const [department, setDepartment] = useState<string | undefined>()
|
||||
|
||||
const setSearchValue = (value: string) => {
|
||||
setSearch(value)
|
||||
table.setGlobalFilter(value || undefined)
|
||||
}
|
||||
const reset = () => {
|
||||
setSearch("")
|
||||
setStatus("all")
|
||||
setDate(undefined)
|
||||
setDepartment(undefined)
|
||||
table.resetGlobalFilter(true)
|
||||
table.resetColumnFilters(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid w-full gap-4">
|
||||
<FilterBar>
|
||||
<FilterBarSearch value={search} onValueChange={setSearchValue} placeholder="이름·부서 검색" />
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(value) => {
|
||||
setStatus(value)
|
||||
table.getColumn("status")?.setFilterValue(value === "all" ? undefined : value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="medium" aria-label="상태">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 상태</SelectItem>
|
||||
{statusOptions.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Combobox
|
||||
controlled
|
||||
size="medium"
|
||||
aria-label="부서"
|
||||
options={departmentOptions}
|
||||
value={department}
|
||||
placeholder="부서"
|
||||
onValueChange={(value) => {
|
||||
setDepartment(value)
|
||||
setSearchValue(value)
|
||||
}}
|
||||
/>
|
||||
<DatePicker
|
||||
controlled
|
||||
size="medium"
|
||||
aria-label="등록일"
|
||||
placeholder="등록일"
|
||||
value={date}
|
||||
onValueChange={(value) => {
|
||||
setDate(value)
|
||||
table.getColumn("createdAt")?.setFilterValue(value)
|
||||
}}
|
||||
/>
|
||||
<FilterBarActions>
|
||||
<Button type="button" variant="cancel" appearance="outline" onClick={reset}>
|
||||
초기화
|
||||
</Button>
|
||||
<DataTableViewOptions table={table} labels={columnLabels} />
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = { title: "Patterns/Data/Data Table", parameters: { layout: "padded", controls: { disable: true } } } satisfies Meta
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Representative Story: selection (mixed header), sorting, column visibility, row actions and pagination with page size.
|
||||
export const All: Story = {
|
||||
name: "Data Table",
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Data Table"
|
||||
description="업무 목록을 표로 보여 주는 Pattern입니다. 선택·정렬·열 표시·페이지 이동을 하나의 표에서 다룹니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="행 선택(헤더 혼합 상태), 정렬, 열 표시, 행 작업, 페이지 크기와 페이지 이동을 실제로 조작합니다.">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// Combination Story: Filter Bar (search, status, department, date, reset) filtering the Data Table rows.
|
||||
export const WithFilterBarStory: Story = { tags: ["verification"], name: "With Filter Bar", render: () => <WithFilterBar /> }
|
||||
|
||||
export const Loading: Story = { tags: ["verification"], render: () => <Basic loading /> }
|
||||
export const Empty: Story = { tags: ["verification"], render: () => <Basic data={[]} /> }
|
||||
Reference in new issue
Block a user