181 lines
8.0 KiB
TypeScript
181 lines
8.0 KiB
TypeScript
import { useState, type ReactNode } from "react"
|
|
import type { Meta, StoryObj } from "@storybook/react-vite"
|
|
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
|
|
import { ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
|
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
|
|
|
// Toggle Group is a group of pressed-state items (toolbar-style), not a content switcher (that is Tabs). The representative Story
|
|
// shows single / multiple / icon-only / disabled / vertical / spaced groups with real controls; the same specimens also exist as
|
|
// Stories tagged `verification` (hidden from the sidebar). Icon-only items carry an `aria-label`.
|
|
|
|
// `showValue={false}` hides the selected-value readout on the representative page; the verification Stories keep it.
|
|
function Single({ showValue = true }: { showValue?: boolean }) {
|
|
const [value, setValue] = useState("left")
|
|
return (
|
|
<div className="grid gap-2">
|
|
<ToggleGroup type="single" variant="outline" value={value} onValueChange={(v) => v && setValue(v)} aria-label="정렬">
|
|
<ToggleGroupItem value="left">왼쪽</ToggleGroupItem>
|
|
<ToggleGroupItem value="center">가운데</ToggleGroupItem>
|
|
<ToggleGroupItem value="right">오른쪽</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
{showValue ? <span className="text-sm text-muted-foreground" data-testid="single">{`value = ${value}`}</span> : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Multiple({ showValue = true }: { showValue?: boolean }) {
|
|
const [value, setValue] = useState<string[]>(["bold"])
|
|
return (
|
|
<div className="grid gap-2">
|
|
<ToggleGroup type="multiple" variant="outline" value={value} onValueChange={setValue} aria-label="서식">
|
|
<ToggleGroupItem value="bold" aria-label="굵게">
|
|
<BoldIcon />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="italic" aria-label="기울임">
|
|
<ItalicIcon />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="underline" aria-label="밑줄">
|
|
<UnderlineIcon />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
{showValue ? <span className="text-sm text-muted-foreground" data-testid="multiple">{`value = [${value.join(", ")}]`}</span> : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DisabledGroup() {
|
|
return (
|
|
<ToggleGroup type="single" variant="outline" defaultValue="a" disabled aria-label="비활성 그룹">
|
|
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
<ToggleGroupItem value="b">B</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)
|
|
}
|
|
|
|
function DisabledItem() {
|
|
return (
|
|
<ToggleGroup type="single" variant="outline" defaultValue="a" aria-label="비활성 항목">
|
|
<ToggleGroupItem value="a">A</ToggleGroupItem>
|
|
<ToggleGroupItem value="b" disabled>
|
|
B (비활성)
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="c">C</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)
|
|
}
|
|
|
|
function Vertical() {
|
|
return (
|
|
<ToggleGroup type="single" variant="outline" orientation="vertical" defaultValue="left" aria-label="세로 정렬">
|
|
<ToggleGroupItem value="left" aria-label="왼쪽">
|
|
<AlignLeftIcon />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="center" aria-label="가운데">
|
|
<AlignCenterIcon />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="right" aria-label="오른쪽">
|
|
<AlignRightIcon />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)
|
|
}
|
|
|
|
function Spaced() {
|
|
return (
|
|
<ToggleGroup type="multiple" variant="outline" spacing={2} aria-label="간격 있는 그룹">
|
|
<ToggleGroupItem value="a">첫째</ToggleGroupItem>
|
|
<ToggleGroupItem value="b">둘째</ToggleGroupItem>
|
|
<ToggleGroupItem value="c">셋째</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
)
|
|
}
|
|
|
|
function Many() {
|
|
return (
|
|
<ToggleGroup type="multiple" variant="outline" spacing={1} className="flex-wrap" aria-label="많은 항목">
|
|
{Array.from({ length: 10 }, (_, i) => (
|
|
<ToggleGroupItem key={i} value={`${i}`}>{`옵션 ${i + 1}`}</ToggleGroupItem>
|
|
))}
|
|
</ToggleGroup>
|
|
)
|
|
}
|
|
|
|
function Sizes() {
|
|
return (
|
|
// 3 equal columns on PC (iframe width 1000px and up): sm / default / lg; fewer columns below that (240px minimum).
|
|
<div className="grid items-start justify-items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,240px),1fr))] min-[1000px]:grid-cols-3">
|
|
{(["sm", "default", "lg"] as const).map((size) => (
|
|
<ToggleGroup key={size} type="multiple" size={size} variant="outline" aria-label={`${size} content kinds`}>
|
|
<ToggleGroupItem value="icon" aria-label={`${size} icon-only`}><BoldIcon /></ToggleGroupItem>
|
|
<ToggleGroupItem value="mixed"><BoldIcon />굵게</ToggleGroupItem>
|
|
<ToggleGroupItem value="text">{size}</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// No `component`: ToggleGroup props are a union (single | multiple), so Stories without args would not type-check.
|
|
// `internal` keeps it out of the sidebar by default; Story IDs and direct URLs stay valid.
|
|
const meta = { title: "Components/Form/Toggle Group", tags: ["internal"], parameters: { layout: "padded" } } satisfies Meta
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// The representative page shows the groups without example titles: the section title names them. Equal columns on PC
|
|
// (iframe width 1000px and up), fewer below that (200px minimum); the groups keep their own size (they are not stretched to the
|
|
// column). Full class names are listed so Tailwind can see them.
|
|
const specimenColumns = {
|
|
3: "min-[1000px]:grid-cols-3",
|
|
4: "min-[1000px]:grid-cols-4",
|
|
} as const
|
|
|
|
function Examples({ columns, nodes }: { columns: keyof typeof specimenColumns; nodes: ReactNode[] }) {
|
|
return (
|
|
<div className={`grid items-start justify-items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,200px),1fr))] ${specimenColumns[columns]}`}>
|
|
{nodes.map((node, i) => (
|
|
<div key={i}>{node}</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Representative Story: the common Story page; the sections group the same specimens.
|
|
// Overview is merged into Variants (the default group is its first item). The 10-option Usage example is the hidden `ManyOptions` Story.
|
|
export const All: Story = {
|
|
name: "Toggle Group",
|
|
parameters: { layout: "fullscreen", controls: { disable: true } },
|
|
render: () => (
|
|
<StoryPage>
|
|
<StoryPageHeader title="Toggle Group" />
|
|
<StorySection title="Variants">
|
|
<Examples columns={4} nodes={[<Single key="single" showValue={false} />, <Multiple key="multiple" showValue={false} />, <Vertical key="vertical" />, <Spaced key="spaced" />]} />
|
|
</StorySection>
|
|
<StorySection title="Sizes">
|
|
<Sizes />
|
|
</StorySection>
|
|
<StorySection title="States">
|
|
{/* First two columns of a 3-column grid; the third stays empty. */}
|
|
<Examples columns={3} nodes={[<DisabledGroup key="group" />, <DisabledItem key="item" />]} />
|
|
</StorySection>
|
|
</StoryPage>
|
|
),
|
|
}
|
|
|
|
export const SingleSelection: Story = { tags: ["verification"], name: "Single", render: () => <Single /> }
|
|
export const MultipleSelection: Story = { tags: ["verification"], name: "Multiple", render: () => <Multiple /> }
|
|
export const VerticalGroup: Story = { tags: ["verification"], name: "Vertical", render: () => <Vertical /> }
|
|
// Former Usage section of the representative page (10 options that wrap), kept as its own Story.
|
|
export const ManyOptions: Story = { tags: ["verification"], name: "Many Options", render: () => <div className="max-w-sm"><Many /></div> }
|
|
|
|
export const ContentNodes: Story = {
|
|
tags: ["verification"],
|
|
render: () => (
|
|
<ToggleGroup type="multiple" variant="outline" aria-label="Content nodes">
|
|
<ToggleGroupItem value="array" aria-label="Array icon">{[<BoldIcon key="icon" />]}</ToggleGroupItem>
|
|
<ToggleGroupItem value="fragment" aria-label="Fragment icon"><><BoldIcon /></></ToggleGroupItem>
|
|
<ToggleGroupItem value="mixed"><><BoldIcon />굵게</></ToggleGroupItem>
|
|
<ToggleGroupItem value="span"><BoldIcon /><span>굵게</span></ToggleGroupItem>
|
|
</ToggleGroup>
|
|
),
|
|
}
|