From b33ca2a157d3dfd6ddf474f451f702d5e18d9d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Mon, 9 Nov 2026 16:42:47 +0900 Subject: [PATCH] style(storybook): refine patterns data and layout stories --- docs/patterns/data-table.md | 2 +- docs/patterns/gnb.md | 4 +- docs/patterns/lnb.md | 2 +- docs/patterns/page-header.md | 2 +- src/patterns/dashboard-layout.stories.css | 12 ++ src/patterns/dashboard-layout.stories.tsx | 2 +- src/patterns/data-table.stories.tsx | 13 ++- src/patterns/filter-bar.stories.tsx | 2 +- src/patterns/gnb.stories.tsx | 95 +++++++++------- src/patterns/lnb.stories.tsx | 52 +-------- src/patterns/loading-screen.stories.tsx | 6 +- src/patterns/page-header.stories.tsx | 128 +++++++++++++--------- 12 files changed, 168 insertions(+), 152 deletions(-) diff --git a/docs/patterns/data-table.md b/docs/patterns/data-table.md index d5383f6..8f7ca2e 100644 --- a/docs/patterns/data-table.md +++ b/docs/patterns/data-table.md @@ -70,7 +70,7 @@ Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table ## 9. Storybook -- `Patterns/Data/Data Table` → `Data Table`(대표: Overview 선택·정렬·열 표시·행 액션·페이지 크기·페이지, `기본 목록 검색`, `추가 조건 목록 검색`), `With Filter Bar`(숨김 Filter Bar 연동 검증). 두 목록 검색 구역은 흰 Card 하나에 CardTitle(`사용자 목록`) → 검색·열 표시 도구 → 표 순서다. 페이지 제목 PageHeader를 쓰는 화면에서는 PageHeader를 이 Card 밖에 둔다. verification: `WithFilterBar`, `Loading`, `Empty`. +- `Patterns/Data/Data Table` → `Data Table`(대표: `Advanced Search` 구역 하나. 추가 조건 목록 검색 + 선택·정렬·열 표시·행 액션·페이지 크기·페이지), 숨김 verification Story `Overview`(기본 표, `patterns-data-data-table--overview`), `Basic Search`(기본 목록 검색, `patterns-data-data-table--basic-search`), `With Filter Bar`(추가 조건 연동 검증). 목록 검색 구역은 흰 Card 하나에 CardTitle(`사용자 목록`) → 검색·열 표시 도구 → 표 순서다. 페이지 제목 PageHeader를 쓰는 화면에서는 PageHeader를 이 Card 밖에 둔다. verification: `WithFilterBar`, `Loading`, `Empty`. - 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음). ## 10. Accessibility diff --git a/docs/patterns/gnb.md b/docs/patterns/gnb.md index f04a89f..e075d17 100644 --- a/docs/patterns/gnb.md +++ b/docs/patterns/gnb.md @@ -112,7 +112,7 @@ ui-viz에서 GNB(Variant B, top-header)와 LNB(Variant A, sidebar)는 서로 대 **AI 구현 필수**: `GNBNav`는 모바일에서 숨겨질 뿐 모바일 메뉴를 자동 생성하지 않는다. 최초 구현부터 Desktop `GNBNav`와 Mobile `GNBMobileMenu`를 함께 넣고 같은 메뉴 데이터·현재 경로를 사용한다. `GNBMobileMenu`가 햄버거와 Sheet를 모두 제공하므로 별도 메뉴 버튼이나 Sheet를 추가하지 않는다. 기본 GNB는 `GNBMobileLink / GNBMobileGroup`을 사용한다. 링크 선택은 router의 `preventDefault()` 여부와 관계없이 Sheet를 닫으며, 닫기 버튼·Escape·focus trap·메뉴 버튼으로의 포커스 복귀는 기존 Sheet/Radix 동작을 유지한다. -Reference는 [대표 Story 소스](../../src/patterns/gnb.stories.tsx)의 `All`(`Patterns/Layout/GNB → GNB`, `patterns-layout-gnb--all`) 안 Overview·Usage · GNB Sidebar를 좁은 폭에서 확인한다. `Mobile / Mobile Menu Open`은 숨김 verification이며 동작 검사에만 사용한다. Sidebar를 포함하면 [§15의 공개 API 소비 예제](#15-gnb-only-sidebar-composition-2026-10-08)를 따른다. LNB 모바일 링크 재사용 시 자동 닫기 Context가 다르므로 controlled Sheet를 route handler에서 닫고 Desktop 복귀 시에도 닫는다. 이는 GNB ONLY 내부 Composition이며 별도 LNB ONLY 모드가 아니다. +Reference는 [대표 Story 소스](../../src/patterns/gnb.stories.tsx)의 `All`(`Patterns/Layout/GNB → GNB`, `patterns-layout-gnb--all`) 안 Variants(Default, Long Labels)·States(Navigation Menu Open)를 좁은 폭에서 확인하고, With Logo·Utilities는 숨김 `With Logo`·`Utilities` Story에서 확인하며, GNB Sidebar는 숨김 `GNB Sidebar`(`patterns-layout-gnb--sidebar`)·`Brown Sidebar`(`patterns-layout-gnb--brown-sidebar`), Sticky는 숨김 `Sticky` Story에서 확인한다(2026-11-09: 대표 페이지의 Usage 3개를 개별 verification Story로 분리). `Mobile / Mobile Menu Open`은 숨김 verification이며 동작 검사에만 사용한다. Sidebar를 포함하면 [§15의 공개 API 소비 예제](#15-gnb-only-sidebar-composition-2026-10-08)를 따른다. LNB 모바일 링크 재사용 시 자동 닫기 Context가 다르므로 controlled Sheet를 route handler에서 닫고 Desktop 복귀 시에도 닫는다. 이는 GNB ONLY 내부 Composition이며 별도 LNB ONLY 모드가 아니다. - Desktop(1024px 이상): 위 표. 높이 70, 좌우 28, column gap 24. - Mobile(1023px 이하): 높이 56, padding 16. 주 메뉴·eyebrow·사용자 글자를 숨기고 햄버거(흰 ghost Button, `aria-label`)를 맨 앞에, 사용자는 같은 Avatar 하나만 보인다(중복 없음). 햄버거 → HM Sheet(left): 제목, 링크(44px), 그룹 제목. Radix가 focus trap·Escape·focus 복귀를 맡는다. breakpoint를 768에서 1024로 올린 이유: 메뉴 4개(항목 최소 112) 가운데 정렬은 그 이하에서 brand/profile이 너무 좁아진다. ui-viz에는 반응형 기준이 없다. @@ -176,7 +176,7 @@ src/patterns/gnb.stories.tsx ## 15. GNB ONLY Sidebar Composition (2026-10-08) -대표 `Patterns/Layout/GNB → GNB`의 **Usage · GNB Sidebar**에서 확인한다. 직접 동작 검증은 숨김 `GNB Sidebar` Story(`patterns-layout-gnb--sidebar`)를 사용한다. 기존 `LNB → VERIFICATION ONLY · GNB + LNB`는 운영 기준으로 승격하지 않는다. +숨김 `GNB Sidebar`(`patterns-layout-gnb--sidebar`)와 `Brown Sidebar`(`patterns-layout-gnb--brown-sidebar`) Story에서 확인한다(대표 `GNB` 페이지에서는 분리됨). 기존 `LNB → VERIFICATION ONLY · GNB + LNB`는 운영 기준으로 승격하지 않는다. 구조는 **GNB Header → Sidebar + Main Content**다. Sidebar는 공개 `LNB / LNBNav / LNBGroup / LNBItem`을 그대로 사용한다. 기존 DOM/data-slot을 유지하고 브랜드·사용자 영역은 GNB만 담당한다. `LNBBrand / LNBProfile`은 기본 조합에서 생략한다. 새로운 GNBSidebar 공개 컴포넌트·메뉴 로직은 없다. diff --git a/docs/patterns/lnb.md b/docs/patterns/lnb.md index fc63e16..60ea0e8 100644 --- a/docs/patterns/lnb.md +++ b/docs/patterns/lnb.md @@ -19,7 +19,7 @@ Implementation: current @hm-design/ui primitives / patterns (HM Collapsible + pl |---|---| | 구현 상태 | 구현 완료 (2026-10-03), 3뎁스 추가 (2026-10-06) | | 기반 | HM Collapsible + 일반 요소 (새 dependency 없음) | -| 대표 Story | `Patterns/Layout/LNB` → `LNB` (verification: Third Level, Third Level Long Labels, Default, Selected, Expanded, Selected Child, Collapsed With Selection, With Logo, Long Labels, Scroll, Third Level Scroll, VERIFICATION ONLY · GNB + LNB, LNB Standalone Desktop, LNB Standalone Mobile, LNB Standalone Mobile Menu Open) | +| 대표 Story | `Patterns/Layout/LNB` → `LNB` (States: Selected·Expanded·3뎁스 Selected, Variants: With Logo·Long Labels. Scroll·Standalone 반응형은 2026-11-09 대표 페이지에서 분리되어 아래 verification Story로 확인; verification: Third Level, Third Level Long Labels, Default, Selected, Expanded, Selected Child, Collapsed With Selection, With Logo, Long Labels, Scroll, Third Level Scroll, VERIFICATION ONLY · GNB + LNB, LNB Standalone Desktop, LNB Standalone Mobile, LNB Standalone Mobile Menu Open) | | 보류 | §11 | ## 2. 목적 / 역할 diff --git a/docs/patterns/page-header.md b/docs/patterns/page-header.md index 266ff90..d7b052e 100644 --- a/docs/patterns/page-header.md +++ b/docs/patterns/page-header.md @@ -79,7 +79,7 @@ Breadcrumb는 HM Breadcrumb 그대로(14px, 링크). ui-viz는 12px 정적 텍 페이지 content 면에 바로 놓는 Tabs는 PageHeader 다음에 기본 Primary(`panelSurface="content"`)를 사용한다. PageHeader의 제목→설명 **8px**, 아래 첫 콘텐츠까지 **20px**을 유지한다. 탭을 감싸는 새 White Card·border·padding은 필요하지 않으며 내부 `TabsContent`는 `px-0 py-6`로 조합할 수 있다. [Tabs의 표면·여백 소유권](../components/tabs.md#16-publisher--developer-조합-안내-2026-10-09)을 따른다. -2026-10-09 실제 반영: 대표 Story의 Overview에서 기존 PageHeader 바로 뒤에 `내 업무 / 완료 업무` Tabs와 공개 Table을 조합한다. 선택 탭의 면은 [Primary 상단 White alpha 0.8](../components/tabs.md#163-d-02-상단-흰색-80-실제-반영) → content로 이어지고, 탭 전체 opacity는 1이다. PageHeader Component·Typography·8px/20px Token과 기존 단일 Brand Action은 유지한다. 별도 Pattern API를 추가하지 않는다. +2026-10-09 실제 반영(2026-11-09: 대표 Story는 Variants의 Basic·With Breadcrumb만 두고, 이 Tabs/Table 조합은 숨김 verification Story `Full Page`(`patterns-layout-page-header--full-page`)로 분리): 기존 PageHeader 바로 뒤에 `내 업무 / 완료 업무` Tabs와 공개 Table을 조합한다. 선택 탭의 면은 [Primary 상단 White alpha 0.8](../components/tabs.md#163-d-02-상단-흰색-80-실제-반영) → content로 이어지고, 탭 전체 opacity는 1이다. PageHeader Component·Typography·8px/20px Token과 기존 단일 Brand Action은 유지한다. 별도 Pattern API를 추가하지 않는다. 이때 TabsContent의 상하 24px을 PageHeader 하단 20px에 더해 탭 바 앞 간격으로 사용하지 않는다. Card 안의 제목은 CardTitle 규격(20/28/600), 페이지 제목은 PageHeaderTitle 규격(28/36/700)을 사용한다. CardDescription과 PageHeaderDescription은 서로 다른 계약이다. 비교 이미지의 48px 간격이나 Card 전용 Token을 페이지 전체에 적용하지 않는다. diff --git a/src/patterns/dashboard-layout.stories.css b/src/patterns/dashboard-layout.stories.css index 1f6227a..9bb718b 100644 --- a/src/patterns/dashboard-layout.stories.css +++ b/src/patterns/dashboard-layout.stories.css @@ -180,3 +180,15 @@ } /* Compact geometry is shared by all viewports. Dashboard's native scroll frame preserves its minimum width. */ + +/* Representative page only: the Dashboard grows with its content instead of being fixed to 100dvh, so the content surface has no + inner vertical scrollbar and the Storybook page itself scrolls. The body's `flex: 1 1 0` is a vertical share of the fixed height, + so it becomes content-sized; `dashboard-main` keeps its own `flex: 1 1 0`, which is its horizontal fill inside the body row. + The package's rules (fixed height, overflow-y: auto) stay as they are for operating screens; the horizontal scroll frame + (min-width 960px) is unchanged. */ +.dashboard-all-page [data-slot="dashboard-scroll"] { + height: auto; +} +.dashboard-all-page [data-slot="dashboard-body"] { + flex: 0 0 auto; +} diff --git a/src/patterns/dashboard-layout.stories.tsx b/src/patterns/dashboard-layout.stories.tsx index 920b791..293b2b0 100644 --- a/src/patterns/dashboard-layout.stories.tsx +++ b/src/patterns/dashboard-layout.stories.tsx @@ -429,7 +429,7 @@ const frame = (height?: number): CSSProperties | undefined => (height ? ({ "--hm // story composition here prevents the export from becoming a second hand-written UI. export function DashboardAll() { return ( - + diff --git a/src/patterns/data-table.stories.tsx b/src/patterns/data-table.stories.tsx index ba1c994..159ffea 100644 --- a/src/patterns/data-table.stories.tsx +++ b/src/patterns/data-table.stories.tsx @@ -155,15 +155,18 @@ export const All: Story = { render: () => ( - - - - - + ), } +// The other two usage types that used to be stacked on the representative page; kept as separate Stories. +// Plain table with column visibility (selection, sorting, row actions, pagination). +export const Overview: Story = { tags: ["verification"], render: () => } + +// Basic list search: status + keyword search, centered above the table. +export const BasicSearch: Story = { tags: ["verification"], name: "Basic Search", render: () => } + // Combination Story: Filter Bar (search, status, department, date, reset) filtering the Data Table rows. export const WithFilterBarStory: Story = { tags: ["verification"], name: "With Filter Bar", render: () => } diff --git a/src/patterns/filter-bar.stories.tsx b/src/patterns/filter-bar.stories.tsx index 697d6f9..f65d867 100644 --- a/src/patterns/filter-bar.stories.tsx +++ b/src/patterns/filter-bar.stories.tsx @@ -90,7 +90,7 @@ export const All: Story = { render: () => ( - + diff --git a/src/patterns/gnb.stories.tsx b/src/patterns/gnb.stories.tsx index 4c2e8a7..e02502e 100644 --- a/src/patterns/gnb.stories.tsx +++ b/src/patterns/gnb.stories.tsx @@ -46,6 +46,7 @@ import { TooltipContent, TooltipTrigger, } from "@hm-design/ui" +import { StateBoard } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // GNB is a composition pattern. Layout visual reference is the ui-viz GNB (green 70px bar, centered menu, Beige current item joined to @@ -308,12 +309,14 @@ function Bar({ } // The bar with the content that follows it (the GNB Mode page owns this part; here it only shows the join). -function Page({ children, label }: { children: ReactNode; label?: string }) { +// `compact` is for the representative page only: it trims the connected area's height and padding (the 12px join and rounded corners stay). +const compactContentStyle: CSSProperties = { ...contentStyle, minHeight: 0, padding: 16 } +function Page({ children, label, compact = false }: { children: ReactNode; label?: string; compact?: boolean }) { return (
{children} -
-

{label ?? "연결된 콘텐츠 영역: GNB는 자기 높이와 12px 접점까지만 차지합니다."}

+
+ {label === "" ? null :

{label ?? "연결된 콘텐츠 영역: GNB는 자기 높이와 12px 접점까지만 차지합니다."}

}
) @@ -426,50 +429,60 @@ const meta = { title: "Patterns/Layout/GNB", component: GNB, parameters: { layou export default meta type Story = StoryObj -// Representative Story: the green bar with a current item, an open 2-depth panel, long labels, a logo, the optional utility icons and -// sticky. Menus and the user menu open for real. Narrow the page to 390px for the mobile bar (hamburger → Sheet). -// Each GNB fills its specimen parent inside the same Inner as the page and section headings. -// The representative page names the connected area only; the verification Stories keep the default 12px join note. -const contentLabel = "콘텐츠 영역" +// Representative Story: Variants (Default, Long Labels) and States (Navigation Menu Open). Menus and the user menu open for real. +// Narrow the page to 390px for the mobile bar (hamburger → Sheet). Each GNB fills its specimen parent inside the same Inner as the +// page and section headings. The connected-area note shows once (Default); the verification Stories keep the default 12px join note. +// With Logo, Utilities, Sticky, GNB Sidebar and Brown Sidebar live in their own verification Stories. export const All: Story = { name: "GNB", render: () => ( - - - - + + + + + ), + }, + { + id: "long-labels", + label: "Long Labels", + wide: true, + children: ( + + + + ), + }, + ]} + /> - -
- - - -
+ {/* The open 2-depth panel ends 238px below the bar's top; the wrapper reserves that room so it never overlaps the page below. */} + + + + + +
+ ), + }, + ]} + />
- - - - - - - - - - - - - - - - - - - - - -
), } @@ -477,6 +490,8 @@ export const All: Story = { export const Default: Story = { tags: ["verification"], render: () => } export const Sticky: Story = { tags: ["verification"], render: () => } export const Sidebar: Story = { tags: ["verification"], name: "GNB Sidebar", render: () => } +// Brown content LNB: same condition the representative page used to show (560px frame, no route readout). +export const BrownSidebar: Story = { tags: ["verification"], name: "Brown Sidebar", render: () => } export const NavigationMenuOpen: Story = { tags: ["verification"], name: "Navigation Menu Open", diff --git a/src/patterns/lnb.stories.tsx b/src/patterns/lnb.stories.tsx index 43ef77f..85a5f49 100644 --- a/src/patterns/lnb.stories.tsx +++ b/src/patterns/lnb.stories.tsx @@ -619,28 +619,6 @@ const mobileViewport = { globals: { viewport: { value: "hmMobile", isRotated: false } }, } -// Representative-board preview of a Story at a fixed viewport. The LNB/bar switch is a viewport media query, so the board embeds the -// verification Story (the same `Standalone` composition) in an iframe of that width instead of rebuilding it in a narrow container. -// Each iframe is its own document, so the closed Sheets do not share a portal or focus with the board. -function ViewportPreview({ story, label, width, height, args }: { story: string; label: string; width: number; height: number; args?: string }) { - return ( -
-
- {label} · {width}px -
-
-