Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
@@ -0,0 +1,140 @@
|
||||
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>
|
||||
),
|
||||
}
|
||||
Reference in new issue
Block a user