Merge pull request 'style(storybook): refine patterns data and layout stories' (#18) from feat/storybook-ui-refinement into main
Deploy Storybook / deploy (push) Successful in 57s
Deploy Storybook / deploy (push) Successful in 57s
Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
commit
84dddcc7f7
12 files changed
+168
-152
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 공개 컴포넌트·메뉴 로직은 없다.
|
||||
|
||||
|
||||
@@ -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. 목적 / 역할
|
||||
|
||||
@@ -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을 페이지 전체에 적용하지 않는다.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<StoryPage>
|
||||
<StoryPage className="dashboard-all-page">
|
||||
<StoryPageHeader title="Dashboard" />
|
||||
<StorySection title="Overview">
|
||||
<DashboardScreen mode="compact" />
|
||||
|
||||
@@ -155,15 +155,18 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Data Table" />
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="기본 목록 검색"><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색"><WithFilterBar /></StorySection>
|
||||
<StorySection title="Advanced Search"><WithFilterBar /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// 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 /> }
|
||||
|
||||
// Basic list search: status + keyword search, centered above the table.
|
||||
export const BasicSearch: Story = { tags: ["verification"], name: "Basic Search", render: () => <WithFilterBar extended={false} /> }
|
||||
|
||||
// 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: () => <WithFilterBar /> }
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Filter Bar" />
|
||||
<StorySection title="Overview">
|
||||
<StorySection title="Basic">
|
||||
<Full />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
{children}
|
||||
<div style={contentStyle}>
|
||||
<p className="text-sm text-muted-foreground">{label ?? "연결된 콘텐츠 영역: GNB는 자기 높이와 12px 접점까지만 차지합니다."}</p>
|
||||
<div style={compact ? compactContentStyle : contentStyle}>
|
||||
{label === "" ? null : <p className="text-sm text-muted-foreground">{label ?? "연결된 콘텐츠 영역: GNB는 자기 높이와 12px 접점까지만 차지합니다."}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -426,50 +429,60 @@ const meta = { title: "Patterns/Layout/GNB", component: GNB, parameters: { layou
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 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: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="GNB" />
|
||||
<StorySection title="Overview">
|
||||
<Page label={contentLabel}>
|
||||
<Bar />
|
||||
</Page>
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
states={[
|
||||
{
|
||||
id: "default",
|
||||
label: "Default",
|
||||
wide: true,
|
||||
children: (
|
||||
<Page compact label="콘텐츠 영역">
|
||||
<Bar />
|
||||
</Page>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "long-labels",
|
||||
label: "Long Labels",
|
||||
wide: true,
|
||||
children: (
|
||||
<Page compact label="">
|
||||
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
||||
</Page>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="States · Navigation Menu Open">
|
||||
<div style={{ minHeight: 360 }}>
|
||||
<Page label="프로젝트 > 진행 중 프로젝트">
|
||||
<Bar initial="projects/active" openMenu="projects" />
|
||||
</Page>
|
||||
</div>
|
||||
{/* The open 2-depth panel ends 238px below the bar's top; the wrapper reserves that room so it never overlaps the page below. */}
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
states={[
|
||||
{
|
||||
id: "navigation-menu-open",
|
||||
label: "Navigation Menu Open",
|
||||
wide: true,
|
||||
children: (
|
||||
<div style={{ minHeight: 248 }}>
|
||||
<Page compact label="프로젝트 > 진행 중 프로젝트">
|
||||
<Bar initial="projects/active" openMenu="projects" />
|
||||
</Page>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Variants · Long Labels">
|
||||
<Page label={contentLabel}>
|
||||
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection title="Variants · With Logo">
|
||||
<Page label={contentLabel}>
|
||||
<Bar logo={testLogo} />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection title="Variants · Utilities">
|
||||
<Page label={contentLabel}>
|
||||
<Bar utilities />
|
||||
</Page>
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Sticky">
|
||||
<StickyPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · GNB Sidebar">
|
||||
<SidebarPage showCurrent={false} />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB"><SidebarPage color="brown" showCurrent={false} /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -477,6 +490,8 @@ export const All: Story = {
|
||||
export const Default: Story = { tags: ["verification"], render: () => <Page><Bar /></Page> }
|
||||
export const Sticky: Story = { tags: ["verification"], render: () => <StickyPage /> }
|
||||
export const Sidebar: Story = { tags: ["verification"], name: "GNB Sidebar", render: () => <SidebarPage fullHeight /> }
|
||||
// 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: () => <SidebarPage color="brown" showCurrent={false} /> }
|
||||
export const NavigationMenuOpen: Story = {
|
||||
tags: ["verification"],
|
||||
name: "Navigation Menu Open",
|
||||
|
||||
@@ -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 (
|
||||
<figure className="grid min-w-0 content-start gap-2">
|
||||
<figcaption className="text-sm text-muted-foreground">
|
||||
{label} · {width}px
|
||||
</figcaption>
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<iframe
|
||||
title={`${label} ${width}px`}
|
||||
src={`iframe.html?id=patterns-layout-lnb--${story}&viewMode=story${args ? `&args=${args}` : ""}`}
|
||||
width={width}
|
||||
height={height}
|
||||
className="box-content block border border-black/10"
|
||||
/>
|
||||
</div>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
// `hm-lnb-story` (display: contents, see lnb.stories.css) scopes the 212px min width and 14px menu font to this Story only.
|
||||
const meta = { title: "Patterns/Layout/LNB", component: LNB, decorators: [(Story) => <div className="hm-lnb-story"><Story /></div>], parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta<typeof LNB>
|
||||
export default meta
|
||||
@@ -653,19 +631,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="LNB" />
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "selected", label: "Selected", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} dividers={false} /> },
|
||||
{ id: "third-selected", label: "3뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} dividers={false} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "selected", label: "Selected", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} dividers={false} /> },
|
||||
{ id: "expanded", label: "Expanded", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||
{ id: "states-third-selected", label: "3뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
||||
]}
|
||||
@@ -680,25 +650,12 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Scroll">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "scroll", label: "Scroll", children: <ResponsiveSpecimen nodes={simpleNav(false, true)} initial="overview" initialOpen={{ tasks: true }} height={460} dividers={false} /> },
|
||||
{ id: "third-scroll", label: "3뎁스 · Scroll", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} dividers={false} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Standalone 반응형">
|
||||
<div className="flex flex-wrap items-start gap-6">
|
||||
<ViewportPreview story="standalone-desktop" label="Desktop" width={1024} height={640} args="dividers:!false" />
|
||||
<ViewportPreview story="standalone-mobile" label="Mobile" width={390} height={640} />
|
||||
</div>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// Usage examples live in their own verification Stories: Scroll, Third Level Scroll, LNB Standalone Desktop / Mobile (and Mobile Menu Open).
|
||||
|
||||
export const Default: Story = { tags: ["verification"], render: () => <Menu initial={null} groupOpen={false} /> }
|
||||
export const Selected: Story = { tags: ["verification"], render: () => <Menu dividers={false} /> }
|
||||
export const Expanded: Story = { tags: ["verification"], render: () => <Menu initial={null} /> }
|
||||
@@ -754,8 +711,7 @@ export const WithGNB: Story = {
|
||||
|
||||
// LNB without a GNB, responsive. Desktop (>= 1024px): the column. Mobile (<= 1023px): the column is hidden and the connected-content
|
||||
// column becomes a full-width stack whose first row opens a Sheet with the same tree, selected item and every depth.
|
||||
// `dividers` is a Story arg (default true: the Story itself keeps its group dividers). The representative board's Desktop preview
|
||||
// opens this Story with `&args=dividers:!false` (see ViewportPreview) to show it without them.
|
||||
// `dividers` is a Story arg (default true: the Story itself keeps its group dividers).
|
||||
export const StandaloneDesktop: StoryObj<{ dividers: boolean }> = {
|
||||
tags: ["verification"],
|
||||
name: "LNB Standalone Desktop",
|
||||
|
||||
@@ -33,12 +33,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Loading Screen" />
|
||||
<StorySection title="Overview">
|
||||
<LoadingScreen />
|
||||
</StorySection>
|
||||
<StorySection title="Variants">
|
||||
{/* Default spans the full width; the other two share an equal 2-column row (1 column below 2 × 560px). */}
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "default", label: "Default", wide: true, children: <LoadingScreen /> },
|
||||
{ id: "label", label: "Custom label", children: <LoadingScreen>데이터를 불러오는 중</LoadingScreen> },
|
||||
{ id: "narrow", label: "Narrow parent (240px)", children: <div style={{ width: 240 }}><LoadingScreen /></div> },
|
||||
]}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
const meta = {
|
||||
@@ -27,62 +28,91 @@ type Story = StoryObj<typeof meta>
|
||||
|
||||
// Content composition only. The consuming screen owns navigation and action handlers.
|
||||
// Dashboard's minimum width and internal navigation belong to DashboardLayout.
|
||||
// The Breadcrumb header and the task Tabs / Table are shared by the representative page (header only) and the Full Page Story.
|
||||
function BreadcrumbHeader() {
|
||||
return (
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#home">홈</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#projects">프로젝트</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>업무 현황</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<PageHeaderTitle>업무 현황</PageHeaderTitle>
|
||||
<PageHeaderDescription>프로젝트별 업무 진행 상황을 확인하고 조회합니다.</PageHeaderDescription>
|
||||
</PageHeaderContent>
|
||||
<PageHeaderActions>
|
||||
<Button type="button" variant="primary" appearance="brand" size="default">
|
||||
신규 등록
|
||||
</Button>
|
||||
</PageHeaderActions>
|
||||
</PageHeader>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskTabs() {
|
||||
return (
|
||||
<Tabs defaultValue="mine" className="min-w-0">
|
||||
<TabsList aria-label="업무 목록 보기">
|
||||
<TabsTrigger value="mine">내 업무</TabsTrigger>
|
||||
<TabsTrigger value="completed">완료 업무</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="mine" className="px-0 py-6">
|
||||
<Table aria-label="내 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">마감일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<TableRow><TableCell>화면 설계서 검토</TableCell><TableCell>10월 14일</TableCell><TableCell>검토</TableCell></TableRow>
|
||||
<TableRow><TableCell>고객 요구사항 정리</TableCell><TableCell>10월 16일</TableCell><TableCell>진행</TableCell></TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
<TabsContent value="completed" className="px-0 py-6">
|
||||
<Table aria-label="완료 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">완료일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>프로젝트 기본 정보 등록</TableCell><TableCell>10월 9일</TableCell><TableCell>완료</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export const All: Story = {
|
||||
name: "Page Header",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Page Header" />
|
||||
<StorySection title="Overview">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>업무 현황</PageHeaderTitle></PageHeaderContent></PageHeader>
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "basic", label: "Basic", wide: true, children: <PageHeader><PageHeaderContent><PageHeaderTitle>업무 현황</PageHeaderTitle></PageHeaderContent></PageHeader> },
|
||||
{ id: "with-breadcrumb", label: "With Breadcrumb", wide: true, children: <BreadcrumbHeader /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
// The former representative page's full work screen (Breadcrumb header + Tabs + Table), kept unchanged as its own Story.
|
||||
export const FullPage: Story = {
|
||||
tags: ["verification"],
|
||||
name: "Full Page",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="Page Header" />
|
||||
<StorySection title="Breadcrumb 포함형">
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#home">홈</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#projects">프로젝트</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>업무 현황</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<PageHeaderTitle>업무 현황</PageHeaderTitle>
|
||||
<PageHeaderDescription>프로젝트별 업무 진행 상황을 확인하고 조회합니다.</PageHeaderDescription>
|
||||
</PageHeaderContent>
|
||||
<PageHeaderActions>
|
||||
<Button type="button" variant="primary" appearance="brand" size="default">
|
||||
신규 등록
|
||||
</Button>
|
||||
</PageHeaderActions>
|
||||
</PageHeader>
|
||||
<Tabs defaultValue="mine" className="min-w-0">
|
||||
<TabsList aria-label="업무 목록 보기">
|
||||
<TabsTrigger value="mine">내 업무</TabsTrigger>
|
||||
<TabsTrigger value="completed">완료 업무</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="mine" className="px-0 py-6">
|
||||
<Table aria-label="내 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">마감일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<TableRow><TableCell>화면 설계서 검토</TableCell><TableCell>10월 14일</TableCell><TableCell>검토</TableCell></TableRow>
|
||||
<TableRow><TableCell>고객 요구사항 정리</TableCell><TableCell>10월 16일</TableCell><TableCell>진행</TableCell></TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
<TabsContent value="completed" className="px-0 py-6">
|
||||
<Table aria-label="완료 업무">
|
||||
<TableHeader><TableRow><TableHead scope="col">업무</TableHead><TableHead scope="col">완료일</TableHead><TableHead scope="col">상태</TableHead></TableRow></TableHeader>
|
||||
<TableBody><TableRow><TableCell>프로젝트 기본 정보 등록</TableCell><TableCell>10월 9일</TableCell><TableCell>완료</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<BreadcrumbHeader />
|
||||
<TaskTabs />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user