docs(storybook): simplify sidebar Story pages around the UI
Drop the category kicker from the 83 sidebar Stories (kicker is now optional; hidden verification Stories keep it) and shorten or remove repeated, obvious or internal page/section descriptions. Required specs, limits, virtual-data and undecided-value notices stay. Story IDs, exports, fixtures, shared state notes and package code are unchanged. Record: docs/reports/storybook-ui-simplification-2026-10-22.md
This commit is contained in:
1 parent
aa8b7f8df1
commit
da0e647b49
70 files changed
+305
-333
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# Storybook UI 중심 간소화 (2026-10-22)
|
||||
|
||||
기준: `main` `aa8b7f8`. Storybook 전용 파일만 수정했습니다. 패키지 컴포넌트·공개 API·Theme Token·Story ID·export는 변경하지 않았습니다.
|
||||
|
||||
## 범위
|
||||
|
||||
- 조사: Story 파일 80개, Story 359개(사이드바 표시 83개, `verification`/`internal` 숨김 276개)
|
||||
- 적용: 사이드바 표시 Story 83개
|
||||
- 숨김 Story 276개는 테스트 기준 화면이므로 그대로 둡니다. 숨김 Story와 공유하는 State note(Input Readonly, Textarea Hover/Readonly, Progress, Table 등)도 유지합니다.
|
||||
|
||||
## 기준
|
||||
|
||||
| 유형 | 처리 |
|
||||
|---|---|
|
||||
| 상단 분류 라벨 `HM DESIGN SYSTEM / …` | 표시 Story에서 제거. `StoryPageHeader`의 `kicker`는 선택 Prop이 되었고 숨김 Story는 계속 전달합니다. |
|
||||
| 섹션 제목을 반복하는 설명(`기본 Tooltip입니다.`, `side: …`, `className 확장입니다.`) | 제거 |
|
||||
| 당연한 조작 안내(`버튼을 눌러 엽니다`, `포인터를 올려 확인합니다`) | 제거 |
|
||||
| 긴 페이지·섹션 설명 | 핵심만 한 줄로 축약 |
|
||||
| 내부 기술 설명(Figma 노드, 날짜, 내부 토큰명, CSS 파일명) | 화면에서 제거. 동작 상세는 `packages/sw/ai/COMPONENT_CATALOG.md`, `DESIGN_RULES.md`, `docs/components/*.md`에 이미 있습니다. |
|
||||
| CENTER 반복 경계 안내(`CENTER_BOUNDARY`) | 한 줄로 축약하고 유지 |
|
||||
| 필수 규격·제한(px 크기, 허용 조합, Hover 없음, 가상 데이터, 실제 인원 아님, 테스트 로고, 미확정 TBD/Candidate/Q3, 고정 날짜, GNB_ONLY Sidebar 금지) | 유지 |
|
||||
| OrgChart 설명 | 제거하지 않고 축약만 합니다(verify-p8/p9가 `.sb-page-description`, `.sb-page-section-description` 스타일을 읽음). |
|
||||
| SW 페이지 설명 | 기존 CSS가 이미 화면에서 숨기므로 표시 변화는 분류 라벨 제거뿐입니다. Home Menu만 소스 문구를 함께 축약했습니다. |
|
||||
|
||||
## 결과 (1440 × 900, 첫 UI 영역 상단 위치)
|
||||
|
||||
- 표시 설명 글자 수: 15,240 → 7,431
|
||||
- 첫 UI 위치: 평균 28px 위로 이동(최소 24, 최대 81). Button 290 → 266, Dialog 290 → 234, Resizable 266 → 210, CENTER Overview 317 → 236, SW 페이지 234 → 210
|
||||
- Story ID 359개는 변경 전후 동일합니다.
|
||||
|
||||
## 검증
|
||||
|
||||
- `tsc -p tsconfig.app.json --noEmit` PASS, `npm run test:guard` 94/94, `npm run check:ai-docs` PASS
|
||||
- `build-storybook` 성공. Story 359개를 직접 URL로 렌더했을 때 오류 화면은 없었습니다. Avatar `all`·`fallback-error`의 404는 의도된 이미지 실패 예시이며 변경 전에도 같습니다.
|
||||
- `packages/sw/tests/storybook-simplification.browser.mjs` 9/9 PASS
|
||||
- `examples/org-chart/verify-p9.mjs`: 변경 전 빌드로 만든 로컬 기준에 대해 변경 후 67 PASS / 17 FAIL입니다. 변경 전 빌드도 같은 17건이 FAIL이므로 기존 이슈입니다.
|
||||
- `packages/sw/tests/integrated-visual.browser.mjs` capture/compare: 숨김 검증 Story 6종(detail-buttons 등)은 동일합니다. 표시 Story(overview, logo-slot, responsive-more, property-panel-compare)의 차이는 모두 분류 라벨 제거에 따른 세로 이동(24px, Overview는 12px)이며 그 외 픽셀 차이는 없습니다. 시각 기준 이미지는 갱신하지 않았습니다.
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
RoadsideReferenceDemo,
|
||||
TunnelReferenceDemo,
|
||||
} from "@hm-design/center-graphic/reference"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/facility.md and facility-assets.md (128 approved SVG references, shipped unchanged).
|
||||
@@ -30,9 +30,8 @@ const page = (name: string, Demo: ComponentType): Story => ({
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.center}
|
||||
title={name}
|
||||
description="CENTER 시설물 그래픽입니다. 승인 SVG Reference를 위치도(Position Map)와 실사화(Actual / Detail)로 표시합니다."
|
||||
description="승인 SVG Reference · 위치도 / 실사화"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
{withRepresentation(() => <Demo />)}
|
||||
@@ -54,11 +53,10 @@ export const Linked: Story = {
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-linked-page">
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.center}
|
||||
title="연계 시설물"
|
||||
description={
|
||||
<>
|
||||
CENTER 시설물 그래픽입니다. 승인 SVG Reference를 위치도(Position Map)와 실사화(Actual / Detail)로 표시합니다.
|
||||
승인 SVG Reference · 위치도 / 실사화
|
||||
<br />
|
||||
{CENTER_BOUNDARY}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CenterInformationDisplayReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
|
||||
// Owner: ui-viz docs/patterns/center-information-presentation.md and center-information-{box,table,emphasis}.md.
|
||||
@@ -18,9 +18,8 @@ export const Information: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.center}
|
||||
title="Information"
|
||||
description="CENTER 정보 표시(Box · Table · Emphasis) Reference입니다. 단일행 말줄임만 해결된 상태이며 배율(Q3)은 정해지지 않았습니다."
|
||||
description="Box · Table · Emphasis · 배율(Q3) 미정"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
{withRepresentation(() => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, CenterFrame, SCALE_NOTE, StatusTable } from "./center-shared"
|
||||
|
||||
// Mirrors the ui-viz docs/governance/center-domain.md §5 registry rows. The values stay in the owner documents.
|
||||
@@ -28,13 +28,13 @@ export const Overview: Story = {
|
||||
name: "Overview",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={STORY_KICKER.center} title="Overview" description={CENTER_BOUNDARY} />
|
||||
<StoryPageHeader title="Overview" description={CENTER_BOUNDARY} />
|
||||
<CenterFrame>
|
||||
<div className="sb-center-overview">
|
||||
<StorySection title="Representation Scale" description="제작 해상도별 CENTER 그래픽 표시 배율입니다.">
|
||||
<StorySection title="Representation Scale">
|
||||
<p className="sb-center-note">{SCALE_NOTE}</p>
|
||||
</StorySection>
|
||||
<StorySection title="Status" description="ui-viz center-domain.md §5 registry의 항목별 상태와 이 Storybook에서의 표현입니다.">
|
||||
<StorySection title="Status" description="항목별 확정 상태와 Storybook 표현">
|
||||
<div className="sb-center-scroll">
|
||||
<StatusTable rows={STATUS} />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./center-stories.css"
|
||||
// CENTER is a separate domain from the UI Component System. These stories present the packaged CENTER components and the
|
||||
// reference demo pages; they do not own, create or promote CENTER contract values (see docs/center-graphic/README.md).
|
||||
export const CENTER_BOUNDARY =
|
||||
"CENTER Web Reference 렌더링입니다. Canonical 값·상태는 ui-viz CENTER owner 문서가 소유하며 HOLD / TBD / Candidate / OWNER_DECISION_REQUIRED를 이 화면이 확정하지 않습니다."
|
||||
"Reference 렌더링 · HOLD / TBD / Candidate 값은 ui-viz owner 문서 기준이며 이 화면이 확정하지 않음"
|
||||
|
||||
export const SCALE_NOTE =
|
||||
"제작 대상이 7680 × 2160이면 CENTER 그래픽을 100%, 3840 × 2160이면 75%로 표시합니다. Storybook 화면 크기는 배율을 정하지 않으며, 다른 제작 해상도의 배율은 정해지지 않았습니다. 제작 대상은 주소에 centerResolution=3840x2160 을 붙여 바꿉니다."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { RouteLine } from "@hm-design/center-graphic"
|
||||
import { CenterSpatialInformationReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/route.md, spatial-map.md, spatial-section.md, composition.md.
|
||||
@@ -20,9 +20,8 @@ export const Spatial: Story = {
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-spatial-page">
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.center}
|
||||
title="Spatial"
|
||||
description="CENTER 노선·공간 정보 Reference입니다. 노선 레이어 순서(Shadow → Outline → Primary Stroke)는 확정이며, 전체 지도 레이어 순서는 Candidate, line join은 TBD입니다."
|
||||
description="노선 레이어 순서 확정 · 전체 지도 레이어 순서 Candidate · line join TBD"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
{withRepresentation(() => <CenterSpatialInformationReferenceDemo />)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CenterTitle, CENTER_TITLE_COMPOSITIONS, type CenterTitleComposition } from "@hm-design/center-graphic"
|
||||
import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { CENTER_BOUNDARY, withRepresentation } from "./center-shared"
|
||||
|
||||
// Owner: ui-viz docs/components/center-title.md. Title Resolution B numeric output is TBD; the per-Type palette is SCOPED_DESIGN_APPROVAL.
|
||||
@@ -21,9 +21,8 @@ export const Compositions: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.center}
|
||||
title="Title"
|
||||
description="CENTER 제목 그래픽의 네 가지 Type Reference입니다. Resolution B 수치는 TBD, Type별 palette는 SCOPED_DESIGN_APPROVAL입니다."
|
||||
description="4가지 Type · Resolution B 수치 TBD · palette SCOPED_DESIGN_APPROVAL"
|
||||
/>
|
||||
<StorySection title="Overview" description={CENTER_BOUNDARY}>
|
||||
{withRepresentation(() => <CenterTitleReferenceDemo />)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative "Accordion" Story puts each Radix mode on one page with real, clickable items. The same sections also exist
|
||||
// as Stories tagged `verification`, which the sidebar hides by default (see .storybook/main.ts). Modes use the Radix API
|
||||
@@ -126,12 +126,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Accordion"
|
||||
description="제목을 눌러 관련 내용을 펼치고 접는 목록입니다. 동작은 Radix API(type, collapsible, defaultValue, disabled)로 정하며 HM variant prop은 없습니다."
|
||||
description="동작은 Radix API · HM variant prop 없음"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본 Accordion입니다.">{board("single")}</StorySection>
|
||||
<StorySection title="Variants" description="모두 접을 수 있는 single과, 여러 항목을 동시에 여는 multiple입니다.">
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Variants">
|
||||
{board("collapsible-single", "multiple")}
|
||||
</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Alert Dialog is a modal Radix overlay (Portal) for a decision that must be confirmed or cancelled: no Close (X), no outside
|
||||
// dismiss. The representative Story opens the real dialogs; the same cases start open in the verification Stories.
|
||||
@@ -74,14 +74,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Alert Dialog"
|
||||
description="반드시 확인하거나 취소해야 하는 결정을 묻는 모달입니다. 닫기(X)와 바깥 클릭 닫기가 없습니다."
|
||||
description="닫기(X)·바깥 클릭 닫기 없음"
|
||||
/>
|
||||
<StorySection title="Overview" description="일반 확인창은 같은 크기의 Solid 버튼 세트입니다. 버튼을 눌러 엽니다.">
|
||||
<StorySection title="Overview" description="같은 크기 Solid 버튼 세트">
|
||||
<Confirmation />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="삭제 확인창은 취소와 삭제 모두 같은 크기의 Standard Solid 세트입니다.">
|
||||
<StorySection title="Variants" description="취소·삭제 같은 크기 Standard Solid">
|
||||
<Destructive />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { InfoIcon, TriangleAlertIcon } from "lucide-react"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
const meta = {
|
||||
title: "Components/Feedback/Alert",
|
||||
@@ -27,9 +27,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Alert"
|
||||
description="화면 안에 놓이는 인라인 메시지입니다(Toast 아님). variant는 default(neutral)와 destructive만 두며 닫기 동작은 없습니다."
|
||||
description="인라인 메시지(Toast 아님) · default / destructive만 · 닫기 없음"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
@@ -67,7 +66,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="아이콘 없음, 제목만, 긴 내용 구성입니다.">
|
||||
<StorySection title="Usage">
|
||||
<StateBoard
|
||||
min={320}
|
||||
states={[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { AspectRatio, Card, CardContent } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// AspectRatio is a layout primitive: it keeps the ratio and nothing else. The image styling (object-fit, radius) in these Stories is
|
||||
// the consumer's composition on the child, not part of AspectRatio. Images are inline data URIs (no network). The Card example only
|
||||
@@ -68,13 +68,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Aspect Ratio"
|
||||
description="콘텐츠를 지정한 가로세로 비율로 유지하는 컨테이너입니다. 폭이 바뀌어도 비율대로 높이가 정해집니다."
|
||||
description="폭에 따라 비율대로 높이 결정"
|
||||
/>
|
||||
<StorySection title="Overview" description="ratio = 16 / 9.">{board("r169")}</StorySection>
|
||||
<StorySection title="Variants" description="ratio = 4 / 3, 1과 영상 자리 표시입니다.">{board("r43", "r11", "video")}</StorySection>
|
||||
<StorySection title="Usage" description="Card 안의 이미지 비율 조합입니다.">{board("card")}</StorySection>
|
||||
<StorySection title="Variants" description="ratio 4 / 3 · 1 · 영상 자리">{board("r43", "r11", "video")}</StorySection>
|
||||
<StorySection title="Usage">{board("card")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Images are inline data URIs so the Story does not depend on the network. "Broken" uses an address that cannot load so the
|
||||
// Radix fallback shows. Radix renders the image only after it loads.
|
||||
@@ -86,13 +86,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Avatar"
|
||||
description="사용자를 나타내는 원형 이미지입니다. 이미지를 불러오지 못하면 fallback 글자를 표시합니다."
|
||||
description="이미지 실패 시 fallback 글자"
|
||||
/>
|
||||
<StorySection title="Overview">{board("image")}</StorySection>
|
||||
<StorySection title="States">{board("fallback")}</StorySection>
|
||||
<StorySection title="Usage" description="이름·소속과 함께 쓰는 소비 조합입니다.">{board("composition")}</StorySection>
|
||||
<StorySection title="Usage">{board("composition")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { Badge } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
const variants = ["default", "secondary", "destructive", "outline"] as const
|
||||
|
||||
@@ -29,11 +29,10 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Badge"
|
||||
description="상태나 분류를 짧게 표시하는 라벨입니다. Button variant를 재사용하지 않는 별도 컴포넌트입니다."
|
||||
description="Button variant와 별도인 컴포넌트"
|
||||
/>
|
||||
<StorySection title="Overview" description="variant를 생략한 기본 Badge(primary)입니다.">
|
||||
<StorySection title="Overview" description="기본값: primary">
|
||||
<Badge>Badge</Badge>
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="default(primary) / secondary(neutral) / destructive / outline.">
|
||||
@@ -70,7 +69,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="긴 텍스트와 링크(asChild)입니다.">
|
||||
<StorySection title="Usage">
|
||||
<StateBoard
|
||||
min={280}
|
||||
states={[
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
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
|
||||
// as Stories tagged `verification` (hidden from the sidebar). BreadcrumbEllipsis is presentational: the Dropdown Menu example is
|
||||
@@ -133,12 +133,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Breadcrumb"
|
||||
description="현재 페이지까지의 상위 경로를 보여주는 내비게이션입니다. 마지막 항목은 현재 페이지입니다."
|
||||
description="마지막 항목 = 현재 페이지"
|
||||
/>
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants" description="중간 경로 생략(Ellipsis)과 사용자 링크 · 구분자입니다.">{board("ellipsis", "custom")}</StorySection>
|
||||
<StorySection title="Variants" description="Ellipsis · 사용자 링크 · 구분자">{board("ellipsis", "custom")}</StorySection>
|
||||
<StorySection title="Usage">{board("long")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DownloadIcon } from "lucide-react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, type ButtonAppearance } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Story structure follows ui_viz (implementations/web/storybook/Button.stories.jsx):
|
||||
// one representative "Button" Story shows kinds, sizes and states on a single screen; the same sections also exist
|
||||
@@ -222,21 +222,17 @@ export const All: Story = {
|
||||
parameters: { layout: "fullscreen" },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Button"
|
||||
description="사용자가 동작을 실행하는 기본 컨트롤입니다. variant는 역할(색), appearance는 Brand / Standard Solid / Standard Outline, size는 높이를 정합니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="props를 생략한 기본 Button: Brand · Primary(Green) · Medium 34px.">
|
||||
<StoryPageHeader title="Button" description="variant = 역할(색) · appearance = Brand / Standard Solid / Standard Outline · size = 높이" />
|
||||
<StorySection title="Overview" description="기본값: Brand · Primary · Medium 34px">
|
||||
<Button>Button</Button>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="Small 28 / Medium 34 / Large 42px · 글자 12/18 · 13/20 · 15/22. 위는 Brand, 아래는 Standard Solid입니다.">
|
||||
<StorySection title="Sizes" description="28 / 34 / 42px · 위 Brand, 아래 Standard Solid">
|
||||
<SizesBoard />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="appearance별로 허용된 역할 조합입니다.">
|
||||
<StorySection title="Variants" description="appearance별 허용 역할 조합">
|
||||
<RolesBoard />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Default / Hover / Pressed / Disabled. Hover·Pressed는 Storybook이 고정한 상태입니다.">
|
||||
<StorySection title="States" description="Hover·Pressed는 Storybook 고정 상태">
|
||||
<StateBoard
|
||||
min={1200}
|
||||
states={[
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import type { DateRange } from "react-day-picker"
|
||||
import { Calendar } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative "Calendar" Story shows each mode with real, clickable calendars; the same specimens also exist as Stories
|
||||
// tagged `verification` (hidden from the sidebar). `today` is pinned to 2026-10-02 so the Story looks the same on any day; this
|
||||
@@ -70,13 +70,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Calendar"
|
||||
description="날짜를 고르는 달력입니다. 기본 locale은 한국어이며, 이 화면의 오늘은 2026-10-02로 고정되어 있습니다."
|
||||
description="기본 locale 한국어 · 오늘은 2026-10-02로 고정"
|
||||
/>
|
||||
<StorySection title="Overview" description='mode="single"'>{board("single")}</StorySection>
|
||||
<StorySection title="Variants" description='mode="range"와 여러 달 표시(numberOfMonths=2)입니다.'>{board("range", "months")}</StorySection>
|
||||
<StorySection title="States" description="선택할 수 없는 날짜(disabled)입니다.">{board("disabled")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@hm-design/ui"
|
||||
import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardSurface } from "@hm-design/ui"
|
||||
import { Input } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Story structure follows Button / Input / Select: one representative "Card" Story shows the ui_viz cases on one page,
|
||||
// the same specimens also exist as separate Stories tagged `verification` (hidden from the sidebar, see .storybook/main.ts).
|
||||
@@ -71,14 +71,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Card"
|
||||
description="제목과 본문을 하나의 면으로 묶는 콘텐츠 컨테이너입니다(ui_viz Content Card). Default 상태만 정의되어 있습니다."
|
||||
description="Default 상태만 정의"
|
||||
/>
|
||||
<StorySection title="Overview" description="surface를 생략한 기본 Card(White)입니다.">
|
||||
<StorySection title="Overview" description="기본값: White">
|
||||
<StateBoard min={360} states={[{ id: "surface", label: "Surface · White (기본)", children: <Specimen /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="surface: content(Beige)와 제목 옆 Action입니다.">
|
||||
<StorySection title="Variants" description="content(Beige) · 제목 옆 Action">
|
||||
<StateBoard
|
||||
min={360}
|
||||
states={[
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { AspectRatio, Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative Story shows real, draggable carousels; the same specimens also exist as Stories tagged `verification`
|
||||
// (hidden from the sidebar). Slide widths (`basis-*`) are the consumer's `className` on CarouselItem; Card and AspectRatio are
|
||||
@@ -152,13 +152,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Carousel"
|
||||
description="여러 슬라이드를 끌기·버튼으로 넘겨 보는 영역입니다. 슬라이드 폭(basis-*)은 CarouselItem의 className으로 정합니다."
|
||||
description="슬라이드 폭(basis-*)은 CarouselItem className"
|
||||
/>
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants" description="여러 항목 표시, 반복(loop), 세로 방향입니다.">{board("multiple", "loop", "vertical")}</StorySection>
|
||||
<StorySection title="Usage" description="AspectRatio와 조합한 이미지 슬라이드입니다.">{board("media")}</StorySection>
|
||||
<StorySection title="Variants" description="여러 항목 · loop · 세로">{board("multiple", "loop", "vertical")}</StorySection>
|
||||
<StorySection title="Usage">{board("media")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Checkbox, Label } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Current HM Checkbox. ui-viz has no Checkbox spec, so it was reference only.
|
||||
// The option label is the HM Label; its foreground role for Checkbox options is still TBD in ui_viz.
|
||||
@@ -48,9 +48,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Checkbox"
|
||||
description="항목을 선택하거나 해제하는 컨트롤입니다. 18px 박스이며 Checked·Indeterminate는 state-active 색입니다."
|
||||
description="18px 박스 · Checked·Indeterminate는 state-active 색"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
@@ -82,7 +81,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시합니다.">
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리">
|
||||
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Option label="Option">{(id) => <Checkbox id={id} aria-invalid />}</Option> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Usage">
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative "Collapsible" Story shows one independent disclosure per state with real, clickable triggers. The same
|
||||
// sections also exist as Stories tagged `verification` (hidden from the sidebar). Collapsible is a single disclosure: no divider
|
||||
@@ -81,13 +81,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Collapsible"
|
||||
description="하나의 영역을 펼치고 접는 단일 disclosure입니다. 구분선 체계와 애니메이션은 없습니다."
|
||||
description="구분선·애니메이션 없음"
|
||||
/>
|
||||
<StorySection title="Overview">{board("closed")}</StorySection>
|
||||
<StorySection title="States">{board("open", "disabled")}</StorySection>
|
||||
<StorySection title="Usage" description="긴 콘텐츠와, Button을 trigger로 쓰는 제어형 조합입니다.">{board("long", "controlled")}</StorySection>
|
||||
<StorySection title="Usage">{board("long", "controlled")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Combobox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, type ComboboxOption } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Combobox is a Popover + Command composition (search + single choice); Select stays for short lists without search.
|
||||
// The representative Story shows real comboboxes; the same specimens also exist as Stories tagged `verification` (hidden from the
|
||||
@@ -122,12 +122,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Combobox"
|
||||
description="검색해서 하나를 고르는 Popover + Command 조합입니다. 검색이 필요 없는 짧은 목록은 Select를 씁니다."
|
||||
description="Popover + Command · 짧은 목록은 Select 사용"
|
||||
/>
|
||||
<StorySection title="Overview" description="Trigger는 dialog 팝업을 열고, 내부 검색 입력이 listbox를 제어합니다. Enter/Space로 열기, 방향키·Enter로 선택, Escape로 닫기 후 Trigger로 focus가 복귀합니다.">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="size를 생략하면 Large입니다. 같은 size의 Select와 나란히 비교합니다.">
|
||||
<StorySection title="Overview" description="Escape로 닫으면 Trigger로 focus 복귀">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="기본값: Large · 같은 size의 Select와 비교">
|
||||
<StateBoard
|
||||
min={320}
|
||||
states={[
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CalendarIcon, SettingsIcon, UserIcon } from "lucide-react"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative "Command" Story shows real, typeable lists; the same specimens also exist as Stories tagged `verification`
|
||||
// (hidden from the sidebar). Filtering is cmdk's. The bordered box is Story composition (Command draws no border of its own).
|
||||
@@ -100,9 +100,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Command"
|
||||
description="입력한 검색어로 항목을 걸러 고르는 목록입니다. Combobox와 명령 팔레트의 기반입니다."
|
||||
description="Combobox와 명령 팔레트의 기반"
|
||||
/>
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="Variants">{board("grouped")}</StorySection>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Context Menu opens from pointer context (right click, the context-menu key / Shift+F10, long press) on its trigger area.
|
||||
// The representative Story is a target area: right click it. Radix owns keyboard navigation, focus return and Escape.
|
||||
@@ -83,11 +83,10 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Context Menu"
|
||||
description="영역에서 오른쪽 클릭, 컨텍스트 메뉴 키 / Shift+F10, 길게 누르기로 여는 메뉴입니다."
|
||||
description="오른쪽 클릭 · Shift+F10 · 길게 누르기로 열기"
|
||||
/>
|
||||
<StorySection title="Overview" description="항목 · Disabled · 단축키 · Checkbox · Radio Group · 하위 메뉴 · 위험 항목을 한 메뉴에서 확인합니다. 점선 영역에서 오른쪽 클릭합니다.">
|
||||
<StorySection title="Overview" description="점선 영역에서 오른쪽 클릭">
|
||||
<Full />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { DatePicker } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// DatePicker is a Popover + Calendar composition packaged as a component (value handling, trigger text, close on select).
|
||||
// The representative Story shows real pickers; the same specimens also exist as Stories tagged `verification` (hidden from the
|
||||
@@ -116,14 +116,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Date Picker"
|
||||
description="Trigger를 눌러 달력에서 날짜를 고르는 Popover + Calendar 조합 컴포넌트입니다. 날짜를 고르면 닫힙니다."
|
||||
description="Popover + Calendar · 날짜 선택 시 닫힘"
|
||||
/>
|
||||
<StorySection title="Overview">{board("empty")}</StorySection>
|
||||
<StorySection title="Sizes" description="size를 생략하면 Large입니다.">{board("medium")}</StorySection>
|
||||
<StorySection title="Sizes" description="기본값: Large">{board("medium")}</StorySection>
|
||||
<StorySection title="States">{board("selected", "controlled", "disabled-dates", "disabled")}</StorySection>
|
||||
<StorySection title="Usage" description="기본 표시 형식과 사용자 형식 지정입니다.">{board("format")}</StorySection>
|
||||
<StorySection title="Usage">{board("format")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@hm-design/ui"
|
||||
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@hm-design/ui"
|
||||
import { Input } from "@hm-design/ui"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Dialog is an overlay (Radix Portal), so it does not fit the StateBoard. The representative Story is a row of triggers that open
|
||||
// the real Dialog for each ui_viz case (dialog.md §9 Reference Viewer examples); the same cases also exist as separate Stories tagged
|
||||
@@ -182,21 +182,19 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Dialog"
|
||||
description="현재 화면 위에 내용을 띄워 확인·입력을 받는 모달입니다. 각 버튼을 눌러 실제 Dialog를 엽니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="짧은 내용의 Dialog는 내용 기준 크기입니다.">
|
||||
<StorySection title="Overview" description="내용 기준 크기">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="내용이 길거나 넓으면 Body 안에서만 scroll 합니다. 본문 스크롤은 공통 Header 70px / Footer 48px를 뺀 높이를 씁니다.">
|
||||
<StorySection title="Variants" description="긴 내용은 Body만 scroll · Header 70 / Footer 48px 제외">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<LongContent />
|
||||
<WideContent />
|
||||
<BodyScroll />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="일반 확인과 삭제 확인 모두 취소·실행을 Standard Solid 세트로 구성합니다.">
|
||||
<StorySection title="Usage" description="취소·실행은 Standard Solid 세트">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<DeleteConfirmation />
|
||||
<FormContent />
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
// Usage: preserved native label fixtures do not define a new-screen exception. Use public Label
|
||||
// for supported fields (docs/components/label.md §5); copy only actions requested by the screen.
|
||||
import { Button, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, Input } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Drawer is a modal vaul overlay (Portal). The representative Story opens the real Drawer from its triggers; drag the handle or
|
||||
// the panel down to dismiss. The same cases start open in the verification Stories. vaul owns the drag, Radix the focus and ARIA.
|
||||
@@ -101,14 +101,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Drawer"
|
||||
description="화면 아래에서 올라오는 모달 패널입니다. 핸들이나 패널을 아래로 끌어 닫습니다."
|
||||
description="핸들·패널을 아래로 끌어 닫기"
|
||||
/>
|
||||
<StorySection title="Overview" description="데스크톱: 하단 우측 가로 취소 → 실행. 모바일(640px 이하): 전체 폭 세로 실행 위 / 취소 아래. 같은 Drawer를 화면 폭에 맞춰 확인합니다.">
|
||||
<StorySection title="Overview" description="데스크톱: 우측 가로 취소 → 실행 · 640px 이하: 세로 실행 위 / 취소 아래">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="폼 입력 조합과, 내용이 길 때 Body만 scroll 하는 경우입니다.">
|
||||
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<FormCase />
|
||||
<LongCase />
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Dropdown Menu is a Radix overlay (Portal), so the representative Story is a trigger that opens the real menu, the same way as
|
||||
// Dialog. The verification Story starts open. Radix owns keyboard navigation, focus return and Escape.
|
||||
@@ -85,11 +85,9 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Dropdown Menu"
|
||||
description="버튼으로 여는 작업 메뉴입니다. 키보드 이동, Esc 닫기, 포커스 복귀는 Radix가 담당합니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="항목 · Disabled · 구분선 · Checkbox · Radio Group · 하위 메뉴 · 위험 항목을 한 메뉴에서 확인합니다. 버튼을 눌러 엽니다.">
|
||||
<StorySection title="Overview" description="항목 · Disabled · 구분선 · Checkbox · Radio · 하위 메뉴 · 위험 항목">
|
||||
<Full />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Hover Card is a Radix overlay (Portal) for a richer preview than Tooltip. The representative Story shows real triggers
|
||||
// (hover or focus to open); the verification Story starts one open. Radix owns the open / close delay and the hover bridge.
|
||||
@@ -54,14 +54,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Hover Card"
|
||||
description="링크에 포인터를 올리거나 포커스하면 Tooltip보다 풍부한 미리보기를 띄우는 overlay입니다."
|
||||
description="링크 hover / focus 시 미리보기"
|
||||
/>
|
||||
<StorySection title="Overview" description="링크 위에 포인터를 올려 미리보기를 엽니다.">
|
||||
<StorySection title="Overview">
|
||||
<Preview />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="긴 콘텐츠는 카드 폭에서 줄바꿈되고 높이만 늘어납니다.">
|
||||
<StorySection title="Usage" description="카드 폭 고정 · 높이만 늘어남">
|
||||
<LongContent />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, type ComponentProps, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Input, type InputSize } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Story structure follows ui_viz (implementations/web/storybook/Input.stories.jsx and InputSizes.stories.jsx):
|
||||
// one representative "Input" Story shows the sizes and every state on one page; the same specimens also exist as
|
||||
@@ -126,17 +126,16 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Input"
|
||||
description="한 줄 텍스트를 입력하는 컨트롤입니다. Label·Helper·Error 문구는 Input이 아니라 소비 조합입니다."
|
||||
description="Label·Helper·Error 문구는 소비 조합"
|
||||
/>
|
||||
<StorySection title="Overview" description="size를 생략한 기본 Input: Large 42px.">
|
||||
<StorySection title="Overview" description="기본값: Large 42px">
|
||||
<StateBoard states={[{ id: "default", label: "Default · size 생략(Large)", children: <Specimen placeholder={placeholder} /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="Small 28 / Medium 34 / Large 42px. 업무 폼·조회 조합은 Medium을 씁니다.">
|
||||
<StorySection title="Sizes" description="28 / 34 / 42px · 업무 폼·조회는 Medium">
|
||||
<SizeComparison />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Focus와 Error는 1.5px 테두리로 구분하며 Hover는 없습니다. Disabled는 흰 면을 유지하고 컨트롤과 소비 Label 각각 60%로 표시합니다. Readonly Label은 100%입니다.">
|
||||
<StorySection title="States" description="Focus·Error 1.5px 테두리 · Hover 없음 · Disabled 60%">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "focus", label: "Focus", children: <Specimen size="medium" placeholder={placeholder} /> },
|
||||
@@ -145,7 +144,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시하고, 오류 문구는 소비 조합으로 연결합니다.">
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "error", label: "Error", children: <Specimen size="medium" error defaultValue="Invalid value" /> },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Input, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Label has no standalone ui_viz spec. Its typography and color are the Field Label role owned by Input / Textarea
|
||||
// (14/20/500, color.fg.secondary). Required star and messages are composed here as a consumer would (not part of Label).
|
||||
@@ -28,9 +28,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Label"
|
||||
description="Label은 항목명이고 Input은 입력칸입니다. Field Label 14/20/500을 쓰며, htmlFor로 연결하면 라벨 클릭 시 해당 Input이나 Select로 초점이 이동합니다."
|
||||
description="Field Label 14/20/500 · htmlFor 연결 시 클릭으로 focus 이동"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
@@ -70,7 +69,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="Required 별표는 Label이 아닌 소비 조합입니다. 긴 라벨은 줄바꿈됩니다.">
|
||||
<StorySection title="Usage" description="Required 별표는 소비 조합">
|
||||
<div className="mb-4 grid max-w-sm gap-2">
|
||||
<Label htmlFor="label-select">부서 선택</Label>
|
||||
<Select><SelectTrigger id="label-select" size="medium"><SelectValue placeholder="부서를 선택하세요" /></SelectTrigger>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Menubar is a desktop menu bar (Left / Right between top menus). The representative Story is the real bar and opens the real
|
||||
// menus; Radix owns the keyboard model and focus. Hidden verification Stories isolate trigger focus and open + focus.
|
||||
@@ -97,11 +97,10 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Menubar"
|
||||
description="데스크톱 앱 형태의 상단 메뉴 막대입니다. 메뉴가 열린 상태에서 좌우 방향키로 이웃 메뉴로 이동합니다."
|
||||
description="열린 상태에서 좌우 방향키로 이웃 메뉴 이동"
|
||||
/>
|
||||
<StorySection title="Overview" description="파일(단축키 · Disabled · 하위 메뉴 · 위험 항목), 편집, 보기(Label · Checkbox · Radio)입니다. 메뉴를 눌러 엽니다.">
|
||||
<StorySection title="Overview" description="단축키 · Disabled · 하위 메뉴 · 위험 항목 · Checkbox · Radio">
|
||||
<div style={{ minHeight: 384 }}>
|
||||
<Specimen />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
NavigationMenuTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Navigation Menu is for destinations (links), not commands. The representative Story opens the real menus with hover, click or
|
||||
// keyboard; the same specimens also exist as Stories tagged `verification` (hidden from the sidebar). Title + description children
|
||||
@@ -102,9 +102,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Navigation Menu"
|
||||
description="목적지(링크)로 이동하는 메뉴입니다. 명령 실행에는 쓰지 않습니다. hover, 클릭 또는 키보드로 엽니다."
|
||||
description="링크 이동용 · 명령 실행에는 쓰지 않음"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PaginationPrevious,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Pagination is presentational: the consumer owns the page and the hrefs. The Story keeps the page in local state and uses
|
||||
// `#page-N` hrefs with preventDefault. The representative Story shows each case on one page; the same specimens also exist as
|
||||
@@ -89,13 +89,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Pagination"
|
||||
description="여러 페이지로 나뉜 목록을 이동하는 내비게이션입니다. 현재 페이지와 href는 소비 코드가 소유합니다."
|
||||
description="현재 페이지와 href는 소비 코드가 소유"
|
||||
/>
|
||||
<StorySection title="Overview">{board("middle")}</StorySection>
|
||||
<StorySection title="States" description="첫 페이지와 마지막 페이지에서는 이전·다음이 aria-disabled입니다.">{board("first", "last")}</StorySection>
|
||||
<StorySection title="Usage" description="페이지가 많으면 생략 부호로 줄입니다.">{board("ellipsis")}</StorySection>
|
||||
<StorySection title="States" description="처음·끝에서 이전·다음 aria-disabled">{board("first", "last")}</StorySection>
|
||||
<StorySection title="Usage">{board("ellipsis")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, Input, Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Popover is a non-modal Radix overlay (Portal). The representative Story opens the real panels from their triggers;
|
||||
// the verification Story starts one open. Radix owns outside click / Escape close and focus return.
|
||||
@@ -84,17 +84,16 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Popover"
|
||||
description="Trigger 옆에 상호작용 가능한 패널을 띄우는 비모달 overlay입니다. 바깥 클릭이나 Esc로 닫힙니다."
|
||||
description="비모달 · 바깥 클릭이나 Esc로 닫힘"
|
||||
/>
|
||||
<StorySection title="Overview" description="버튼을 눌러 안내 패널을 엽니다.">
|
||||
<StorySection title="Overview">
|
||||
<Info />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="align: start / center / end.">
|
||||
<StorySection title="Variants">
|
||||
<Aligned />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="패널 안의 입력 폼 조합입니다. 취소·저장은 PopoverClose로 닫습니다.">
|
||||
<StorySection title="Usage" description="취소·저장은 PopoverClose로 닫음">
|
||||
<FormPanel />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, Progress } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Progress follows `value` (0..max). The representative Story shows fixed values plus one interactive value; the same specimens
|
||||
// also exist as Stories tagged `verification` (hidden from the sidebar). No status colors are defined, so there is one look.
|
||||
@@ -59,13 +59,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Progress"
|
||||
description="작업 진행률을 막대로 표시합니다. value(0..max)를 따라 채워집니다."
|
||||
description="value(0..max) 비율로 채움"
|
||||
/>
|
||||
<StorySection title="Overview">{board("middle")}</StorySection>
|
||||
<StorySection title="States" description="value 0과 100입니다.">{board("zero", "full")}</StorySection>
|
||||
<StorySection title="Usage" description="max를 바꾼 범위와, value를 바꾸는 실제 조작입니다.">{board("custom-maximum", "interactive")}</StorySection>
|
||||
<StorySection title="States">{board("zero", "full")}</StorySection>
|
||||
<StorySection title="Usage">{board("custom-maximum", "interactive")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Label, RadioGroup, RadioGroupItem } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Current HM Radio Group. ui-viz has no Radio spec, so it was reference only.
|
||||
|
||||
@@ -54,9 +54,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Radio Group"
|
||||
description="여러 선택지 중 하나를 고르는 컨트롤입니다. 그룹 안에서는 방향키로 이동·선택합니다(Radix roving focus)."
|
||||
description="그룹 안에서 방향키로 이동·선택"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard min={240} states={[{ id: "vertical", label: "Vertical (default) · Checked / Unchecked", children: <Group prefix="v" defaultValue="b" /> }]} />
|
||||
@@ -75,7 +74,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시합니다.">
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리">
|
||||
<StateBoard min={240} states={[{ id: "error", label: "Error", children: <Group prefix="e" invalid /> }]} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative Story shows real, draggable layouts; the same specimens also exist as Stories tagged `verification` (hidden
|
||||
// from the sidebar). react-resizable-panels v4 sizes: a string without a unit ("30") or with "%" is a percentage, a number is
|
||||
@@ -110,13 +110,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Resizable"
|
||||
description="경계선을 끌어 인접한 패널의 크기를 조절하는 레이아웃입니다."
|
||||
/>
|
||||
<StorySection title="Overview">{board("horizontal")}</StorySection>
|
||||
<StorySection title="Variants" description="세로 방향과 grip 표시입니다.">{board("vertical", "handle")}</StorySection>
|
||||
<StorySection title="Usage" description="최소·최대 크기와 접을 수 있는 패널입니다.">{board("minmax", "collapsible")}</StorySection>
|
||||
<StorySection title="Variants">{board("vertical", "handle")}</StorySection>
|
||||
<StorySection title="Usage" description="최소·최대 크기 · 접기">{board("minmax", "collapsible")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { ScrollArea, ScrollBar } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Scroll Area is Radix scrolling with a styled scrollbar (type "hover": the bar shows while hovering or scrolling). Sizes below
|
||||
// are Story layout. The representative Story shows vertical, horizontal, both axes and a table-like content.
|
||||
@@ -88,12 +88,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Scroll Area"
|
||||
description="정해진 크기 안에서 내용을 스크롤하는 영역입니다. 스크롤바는 포인터를 올리거나 스크롤할 때 나타납니다."
|
||||
description="스크롤바는 hover·스크롤 시 표시"
|
||||
/>
|
||||
<StorySection title="Overview">{board("vertical")}</StorySection>
|
||||
<StorySection title="Variants" description="가로와 양방향 스크롤입니다.">{board("horizontal", "both")}</StorySection>
|
||||
<StorySection title="Variants">{board("horizontal", "both")}</StorySection>
|
||||
<StorySection title="Usage">{board("table-like")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, type SelectSize } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Story structure follows ui_viz (implementations/web/storybook/Select.stories.jsx and SelectSizes.stories.jsx):
|
||||
// one representative "Select" Story shows the default, selected, disabled and open states on one page; the same specimens
|
||||
@@ -151,12 +151,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Select"
|
||||
description="목록에서 하나의 값을 고르는 컨트롤입니다. Option은 Trigger의 size를 따르고 목록 폭은 Trigger와 같습니다."
|
||||
description="Option은 Trigger size를 따르고 목록 폭 = Trigger 폭"
|
||||
/>
|
||||
{/* Open stays in the first section: the list opens on load, so its trigger must be on the first screen. */}
|
||||
<StorySection title="Overview" description="size를 생략한 기본 Select(Large 44px)와 열린 목록입니다. 목록은 Escape 또는 바깥 클릭으로 닫히고, Trigger를 클릭하면 다시 열립니다.">
|
||||
<StorySection title="Overview" description="기본값: Large 44px · 열린 목록">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
@@ -173,10 +172,10 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Sizes" description="Medium 34 / Large 44px. Input과 독립된 두 크기입니다.">
|
||||
<StorySection title="Sizes" description="34 / 44px · Input과 별도 크기">
|
||||
<SizeComparison />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="값이 선택된 상태와 Disabled입니다.">
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
@@ -185,7 +184,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Invalid / Focus" description="aria-invalid는 Error 테두리로 표시합니다. Tab으로 Trigger에 포커스해 Invalid + Focus를 확인합니다.">
|
||||
<StorySection title="Invalid / Focus" description="aria-invalid → Error 테두리">
|
||||
<StateBoard
|
||||
fixed
|
||||
states={[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Separator } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
const meta = {
|
||||
title: "Components/Data & Display/Separator",
|
||||
@@ -26,9 +26,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Separator"
|
||||
description="콘텐츠 사이를 나누는 선입니다. 제목과 설명을 하나의 묶음으로 두고 구분선은 묶음 뒤 한 곳에 사용합니다. 기본은 장식용(decorative)입니다."
|
||||
description="구분선은 묶음 뒤 한 곳 · 기본 decorative"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
// Usage: preserved native label fixtures do not define a new-screen exception. Use public Label
|
||||
// for supported fields (docs/components/label.md §5); copy only actions requested by the screen.
|
||||
import { Button, Input, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Sheet is a modal Radix overlay (Portal), so the representative Story is a row of triggers that open the real Sheet for each
|
||||
// side and content case. The same cases start open in the verification Stories. Radix owns focus trap, Escape and focus return.
|
||||
@@ -99,21 +99,19 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Sheet"
|
||||
description="화면 가장자리에서 열리는 보조 모달 패널입니다. 각 버튼을 눌러 실제 Sheet를 엽니다."
|
||||
/>
|
||||
<StorySection title="Overview" description="side를 생략한 기본 방향(right)입니다.">
|
||||
<StorySection title="Overview" description="기본값: right">
|
||||
<SideCase side="right" />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="side: left / top / bottom.">
|
||||
<StorySection title="Variants">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<SideCase side="left" />
|
||||
<SideCase side="top" />
|
||||
<SideCase side="bottom" />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="폼 입력 조합과, 내용이 길 때 Body만 scroll 하는 경우입니다.">
|
||||
<StorySection title="Usage" description="긴 내용은 Body만 scroll">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<FormCase />
|
||||
<LongCase />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Card, CardContent, CardHeader, Skeleton } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Skeleton is a visual placeholder; size and shape come from `className` (layout classes here are Story composition).
|
||||
// There is no data fetching. The representative Story shows text lines, an avatar, and a card / list loading composition.
|
||||
@@ -64,12 +64,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Skeleton"
|
||||
description="콘텐츠를 불러오는 동안 자리를 표시하는 placeholder입니다. 크기와 모양은 className으로 정합니다."
|
||||
description="크기·모양은 className으로 지정"
|
||||
/>
|
||||
<StorySection title="Overview">{board("text")}</StorySection>
|
||||
<StorySection title="Usage" description="Avatar 행과 카드·목록 로딩 조합입니다.">{board("avatar", "card")}</StorySection>
|
||||
<StorySection title="Usage">{board("avatar", "card")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Slider } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// The representative Story shows each Slider mode with real, draggable controls; the same specimens also exist as Stories tagged
|
||||
// `verification` (hidden from the sidebar). The value label is the Story's composition (Slider has no built-in label).
|
||||
@@ -82,12 +82,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Slider"
|
||||
description="막대 위의 thumb을 끌어 범위 안의 값을 고르는 컨트롤입니다. 화살표 키로도 값을 바꿉니다."
|
||||
description="끌기 또는 화살표 키로 값 변경"
|
||||
/>
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Variants" description="두 thumb의 범위, step, 세로 방향입니다.">{board("range", "step", "vertical")}</StorySection>
|
||||
<StorySection title="Variants">{board("range", "step", "vertical")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, toast, Toaster } from "@hm-design/ui"
|
||||
import { StateBoard } from "./story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "./story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "./story-page"
|
||||
|
||||
// Toast is created with the real Sonner API (`toast`, re-exported by @hm-design/ui); nothing is a static specimen.
|
||||
// One <Toaster /> is mounted in .storybook/preview.tsx for every Story (an app mounts it once at its root).
|
||||
@@ -125,14 +125,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Sonner"
|
||||
description="작업 결과를 잠시 띄우는 Toast 알림입니다. 실제 Sonner API(toast)로 만들며, Toaster는 앱 루트에 한 번 둡니다."
|
||||
description="Toaster는 앱 루트에 한 번"
|
||||
/>
|
||||
<StorySection title="Variants" description="버튼을 눌러 각 유형의 Toast를 띄웁니다.">
|
||||
<StorySection title="Variants">
|
||||
<Board ids={["types"]} />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="Action / Cancel / Close, Promise, 여러 개 쌓기와 긴 메시지입니다.">
|
||||
<StorySection title="Usage">
|
||||
<Board ids={["action", "promise", "stack"]} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -13,10 +13,12 @@ export function StoryPage({ className, children }: { className?: string; childre
|
||||
return <div className={className ? `sb-page ${className}` : "sb-page"}><div className="sb-page-inner">{children}</div></div>
|
||||
}
|
||||
|
||||
export function StoryPageHeader({ kicker, title, description }: { kicker: string; title: string; description?: ReactNode }) {
|
||||
// Representative (sidebar) Stories omit `kicker`: the sidebar path already names the category.
|
||||
// Hidden verification Stories keep passing it so their page structure stays unchanged.
|
||||
export function StoryPageHeader({ kicker, title, description }: { kicker?: string; title: string; description?: ReactNode }) {
|
||||
return (
|
||||
<header className="sb-page-header">
|
||||
<p className="sb-page-kicker">{kicker}</p>
|
||||
{kicker ? <p className="sb-page-kicker">{kicker}</p> : null}
|
||||
<h1 className="sb-page-title">{title}</h1>
|
||||
{description ? <p className="sb-page-description">{description}</p> : null}
|
||||
</header>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Label, Switch } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
function Option({ label, children }: { label: string; children: (id: string) => ReactNode }) {
|
||||
const id = useId()
|
||||
@@ -41,9 +41,8 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Switch"
|
||||
description="설정을 즉시 켜고 끄는 컨트롤입니다. 트랙 36×20, 썸 16이며 On은 state-active 색입니다."
|
||||
description="트랙 36×20 · 썸 16 · On은 state-active 색"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Badge, Checkbox, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Table is a static display primitive, so the representative Story is a board of specimens. Checkbox and Badge appear only as
|
||||
// cell content (Story composition); Table has no dependency on them and no selection, sorting or paging behavior.
|
||||
@@ -153,13 +153,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Table"
|
||||
description="행과 열로 데이터를 보여주는 정적 표시 primitive입니다. 정렬·선택·페이지 로직은 Data Table Pattern이 담당합니다."
|
||||
description="정적 표시 · 정렬·선택·페이지는 Data Table Pattern"
|
||||
/>
|
||||
<StorySection title="Overview">{board("basic")}</StorySection>
|
||||
<StorySection title="States">{board("selected")}</StorySection>
|
||||
<StorySection title="Usage" description="긴 텍스트와 가로 넘침입니다.">{board("long", "wide")}</StorySection>
|
||||
<StorySection title="Usage">{board("long", "wide")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Representative "Tabs" Story shows default Primary, explicit API comparisons and Secondary; Legacy stays verification-only.
|
||||
// D-01 applies to every Fill specimen: flat panels use the actual parent surface and padding 24px 0.
|
||||
@@ -147,14 +147,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Tabs"
|
||||
description="같은 영역의 콘텐츠를 전환하는 탭입니다. 선택된 탭은 하단으로 콘텐츠 면과 이어지며, 넘칠 때만 이전/다음 이동 버튼이 나타납니다."
|
||||
description="선택 탭은 콘텐츠 면과 이어짐 · 넘칠 때만 이전/다음 버튼"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary. 선택 면이 위 White alpha 0.8 → 아래 content panel색 gradient로 콘텐츠와 이어집니다. 탭 전체 opacity는 1이며 비선택 hover는 기존 Black 20% overlay입니다.", children: <Specimen /> },
|
||||
{ id: "primary", label: "Primary · 기본 TabsList", noteAfter: true, note: "fill / panelSurface 생략 → Primary · 비선택 Hover Black 20%", children: <Specimen /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
@@ -169,24 +168,24 @@ export const All: Story = {
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "secondary", label: "Secondary · Fill + panelSurface surface", noteAfter: true, note: "서브형. Surface(White) 위에서 사용하며 선택 면이 White로 이어집니다.", children: <Specimen variant="secondary" /> },
|
||||
{ id: "secondary", label: "Secondary · Fill + panelSurface surface", noteAfter: true, note: "서브형 · Surface(White) 위에서 사용", children: <Specimen variant="secondary" /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="States">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "overflow", wide: true, label: "Overflow · Primary + 이동 버튼", noteAfter: true, note: "일반 화면 폭에서 20개 탭의 넘침을 확인합니다. 이전/다음 버튼은 선택을 바꾸지 않고 목록만 이동합니다. 모바일에서도 같은 내부 스크롤을 유지합니다.", children: <div className="w-full min-w-0 max-w-4xl"><Specimen count={20} /></div> },
|
||||
{ id: "overflow", wide: true, label: "Overflow · Primary + 이동 버튼", noteAfter: true, note: "20개 탭 · 이전/다음은 선택을 바꾸지 않고 목록만 이동", children: <div className="w-full min-w-0 max-w-4xl"><Specimen count={20} /></div> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="실제 상위 표면에 panelSurface를 맞추고 TabsContent의 중복 박스를 제거합니다. px-0 py-6는 이 조합의 예시이며 공통 spacing 규칙이 아닙니다.">
|
||||
<StorySection title="Usage" description="panelSurface를 상위 표면에 맞춤 · px-0 py-6은 예시이며 공통 규칙 아님">
|
||||
<StateBoard states={[
|
||||
{ id: "usage-card", label: "Card 내부 · Secondary", noteAfter: true, note: "바깥 Card의 배경·테두리·기본 여백 24px은 유지합니다. 내부 패널은 배경 transparent·border 0·padding 24px 0이며 입력 요소의 테두리를 보존합니다.", children: <SurfaceUsage inCard /> },
|
||||
{ id: "usage-page", label: "페이지 직접 배치 · Primary", noteAfter: true, note: "기존 Layout의 content 배경을 사용하는 조합입니다. 추가 흰색 박스를 만들지 않습니다. 일정 탭의 Table은 고유 표면과 내부 스크롤을 유지하고 입력값은 탭 전환 후에도 보존됩니다.", children: <SurfaceUsage /> },
|
||||
{ id: "usage-card", label: "Card 내부 · Secondary", noteAfter: true, note: "바깥 Card 유지 · 내부 패널 transparent · border 0 · padding 24px 0", children: <SurfaceUsage inCard /> },
|
||||
{ id: "usage-page", label: "페이지 직접 배치 · Primary", noteAfter: true, note: "Layout content 배경 사용 · 추가 흰색 박스 없음", children: <SurfaceUsage /> },
|
||||
]} />
|
||||
</StorySection>
|
||||
<StorySection title="Do / Don't" description="Card 전용 Token을 화면 전체 간격에 사용하거나 모든 제목·설명 간격을 24px로 통일하지 않습니다.">
|
||||
<StorySection title="Do / Don't" description="Card Token을 화면 전체 간격에 쓰지 않음">
|
||||
<p className="text-sm text-muted-foreground">Do: Card 또는 Layout이 외곽 여백을 소유하고 내부 Input·Table·Alert의 고유 스타일을 유지합니다. Don't: 패널 박스 제거를 모든 border 제거로 해석하거나, 그라데이션 alpha 조정을 탭 전체 opacity 변경으로 구현하지 않습니다. 실제 패널 계산값·키보드 동작·모바일 넘침을 확인합니다.</p>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, type ComponentProps, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Textarea } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Story structure follows Input: one representative "Textarea" Story shows every state on one page; the same specimens
|
||||
// also exist as separate Stories tagged `verification`, hidden from the sidebar by default (see .storybook/main.ts).
|
||||
@@ -87,14 +87,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Textarea"
|
||||
description="여러 줄 텍스트를 입력하는 컨트롤입니다. 최소 높이 112px, resize 기본 vertical이며 size 체계는 없습니다. Label·Helper·Error 문구는 소비 조합입니다."
|
||||
description="최소 높이 112px · resize vertical · size 없음 · Label·Helper·Error는 소비 조합"
|
||||
/>
|
||||
<StorySection title="Overview">
|
||||
<StateBoard states={[{ id: "default", label: "Default", children: <Specimen placeholder={placeholder} /> }]} />
|
||||
</StorySection>
|
||||
<StorySection title="States" description="Focus는 1.5px 테두리로 구분하며 shadow·glow를 쓰지 않습니다. Hover는 Default와 같습니다.">
|
||||
<StorySection title="States" description="Focus 1.5px 테두리(shadow 없음) · Hover = Default">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "hover", label: "Hover (= Default)", note: hoverNote, noteAfter: true, children: <Specimen defaultValue="Hover over this field" /> },
|
||||
@@ -104,7 +103,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Validation" description="aria-invalid로 Error 테두리를 표시하고, 오류 문구는 소비 조합으로 연결합니다.">
|
||||
<StorySection title="Validation" description="aria-invalid → Error 테두리 · 오류 문구는 소비 조합">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "error", label: "Error", children: <Specimen error defaultValue="Invalid value" /> },
|
||||
@@ -112,7 +111,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="줄바꿈 유지와 resize 해제입니다.">
|
||||
<StorySection title="Usage">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "multiline", label: "Multiline · Wrap", children: <Specimen defaultValue={multiline} /> },
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
|
||||
import { ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Toggle Group is a group of pressed-state items (toolbar-style), not a content switcher (that is Tabs). The representative Story
|
||||
// shows single / multiple / icon-only / disabled / vertical / spaced groups with real controls; the same specimens also exist as
|
||||
@@ -137,15 +137,14 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Toggle Group"
|
||||
description="눌림 상태를 갖는 항목 묶음(툴바형)입니다. 내용을 전환하는 용도는 Tabs를 씁니다."
|
||||
description="툴바형 눌림 항목 묶음 · 내용 전환은 Tabs 사용"
|
||||
/>
|
||||
<StorySection title="Overview">{board("single")}</StorySection>
|
||||
<StorySection title="Sizes" description="Icon-only / Icon + Text / Text-only.">
|
||||
<StorySection title="Sizes">
|
||||
<Sizes />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="다중 선택, 세로 방향, 항목 간격(spacing)입니다.">{board("multiple", "vertical", "spaced")}</StorySection>
|
||||
<StorySection title="Variants">{board("multiple", "vertical", "spaced")}</StorySection>
|
||||
<StorySection title="States">{board("disabled")}</StorySection>
|
||||
<StorySection title="Usage">{board("many")}</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { BoldIcon, ItalicIcon, StarIcon } from "lucide-react"
|
||||
import { Toggle, ToggleGroup, ToggleGroupItem } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Toggle is a persistent pressed-state control. The representative Story shows each state with real, clickable toggles; the same
|
||||
// specimens also exist as Stories tagged `verification` (hidden from the sidebar). Icon-only toggles carry an `aria-label`.
|
||||
@@ -108,12 +108,11 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Toggle"
|
||||
description="Toggle과 Toggle Group은 눌림(On) 상태를 공유합니다. Default·Outline, Off·On·Disabled와 키보드 Focus를 같은 기준으로 비교합니다."
|
||||
description="Toggle Group과 같은 눌림(On) 상태 기준"
|
||||
/>
|
||||
<StorySection title="Overview">{board("offon")}</StorySection>
|
||||
<StorySection title="단독 · 연결 그룹" description="단독 Toggle은 독립 선택입니다. spacing=0 그룹은 연결 형태이며 single은 하나, multiple은 여러 항목을 선택합니다.">
|
||||
<StorySection title="단독 · 연결 그룹" description="단독 = 독립 선택 · spacing=0 그룹 = 연결 형태">
|
||||
<div className="grid gap-4">
|
||||
<div><p className="mb-2">단독 · 독립 선택</p><Toggle variant="outline">알림</Toggle></div>
|
||||
<div><p className="mb-2">연결 그룹 · 단일 선택</p><ToggleGroup type="single" variant="outline" defaultValue="left" aria-label="정렬"><ToggleGroupItem value="left">왼쪽</ToggleGroupItem><ToggleGroupItem value="center">가운데</ToggleGroupItem><ToggleGroupItem value="right">오른쪽</ToggleGroupItem></ToggleGroup></div>
|
||||
@@ -123,9 +122,9 @@ export const All: Story = {
|
||||
<StorySection title="Sizes">
|
||||
<Sizes />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="아이콘만, 아이콘 + 텍스트입니다.">{board("icon")}</StorySection>
|
||||
<StorySection title="Variants">{board("icon")}</StorySection>
|
||||
<StorySection title="States">{board("disabled", "controlled")}</StorySection>
|
||||
<StorySection title="Usage" description="className 확장입니다.">{board("extended")}</StorySection>
|
||||
<StorySection title="Usage">{board("extended")}</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Tooltip is a Radix overlay (Portal) with short, non-interactive text. The representative Story shows real triggers
|
||||
// (hover or focus to open); the verification Story starts one open. Interactive content belongs in Popover / Hover Card.
|
||||
@@ -77,17 +77,16 @@ export const All: Story = {
|
||||
<TooltipProvider>
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.components}
|
||||
title="Tooltip"
|
||||
description="짧고 상호작용 없는 설명을 띄우는 overlay입니다. Trigger에 포인터를 올리거나 포커스하면 열립니다."
|
||||
description="상호작용 없는 짧은 설명 · hover 또는 focus로 열림"
|
||||
/>
|
||||
<StorySection title="Overview" description="기본 Tooltip입니다.">
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="side: top / right / bottom / left.">
|
||||
<StorySection title="Variants">
|
||||
<Placements />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="긴 문구는 줄바꿈됩니다. Disabled 버튼은 포인터 이벤트가 없으므로 감싼 요소를 Trigger로 씁니다.">
|
||||
<StorySection title="Usage" description="Disabled 버튼은 감싼 요소를 Trigger로 사용">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<LongText />
|
||||
<Disabled />
|
||||
|
||||
@@ -7,12 +7,12 @@ function LayoutReferencePage() {
|
||||
const values = useResolvedProperties(layoutProperties)
|
||||
const [desktop, setDesktop] = useState(() => matchMedia("(min-width: 1024px)").matches)
|
||||
useEffect(() => { const media = matchMedia("(min-width: 1024px)"); const update = () => setDesktop(media.matches); media.addEventListener("change", update); return () => media.removeEventListener("change", update) }, [])
|
||||
return <ReferencePage title="Layout" intro="현재 HM의 responsive boundary와 컴포넌트별 layout geometry를 확인합니다. 전역 spacing scale이나 grid system을 새로 정의하지 않습니다.">
|
||||
<ReferenceSection title="Responsive Boundary" note="CURRENT_STANDARD · gnb.css / lnb.css / dashboard-layout.css 및 GNB의 matchMedia 규칙에 근거합니다."><div className="hm-foundation-grid"><article className="hm-foundation-panel"><h3>≥ 1024px · Desktop</h3><p>Desktop Navigation · LNB 표시 · GNB desktop menu</p></article><article className="hm-foundation-panel"><h3>≤ 1023px · Mobile</h3><p>Mobile Navigation · LNB 숨김 · GNB mobile menu</p></article></div><p role="status">현재 preview: <strong>{desktop ? "Desktop" : "Mobile"}</strong> · GNB 높이 {values["--hm-gnb-height"]} · LNB track {values["--hm-lnb-width"]}</p><p className="hm-foundation-note">CSS는 min-width: 1024px / max-width: 1023px, GNB JS는 min-width: 1024px를 사용합니다. 표시는 기존 정수 QA 경계를 따릅니다.</p></ReferenceSection>
|
||||
return <ReferencePage title="Layout" intro="현재 responsive boundary와 컴포넌트별 geometry · 전역 spacing scale / grid 없음">
|
||||
<ReferenceSection title="Responsive Boundary" note="GNB · LNB · Dashboard CSS와 matchMedia 기준"><div className="hm-foundation-grid"><article className="hm-foundation-panel"><h3>≥ 1024px · Desktop</h3><p>Desktop Navigation · LNB 표시 · GNB desktop menu</p></article><article className="hm-foundation-panel"><h3>≤ 1023px · Mobile</h3><p>Mobile Navigation · LNB 숨김 · GNB mobile menu</p></article></div><p role="status">현재 preview: <strong>{desktop ? "Desktop" : "Mobile"}</strong> · GNB 높이 {values["--hm-gnb-height"]} · LNB track {values["--hm-lnb-width"]}</p><p className="hm-foundation-note">CSS는 min-width: 1024px / max-width: 1023px, GNB JS는 min-width: 1024px를 사용합니다. 표시는 기존 정수 QA 경계를 따릅니다.</p></ReferenceSection>
|
||||
<ReferenceSection title="QA Reference Viewports" note="1280 / 390은 검증용 viewport이며 breakpoint token이 아닙니다."><div className="hm-foundation-grid"><div className="hm-foundation-panel"><h3>1280 × 900</h3><p>HM Desktop · QA reference</p></div><div className="hm-foundation-panel"><h3>390 × 844</h3><p>HM Mobile · QA reference</p></div></div><p className="hm-foundation-note">Boundary QA presets: 1024 × 900 / 1023 × 900</p></ReferenceSection>
|
||||
<ReferenceSection title="Spacing · Current Usage Reference" note="전역 공식 spacing scale은 정의되어 있지 않습니다. 아래 변수는 각 pattern/component의 현재 사용값이며 동일한 숫자를 하나의 token으로 합치지 않습니다."><div className="hm-foundation-grid">{spacingReferences.map(([property, name, source]) => <article className="hm-foundation-card" key={property}><h3>{name}</h3><code>{property}</code><p data-layout-value>{values[property]}</p><div className="hm-foundation-spacing" style={{ width: `var(${property})` }} aria-hidden="true" /><p className="hm-foundation-note">packages/ui/src/theme/{source}</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Radius" note="기존 default.css의 geometry variables입니다. 역할이 다른 같은 반경은 따로 표시합니다."><div className="hm-foundation-grid">{radiusReferences.map(property => <article className="hm-foundation-card" key={property}><div className="hm-foundation-radius" style={{ borderRadius: `var(${property})` }} /><code>{property}</code><p>{values[property]}</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Layout / Container Reference" note="CURRENT_USAGE_REFERENCE · Dashboard의 가용 폭, 외곽 padding, 섹션 gap과 Card padding만 축소 specimen으로 보여줍니다."><div className="hm-foundation-layout-sample" style={{ padding: "var(--hm-dashboard-padding)", gap: "var(--hm-dashboard-gap)" }}><div className="hm-foundation-layout-card" style={{ padding: "var(--hm-card-padding)" }}><strong>Dashboard content</strong><p>가용 폭을 사용하며 긴 텍스트는 내부에서 줄바꿈합니다.</p></div><div className="hm-foundation-layout-card" style={{ padding: "var(--hm-card-padding)" }}>다음 섹션 · gap: {values["--hm-dashboard-gap"]}</div></div><p className="hm-foundation-note">Grid / Container: NOT_FORMALLY_DEFINED · 공용 column 수 / max-width token은 FOLLOW_UP. src/hm-theme.css의 .hm-container는 앱 데모의 로컬 사용 규칙입니다.</p></ReferenceSection>
|
||||
<ReferenceSection title="Spacing · Current Usage Reference" note="pattern / component별 현재 사용값 · 같은 숫자도 하나의 token으로 합치지 않음"><div className="hm-foundation-grid">{spacingReferences.map(([property, name, source]) => <article className="hm-foundation-card" key={property}><h3>{name}</h3><code>{property}</code><p data-layout-value>{values[property]}</p><div className="hm-foundation-spacing" style={{ width: `var(${property})` }} aria-hidden="true" /><p className="hm-foundation-note">packages/ui/src/theme/{source}</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Radius" note="역할이 다른 같은 반경은 따로 표시"><div className="hm-foundation-grid">{radiusReferences.map(property => <article className="hm-foundation-card" key={property}><div className="hm-foundation-radius" style={{ borderRadius: `var(${property})` }} /><code>{property}</code><p>{values[property]}</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Layout / Container Reference" note="Dashboard 가용 폭 · 외곽 padding · 섹션 gap · Card padding 축소 specimen"><div className="hm-foundation-layout-sample" style={{ padding: "var(--hm-dashboard-padding)", gap: "var(--hm-dashboard-gap)" }}><div className="hm-foundation-layout-card" style={{ padding: "var(--hm-card-padding)" }}><strong>Dashboard content</strong><p>가용 폭을 사용하며 긴 텍스트는 내부에서 줄바꿈합니다.</p></div><div className="hm-foundation-layout-card" style={{ padding: "var(--hm-card-padding)" }}>다음 섹션 · gap: {values["--hm-dashboard-gap"]}</div></div><p className="hm-foundation-note">Grid / Container: NOT_FORMALLY_DEFINED · 공용 column 수 / max-width token은 FOLLOW_UP. src/hm-theme.css의 .hm-container는 앱 데모의 로컬 사용 규칙입니다.</p></ReferenceSection>
|
||||
</ReferencePage>
|
||||
}
|
||||
const meta = { title: "Foundation/Layout", parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import "./foundation.css"
|
||||
|
||||
export function useResolvedProperties(properties: readonly string[]) {
|
||||
@@ -25,7 +25,7 @@ export function useResolvedProperties(properties: readonly string[]) {
|
||||
|
||||
// The page frame, title and sections are the common Story page (src/components/story-page.tsx); .hm-foundation styles the reference content.
|
||||
export function ReferencePage({ title, intro, children }: { title: string; intro: string; children: ReactNode }) {
|
||||
return <StoryPage className="hm-foundation"><StoryPageHeader kicker={STORY_KICKER.foundation} title={title} description={intro} />{children}</StoryPage>
|
||||
return <StoryPage className="hm-foundation"><StoryPageHeader title={title} description={intro} />{children}</StoryPage>
|
||||
}
|
||||
export function ReferenceSection({ title, note, children, id }: { title: string; note: string; children: ReactNode; id?: string }) {
|
||||
return <StorySection id={id} title={title} description={note}>{children}</StorySection>
|
||||
|
||||
@@ -9,10 +9,10 @@ const weights = [
|
||||
]
|
||||
function TypographyReferencePage() {
|
||||
const values = useResolvedProperties(typographyProperties)
|
||||
return <ReferencePage title="Typography" intro="Current Typography Reference · 기존 HM 컴포넌트의 실제 구현 규격입니다. 새로운 Heading / Body / Caption token 체계를 정의하지 않습니다.">
|
||||
<ReferenceSection title="Font Family" note="기본 폰트는 Noto Sans KR입니다. 로딩 실패 시 맑은 고딕, 이후 시스템 sans-serif를 사용합니다."><div className="hm-foundation-panel"><p>Noto Sans KR → 맑은 고딕 → 시스템 sans-serif</p><details><summary>구현 참고 · 전체 CSS Font Stack</summary><code>--hm-font-family-base</code><p>{values["--hm-font-family-base"]}</p><p>/fonts/NotoSansKR-variable.ttf · font-weight 100–900 asset</p></details></div></ReferenceSection>
|
||||
<ReferenceSection title="Font Weight" note="400(Regular) · 500(Medium) · 700(Bold). 동일한 문자열로 3단계 글자 굵기를 비교합니다."><div className="hm-foundation-grid">{weights.map(({ property, label }) => <article className="hm-foundation-card" key={property}><code>{property}</code><p>{values[property]} — {label}</p><p style={{ fontWeight: `var(${property})` }}>한맥 프로젝트<br />Hanmac Project<br />0123456789</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Component Specimens" note="크기 / 행높이 / 굵기 / 자간은 specimen에서 computed style로 읽습니다. 자간을 별도 지정하지 않은 컴포넌트는 현재 normal을 유지합니다."><div className="hm-foundation-specimens">{typographyReferences.map(item => <article className="hm-foundation-panel" key={item.label}><h3>{item.label}</h3><p className="hm-foundation-note">packages/ui/src/theme/{item.source}</p><dl><dt>Size</dt><dd><code>{item.size}</code> · {values[item.size]}</dd><dt>Line Height</dt><dd><code>{item.line}</code> · {values[item.line]}</dd><dt>Weight</dt><dd>{item.weight ? <><code>{item.weight}</code> · {values[item.weight]}</> : "normal · 컴포넌트에서 별도 weight 지정 없음"}</dd></dl><Specimen {...item} /></article>)}</div></ReferenceSection>
|
||||
return <ReferencePage title="Typography" intro="기존 HM 컴포넌트의 실제 구현 규격 · 별도 Heading / Body / Caption token 없음">
|
||||
<ReferenceSection title="Font Family" note="Noto Sans KR → 맑은 고딕 → 시스템 sans-serif"><div className="hm-foundation-panel"><p>Noto Sans KR → 맑은 고딕 → 시스템 sans-serif</p><details><summary>구현 참고 · 전체 CSS Font Stack</summary><code>--hm-font-family-base</code><p>{values["--hm-font-family-base"]}</p><p>/fonts/NotoSansKR-variable.ttf · font-weight 100–900 asset</p></details></div></ReferenceSection>
|
||||
<ReferenceSection title="Font Weight" note="400(Regular) · 500(Medium) · 700(Bold) 3단계"><div className="hm-foundation-grid">{weights.map(({ property, label }) => <article className="hm-foundation-card" key={property}><code>{property}</code><p>{values[property]} — {label}</p><p style={{ fontWeight: `var(${property})` }}>한맥 프로젝트<br />Hanmac Project<br />0123456789</p></article>)}</div></ReferenceSection>
|
||||
<ReferenceSection title="Component Specimens" note="specimen의 computed style 기준 · 자간 미지정은 normal"><div className="hm-foundation-specimens">{typographyReferences.map(item => <article className="hm-foundation-panel" key={item.label}><h3>{item.label}</h3><p className="hm-foundation-note">packages/ui/src/theme/{item.source}</p><dl><dt>Size</dt><dd><code>{item.size}</code> · {values[item.size]}</dd><dt>Line Height</dt><dd><code>{item.line}</code> · {values[item.line]}</dd><dt>Weight</dt><dd>{item.weight ? <><code>{item.weight}</code> · {values[item.weight]}</> : "normal · 컴포넌트에서 별도 weight 지정 없음"}</dd></dl><Specimen {...item} /></article>)}</div></ReferenceSection>
|
||||
</ReferencePage>
|
||||
}
|
||||
const meta = { title: "Foundation/Typography", parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
|
||||
@@ -113,11 +113,10 @@ function ExpandedPreview() {
|
||||
|
||||
return <StoryPage className="hm-orgchart-story-page hm-orgchart-story-expanded">
|
||||
<StoryPageHeader
|
||||
kicker="HM DESIGN SYSTEM / ORGCHART"
|
||||
title="Expanded OrgChart"
|
||||
description="데이터·회사·표시 유형을 선택하고 직원 카드를 눌러 정보를 확인하세요. 모든 직원은 가상 데이터입니다."
|
||||
description="모든 직원은 가상 데이터"
|
||||
/>
|
||||
<StorySection title="데이터 설정" description="회사 선택은 즉시 반영됩니다. 회사당 직원 수 변경은 데이터 적용을 눌러 반영합니다.">
|
||||
<StorySection title="데이터 설정" description="직원 수 변경은 데이터 적용으로 반영">
|
||||
<fieldset className="hm-orgchart-story-settings">
|
||||
<legend className="hm-orgchart-story-section-legend">데이터 설정</legend>
|
||||
<FilterBar layout="list-extended">
|
||||
@@ -167,7 +166,7 @@ function ExpandedPreview() {
|
||||
<p className="hm-orgchart-story-note">회사 숨김·다시 표시는 현재 소속 변경을 유지합니다. 데이터 적용은 선택한 조건의 예시를 새로 만들고, 초기화는 최초 데이터 설정과 소속으로 돌아갑니다.</p>
|
||||
</fieldset>
|
||||
</StorySection>
|
||||
<StorySection title="레이아웃 설정" description="TB·LR·Hybrid 표시 유형과 초기 표시 Depth를 설정합니다.">
|
||||
<StorySection title="레이아웃 설정" description="표시 유형 · 초기 Depth">
|
||||
<fieldset className="hm-orgchart-story-settings">
|
||||
<legend className="hm-orgchart-story-section-legend">레이아웃 설정</legend>
|
||||
<div className="hm-orgchart-story-controls">
|
||||
@@ -205,7 +204,7 @@ function ExpandedPreview() {
|
||||
<p className="hm-orgchart-story-note">Compact: 미지원. 기존 직원 카드 디자인을 보존하며, 압축 배치 선택은 제공하지 않습니다.</p>
|
||||
</fieldset>
|
||||
</StorySection>
|
||||
<StorySection title="인터랙션·직원 정보" description="Drag & Drop과 탐색 기능을 개별 선택하고 직원 카드의 팝업 방식을 설정합니다.">
|
||||
<StorySection title="인터랙션·직원 정보" description="Drag & Drop · 탐색 · 팝업 방식">
|
||||
<div className="hm-orgchart-story-settings">
|
||||
<fieldset className="hm-orgchart-story-companies">
|
||||
<legend>독립 기능 설정 · 모든 표시 유형에서 개별 선택</legend>
|
||||
@@ -232,7 +231,7 @@ function ExpandedPreview() {
|
||||
</div>
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="조직도 미리보기" description="직원 카드를 눌러 정보를 확인하고, 활성화한 탐색 기능으로 조직도를 살펴봅니다.">
|
||||
<StorySection title="조직도 미리보기" description="직원 카드를 눌러 정보 확인">
|
||||
<div className="hm-orgchart-story">
|
||||
<p className="hm-orgchart-story-note">빈 공간 드래그는 Pan입니다. {dragDropEnabled ? '직원 카드·그룹 타이틀 드래그로 회사 안팎의 소속을 변경합니다. 키보드: Alt+M → 방향키 → Enter / ESC 취소.' : '소속 변경은 Drag & Drop을 ON으로 설정하면 사용할 수 있습니다.'} 클릭은 직원 정보·그룹 접기입니다.</p>
|
||||
<p className="hm-orgchart-story-note" data-component="OrgChartExampleCounts" aria-live="polite">
|
||||
|
||||
@@ -19,8 +19,8 @@ export function CompositionPreview() {
|
||||
const host = useRef<HTMLElement>(null);
|
||||
useLayoutEffect(() => centerCompositionPreview(host.current!), []);
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader kicker="HM DESIGN SYSTEM / ORGCHART" title="Full Composition" description="Workdesk 기반 고정형 조직도입니다. 대표자와 6개 조직, 협업 허브 및 인원 현황을 한 화면에서 확인합니다." />
|
||||
<StorySection title="고정형 조직도" description="직원 카드를 눌러 정보를 확인하세요. 미리보기 영역의 실제 폭에 맞춰 전체 구성을 비례 축소합니다.">
|
||||
<StoryPageHeader title="Full Composition" description="Workdesk 기반 고정형 조직도" />
|
||||
<StorySection title="고정형 조직도" description="직원 카드를 눌러 정보 확인 · 미리보기 폭에 맞춰 비례 축소">
|
||||
<section ref={host} className="hm-orgchart-story hm-orgchart-story-full" aria-label="Full Composition">
|
||||
<OrgChart dataSource={compositionSource} stylesheetUrls={compositionStylesheets} displayMode="readOnly" className="hm-orgchart-story-chart" />
|
||||
</section>
|
||||
@@ -30,7 +30,7 @@ export function CompositionPreview() {
|
||||
|
||||
export function DepartmentTypographyPreview() {
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader kicker="HM DESIGN SYSTEM / ORGCHART" title="Department Colors" />
|
||||
<StoryPageHeader title="Department Colors" />
|
||||
<StorySection title="조직별 컬러 · 카드 미리보기">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="DepartmentColors" presentation="design" /></div>
|
||||
</StorySection>
|
||||
@@ -39,14 +39,14 @@ export function DepartmentTypographyPreview() {
|
||||
|
||||
export function OverviewPreview() {
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader kicker="HM DESIGN SYSTEM / ORGCHART" title="Overview" description="회사 로고와 조직·직위별, 관계사별 인원 현황입니다. 인원 현황은 고정형 조직도와 같은 Core 집계·표시 디자인을 사용하며 기존 가상 데이터를 표시합니다." />
|
||||
<StorySection title="관계사·회사 로고" description="회사 로고와 기준 안내를 확인합니다.">
|
||||
<StoryPageHeader title="Overview" description="회사 로고와 인원 현황 · 가상 데이터" />
|
||||
<StorySection title="관계사·회사 로고" description="회사 로고와 기준 안내">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="Logo" hideLogoHeading /></div>
|
||||
</StorySection>
|
||||
<StorySection title="조직·직위별 인원 현황" description="기존 가상 직원 데이터를 조직·직위별로 집계합니다.">
|
||||
<StorySection title="조직·직위별 인원 현황" description="가상 데이터 조직·직위별 집계">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="PositionSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
<StorySection title="관계사별 인원 현황" description="동일 데이터의 관계사별 인원 현황입니다. 실제 운영 인원수가 아닙니다.">
|
||||
<StorySection title="관계사별 인원 현황" description="관계사별 · 실제 운영 인원수 아님">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="AffiliationSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
useDataTable,
|
||||
type DataTableColumnDef,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { formatDate } from "./sample-users"
|
||||
import { countDashboardWorkFilter, dashboardWorks, dashboardWorkFilters, dashboardWorkStatus, dashboardWorkStyle, matchesDashboardWorkStatus, type DashboardWork, type DashboardWorkFilter } from "./dashboard-work-status"
|
||||
import "./dashboard-layout.stories.css"
|
||||
@@ -431,13 +431,12 @@ export function DashboardAll() {
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Dashboard"
|
||||
description="업무 화면의 전체 틀입니다. DASHBOARD 단일 모드로, 내부 48px Compact Header와 상태 필터 바 아래에 콘텐츠를 바로 잇습니다."
|
||||
description="DASHBOARD 단일 모드 · 48px Compact Header + 상태 필터 + 콘텐츠"
|
||||
/>
|
||||
<StorySection
|
||||
title="Overview"
|
||||
description="Compact Header(48px) · 상태 필터 · 콘텐츠. 최소 폭 960px을 유지하며 좁은 화면에서는 Dashboard 전체를 가로 스크롤합니다."
|
||||
description="최소 폭 960px · 좁으면 가로 스크롤"
|
||||
>
|
||||
<DashboardScreen mode="compact" />
|
||||
</StorySection>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from "@hm-design/ui"
|
||||
import "../list-screen.css"
|
||||
import { MoreHorizontalIcon } from "lucide-react"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { formatDate, sameDay, sampleUsers, statusLabel, type SampleUser } from "./sample-users"
|
||||
|
||||
// Data Table is a pattern: the HM Table primitive driven by TanStack Table v9 (useDataTable). Columns and data stay referentially stable
|
||||
@@ -154,15 +154,14 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Data Table"
|
||||
description="업무 목록을 표로 보여 주는 Pattern입니다. 선택·정렬·열 표시·페이지 이동을 하나의 표에서 다룹니다."
|
||||
description="선택 · 정렬 · 열 표시 · 페이지 이동"
|
||||
/>
|
||||
<StorySection title="Overview" description="행 선택(헤더 혼합 상태), 정렬, 열 표시, 행 작업, 페이지 크기와 페이지 이동을 실제로 조작합니다.">
|
||||
<StorySection title="Overview">
|
||||
<Basic />
|
||||
</StorySection>
|
||||
<StorySection title="기본 목록 검색" description="조건·검색어·조회·초기화를 8px 간격으로 가운데 정렬합니다. 공간이 충분하면 열 버튼은 같은 행 우측, 부족할 때만 다음 행에 배치합니다. 제목은 Card 밖에 배치합니다."><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색" description="부서·등록일을 제공하는 구성은 입력값과 관계없이 왼쪽 정렬합니다. 열 버튼은 공간이 충분하면 같은 행 우측에 배치하며 모바일 버튼 행은 우측 정렬합니다."><WithFilterBar /></StorySection>
|
||||
<StorySection title="기본 목록 검색" description="8px 간격 가운데 정렬 · 열 버튼은 공간 부족 시만 다음 행 · 제목은 Card 밖"><WithFilterBar extended={false} /></StorySection>
|
||||
<StorySection title="추가 조건 목록 검색" description="부서·등록일 구성은 왼쪽 정렬 · 모바일 버튼 행은 우측 정렬"><WithFilterBar /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Filter Bar is a layout pattern: the consumer composes the HM controls as children and owns every value. It does not depend on Data Table.
|
||||
|
||||
@@ -85,14 +85,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Filter Bar"
|
||||
description="목록 위에서 조회 조건을 고르는 레이아웃 Pattern입니다. HM 컨트롤을 children으로 조합하며 모든 값은 소비 코드가 소유합니다. Data Table에 의존하지 않습니다."
|
||||
description="HM 컨트롤을 children으로 조합 · 값은 소비 코드 소유 · Data Table 비의존"
|
||||
/>
|
||||
<StorySection title="Overview" description="검색 · Select · Combobox · Date Picker와 초기화 / 조회 버튼입니다. 폭이 좁으면 줄바꿈됩니다.">
|
||||
<StorySection title="Overview" description="좁은 폭에서 줄바꿈">
|
||||
<Full />
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="추가 조건은 list-extended, 기본 조건은 list-basic. 기본 layout은 기존 행 끝 정렬을 유지합니다. 390px에서는 검색과 액션이 줄바꿈됩니다.">
|
||||
<StorySection title="Usage" description="list-extended / list-basic · 390px에서 검색·액션 줄바꿈">
|
||||
<p>입력은 draft로 유지하고 조회에만 적용합니다. 초기화는 입력과 적용값을 함께 비웁니다. 목록은 PageHeader 밖의 흰 Card 하나에 검색과 결과를 함께 배치합니다.</p>
|
||||
</StorySection>
|
||||
<StorySection title="Do & Don't" description="요구한 조건과 액션만 구성합니다.">
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
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
|
||||
// the content, White 2-depth panel, text-only profile); every part inside is an HM component. The menu data, the user and the current
|
||||
@@ -433,13 +433,12 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="GNB"
|
||||
description="서비스 최상단의 전역 내비게이션입니다. 1024px 이상은 Desktop 메뉴, 1023px 이하는 햄버거 → Sheet 메뉴를 사용합니다."
|
||||
description="1024px 이상 Desktop 메뉴 · 1023px 이하 햄버거 → Sheet"
|
||||
/>
|
||||
<StorySection
|
||||
title="Overview"
|
||||
description="현재 위치(대시보드)는 Beige 면이 아래 콘텐츠와 오목하게 이어집니다. hover는 Black 10%. 프로젝트·관리는 hover/click으로 White 하위 패널이 열립니다."
|
||||
description="현재 위치 Beige · hover Black 10% · 하위 패널은 hover / click"
|
||||
>
|
||||
<Page>
|
||||
<Bar />
|
||||
@@ -447,7 +446,7 @@ export const All: Story = {
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="States · Navigation Menu Open"
|
||||
description="프로젝트 메뉴가 열려 White 면으로 패널과 이어지고, 하위 현재 위치(진행 중 프로젝트)는 Beige로 표시됩니다. 긴 하위 이름은 Desktop 패널 폭을 확장하여 한 줄로 표시합니다."
|
||||
description="하위 현재 위치 Beige · 긴 하위 이름은 패널 폭 확장"
|
||||
>
|
||||
<div style={{ minHeight: 360 }}>
|
||||
<Page label="프로젝트 > 진행 중 프로젝트">
|
||||
@@ -457,7 +456,7 @@ export const All: Story = {
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · Long Labels"
|
||||
description="긴 브랜드명·메뉴명·사용자명. 브랜드와 사용자 글자는 말줄임, 메뉴 항목은 줄바꿈 없이 유지되고 공간을 넘으면 메뉴 영역에서 잘립니다."
|
||||
description="브랜드·사용자는 말줄임 · 메뉴는 줄바꿈 없이 영역에서 잘림"
|
||||
>
|
||||
<Page>
|
||||
<Bar items={longNavItems} person={longUser} eyebrow="한맥기술 인프라 통합 시스템" brand="스마트 시설물 통합 관리 시스템" />
|
||||
@@ -465,7 +464,7 @@ export const All: Story = {
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · With Logo"
|
||||
description="로고 asset이 있을 때만 왼쪽에 원본 비율로 놓입니다. 없으면 영역·간격 없이 글자만 둡니다(Overview). 표시 이미지는 배치 확인용 테스트 이미지이며 한맥 로고가 아닙니다."
|
||||
description="로고는 있을 때만 원본 비율 · 테스트 이미지(한맥 로고 아님)"
|
||||
>
|
||||
<Page>
|
||||
<Bar logo={testLogo} />
|
||||
@@ -473,7 +472,7 @@ export const All: Story = {
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Variants · Utilities"
|
||||
description="선택 사항. ui-viz의 GNB 오른쪽은 이름·역할만 둡니다. 검색·알림이 필요한 서비스는 HM ghost Button 아이콘을 GNBUtilities에 조합합니다(Tooltip, Badge, Popover, Command)."
|
||||
description="선택 사항 · 검색·알림은 GNBUtilities에 ghost Button 아이콘 조합"
|
||||
>
|
||||
<Page>
|
||||
<Bar utilities />
|
||||
@@ -481,15 +480,15 @@ export const All: Story = {
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Usage · Sticky"
|
||||
description="일반 GNB와 같은 외형으로 스크롤 영역 상단에 붙습니다. 본문 위에서 휠/터치로 스크롤하거나 영역에 포커스한 뒤 PageDown/End/Home 키로 확인합니다."
|
||||
description="스크롤 영역 상단에 고정"
|
||||
>
|
||||
<StickyPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · GNB Sidebar"
|
||||
description="기존 Reference 전용 콘텐츠 LNB입니다. 본문과 같은 배경, 세로 구분선 하나, 아이콘 기본 생략·화살표 유지. 신규 AI GNB_ONLY 화면에는 Sidebar를 생성하지 않습니다.">
|
||||
description="기존 Reference 전용 · 신규 AI GNB_ONLY 화면에는 Sidebar 생성 금지">
|
||||
<SidebarPage />
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB" description="기존 Reference의 Brown 선택·Hover를 비교합니다. Green 선택색을 혼용하지 않습니다."><SidebarPage color="brown" /></StorySection>
|
||||
<StorySection title="Usage · Brown 콘텐츠 LNB" description="Brown 선택·Hover · Green 선택색 혼용 금지"><SidebarPage color="brown" /></StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
LNBMobileMenu,
|
||||
} from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// LNB is a composition pattern. Layout visual reference is the ui-viz LNB (272px green column, 18/700 items, Beige current item joined
|
||||
// to the content, Black 10% hover, Black 8% open group). The menu data and the current location are local sample data for this Story
|
||||
@@ -634,53 +634,52 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="LNB"
|
||||
description="화면 왼쪽의 지역 내비게이션입니다. 1024px 이상에서 표시하고, 1023px 이하는 숨긴 뒤 모바일 메뉴로 대체합니다."
|
||||
description="1024px 이상 표시 · 1023px 이하 모바일 메뉴로 대체"
|
||||
/>
|
||||
<StorySection title="Overview" description="모든 뎁스 18px. 선택은 Beige 면 + 초록 글자, 왼쪽 모서리 8로 콘텐츠와 오목하게 이어집니다.">
|
||||
<StorySection title="Overview" description="모든 뎁스 18px · 선택 Beige 면 + 초록 글자">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "selected", label: "Selected", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} /> },
|
||||
{ id: "third-selected", label: "3뎁스 메뉴 · 선택", note: "3뎁스는 20px 들여쓰기와 1px 그린 연결선으로 구분하며, 선은 마지막 메뉴에서 끝납니다. 선택은 베이지, 상위 경로는 노란색입니다.", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
||||
{ id: "third-selected", label: "3뎁스 메뉴 · 선택", note: "3뎁스 20px 들여쓰기 · 1px 그린 연결선", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="States" description="그룹 펼침·접힘과 하위 선택에 따른 표시입니다. 선택된 경로의 상위는 접혀도 노란색을 유지합니다.">
|
||||
<StorySection title="States" description="선택 경로의 상위는 접혀도 노란색">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "default", label: "Default · 기존 LNB", note: "선택 없음, 2뎁스 그룹 접힘. 항목에 마우스를 올리면 Black 10%가 콘텐츠 경계까지 닿습니다.", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: false }} /> },
|
||||
{ id: "default", label: "Default · 기존 LNB", note: "선택 없음 · 그룹 접힘 · Hover Black 10%", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: false }} /> },
|
||||
{ id: "expanded", label: "Expanded · 2뎁스 펼침", note: "하위 선택 없이 펼친 그룹: Black 8% 면 + 1px 초록 선.", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||
{ id: "selected-child", label: "Selected Child · 2뎁스 선택", note: "하위 선택: 상위 이름·화살표가 노란색, 하위는 Beige.", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: true }} /> },
|
||||
{ id: "collapsed-with-selection", label: "Collapsed With Selection · 접힘", note: "하위가 선택된 채 접힌 그룹도 상위가 노란색을 유지합니다.", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: false }} /> },
|
||||
{ id: "third-second-collapsed", label: "3뎁스 메뉴 · 2뎁스 접힘", note: "디자인 관리를 접어도 선택된 경로의 1·2뎁스 표시를 유지합니다. 하위 메뉴와 연결선이 함께 숨겨집니다.", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": false }} /> },
|
||||
{ id: "third-first-collapsed", label: "3뎁스 메뉴 · 1뎁스 접힘", note: "프로젝트 관리를 접어도 노란색을 유지합니다. 다시 펼치면 디자인 관리의 열림 상태가 그대로입니다(open은 화면이 관리).", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: false, "projects/design": true }} /> },
|
||||
{ id: "third-second-collapsed", label: "3뎁스 메뉴 · 2뎁스 접힘", note: "접어도 선택 경로 1·2뎁스 표시 유지", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": false }} /> },
|
||||
{ id: "third-first-collapsed", label: "3뎁스 메뉴 · 1뎁스 접힘", note: "접어도 노란색 유지 · open 상태는 화면이 관리", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: false, "projects/design": true }} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="로고 유무와 긴 이름입니다.">
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "with-logo", label: "With Logo · 로고 있음(테스트 이미지)", note: "로고는 서비스명 위, 같은 시작선. 없으면 영역·간격 없이 서비스명만 둡니다.", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} /> },
|
||||
{ id: "with-logo", label: "With Logo · 로고 있음(테스트 이미지)", note: "로고는 서비스명 위 · 없으면 서비스명만", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} /> },
|
||||
{ id: "long", label: "Long labels", note: "긴 이름은 줄바꿈되며 항목 높이가 늘어납니다.", children: <ResponsiveSpecimen nodes={simpleNav(true)} initial="tasks/list" initialOpen={{ tasks: true }} longLabels /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage · Scroll" description="메뉴가 열보다 길면 메뉴 영역만 세로로 스크롤하고 브랜드·프로필은 그대로입니다(높이 460).">
|
||||
<StorySection title="Usage · Scroll" description="메뉴 영역만 세로 스크롤 · 브랜드·프로필 고정">
|
||||
<StateBoard
|
||||
min={560}
|
||||
states={[
|
||||
{ id: "scroll", label: "Scroll", children: <ResponsiveSpecimen nodes={simpleNav(false, true)} initial="overview" initialOpen={{ tasks: true }} height={460} /> },
|
||||
{ id: "third-scroll", label: "3뎁스 메뉴 · Scroll", note: "3뎁스·연결선·긴 이름도 메뉴 영역 안에서 함께 스크롤됩니다. 스크롤 영역은 브랜드 바로 아래에서 시작해 프로필 바로 위에서 끝납니다.", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} /> },
|
||||
{ id: "third-scroll", label: "3뎁스 메뉴 · Scroll", note: "스크롤 영역: 브랜드 아래 ~ 프로필 위", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} /> },
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection
|
||||
title="Usage · LNB Standalone 반응형 (GNB 없음)"
|
||||
description="1024px 이상은 왼쪽 LNB의 브랜드·프로필을, 1023px 이하는 LNB를 숨긴 뒤 Local 모바일 바와 Sheet의 메뉴·프로필을 사용합니다. 열림·키보드·Escape는 LNB Standalone Mobile Menu Open에서 확인합니다."
|
||||
description="1023px 이하는 모바일 바 + Sheet"
|
||||
>
|
||||
<div className="flex flex-wrap items-start gap-6">
|
||||
<ViewportPreview story="standalone-desktop" label="Desktop" width={1024} height={640} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { Button, LoadingScreen } from "@hm-design/ui"
|
||||
import { StateBoard } from "@/components/story-board"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
// Loading Screen is a Dashboard-local pattern (ui-viz LoadingScreen). Inline fills its parent; fullscreen covers the viewport. The
|
||||
// fullscreen demo is mounted and removed by local state only; there is no async task, cancel or close control in the pattern.
|
||||
@@ -33,14 +33,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Loading Screen"
|
||||
description="Dashboard 안에서 데이터를 불러오는 동안 표시하는 화면입니다. Inline은 부모 영역을 채우고, fullscreen은 viewport 전체를 덮습니다."
|
||||
description="Inline은 부모 영역, fullscreen은 viewport 전체"
|
||||
/>
|
||||
<StorySection title="Overview" description="Inline: 부모 영역(여기서는 공통 Inner 폭)을 채웁니다.">
|
||||
<StorySection title="Overview">
|
||||
<LoadingScreen />
|
||||
</StorySection>
|
||||
<StorySection title="Variants" description="사용자 문구와 좁은 부모 영역입니다.">
|
||||
<StorySection title="Variants">
|
||||
<StateBoard
|
||||
states={[
|
||||
{ id: "label", label: "Custom label", children: <LoadingScreen>데이터를 불러오는 중</LoadingScreen> },
|
||||
@@ -48,7 +47,7 @@ export const All: Story = {
|
||||
]}
|
||||
/>
|
||||
</StorySection>
|
||||
<StorySection title="Usage" description="fullscreen은 viewport 전체를 덮습니다. 버튼으로 열고 오른쪽 위 닫기로 닫습니다.">
|
||||
<StorySection title="Usage">
|
||||
<Fullscreen />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,
|
||||
Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
} from "@hm-design/ui"
|
||||
import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
|
||||
const meta = {
|
||||
title: "Patterns/Layout/Page Header",
|
||||
@@ -32,14 +32,13 @@ export const All: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={STORY_KICKER.patterns}
|
||||
title="Page Header"
|
||||
description="기본형은 Breadcrumb 없이 제목 중심입니다. 설명·액션·Breadcrumb는 화면의 필요에 따라 선택 적용합니다."
|
||||
description="기본형은 제목 중심 · 설명·액션·Breadcrumb는 선택"
|
||||
/>
|
||||
<StorySection title="Overview" description="제목 중심 기본형입니다. Breadcrumb는 생략하고 필요한 설명·액션을 선택 적용합니다.">
|
||||
<StorySection title="Overview">
|
||||
<PageHeader><PageHeaderContent><PageHeaderTitle>업무 현황</PageHeaderTitle></PageHeaderContent></PageHeader>
|
||||
</StorySection>
|
||||
<StorySection title="Breadcrumb 포함형" description="Breadcrumb·설명·단일 Brand Action과 Primary Tabs 조합입니다. 제목→설명 8px, Header→Tabs 20px을 유지합니다.">
|
||||
<StorySection title="Breadcrumb 포함형" description="제목→설명 8px · Header→Tabs 20px">
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const FullFooter: Story = {
|
||||
name: "SW Footer",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||
<StoryPageHeader title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||
<StorySection title="실제 Footer 조합 (동작)" description="명령 클릭 = 선택(다시 클릭 해제)·명령 기록 추가, 분류 탭(←/→, 바꿔도 칸 영역 고정), 빈 칸 + 클릭·Hover 가이드(영상 대신 Figma 6프레임 순환), 보조기능 토글(켜짐 = 주황 테두리·글자), 명령어 전체보기·▲ = 명령창 위로 펼치기, Layout 탭 선택·+·×·더블클릭/F2 이름 변경·Delete, 명령창 Enter 실행. 좁은 폭의 ▲ 확장 목록은 「화면 폭 변화」, Hover · Active · Selected · Disabled는 이 화면의 각 부품에서 동작합니다.">
|
||||
<Workspace width={1920} />
|
||||
</StorySection>
|
||||
@@ -378,7 +378,7 @@ export const Responsive: Story = {
|
||||
name: "Responsive More",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
<StoryPageHeader title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
<div key={width} className="sw-footer-story-width">
|
||||
<StorySection title={`${width}px`}>
|
||||
|
||||
@@ -90,18 +90,17 @@ export const HomeMenu: Story = {
|
||||
name: "Home Menu",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="Home Menu — 로고(홈) 버튼 메뉴"
|
||||
description="SWHeader의 homeMenu 하나로 연결합니다(SW 일반 모드 전용, SW 뷰어 모드에는 적용하지 않음). 로고 클릭 = 열기, 다시 클릭 = 닫기(onHomeClick은 호출하지 않음), 메뉴 빈 곳 클릭 = 닫기(Figma CLOSE), 타일 클릭 = 실행(누르는 동안만 Variant3, 클릭 후 ON 없음), 최근파일만 목록이 열린 동안 Variant3. Escape는 더보기 → 하위 패널 → 메뉴 순으로 닫고 로고로 Focus를 돌립니다. ←/→/Home/End로 타일을 이동합니다. New / Open은 문서 Dropdown 안에 그대로 있고, Dropdown을 열면 메뉴가 닫힙니다. 메뉴는 Header 위에 겹치는 층이라 열고 닫아도 Header 요소의 위치·너비가 바뀌지 않습니다." />
|
||||
<StorySection title="로고 클릭 · 열기 / 닫기 (현재 폭)" description="5개 섹션(파일 · 손상된 파일 · Display · 설정 · 궁금하면) 15개 타일. 마우스를 올리면 Hover(Variant2), 누르는 동안 Pressed(Variant3)이며 클릭 후 ON 상태는 남지 않습니다. 최근파일 타일만 하위 패널을 열고, 목록이 열린 동안 Variant3를 유지합니다.">
|
||||
<StoryPageHeader title="Home Menu — 로고(홈) 버튼 메뉴" description="SW 일반 모드 전용(뷰어 모드 미적용) · 로고 클릭으로 열고 닫기 · 상세 동작은 SW AI 가이드 COMPONENT_CATALOG" />
|
||||
<StorySection title="로고 클릭 · 열기 / 닫기 (현재 폭)" description="5개 섹션 15개 타일 · 클릭 후 ON 상태 없음">
|
||||
<HeaderWithMenu>{log => log}</HeaderWithMenu>
|
||||
</StorySection>
|
||||
<StorySection title="열림 (현재 폭)" description="열림 동안 로고는 Home On(노란 테두리), Header 상·하단에 #5F4B0F 2px 선이 표시되고 닫으면 기본으로 돌아갑니다(Figma 26:10591 / 662:67267).">
|
||||
<StorySection title="열림 (현재 폭)" description="로고 Home On · Header 상·하단 2px 선">
|
||||
<HeaderWithMenu defaultOpen autoFocus={false} />
|
||||
</StorySection>
|
||||
<StorySection title="최근파일 하위 패널 (662:67267)" description="최근파일 타일을 누르면(Variant3) 메뉴 하단 강조가 전체 2px 외곽선으로 바뀌고 그 아래에 최근파일 목록이 열립니다. 행에 마우스를 올리면 2뎁스 미리보기 영역이 옆에 나타나며 이미지는 표시하지 않고 빈 영역으로 둡니다(2026-10-21). 다시 누르거나 Escape로 닫습니다. 최근파일이 더보기 안에 있으면 목록은 더보기 타일 아래에 열리고 더보기 타일이 Variant3가 됩니다. 목록은 Story 데이터입니다.">
|
||||
<StorySection title="최근파일 하위 패널 (662:67267)" description="미리보기는 빈 영역 · 목록은 Story 데이터">
|
||||
<div className="sw-home-menu-story__tall"><HeaderWithMenu defaultOpen autoFocus={false} defaultActiveIds={["recent"]}>{log => log}</HeaderWithMenu></div>
|
||||
</StorySection>
|
||||
<StorySection title="통합 · 속성창 · SWPopup · Escape" description="Escape는 Focus가 있는 곳을 먼저 닫습니다(메뉴 안: 하위 패널 → 메뉴, Popup 안: 그 Popup). Focus가 모두의 밖이면 가장 최근에 열거나 사용한 하나만 닫습니다.">
|
||||
<StorySection title="통합 · 속성창 · SWPopup · Escape" description="Escape는 Focus 위치부터 닫고, 밖에서는 가장 최근 하나만 닫음">
|
||||
<IntegrationDemo />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
|
||||
@@ -131,7 +131,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
||||
const weight = weights.find(item => item.id === weightId)
|
||||
|
||||
return <StoryPage className="sw-story-overview">
|
||||
<StoryPageHeader kicker="HM DESIGN SYSTEM / SW" title="SW Header" />
|
||||
<StoryPageHeader title="SW Header" />
|
||||
{showVerificationControls() && <div className="sw-overview-toolbar" aria-label="Storybook 전용 미리보기 설정">
|
||||
<label htmlFor={`${controlId}-selection`}>선택 상태</label>
|
||||
<select id={`${controlId}-selection`} value={example} onChange={event => changeExample(event.target.value as Selection)}>
|
||||
|
||||
@@ -453,7 +453,6 @@ export const LogoSlot: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Logo Slot"
|
||||
description="로고 영역의 위치·크기(홈 버튼 안 12 / 16, 32 × 17)는 공통 디자인이고, 로고 이미지는 각 SW 프로그램이 정합니다. Storybook 기본 로고는 임시 자산(sw-header-logo-placeholder.svg)이며 확정 로고가 아닙니다."
|
||||
/>
|
||||
@@ -785,7 +784,7 @@ export const ResponsiveMore: Story = {
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Header — 화면 너비" />
|
||||
<StoryPageHeader title="SW Header — 화면 너비" />
|
||||
{MORE_WIDTHS.map(width => (
|
||||
<StorySection key={width} title={`${width}px`}>
|
||||
<MenuFrame width={width}><StyleHeader /></MenuFrame>
|
||||
@@ -931,5 +930,5 @@ function PropertyCompare() {
|
||||
}
|
||||
/** Rendered by SW/Popup › Property Panel · 4개 비교 (sw-property-panel.stories.tsx, ID sw-header--property-panel-compare). */
|
||||
export function PropertyPanelComparePage() {
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" /><PropertyCompare /></StoryPage>
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader title="속성창 4개 상태 비교" /><PropertyCompare /></StoryPage>
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import "./sw-integrated-stories.css"
|
||||
|
||||
// SWPopup (2026-10-19): the SW property window frame with free children. Public API only, like a consuming
|
||||
// project. HM UI components are placed inside to show that the content is not restricted.
|
||||
const KICKER = "HM DESIGN SYSTEM / SW"
|
||||
const LAYERS: SWHeaderLayer[] = [
|
||||
{ id: "0", name: "0", color: "#ffffff" },
|
||||
{ id: "wall", name: "WALL", color: "#ff4242" },
|
||||
@@ -159,7 +158,7 @@ function PopupPage() {
|
||||
const [log, record] = useLog()
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Popup — 외형 고정, 콘텐츠 자유"
|
||||
<StoryPageHeader title="SW Popup — 외형 고정, 콘텐츠 자유"
|
||||
description="SWPopup은 속성창 프레임(314px, 제목줄, 닫기, 내부 스크롤)을 그대로 쓰고 children으로 아무 콘텐츠나 받습니다. SWPopupSection / SWPopupRow는 속성창 그룹·행 모양을 그대로 재사용합니다. Header 없이 SWTheme 안에서 씁니다." />
|
||||
{log}
|
||||
<StorySection title="사용자 정의 설정 · 객체 정보 · 기존 속성창 + children" description="SW Input·Select(단위, Viewer 축척 지정과 같은 SWSelect)·레이어/색상(속성창 컨트롤 SWPopupPropertyRow — SWHeader 필드 Dropdown은 Header 전용)·투명도 게이지바(SWSlider)와, SW에 없는 컨트롤인 HM UI Switch·Button을 함께 배치합니다. 기존 SWHeaderPropertyPanel도 children을 받습니다.">
|
||||
@@ -188,7 +187,7 @@ function StackPage() {
|
||||
const rows = Array.from({ length: 30 }, (_, index) => index + 1)
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="여러 Popup · Escape · 스크롤"
|
||||
<StoryPageHeader title="여러 Popup · Escape · 스크롤"
|
||||
description="Popup 안의 Escape는 그 Popup만 닫습니다. 바깥의 Escape는 가장 최근에 열리거나 사용한 Popup 하나만 닫습니다. Dropdown·편집 중인 Input이 처리한 Escape로는 닫히지 않습니다. 긴 콘텐츠는 본문이 스크롤됩니다." />
|
||||
{log}
|
||||
<SWTheme className="sw-story-popups" data-stack="">
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ViewerHeader: Story = {
|
||||
name: "Viewer Header",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — SWViewerHeader" />
|
||||
<StoryPageHeader title="SW Viewer — SWViewerHeader" />
|
||||
<StorySection title="기본형 (1440px, 도면 열림)">
|
||||
<ViewerScreen />
|
||||
</StorySection>
|
||||
@@ -278,7 +278,7 @@ export const ContentVariants: Story = {
|
||||
const [snap, setSnap] = useState(true)
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||||
<StoryPageHeader title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||||
<Log lines={demo.log} />
|
||||
<ViewerFrame height={760} header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<LayerPanel demo={demo} strategy="absolute" defaultPosition={{ left: 24, top: 16 }} />
|
||||
@@ -333,7 +333,7 @@ export const Responsive: Story = {
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 화면 너비" />
|
||||
<StoryPageHeader title="SW Viewer — 화면 너비" />
|
||||
<Log lines={demo.log} />
|
||||
<div className="sw-story-stack">
|
||||
{[1920, 1440, 1280, 950, 860, 760, 520].map(width => (
|
||||
@@ -372,7 +372,7 @@ export const CompareWithSWHeader: Story = {
|
||||
name: "SWHeader vs Viewer Header",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 일반 SW Header와 비교" />
|
||||
<StoryPageHeader title="SW Viewer — 일반 SW Header와 비교" />
|
||||
<StorySection title="나란히 비교 (1440px)">
|
||||
<div className="sw-story-stack">
|
||||
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWHeader documentTitle="C0060202-001-평면및종단면도" /></div></div>
|
||||
@@ -397,7 +397,7 @@ export const Logo: Story = {
|
||||
]
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 로고" />
|
||||
<StoryPageHeader title="SW Viewer — 로고" />
|
||||
<div className="sw-story-stack">
|
||||
{rows.map(row => (
|
||||
<StorySection key={row.title} title={row.title}>
|
||||
|
||||
Reference in new issue
Block a user