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.
187 lines
7.7 KiB
TypeScript
187 lines
7.7 KiB
TypeScript
import { useId, type ComponentProps, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { Textarea } from "@hm-design/ui"
|
|
import { StateBoard } from "@/components/story-board"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Story structure follows Input: one representative "Textarea" Story shows every state on one page; the same specimens
|
|
// also exist as separate Stories tagged `verification`, hidden from the sidebar by default (see .storybook/main.ts).
|
|
// ui_viz docs/components/textarea.md §6 Reference Viewer: Default, Hover, Focus, Error, Error + Focus, Readonly, Disabled and Multiline.
|
|
//
|
|
// Label, Required star and Helper/Error message are not part of the Textarea component. Specimen 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 = "Description",
|
|
message,
|
|
error,
|
|
required,
|
|
id,
|
|
...textarea
|
|
}: ComponentProps<typeof Textarea> & { label?: string; message?: string; error?: boolean }) {
|
|
const uid = useId()
|
|
const fieldId = id ?? `textarea-${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">
|
|
{label}
|
|
{required ? (
|
|
<span aria-hidden="true" className="text-destructive">
|
|
{" *"}
|
|
</span>
|
|
) : null}
|
|
</label>
|
|
<Textarea
|
|
id={fieldId}
|
|
required={required}
|
|
aria-invalid={error ? true : undefined}
|
|
aria-describedby={message ? messageId : undefined}
|
|
{...textarea}
|
|
/>
|
|
{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 description"
|
|
const multiline = "First line\nSecond line keeps its line break.\nThird line."
|
|
|
|
const readonlyNote =
|
|
"Readonly는 Default의 면, 테두리, typography를 유지하고 흐리게 표시하지 않습니다. 값 변경은 막고 선택·복사는 허용하며 resize 표시가 없습니다."
|
|
|
|
const meta = {
|
|
title: "Components/Form/Textarea",
|
|
component: Textarea,
|
|
parameters: {
|
|
layout: "padded",
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"ui_viz docs/components/textarea.md: 최소 높이 112px, 안쪽 여백 8/12px(Textarea 한정 owner 승인, SCOPED_DESIGN_APPROVAL), 값 16/24/400, resize는 기본 vertical. Focus와 Error는 1.5px 테두리로 구분하며 shadow·glow를 쓰지 않습니다. Hover는 Default와 같습니다. Label·Helper·Error 문구는 Textarea가 아니라 소비 조합입니다. size 체계는 없습니다.",
|
|
},
|
|
},
|
|
},
|
|
argTypes: {
|
|
resize: {
|
|
control: "inline-radio",
|
|
options: ["vertical", "none", "both"],
|
|
description: "기본 vertical. Readonly에서는 항상 none.",
|
|
},
|
|
},
|
|
} satisfies Meta<typeof Textarea>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative Story: every state on one page. Focus is placed by the play function. Named after the component, as in ui_viz.
|
|
export const All: Story = {
|
|
name: "Textarea",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Textarea" />
|
|
<StorySection title="Overview">
|
|
<StateBoard states={[{ id: "default", label: "Default", children: <Specimen placeholder={placeholder} /> }]} />
|
|
</StorySection>
|
|
<StorySection title="States">
|
|
<StateBoard
|
|
states={[
|
|
{ id: "focus", label: "Focus", children: <Specimen placeholder={placeholder} /> },
|
|
{ id: "readonly", label: "Readonly", children: <Specimen readOnly defaultValue="Confirmed description" /> },
|
|
{ id: "disabled", label: "Disabled", children: <Specimen disabled defaultValue="Unavailable description" /> },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
<StorySection title="Validation">
|
|
<StateBoard
|
|
states={[
|
|
{ id: "error", label: "Error", children: <Specimen error defaultValue="Invalid value" /> },
|
|
{ id: "error-message", label: "Error Message", children: <Specimen error message="설명을 입력하세요." /> },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
<StorySection title="Usage">
|
|
<StateBoard
|
|
states={[
|
|
{ id: "multiline", label: "Multiline · Wrap", children: <Specimen defaultValue={multiline} /> },
|
|
{ id: "resize", label: "Resize · none", children: <Specimen resize="none" defaultValue="This field cannot be resized." /> },
|
|
]}
|
|
/>
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
play: async ({ canvasElement }) => {
|
|
// preventScroll: the page opens at its header, not at the States section.
|
|
canvasElement.querySelector<HTMLTextAreaElement>('[data-overview-state="focus"] textarea')?.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} />),
|
|
}
|
|
export const Focus: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen placeholder={placeholder} />),
|
|
play: async ({ canvasElement }) => canvasElement.querySelector("textarea")?.focus(),
|
|
parameters: { docs: { description: { story: "Focus는 실제로 focus를 받은 컨트롤에서만 보입니다. play 함수가 focus를 줍니다. Tab 또는 클릭으로도 확인합니다." } } },
|
|
}
|
|
export const Error: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen error defaultValue="Invalid value" />),
|
|
}
|
|
export const InvalidFocus: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen error defaultValue="Invalid value" />),
|
|
play: async ({ canvasElement }) => canvasElement.querySelector("textarea")?.focus(),
|
|
}
|
|
export const ErrorMessage: Story = {
|
|
tags: ["verification"],
|
|
name: "Error Message",
|
|
render: () => narrow(<Specimen error message="설명을 입력하세요." />),
|
|
}
|
|
export const Required: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen required />),
|
|
}
|
|
export const Readonly: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen readOnly defaultValue="Confirmed description" />),
|
|
parameters: { docs: { description: { story: readonlyNote } } },
|
|
}
|
|
export const Disabled: Story = {
|
|
tags: ["verification"],
|
|
render: () => narrow(<Specimen disabled defaultValue="Unavailable description" />),
|
|
}
|
|
export const LongContent: Story = {
|
|
tags: ["verification"],
|
|
name: "Long Content",
|
|
render: () =>
|
|
narrow(
|
|
<Specimen
|
|
defaultValue={Array.from({ length: 12 }, (_, i) => `Line ${i + 1}: a long value keeps its line order and scrolls inside the control.`).join("\n")}
|
|
/>
|
|
),
|
|
}
|
|
|
|
// HM only: not part of the ui_viz Textarea Stories. Tagged verification so the default sidebar keeps only the representative Story.
|
|
export const Playground: Story = {
|
|
tags: ["verification"],
|
|
args: { resize: "vertical", placeholder, disabled: false, readOnly: false },
|
|
argTypes: {
|
|
placeholder: { control: "text" },
|
|
disabled: { control: "boolean" },
|
|
readOnly: { control: "boolean" },
|
|
"aria-invalid": { control: "boolean" },
|
|
className: { table: { disable: true } },
|
|
},
|
|
render: (args) => narrow(<Specimen {...args} />),
|
|
}
|