diff --git a/src/components/story-page.css b/src/components/story-page.css index 4088575..6c879db 100644 --- a/src/components/story-page.css +++ b/src/components/story-page.css @@ -61,3 +61,29 @@ font-weight: var(--hm-input-font-weight); } .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; } diff --git a/src/components/story-page.tsx b/src/components/story-page.tsx index 0ca67bb..a52b61c 100644 --- a/src/components/story-page.tsx +++ b/src/components/story-page.tsx @@ -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 ( +
+
+

+ + {children} +

+
+
+ ) +} + // Headings and specimens share StoryPage's single Inner. Wide specimens own their local scroll frame. export function StorySection({ title, diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css index c8e3d13..f1a31c9 100644 --- a/src/sw/sw-footer-stories.css +++ b/src/sw/sw-footer-stories.css @@ -3,8 +3,9 @@ .sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; } .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 - 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; } + scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping. + 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-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; } diff --git a/src/sw/sw-footer.stories.tsx b/src/sw/sw-footer.stories.tsx index 3becd20..50901cb 100644 --- a/src/sw/sw-footer.stories.tsx +++ b/src/sw/sw-footer.stories.tsx @@ -1,8 +1,9 @@ import { useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { 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, } from "@hm-design/sw" import "@hm-design/sw/styles.css" @@ -14,7 +15,6 @@ import { } from "./sw-footer-fixture" import "./sw-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. // 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 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 } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj @@ -51,7 +54,7 @@ function FigmaFooter() { 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 }) { const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY }) const [commandOpen, setCommandOpen] = useState(false) @@ -60,7 +63,6 @@ function Workspace({ width = "100%" }: { width?: number | string }) { {/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */}
-
( - + {/* 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. */}
@@ -379,10 +381,10 @@ function ReferenceFooter() { } export const Responsive: Story = { - name: "Responsive More", + name: "화면 너비", render: () => ( - + {/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */}
{[1920, 1440, 1280, 950, 860, 760].map(width => ( diff --git a/src/sw/sw-header-dropdown.stories.tsx b/src/sw/sw-header-dropdown.stories.tsx index 5951717..586af3e 100644 --- a/src/sw/sw-header-dropdown.stories.tsx +++ b/src/sw/sw-header-dropdown.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { ComposedDemo } from "./sw-header.stories" import "./sw-composed-controls.css" @@ -7,6 +8,7 @@ const meta = { // Keep existing bookmarks; Dropdown is a Story directly under Header. id: "sw-header", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta diff --git a/src/sw/sw-header-home-menu.stories.tsx b/src/sw/sw-header-home-menu.stories.tsx index 725d39a..fd69699 100644 --- a/src/sw/sw-header-home-menu.stories.tsx +++ b/src/sw/sw-header-home-menu.stories.tsx @@ -1,5 +1,6 @@ import { useLayoutEffect, useRef, useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" // Public API only, like a consuming project. import { SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection, @@ -25,6 +26,7 @@ const KICKER = "HM DESIGN SYSTEM / SW" const meta = { title: "SW/Header", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj diff --git a/src/sw/sw-header-open.stories.tsx b/src/sw/sw-header-open.stories.tsx index 3c8e78a..45b605f 100644 --- a/src/sw/sw-header-open.stories.tsx +++ b/src/sw/sw-header-open.stories.tsx @@ -1,5 +1,6 @@ import { useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { SWHeader, SWHeaderDocumentTitle, SWHeaderHome, SWHeaderLayerMenu, SWTheme, SWToolButton, type SWHeaderDocument, type SWHeaderLayer, type SWHeaderProps, @@ -54,6 +55,7 @@ const meta = { title: "SW/Header", id: "sw-header", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj diff --git a/src/sw/sw-header-overview.tsx b/src/sw/sw-header-overview.tsx index 3961963..809199e 100644 --- a/src/sw/sw-header-overview.tsx +++ b/src/sw/sw-header-overview.tsx @@ -69,8 +69,10 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi if (!slot) return const measure = () => { const bounds = slot.getBoundingClientRect() - const width = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9)) - setSize(previous => previous.width === width ? previous : { width, height: width * 9 / 16 }) + // 2026-11-09: the Stage height is still the 16:9 height of the fitted area (unchanged); the width now fills the slot. + 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() const observer = new ResizeObserver(measure) @@ -150,7 +152,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
}
-
({ id: "layer", name: const meta = { title: "SW/Popup", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj diff --git a/src/sw/sw-process-header.stories.tsx b/src/sw/sw-process-header.stories.tsx index e49f993..90f883b 100644 --- a/src/sw/sw-process-header.stories.tsx +++ b/src/sw/sw-process-header.stories.tsx @@ -1,5 +1,6 @@ import { useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType, } from "@hm-design/sw" @@ -31,6 +32,7 @@ const LOGO = { logoSrc: placeholderLogo, logoAlt: "임시 로고" } satisfies Pa const meta = { title: "SW/Step Header", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj diff --git a/src/sw/sw-property-panel.stories.tsx b/src/sw/sw-property-panel.stories.tsx index cb6f4bb..bac20af 100644 --- a/src/sw/sw-property-panel.stories.tsx +++ b/src/sw/sw-property-panel.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { PropertyPanelComparePage } from "./sw-header.stories" const meta = { @@ -8,6 +9,7 @@ const meta = { // Custom Content and Multiple Popups. id: "sw-header", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index ecf37ee..36bc2b8 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -68,20 +68,20 @@ .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. */ -.sw-story-overview.sb-page { height: 100dvh; min-height: 0; padding-block: 16px; } -.sw-story-overview > .sb-page-inner { height: 100%; display: flex; flex-direction: column; gap: 12px; } -.sw-story-overview .sb-page-header { flex: 0 0 auto; padding-bottom: 8px; } -.sw-story-overview .sb-page-header .sb-page-kicker { margin-bottom: 4px; } -.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; } +/* 2026-11-09: the page frame (gutter, title block, Inner) is the common SW Story page; only the Stage keeps a viewport-based height. + 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-inner { display: flex; flex-direction: column; } +.sw-overview-toolbar { display: flex; flex: 0 0 auto; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 12px; 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 details { flex-basis: 100%; } .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 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-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 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. diff --git a/src/sw/sw-story-notice.tsx b/src/sw/sw-story-notice.tsx new file mode 100644 index 0000000..aa6e622 --- /dev/null +++ b/src/sw/sw-story-notice.tsx @@ -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 + return ( + <> + {SW_NOTICE_TEXT} + + + ) +} diff --git a/src/sw/sw-theme.stories.tsx b/src/sw/sw-theme.stories.tsx index 9833ebe..e3087c4 100644 --- a/src/sw/sw-theme.stories.tsx +++ b/src/sw/sw-theme.stories.tsx @@ -1,6 +1,7 @@ import { useState, type CSSProperties, type ReactNode } from "react" import { createPortal } from "react-dom" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { SWFooter, SWFooterCommandLine, SWHeader, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerSection, } from "@hm-design/sw" @@ -23,6 +24,7 @@ const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-seri const meta = { title: "SW/Theme", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj diff --git a/src/sw/sw-viewer.stories.tsx b/src/sw/sw-viewer.stories.tsx index 85af3b2..c220369 100644 --- a/src/sw/sw-viewer.stories.tsx +++ b/src/sw/sw-viewer.stories.tsx @@ -1,5 +1,6 @@ import { useRef, useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" +import { withSWNotice } from "./sw-story-notice" import { SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection, type SWViewerHeaderGroup, type SWViewerMenuEntry, @@ -20,6 +21,7 @@ const KICKER = "HM DESIGN SYSTEM / SW" const meta = { title: "SW/Viewer", parameters: { layout: "fullscreen", controls: { disable: true } }, + decorators: [withSWNotice], } satisfies Meta export default meta type Story = StoryObj