Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
@@ -0,0 +1,320 @@
|
||||
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"
|
||||
? role !== "destructive"
|
||||
: ["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는 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<typeof Button>
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const row = "flex flex-wrap items-center gap-4"
|
||||
const stack = "grid gap-6"
|
||||
const heading = "text-base font-semibold"
|
||||
const note = "text-sm text-muted-foreground"
|
||||
|
||||
function RolesBoard() {
|
||||
return (
|
||||
<div className={stack}>
|
||||
{(
|
||||
[
|
||||
["brand", "Brand"],
|
||||
["solid", "Standard Solid"],
|
||||
["outline", "Standard Outline"],
|
||||
] as const
|
||||
).map(([appearance, title]) => (
|
||||
<section key={appearance}>
|
||||
<h3 className={heading}>{title}</h3>
|
||||
<div className={`${row} mt-3`}>
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<Button key={variant} variant={variant} appearance={appearance}>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sizes = [
|
||||
["sm", "Small"],
|
||||
["default", "Medium"],
|
||||
["lg", "Large"],
|
||||
] as const
|
||||
|
||||
function SizesBoard() {
|
||||
return (
|
||||
<div className={stack}>
|
||||
{(["brand", "solid"] as const).map((appearance) => (
|
||||
<div className={row} key={appearance}>
|
||||
{sizes.map(([size, label]) => (
|
||||
<Button key={size} appearance={appearance} size={size}>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BrandStates() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<div className={stack}>
|
||||
<div className={row}>
|
||||
<Button>Default</Button>
|
||||
<Button data-sb-hover="">Hover</Button>
|
||||
<Button data-sb-active="">Pressed</Button>
|
||||
<Button onClick={() => setCount((n) => n + 1)}>Interact</Button>
|
||||
<Button disabled onClick={() => setCount((n) => n + 1)}>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<output aria-live="polite">Activated: {count}</output>
|
||||
<p className={note}>
|
||||
Hover와 Pressed는 Storybook이 고정한 상태입니다. Interact 위에서 실제 Hover·Pressed·포인터 이탈을 확인합니다. Disabled는 실행되지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="min-w-0">
|
||||
<h3 className={heading}>{title}</h3>
|
||||
<div className="mt-3 min-w-0 max-w-full overflow-x-auto">
|
||||
<table className="w-full border-collapse [&_td]:p-3 [&_th]:p-3 [&_th]:text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Role</th>
|
||||
{standardStates.map(([state]) => (
|
||||
<th scope="col" key={state}>
|
||||
{state}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<tr key={variant}>
|
||||
<th scope="row">{label}</th>
|
||||
{standardStates.map(([state, props]) => (
|
||||
<td key={state}>
|
||||
<Button variant={variant} appearance={appearance} {...props}>
|
||||
{label}
|
||||
</Button>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function StandardStatesBoard() {
|
||||
return (
|
||||
<div className={stack}>
|
||||
<StandardStateTable appearance="solid" title="Standard Solid" />
|
||||
<StandardStateTable appearance="outline" title="Standard Outline" />
|
||||
<p className={note}>
|
||||
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에서 확인합니다.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LongLabelBoard() {
|
||||
return (
|
||||
<div className="w-[180px]">
|
||||
<Button className="whitespace-normal">Apply changes to all selected project records</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DestructiveBoard() {
|
||||
return (
|
||||
<div className={row}>
|
||||
<Button variant="cancel" appearance="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" appearance="outline">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Button"
|
||||
description="사용자가 동작을 실행하는 기본 컨트롤입니다. variant는 역할(색), appearance는 Brand / Standard Solid / Standard Outline, size는 높이를 정합니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="props를 생략한 기본 Button: Brand · Primary(Green) · Medium 34px.">
|
||||
<Button>Button</Button>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="Small 28 / Medium 34 / Large 42px · 글자 12/18 · 13/20 · 15/22. 위는 Brand, 아래는 Standard Solid입니다.">
|
||||
<SizesBoard />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="appearance별로 허용된 역할 조합입니다.">
|
||||
<RolesBoard />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Default / Hover / Pressed / Disabled. Hover·Pressed는 Storybook이 고정한 상태입니다.">
|
||||
<StateBoard
|
||||
min={1200}
|
||||
states={[
|
||||
{ id: "brand-states", label: "Brand", children: <BrandStates /> },
|
||||
{ id: "standard-states", label: "Standard Solid · Standard Outline", children: <StandardStatesBoard /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// 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: () => <RolesBoard /> }
|
||||
export const Sizes: Story = { tags: ["verification"], render: () => <SizesBoard /> }
|
||||
export const States: Story = { tags: ["verification"], name: "Brand States", render: () => <BrandStates /> }
|
||||
export const StandardStates: Story = { tags: ["verification"], name: "Standard States", render: () => <StandardStatesBoard /> }
|
||||
export const LongLabel: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => <LongLabelBoard />,
|
||||
parameters: {
|
||||
docs: { description: { story: "일반 Label은 한 줄입니다. 긴 Label은 기존 className API의 whitespace-normal로 줄바꿈을 명시하며, 폭이 부족하면 높이가 늘어나고 각 줄은 가운데 정렬됩니다." } },
|
||||
},
|
||||
}
|
||||
|
||||
export const KoreanLabels: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: 16, width: 88 }}>
|
||||
{["저장", "변경사항 적용", "설정 저장", "파일 다운로드"].map((label) => <div key={label}><Button>{label}</Button></div>)}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
export const IconTextNarrow: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: 16, width: 88 }}>
|
||||
{(["sm", "default", "lg"] as const).map((size) => <div key={size}><Button size={size}><DownloadIcon />파일 다운로드</Button></div>)}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
export const Destructive: Story = {
|
||||
tags: ["verification"],
|
||||
render: () => <DestructiveBoard />,
|
||||
parameters: {
|
||||
docs: { description: { story: "검증 전용 삭제·취소 세트: Cancel은 Gray Outline, Delete는 Red Outline이며 appearance와 size를 통일합니다. Red Hover/Pressed 면은 Red 8%입니다." } },
|
||||
},
|
||||
}
|
||||
|
||||
// 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: () => (
|
||||
<div className={stack}>
|
||||
<section>
|
||||
<h2 className={heading}>shadcn compatibility</h2>
|
||||
<p className={`${note} mt-1`}>ui_viz 공식 Button 규격이 아닙니다. shadcn 기본 스타일을 그대로 씁니다.</p>
|
||||
<div className={`${row} mt-3`}>
|
||||
<Button variant="ghost">ghost</Button>
|
||||
<Button variant="link">link</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className={heading}>alias</h2>
|
||||
<p className={`${note} mt-1`}>기존 shadcn 사용 코드를 위한 별칭입니다. default는 primary, outline은 primary + outline과 같습니다.</p>
|
||||
<div className={`${row} mt-3`}>
|
||||
<Button variant="default">default</Button>
|
||||
<Button variant="outline">outline</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
Reference in new issue
Block a user