98 lines
4.4 KiB
TypeScript
98 lines
4.4 KiB
TypeScript
import { useId, type ReactNode } from "react"
|
||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||
import { Label, Switch } from "@hm-design/ui"
|
||
import { StateBoard, type BoardState } from "@/components/story-board"
|
||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||
|
||
// Story-only Label color. The shared Label sets `color: var(--hm-label-text)` from an unlayered theme rule, so a utility
|
||
// like `text-foreground` loses to it; overriding the Label's own token on the element works. Option labels use the default
|
||
// text color; a Label next to a disabled Switch (`peer-disabled`) keeps the secondary color. State titles are untouched.
|
||
const optionLabel = "[--hm-label-text:var(--foreground)] peer-disabled:[--hm-label-text:var(--hm-sem-fg-secondary)]"
|
||
|
||
function Option({ label, children }: { label: string; children: (id: string) => ReactNode }) {
|
||
const id = useId()
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
{children(id)}
|
||
<Label htmlFor={id} className={optionLabel}>{label}</Label>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Representative-page layout (Storybook presentation only): 5 equal columns on PC (iframe width 1000px and up), shrinking
|
||
// to fit below that (150px minimum per column). Each specimen sits in its own single-column StateBoard (min larger than
|
||
// any column), so StateBoard's auto-fill grid cannot leave an empty or stretched track.
|
||
function StateGrid({ states }: { states: BoardState[] }) {
|
||
return (
|
||
<div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,150px),1fr))] min-[1000px]:grid-cols-5">
|
||
{states.map((state) => (
|
||
<StateBoard key={state.id} min={10000} states={[state]} />
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const meta = {
|
||
title: "Components/Form/Switch",
|
||
component: Switch,
|
||
parameters: {
|
||
layout: "padded",
|
||
docs: {
|
||
description: {
|
||
component:
|
||
"현재 HM 규격: 트랙 36×20, 썸 16, On은 state-active, Off는 stroke 색. ui-viz에는 Switch 규격이 없어 참고만 했습니다. Radix Switch의 role=switch, 키보드, 접근성과 shadcn 기본 transition을 유지합니다.",
|
||
},
|
||
},
|
||
},
|
||
} satisfies Meta<typeof Switch>
|
||
export default meta
|
||
type Story = StoryObj<typeof meta>
|
||
|
||
export const All: Story = {
|
||
name: "Switch",
|
||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||
render: () => (
|
||
<StoryPage>
|
||
<StoryPageHeader title="Switch" />
|
||
<StorySection title="States">
|
||
<StateGrid
|
||
states={[
|
||
{ id: "off", label: "Off", children: <Option label="Option">{(id) => <Switch id={id} />}</Option> },
|
||
{ id: "on", label: "On", children: <Option label="Option">{(id) => <Switch id={id} defaultChecked />}</Option> },
|
||
{
|
||
id: "hover",
|
||
label: "Hover",
|
||
// data-sb-hover pins :hover (Storybook only, .storybook/pseudo-states.ts).
|
||
children: (
|
||
<div className="grid gap-3">
|
||
<Option label="Off">{(id) => <Switch id={id} data-sb-hover="" />}</Option>
|
||
<Option label="On">{(id) => <Switch id={id} defaultChecked data-sb-hover="" />}</Option>
|
||
</div>
|
||
),
|
||
},
|
||
{ id: "focus", label: "Focus", children: <Option label="Option">{(id) => <Switch id={id} />}</Option> },
|
||
{
|
||
id: "disabled",
|
||
label: "Disabled",
|
||
children: (
|
||
<div className="grid gap-3">
|
||
<Option label="Off">{(id) => <Switch id={id} disabled />}</Option>
|
||
<Option label="On">{(id) => <Switch id={id} disabled defaultChecked />}</Option>
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</StorySection>
|
||
</StoryPage>
|
||
),
|
||
play: async ({ canvasElement }) => {
|
||
// preventScroll: the page opens at its header, not at the States section.
|
||
canvasElement.querySelector<HTMLButtonElement>('[data-overview-state="focus"] [role="switch"]')?.focus({ preventScroll: true })
|
||
},
|
||
}
|
||
|
||
export const Off: Story = { tags: ["verification"], render: () => <Option label="Option">{(id) => <Switch id={id} />}</Option> }
|
||
export const On: Story = { tags: ["verification"], render: () => <Option label="Option">{(id) => <Switch id={id} defaultChecked />}</Option> }
|
||
export const Disabled: Story = { tags: ["verification"], render: () => <Option label="Option">{(id) => <Switch id={id} disabled defaultChecked />}</Option> }
|