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.
91 lines
4.1 KiB
TypeScript
91 lines
4.1 KiB
TypeScript
import { useState } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { Button, Progress } from "@hm-design/ui"
|
|
import { StateBoard } from "@/components/story-board"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Progress follows `value` (0..max). The representative Story shows fixed values plus one interactive value; the same specimens
|
|
// also exist as Stories tagged `verification` (hidden from the sidebar). No status colors are defined, so there is one look.
|
|
|
|
// `showValue={false}` hides the value readout on the representative page; the verification Stories keep it.
|
|
function Interactive({ showValue = true }: { showValue?: boolean }) {
|
|
const [value, setValue] = useState(30)
|
|
return (
|
|
<div className="grid gap-3">
|
|
<Progress value={value} aria-label="진행률" />
|
|
{showValue ? <p className="text-sm text-muted-foreground" data-testid="value">{`value = ${value}`}</p> : null}
|
|
<div className="flex gap-2">
|
|
<Button variant="cancel" appearance="solid" size="sm" onClick={() => setValue((v) => Math.max(0, v - 25))}>
|
|
-25
|
|
</Button>
|
|
<Button variant="primary" appearance="solid" size="sm" onClick={() => setValue((v) => Math.min(100, v + 25))}>
|
|
+25
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CustomMaximum() {
|
|
return (
|
|
<div className="grid gap-3">
|
|
<Progress value={0} max={200} aria-label="0 of 200" />
|
|
<Progress value={50} max={200} aria-label="50 of 200" />
|
|
<Progress value={200} max={200} aria-label="200 of 200" />
|
|
<Progress max={200} aria-label="Indeterminate" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const sections = [
|
|
{ id: "zero", label: "0%", node: <Progress value={0} aria-label="0%" /> },
|
|
{ id: "middle", label: "50%", node: <Progress value={50} aria-label="50%" /> },
|
|
{ id: "full", label: "100%", node: <Progress value={100} aria-label="100%" /> },
|
|
{ id: "custom-maximum", label: "max = 200", node: <CustomMaximum /> },
|
|
{ id: "interactive", label: "Interactive", node: <Interactive showValue={false} /> },
|
|
]
|
|
|
|
const meta = { title: "Components/Data & Display/Progress", component: Progress, parameters: { layout: "padded" } } satisfies Meta<typeof Progress>
|
|
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: "Progress",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Progress" />
|
|
<StorySection title="Overview">{board("middle")}</StorySection>
|
|
<StorySection title="States">{board("zero", "full")}</StorySection>
|
|
<StorySection title="Usage">{board("custom-maximum", "interactive")}</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const Zero: Story = { tags: ["verification"], render: () => <Progress value={0} aria-label="0%" /> }
|
|
export const Middle: Story = { tags: ["verification"], render: () => <Progress value={50} aria-label="50%" /> }
|
|
export const Full: Story = { tags: ["verification"], render: () => <Progress value={100} aria-label="100%" /> }
|
|
|
|
export const NonDefaultMax: Story = {
|
|
tags: ["verification"],
|
|
render: () => <CustomMaximum />,
|
|
play: async ({ canvasElement }) => {
|
|
const bars = canvasElement.querySelectorAll('[role="progressbar"]')
|
|
const expected = ["translateX(-100%)", "translateX(-75%)", "translateX(0%)", "translateX(-100%)"]
|
|
bars.forEach((bar, index) => {
|
|
const indicator = bar.querySelector<HTMLElement>('[data-slot="progress-indicator"]')
|
|
if (bar.getAttribute("aria-valuemax") !== "200" || indicator?.style.transform !== expected[index]) {
|
|
throw new Error(`Progress max=200: ARIA/fill mismatch in specimen ${index}`)
|
|
}
|
|
})
|
|
if (bars.length !== 4 || bars[1]?.getAttribute("aria-valuenow") !== "50" || bars[3]?.hasAttribute("aria-valuenow")) {
|
|
throw new Error("Progress max=200: incorrect determinate/indeterminate ARIA state")
|
|
}
|
|
},
|
|
}
|