diff --git a/docs/guidelines/프로젝트-지침.md b/docs/guidelines/프로젝트-지침.md index ee2f38c..93cd644 100644 --- a/docs/guidelines/프로젝트-지침.md +++ b/docs/guidelines/프로젝트-지침.md @@ -157,7 +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`)으로 쓴다. +- Storybook 페이지 프레임(2026-11-11 확정)은 `src/components/story-page.tsx`·`story-page.css`의 `StoryPage`·`StoryPageHeader`·`StorySection`·`StorySectionGrid`가 공통으로 소유한다. 2026-11-11 사용자 요청으로 `StoryPageHeader`는 H1 페이지 제목·kicker·설명·SW 공통 안내 문구를 표시하지 않으며(호출과 props는 유지), 페이지는 위 여백(16–24px) 바로 아래 첫 섹션부터 시작한다. 단, Preview 문서 제목이 Story ID라 스크린 리더가 페이지를 식별할 수 있도록 `title`만 시각적으로 숨긴 접근성 전용 H1(`.sb-page-a11y-title`, 1px clip·문서 흐름 밖)로 렌더링한다. 검증 스크립트는 이 H1의 시각 스타일에 의존하지 않고 첫 콘텐츠 블록(`.sb-page-inner > :not(h1)`)으로 렌더링을 기다리며, 페이지 식별은 H1 텍스트로 한다. 제품 `PageHeader` 컴포넌트와는 무관하다. Web·Chart·OrgChart(Admin)의 문서 위계는 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/examples/org-chart/verify-p8.mjs b/examples/org-chart/verify-p8.mjs index be57008..e28d564 100644 --- a/examples/org-chart/verify-p8.mjs +++ b/examples/org-chart/verify-p8.mjs @@ -46,8 +46,11 @@ page.setDefaultTimeout(20000); async function settle(target = page) { await target.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); }); } +// The Storybook page title is a visually hidden H1 (2026-11-11), so readiness waits for the first visible content block +// of the StoryPage frame, and page identity reads the H1 text (the Story's accessible page heading). +const pageContent = '.sb-page-inner > :not(h1)', pageHeading = '.sb-page-inner > h1'; async function ready(expected) { - await page.locator('.sb-page-title').waitFor(); + await page.locator(pageContent).first().waitFor(); await page.waitForFunction(expected => { const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')]; if (!hosts.length) return false; @@ -76,10 +79,8 @@ async function commonStyle() { const properties = { '.sb-page': ['fontFamily', 'color', 'paddingTop', 'paddingBottom', 'boxSizing', 'width'], '.sb-page-inner': ['width', 'boxSizing'], - '.sb-page-header': ['paddingBottom', 'borderBottomWidth', 'borderBottomColor', 'fontSize', 'lineHeight'], - '.sb-page-kicker': ['fontSize', 'fontWeight', 'color', 'marginBottom'], - '.sb-page-title': ['fontSize', 'fontWeight', 'lineHeight', 'marginTop', 'marginBottom'], - '.sb-page-description': ['fontSize', 'fontWeight', 'lineHeight', 'marginBottom'], + // The visible header (kicker, title, description) is no longer rendered; the page H1 stays visually hidden. + '.sb-page-inner > h1': ['position', 'width', 'height', 'overflow', 'clipPath'], '.sb-page-section': ['marginTop', 'scrollMarginTop'], '.sb-page-section-header': ['paddingBottom', 'marginBottom', 'borderBottomWidth', 'borderBottomColor'], '.sb-page-section-title': ['fontSize', 'lineHeight', 'fontWeight', 'marginTop', 'marginBottom'], @@ -105,7 +106,7 @@ async function pageGeometry() { return page.evaluate(() => { const rect = element => { if (!element) return null; const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; }; const documentGeometry = { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, innerWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }; - const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-title')?.textContent }; + const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-inner > h1')?.textContent }; const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot; const wrapper = shadow?.querySelector('.hm-orgchart-composition'); const stage = shadow?.querySelector('.hm-orgchart-stage'); @@ -159,10 +160,10 @@ try { await manager.waitForFunction(() => { const preview = document.querySelector('#storybook-preview-iframe')?.contentDocument; const hosts = [...preview?.querySelectorAll('.hm-orgchart-component-host') ?? []]; - return preview?.querySelector('.sb-page-title')?.textContent === 'Overview' && hosts.length > 0 + return preview?.querySelector('.sb-page-inner > h1')?.textContent === 'Overview' && hosts.length > 0 && hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet)); }); - await manager.frameLocator('#storybook-preview-iframe').locator('.sb-page-title').evaluate(async element => { + await manager.frameLocator('#storybook-preview-iframe').locator(pageHeading).evaluate(async element => { const document = element.ownerDocument, view = document.defaultView; await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => view.requestAnimationFrame(resolve)); @@ -198,7 +199,7 @@ try { await gotoStory(story); const rendered = await page.evaluate(({ selectors }) => { const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')]; - return { title: document.querySelector('.sb-page-title')?.textContent, hosts: hosts.length, + return { title: document.querySelector('.sb-page-inner > h1')?.textContent, hosts: hosts.length, specimens: Object.fromEntries(selectors.map(selector => [selector, hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll(selector).length, 0)])), employees: hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll('.hm-orgchart-employee').length, 0), autoLayout: hosts.find(host => host.classList.contains('hm-orgchart-host'))?.shadowRoot.querySelector('.hm-orgchart-expanded')?.dataset.autoLayout }; @@ -214,7 +215,7 @@ try { for (const id of patterns) { await page.setViewportSize({ width: 1440, height: 1100 }); await page.goto(`${origin}/iframe.html?id=${id}&viewMode=story`); - await page.locator('.sb-page-title').waitFor(); await settle(); + await page.locator(pageContent).first().waitFor(); await settle(); references.push({ id, style: await commonStyle() }); } const reference = references[0].style; @@ -268,7 +269,7 @@ try { await section('full-repair', async () => { await page.setViewportSize({ width: 1440, height: 1100 }); await page.goto(`${origin}/iframe.html?id=${patterns[0]}&viewMode=story`); - await page.locator('.sb-page-title').waitFor(); await settle(); + await page.locator(pageContent).first().waitFor(); await settle(); const reference = await commonStyle(); await gotoStory(stories[2]); const style = await commonStyle(); diff --git a/examples/org-chart/verify-p9-1.mjs b/examples/org-chart/verify-p9-1.mjs index 8ab6d8a..531d8ab 100644 --- a/examples/org-chart/verify-p9-1.mjs +++ b/examples/org-chart/verify-p9-1.mjs @@ -39,7 +39,8 @@ async function settle() { async function goto(name, size) { await page.setViewportSize(size); await page.goto(`${origin}/iframe.html?id=${stories[name]}&viewMode=story`); - await page.locator('.sb-page-title').waitFor(); + // The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead. + await page.locator('.sb-page-inner > :not(h1)').first().waitFor(); await page.waitForFunction(() => { const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')]; return hosts.length === 1 && hosts.every(host => host.shadowRoot && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet)); @@ -86,7 +87,7 @@ async function snapshot() { const css = getComputedStyle(element); return { scrollWidth: element.scrollWidth, clientWidth: element.clientWidth, scrollHeight: element.scrollHeight, clientHeight: element.clientHeight, left: element.scrollLeft, top: element.scrollTop, overflowX: css.overflowX, overflowY: css.overflowY }; }; - const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-title', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))])); + const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))])); // Page/section height is content-dependent; typography, borders and gutters are invariant. for (const value of Object.values(common)) if (value) for (const key of ['height', 'minHeight']) delete value[key]; const documentScroll = { viewportWidth: innerWidth, viewportHeight: innerHeight, root: scroll(document.documentElement), body: scroll(document.body), scrollingElement: scroll(document.scrollingElement) }; @@ -145,7 +146,7 @@ async function snapshot() { }; } return { - title: document.querySelector('.sb-page-title')?.textContent, common, documentScroll, departments, unwanted, + title: document.querySelector('.sb-page-inner > h1')?.textContent, common, documentScroll, departments, unwanted, optionsPresent: [...document.querySelectorAll('button')].filter(visible).some(button => /긴 이름 검증/.test(button.textContent)), sectionTitles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), fixed, }; diff --git a/examples/org-chart/verify-p9-2-composition.mjs b/examples/org-chart/verify-p9-2-composition.mjs index 62cc3b8..a766a96 100644 --- a/examples/org-chart/verify-p9-2-composition.mjs +++ b/examples/org-chart/verify-p9-2-composition.mjs @@ -33,7 +33,8 @@ async function settle() { async function open(size) { await page.setViewportSize(size); await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`); - await page.locator('.sb-page-title').waitFor(); + // The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead. + await page.locator('.sb-page-inner > :not(h1)').first().waitFor(); await page.waitForFunction(() => { const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot; return shadow?.querySelector('.hm-orgchart-stage[data-workdesk-artwork]') && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet); diff --git a/examples/org-chart/verify-p9.mjs b/examples/org-chart/verify-p9.mjs index 2369c66..5651db3 100644 --- a/examples/org-chart/verify-p9.mjs +++ b/examples/org-chart/verify-p9.mjs @@ -39,7 +39,8 @@ page.setDefaultTimeout(20000); let expectedHosts = 1; async function settle() { await page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); }); } async function ready(requirePage = true) { - if (requirePage) await page.locator('.sb-page-title').waitFor(); + // The page title is a visually hidden H1 (2026-11-11): wait for the first visible StoryPage content block instead. + if (requirePage) await page.locator('.sb-page-inner > :not(h1)').first().waitFor(); await page.waitForFunction(expected => { const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')]; return hosts.length === expected && hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet) && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('img')].every(image => image.complete)); @@ -111,8 +112,8 @@ async function snapshot() { cards: [...expanded.querySelectorAll('.hm-orgchart-employee')].map(card => { const css = getComputedStyle(card), direction = card.dataset.connectionDirection; return { company: card.dataset.companyId, direction, width: card.offsetWidth, height: card.offsetHeight, border: hex(css[direction === 'left' ? 'borderLeftColor' : direction === 'right' ? 'borderRightColor' : 'borderTopColor']), image: Boolean(card.querySelector('img')) }; }), connectors: expanded.querySelectorAll('svg,.edge,.lines').length, } : null; - const common = Object.fromEntries(['.sb-page-title', '.sb-page-description', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))])); - return { title: document.querySelector('.sb-page-title')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto }; + const common = Object.fromEntries(['.sb-page-inner > h1', '.sb-page-section-title', '.sb-page-section-header'].map(selector => [selector, style(document.querySelector(selector))])); + return { title: document.querySelector('.sb-page-inner > h1')?.textContent, titles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), document: { width: innerWidth, scrollWidth: document.documentElement.scrollWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight }, common, summary, affiliation, logos, departments, fixed, auto }; }); } function noHorizontalOverflow(state) { return state.document.scrollWidth <= state.document.width + 1; } diff --git a/packages/sw/tests/overview.browser.mjs b/packages/sw/tests/overview.browser.mjs index cba946c..93975b1 100644 --- a/packages/sw/tests/overview.browser.mjs +++ b/packages/sw/tests/overview.browser.mjs @@ -161,7 +161,7 @@ try { await open('color','색상');await open('layer','레이어') check('opening another dropdown closes the previous one',await header.locator('.hm-sw-header__color-menu').count()===0&&await header.locator('.hm-sw-header__layer-panel').count()===1) await page.keyboard.press('Escape');check('Escape closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0) - await open('layer','레이어');await page.locator('.sb-page-title').click();check('outside click closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0) + await open('layer','레이어');await page.locator('.sb-page').click({position:{x:4,y:4}});check('outside click closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0) const barState=await page.locator('.hm-sw-property-bar').evaluate(bar=>({object:bar.querySelector('.hm-sw-property-bar__object').textContent,order:[...bar.querySelectorAll('[data-property-id]')].map(e=>e.dataset.propertyId),inputs:bar.querySelectorAll('input').length,focusable:[...bar.querySelectorAll('output')].filter(e=>e.tabIndex>=0).length,colors:[...new Set([...bar.querySelectorAll('output')].map(e=>getComputedStyle(e).color))]})) check('Bar: object text 원, order 반지름 / 지름 / 원주 / 면적, read-only red text (edited in the panel) without input or focus stop',barState.object==='원'&&barState.order.join()==='radius,diameter,circumference,area'&&barState.inputs===0&&barState.focusable===0&&barState.colors.join()==='rgb(230, 4, 3)',barState) diff --git a/src/components/story-page.css b/src/components/story-page.css index 0987f0b..d7a7203 100644 --- a/src/components/story-page.css +++ b/src/components/story-page.css @@ -1,7 +1,7 @@ /* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page. 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); + 2026-11-11: the H1 page title / description header is no longer shown (the H1 stays for screen readers only); pages start with the + 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. */ @@ -27,32 +27,21 @@ 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 { - 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 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: 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: 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); +/* 2026-11-11: no visible page header, so the first block starts right at the top gutter. StoryPageHeader only renders the + visually hidden page H1 (screen readers only, out of flow), so the first visible block after it gets the same reset. */ +.sb-page > .sb-page-inner > :first-child, +.sb-page > .sb-page-inner > .sb-page-a11y-title:first-child + * { margin-top: 0; } +.sb-page .sb-page-a11y-title { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; } .sb-page .sb-page-section { margin-top: var(--sb-page-section-gap); scroll-margin-top: 16px; } @@ -80,25 +69,3 @@ 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; } - -/* 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: 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-page-header-description-color); - overflow-wrap: anywhere; -} -.sb-page .sb-notice-icon { - flex: none; - 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 9fbd422..d8d2ddd 100644 --- a/src/components/story-page.tsx +++ b/src/components/story-page.tsx @@ -1,9 +1,9 @@ -import { createContext, useContext, type ReactNode } from "react" +import { createContext, 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. -// Page → Header (title, description) → Sections (title, description, specimen). It never styles the component being reviewed. -// Inner and title follow Foundation / Color; the section header → content split follows Center Graphic / 교량. +// Page → Sections (title, description, specimen). It never styles the component being reviewed. +// Inner follows Foundation / Color; the section header → content split follows Center Graphic / 교량. // A Story using it sets `parameters.layout: "fullscreen"` so the page owns its inner padding. // Lets StateBoard render its specimen labels one level below the section heading. @@ -13,36 +13,18 @@ export function StoryPage({ className, children }: { className?: string; childre return
{kicker}
: null} -{description}
: null} -- - {children} -
- ) +// 2026-11-11 (user request): the Storybook page header (kicker, description and group notice) is no longer shown, so every +// page visually starts with its first section. Only the title stays, as a visually hidden H1 (no box, no space), so the +// preview document keeps one page heading above its H2 sections: the preview's own document title is the Story ID and the +// iframe title is generic, so this is the only place a screen reader can identify the page. +// Stories keep their calls and props, so the visible header can be restored here. +// The product Page Header component (@hm-design/ui PageHeader) is unrelated and unchanged. +export function StoryPageHeader({ title }: { kicker?: string; title: string; description?: ReactNode }) { + return