fix(storybook): refine SW notice placement and icon size

This commit is contained in:
shkim4 committed 2026-11-11 11:47:26 +09:00
1 parent ff55a805d5
commit bbc7ee562e
4 files changed
+33 -33

No files matched your search

+6 -1
View File
@@ -1,6 +1,11 @@
# Working Context # Working Context
## 2026-11-11 — Storybook 페이지 위계·여백 공통화 (미커밋) ## 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)은 그대로. - 공통 프레임 `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. - `StorySectionGrid`(story-page.tsx) 추가: 나란히 놓인 섹션의 row gap을 grid가 소유. Chart 7개·Toggle이 사용. `StateBoard` 라벨 구분선 제거, 행 간격 40 → 24px.
+11 -15
View File
@@ -81,28 +81,24 @@
.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 { 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; } .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). /* Group notice under the page title (StoryNotice inside StoryPageHeader): 8px below the H1, same type and color as the
The frame drops its bottom gutter and pulls the next page up so the notice sits 24px above the page title. */ 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-page-notice {
padding-bottom: 0;
margin-bottom: calc(24px - var(--sb-page-gutter));
}
.sb-page .sb-notice { .sb-page .sb-notice {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 6px;
margin: 0; margin: var(--hm-page-header-description-gap) 0 0;
font-size: var(--hm-label-font-size); font-size: var(--hm-page-header-description-font-size);
line-height: var(--hm-label-line-height); line-height: var(--hm-page-header-description-line-height);
font-weight: var(--hm-input-font-weight); font-weight: var(--hm-input-font-weight);
color: var(--hm-sem-fg-primary); color: var(--hm-page-header-description-color);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.sb-page .sb-notice-icon { .sb-page .sb-notice-icon {
flex: none; flex: none;
width: 20px; width: 14px;
height: 20px; height: 14px;
/* centers the icon on the first text line even when the line height differs from 20px */ /* centers the 14px icon on the first text line (20px line height → 3px above and below) */
margin-block: calc((var(--hm-label-line-height) - 20px) / 2); margin-block: calc((var(--hm-page-header-description-line-height) - 14px) / 2);
} }
.sb-page .sb-notice-text { min-width: 0; } .sb-page .sb-notice-text { min-width: 0; }
+11 -11
View File
@@ -1,4 +1,4 @@
import { createContext, type ReactNode } from "react" import { createContext, useContext, type ReactNode } from "react"
import "./story-page.css" import "./story-page.css"
// Storybook presentation only (not part of @hm-design/ui): the common frame of a representative Story page. // 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 <div className={className ? `sb-page ${className}` : "sb-page"}><div className="sb-page-inner">{children}</div></div> return <div className={className ? `sb-page ${className}` : "sb-page"}><div className="sb-page-inner">{children}</div></div>
} }
// 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<ReactNode>(null)
// Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category. // Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category.
// Hidden verification Stories keep passing it so their page structure stays unchanged. // Hidden verification Stories keep passing it so their page structure stays unchanged.
export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) { export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) {
const notice = useContext(StoryNoticeContext)
return ( return (
<header className="sb-page-header"> <header className="sb-page-header">
{kicker ? <p className="sb-page-kicker">{kicker}</p> : null} {kicker ? <p className="sb-page-kicker">{kicker}</p> : null}
<h1 className="sb-page-title">{title}</h1> <h1 className="sb-page-title">{title}</h1>
{notice ? <StoryNotice>{notice}</StoryNotice> : null}
{description ? <p className="sb-page-description">{description}</p> : null} {description ? <p className="sb-page-description">{description}</p> : null}
</header> </header>
) )
} }
// Notice row (icon + text) shown above a Story page title. Reusable by any Story group through a decorator // Notice row (icon + text) under the page title, one level below it (Page Header description type and color).
// (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 // No box, background or border. Icon: Group 1171290812.svg (circle + "!"), colors via currentColor.
// 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.
export function StoryNotice({ children }: { children: ReactNode }) { export function StoryNotice({ children }: { children: ReactNode }) {
return ( return (
<div className="sb-page sb-page-notice" data-story-notice=""> <p className="sb-notice" data-story-notice="">
<div className="sb-page-inner"> <svg className="sb-notice-icon" width="14" height="14" viewBox="0 0 29 29" fill="none" aria-hidden="true" focusable="false">
<p className="sb-notice">
<svg className="sb-notice-icon" width="20" height="20" viewBox="0 0 29 29" fill="none" aria-hidden="true" focusable="false">
<path d="M15.7415 6L15.5383 17.3984H13.3743L13.1711 6H15.7415ZM14.4602 22.1484C14.0279 22.1484 13.6581 21.9974 13.3508 21.6953C13.0435 21.388 12.8925 21.0182 12.8977 20.5859C12.8925 20.1589 13.0435 19.7943 13.3508 19.4922C13.6581 19.1849 14.0279 19.0312 14.4602 19.0312C14.8821 19.0312 15.2467 19.1849 15.554 19.4922C15.8612 19.7943 16.0175 20.1589 16.0227 20.5859C16.0175 20.8724 15.942 21.1354 15.7961 21.375C15.6555 21.6094 15.468 21.7969 15.2336 21.9375C14.9993 22.0781 14.7415 22.1484 14.4602 22.1484Z" fill="currentColor" /> <path d="M15.7415 6L15.5383 17.3984H13.3743L13.1711 6H15.7415ZM14.4602 22.1484C14.0279 22.1484 13.6581 21.9974 13.3508 21.6953C13.0435 21.388 12.8925 21.0182 12.8977 20.5859C12.8925 20.1589 13.0435 19.7943 13.3508 19.4922C13.6581 19.1849 14.0279 19.0312 14.4602 19.0312C14.8821 19.0312 15.2467 19.1849 15.554 19.4922C15.8612 19.7943 16.0175 20.1589 16.0227 20.5859C16.0175 20.8724 15.942 21.1354 15.7961 21.375C15.6555 21.6094 15.468 21.7969 15.2336 21.9375C14.9993 22.0781 14.7415 22.1484 14.4602 22.1484Z" fill="currentColor" />
<circle cx="14.5" cy="14.5" r="13.5" stroke="currentColor" strokeWidth="2" /> <circle cx="14.5" cy="14.5" r="13.5" stroke="currentColor" strokeWidth="2" />
</svg> </svg>
<span className="sb-notice-text">{children}</span> <span className="sb-notice-text">{children}</span>
</p> </p>
</div>
</div>
) )
} }
+5 -6
View File
@@ -1,8 +1,8 @@
import type { Decorator } from "@storybook/react-vite" 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): // Storybook presentation only. Common notice of every SW Story page (SW/* groups only), shown directly under the page
// each SW stories file opts in with `decorators: [withSWNotice]`, so other menus never render it. // 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 // 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). // (use it where the page already carries the same notice, so it is never shown twice).
export const SW_NOTICE_TEXT = 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)) const hidden = (context.tags ?? []).some(tag => HIDDEN_TAGS.includes(tag))
if (hidden || context.parameters.swNotice === false) return <Story /> if (hidden || context.parameters.swNotice === false) return <Story />
return ( return (
<> <StoryNoticeContext.Provider value={SW_NOTICE_TEXT}>
<StoryNotice>{SW_NOTICE_TEXT}</StoryNotice>
<Story /> <Story />
</> </StoryNoticeContext.Provider>
) )
} }