style(storybook): unify page hierarchy and compact layouts

This commit is contained in:
shkim4 committed 2026-11-11 11:28:58 +09:00
1 parent cda60840d8
commit ff55a805d5
21 files changed
+115 -85

No files matched your search

+2 -13
View File
@@ -231,18 +231,7 @@ function DestructiveBoard() {
)
}
// Compact PC layout of the representative page (Storybook presentation only; Button itself is unchanged).
// Page and section spacing are tightened for this Story only through `compactPage`; the shared story-page.css is not edited.
// The important modifier is required because the shared rules use the same specificity.
const compactPage = [
"py-4!",
"[&_.sb-page-header]:pb-3!",
"[&_.sb-page-title]:mt-1!",
"[&_.sb-page-title]:mb-2!",
"[&_.sb-page-section]:mt-5!",
"[&_.sb-page-section-header]:pb-2!",
"[&_.sb-page-section-header]:mb-3!",
].join(" ")
// 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
@@ -285,7 +274,7 @@ export const All: Story = {
name: "Button",
parameters: { layout: "fullscreen" },
render: () => (
<StoryPage className={compactPage}>
<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
+4 -2
View File
@@ -10,12 +10,14 @@ export type BoardState = { id: string; label: string; note?: string; noteAfter?:
// `fixed` uses columns of exactly `min` px instead of stretching them (1fr). Specimens whose popup is anchored to a Radix
// trigger need whole-pixel columns: a fractional column (e.g. 358.4px) puts the trigger edge between device pixels while the
// popper rounds the popup to whole pixels, so the two outlines end up 1px apart.
// The labels are H3 (14/20/500, secondary) under the section's divider: they carry no divider of their own, and rows sit
// 24px apart (the section gap of story-page.css).
export function StateBoard({ states, min = 320, fixed = false }: { states: BoardState[]; min?: number; fixed?: boolean }) {
// Inside a StorySection the labels sit one heading level below the section title.
const Label = useContext(StorySectionContext) ? "h3" : "h2"
return (
<div
className={`grid items-start gap-x-6 gap-y-10 ${fixed ? "[grid-template-columns:repeat(auto-fill,min(100%,var(--sb-state-min,320px)))]" : "[grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-state-min,320px)),1fr))]"}`}
className={`grid items-start gap-x-6 gap-y-6 ${fixed ? "[grid-template-columns:repeat(auto-fill,min(100%,var(--sb-state-min,320px)))]" : "[grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--sb-state-min,320px)),1fr))]"}`}
style={{ "--sb-state-min": `${min}px` } as CSSProperties}
>
{states.map(({ id, label, note, noteAfter, wide, children }) => (
@@ -25,7 +27,7 @@ export function StateBoard({ states, min = 320, fixed = false }: { states: Board
data-overview-state={id}
aria-label={label}
>
<header className="grid gap-2 border-b border-black/10 pb-2">
<header className="grid gap-1">
<Label className="text-sm font-medium text-muted-foreground">{label}</Label>
{note && !noteAfter ? <p className="text-sm text-muted-foreground">{note}</p> : null}
</header>
+33 -14
View File
@@ -1,8 +1,15 @@
/* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page.
Inner (fluid; 16px each side up to 480px, growing to 60px at 768px; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, no divider below) reuse Foundation / Color.
Section reuses the Center Graphic / 교량 split: heading + description, a divider, then the specimen. */
2026-11-11 compact document rhythm, shared by Web / Chart / OrgChart (SW and CENTER pages use the same frame only):
H1 page title 28/36/700 (Page Header title) → description 14/20/400 secondary, 8px below (Page Header description)
→ 24px → H2 section title 16/24/700 (body-md) over a thin divider → 12px → specimen; sections 24px apart (Dashboard gap);
H3 specimen labels 14/20/500 secondary, no divider of their own (story-board.tsx). Top / bottom gutter 16–24px.
Side margins are unchanged (16px up to 480px, growing to 60px at 768px): the SW breakpoints 2040 / 1125 / 787 depend on them.
No design token is added; --sb-page-* only names existing component token values for this Storybook frame. */
.sb-page {
--sb-page-gutter: clamp(16px, 3vw, 40px);
--sb-page-gutter: clamp(16px, 2vw, 24px);
--sb-page-section-gap: 24px;
--sb-page-section-title-font-size: var(--hm-dialog-body-font-size);
--sb-page-section-title-line-height: var(--hm-dialog-body-line-height);
box-sizing: border-box;
width: 100%;
min-width: 0;
@@ -20,38 +27,46 @@
margin-inline: auto;
}
/* No bottom padding: the header's margin collapses with the next block's own top margin (a section's 24px), so the first
content sits 24px below the title whether it is a section, a section grid or a plain preview. */
.sb-page .sb-page-header {
padding-bottom: 24px;
margin-bottom: var(--sb-page-section-gap);
font-size: var(--hm-label-font-size);
line-height: var(--hm-label-line-height);
overflow-wrap: anywhere;
}
.sb-page .sb-page-header .sb-page-kicker {
margin: 0 0 12px;
margin: 0 0 4px;
color: var(--hm-sem-fg-secondary);
font-size: var(--hm-button-font-size-sm);
font-weight: var(--hm-font-weight-bold);
}
.sb-page .sb-page-header .sb-page-title {
margin: 8px 0 12px;
margin: 0;
font-size: var(--hm-page-header-title-font-size);
line-height: var(--hm-page-header-title-line-height);
font-weight: var(--hm-page-header-title-font-weight);
}
.sb-page .sb-page-header .sb-page-description { margin: 0 0 12px; font-weight: var(--hm-input-font-weight); }
.sb-page .sb-page-header .sb-page-description {
margin: var(--hm-page-header-description-gap) 0 0;
color: var(--hm-page-header-description-color);
font-size: var(--hm-page-header-description-font-size);
line-height: var(--hm-page-header-description-line-height);
font-weight: var(--hm-input-font-weight);
}
.sb-page .sb-page-section { margin-top: 32px; scroll-margin-top: 16px; }
.sb-page .sb-page-section { margin-top: var(--sb-page-section-gap); scroll-margin-top: 16px; }
.sb-page .sb-page-section-header {
padding-bottom: 12px;
margin-bottom: 16px;
padding-bottom: 8px;
margin-bottom: 12px;
border-bottom: 1px solid var(--hm-sem-stroke-input-default);
overflow-wrap: anywhere;
}
.sb-page .sb-page-section-header .sb-page-section-title {
margin: 0;
font-size: var(--hm-card-title-font-size);
line-height: var(--hm-card-title-line-height);
font-weight: var(--hm-card-title-font-weight);
font-size: var(--sb-page-section-title-font-size);
line-height: var(--sb-page-section-title-line-height);
font-weight: var(--hm-font-weight-bold);
}
.sb-page .sb-page-section-header .sb-page-section-description {
margin: 4px 0 0;
@@ -61,9 +76,13 @@
font-weight: var(--hm-input-font-weight);
}
.sb-page .sb-page-section-body { min-width: 0; }
/* Sections side by side (StorySectionGrid): grid items keep their margins, so the grid owns the row gap instead.
Columns and alignment stay with the Story (className): this unlayered rule would override Tailwind's layered grid-cols-*. */
.sb-page .sb-page-section-grid { display: grid; gap: var(--sb-page-section-gap) 24px; margin-top: var(--sb-page-section-gap); }
.sb-page .sb-page-section-grid > .sb-page-section { margin-top: 0; }
/* Notice above a page title (StoryNotice). Reuses .sb-page / .sb-page-inner (width and side margin are not redefined).
The frame drops its bottom gutter and pulls the next page up so the notice sits 24px above the title block's own top gutter. */
The frame drops its bottom gutter and pulls the next page up so the notice sits 24px above the page title. */
.sb-page.sb-page-notice {
padding-bottom: 0;
margin-bottom: calc(24px - var(--sb-page-gutter));
+6
View File
@@ -70,6 +70,12 @@ export function StorySection({
)
}
// Sections side by side (e.g. two charts per row). The grid owns the row gap (the section gap), so rows line up the same
// way as stacked sections; columns and alignment come from `className`.
export function StorySectionGrid({ className, children }: { className?: string; children: ReactNode }) {
return <div className={className ? `sb-page-section-grid ${className}` : "sb-page-section-grid"}>{children}</div>
}
export const STORY_KICKER = {
foundation: "HM DESIGN SYSTEM / FOUNDATION",
components: "HM DESIGN SYSTEM / COMPONENTS",
+2 -3
View File
@@ -159,9 +159,8 @@ export const All: Story = {
name: "Tabs",
parameters: { layout: "fullscreen", controls: { disable: true } },
render: () => (
// `py-4!` trims this page's top and bottom padding (30-40px by viewport width) so the page fits a 900px-high PC screen without
// shrinking any component; the important modifier is needed because the shared rule has the same specificity. Section gaps stay 32px.
<StoryPage className="py-4!">
// The common compact Story page (16–24px gutter, 24px section gap) fits this page on a 900px-high PC screen without an override.
<StoryPage>
<StoryPageHeader title="Tabs" />
<StorySection title="Variants">
{/* The former States section (Overflow) is the third Variants item. */}
+3 -3
View File
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
import { Toggle, ToggleGroup, ToggleGroupItem } 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"
// Toggle is a persistent pressed-state control. The representative Story shows each state with real, clickable toggles; the same
// specimens also exist as Stories tagged `verification` (hidden from the sidebar). Icon-only toggles carry an `aria-label`.
@@ -97,14 +97,14 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader title="Toggle" />
<div className="grid gap-x-6 min-[1000px]:grid-cols-3">
<StorySectionGrid className="grid-cols-1 min-[1000px]:grid-cols-3">
<StorySection title="Variants">
<StateBoard min={10000} states={[specimen("offon")]} />
</StorySection>
<StorySection title="States">
<StateBoard min={10000} states={[specimen("disabled")]} />
</StorySection>
</div>
</StorySectionGrid>
<StorySection title="단독 · 연결 그룹">
<SpecimenGrid
states={[