style(storybook): unify page hierarchy and compact layouts
This commit is contained in:
1 parent
cda60840d8
commit
ff55a805d5
21 files changed
+115
-85
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
Reference in new issue
Block a user