style(storybook): refine form component stories

This commit is contained in:
shkim4 committed 2026-11-09 15:33:23 +09:00
1 parent e47eb040f2
commit f6b92d781a
23 files changed
+471 -182

No files matched your search

+42 -9
View File
@@ -1,10 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { Label, RadioGroup, RadioGroupItem } from "@hm-design/ui"
import { StateBoard } from "@/components/story-board"
import type { CSSProperties } from "react"
import { StateBoard, type BoardState } from "@/components/story-board"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
// Current HM Radio Group. ui-viz has no Radio spec, so it was reference only.
// 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 item (`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)]"
// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), shrinking to
// fit below that (`min` is the narrowest 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 track. Full class names are listed so Tailwind sees them.
const specimenColumns = {
2: "min-[1000px]:grid-cols-2",
4: "min-[1000px]:grid-cols-4",
} as const
function SpecimenGrid({ states, columns, min = 220 }: { states: BoardState[]; columns: keyof typeof specimenColumns; min?: number }) {
return (
<div
className={`grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-col-min)),1fr))] ${specimenColumns[columns]}`}
style={{ "--sb-col-min": `${min}px` } as CSSProperties}
>
{states.map((state) => (
<StateBoard key={state.id} min={10000} states={[state]} />
))}
</div>
)
}
function Group({
prefix,
defaultValue = "a",
@@ -28,7 +55,7 @@ function Group({
{["a", "b", "c"].map((v) => (
<div key={v} className="flex items-center gap-2">
<RadioGroupItem value={v} id={`${prefix}-${v}`} disabled={disabledItem === v} aria-invalid={invalid ? true : undefined} data-sb-hover={hoverItem === v ? "" : undefined} />
<Label htmlFor={`${prefix}-${v}`}>Option {v.toUpperCase()}</Label>
<Label htmlFor={`${prefix}-${v}`} className={optionLabel}>Option {v.toUpperCase()}</Label>
</div>
))}
</RadioGroup>
@@ -57,15 +84,20 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader title="Radio Group" />
<StorySection title="Overview">
<StateBoard min={240} states={[{ id: "vertical", label: "Vertical", children: <Group prefix="v" defaultValue="b" /> }]} />
</StorySection>
{/* Overview is merged into Variants: its Vertical example is the first Variants item. */}
<StorySection title="Variants">
<StateBoard min={240} states={[{ id: "horizontal", label: "Horizontal", children: <Group prefix="h" orientation="horizontal" /> }]} />
<SpecimenGrid
columns={2}
min={320}
states={[
{ id: "vertical", label: "Vertical", children: <Group prefix="v" defaultValue="b" /> },
{ id: "horizontal", label: "Horizontal", children: <Group prefix="h" orientation="horizontal" /> },
]}
/>
</StorySection>
<StorySection title="States">
<StateBoard
min={240}
<SpecimenGrid
columns={4}
states={[
{ id: "hover", label: "Hover", children: <Group prefix="hv" defaultValue="" hoverItem="a" /> },
{ id: "focus", label: "Focus", children: <Group prefix="f" /> },
@@ -75,7 +107,8 @@ export const All: Story = {
/>
</StorySection>
<StorySection title="Validation">
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Group prefix="e" invalid /> }]} />
{/* Same 4-column track as States, so the Error example keeps the width of one States column. */}
<SpecimenGrid columns={4} states={[{ id: "error", label: "Error", children: <Group prefix="e" invalid /> }]} />
</StorySection>
</StoryPage>
),