Initial commit: HMDesignSystem baseline

This commit is contained in:
shkim4 committed 2026-10-08 16:58:09 +09:00
commit b6a0a61103
486 files changed
+68432

No files matched your search

+140
View File
@@ -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>
),
}