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.
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
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>
|
||
),
|
||
}
|