import { useState } from "react" import { DownloadIcon } from "lucide-react" import type { Meta, StoryObj } from "@storybook/react-vite" import { Button, type ButtonAppearance } from "@hm-design/ui" import { StateBoard } from "@/components/story-board" import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // Story structure follows ui_viz (implementations/web/storybook/Button.stories.jsx): // one representative "Button" Story shows kinds, sizes and states on a single screen; the same sections also exist // as separate Stories tagged `verification`, which the sidebar hides by default (see .storybook/main.ts). // Playground and Compatibility are HM-only and come after the ui_viz Stories. type Role = "primary" | "secondary" | "modify" | "destructive" | "cancel" // [role, label]. The gallery shows supported role/treatment combinations; usage sets are documented separately. const roles: [Role, string][] = [ ["primary", "Green"], ["secondary", "Brown"], ["modify", "Orange"], ["cancel", "Gray"], ["destructive", "Red"], ] const rolesFor = (appearance: ButtonAppearance) => roles.filter(([role]) => appearance === "brand" ? ["primary", "secondary", "modify"].includes(role) : appearance === "solid" ? true : ["primary", "secondary", "destructive", "cancel"].includes(role), ) const meta = { title: "Components/Form/Button", component: Button, parameters: { layout: "padded", docs: { description: { component: "ui_viz docs/components/button.md D-087: Brand Gradient와 Standard Solid/Outline. 크기 28/34/42, 글자 12/18 · 13/20 · 15/22, Brand 700 · Standard 500. Focus 시각 값은 ui_viz에서 전역 TBD이므로 shadcn 기본 focus 동작을 그대로 사용하며, Tab 키로 확인합니다.", }, }, }, argTypes: { variant: { control: "select", options: roles.map(([role]) => role), description: "역할(Role): primary Green, secondary Brown, modify Orange, destructive Red, cancel Gray. destructive는 solid/outline, modify는 brand/solid만, cancel은 solid/outline만 지원합니다. docs/components/button.md §6.1 참조.", }, appearance: { control: "inline-radio", options: ["brand", "solid", "outline"], description: "Treatment 기본값은 brand. Controls는 Role별 허용 조합을 제한하지 않습니다. destructive/cancel은 허용 appearance를 명시합니다. docs/components/button.md §6.1~6.2 참조.", }, size: { control: "inline-radio", options: ["sm", "default", "lg"], description: "sm은 Small 28px, default는 Medium 34px, lg는 Large 42px.", }, disabled: { control: "boolean" }, children: { control: "text" }, asChild: { table: { disable: true } }, className: { table: { disable: true } }, }, } satisfies Meta export default meta type Story = StoryObj const row = "flex flex-wrap items-center gap-4" const stack = "grid gap-6" const heading = "text-base font-bold" const note = "text-sm text-muted-foreground" function RolesBoard() { return (
{( [ ["brand", "Brand"], ["solid", "Standard Solid"], ["outline", "Standard Outline"], ] as const ).map(([appearance, title]) => (

{title}

{rolesFor(appearance).map(([variant, label]) => ( ))}
))}
) } const sizes = [ ["sm", "Small"], ["default", "Medium"], ["lg", "Large"], ] as const function SizesBoard() { return (
{(["brand", "solid"] as const).map((appearance) => (
{sizes.map(([size, label]) => ( ))}
))}
) } function BrandStates() { const [count, setCount] = useState(0) return (
Activated: {count}

Hover와 Pressed는 Storybook이 고정한 상태입니다. Interact 위에서 실제 Hover·Pressed·포인터 이탈을 확인합니다. Disabled는 실행되지 않습니다.

) } const standardStates = [ ["Default", {}], ["Hover", { "data-sb-hover": "" }], ["Pressed", { "data-sb-active": "" }], ["Disabled", { disabled: true }], ] as const function StandardStateTable({ appearance, title }: { appearance: "solid" | "outline"; title: string }) { return (

{title}

{standardStates.map(([state]) => ( ))} {rolesFor(appearance).map(([variant, label]) => ( {standardStates.map(([state, props]) => ( ))} ))}
Role {state}
{label}
) } function StandardStatesBoard() { return (

Standard Solid의 Hover·Pressed는 역할 면 위에 Black 20%를 합성합니다(Pressed = Hover). Green·Brown Outline은 Hover에서 역할색 면 + White 라벨, Pressed에서 White 면 + Black 20%입니다. Red Outline은 Hover·Pressed 모두 Red 8% 면이며 Red 라벨·보더를 유지합니다. Disabled는 Default 모양 그대로 opacity 60%이고 상태 면을 적용하지 않습니다. Default 열의 버튼으로 실제 포인터 상태도 확인할 수 있습니다. Brand 상태는 Brand States에서 확인합니다.

) } function LongLabelBoard() { return (
) } function DestructiveBoard() { return (
) } // Representative Story: the common Story page (Overview → Sizes → Variants → States) with the ui_viz Button "All" boards. // Named after the component, as in ui_viz. It is the only Button entry in the default sidebar, so Storybook shows Button as a single 1-depth item. export const All: Story = { name: "Button", parameters: { layout: "fullscreen" }, render: () => ( }, { id: "standard-states", label: "Standard Solid · Standard Outline", children: }, ]} /> ), } // Verification Stories: the same sections as separate Stories, hidden from the sidebar (tag `verification`), reachable by URL or the sidebar tag filter. export const Roles: Story = { tags: ["verification"], render: () => } export const Sizes: Story = { tags: ["verification"], render: () => } export const States: Story = { tags: ["verification"], name: "Brand States", render: () => } export const StandardStates: Story = { tags: ["verification"], name: "Standard States", render: () => } export const LongLabel: Story = { tags: ["verification"], render: () => , parameters: { docs: { description: { story: "일반 Label은 한 줄입니다. 긴 Label은 기존 className API의 whitespace-normal로 줄바꿈을 명시하며, 폭이 부족하면 높이가 늘어나고 각 줄은 가운데 정렬됩니다." } }, }, } export const KoreanLabels: Story = { tags: ["verification"], render: () => (
{["저장", "변경사항 적용", "설정 저장", "파일 다운로드"].map((label) =>
)}
), } export const IconTextNarrow: Story = { tags: ["verification"], render: () => (
{(["sm", "default", "lg"] as const).map((size) =>
)}
), } export const Destructive: Story = { tags: ["verification"], render: () => , parameters: { docs: { description: { story: "검증 전용 삭제·취소 세트: Cancel은 Gray Solid, Delete는 Red Solid이며 appearance와 size를 통일합니다. Solid Hover/Pressed는 기존 Black 20% overlay이며 Red 토큰을 사용합니다." } }, }, } // HM only: not part of the ui_viz Button Stories. Tagged verification so the default sidebar keeps only the representative Story. export const Playground: Story = { tags: ["verification"], args: { children: "저장", variant: "primary", appearance: "brand", size: "default", disabled: false }, parameters: { docs: { description: { story: "검증 전용 Controls는 역할과 Treatment를 독립적으로 선택하므로 미지원 조합도 선택할 수 있습니다. 대표 Button의 Variants와 docs/components/button.md §6.1의 허용 조합만 사용하세요. Focus는 ui_viz에서 TBD이므로 Tab 키로 shadcn 기본 focus 동작을 확인합니다." } }, }, } export const Compatibility: Story = { tags: ["verification"], render: () => (

shadcn compatibility

ui_viz 공식 Button 규격이 아닙니다. shadcn 기본 스타일을 그대로 씁니다.

alias

기존 shadcn 사용 코드를 위한 별칭입니다. default는 primary, outline은 primary + outline과 같습니다.

), }