From 8827eb2681b00ec65366a72cc6e4e6306b95be79 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 15:48:34 +0900 Subject: [PATCH] style(storybook): refine foundation and navigation stories --- docs/components/breadcrumb.md | 2 +- docs/components/menubar.md | 2 +- docs/components/navigation-menu.md | 2 +- docs/components/pagination.md | 2 +- docs/components/tabs.md | 4 +- src/components/breadcrumb.stories.tsx | 27 ++++++++--- src/components/menubar.stories.tsx | 8 ++-- src/components/navigation-menu.stories.tsx | 29 +++++------- src/components/pagination.stories.tsx | 24 +++++++--- src/components/tabs.stories.tsx | 53 ++++++++++----------- src/foundation/company-colors.stories.tsx | 14 +++--- src/foundation/foundation.css | 54 ++++++++++++++++++++-- src/foundation/layout.stories.tsx | 28 +++++++++-- src/foundation/typography.stories.tsx | 52 +++++++++++++++++++-- 14 files changed, 215 insertions(+), 86 deletions(-) diff --git a/docs/components/breadcrumb.md b/docs/components/breadcrumb.md index 53ed71f..d0280c7 100644 --- a/docs/components/breadcrumb.md +++ b/docs/components/breadcrumb.md @@ -69,7 +69,7 @@ Component Mapping 위치: `packages/ui/src/theme/breadcrumb.css` (`--hm-breadcru ## 9. Storybook -- 대표 Story `All`("Breadcrumb"): Basic(3 depth), Ellipsis(Dropdown Menu 조합), Long path, Custom link(asChild)와 custom separator를 한 페이지에 둔다. +- 대표 Story `All`("Breadcrumb"): Variants(Basic(3 depth), Ellipsis(Dropdown Menu 조합), Custom link(asChild)와 custom separator)를 PC 균등 3열로, Usage(Long path, 최대 900px)를 한 페이지에 둔다(2026-11-09 Overview 제거). - verification: `Basic`, `Ellipsis`, `Long Path`. ## 10. 사용자 확정 결정 diff --git a/docs/components/menubar.md b/docs/components/menubar.md index 1d16227..ab0cce6 100644 --- a/docs/components/menubar.md +++ b/docs/components/menubar.md @@ -76,7 +76,7 @@ Radix 구조·ARIA(`role=menubar`, `menu`, `menuitem*`)·키보드 모델(Down/E ## 9. Storybook -- 대표 Story `All`("Menubar"): 파일(item+shortcut, disabled, separator, sub menu 내보내기, destructive 종료), 편집, 보기(Label, Checkbox, Radio Group)를 가진 실제 bar. +- 대표 Story `All`("Menubar"): 구역 제목은 Basic(2026-11-09 Overview에서 변경, 열린 메뉴용 고정 min-height 384px 제거). 파일(item+shortcut, disabled, separator, sub menu 내보내기, destructive 종료), 편집, 보기(Label, Checkbox, Radio Group)를 가진 실제 bar. - 숨김 verification: `TriggerFocus`, `OpenFocus`. `OpenFocus`는 검수 목적으로 Story의 `play`에서 실제 열린 menu의 Trigger에 focus를 준다. 일반 Story/Production은 Radix 기본대로 메뉴 항목에 focus를 이동한다. - Focus 검수는 일반 Story의 computed style 기준으로 한다. Designer의 `.hm-designer button:focus-visible` 장식 outline을 컴포넌트 ring의 근거로 사용하지 않는다. diff --git a/docs/components/navigation-menu.md b/docs/components/navigation-menu.md index 6f246b6..0560dab 100644 --- a/docs/components/navigation-menu.md +++ b/docs/components/navigation-menu.md @@ -72,7 +72,7 @@ Radix 구조·동작(hover open delay, Arrow 키와 Tab 이동, Escape 닫힘, f ## 9. Storybook -- 대표 Story `All`("Navigation Menu"): `Viewport(기본)`과 `viewport={false}` 두 구성을 한 페이지에 둔다. 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger). +- 대표 Story `All`("Navigation Menu"): Variants 한 구역에 `Viewport(기본)`과 `viewport={false}` 두 구성을 PC 균등 2열로 둔다(2026-11-09 Overview 제거, 닫힌 메뉴용 min-height 256px는 PC에서만 해제하고 1000px 미만 1열에서는 유지). 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger). - verification: `Default`, `No Viewport`. - Story의 Content 폭은 `style` 인라인으로 지정한다(새 Story 파일의 Tailwind 임의값 클래스가 개발 서버에서 생성되지 않는 일이 있었다). diff --git a/docs/components/pagination.md b/docs/components/pagination.md index 9dc1f6e..e301713 100644 --- a/docs/components/pagination.md +++ b/docs/components/pagination.md @@ -67,7 +67,7 @@ Component Mapping 위치: `packages/ui/src/theme/pagination.css` (`--hm-paginati ## 9. Storybook -- 대표 Story `All`("Pagination"): First page, Middle page, Last page, Long range with ellipsis(1 … 9 10 11 … 20)를 한 페이지에 둔다. 각 Specimen이 local state를 가지고 클릭 시 현재 페이지가 실제로 바뀐다(`#page-N` href + `preventDefault`). +- 대표 Story `All`("Pagination"): Variants 한 구역에 First page, Middle page, Last page, Long range with ellipsis(1 … 9 10 11 … 20)를 PC 2열 × 2행으로 둔다(2026-11-09 Overview / States / Usage 통합, 버튼 묶음은 칸 안 왼쪽 정렬). 각 Specimen이 local state를 가지고 클릭 시 현재 페이지가 실제로 바뀐다(`#page-N` href + `preventDefault`). - verification: `First Page`, `Last Page`, `Long Range`. ## 10. 사용자 확정 결정 diff --git a/docs/components/tabs.md b/docs/components/tabs.md index f297244..408881d 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -126,12 +126,12 @@ Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving ta ## 9. Storybook -- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary다. 같은 Primary가 반복되던 **Default API** 구역(`fill`만, `fill` + `panelSurface="content"` 명시)은 2026-10-22 숨김 verification `DefaultApi`로 옮겼다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다. +- 대표 Story `All`("Tabs"): **Variants**는 Primary(fill / panelSurface를 생략한 기본)·Secondary(`panelSurface="surface"`)·Overflow(20개 탭, 이전 States의 Overflow 예시)를 PC 균등 3열로 두고, **Usage**는 Card 내부 · Secondary / 페이지 직접 배치 · Primary 2열이다(2026-11-09 Overview·States 구역을 Variants로 통합, 이 페이지의 위아래 패딩만 줄여 1280×900 한 화면). 같은 Primary가 반복되던 **Default API** 구역(`fill`만, `fill` + `panelSurface="content"` 명시)은 2026-10-22 숨김 verification `DefaultApi`로 옮겼다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다. - verification(`tags: ["verification"]`): `DefaultApi`, `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다. - `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다. - non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10). - 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다. -- Overview / Variants / States, `DefaultApi` 및 Fill verification 예제의 콘텐츠 panel은 `px-0 py-6`로 통일해 D-01을 실제로 반영한다. Primary는 상위 content 면, Secondary는 상위 surface 면에 연결하며 내부 패널의 별도 border·배경·radius를 제거한다. Compatibility의 Legacy non-fill 두 예제만 이전 `border bg-card rounded-b p-4`를 보존한다. +- Variants / Usage, `DefaultApi` 및 Fill verification 예제의 콘텐츠 panel은 `px-0 py-6`로 통일해 D-01을 실제로 반영한다. Primary는 상위 content 면, Secondary는 상위 surface 면에 연결하며 내부 패널의 별도 border·배경·radius를 제거한다. Compatibility의 Legacy non-fill 두 예제만 이전 `border bg-card rounded-b p-4`를 보존한다. - **Usage**(2026-10-09 추가)는 공개 Card / Input / Textarea / Table을 조합해 Card 내부 Secondary와 페이지 content 면 위 Primary를 보여준다. 두 예제 모두 `TabsContent`는 `px-0 py-6`이며 별도 배경·border·radius를 추가하지 않는다. 대표 Story ID·기존 구역·verification Story는 유지한다. ## 10. 사용자 확정 결정 diff --git a/src/components/breadcrumb.stories.tsx b/src/components/breadcrumb.stories.tsx index 8f02574..f372a3b 100644 --- a/src/components/breadcrumb.stories.tsx +++ b/src/components/breadcrumb.stories.tsx @@ -13,7 +13,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@hm-design/ui" -import { StateBoard } from "@/components/story-board" +import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // The representative "Breadcrumb" Story shows each case on one page (real links with sample hrefs); the same specimens also exist @@ -122,20 +122,33 @@ const meta = { title: "Components/Navigation/Breadcrumb", component: Breadcrumb, export default meta type Story = StoryObj -const board = (...ids: string[]) => ( - ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: narrow(s.node) }))} /> -) +// Representative-page layout (Storybook presentation only): Variants use 3 equal columns on PC (iframe width 1000px and up), +// fewer below that (240px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so +// StateBoard's auto-fill grid cannot leave an empty or stretched track. +const specimen = (id: string, children?: ReactNode): BoardState => { + const s = sections.find((section) => section.id === id)! + return { id: s.id, label: s.label, children: children ?? narrow(s.node) } +} // Representative Story: the common Story page; the sections group the same specimens. +// Overview is merged into Variants (Basic is its first item). The Long path example gets up to 900px so it fits on one line on +// PC; BreadcrumbList is `flex-wrap`, so it wraps naturally (never clipped or forced nowrap) when the space is narrower. export const All: Story = { name: "Breadcrumb", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - {board("basic")} - {board("ellipsis", "custom")} - {board("long")} + +
+ {["basic", "ellipsis", "custom"].map((id) => ( + + ))} +
+
+ + {sections.find((s) => s.id === "long")!.node})]} /> +
), } diff --git a/src/components/menubar.stories.tsx b/src/components/menubar.stories.tsx index 392bc17..0686195 100644 --- a/src/components/menubar.stories.tsx +++ b/src/components/menubar.stories.tsx @@ -97,10 +97,10 @@ export const All: Story = { render: () => ( - -
- -
+ {/* The section is titled Basic (was Overview). The former `minHeight: 384` reserved room for the open menus; they render in a + portal over the page (with collision handling), nothing follows this section, so the fixed height is not needed. */} + +
), diff --git a/src/components/navigation-menu.stories.tsx b/src/components/navigation-menu.stories.tsx index f2cca64..9f8333c 100644 --- a/src/components/navigation-menu.stories.tsx +++ b/src/components/navigation-menu.stories.tsx @@ -83,10 +83,13 @@ function Specimen({ viewport = true }: { viewport?: boolean }) { // Size specimens to their review area (including the panel's 8px padding + 1px border on each side). // Mobile uses the content's natural minimum width instead of a fixed desktop specimen width. -const frame = (node: ReactNode) => ( +// `minHeight: 256` reserves room for the open panel (about 232px below the menu bar) so it never covers what follows. +// `compact` (representative page only) drops that reservation on PC (iframe width 1000px and up), where the two examples sit side by +// side and nothing is below them; in the one-column layout below 1000px the 256px stays so an open menu does not cover the next example. +const frame = (node: ReactNode, compact = false) => (
{node}
@@ -102,21 +105,13 @@ export const All: Story = { render: () => ( - - ) }, - ]} - /> - + {/* Overview is merged into Variants (Viewport is its first item). Two equal columns on PC (iframe width 1000px and up), one + column below that; each specimen sits in its own single-column StateBoard (min larger than any column). */} - ) }, - ]} - /> +
+ , true) }]} /> + , true) }]} /> +
), diff --git a/src/components/pagination.stories.tsx b/src/components/pagination.stories.tsx index 7f7346d..55e9209 100644 --- a/src/components/pagination.stories.tsx +++ b/src/components/pagination.stories.tsx @@ -79,20 +79,30 @@ const meta = { title: "Components/Navigation/Pagination", component: Pagination, export default meta type Story = StoryObj -const board = (...ids: string[]) => ( - ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: wrap(s.node) }))} /> -) +// Representative-page layout (Storybook presentation only): one Variants section, 2 equal columns x 2 rows on PC (iframe width +// 1030px and up), 1 column below that. Each specimen sits in its own single-column StateBoard (min larger than any +// column), so every title and divider has the width of its column. The Pagination root is `mx-auto flex w-full justify-center`; +// `justify-start` on the cell left-aligns the button group in its column without stretching it. +const cellAlignLeft = "w-full [&_[data-slot=pagination]]:justify-start" -// Representative Story: the common Story page; the sections group the same specimens. +// Representative Story: the common Story page. Overview / States / Usage are merged into Variants +// (First page, Middle page, Last page, Long range). export const All: Story = { name: "Pagination", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - {board("middle")} - {board("first", "last")} - {board("ellipsis")} + + {/* Long range is 447px wide (20 pages, 7 items), so a column must be at least that: 16px column gap and the 2-column switch at + iframe width 1030px give 449px columns at 1280px; below that it is one column (460px minimum) instead of a wrapped "Next". */} +
+ {["first", "middle", "last", "ellipsis"].map((id) => { + const spec = sections.find((section) => section.id === id)! + return {spec.node}
}]} /> + })} + +
), } diff --git a/src/components/tabs.stories.tsx b/src/components/tabs.stories.tsx index 864a11b..cd1ec30 100644 --- a/src/components/tabs.stories.tsx +++ b/src/components/tabs.stories.tsx @@ -1,11 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react-vite" -import { useId, useState, type ComponentProps, type CSSProperties } from "react" +import { useId, useState, type ComponentProps, type CSSProperties, type ReactNode } from "react" import { Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, } from "@hm-design/ui" -import { StateBoard } from "@/components/story-board" +import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // Representative "Tabs" Story shows default Primary, explicit API comparisons and Secondary; Legacy stays verification-only. @@ -127,6 +127,18 @@ function LongLabelSpecimens() { ) } +// Equal two columns on PC (one column below 900px). Each board holds a single specimen: StateBoard's own auto-fill grid +// would leave an empty third column for two items, so `min` is forced above any column width (same approach as Button). +const TwoColumns = ({ children }: { children: ReactNode }) => ( +
{children}
+) +const SingleBoard = ({ states }: { states: BoardState[] }) => +// Variants: equal three columns on PC (iframe width 1000px and up); fewer columns below that (240px minimum). `min-w-0` lets the +// Overflow Tabs keep its own internal scroll (previous / next buttons) inside its column instead of widening the page. +const ThreeColumns = ({ children }: { children: ReactNode }) => ( +
{children}
+) + const meta = { title: "Components/Navigation/Tabs", component: Tabs, @@ -147,34 +159,23 @@ export const All: Story = { name: "Tabs", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - + // `py-4!` trims this page's top and bottom padding (30-40px by viewport width) so the page fits a 900px-high PC screen without + // shrinking any component; the important modifier is needed because the shared rule has the same specificity. Section gaps stay 32px. + - - }, - ]} - /> - - }, - ]} - /> - - - }, - ]} - /> + {/* The former States section (Overflow) is the third Variants item. */} + + }]} /> + }]} /> + }]} /> + - }, - { id: "usage-page", label: "페이지 직접 배치 · Primary", children: }, - ]} /> + + }]} /> + }]} /> + ), diff --git a/src/foundation/company-colors.stories.tsx b/src/foundation/company-colors.stories.tsx index 6e36708..e19cad1 100644 --- a/src/foundation/company-colors.stories.tsx +++ b/src/foundation/company-colors.stories.tsx @@ -15,12 +15,14 @@ function CompanyColorChip({ label, hex, main }: { label: string; hex: string; ma function CompanyColorsPage() { return - {companies.map(company => -
- - {company.secondary.map((hex, index) => )} -
-
)} +
+ {companies.map(company => +
+ + {company.secondary.map((hex, index) => )} +
+
)} +
} const meta = { title: "Foundation/Company Colors", parameters: { layout: "fullscreen" } } satisfies Meta diff --git a/src/foundation/foundation.css b/src/foundation/foundation.css index 33205e0..7bf35f0 100644 --- a/src/foundation/foundation.css +++ b/src/foundation/foundation.css @@ -24,8 +24,6 @@ .hm-foundation-specimen small { display: block; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); font-weight: var(--hm-input-font-weight); } .hm-foundation-spacing { max-width: 100%; height: 24px; margin-block: 12px; background: var(--hm-sem-state-active); } .hm-foundation-radius { width: 100%; height: 64px; margin-bottom: 16px; border: 1px solid var(--hm-sem-stroke-default); background: var(--hm-sem-bg-content); } -.hm-foundation-layout-sample { display: grid; min-width: 0; background: var(--hm-sem-bg-content); border: 1px solid var(--hm-sem-stroke-input-default); } -.hm-foundation-layout-card { min-width: 0; border: 1px solid var(--hm-card-border-color); border-radius: var(--hm-card-radius); background: var(--hm-card-surface); } .hm-brand-color-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; } .hm-brand-color-chip { flex: 1 0 110px; min-width: 0; padding: 8px; border: 1px solid var(--hm-sem-stroke-default); } .hm-brand-color-chip[data-base] { border: 2px solid var(--hm-sem-fg-primary); font-weight: var(--hm-font-weight-bold); } @@ -39,5 +37,53 @@ .hm-foundation .hm-color-gradient-stops .hm-brand-color-chip[data-gradient-stop-token] { flex-basis: 220px; } .hm-foundation .hm-color-foreground-sample { padding: 16px; background: var(--hm-sem-bg-surface); } -/* Company Colors: keep each chip close to the Color row width so 1-3 chips per company do not stretch across the page. */ -.hm-foundation .hm-brand-color-row[data-company-id] .hm-brand-color-chip { flex: 0 1 140px; } +/* Company Colors: two-level grid. Columns follow the Story content width (not the viewport): companies 3 -> 2 -> 1 (min 316px each), + chips up to 3 per row (min 100px each, 3 -> 2 -> 1). Chip size stays at one third even when a company has fewer chips. */ +.hm-foundation .hm-company-grid { display: grid; gap: 48px 32px; grid-template-columns: repeat(auto-fill, minmax(max(316px, (100% - 64px) / 3), 1fr)); } +.hm-foundation .hm-company-grid > .sb-page-section { min-width: 0; margin-top: 0; } +.hm-foundation .hm-brand-color-row[data-company-id] { display: grid; gap: 8px; overflow: visible; padding-bottom: 0; grid-template-columns: repeat(auto-fill, minmax(max(100px, (100% - 16px) / 3), 1fr)); } + +/* Typography: compact specification page. Font Family one line, Font Weight small cards, Specimens as comparison tables + (two columns on wide screens, stacked rows on narrow screens). */ +.hm-foundation .hm-typo-family { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin: 0; } +.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px; } +.hm-foundation .hm-typo-weight { padding: 12px 16px; } +.hm-foundation .hm-typo-weight p { margin: 0; } +.hm-foundation .hm-typo-weight-preview { margin: 4px 0 !important; font-size: var(--hm-card-title-font-size); line-height: var(--hm-card-title-line-height); } +.hm-foundation .hm-typo-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px 32px; align-items: start; } +.hm-foundation .hm-typo-table { width: 100%; border-collapse: collapse; } +.hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--hm-sem-stroke-input-default); text-align: left; vertical-align: baseline; font-weight: var(--hm-input-font-weight); } +.hm-foundation .hm-typo-table thead th { color: var(--hm-sem-fg-secondary); font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } +.hm-foundation .hm-typo-table tbody th { width: 27%; font-weight: var(--hm-font-weight-bold); } +.hm-foundation .hm-typo-table td:last-child { width: 26%; padding-right: 0; } +.hm-foundation .hm-typo-metrics span, .hm-foundation .hm-typo-metrics small { display: block; } +.hm-foundation .hm-typo-metrics small { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } +@media (min-width: 1100px) { .hm-foundation .hm-typo-tables { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 640px) { + .hm-foundation .hm-typo-table thead { display: none; } + .hm-foundation .hm-typo-table, .hm-foundation .hm-typo-table tbody, .hm-foundation .hm-typo-table tr, .hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { display: block; width: auto !important; } + .hm-foundation .hm-typo-table tr { padding: 8px 0; border-bottom: 1px solid var(--hm-sem-stroke-input-default); } + .hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { padding: 2px 0; border-bottom: 0; } +} + +/* Layout: compact specification page (PC: three bands; narrow screens stack and scroll naturally). + Tightening applies only inside .hm-layout-ref; the common Story page and other Foundation pages are unchanged. No fixed heights or clipping. */ +.hm-foundation .hm-layout-ref { display: grid; gap: 32px; word-break: keep-all; } +.hm-foundation .hm-layout-ref .sb-page-section { margin-top: 0; } +.hm-foundation .hm-layout-ref .sb-page-section-header { padding-bottom: 8px; margin-bottom: 12px; } +.hm-foundation .hm-layout-ref p, .hm-foundation .hm-layout-ref h3 { margin: 0; } +.hm-foundation .hm-layout-ref .hm-foundation-card, .hm-foundation .hm-layout-ref .hm-foundation-panel { padding: 8px 16px; } +.hm-foundation .hm-layout-ref code { font-size: var(--hm-button-font-size-sm); color: var(--hm-sem-fg-secondary); } +.hm-foundation .hm-layout-ref .hm-layout-value { font-weight: var(--hm-font-weight-bold); } +.hm-foundation .hm-layout-list { display: grid; gap: 8px 24px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); align-content: center; } +.hm-foundation .hm-layout-spacing-grid { display: grid; gap: 12px; grid-template-columns: minmax(0, 1fr); } +.hm-foundation .hm-layout-spacing-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; } +.hm-foundation .hm-layout-spacing-foot { display: flex; align-items: center; gap: 12px; margin-top: 4px; min-height: 14px; } +.hm-foundation .hm-layout-spacing-foot .hm-foundation-spacing { flex: none; height: 6px; margin: 0; } +.hm-foundation .hm-layout-spacing-foot .hm-foundation-note { min-width: 0; font-size: var(--hm-button-font-size-sm); } +.hm-foundation .hm-layout-radius-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); } +.hm-foundation .hm-layout-ref .hm-layout-radius .hm-foundation-radius { height: 24px; margin: 0 0 6px; } +.hm-foundation .hm-layout-radius .hm-layout-value { display: block; } +.hm-foundation .hm-layout-radius .hm-layout-spacing-head { margin-top: 0; } +@media (min-width: 480px) { .hm-foundation .hm-layout-spacing-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (min-width: 640px) { .hm-foundation .hm-layout-spacing-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } diff --git a/src/foundation/layout.stories.tsx b/src/foundation/layout.stories.tsx index d185bbb..7c536a7 100644 --- a/src/foundation/layout.stories.tsx +++ b/src/foundation/layout.stories.tsx @@ -2,14 +2,32 @@ import type { Meta, StoryObj } from "@storybook/react-vite" import { layoutProperties, radiusReferences, spacingReferences } from "./reference-data" import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" +// Compact specification page: item name, key value and preview only. PC shows every section within one screen, narrow screens stack and scroll naturally. +// Token names stay in the data (reference-data.ts) and in data-token attributes. Shortened wording (kept here for reference): +// Desktop: "Desktop Navigation · LNB 표시 · GNB desktop menu" / Mobile: "Mobile Navigation · LNB 숨김 · GNB mobile menu" +// Only verified items are shown (checked against the token definitions, CSS usage and docs on 2026-11-09): +// - "Mobile 20px" for Dashboard padding is not implemented: --hm-dashboard-padding is 34px in every viewport (docs/patterns/dashboard-layout.md). +// - --hm-radius-select is declared in default.css but has no consumer (Select uses --hm-select-radius -> --hm-radius-input), so it is not listed. +const radiusLabels: Record = { + "--hm-radius-control-md": "Control", + "--hm-radius-button": "Button", + "--hm-radius-input": "Input", + "--hm-radius-container": "Container", +} +const shownRadiusReferences = radiusReferences.filter(property => property in radiusLabels) + +function Item({ name, value, note }: { name: string; value: string; note?: string }) { + return

{name}

{value}
{note ?

{note}

: null}
+} + function LayoutReferencePage() { const values = useResolvedProperties(layoutProperties) return -

≥ 1024px · Desktop

Desktop Navigation · LNB 표시 · GNB desktop menu

≤ 1023px · Mobile

Mobile Navigation · LNB 숨김 · GNB mobile menu

-

1280 × 900

HM Desktop · QA reference

390 × 844

HM Mobile · QA reference

-
{spacingReferences.map(([property, name, responsive]) =>

{name}

{property}

{values[property]}

)}
-
{radiusReferences.map(property =>
{property}

{values[property]}

)}
-
Dashboard content

가용 폭을 사용하며 긴 텍스트는 내부에서 줄바꿈합니다.

다음 섹션 · gap: {values["--hm-dashboard-gap"]}
+
+
+
{spacingReferences.map(([property, name]) =>

{name}

{values[property]}
)}
+
{shownRadiusReferences.map(property =>

{radiusLabels[property]}

{values[property]}
)}
+
} const meta = { title: "Foundation/Layout", parameters: { layout: "fullscreen" } } satisfies Meta diff --git a/src/foundation/typography.stories.tsx b/src/foundation/typography.stories.tsx index d94eeb0..050ebc6 100644 --- a/src/foundation/typography.stories.tsx +++ b/src/foundation/typography.stories.tsx @@ -1,18 +1,62 @@ +import { useEffect, useRef, useState } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { typographyProperties, typographyReferences } from "./reference-data" -import { ReferencePage, ReferenceSection, Specimen, useResolvedProperties } from "./reference" +import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" + +type TypographyReference = (typeof typographyReferences)[number] const weights = [ { property: "--hm-input-font-weight", label: "Regular" }, { property: "--hm-font-weight-medium", label: "Medium" }, { property: "--hm-font-weight-bold", label: "Bold" }, ] +// Left table: text styles. Right table: form controls. Together they cover every entry in typographyReferences. +const textLabels = ["Page Header · title", "Card · title", "Page Header · description", "Label"] +const textRows = typographyReferences.filter(item => textLabels.includes(item.label)) +const controlRows = typographyReferences.filter(item => !textLabels.includes(item.label)) + +function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) { + const ref = useRef(null) + const [metrics, setMetrics] = useState({ main: "", spacing: "" }) + useEffect(() => { + let mounted = true + void document.fonts.ready.then(() => { + if (!mounted || !ref.current) return + const style = getComputedStyle(ref.current) + setMetrics({ main: `${style.fontSize} / ${style.lineHeight} / ${style.fontWeight}`, spacing: `letter-spacing: ${style.letterSpacing}` }) + }) + return () => { mounted = false } + }, []) + return + {label} +
한맥 Hanmac 0123
+ {metrics.main}{metrics.spacing} + +} + +function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) { + return + + {rows.map(item => )} +
ComponentSampleSize / Line / Weight
+} + function TypographyReferencePage() { const values = useResolvedProperties(typographyProperties) return -

Noto Sans KR → 맑은 고딕 → 시스템 sans-serif

-
{weights.map(({ property, label }) =>
{property}

{values[property]} — {label}

한맥 프로젝트
Hanmac Project
0123456789

)}
-
{typographyReferences.map(item =>

{item.label}

)}
+ +

Noto Sans KR → 맑은 고딕 → 시스템 sans-serif--hm-font-family-base: {values["--hm-font-family-base"]}

+
+ +
{weights.map(({ property, label }) =>
+

{label} {values[property]}

+

한맥 Hanmac 0123

+ {property} +
)}
+
+ +
+
} const meta = { title: "Foundation/Typography", parameters: { layout: "fullscreen" } } satisfies Meta