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.
213 lines
7.9 KiB
TypeScript
213 lines
7.9 KiB
TypeScript
import type { CSSProperties, ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { Button } from "@hm-design/ui"
|
|
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@hm-design/ui"
|
|
import { Input } from "@hm-design/ui"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Dialog is an overlay (Radix Portal), so it does not fit the StateBoard. The representative Story is a row of triggers that open
|
|
// the real Dialog for each ui_viz case (dialog.md §9 Reference Viewer examples); the same cases also exist as separate Stories tagged
|
|
// `verification` that start open. Pseudo-state helpers are not used to fake an open Dialog.
|
|
// Usage: native label below is a preserved Reference fixture. New HM React fields use public Label
|
|
// with the actual control id (docs/components/label.md §5); fixture actions are not screen requirements.
|
|
|
|
const longText = "긴 텍스트는 줄바꿈으로 전체를 표시합니다. 공백 없는 긴 문자열도 Dialog 너비를 밀어내지 않고 줄바꿈합니다. ".repeat(8)
|
|
|
|
function Frame({
|
|
trigger,
|
|
defaultOpen,
|
|
title,
|
|
description,
|
|
children,
|
|
actions,
|
|
contentStyle,
|
|
}: {
|
|
trigger: string
|
|
defaultOpen?: boolean
|
|
title: string
|
|
description?: string
|
|
children: ReactNode
|
|
actions?: ReactNode
|
|
contentStyle?: CSSProperties
|
|
}) {
|
|
return (
|
|
<Dialog defaultOpen={defaultOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="primary" appearance="outline">
|
|
{trigger}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent style={contentStyle}>
|
|
<DialogHeader>
|
|
<DialogTitle>{title}</DialogTitle>
|
|
{description ? <DialogDescription>{description}</DialogDescription> : null}
|
|
</DialogHeader>
|
|
<DialogBody>{children}</DialogBody>
|
|
<DialogFooter>{actions}</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
const confirmActions = (
|
|
<>
|
|
<DialogClose asChild>
|
|
<Button variant="cancel" appearance="solid">
|
|
취소
|
|
</Button>
|
|
</DialogClose>
|
|
<DialogClose asChild>
|
|
<Button variant="primary" appearance="solid">
|
|
확인
|
|
</Button>
|
|
</DialogClose>
|
|
</>
|
|
)
|
|
|
|
const deleteActions = (
|
|
<>
|
|
<DialogClose asChild>
|
|
<Button variant="cancel" appearance="solid">
|
|
취소
|
|
</Button>
|
|
</DialogClose>
|
|
<DialogClose asChild>
|
|
<Button variant="destructive" appearance="solid">
|
|
삭제
|
|
</Button>
|
|
</DialogClose>
|
|
</>
|
|
)
|
|
|
|
function Basic({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame trigger="기본" defaultOpen={defaultOpen} title="작업 확인" description="현재 작업을 계속 진행하시겠습니까?" actions={confirmActions}>
|
|
<p>짧은 내용의 Dialog는 내용 기준 크기입니다.</p>
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
function LongContent({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame trigger="긴 내용" defaultOpen={defaultOpen} title="긴 내용" description="Body만 세로로 scroll 합니다." actions={confirmActions}>
|
|
{Array.from({ length: 6 }, (_, i) => (
|
|
<p key={i} className="mb-4">
|
|
{longText}
|
|
</p>
|
|
))}
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
// ui_viz "세로 스크롤 · 본문만": the max height also leaves out the app common Header 70px / Footer 48px. The Dialog renders in a Portal,
|
|
// so the app heights are passed on DialogContent as Dialog Tokens, not on a wrapper around the trigger.
|
|
const appBars = { "--hm-dialog-app-header-height": "70px", "--hm-dialog-app-footer-height": "48px" } as CSSProperties
|
|
|
|
function BodyScroll({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame
|
|
trigger="본문 스크롤"
|
|
defaultOpen={defaultOpen}
|
|
title="본문 스크롤"
|
|
description="공통 Header 70px / Footer 48px를 뺀 높이 안에서 Body만 세로로 scroll 합니다."
|
|
actions={confirmActions}
|
|
contentStyle={appBars}
|
|
>
|
|
{Array.from({ length: 60 }, (_, i) => (
|
|
<p key={i}>검토 기록 {i + 1}. 현장 조사와 부재별 검토 내용을 확인하고 후속 조치를 기록합니다.</p>
|
|
))}
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
// The wide case composes the HM Table primitive (same as the Table Story), so it carries the Table look inside the Dialog Portal.
|
|
const wideColumns = Array.from({ length: 10 }, (_, i) => `컬럼 ${i + 1}`)
|
|
|
|
function WideContent({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame trigger="넓은 표" defaultOpen={defaultOpen} title="넓은 내용" description="표처럼 줄이기 어려운 내용은 Body 안에서 가로 scroll 합니다." actions={confirmActions}>
|
|
<Table style={{ minWidth: 1600 }}>
|
|
<TableHeader>
|
|
<TableRow>
|
|
{wideColumns.map((c) => (
|
|
<TableHead key={c}>{c}</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Array.from({ length: 4 }, (_, r) => (
|
|
<TableRow key={r}>
|
|
{wideColumns.map((_, c) => (
|
|
<TableCell key={c}>{`행 ${r + 1} · 열 ${c + 1}`}</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
function DeleteConfirmation({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame trigger="삭제 확인" defaultOpen={defaultOpen} title="항목 삭제" description="삭제하면 되돌릴 수 없습니다." actions={deleteActions}>
|
|
<p>선택한 항목을 삭제합니다. 취소와 삭제 실행은 같은 크기의 Standard Solid 세트입니다.</p>
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
function FormContent({ defaultOpen }: { defaultOpen?: boolean }) {
|
|
return (
|
|
<Frame trigger="폼" defaultOpen={defaultOpen} title="프로젝트 정보" actions={confirmActions}>
|
|
<div className="grid gap-2">
|
|
<label htmlFor="dialog-form-name" className="text-sm font-medium text-muted-foreground">
|
|
프로젝트명
|
|
</label>
|
|
<Input id="dialog-form-name" size="medium" placeholder="프로젝트명을 입력하세요" />
|
|
</div>
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
const meta = { title: "Components/Overlay/Dialog", component: Dialog, parameters: { layout: "centered" } } satisfies Meta<typeof Dialog>
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// Representative Story: the common Story page; every case opens from its trigger. Named after the component, as in ui_viz.
|
|
// It is the only Dialog entry in the default sidebar, so Storybook shows Dialog as a single 1-depth item.
|
|
export const All: Story = {
|
|
name: "Dialog",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader
|
|
title="Dialog"
|
|
/>
|
|
<StorySection title="Overview">
|
|
<Basic />
|
|
</StorySection>
|
|
<StorySection title="Variants">
|
|
<div className="flex flex-wrap gap-3">
|
|
<LongContent />
|
|
<WideContent />
|
|
<BodyScroll />
|
|
</div>
|
|
</StorySection>
|
|
<StorySection title="Usage">
|
|
<div className="flex flex-wrap gap-3">
|
|
<DeleteConfirmation />
|
|
<FormContent />
|
|
</div>
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
// Verification Stories: the same cases starting open, hidden from the sidebar (tag `verification`).
|
|
export const Open: Story = { tags: ["verification"], render: () => <Basic defaultOpen /> }
|
|
export const LongContentOpen: Story = { tags: ["verification"], name: "Long Content", render: () => <LongContent defaultOpen /> }
|
|
export const WideContentOpen: Story = { tags: ["verification"], name: "Wide Content", render: () => <WideContent defaultOpen /> }
|
|
export const BodyScrollOpen: Story = { tags: ["verification"], name: "Body Scroll", render: () => <BodyScroll defaultOpen /> }
|
|
export const DeleteConfirmationOpen: Story = { tags: ["verification"], name: "Delete Confirmation", render: () => <DeleteConfirmation defaultOpen /> }
|