diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md
index 94b3d79..ab6ba20 100644
--- a/WORKING_CONTEXT.md
+++ b/WORKING_CONTEXT.md
@@ -1,5 +1,18 @@
# Working Context
+## 2026-11-11 — SW 공통 안내 문구를 H1 아래로 이동 (미커밋)
+
+- `withSWNotice`(src/sw/sw-story-notice.tsx)는 `StoryNoticeContext`로 문구만 전달하고, 공통 `StoryPageHeader`가 H1 바로 아래(설명 앞)에 `StoryNotice`를 렌더링. 별도 페이지 프레임(`.sb-page-notice`)은 제거.
+- 안내 문구: H1 아래 8px, Page Header description 글꼴·보조색, 아이콘 14px(텍스트와 6px, 첫 줄 세로 중앙), 박스 없음. 첫 섹션까지는 header margin 24px. Context 제공자가 없는 Web/Chart/OrgChart 등은 렌더 결과 변화 없음.
+
+## 2026-11-11 — Storybook 페이지 위계·여백 공통화 (ff55a80)
+
+- 공통 프레임 `src/components/story-page.css`: 위·아래 여백 16–24px, H1 28/36/700 margin 0, 설명 Page Header description 토큰(8px·보조색), 제목→첫 내용·섹션 사이 24px(`--sb-page-section-gap`, header margin이 다음 블록 margin과 상쇄), H2 16/24/700(`--sb-page-section-title-*` = dialog body 토큰), 섹션 제목 구분선 8 + 12px. 좌우 여백·SW 기준 폭(2040/1125/787)은 그대로.
+- `StorySectionGrid`(story-page.tsx) 추가: 나란히 놓인 섹션의 row gap을 grid가 소유. Chart 7개·Toggle이 사용. `StateBoard` 라벨 구분선 제거, 행 간격 40 → 24px.
+- Button `compactPage`·Tabs `py-4!` 덮어쓰기 제거, Foundation Layout 중복 규칙 제거, 폰트 Weight 카드 박스 제거.
+- Chart 섹션명을 카드 제목과 겹치지 않는 변형명(Basic·Label·Solid 등)으로 통일, SW 제목 2개(`SW Popup — 속성창`, `SW Viewer — 설정창`) 사이드바 기준으로 정리. SW Story 전용 묶음(`sw-story-stack` 섹션 묶음, Footer 너비 행)은 공통 섹션 간격을 쓰도록 조정 — SW 컴포넌트 CSS는 변경 없음.
+- 기준 문서: `docs/guidelines/프로젝트-지침.md` §6.3 Storybook 페이지 프레임 항목.
+
## 2026-11-05 — Step Header 프로세스 영역 양 끝 직각
- 사용자 결정: `SWProcessHeader appearance="multi"`만 프로세스 영역(파일명 옆·창 영역 옆) 위 모서리를 직각으로(`default`는 Strana 6px·덮개 5px 유지 — 처음 두 타입 모두로 들었다가 사용자 정정). `sw-process-header.css` multi 블록에 바탕 `::before`·양 끝 덮개 `border-radius: 0` 1줄. 단계 탭 캡슐·화살표·사다리꼴 면·색·높이 변경 없음. Header 최외곽은 원래 직각.
diff --git a/docs/guidelines/프로젝트-지침.md b/docs/guidelines/프로젝트-지침.md
index 947d6b8..ee2f38c 100644
--- a/docs/guidelines/프로젝트-지침.md
+++ b/docs/guidelines/프로젝트-지침.md
@@ -157,6 +157,7 @@ HM 지정값이 있는 색상, radius, control height, typography 등은 Theme T
- 컴포넌트가 실제 지원하는 default, variant, size, disabled 및 주요 상태를 확인한다. invalid, open 등 해당 컴포넌트에 실제로 있는 상태만 다루고, 없는 상태를 문서화를 위해 만들지 않는다.
- Storybook은 구현 Reference Catalog이자 컴포넌트 단위의 검수와 사용법 문서화 도구다. 신규 UI 구현 전 유사 대표/PRIMARY Story를 먼저 확인한다. `Demo/App` 화면은 여러 컴포넌트를 조합한 적용 결과 확인용이며, Demo의 페이지·타일 UI를 Story의 기준으로 쓰지 않는다.
- 컴포넌트 Story는 현재 HM 대표 Story의 공통 구조를 따른다. 컴포넌트 이름의 대표 Story 하나에 종류·크기·상태를 한 화면으로 모으고, 같은 구역을 `verification` 태그 Story(예: Roles, Sizes, Brand States)로도 둔다. `verification` Story는 메뉴와 검색에서 기본으로 숨기고(`.storybook/main.ts`의 `tags`) 직접 URL과 사이드바 태그 필터로 연다. HM 전용 Story(Playground, Compatibility)도 같은 태그로 숨긴다. 기본 사이드바에는 컴포넌트가 하위 메뉴 없는 1뎁스 항목 하나로만 보이고(대표 Story 하나만 노출되면 Storybook이 컴포넌트 항목에 합친다), 클릭하면 한 페이지에서 전체 검수 내용이 열린다. Story 이름과 순서, 행·열 구성은 현재 HM 유사 대표 Story의 공통 구성을 먼저 따른다. 외부 ui_viz Story는 제공된 경우에만 충돌하지 않는 참고로 사용한다.
+- Storybook 페이지 프레임(2026-11-11 확정)은 `src/components/story-page.tsx`·`story-page.css`의 `StoryPage`·`StoryPageHeader`·`StorySection`·`StorySectionGrid`가 공통으로 소유한다. Web·Chart·OrgChart(Admin)의 문서 위계는 H1 페이지 제목 28/36/700 → 설명 14/20/400 보조색(제목 아래 8px) → 24px → H2 섹션 제목 16/24/700과 얇은 구분선 → 12px → 예제이며, 섹션 사이는 24px, `StateBoard`의 H3 라벨은 14/20/500 보조색이고 자체 구분선을 두지 않는다. 위·아래 여백은 16–24px, 좌우 여백은 기존 값(480px까지 16px, 768px부터 60px)을 유지한다. 값은 기존 Component Token을 `--sb-page-*`로 참조하며 새 Design Token이 아니다. 섹션을 나란히 놓을 때는 `StorySectionGrid`를 쓰고, Story별 `!important`·`[&_.sb-page-*]` 덮어쓰기로 간격을 바꾸지 않는다. SW와 노선도(CENTER)는 이 페이지 프레임과 섹션 간격만 공유하고 컴포넌트 크기·글꼴·캔버스·카드 규격은 각자 유지한다. 페이지 제목은 사이드바 이름을 따르며, 한 그룹에 Story가 여럿이면 `그룹 — Story 이름`(예: `SW Header — 화면 너비`)으로 쓴다. 섹션 제목은 페이지 제목과 예제 카드 제목을 반복하지 않는 짧은 변형명(예: Chart `Basic`, `Label`, `Solid`)으로 쓴다.
- 대표 Story의 Focus 구역은 pseudo-state 복제 대신 `play` 함수로 실제 요소에 focus를 준다(ui_viz도 Focus는 정적 specimen을 만들지 않는다). hover 같은 상태가 ui_viz에 없으면 `.storybook/pseudo-states.ts`를 쓰지 않는다. 구역 배치 helper는 `src/components/story-board.tsx`의 `StateBoard`를 쓴다.
- hover, pressed 같은 pseudo state는 Storybook이 `:hover`/`:active` 규칙을 `data-sb-hover`/`data-sb-active` 규칙으로 복제해 고정한다(`.storybook/pseudo-states.ts`). 복제만 하고 값을 새로 정의하지 않으므로 실제 디자인을 보정하는 용도로 쓰지 않는다. Story에서는 `data-sb-*` 속성만 붙인다.
- ui_viz에서 값이 `TBD`인 상태(예: Button Focus)는 별도 specimen을 만들지 않고 키보드로 확인하는 안내만 둔다. ui_viz에 없는 상태(예: Button `aria-invalid`)와 `REFERENCE_ONLY` 항목은 검수 Story에 넣지 않는다. shadcn 호환 API는 `Compatibility` Story로 분리한다.
diff --git a/src/chart/area-chart.stories.tsx b/src/chart/area-chart.stories.tsx
index 7cca540..afe16a8 100644
--- a/src/chart/area-chart.stories.tsx
+++ b/src/chart/area-chart.stories.tsx
@@ -9,7 +9,7 @@ import {
AreaStraightColorLabelExample,
AreaStraightExample,
} from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. The eight examples share the Line Chart data: connection styles as a plain area (left) and with Dots and
// value Labels (right), then Stacked (running total) and Expanded (each month normalized to 100%).
@@ -26,7 +26,7 @@ export const Area: Story = {
{/* Same grid and order as Line Chart, plus a Stacked | Expanded row. */}
-
+
),
}
diff --git a/src/chart/bar-chart.stories.tsx b/src/chart/bar-chart.stories.tsx
index 331e45e..6bd84ed 100644
--- a/src/chart/bar-chart.stories.tsx
+++ b/src/chart/bar-chart.stories.tsx
@@ -5,7 +5,7 @@ import {
SignedBarChartExample,
StackedBarChartExample,
} from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// shadcn/ui Chart (Recharts) with HM tokens, from the public @hm-design/chart entries. All values are sample data.
// Narrow widths: use the toolbar Viewport presets (HM Mobile · 390). The chart is fluid.
@@ -24,7 +24,7 @@ export const Bar: Story = {
{/* Two sections per row from 1024px (HM desktop boundary), one below. Source order is the mobile order; on desktop it reads
row by row: Grouped | Horizontal, then Stacked | Positive / Negative. On desktop every row has the same height and each
card fills its section, so card tops, bottoms and chart areas line up. */}
-
+
@@ -37,7 +37,7 @@ export const Bar: Story = {
-
+
),
}
diff --git a/src/chart/donut-chart.stories.tsx b/src/chart/donut-chart.stories.tsx
index e061828..9f0abea 100644
--- a/src/chart/donut-chart.stories.tsx
+++ b/src/chart/donut-chart.stories.tsx
@@ -6,7 +6,7 @@ import {
DonutPlainExample,
DonutSolidExample,
} from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. The four examples share one data set, color mapping and item order.
const meta = {
@@ -22,11 +22,11 @@ export const Donut: Story = {
{/* Same grid as Bar and Line Chart: two per row from 1024px, one below; source order is the mobile order. */}
-
+
),
}
diff --git a/src/chart/line-chart.stories.tsx b/src/chart/line-chart.stories.tsx
index 1f6e0ff..ef8cfc3 100644
--- a/src/chart/line-chart.stories.tsx
+++ b/src/chart/line-chart.stories.tsx
@@ -7,7 +7,7 @@ import {
LineStraightColorLabelExample,
LineStraightExample,
} from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. All six examples share one data set and axis range, so connection styles compare directly: curve, straight
// and step, each as a plain line (left) and with Dots and value labels (right).
@@ -25,7 +25,7 @@ export const Line: Story = {
{/* Same grid as Bar Chart: two per row from 1024px, one below. Source order is the mobile order; desktop reads row by row:
Curve | Curve + labels, Straight | Straight + colored labels, Step | Step + labels. */}
-
+
),
}
diff --git a/src/chart/pie-chart.stories.tsx b/src/chart/pie-chart.stories.tsx
index f991896..b115cf1 100644
--- a/src/chart/pie-chart.stories.tsx
+++ b/src/chart/pie-chart.stories.tsx
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { PieLabelExample, PieLabelListExample, PiePlainExample, PieSolidExample, PieStackedExample } from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. All pies share one item list and color mapping; the stacked pie draws two series as concentric rings.
const meta = {
@@ -16,23 +16,23 @@ export const Pie: Story = {
{/* Same grid as Bar, Line and Donut Chart: two per row from 1024px, one below; source order is the mobile order. */}
-
-
+
+
-
+
-
+
-
+
-
+
-
+
),
}
diff --git a/src/chart/radar-chart.stories.tsx b/src/chart/radar-chart.stories.tsx
index f727cc0..0a41151 100644
--- a/src/chart/radar-chart.stories.tsx
+++ b/src/chart/radar-chart.stories.tsx
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { RadarCustomLabelExample, RadarDotExample, RadarLinesOnlyExample, RadarPlainExample } from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. All radars share one data set (six axes, two series) and the 0-100 axis range.
const meta = {
@@ -16,20 +16,20 @@ export const Radar: Story = {
{/* Same grid as the other Chart pages: two per row from 1024px, one below. */}
-
-
+
+
-
+
-
+
-
+
-
+
),
}
diff --git a/src/chart/radial-chart.stories.tsx b/src/chart/radial-chart.stories.tsx
index f88e466..d545062 100644
--- a/src/chart/radial-chart.stories.tsx
+++ b/src/chart/radial-chart.stories.tsx
@@ -7,7 +7,7 @@ import {
RadialStackedExample,
RadialTextExample,
} from "@hm-design/chart/examples"
-import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
+import { StoryPage, StoryPageHeader, StorySection, StorySectionGrid } from "@/components/story-page"
// Sample data only. One example per official shadcn/ui radial chart (radial-simple, -label, -grid, -text, -shape, -stacked).
const meta = {
@@ -23,26 +23,26 @@ export const Radial: Story = {
{/* Same grid as the other Chart pages: two per row from 1024px, one below; source order is the mobile order. */}
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
),
}
diff --git a/src/components/button.stories.tsx b/src/components/button.stories.tsx
index eca8dbf..3279791 100644
--- a/src/components/button.stories.tsx
+++ b/src/components/button.stories.tsx
@@ -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: () => (
-
+
{/* Same columns as Variants below (Brand / Standard Solid / Standard Outline): Brand and Standard Solid sizes line up with
diff --git a/src/components/story-board.tsx b/src/components/story-board.tsx
index 2d5457c..6311443 100644
--- a/src/components/story-board.tsx
+++ b/src/components/story-board.tsx
@@ -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 (
: null}
diff --git a/src/components/story-page.css b/src/components/story-page.css
index 6c879db..0987f0b 100644
--- a/src/components/story-page.css
+++ b/src/components/story-page.css
@@ -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,29 +76,29 @@
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. */
-.sb-page.sb-page-notice {
- padding-bottom: 0;
- margin-bottom: calc(24px - var(--sb-page-gutter));
-}
+/* Group notice under the page title (StoryNotice inside StoryPageHeader): 8px below the H1, same type and color as the
+ Page Header description (one level below the title), no box. The header's own bottom margin keeps the first section 24px below. */
.sb-page .sb-notice {
display: flex;
align-items: flex-start;
- gap: 8px;
- margin: 0;
- font-size: var(--hm-label-font-size);
- line-height: var(--hm-label-line-height);
+ gap: 6px;
+ margin: var(--hm-page-header-description-gap) 0 0;
+ 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);
- color: var(--hm-sem-fg-primary);
+ color: var(--hm-page-header-description-color);
overflow-wrap: anywhere;
}
.sb-page .sb-notice-icon {
flex: none;
- width: 20px;
- height: 20px;
- /* centers the icon on the first text line even when the line height differs from 20px */
- margin-block: calc((var(--hm-label-line-height) - 20px) / 2);
+ width: 14px;
+ height: 14px;
+ /* centers the 14px icon on the first text line (20px line height → 3px above and below) */
+ margin-block: calc((var(--hm-page-header-description-line-height) - 14px) / 2);
}
.sb-page .sb-notice-text { min-width: 0; }
diff --git a/src/components/story-page.tsx b/src/components/story-page.tsx
index a52b61c..9fbd422 100644
--- a/src/components/story-page.tsx
+++ b/src/components/story-page.tsx
@@ -1,4 +1,4 @@
-import { createContext, type ReactNode } from "react"
+import { createContext, useContext, type ReactNode } from "react"
import "./story-page.css"
// Storybook presentation only (not part of @hm-design/ui): the common frame of a representative Story page.
@@ -13,35 +13,35 @@ export function StoryPage({ className, children }: { className?: string; childre
return
{children}
}
+// Group notice shown directly under the page title (H1 → notice → description). A Story group opts in through a decorator
+// that provides this context (e.g. src/sw/sw-story-notice.tsx); without a provider nothing is rendered.
+export const StoryNoticeContext = createContext(null)
+
// Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category.
// Hidden verification Stories keep passing it so their page structure stays unchanged.
export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) {
+ const notice = useContext(StoryNoticeContext)
return (
{kicker ?
{kicker}
: null}
{title}
+ {notice ? {notice} : null}
{description ?
{description}
: null}
)
}
-// Notice row (icon + text) shown above a Story page title. Reusable by any Story group through a decorator
-// (e.g. src/sw/sw-story-notice.tsx); nothing renders it unless a group opts in. It owns a page frame whose Inner is the
-// same as StoryPage's, and cancels its own bottom gutter so the page title below stays where it was.
-// Icon: Group 1171290812.svg (circle + "!"), colors via currentColor.
+// Notice row (icon + text) under the page title, one level below it (Page Header description type and color).
+// No box, background or border. Icon: Group 1171290812.svg (circle + "!"), colors via currentColor.
export function StoryNotice({ children }: { children: ReactNode }) {
return (
-
-
-
-
- {children}
-
-
-
+
+
+ {children}
+
)
}
@@ -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
{children}
+}
+
export const STORY_KICKER = {
foundation: "HM DESIGN SYSTEM / FOUNDATION",
components: "HM DESIGN SYSTEM / COMPONENTS",
diff --git a/src/components/tabs.stories.tsx b/src/components/tabs.stories.tsx
index cd1ec30..0ec7b3f 100644
--- a/src/components/tabs.stories.tsx
+++ b/src/components/tabs.stories.tsx
@@ -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.
-
+ // The common compact Story page (16–24px gutter, 24px section gap) fits this page on a 900px-high PC screen without an override.
+
{/* The former States section (Overflow) is the third Variants item. */}
diff --git a/src/components/toggle.stories.tsx b/src/components/toggle.stories.tsx
index 847104e..70b0584 100644
--- a/src/components/toggle.stories.tsx
+++ b/src/components/toggle.stories.tsx
@@ -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: () => (
-
+
-
+
#company-jangheon-industry { grid-column-start: 1; }
.hm-foundation .hm-brand-color-row[data-company-id] { display: grid; gap: 8px; overflow: visible; padding-bottom: 0; grid-template-columns: repeat(auto-fill, minmax(max(100px, (100% - 16px) / 3), 1fr)); }
-/* Typography: compact specification page. Font Family one line, Font Weight small cards, Specimens as comparison tables
- (two columns on wide screens, stacked rows on narrow screens). */
+/* Typography: compact specification page. Font Family one line, Font Weight as three plain columns (no card box; the
+ section divider already frames them), Specimens as comparison tables (two columns on wide screens, stacked rows on narrow screens). */
.hm-foundation .hm-typo-family { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin: 0; }
-.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px; }
-.hm-foundation .hm-typo-weight { padding: 12px 16px; }
+.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px 24px; }
+.hm-foundation .hm-typo-weight { min-width: 0; }
.hm-foundation .hm-typo-weight p { margin: 0; }
.hm-foundation .hm-typo-weight-preview { margin: 4px 0 !important; font-size: var(--hm-card-title-font-size); line-height: var(--hm-card-title-line-height); }
.hm-foundation .hm-typo-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px 32px; align-items: start; }
@@ -73,10 +73,9 @@
}
/* Layout: compact specification page (PC: three bands; narrow screens stack and scroll naturally).
- Tightening applies only inside .hm-layout-ref; the common Story page and other Foundation pages are unchanged. No fixed heights or clipping. */
-.hm-foundation .hm-layout-ref { display: grid; gap: 32px; word-break: keep-all; }
+ Section header spacing and the section gap come from the common Story page; the bands only own their grid. No fixed heights or clipping. */
+.hm-foundation .hm-layout-ref { display: grid; gap: var(--sb-page-section-gap); word-break: keep-all; }
.hm-foundation .hm-layout-ref .sb-page-section { margin-top: 0; }
-.hm-foundation .hm-layout-ref .sb-page-section-header { padding-bottom: 8px; margin-bottom: 12px; }
.hm-foundation .hm-layout-ref p, .hm-foundation .hm-layout-ref h3 { margin: 0; }
.hm-foundation .hm-layout-ref .hm-foundation-card, .hm-foundation .hm-layout-ref .hm-foundation-panel { padding: 8px 16px; }
.hm-foundation .hm-layout-ref code { font-size: var(--hm-button-font-size-sm); color: var(--hm-sem-fg-secondary); }
diff --git a/src/foundation/typography.stories.tsx b/src/foundation/typography.stories.tsx
index fab208d..0dad925 100644
--- a/src/foundation/typography.stories.tsx
+++ b/src/foundation/typography.stories.tsx
@@ -48,7 +48,7 @@ function TypographyReferencePage() {
Noto Sans KR → 맑은 고딕 → 시스템 sans-serif--hm-font-family-base: {values["--hm-font-family-base"]}
-
{weights.map(({ property, label }) =>
+
{weights.map(({ property, label }) =>
{label}{values[property]}
한맥 Hanmac 0123
{property}
diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css
index 1bb1c5d..84f1d9b 100644
--- a/src/sw/sw-footer-stories.css
+++ b/src/sw/sw-footer-stories.css
@@ -36,7 +36,10 @@
.sw-footer-story-row { display: flex; align-items: flex-start; gap: 16px; }
.sw-footer-story-row--tight { gap: 0; }
.sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; }
-.sw-footer-story-width + .sw-footer-story-width { margin-top: 16px; }
+/* Story page frame only (2026-11-11): the width rows sit in a container (.sw-story-responsive), where margins do not collapse,
+ so the rows own the common section gap (story-page.css) and their sections drop their own top margin. */
+.sb-page .sw-footer-story-width > .sb-page-section { margin-top: 0; }
+.sw-footer-story-width + .sw-footer-story-width { margin-top: var(--sb-page-section-gap, 24px); }
/* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */
@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden + .sw-footer-story-width { margin-top: 0; } }
.sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; }
diff --git a/src/sw/sw-header.stories.tsx b/src/sw/sw-header.stories.tsx
index 80b146b..cb346ce 100644
--- a/src/sw/sw-header.stories.tsx
+++ b/src/sw/sw-header.stories.tsx
@@ -944,5 +944,5 @@ function PropertyCompare() {
}
/** Rendered by SW/Popup › Property Panel · 4개 비교 (sw-property-panel.stories.tsx, ID sw-header--property-panel-compare). */
export function PropertyPanelComparePage() {
- return
+ return
}
diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css
index 36bc2b8..0a71dd4 100644
--- a/src/sw/sw-stories.css
+++ b/src/sw/sw-stories.css
@@ -28,6 +28,10 @@
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style[data-shifted="true"]::before { top: calc(-1 * var(--hm-sw-header-field-height) - 1px); }
.sw-story-log { margin: 0 0 8px; font-size: 13px; color: #3d4a45; }
.sw-story-stack { display: flex; flex-direction: column; gap: 8px; }
+/* Story page frame only (2026-11-11): a stack of sections (e.g. the 화면 너비 rows) takes the common section gap of
+ story-page.css, so the 8px stack gap and each section's own top margin do not add up. Headers inside are unchanged. */
+.sb-page .sw-story-stack:has(> .sb-page-section, > div > .sb-page-section) { gap: var(--sb-page-section-gap); margin-top: var(--sb-page-section-gap); }
+.sb-page .sw-story-stack > .sb-page-section, .sb-page .sw-story-stack > div > .sb-page-section { margin-top: 0; }
/* Window Move Motion: window controls alone on the window-area surface, magnified for comparison. */
.sw-story-window { display: flex; width: 152px; height: 48px; background: var(--hm-sw-header-document-background); }
.sw-story-zoom { zoom: 3; width: max-content; }
diff --git a/src/sw/sw-story-notice.tsx b/src/sw/sw-story-notice.tsx
index aa6e622..796268c 100644
--- a/src/sw/sw-story-notice.tsx
+++ b/src/sw/sw-story-notice.tsx
@@ -1,8 +1,8 @@
import type { Decorator } from "@storybook/react-vite"
-import { StoryNotice } from "@/components/story-page"
+import { StoryNoticeContext } from "@/components/story-page"
-// Storybook presentation only. Common notice shown above the page title of every SW Story (SW/* groups only):
-// each SW stories file opts in with `decorators: [withSWNotice]`, so other menus never render it.
+// Storybook presentation only. Common notice of every SW Story page (SW/* groups only), shown directly under the page
+// title by StoryPageHeader: each SW stories file opts in with `decorators: [withSWNotice]`, so other menus never render it.
// Hidden verification / internal Stories are skipped; `parameters.swNotice: false` opts a Story out
// (use it where the page already carries the same notice, so it is never shown twice).
export const SW_NOTICE_TEXT =
@@ -14,9 +14,8 @@ export const withSWNotice: Decorator = (Story, context) => {
const hidden = (context.tags ?? []).some(tag => HIDDEN_TAGS.includes(tag))
if (hidden || context.parameters.swNotice === false) return
return (
- <>
- {SW_NOTICE_TEXT}
+
- >
+
)
}
diff --git a/src/sw/sw-viewer.stories.tsx b/src/sw/sw-viewer.stories.tsx
index c220369..d48f5bd 100644
--- a/src/sw/sw-viewer.stories.tsx
+++ b/src/sw/sw-viewer.stories.tsx
@@ -275,7 +275,7 @@ export const ContentVariants: Story = {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
return (
-
+
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */}
}>