Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
169 lines
6.6 KiB
TypeScript
169 lines
6.6 KiB
TypeScript
import { useState, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { Badge, Checkbox, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
|
import { StateBoard } from "@/components/story-board"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Table is a static display primitive, so the representative Story is a board of specimens. Checkbox and Badge appear only as
|
|
// cell content (Story composition); Table has no dependency on them and no selection, sorting or paging behavior.
|
|
// The Story keeps the selected rows in local state and passes `data-state="selected"` itself.
|
|
|
|
const projects = [
|
|
{ id: "P-001", name: "한맥 노선 정비", status: "진행", amount: 1280000 },
|
|
{ id: "P-002", name: "시설물 점검 리포트", status: "완료", amount: 450000 },
|
|
{ id: "P-003", name: "교량 안전 진단", status: "대기", amount: 9800000 },
|
|
]
|
|
const won = (n: number) => n.toLocaleString("ko-KR")
|
|
|
|
// `showCaption={false}` drops the visible caption on the representative page; the caption text becomes the table's
|
|
// accessible name. The verification Story keeps the TableCaption.
|
|
function BasicTable({ showCaption = true }: { showCaption?: boolean }) {
|
|
return (
|
|
<Table aria-label={showCaption ? undefined : "프로젝트별 계약 금액(원)"}>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>번호</TableHead>
|
|
<TableHead>프로젝트</TableHead>
|
|
<TableHead>상태</TableHead>
|
|
<TableHead className="text-right">금액</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{projects.map((p) => (
|
|
<TableRow key={p.id}>
|
|
<TableCell>{p.id}</TableCell>
|
|
<TableCell>{p.name}</TableCell>
|
|
<TableCell>
|
|
<Badge>{p.status}</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right">{won(p.amount)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
<TableFooter>
|
|
<TableRow>
|
|
<TableCell colSpan={3}>합계</TableCell>
|
|
<TableCell className="text-right">{won(projects.reduce((s, p) => s + p.amount, 0))}</TableCell>
|
|
</TableRow>
|
|
</TableFooter>
|
|
{showCaption ? <TableCaption>프로젝트별 계약 금액(원)</TableCaption> : null}
|
|
</Table>
|
|
)
|
|
}
|
|
|
|
function SelectableTable() {
|
|
const [selected, setSelected] = useState<string[]>(["P-002"])
|
|
const toggle = (id: string) => setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]))
|
|
return (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-10">
|
|
<Checkbox
|
|
aria-label="전체 선택"
|
|
checked={selected.length === projects.length ? true : selected.length ? "indeterminate" : false}
|
|
onCheckedChange={(v) => setSelected(v ? projects.map((p) => p.id) : [])}
|
|
/>
|
|
</TableHead>
|
|
<TableHead>프로젝트</TableHead>
|
|
<TableHead className="text-right">금액</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{projects.map((p) => (
|
|
<TableRow key={p.id} data-state={selected.includes(p.id) ? "selected" : undefined}>
|
|
<TableCell>
|
|
<Checkbox aria-label={`${p.name} 선택`} checked={selected.includes(p.id)} onCheckedChange={() => toggle(p.id)} />
|
|
</TableCell>
|
|
<TableCell>{p.name}</TableCell>
|
|
<TableCell className="text-right">{won(p.amount)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)
|
|
}
|
|
|
|
function LongTextTable() {
|
|
return (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>제목</TableHead>
|
|
<TableHead>비고</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell>긴 텍스트는 셀 안에서 줄바꿈되고 행 높이만 늘어납니다. 공백 없는 문자열도 열 폭을 밀어내지 않습니다.</TableCell>
|
|
<TableCell>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>짧은 행</TableCell>
|
|
<TableCell>-</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
)
|
|
}
|
|
|
|
function WideTable() {
|
|
const cols = Array.from({ length: 10 }, (_, i) => `컬럼 ${i + 1}`)
|
|
return (
|
|
<Table style={{ minWidth: 1100 }}>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{cols.map((c) => (
|
|
<TableHead key={c}>{c}</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Array.from({ length: 3 }, (_, r) => (
|
|
<TableRow key={r}>
|
|
{cols.map((c) => (
|
|
<TableCell key={c}>{`값 ${r + 1}-${c.slice(3)}`}</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)
|
|
}
|
|
|
|
const box = (node: ReactNode) => <div style={{ width: "100%", maxWidth: 720, minWidth: 0 }}>{node}</div>
|
|
|
|
const sections = [
|
|
{ id: "basic", label: "Basic", node: <BasicTable showCaption={false} /> },
|
|
{ id: "selected", label: "Selected row", node: <SelectableTable /> },
|
|
{ id: "long", label: "Long text", node: <LongTextTable /> },
|
|
{ id: "wide", label: "Horizontal overflow", node: <WideTable /> },
|
|
]
|
|
|
|
const meta = { title: "Components/Data & Display/Table", component: Table, parameters: { layout: "padded" } } satisfies Meta<typeof Table>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
const board = (...ids: string[]) => (
|
|
<StateBoard min={460} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, wide: true, children: box(s.node) }))} />
|
|
)
|
|
|
|
// Representative Story: the common Story page; the sections group the same specimens.
|
|
export const All: Story = {
|
|
name: "Table",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Table" />
|
|
<StorySection title="Overview">{board("basic")}</StorySection>
|
|
<StorySection title="States">{board("selected")}</StorySection>
|
|
<StorySection title="Usage">{board("long", "wide")}</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const Basic: Story = { tags: ["verification"], render: () => box(<BasicTable />) }
|
|
export const Selected: Story = { tags: ["verification"], render: () => box(<SelectableTable />) }
|
|
export const LongText: Story = { tags: ["verification"], name: "Long Text", render: () => box(<LongTextTable />) }
|
|
export const Overflow: Story = { tags: ["verification"], name: "Horizontal Overflow", render: () => box(<WideTable />) }
|