Files
HMDesign/src/components/sonner.stories.tsx
T

141 lines
6.2 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 { STORY_KICKER, 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",
noteAfter: true,
note: "Default / Success / Error / Warning / Info / Loading. Only Error has its own color; the other types are neutral (status colors are undecided).",
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",
noteAfter: true,
note: "Loading, then Success or Error (local timeout, no network).",
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
kicker={STORY_KICKER.components}
title="Sonner"
description="작업 결과를 잠시 띄우는 Toast 알림입니다. 실제 Sonner API(toast)로 만들며, Toaster는 앱 루트에 한 번 둡니다."
/>
<StorySection title="Variants" description="버튼을 눌러 각 유형의 Toast를 띄웁니다.">
<Board ids={["types"]} />
</StorySection>
<StorySection title="Usage" description="Action / Cancel / Close, Promise, 여러 개 쌓기와 긴 메시지입니다.">
<Board ids={["action", "promise", "stack"]} />
</StorySection>
</StoryPage>
),
}