Files
HMDesign/src/components/carousel.stories.tsx
T
shkim4 4c29894e56 refactor(storybook): simplify component and preview examples
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.
2026-10-23 00:28:05 +09:00

167 lines
6.3 KiB
TypeScript

import { useEffect, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { AspectRatio, Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// The representative Story shows real, draggable carousels; the same specimens also exist as Stories tagged `verification`
// (hidden from the sidebar). Slide widths (`basis-*`) are the consumer's `className` on CarouselItem; Card and AspectRatio are
// Story composition (Carousel imports neither). `setApi` feeds the "slide n / total" readout.
const frame = (node: ReactNode) => <div style={{ width: "100%", maxWidth: 420, minWidth: 0 }}>{node}</div>
const svg = (n: number) =>
"data:image/svg+xml;utf8," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="480" height="270"><rect width="480" height="270" fill="${["#266651", "#3e3523", "#205241", "#604f32", "#123328"][n % 5]}"/><text x="24" y="52" font-family="sans-serif" font-size="34" fill="#ffffff">Slide ${n + 1}</text></svg>`
)
function SlideCard({ n }: { n: number }) {
return (
<Card>
<CardContent className="grid h-24 place-items-center text-2xl font-bold">{n + 1}</CardContent>
</Card>
)
}
function useReadout() {
const [api, setApi] = useState<CarouselApi>()
const [current, setCurrent] = useState(0)
const [count, setCount] = useState(0)
useEffect(() => {
if (!api) return
setCount(api.scrollSnapList().length)
setCurrent(api.selectedScrollSnap() + 1)
api.on("select", () => setCurrent(api.selectedScrollSnap() + 1))
}, [api])
return { setApi, current, count }
}
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
function Basic({ showValue = true }: { showValue?: boolean }) {
const { setApi, current, count } = useReadout()
return frame(
<div className="grid gap-2">
<Carousel setApi={setApi} aria-label="기본 carousel">
<CarouselContent>
{Array.from({ length: 5 }, (_, i) => (
<CarouselItem key={i}>
<SlideCard n={i} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
{showValue ? <span className="text-center text-sm text-muted-foreground" data-testid="readout">{`slide ${current} / ${count}`}</span> : null}
</div>
)
}
function Multiple() {
return frame(
<Carousel opts={{ align: "start" }} aria-label="여러 항목 carousel">
<CarouselContent>
{Array.from({ length: 8 }, (_, i) => (
<CarouselItem key={i} className="basis-1/2">
<SlideCard n={i} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
function Loop({ showValue = true }: { showValue?: boolean }) {
const { setApi, current, count } = useReadout()
return frame(
<div className="grid gap-2">
<Carousel opts={{ loop: true }} setApi={setApi} aria-label="loop carousel">
<CarouselContent>
{Array.from({ length: 3 }, (_, i) => (
<CarouselItem key={i}>
<SlideCard n={i} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
{showValue ? <span className="text-center text-sm text-muted-foreground" data-testid="loop-readout">{`slide ${current} / ${count}`}</span> : null}
</div>
)
}
function Vertical() {
return (
<div style={{ width: 220, height: 260 }}>
<Carousel orientation="vertical" opts={{ align: "start" }} aria-label="세로 carousel" className="h-full">
<CarouselContent style={{ height: 260 }}>
{Array.from({ length: 4 }, (_, i) => (
<CarouselItem key={i} className="basis-1/2">
<SlideCard n={i} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
)
}
function Media() {
return frame(
<Carousel aria-label="미디어 carousel">
<CarouselContent>
{Array.from({ length: 4 }, (_, i) => (
<CarouselItem key={i}>
<AspectRatio ratio={16 / 9}>
<img src={svg(i)} alt={`슬라이드 ${i + 1}`} className="size-full rounded-md object-cover" />
</AspectRatio>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
)
}
const sections = [
{ id: "basic", label: "Basic", node: <Basic showValue={false} /> },
{ id: "multiple", label: "Multiple items", node: <Multiple /> },
{ id: "loop", label: "Loop", node: <Loop showValue={false} /> },
{ id: "vertical", label: "Vertical", node: <Vertical /> },
{ id: "media", label: "Media", node: <Media /> },
]
const meta = { title: "Components/Data & Display/Carousel", component: Carousel, parameters: { layout: "padded" } } satisfies Meta<typeof Carousel>
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, children: s.node }))} />
)
// Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = {
name: "Carousel",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title="Carousel" />
<StorySection title="Overview">{board("basic")}</StorySection>
<StorySection title="Variants">{board("multiple", "loop", "vertical")}</StorySection>
<StorySection title="Usage">{board("media")}</StorySection>
</StoryPage>
),
}
export const BasicCarousel: Story = { tags: ["verification"], name: "Basic", render: () => <Basic /> }
export const MultipleItems: Story = { tags: ["verification"], name: "Multiple Items", render: () => <Multiple /> }
export const LoopCarousel: Story = { tags: ["verification"], name: "Loop", render: () => <Loop /> }
export const VerticalCarousel: Story = { tags: ["verification"], name: "Vertical", render: () => <Vertical /> }