import { useId, type ComponentProps, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { Input, type InputSize } 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/Input.stories.jsx and InputSizes.stories.jsx): // one representative "Input" Story shows the sizes and every state on one page; the same specimens also exist as // separate Stories tagged `verification`, which the sidebar hides by default (see .storybook/main.ts). // // Label, Required star and Helper/Error message are not part of the Input component. Specimen below composes them with // plain elements and shadcn Token utilities, the way a consumer would: Label 14/20/500 and Helper 12/18/400 in the // secondary text color, Error message 11/18 in the destructive color, field gap 8. function Specimen({ label = "Project name", message, error, required, id, ...input }: ComponentProps & { label?: string; message?: string; error?: boolean }) { const uid = useId() const fieldId = id ?? `input-${uid}` const messageId = `${fieldId}-message` return (
{message ? (

{message}

) : null}
) } const narrow = (children: ReactNode) =>
{children}
const placeholder = "Enter a project name" // ui_viz docs/components/input.md §9, §24: display metadata only, never sets control geometry. const sizes: { size: InputSize; name: string; height: number; padding: number; typography: string; usage: string }[] = [ { size: "small", name: "Small", height: 28, padding: 10, typography: "12 / 18 / 400", usage: "Small control" }, { size: "medium", name: "Medium", height: 34, padding: 12, typography: "14 / 20 / 400", usage: "Business forms / Search" }, { size: "large", name: "Large", height: 42, padding: 12, typography: "16 / 24 / 400", usage: "Component default" }, ] function SizeCard({ spec }: { spec: (typeof sizes)[number] }) { return (
Height
{spec.height}px
Padding
0 / {spec.padding}px
Typography
{spec.typography}
Usage
{spec.usage}
) } function SizeComparison() { return (
{sizes.map((spec) => ( ))}

높이는 입력 컨트롤의 외곽 높이입니다. 값·Placeholder 글자는 size를 따르고 Label 14/20/500은 공통입니다. 업무 폼·조회 조합은 Medium을 쓰며, size를 생략하면 Large입니다.

) } const readonlyNote = "Readonly는 입력 상자 없이 확정된 텍스트만 표시합니다. size와 무관하게 Large 기준 typography와 42px 정렬 높이를 유지하며, Disabled처럼 흐리게 표시하지 않습니다." const meta = { title: "Components/Form/Input", component: Input, parameters: { layout: "padded", docs: { description: { component: "ui_viz docs/components/input.md: Small 28 / Medium 34 / Large 42px, size를 생략하면 Large. Focus와 Error는 1.5px 테두리로 구분하며 shadow·glow를 쓰지 않습니다. Hover는 없습니다. Label·Helper·Error 문구는 Input이 아니라 소비 조합입니다. Disabled 컨트롤은 흰 면과 opacity 0.6을 유지합니다(2026-10-02 사용자 결정). 이 Story 소비 조합의 Disabled Label도 0.6입니다(2026-10-11 최종 사용자 결정). 기본·Readonly·Error Label은 1, 공통 Label Component Disabled는 기존 0.5이며 Helper·Message는 변경하지 않습니다.", }, }, }, argTypes: { size: { control: "inline-radio", options: ["small", "medium", "large"], description: "무지정 기본값 Large. 업무 폼·조회 소비 역할은 Medium.", }, }, } satisfies Meta export default meta type Story = StoryObj // Representative Story: the common Story page (Overview → Sizes → States → Validation). Focus is placed by the play function. // Named after the component, as in ui_viz. export const All: Story = { name: "Input", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( }]} /> }, { id: "disabled", label: "Disabled", children: }, { id: "readonly", label: "Readonly", note: readonlyNote, noteAfter: true, children: }, ]} /> }, { id: "error-message", label: "Error Message", children: }, ]} /> ), play: async ({ canvasElement }) => { // preventScroll: the page opens at its header, not at the States section. canvasElement.querySelector('[data-overview-state="focus"] input')?.focus({ preventScroll: true }) }, } // Verification Stories: the same specimens as separate Stories, hidden from the sidebar (tag `verification`), reachable by URL or the sidebar tag filter. export const Default: Story = { tags: ["verification"], render: () => narrow(), parameters: { docs: { description: { story: "size를 생략한 실제 기본값(Large 42px)입니다. 업무 폼·조회 역할의 Medium 34px과 구분합니다." } } }, } export const Focus: Story = { tags: ["verification"], render: () => narrow(), play: async ({ canvasElement }) => canvasElement.querySelector("input")?.focus(), parameters: { docs: { description: { story: "Focus는 실제로 focus를 받은 컨트롤에서만 보입니다. play 함수가 focus를 줍니다. Tab 또는 클릭으로도 확인합니다." } } }, } export const Error: Story = { tags: ["verification"], render: () => narrow(), } export const InvalidFocus: Story = { tags: ["verification"], render: () => narrow(), play: async ({ canvasElement }) => canvasElement.querySelector("input")?.focus(), } export const ErrorMessage: Story = { tags: ["verification"], name: "Error Message", render: () => narrow(), } export const Required: Story = { tags: ["verification"], render: () => narrow(), } export const Disabled: Story = { tags: ["verification"], render: () => narrow(), parameters: { docs: { description: { story: "2026-10-11 최종 사용자 결정: Input과 이 소비 조합의 Label 각각 opacity 0.6, 부모는 1. Label 원본 색은 #6E6759이며 공통 Label Component의 Disabled 0.5와 구분합니다. Helper·Message는 이번 변경 대상이 아닙니다." } } }, } export const Readonly: Story = { tags: ["verification"], render: () => narrow(), parameters: { docs: { description: { story: readonlyNote } } }, } export const Sizes: Story = { tags: ["verification"], name: "Compare Sizes", render: () => } export const Small: Story = { tags: ["verification"], render: () => narrow() } export const Medium: Story = { tags: ["verification"], render: () => narrow() } export const Large: Story = { tags: ["verification"], render: () => narrow() } // HM only: not part of the ui_viz Input Stories. Tagged verification so the default sidebar keeps only the representative Story. export const Playground: Story = { tags: ["verification"], args: { size: "medium", type: "text", placeholder, disabled: false, readOnly: false }, argTypes: { type: { control: "select", options: ["text", "email", "password", "search", "number", "tel", "url"] }, placeholder: { control: "text" }, disabled: { control: "boolean" }, readOnly: { control: "boolean" }, "aria-invalid": { control: "boolean" }, className: { table: { disable: true } }, }, render: (args) => narrow(), }