feat(storybook): add shared SW notice and refine SW previews

- Add StoryNotice and a withSWNotice decorator so every visible SW Story
  shows the common notice above its title; non-SW Stories are unaffected.
- Align the SW Header overview with the common SW Story page layout, let the
  preview Stage fill the available width and keep inner scroll on narrow screens.
- Rename SW/Footer to SW/하단 명령어바 (sidebar title only, Story IDs fixed via
  id: "sw-footer"), drop the top SWHeader from its preview and lower the
  workspace height bounds by the removed 49px.
This commit is contained in:
shkim4 committed 2026-11-09 19:23:17 +09:00
1 parent a460426535
commit 9394050c72
16 files changed
+113 -22

No files matched your search

+26
View File
@@ -61,3 +61,29 @@
font-weight: var(--hm-input-font-weight); font-weight: var(--hm-input-font-weight);
} }
.sb-page .sb-page-section-body { min-width: 0; } .sb-page .sb-page-section-body { min-width: 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));
}
.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);
font-weight: var(--hm-input-font-weight);
color: var(--hm-sem-fg-primary);
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);
}
.sb-page .sb-notice-text { min-width: 0; }
+20
View File
@@ -25,6 +25,26 @@ export function StoryPageHeader({ kicker, title, description }: { kicker?: strin
) )
} }
// 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.
export function StoryNotice({ children }: { children: ReactNode }) {
return (
<div className="sb-page sb-page-notice" data-story-notice="">
<div className="sb-page-inner">
<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" />
<circle cx="14.5" cy="14.5" r="13.5" stroke="currentColor" strokeWidth="2" />
</svg>
<span className="sb-notice-text">{children}</span>
</p>
</div>
</div>
)
}
// Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame. // Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame.
export function StorySection({ export function StorySection({
title, title,
+3 -2
View File
@@ -3,8 +3,9 @@
.sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; } .sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; }
.sw-footer-story-canvas { position: relative; background: var(--sw-story-canvas); } .sw-footer-story-canvas { position: relative; background: var(--sw-story-canvas); }
/* 2026-10-22: the workspace fits the viewport under the page title (720px at most), so the page itself does not /* 2026-10-22: the workspace fits the viewport under the page title (720px at most), so the page itself does not
scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping. */ scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping.
.sw-footer-story-workspace { display: flex; flex-direction: column; height: clamp(480px, calc(100dvh - 210px), 720px); background: #060606; } 2026-11-09: the 49px SWHeader above the drawing area was removed, so every bound is 49px lower (drawing area and footer unchanged). */
.sw-footer-story-workspace { display: flex; flex-direction: column; height: clamp(431px, calc(100dvh - 259px), 671px); background: #060606; }
.sw-footer-story-canvas--fill { flex: 1 1 auto; min-height: 0; } .sw-footer-story-canvas--fill { flex: 1 1 auto; min-height: 0; }
.sw-footer-story-nodes { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; } .sw-footer-story-nodes { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
.sw-footer-story-node { margin: 0; padding: 12px; border: 1px solid #dedbd2; border-radius: 6px; background: #fff; } .sw-footer-story-node { margin: 0; padding: 12px; border: 1px solid #dedbd2; border-radius: 6px; background: #fff; }
+11 -9
View File
@@ -1,8 +1,9 @@
import { useState, type ReactNode } from "react" import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWFooter, SWFooterAddSlot, SWFooterAuxButton, SWFooterAuxGroup, SWFooterCommandLine, SWFooterGuide, SWFooterLayoutTabs, SWFooterMenu, SWFooter, SWFooterAddSlot, SWFooterAuxButton, SWFooterAuxGroup, SWFooterCommandLine, SWFooterGuide, SWFooterLayoutTabs, SWFooterMenu,
SWFooterToolButton, SWHeader, SWTheme, SW_FOOTER_MODEL, swFooterToolIcons, SWFooterToolButton, SWTheme, SW_FOOTER_MODEL, swFooterToolIcons,
type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool, type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool,
} from "@hm-design/sw" } from "@hm-design/sw"
import "@hm-design/sw/styles.css" import "@hm-design/sw/styles.css"
@@ -14,7 +15,6 @@ import {
} from "./sw-footer-fixture" } from "./sw-footer-fixture"
import "./sw-stories.css" import "./sw-stories.css"
import "./sw-footer-stories.css" import "./sw-footer-stories.css"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
// SW/Footer (2026-10-20): Figma 이지빔-SW 26:10097 bottom bar and its 34 component sets as @hm-design/sw components. // SW/Footer (2026-10-20): Figma 이지빔-SW 26:10097 bottom bar and its 34 component sets as @hm-design/sw components.
// Stories use the public API only, like a consuming program. No CAD engine: commands are logged. // Stories use the public API only, like a consuming program. No CAD engine: commands are logged.
@@ -22,8 +22,11 @@ const KICKER = "HM DESIGN SYSTEM / SW"
const ref = (id: string) => new URL(`./assets/figma-reference/footer/${id}.png`, import.meta.url).href const ref = (id: string) => new URL(`./assets/figma-reference/footer/${id}.png`, import.meta.url).href
const meta = { const meta = {
title: "SW/Footer", title: "SW/하단 명령어바",
// Fixed so the Story IDs (sw-footer--*) and direct URLs stay unchanged when the sidebar title changes.
id: "sw-footer",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
@@ -51,7 +54,7 @@ function FigmaFooter() {
const LAYOUTS_2: SWFooterLayout[] = [{ id: "layout2", label: "Layout 02" }, { id: "layout1", label: "Layout 01" }] const LAYOUTS_2: SWFooterLayout[] = [{ id: "layout2", label: "Layout 02" }, { id: "layout1", label: "Layout 01" }]
/** The reference screen (2026-10-20): SWHeader, drawing area, footer with the command line inside the bar. */ /** The reference screen (2026-10-20): drawing area, footer with the command line inside the bar. 2026-11-09: no SWHeader above the drawing area. */
function Workspace({ width = "100%" }: { width?: number | string }) { function Workspace({ width = "100%" }: { width?: number | string }) {
const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY }) const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY })
const [commandOpen, setCommandOpen] = useState(false) const [commandOpen, setCommandOpen] = useState(false)
@@ -60,7 +63,6 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
<Log lines={demo.log} /> <Log lines={demo.log} />
{/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */} {/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */}
<div className="sw-footer-story-workspace" style={{ width: "100%", maxWidth: width }}> <div className="sw-footer-story-workspace" style={{ width: "100%", maxWidth: width }}>
<SWHeader documentTitle="Drawing1.grm" logoSrc={placeholderLogo} logoAlt="예제 SW" />
<div className="sw-footer-story-canvas sw-footer-story-canvas--fill" aria-label="도면 영역 (예시)" /> <div className="sw-footer-story-canvas sw-footer-story-canvas--fill" aria-label="도면 영역 (예시)" />
<SWFooter categories={REFERENCE_CATEGORIES} categoryId={demo.categoryId} onCategoryChange={demo.onCategoryChange} <SWFooter categories={REFERENCE_CATEGORIES} categoryId={demo.categoryId} onCategoryChange={demo.onCategoryChange}
status="7.4177, -0.2807, 0.0000" status="7.4177, -0.2807, 0.0000"
@@ -76,10 +78,10 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
} }
export const FullFooter: Story = { export const FullFooter: Story = {
name: "SW Footer", name: "SW 하단 명령어바",
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." /> <StoryPageHeader title="SW 하단 명령어바"description="사용자 제공 실제 화면(2026-10-20) 구성: 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
{/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command {/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command
click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */} click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */}
<section className="sb-page-section" aria-label="실제 Footer 조합 (동작)"> <section className="sb-page-section" aria-label="실제 Footer 조합 (동작)">
@@ -379,10 +381,10 @@ function ReferenceFooter() {
} }
export const Responsive: Story = { export const Responsive: Story = {
name: "Responsive More", name: "화면 너비",
render: () => ( render: () => (
<StoryPage> <StoryPage>
<StoryPageHeader title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." /> <StoryPageHeader title="SW 하단 명령어바 — 화면 너비"description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
{/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */} {/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */}
<div className="sw-story-responsive"> <div className="sw-story-responsive">
{[1920, 1440, 1280, 950, 860, 760].map(width => ( {[1920, 1440, 1280, 950, 860, 760].map(width => (
+2
View File
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { ComposedDemo } from "./sw-header.stories" import { ComposedDemo } from "./sw-header.stories"
import "./sw-composed-controls.css" import "./sw-composed-controls.css"
@@ -7,6 +8,7 @@ const meta = {
// Keep existing bookmarks; Dropdown is a Story directly under Header. // Keep existing bookmarks; Dropdown is a Story directly under Header.
id: "sw-header", id: "sw-header",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
+2
View File
@@ -1,5 +1,6 @@
import { useLayoutEffect, useRef, useState, type ReactNode } from "react" import { useLayoutEffect, useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
// Public API only, like a consuming project. // Public API only, like a consuming project.
import { import {
SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection, SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection,
@@ -25,6 +26,7 @@ const KICKER = "HM DESIGN SYSTEM / SW"
const meta = { const meta = {
title: "SW/Header", title: "SW/Header",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
+2
View File
@@ -1,5 +1,6 @@
import { useState, type ReactNode } from "react" import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWHeader, SWHeaderDocumentTitle, SWHeaderHome, SWHeaderLayerMenu, SWTheme, SWToolButton, SWHeader, SWHeaderDocumentTitle, SWHeaderHome, SWHeaderLayerMenu, SWTheme, SWToolButton,
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderProps, type SWHeaderDocument, type SWHeaderLayer, type SWHeaderProps,
@@ -54,6 +55,7 @@ const meta = {
title: "SW/Header", title: "SW/Header",
id: "sw-header", id: "sw-header",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
+5 -3
View File
@@ -69,8 +69,10 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
if (!slot) return if (!slot) return
const measure = () => { const measure = () => {
const bounds = slot.getBoundingClientRect() const bounds = slot.getBoundingClientRect()
const width = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9)) // 2026-11-09: the Stage height is still the 16:9 height of the fitted area (unchanged); the width now fills the slot.
setSize(previous => previous.width === width ? previous : { width, height: width * 9 / 16 }) const height = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9)) * 9 / 16
const width = Math.max(0, bounds.width)
setSize(previous => previous.width === width && previous.height === height ? previous : { width, height })
} }
measure() measure()
const observer = new ResizeObserver(measure) const observer = new ResizeObserver(measure)
@@ -150,7 +152,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
</div></details> </div></details>
</div>} </div>}
<div ref={slotRef} className="sw-overview-slot" data-preview-slot=""> <div ref={slotRef} className="sw-overview-slot" data-preview-slot="">
<div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 16:9 미리보기, 작은 화면에서는 내부 스크롤" <div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 미리보기, 작은 화면에서는 내부 스크롤"
style={{ ...size, "--sw-preview-stage-height": `${size.height}px`, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}> style={{ ...size, "--sw-preview-stage-height": `${size.height}px`, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
<div className="sw-overview-world"> <div className="sw-overview-world">
<SWHeader ref={headerRef} {...initial} <SWHeader ref={headerRef} {...initial}
+2
View File
@@ -1,5 +1,6 @@
import { useEffect, useId, useRef, useState, type ReactNode } from "react" import { useEffect, useId, useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu, SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar, SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
@@ -68,6 +69,7 @@ const meta = {
excludeStories: ["ComposedDemo", "PropertyPanelComparePage"], excludeStories: ["ComposedDemo", "PropertyPanelComparePage"],
component: SWHeader, component: SWHeader,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
decorators: [withSWNotice],
args: { ...figmaArgs, ...callbacks }, args: { ...figmaArgs, ...callbacks },
argTypes: { argTypes: {
activeMenu: { control: "select", options: [null, "layer", "color", "lineStyle", "textStyle", "dimensionStyle"] }, activeMenu: { control: "select", options: [null, "layer", "color", "lineStyle", "textStyle", "dimensionStyle"] },
+2
View File
@@ -1,5 +1,6 @@
import { useId, useState } from "react" import { useId, useState } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWHeaderInput, SWHeaderPropertyPanel, SWPopup, SWPopupPropertyRow, SWPopupRow, SWPopupSection, SWSelect, SWSlider, SWTheme, SWHeaderInput, SWHeaderPropertyPanel, SWPopup, SWPopupPropertyRow, SWPopupRow, SWPopupSection, SWSelect, SWSlider, SWTheme,
type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty, type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty,
@@ -30,6 +31,7 @@ const layerProperty = (value: string): SWHeaderProperty => ({ id: "layer", name:
const meta = { const meta = {
title: "SW/Popup", title: "SW/Popup",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
+2
View File
@@ -1,5 +1,6 @@
import { useState, type ReactNode } from "react" import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType, SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType,
} from "@hm-design/sw" } from "@hm-design/sw"
@@ -31,6 +32,7 @@ const LOGO = { logoSrc: placeholderLogo, logoAlt: "임시 로고" } satisfies Pa
const meta = { const meta = {
title: "SW/Step Header", title: "SW/Step Header",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
+2
View File
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { PropertyPanelComparePage } from "./sw-header.stories" import { PropertyPanelComparePage } from "./sw-header.stories"
const meta = { const meta = {
@@ -8,6 +9,7 @@ const meta = {
// Custom Content and Multiple Popups. // Custom Content and Multiple Popups.
id: "sw-header", id: "sw-header",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
+8 -8
View File
@@ -68,20 +68,20 @@
.sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; } .sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; }
/* Overview host layout only. Native controls below are Storybook data controls, outside the product. */ /* Overview host layout only. Native controls below are Storybook data controls, outside the product. */
.sw-story-overview.sb-page { height: 100dvh; min-height: 0; padding-block: 16px; } /* 2026-11-09: the page frame (gutter, title block, Inner) is the common SW Story page; only the Stage keeps a viewport-based height.
.sw-story-overview > .sb-page-inner { height: 100%; display: flex; flex-direction: column; gap: 12px; } The Stage fits the slot (16:9 inside it), so the slot gets a definite height instead of the page being locked to 100dvh. */
.sw-story-overview .sb-page-header { flex: 0 0 auto; padding-bottom: 8px; } .sw-story-overview > .sb-page-inner { display: flex; flex-direction: column; }
.sw-story-overview .sb-page-header .sb-page-kicker { margin-bottom: 4px; } .sw-overview-toolbar { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; }
.sw-story-overview .sb-page-header .sb-page-title { margin-block: 0 4px; }
.sw-overview-toolbar { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 13px; }
.sw-overview-toolbar select, .sw-overview-data button { font: inherit; padding: 3px 8px; color: inherit; background: var(--hm-sem-bg-surface); border: 1px solid var(--hm-sem-stroke-input-default); border-radius: 4px; } .sw-overview-toolbar select, .sw-overview-data button { font: inherit; padding: 3px 8px; color: inherit; background: var(--hm-sem-bg-surface); border: 1px solid var(--hm-sem-stroke-input-default); border-radius: 4px; }
.sw-overview-toolbar details { flex-basis: 100%; } .sw-overview-toolbar details { flex-basis: 100%; }
.sw-overview-toolbar summary { width: max-content; cursor: pointer; } .sw-overview-toolbar summary { width: max-content; cursor: pointer; }
.sw-overview-data { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding-block: 8px; } .sw-overview-data { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding-block: 8px; }
.sw-overview-data label { display: inline-flex; align-items: center; gap: 4px; } .sw-overview-data label { display: inline-flex; align-items: center; gap: 4px; }
.sw-overview-slot { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; align-items: center; justify-content: center; } .sw-overview-slot { flex: none; height: max(360px, calc(100dvh - 260px)); min-width: 0; min-height: 0; display: flex; align-items: center; justify-content: center; }
.sw-overview-stage { flex: 0 0 auto; overflow: auto; overscroll-behavior: contain; background: var(--sw-story-canvas); outline: 1px solid var(--hm-sem-stroke-input-default); } .sw-overview-stage { flex: 0 0 auto; overflow: auto; overscroll-behavior: contain; background: var(--sw-story-canvas); outline: 1px solid var(--hm-sem-stroke-input-default); }
.sw-overview-world { min-width: 390px; min-height: 100%; position: relative; } /* 2026-11-09: floor 390 -> 358, the measured minimum width of SWHeader (content width 358px; narrower overflows). 390 forced a 32px
Stage scroll at a 390px viewport (Inner 358px). Below 358px the Stage still scrolls instead of clipping. */
.sw-overview-world { min-width: 358px; min-height: 100%; position: relative; }
/* Placement-only Storybook exception: the panel sits in the Stage. 2026-10-26 (user request): no Stage scrollbar when /* Placement-only Storybook exception: the panel sits in the Stage. 2026-10-26 (user request): no Stage scrollbar when
it opens — the panel is clamped to the Stage height (as the product clamps it to the viewport) and its body scrolls. it opens — the panel is clamped to the Stage height (as the product clamps it to the viewport) and its body scrolls.
The Stage height comes from the Story (--sw-preview-stage-height): the panel's containing block is the Header. The Stage height comes from the Story (--sw-preview-stage-height): the panel's containing block is the Header.
+22
View File
@@ -0,0 +1,22 @@
import type { Decorator } from "@storybook/react-vite"
import { StoryNotice } 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.
// 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 =
"SW UI는 공통 컴포넌트의 기능과 동작을 유지하면서, 제품별 특성에 따라 시각적 스타일을 다르게 적용할 수 있습니다."
const HIDDEN_TAGS = ["verification", "internal"]
export const withSWNotice: Decorator = (Story, context) => {
const hidden = (context.tags ?? []).some(tag => HIDDEN_TAGS.includes(tag))
if (hidden || context.parameters.swNotice === false) return <Story />
return (
<>
<StoryNotice>{SW_NOTICE_TEXT}</StoryNotice>
<Story />
</>
)
}
+2
View File
@@ -1,6 +1,7 @@
import { useState, type CSSProperties, type ReactNode } from "react" import { useState, type CSSProperties, type ReactNode } from "react"
import { createPortal } from "react-dom" import { createPortal } from "react-dom"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection, SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection,
} from "@hm-design/sw" } from "@hm-design/sw"
@@ -23,6 +24,7 @@ const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-seri
const meta = { const meta = {
title: "SW/Theme", title: "SW/Theme",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
+2
View File
@@ -1,5 +1,6 @@
import { useRef, useState, type ReactNode } from "react" import { useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { withSWNotice } from "./sw-story-notice"
import { import {
SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection, SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
type SWViewerHeaderGroup, type SWViewerMenuEntry, type SWViewerHeaderGroup, type SWViewerMenuEntry,
@@ -20,6 +21,7 @@ const KICKER = "HM DESIGN SYSTEM / SW"
const meta = { const meta = {
title: "SW/Viewer", title: "SW/Viewer",
parameters: { layout: "fullscreen", controls: { disable: true } }, parameters: { layout: "fullscreen", controls: { disable: true } },
decorators: [withSWNotice],
} satisfies Meta } satisfies Meta
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>