style(storybook): refine patterns data and layout stories

This commit is contained in:
shkim4 committed 2026-11-09 16:42:47 +09:00
1 parent 3bebe8a660
commit b33ca2a157
12 files changed
+168 -152

No files matched your search

+12
View File
@@ -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;
}
+1 -1
View File
@@ -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" />
+8 -5
View File
@@ -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 /> }
+1 -1
View File
@@ -90,7 +90,7 @@ export const All: Story = {
render: () => (
<StoryPage>
<StoryPageHeader title="Filter Bar" />
<StorySection title="Overview">
<StorySection title="Basic">
<Full />
</StorySection>
</StoryPage>
+55 -40
View File
@@ -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",
+4 -48
View File
@@ -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",
+3 -3
View File
@@ -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> },
]}
+79 -49
View File
@@ -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>
),