Files
HMDesign/src/components/combobox.stories.tsx
T
shkim4 4c29894e56 refactor(storybook): simplify component and preview examples
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.
2026-10-23 00:28:05 +09:00

153 lines
6.1 KiB
TypeScript

import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Combobox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, type ComboboxOption } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Combobox is a Popover + Command composition (search + single choice); Select stays for short lists without search.
// The representative Story shows real comboboxes; the same specimens also exist as Stories tagged `verification` (hidden from the
// sidebar). The Label is Story composition.
const frameworks: ComboboxOption[] = [
{ value: "next", label: "Next.js" },
{ value: "sveltekit", label: "SvelteKit" },
{ value: "nuxt", label: "Nuxt" },
{ value: "remix", label: "Remix", disabled: true },
{ value: "astro", label: "Astro" },
]
const longOptions: ComboboxOption[] = [
{ value: "a", label: "긴 옵션 이름은 한 줄에서 말줄임으로 잘리고 trigger 폭을 넘지 않습니다. 끝까지 읽을 수 있어야 하면 폭을 넓히세요" },
{ value: "b", label: "짧은 옵션" },
]
function Field({ label, id, children }: { label: string; id: string; children: ReactNode }) {
return (
<div className="grid max-w-xs gap-2">
<label htmlFor={id} className="justify-self-start text-sm font-medium text-muted-foreground">
{label}
</label>
{children}
</div>
)
}
function Empty() {
return (
<Field label="프레임워크" id="cb-empty">
<Combobox id="cb-empty" options={frameworks} placeholder="프레임워크 선택" searchPlaceholder="프레임워크 검색..." />
</Field>
)
}
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
function Controlled({ showValue = true }: { showValue?: boolean }) {
const [value, setValue] = useState("nuxt")
return (
<div className="grid gap-2">
<Field label="선택됨 (controlled)" id="cb-controlled">
<Combobox id="cb-controlled" controlled options={frameworks} value={value} onValueChange={setValue} />
</Field>
{showValue ? <span className="text-sm text-muted-foreground" data-testid="controlled">{`value = ${value}`}</span> : null}
</div>
)
}
function Medium() {
return (
<Field label="Medium" id="cb-medium">
<Combobox id="cb-medium" size="medium" options={frameworks} defaultValue="astro" />
</Field>
)
}
function LongLabel() {
return (
<Field label="긴 옵션" id="cb-long">
<Combobox id="cb-long" options={longOptions} defaultValue="a" />
</Field>
)
}
function Disabled() {
return (
<Field label="비활성" id="cb-disabled">
<Combobox id="cb-disabled" options={frameworks} defaultValue="next" disabled />
</Field>
)
}
function SelectComparison() {
return (
<div className="grid gap-6 sm:grid-cols-2">
{(["medium", "large"] as const).map((size) => (
<div key={size} className="grid content-start gap-3" data-comparison-size={size}>
<Field label={`Select · ${size}`} id={`compare-select-${size}`}>
<Select defaultValue="nuxt">
<SelectTrigger id={`compare-select-${size}`} size={size} className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>
{frameworks.map((option) => <SelectItem key={option.value} value={option.value} disabled={option.disabled}>{option.label}</SelectItem>)}
</SelectContent>
</Select>
</Field>
<Field label={`Combobox · ${size}`} id={`compare-combobox-${size}`}>
<Combobox id={`compare-combobox-${size}`} size={size} options={frameworks} defaultValue="nuxt" />
</Field>
</div>
))}
</div>
)
}
const sections = [
{ id: "empty", label: "Placeholder", node: <Empty /> },
{ id: "controlled", label: "Selected", node: <Controlled showValue={false} /> },
{ id: "long", label: "Long option", node: <LongLabel /> },
{ id: "disabled", label: "Disabled", node: <Disabled /> },
]
// No `component`: Combobox has a required `options` prop, so Stories without args would not type-check.
const meta = { title: "Components/Search & Date/Combobox", parameters: { layout: "padded" } } satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
const board = (...ids: string[]) => (
<StateBoard min={320} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: s.node }))} />
)
// Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = {
name: "Combobox",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader title="Combobox" />
<StorySection title="Overview">{board("empty")}</StorySection>
<StorySection title="Sizes">
{/* The field label already names the size, so the Medium specimen has no separate example title. */}
<div className="grid gap-10">
<div className="max-w-sm">
<Medium />
</div>
<StateBoard min={320} states={[{ id: "select-comparison", label: "Select / Combobox", wide: true, children: <SelectComparison /> }]} />
</div>
</StorySection>
<StorySection title="States">{board("controlled", "disabled")}</StorySection>
<StorySection title="Usage">{board("long")}</StorySection>
</StoryPage>
),
}
export const EmptyCombobox: Story = { tags: ["verification"], name: "Empty", render: () => <Empty /> }
export const ControlledCombobox: Story = { tags: ["verification"], name: "Controlled", render: () => <Controlled /> }
export const LongOption: Story = { tags: ["verification"], name: "Long Option", render: () => <LongLabel /> }
export const OpenBottom: Story = {
tags: ["verification"],
render: () => <div style={{ width: 280, maxWidth: "100%", minHeight: "calc(100dvh - 32px)" }}><Controlled /></div>,
}
export const OpenTop: Story = {
tags: ["verification"],
render: () => <div style={{ width: 280, maxWidth: "100%", minHeight: "calc(100dvh - 32px)", display: "grid", alignItems: "end" }}><Controlled /></div>,
}