style(storybook): unify form element story layouts
- Button: Sizes | Variants side by side, States as Brand / Standard Solid / Standard Outline matrices (3 / 2 / 1 columns), 8px button gap; the Interact row stays only in the verification "Brand States" Story - Input / Textarea / Select: state name in the field Label slot, repeated field Label and group titles removed, aria-label keeps field meaning with the visible state name; Select uses the Input field grid - Combobox / Date Picker: Sizes / States use the same state-name Label and Input grid; Usage unchanged - Toggle: top Variants / States in 2 columns, 8px toggle gap, 단독 toggle keeps its own width - Component code, props, tokens and verification Stories unchanged; component docs updated to match
This commit is contained in:
1 parent
e8c16f5292
commit
a7b56ff23e
14 files changed
+351
-187
No files matched your search
@@ -1,9 +1,8 @@
|
||||
import { useState } from "react"
|
||||
import { useState, type ReactNode } from "react"
|
||||
import { DownloadIcon } from "lucide-react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, type ButtonAppearance } from "@hm-design/ui"
|
||||
import { StateBoard, type BoardState } from "@/components/story-board"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
|
||||
|
||||
// Story structure follows ui_viz (implementations/web/storybook/Button.stories.jsx):
|
||||
// one representative "Button" Story shows kinds, sizes and states on a single screen; the same sections also exist
|
||||
@@ -126,8 +125,8 @@ function SizesBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
// `showNote={false}` keeps the representative page UI-only; the verification Story keeps the note and the Activated readout.
|
||||
function BrandStates({ showNote = true }: { showNote?: boolean }) {
|
||||
// Verification "Brand States" (note and Activated readout). The representative page shows Brand states in StateMatrix.
|
||||
function BrandStates() {
|
||||
const [count, setCount] = useState(0)
|
||||
return (
|
||||
<div className={stack}>
|
||||
@@ -140,12 +139,10 @@ function BrandStates({ showNote = true }: { showNote?: boolean }) {
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
{showNote ? <output aria-live="polite">Activated: {count}</output> : null}
|
||||
{showNote ? (
|
||||
<p className={note}>
|
||||
Hover와 Pressed는 Storybook이 고정한 상태입니다. Interact 위에서 실제 Hover·Pressed·포인터 이탈을 확인합니다. Disabled는 실행되지 않습니다.
|
||||
</p>
|
||||
) : null}
|
||||
<output aria-live="polite">Activated: {count}</output>
|
||||
<p className={note}>
|
||||
Hover와 Pressed는 Storybook이 고정한 상태입니다. Interact 위에서 실제 Hover·Pressed·포인터 이탈을 확인합니다. Disabled는 실행되지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -157,15 +154,13 @@ const standardStates = [
|
||||
["Disabled", { disabled: true }],
|
||||
] as const
|
||||
|
||||
// Without `title` the table sits under a StateBoard label (representative page) and drops the Role column:
|
||||
// every button already shows its role name, so the row header would repeat it.
|
||||
// `compact` (representative page only) tightens the cell padding so a 5x4 / 4x4 matrix stays short; buttons are untouched.
|
||||
function StandardStateTable({ appearance, title, compact = false }: { appearance: "solid" | "outline"; title?: string; compact?: boolean }) {
|
||||
// Verification "Standard States" table (rows = roles). The representative page uses StateMatrix (rows = states) instead.
|
||||
function StandardStateTable({ appearance, title }: { appearance: "solid" | "outline"; title?: string }) {
|
||||
return (
|
||||
<section className="min-w-0">
|
||||
{title ? <h3 className={heading}>{title}</h3> : null}
|
||||
<div className={`${title ? "mt-3 " : ""}min-w-0 max-w-full overflow-x-auto`}>
|
||||
<table className={`w-full border-collapse [&_th]:text-left ${compact ? "[&_td]:px-2 [&_td]:py-1 [&_th]:px-2 [&_th]:py-1" : "[&_td]:p-3 [&_th]:p-3"}`}>
|
||||
<table className="w-full border-collapse [&_th]:text-left [&_td]:p-3 [&_th]:p-3">
|
||||
<thead>
|
||||
<tr>
|
||||
{title ? <th scope="col">Role</th> : null}
|
||||
@@ -231,43 +226,92 @@ function DestructiveBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
// Page and section spacing come from the common compact Story page (story-page.css); this Story keeps no override.
|
||||
// 8px gap so the three Brand sizes stay on one line in the narrowest (Brand) column, same as the Variants rows.
|
||||
const compactRow = "flex flex-wrap items-center gap-2"
|
||||
// Equal two columns over the whole content width. StateBoard's own auto-fill grid left an empty third column
|
||||
// (2 items in a grid that fits 3), so the pairs are laid out here and each StateBoard holds one specimen.
|
||||
const twoColumns = "grid gap-6 min-[900px]:grid-cols-[repeat(2,minmax(0,1fr))]"
|
||||
// StateBoard lays its states out in an auto-fill grid. A board that holds one specimen inside a wide column would put it
|
||||
// in the first of several inner columns (about half width at 1920px), so `min` is set larger than any column to force one.
|
||||
const SingleColumnBoard = ({ states }: { states: BoardState[] }) => <StateBoard min={10000} states={states} />
|
||||
const columnsAuto = (min: number) => `[grid-template-columns:repeat(auto-fit,minmax(min(100%,${min}px),1fr))]`
|
||||
// Representative-page layout (2026-11-12 compact comparison). Page and section spacing come from the common Story page
|
||||
// (story-page.css); only the arrangement below is local. Buttons are rendered with their public API and never restyled.
|
||||
// Group label: the StateBoard H3 style (14/20/500, secondary). `data-sb-board` gives the block the common one-step section indent.
|
||||
const groupLabel = "m-0 text-sm font-medium text-muted-foreground"
|
||||
|
||||
// 8px gap keeps all three Variants groups on one line at the 1280px PC width (Brand 3 + Solid 5 + Outline 4 buttons in a 3-column grid); narrower widths wrap.
|
||||
function CompactRoleRow({ appearance }: { appearance: (typeof appearances)[number][0] }) {
|
||||
// Sizes / Variants (top, side by side): one row per group, label on a fixed 128px left column so every group title and first
|
||||
// button line up. At 640px and below the label moves above its buttons. Buttons keep the page-wide 8px gap, across and between
|
||||
// rows (same as the States matrices), and wrap only when too narrow.
|
||||
function GroupRows({ groups }: { groups: { id: string; label: string; children: ReactNode }[] }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{rolesFor(appearance).map(([variant, label]) => (
|
||||
<Button key={variant} variant={variant} appearance={appearance}>
|
||||
{label}
|
||||
</Button>
|
||||
<div data-sb-board="" className="grid gap-2 max-sm:gap-5">
|
||||
{groups.map(({ id, label, children }) => (
|
||||
<section key={id} data-overview-state={id} aria-label={label} className="flex min-w-0 items-center gap-4 max-sm:flex-col max-sm:items-start max-sm:gap-2">
|
||||
<h3 className={`${groupLabel} w-32 shrink-0 max-sm:w-auto`}>{label}</h3>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">{children}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactSizeRow({ appearance }: { appearance: "brand" | "solid" }) {
|
||||
// States (bottom): Brand | Standard Solid | Standard Outline columns, title on top, all tops aligned. The columns use the
|
||||
// whole width, shared in proportion to their matrices (about 301 / 420 / 338px, never narrower than the matrix); each matrix
|
||||
// keeps its 8px button gap and sits at the start of its column. 3 columns from a 1250px preview (the three need about 1106px
|
||||
// of content plus the page margins), 2 columns (Brand + Solid, then Outline) from 890px, 1 column below; a 1-column matrix
|
||||
// wider than the screen scrolls in its own box only (the page never scrolls sideways).
|
||||
function GroupColumns({ groups }: { groups: { id: string; label: string; children: ReactNode }[] }) {
|
||||
return (
|
||||
<div className={compactRow}>
|
||||
{sizes.map(([size, label]) => (
|
||||
<Button key={size} appearance={appearance} size={size}>
|
||||
{label}
|
||||
</Button>
|
||||
<div
|
||||
data-sb-board=""
|
||||
className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 min-[890px]:grid-cols-[minmax(max-content,301fr)_minmax(max-content,420fr)] min-[1250px]:grid-cols-[minmax(max-content,301fr)_minmax(max-content,420fr)_minmax(max-content,338fr)]"
|
||||
>
|
||||
{groups.map(({ id, label, children }) => (
|
||||
<section key={id} data-overview-state={id} aria-label={label} className="grid min-w-0 content-start gap-2">
|
||||
<h3 className={groupLabel}>{label}</h3>
|
||||
{children}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Representative Story: the common Story page (Sizes → Variants → States) with the ui_viz Button "All" boards.
|
||||
// States: one matrix per treatment, rows = Default / Hover / Pressed / Disabled (standardStates, the same 4 rows for Brand;
|
||||
// the live Interact button stays on the verification "Brand States"), columns = that treatment's colors in the Variants order.
|
||||
// Columns follow their buttons (auto table layout). 4px cell padding gives the same 8px gap as Sizes / Variants between
|
||||
// buttons, rows, the state label and the color header; the outer edge keeps none (pl-0 / last:pr-0) so the three matrices
|
||||
// fit one row at 1440px. Buttons sit centered under their color and are never resized.
|
||||
function StateMatrix({ appearance, states }: { appearance: ButtonAppearance; states: readonly (readonly [string, Record<string, unknown>])[] }) {
|
||||
const colors = rolesFor(appearance)
|
||||
return (
|
||||
<div className="min-w-0 max-w-full overflow-x-auto">
|
||||
<table className="border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className={`${groupLabel} pb-1 pl-0 pr-1 text-left`}>
|
||||
<span className="sr-only">State</span>
|
||||
</th>
|
||||
{colors.map(([variant, color]) => (
|
||||
<th scope="col" key={variant} className={`${groupLabel} px-1 pb-1 text-center last:pr-0`}>
|
||||
{color}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{states.map(([state, props]) => (
|
||||
<tr key={state}>
|
||||
<th scope="row" className={`${groupLabel} py-1 pl-0 pr-1 text-left whitespace-nowrap`}>
|
||||
{state}
|
||||
</th>
|
||||
{colors.map(([variant, color]) => (
|
||||
<td key={variant} className="p-1 text-center last:pr-0">
|
||||
<Button variant={variant} appearance={appearance} {...props}>
|
||||
{color}
|
||||
</Button>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Representative Story: the common Story page (Sizes → Variants → States).
|
||||
// The former Overview button was the default Brand Medium button already shown under Sizes, so Overview is merged into Sizes.
|
||||
// Named after the component, as in ui_viz. It is the only Button entry in the default sidebar, so Storybook shows Button as a single 1-depth item.
|
||||
export const All: Story = {
|
||||
@@ -276,29 +320,44 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Button" />
|
||||
<StorySection title="Sizes">
|
||||
{/* Same columns as Variants below (Brand / Standard Solid / Standard Outline): Brand and Standard Solid sizes line up with
|
||||
their Variants columns and the third column stays empty. */}
|
||||
<div className={`grid gap-6 ${columnsAuto(260)} min-[960px]:[grid-template-columns:minmax(0,234fr)_minmax(0,353fr)_minmax(0,271fr)]`}>
|
||||
<SingleColumnBoard states={[{ id: "sizes-brand", label: "Brand", children: <CompactSizeRow appearance="brand" /> }]} />
|
||||
<SingleColumnBoard states={[{ id: "sizes-solid", label: "Standard Solid", children: <CompactSizeRow appearance="solid" /> }]} />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<div className={`grid gap-6 ${columnsAuto(260)} min-[960px]:[grid-template-columns:minmax(0,234fr)_minmax(0,353fr)_minmax(0,271fr)]`}>
|
||||
{appearances.map(([appearance, label]) => (
|
||||
<SingleColumnBoard key={appearance} states={[{ id: `roles-${appearance}`, label, children: <CompactRoleRow appearance={appearance} /> }]} />
|
||||
))}
|
||||
</div>
|
||||
</StorySection>
|
||||
{/* Sizes | Variants on one row from a 1080px preview (1280px screen with the 180px LNB), sharing the whole width in two
|
||||
equal halves (never narrower than their rows). Narrower previews stack the two sections. */}
|
||||
<StorySectionGrid className="grid-cols-1 items-start min-[1080px]:grid-cols-[minmax(max-content,1fr)_minmax(max-content,1fr)]">
|
||||
<StorySection title="Sizes">
|
||||
<GroupRows
|
||||
groups={appearances.slice(0, 2).map(([appearance, label]) => ({
|
||||
id: `sizes-${appearance}`,
|
||||
label,
|
||||
children: sizes.map(([size, sizeLabel]) => (
|
||||
<Button key={size} appearance={appearance} size={size}>
|
||||
{sizeLabel}
|
||||
</Button>
|
||||
)),
|
||||
}))}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
<GroupRows
|
||||
groups={appearances.map(([appearance, label]) => ({
|
||||
id: `roles-${appearance}`,
|
||||
label,
|
||||
children: rolesFor(appearance).map(([variant, color]) => (
|
||||
<Button key={variant} variant={variant} appearance={appearance}>
|
||||
{color}
|
||||
</Button>
|
||||
)),
|
||||
}))}
|
||||
/>
|
||||
</StorySection>
|
||||
</StorySectionGrid>
|
||||
<StorySection title="States">
|
||||
<div className="grid gap-6">
|
||||
<SingleColumnBoard states={[{ id: "brand-states", label: "Brand", wide: true, children: <BrandStates showNote={false} /> }]} />
|
||||
<div className={twoColumns}>
|
||||
<SingleColumnBoard states={[{ id: "standard-solid-states", label: "Standard Solid", children: <StandardStateTable appearance="solid" compact /> }]} />
|
||||
<SingleColumnBoard states={[{ id: "standard-outline-states", label: "Standard Outline", children: <StandardStateTable appearance="outline" compact /> }]} />
|
||||
</div>
|
||||
</div>
|
||||
<GroupColumns
|
||||
groups={appearances.map(([appearance, label]) => ({
|
||||
id: `${appearance}-states`,
|
||||
label,
|
||||
children: <StateMatrix appearance={appearance} states={standardStates} />,
|
||||
}))}
|
||||
/>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user