style(storybook): refine search and date story layout

This commit is contained in:
shkim4 committed 2026-11-09 16:09:01 +09:00
1 parent 8827eb2681
commit 0ea5b46f74
4 files changed
+117 -79

No files matched your search

+22 -13
View File
@@ -48,22 +48,22 @@ function TwoMonths() {
return box(<Calendar mode="single" numberOfMonths={2} defaultMonth={month} today={today} />) return box(<Calendar mode="single" numberOfMonths={2} defaultMonth={month} today={today} />)
} }
const sections = [ // Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles, dividers and
{ id: "single", label: "Single", node: <Single showValue={false} /> }, // specimen tops line up; the wide Two months specimen takes two tracks (the full row on narrow screens).
{ id: "disabled", label: "Disabled dates", node: <DisabledDates /> }, const Grid = ({ children }: { children: ReactNode }) => (
{ id: "range", label: "Range", node: <Range /> }, <div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))] min-[1000px]:grid-cols-3">{children}</div>
{ id: "months", label: "Two months", node: <TwoMonths /> }, )
] const Cell = ({ id, label, wide, children }: { id: string; label: string; wide?: boolean; children: ReactNode }) => (
<div className={wide ? "col-span-full min-w-0 min-[1000px]:col-span-2" : "min-w-0"}>
<StateBoard min={10000} states={[{ id, label, children }]} />
</div>
)
// No `component`: Calendar props are a union of selection modes, so Stories without args would not type-check. // No `component`: Calendar props are a union of selection modes, so Stories without args would not type-check.
const meta = { title: "Components/Search & Date/Calendar", parameters: { layout: "padded" } } satisfies Meta const meta = { title: "Components/Search & Date/Calendar", parameters: { layout: "padded" } } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
const board = (...ids: string[]) => (
<StateBoard min={300} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, wide: s.id === "months", children: s.node }))} />
)
// Representative Story: the common Story page; the sections group the same specimens. // Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = { export const All: Story = {
name: "Calendar", name: "Calendar",
@@ -71,9 +71,18 @@ export const All: Story = {
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="Calendar" /> <StoryPageHeader title="Calendar" />
<StorySection title="Overview">{board("single")}</StorySection> <StorySection title="Variants">
<StorySection title="Variants">{board("range", "months")}</StorySection> <Grid>
<StorySection title="States">{board("disabled")}</StorySection> <Cell id="single" label="Single"><Single showValue={false} /></Cell>
<Cell id="range" label="Range"><Range /></Cell>
<Cell id="months" label="Two months" wide><TwoMonths /></Cell>
</Grid>
</StorySection>
<StorySection title="States">
<Grid>
<Cell id="disabled" label="Disabled dates"><DisabledDates /></Cell>
</Grid>
</StorySection>
</StoryPage> </StoryPage>
), ),
} }
+30 -23
View File
@@ -21,10 +21,11 @@ const longOptions: ComboboxOption[] = [
{ value: "b", label: "짧은 옵션" }, { value: "b", label: "짧은 옵션" },
] ]
function Field({ label, id, children }: { label: string; id: string; children: ReactNode }) { // Label 14/20/500 in the default text color; a Disabled field keeps the secondary color at 0.6 (same as the Form pages).
function Field({ label, id, disabled, children }: { label: string; id: string; disabled?: boolean; children: ReactNode }) {
return ( return (
<div className="grid max-w-xs gap-2"> <div className="grid max-w-xs gap-2">
<label htmlFor={id} className="justify-self-start text-sm font-medium text-muted-foreground"> <label htmlFor={id} className={`justify-self-start text-sm font-medium ${disabled ? "text-muted-foreground opacity-60" : "text-foreground"}`}>
{label} {label}
</label> </label>
{children} {children}
@@ -71,7 +72,7 @@ function LongLabel() {
function Disabled() { function Disabled() {
return ( return (
<Field label="비활성" id="cb-disabled"> <Field label="비활성" id="cb-disabled" disabled>
<Combobox id="cb-disabled" options={frameworks} defaultValue="next" disabled /> <Combobox id="cb-disabled" options={frameworks} defaultValue="next" disabled />
</Field> </Field>
) )
@@ -99,22 +100,22 @@ function SelectComparison() {
) )
} }
const sections = [ // Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles, dividers and
{ id: "empty", label: "Placeholder", node: <Empty /> }, // fields line up; sections with fewer specimens leave the remaining tracks empty. A wide cell takes two tracks (the full row on narrow screens).
{ id: "controlled", label: "Selected", node: <Controlled showValue={false} /> }, const Grid = ({ children }: { children: ReactNode }) => (
{ id: "long", label: "Long option", node: <LongLabel /> }, <div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))] min-[1000px]:grid-cols-3">{children}</div>
{ id: "disabled", label: "Disabled", node: <Disabled /> }, )
] const Cell = ({ id, label, wide, children }: { id: string; label: string; wide?: boolean; children: ReactNode }) => (
<div className={wide ? "col-span-full min-w-0 min-[1000px]:col-span-2" : "min-w-0"}>
<StateBoard min={10000} states={[{ id, label, children }]} />
</div>
)
// No `component`: Combobox has a required `options` prop, so Stories without args would not type-check. // No `component`: Combobox has a required `options` prop, so Stories without args would not type-check.
const meta = { title: "Components/Search & Date/Combobox", parameters: { layout: "padded" } } satisfies Meta const meta = { title: "Components/Search & Date/Combobox", parameters: { layout: "padded" } } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof 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. // Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = { export const All: Story = {
name: "Combobox", name: "Combobox",
@@ -122,18 +123,24 @@ export const All: Story = {
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="Combobox" /> <StoryPageHeader title="Combobox" />
<StorySection title="Overview">{board("empty")}</StorySection>
<StorySection title="Sizes"> <StorySection title="Sizes">
{/* The field label already names the size, so the Medium specimen has no separate example title. */} <Grid>
<div className="grid gap-10"> <Cell id="empty" label="Default"><Empty /></Cell>
<div className="max-w-sm"> <Cell id="medium" label="Medium"><Medium /></Cell>
<Medium /> </Grid>
</div> </StorySection>
<StateBoard min={320} states={[{ id: "select-comparison", label: "Select / Combobox", wide: true, children: <SelectComparison /> }]} /> <StorySection title="States">
</div> <Grid>
<Cell id="controlled" label="Selected"><Controlled showValue={false} /></Cell>
<Cell id="disabled" label="Disabled"><Disabled /></Cell>
</Grid>
</StorySection>
<StorySection title="Usage">
<Grid>
<Cell id="long" label="Long option"><LongLabel /></Cell>
<Cell id="select-comparison" label="Select / Combobox" wide><SelectComparison /></Cell>
</Grid>
</StorySection> </StorySection>
<StorySection title="States">{board("controlled", "disabled")}</StorySection>
<StorySection title="Usage">{board("long")}</StorySection>
</StoryPage> </StoryPage>
), ),
} }
+21 -12
View File
@@ -80,20 +80,21 @@ function Long() {
) )
} }
const sections = [ // Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles, dividers and
{ id: "basic", label: "Search", node: <Basic showValue={false} /> }, // command boxes line up; sections with fewer specimens leave the remaining tracks empty.
{ id: "grouped", label: "Groups", node: <Grouped /> }, const Grid = ({ children }: { children: ReactNode }) => (
{ id: "long", label: "Long list", node: <Long /> }, <div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))] min-[1000px]:grid-cols-3">{children}</div>
] )
const Cell = ({ id, label, children }: { id: string; label: string; children: ReactNode }) => (
<div className="min-w-0">
<StateBoard min={10000} states={[{ id, label, children }]} />
</div>
)
const meta = { title: "Components/Search & Date/Command", component: Command, parameters: { layout: "padded" } } satisfies Meta<typeof Command> const meta = { title: "Components/Search & Date/Command", component: Command, parameters: { layout: "padded" } } satisfies Meta<typeof Command>
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
const board = (...ids: string[]) => (
<StateBoard min={360} 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. // Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = { export const All: Story = {
name: "Command", name: "Command",
@@ -101,9 +102,17 @@ export const All: Story = {
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="Command" /> <StoryPageHeader title="Command" />
<StorySection title="Overview">{board("basic")}</StorySection> <StorySection title="Variants">
<StorySection title="Variants">{board("grouped")}</StorySection> <Grid>
<StorySection title="Usage">{board("long")}</StorySection> <Cell id="basic" label="Search"><Basic showValue={false} /></Cell>
<Cell id="grouped" label="Groups"><Grouped /></Cell>
</Grid>
</StorySection>
<StorySection title="Usage">
<Grid>
<Cell id="long" label="Long list"><Long /></Cell>
</Grid>
</StorySection>
</StoryPage> </StoryPage>
), ),
} }
+44 -31
View File
@@ -12,10 +12,11 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
const today = new Date(2026, 9, 2) const today = new Date(2026, 9, 2)
const calendarProps = { today, startMonth: new Date(2026, 0), endMonth: new Date(2027, 11) } const calendarProps = { today, startMonth: new Date(2026, 0), endMonth: new Date(2027, 11) }
function Field({ label, id, children }: { label: string; id: string; children: ReactNode }) { // Label 14/20/500 in the default text color; a Disabled field keeps the secondary color at 0.6 (same as the Form pages).
function Field({ label, id, disabled, children }: { label: string; id: string; disabled?: boolean; children: ReactNode }) {
return ( return (
<div className="grid max-w-xs gap-2"> <div className="grid max-w-xs gap-2">
<label htmlFor={id} className="justify-self-start text-sm font-medium text-muted-foreground"> <label htmlFor={id} className={`justify-self-start text-sm font-medium ${disabled ? "text-muted-foreground opacity-60" : "text-foreground"}`}>
{label} {label}
</label> </label>
{children} {children}
@@ -39,16 +40,19 @@ function Selected() {
) )
} }
function FormatExamples() { function DefaultFormat() {
return ( return (
<div className="grid gap-3"> <Field label="기본 형식 · YYYY.MM.DD" id="dp-padded">
<Field label="기본 형식 · YYYY.MM.DD" id="dp-padded"> <DatePicker id="dp-padded" defaultValue={new Date(2026, 0, 9)} calendarProps={calendarProps} />
<DatePicker id="dp-padded" defaultValue={new Date(2026, 0, 9)} calendarProps={calendarProps} /> </Field>
</Field> )
<Field label="Custom format override" id="dp-custom"> }
<DatePicker id="dp-custom" defaultValue={new Date(2026, 9, 6)} format={(date) => date.toLocaleDateString("ko-KR")} calendarProps={calendarProps} />
</Field> function CustomFormat() {
</div> return (
<Field label="Custom format override" id="dp-custom">
<DatePicker id="dp-custom" defaultValue={new Date(2026, 9, 6)} format={(date) => date.toLocaleDateString("ko-KR")} calendarProps={calendarProps} />
</Field>
) )
} }
@@ -86,29 +90,27 @@ function DisabledDates() {
function Disabled() { function Disabled() {
return ( return (
<Field label="비활성" id="dp-disabled"> <Field label="비활성" id="dp-disabled" disabled>
<DatePicker id="dp-disabled" disabled defaultValue={new Date(2026, 9, 15)} /> <DatePicker id="dp-disabled" disabled defaultValue={new Date(2026, 9, 15)} />
</Field> </Field>
) )
} }
const sections = [ // Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles, dividers and
{ id: "empty", label: "Empty", node: <Empty /> }, // fields line up; sections with fewer specimens leave the remaining tracks empty.
{ id: "selected", label: "Selected", node: <Selected /> }, const Grid = ({ children }: { children: ReactNode }) => (
{ id: "format", label: "Format", node: <FormatExamples /> }, <div className="grid items-start gap-6 [grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr))] min-[1000px]:grid-cols-3">{children}</div>
{ id: "controlled", label: "Controlled", node: <Controlled showValue={false} /> }, )
{ id: "disabled-dates", label: "Disabled dates", node: <DisabledDates /> }, const Cell = ({ id, label, children }: { id: string; label: string; children: ReactNode }) => (
{ id: "disabled", label: "Disabled", node: <Disabled /> }, <div className="min-w-0">
] <StateBoard min={10000} states={[{ id, label, children }]} />
</div>
)
const meta = { title: "Components/Search & Date/Date Picker", component: DatePicker, parameters: { layout: "padded" } } satisfies Meta<typeof DatePicker> const meta = { title: "Components/Search & Date/Date Picker", component: DatePicker, parameters: { layout: "padded" } } satisfies Meta<typeof DatePicker>
export default meta export default meta
type Story = StoryObj<typeof 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. // Representative Story: the common Story page; the sections group the same specimens.
export const All: Story = { export const All: Story = {
name: "Date Picker", name: "Date Picker",
@@ -116,15 +118,26 @@ export const All: Story = {
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="Date Picker" /> <StoryPageHeader title="Date Picker" />
<StorySection title="Overview">{board("empty")}</StorySection>
{/* The field label already names the size, so the Medium specimen has no separate example title. */}
<StorySection title="Sizes"> <StorySection title="Sizes">
<div className="max-w-sm"> <Grid>
<Medium /> <Cell id="empty" label="Default"><Empty /></Cell>
</div> <Cell id="medium" label="Medium"><Medium /></Cell>
</Grid>
</StorySection>
<StorySection title="States">
<Grid>
<Cell id="selected" label="Selected"><Selected /></Cell>
<Cell id="controlled" label="Controlled"><Controlled showValue={false} /></Cell>
<Cell id="disabled-dates" label="Disabled dates"><DisabledDates /></Cell>
<Cell id="disabled" label="Disabled"><Disabled /></Cell>
</Grid>
</StorySection>
<StorySection title="Usage">
<Grid>
<Cell id="format-default" label="Default format"><DefaultFormat /></Cell>
<Cell id="format-custom" label="Custom format"><CustomFormat /></Cell>
</Grid>
</StorySection> </StorySection>
<StorySection title="States">{board("selected", "controlled", "disabled-dates", "disabled")}</StorySection>
<StorySection title="Usage">{board("format")}</StorySection>
</StoryPage> </StoryPage>
), ),
} }