159 lines
6.0 KiB
TypeScript
159 lines
6.0 KiB
TypeScript
import { Fragment, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbEllipsis,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
BreadcrumbList,
|
|
BreadcrumbPage,
|
|
BreadcrumbSeparator,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@hm-design/ui"
|
|
import { StateBoard, type BoardState } from "@/components/story-board"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// The representative "Breadcrumb" Story shows each case on one page (real links with sample hrefs); the same specimens also exist
|
|
// as Stories tagged `verification` (hidden from the sidebar). BreadcrumbEllipsis is presentational: the Dropdown Menu example is
|
|
// a consumer composition, not part of Breadcrumb.
|
|
|
|
const narrow = (children: ReactNode) => <div className="w-full max-w-xl">{children}</div>
|
|
|
|
function Basic() {
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href="#home">홈</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href="#users">사용자</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbPage>상세</BreadcrumbPage>
|
|
</BreadcrumbItem>
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
)
|
|
}
|
|
|
|
function Ellipsis() {
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href="#home">홈</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger aria-label="중간 경로 보기">
|
|
<BreadcrumbEllipsis />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
<DropdownMenuItem>문서</DropdownMenuItem>
|
|
<DropdownMenuItem>설정</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href="#project">프로젝트</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
<BreadcrumbPage>상세</BreadcrumbPage>
|
|
</BreadcrumbItem>
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
)
|
|
}
|
|
|
|
function Long() {
|
|
const labels = ["홈", "프로젝트", "한맥 프로젝트 관리 시스템", "노선 정보", "시설물 목록", "교량", "상세 정보 및 점검 이력"]
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
{labels.map((label, i) => (
|
|
<Fragment key={label}>
|
|
<BreadcrumbItem>
|
|
{i === labels.length - 1 ? <BreadcrumbPage>{label}</BreadcrumbPage> : <BreadcrumbLink href={`#${i}`}>{label}</BreadcrumbLink>}
|
|
</BreadcrumbItem>
|
|
{i < labels.length - 1 ? <BreadcrumbSeparator /> : null}
|
|
</Fragment>
|
|
))}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
)
|
|
}
|
|
|
|
// asChild: the link is any element (here a plain <a> standing in for a router Link) and keeps the Breadcrumb look.
|
|
function CustomLink() {
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink asChild>
|
|
<a href="#router-home">홈 (custom link)</a>
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
<BreadcrumbSeparator>/</BreadcrumbSeparator>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbPage>현재 페이지</BreadcrumbPage>
|
|
</BreadcrumbItem>
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
)
|
|
}
|
|
|
|
const sections = [
|
|
{ id: "basic", label: "Basic", node: <Basic /> },
|
|
{ id: "ellipsis", label: "Ellipsis", node: <Ellipsis /> },
|
|
{ id: "long", label: "Long path", node: <Long /> },
|
|
{ id: "custom", label: "Custom link · separator", node: <CustomLink /> },
|
|
]
|
|
|
|
const meta = { title: "Components/Navigation/Breadcrumb", component: Breadcrumb, parameters: { layout: "padded" } } satisfies Meta<typeof Breadcrumb>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative-page layout (Storybook presentation only): Variants use 3 equal columns on PC (iframe width 1000px and up),
|
|
// fewer below that (240px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so
|
|
// StateBoard's auto-fill grid cannot leave an empty or stretched track.
|
|
const specimen = (id: string, children?: ReactNode): BoardState => {
|
|
const s = sections.find((section) => section.id === id)!
|
|
return { id: s.id, label: s.label, children: children ?? narrow(s.node) }
|
|
}
|
|
|
|
// Representative Story: the common Story page; the sections group the same specimens.
|
|
// Overview is merged into Variants (Basic is its first item). The Long path example gets up to 900px so it fits on one line on
|
|
// PC; BreadcrumbList is `flex-wrap`, so it wraps naturally (never clipped or forced nowrap) when the space is narrower.
|
|
export const All: Story = {
|
|
name: "Breadcrumb",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Breadcrumb" />
|
|
<StorySection title="Variants">
|
|
<div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,240px),1fr))] min-[1000px]:grid-cols-3">
|
|
{["basic", "ellipsis", "custom"].map((id) => (
|
|
<StateBoard key={id} min={10000} states={[specimen(id)]} />
|
|
))}
|
|
</div>
|
|
</StorySection>
|
|
<StorySection title="Usage">
|
|
<StateBoard min={10000} states={[specimen("long", <div className="w-full max-w-[900px]">{sections.find((s) => s.id === "long")!.node}</div>)]} />
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const Basic3: Story = { tags: ["verification"], name: "Basic", render: () => <Basic /> }
|
|
export const WithEllipsis: Story = { tags: ["verification"], name: "Ellipsis", render: () => <Ellipsis /> }
|
|
export const LongPath: Story = { tags: ["verification"], name: "Long Path", render: () => narrow(<Long />) }
|