Files
HMDesign/src/components/sonner.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

133 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Button, toast, Toaster } from "@hm-design/ui"
import { StateBoard } from "./story-board"
import { StoryPage, StoryPageHeader, StorySection } from "./story-page"
// Toast is created with the real Sonner API (`toast`, re-exported by @hm-design/ui); nothing is a static specimen.
// One <Toaster /> is mounted in .storybook/preview.tsx for every Story (an app mounts it once at its root).
const wait = (ms: number, fail?: boolean) =>
new Promise<string>((resolve, reject) => setTimeout(() => (fail ? reject(new Error("fail")) : resolve("완료")), ms))
function Trigger({ onClick, children }: { onClick: () => void; children: ReactNode }) {
return (
<Button variant="primary" appearance="outline" onClick={onClick}>
{children}
</Button>
)
}
// `ids` picks the specimens for one section of the common Story page.
function Board({ ids }: { ids: string[] }) {
return (
<StateBoard
min={220}
states={[
{
id: "types",
label: "Types",
children: (
<div className="flex flex-wrap gap-2">
<Trigger onClick={() => toast("알림이 도착했습니다", { description: "기본 알림입니다." })}>Default</Trigger>
<Trigger onClick={() => toast.success("저장했습니다", { description: "변경 사항이 저장되었습니다." })}>Success</Trigger>
<Trigger onClick={() => toast.error("저장하지 못했습니다", { description: "잠시 후 다시 시도하세요." })}>Error</Trigger>
<Trigger onClick={() => toast.warning("저장 공간이 부족합니다", { description: "불필요한 파일을 정리하세요." })}>Warning</Trigger>
<Trigger onClick={() => toast.info("새 버전이 있습니다")}>Info</Trigger>
<Trigger onClick={() => toast.loading("처리 중입니다", { duration: 4000 })}>Loading</Trigger>
</div>
),
},
{
id: "action",
label: "Action / Close",
children: (
<div className="flex flex-wrap gap-2">
<Trigger onClick={() => toast("파일을 삭제했습니다", { action: { label: "실행 취소", onClick: () => toast.success("복원했습니다") } })}>
Action
</Trigger>
<Trigger
onClick={() =>
toast("변경 사항을 적용할까요?", {
description: "취소하면 이전 상태가 유지됩니다.",
duration: Infinity,
action: { label: "적용", onClick: () => undefined },
cancel: { label: "취소", onClick: () => undefined },
})
}
>
Action + Cancel
</Trigger>
<Trigger onClick={() => toast("닫기 버튼이 있는 알림", { closeButton: true, duration: Infinity })}>Close button</Trigger>
</div>
),
},
{
id: "promise",
label: "Promise",
children: (
<div className="flex flex-wrap gap-2">
<Trigger onClick={() => toast.promise(wait(1500), { loading: "저장 중...", success: "저장했습니다", error: "저장하지 못했습니다" })}>
Promise → Success
</Trigger>
<Trigger onClick={() => toast.promise(wait(1500, true), { loading: "저장 중...", success: "저장했습니다", error: "저장하지 못했습니다" })}>
Promise → Error
</Trigger>
</div>
),
},
{
id: "stack",
label: "Stack / Long",
children: (
<div className="flex flex-wrap gap-2">
<Trigger
onClick={() => {
toast("첫 번째 알림")
toast.success("두 번째 알림")
toast.error("세 번째 알림")
}}
>
Stack ×3
</Trigger>
<Trigger
onClick={() =>
toast.info("긴 메시지 확인용 제목입니다. 좁은 화면에서도 줄바꿈되어야 합니다.", {
description: "설명도 길게 작성하면 여러 줄로 나뉘고 토스트 폭을 넘치지 않아야 합니다. 이 문장은 줄바꿈을 확인하기 위한 예시입니다.",
action: { label: "자세히 보기", onClick: () => undefined },
closeButton: true,
})
}
>
Long message
</Trigger>
<Trigger onClick={() => toast.dismiss()}>Dismiss all</Trigger>
</div>
),
},
].filter((s) => ids.includes(s.id))}
/>
)
}
const meta = { title: "Components/Feedback/Sonner", component: Toaster, parameters: { layout: "padded" } } satisfies Meta<typeof Toaster>
export default meta
type Story = StoryObj<typeof meta>
// Representative Story: buttons create real toasts through Sonner (types, action / cancel, close, promise, stack, long message).
export const All: Story = {
name: "Sonner",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title="Sonner" />
<StorySection title="Variants">
<Board ids={["types"]} />
</StorySection>
<StorySection title="Usage">
<Board ids={["action", "promise", "stack"]} />
</StorySection>
</StoryPage>
),
}