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.
222 lines
11 KiB
TypeScript
222 lines
11 KiB
TypeScript
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 { 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<typeof Input> & { label?: string; message?: string; error?: boolean }) {
|
|
const uid = useId()
|
|
const fieldId = id ?? `input-${uid}`
|
|
const messageId = `${fieldId}-message`
|
|
return (
|
|
<div className="grid gap-2">
|
|
<label htmlFor={fieldId} className={`justify-self-start text-sm font-medium text-muted-foreground${input.disabled ? " opacity-60" : ""}`}>
|
|
{label}
|
|
{required ? (
|
|
<span aria-hidden="true" className="text-destructive">
|
|
{" *"}
|
|
</span>
|
|
) : null}
|
|
</label>
|
|
<Input
|
|
id={fieldId}
|
|
required={required}
|
|
aria-invalid={error ? true : undefined}
|
|
aria-describedby={message ? messageId : undefined}
|
|
{...input}
|
|
/>
|
|
{message ? (
|
|
<p id={messageId} className={error ? "text-[11px] leading-[18px] text-destructive" : "text-xs leading-[18px] text-muted-foreground"}>
|
|
{message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const narrow = (children: ReactNode) => <div className="max-w-sm">{children}</div>
|
|
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, showSpec = true }: { spec: (typeof sizes)[number]; showSpec?: boolean }) {
|
|
return (
|
|
<section className="grid content-start gap-4" data-input-size={spec.size}>
|
|
{/* Without the spec list the value would repeat the size name, so it shows sample text instead. */}
|
|
<Specimen size={spec.size} label={spec.name} defaultValue={showSpec ? `${spec.name} ${spec.height}px` : "Hanmac Project"} />
|
|
{showSpec ? (
|
|
<dl className="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
|
<dt className="text-muted-foreground">Height</dt>
|
|
<dd className="m-0">{spec.height}px</dd>
|
|
<dt className="text-muted-foreground">Padding</dt>
|
|
<dd className="m-0">0 / {spec.padding}px</dd>
|
|
<dt className="text-muted-foreground">Typography</dt>
|
|
<dd className="m-0">{spec.typography}</dd>
|
|
<dt className="text-muted-foreground">Usage</dt>
|
|
<dd className="m-0">{spec.usage}</dd>
|
|
</dl>
|
|
) : null}
|
|
</section>
|
|
)
|
|
}
|
|
|
|
// `showSpec={false}` keeps the representative page UI-only; the verification Story keeps the spec lists and the size note.
|
|
function SizeComparison({ showSpec = true }: { showSpec?: boolean }) {
|
|
return (
|
|
<div className="grid max-w-[1120px] gap-6">
|
|
<div className="grid gap-6 [grid-template-columns:repeat(auto-fit,minmax(240px,1fr))]">
|
|
{sizes.map((spec) => (
|
|
<SizeCard key={spec.size} spec={spec} showSpec={showSpec} />
|
|
))}
|
|
</div>
|
|
{showSpec ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
높이는 입력 컨트롤의 외곽 높이입니다. 값·Placeholder 글자는 size를 따르고 Label 14/20/500은 공통입니다. 업무 폼·조회 조합은 Medium을 쓰며, size를 생략하면 Large입니다.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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<typeof Input>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative Story: the common Story page (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: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Input" />
|
|
<StorySection title="Sizes">
|
|
<SizeComparison showSpec={false} />
|
|
</StorySection>
|
|
<StorySection title="States">
|
|
<StateBoard
|
|
states={[
|
|
{ id: "focus", label: "Focus", children: <Specimen size="medium" placeholder={placeholder} /> },
|
|
{ id: "disabled", label: "Disabled", children: <Specimen disabled defaultValue="Unavailable project" /> },
|
|
{ id: "readonly", label: "Readonly", children: <Specimen readOnly value="Confirmed project" /> },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
<StorySection title="Validation">
|
|
<StateBoard
|
|
states={[
|
|
{ id: "error", label: "Error", children: <Specimen size="medium" error defaultValue="Invalid value" /> },
|
|
{ id: "error-message", label: "Error Message", children: <Specimen size="medium" placeholder={placeholder} error message="프로젝트명을 입력하세요." /> },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
play: async ({ canvasElement }) => {
|
|
// preventScroll: the page opens at its header, not at the States section.
|
|
canvasElement.querySelector<HTMLInputElement>('[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(<Specimen placeholder={placeholder} />),
|
|
parameters: { docs: { description: { story: "size를 생략한 실제 기본값(Large 42px)입니다. 업무 폼·조회 역할의 Medium 34px과 구분합니다." } } },
|
|
}
|
|
export const Focus: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen size="medium" placeholder={placeholder} />),
|
|
play: async ({ canvasElement }) => canvasElement.querySelector("input")?.focus(),
|
|
parameters: { docs: { description: { story: "Focus는 실제로 focus를 받은 컨트롤에서만 보입니다. play 함수가 focus를 줍니다. Tab 또는 클릭으로도 확인합니다." } } },
|
|
}
|
|
export const Error: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen size="medium" error defaultValue="Invalid value" />),
|
|
}
|
|
export const InvalidFocus: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen size="medium" error defaultValue="Invalid value" />),
|
|
play: async ({ canvasElement }) => canvasElement.querySelector("input")?.focus(),
|
|
}
|
|
export const ErrorMessage: Story = {
|
|
tags: ["verification"],
|
|
name: "Error Message",
|
|
render: () => narrow(<Specimen size="medium" placeholder={placeholder} error message="프로젝트명을 입력하세요." />),
|
|
}
|
|
export const Required: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen size="medium" placeholder={placeholder} required />),
|
|
}
|
|
export const Disabled: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen disabled defaultValue="Unavailable project" />),
|
|
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(<Specimen readOnly value="Confirmed project" />),
|
|
parameters: { docs: { description: { story: readonlyNote } } },
|
|
}
|
|
export const Sizes: Story = { tags: ["verification"], name: "Compare Sizes", render: () => <SizeComparison /> }
|
|
export const Small: Story = { tags: ["verification"], render: () => narrow(<SizeCard spec={sizes[0]} />) }
|
|
export const Medium: Story = { tags: ["verification"], render: () => narrow(<SizeCard spec={sizes[1]} />) }
|
|
export const Large: Story = { tags: ["verification"], render: () => narrow(<SizeCard spec={sizes[2]} />) }
|
|
|
|
// 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(<Specimen {...args} />),
|
|
}
|