diff --git a/docs/components/accordion.md b/docs/components/accordion.md index 613185a..8f39859 100644 --- a/docs/components/accordion.md +++ b/docs/components/accordion.md @@ -11,7 +11,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Accordion (Radix `Accordion`) | -| 대표 Story | `Components/Disclosure/Accordion` → `All` (이름 "Accordion") | +| 대표 Story | `Components/Disclosure/Accordion` → `All` (이름 "Accordion", 사이드바 `접기·펼치기`, Collapsible 예제 포함) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/alert-dialog.md b/docs/components/alert-dialog.md index 4c3d3e5..197847d 100644 --- a/docs/components/alert-dialog.md +++ b/docs/components/alert-dialog.md @@ -11,7 +11,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Alert Dialog (Radix `AlertDialog`) | -| 대표 Story | `Components/Overlay/Alert Dialog` → `All` (이름 "Alert Dialog") | +| 대표 Story | `Components/Overlay/Dialog` → `All`의 "Alert Dialog" 섹션(사이드바 `대화상자`). 기존 `Components/Overlay/Alert Dialog` → `All`(`components-overlay-alert-dialog--all`)은 URL 직접 접근용으로 유지(사이드바 숨김) | | 보류 | 아래 §11 | ## 2. 적용 근거 @@ -75,7 +75,8 @@ Radix AlertDialog 구조 전부: Portal·Overlay·modal·focus trap·초기 focu ## 9. Storybook -- 대표 Story `All`("Alert Dialog"): 트리거 2개(로그아웃 = 일반 확인, 삭제 = destructive)가 각각 실제 Alert Dialog를 연다. +- 사이드바 통합(2026-11-12): 사이드바에는 `대화상자`(Dialog 대표 Story) 하나만 보이고, 그 화면의 "Alert Dialog" 섹션에서 트리거 2개(로그아웃 = 일반 확인, 삭제 = destructive)가 각각 실제 Alert Dialog를 연다. 예제는 공용 파일 `src/components/alert-dialog-examples.tsx`(`AlertDialogConfirmation`, `AlertDialogDestructive`)에 있고 두 Story 파일이 함께 쓴다. +- 이 파일의 `All`("Alert Dialog", 같은 트리거 2개)은 `verification` 태그로 사이드바에서만 숨기며 ID·URL은 그대로다. - verification: `Confirmation`, `Destructive`(처음부터 열림). ## 10. 사용자 확정 결정 diff --git a/docs/components/breadcrumb.md b/docs/components/breadcrumb.md index d0280c7..0779134 100644 --- a/docs/components/breadcrumb.md +++ b/docs/components/breadcrumb.md @@ -69,7 +69,7 @@ Component Mapping 위치: `packages/ui/src/theme/breadcrumb.css` (`--hm-breadcru ## 9. Storybook -- 대표 Story `All`("Breadcrumb"): Variants(Basic(3 depth), Ellipsis(Dropdown Menu 조합), Custom link(asChild)와 custom separator)를 PC 균등 3열로, Usage(Long path, 최대 900px)를 한 페이지에 둔다(2026-11-09 Overview 제거). +- 대표 Story `All`("Breadcrumb"): Variants(Basic(3 depth), Ellipsis(Dropdown Menu 조합), Custom(asChild link와 custom separator))를 iframe 1000px 이상 균등 3열·미만 1열로, Usage(Long path, 섹션 전체 너비, 좁으면 자연 줄바꿈)를 한 페이지에 둔다(2026-11-09 Overview 제거, 2026-11-13 Custom 명칭·1/3열·900px 제한 제거). - verification: `Basic`, `Ellipsis`, `Long Path`. ## 10. 사용자 확정 결정 diff --git a/docs/components/collapsible.md b/docs/components/collapsible.md index f28a717..d418e4f 100644 --- a/docs/components/collapsible.md +++ b/docs/components/collapsible.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Collapsible (Radix `Collapsible`) | -| 대표 Story | `Components/Disclosure/Collapsible` → `All` (이름 "Collapsible") | +| 대표 Story | `Components/Disclosure/Accordion` → `All`의 "Collapsible · …" 섹션(사이드바 `접기·펼치기`). 기존 `Components/Disclosure/Collapsible` → `All`(`components-disclosure-collapsible--all`)은 URL 직접 접근용으로 유지(사이드바 숨김) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/context-menu.md b/docs/components/context-menu.md index f5caa1f..328f060 100644 --- a/docs/components/context-menu.md +++ b/docs/components/context-menu.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-03 | | 기반 | shadcn/ui Context Menu (Radix `ContextMenu`, `radix-ui` 1.6.7 / `@radix-ui/react-context-menu` 2.3.7) | -| 대표 Story | `Components/Overlay/Context Menu` → `All` (이름 "Context Menu") | +| 대표 Story | `Components/Overlay/Dropdown Menu` → `All`의 "Context Menu" 섹션(사이드바 `팝업 메뉴`). 기존 `Components/Overlay/Context Menu` → `All`(`components-overlay-context-menu--all`)은 URL 직접 접근용으로 유지(사이드바 숨김) | | 보류 | 아래 §11 | ## 2. 적용 근거 @@ -61,7 +61,8 @@ Dropdown Menu와 동일: shadcn 시각 class(`rounded`, `shadow`, `p-1`, `focus: ## 9. Storybook -- 대표 Story `All`("Context Menu"): 점선 영역을 우클릭하면 전체 메뉴가 열린다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive). +- 사이드바 통합(2026-11-12): 사이드바에는 `팝업 메뉴`(Dropdown Menu 대표 Story) 하나만 보이고, 그 화면의 "Context Menu" 섹션에서 점선 영역을 우클릭하면 전체 메뉴가 열린다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive). 예제는 공용 파일 `src/components/context-menu-examples.tsx`(`ContextMenuFull`)에 있고 두 Story 파일이 함께 쓴다. +- 이 파일의 `All`("Context Menu", 같은 점선 영역)은 `verification` 태그로 사이드바에서만 숨기며 ID·URL은 그대로다. - verification: `Open`(로드 시 합성 `contextmenu` 이벤트로 열린 상태, fullscreen). ## 10. 사용자 확정 결정 diff --git a/docs/components/dialog.md b/docs/components/dialog.md index 2b03b6b..a37b62d 100644 --- a/docs/components/dialog.md +++ b/docs/components/dialog.md @@ -13,7 +13,7 @@ | 구현 상태 | HM 스타일 적용 완료 (``만으로 적용, `hm-dialog` class 없음) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Dialog + Radix `Dialog` | -| 대표 Story | `Components/Overlay/Dialog` → `All` (이름 "Dialog") | +| 대표 Story | `Components/Overlay/Dialog` → `All` (이름 "Dialog", 사이드바 `대화상자`, Alert Dialog 예제 포함) | | 보류 | Overlay 수치, Close 버튼 규격, 초기 focus 대상, 좁은 화면 Actions 배치 등 (§11) | ## 2. 적용 근거 @@ -111,7 +111,8 @@ Radix 구조 전부를 그대로 둔다: Portal(`DialogPortal`), Overlay, modal ## 9. Storybook -- 대표 Story `All`("Dialog")이 기본 sidebar의 유일한 Dialog 항목이다 → 1뎁스. overlay라 StateBoard를 쓰지 않고, 트리거 버튼 6개(기본, 긴 내용, 넓은 표, 본문 스크롤, 삭제 확인, 폼)가 각각 실제 Dialog를 연다. +- 대표 Story `All`("Dialog")이 기본 sidebar의 유일한 Dialog 항목이다 → 1뎁스. 트리거마다 단일 열 StateBoard(SpecimenGrid)에 두며, Variants(기본, 긴 내용, 넓은 표, 본문 스크롤)·Usage(삭제 확인, 폼)·Alert Dialog(확인, 삭제) 3구역의 트리거가 각각 실제 대화상자를 연다. +- 사이드바 통합(2026-11-12): 같은 대표 Story에 "Alert Dialog" 섹션(로그아웃 = 일반 확인, 삭제 = destructive)을 두어 사이드바 `대화상자` 하나에서 일반·확인·경고 대화상자를 모두 연다. 예제는 공용 파일 `src/components/alert-dialog-examples.tsx`를 Alert Dialog Story와 함께 쓴다(중복 구현 없음). Alert Dialog Story는 ID·URL을 유지하고 사이드바에서만 숨긴다([alert-dialog.md §9](alert-dialog.md#9-storybook)). - 본문 스크롤은 ui_viz "세로 스크롤 · 본문만"과 같은 조건(앱 공통 Header 70px / Footer 48px 제외)이다. Portal 때문에 앱 높이는 trigger wrapper가 아니라 `DialogContent`의 `style`로 `--hm-dialog-app-header-height`/`--hm-dialog-app-footer-height`를 넘긴다. - verification(`tags: ["verification"]`, 닫힌 채가 아니라 처음부터 열림): `Open`, `Long Content`, `Wide Content`, `Body Scroll`, `Delete Confirmation`. - pseudo-state helper로 열린 상태를 흉내 내지 않는다. diff --git a/docs/components/drawer.md b/docs/components/drawer.md index 1203a70..b072c96 100644 --- a/docs/components/drawer.md +++ b/docs/components/drawer.md @@ -11,7 +11,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Drawer = **vaul** (`vaul@1.1.2`, 이번에 `@hm-design/ui` dependency로 추가) + Radix Dialog | -| 대표 Story | `Components/Overlay/Drawer` → `All` (이름 "Drawer") | +| 대표 Story | `Components/Overlay/Sheet` → `All`의 "Drawer" 섹션(사이드바 `시트·드로어`). 기존 `Components/Overlay/Drawer` → `All`(`components-overlay-drawer--all`)은 URL 직접 접근용으로 유지(사이드바 숨김) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/dropdown-menu.md b/docs/components/dropdown-menu.md index 2bc8b64..3b67569 100644 --- a/docs/components/dropdown-menu.md +++ b/docs/components/dropdown-menu.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Dropdown Menu (Radix `DropdownMenu`) | -| 대표 Story | `Components/Overlay/Dropdown Menu` → `All` (이름 "Dropdown Menu") | +| 대표 Story | `Components/Overlay/Dropdown Menu` → `All` (이름 "Dropdown Menu", 사이드바 `팝업 메뉴`, Context Menu 예제 포함) | | 보류 | 아래 §11 | ## 2. 적용 근거 @@ -75,6 +75,7 @@ Radix 구조·ARIA(`role=menu`, `menuitem*`)·키보드(방향키, Home/End, Ent ## 9. Storybook - 대표 Story `All`("Dropdown Menu"): 트리거로 실제 메뉴를 연다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive). +- 사이드바 통합(2026-11-12): 사이드바 표시명은 `팝업 메뉴`이고, 접근성용 숨김 H1 "Dropdown Menu · Context Menu"(화면에는 보이지 않음) 아래 "Dropdown Menu"와 "Context Menu" 섹션을 함께 둔다. Context Menu 예제는 공용 파일 `src/components/context-menu-examples.tsx`를 Context Menu Story와 함께 쓴다(중복 구현 없음). Context Menu Story는 ID·URL을 유지하고 사이드바에서만 숨긴다([context-menu.md §9](context-menu.md#9-storybook)). - verification: `Open`(열린 상태, fullscreen). ## 10. 사용자 확정 결정 diff --git a/docs/components/hover-card.md b/docs/components/hover-card.md index 9f1ce92..4e22d19 100644 --- a/docs/components/hover-card.md +++ b/docs/components/hover-card.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Hover Card (Radix `HoverCard`) | -| 대표 Story | `Components/Overlay/Hover Card` → `All` (이름 "Hover Card") | +| 대표 Story | `Components/Overlay/Tooltip` → `All`의 "Hover Card" 섹션(사이드바 `툴팁·호버 카드`). 기존 `Components/Overlay/Hover Card` → `All`(`components-overlay-hover-card--all`)은 URL 직접 접근용으로 유지(사이드바 숨김) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/navigation-menu.md b/docs/components/navigation-menu.md index 0560dab..7bd993e 100644 --- a/docs/components/navigation-menu.md +++ b/docs/components/navigation-menu.md @@ -72,8 +72,8 @@ Radix 구조·동작(hover open delay, Arrow 키와 Tab 이동, Escape 닫힘, f ## 9. Storybook -- 대표 Story `All`("Navigation Menu"): Variants 한 구역에 `Viewport(기본)`과 `viewport={false}` 두 구성을 PC 균등 2열로 둔다(2026-11-09 Overview 제거, 닫힌 메뉴용 min-height 256px는 PC에서만 해제하고 1000px 미만 1열에서는 유지). 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger). -- verification: `Default`, `No Viewport`. +- 대표 Story `All`("Navigation Menu"): Variants 한 구역에 "Viewport"(기본)와 "No viewport"(`viewport={false}`) 두 구성을 PC 균등 2열로 둔다(2026-11-09 Overview 제거, 닫힌 메뉴용 min-height 256px는 PC에서만 해제하고 1000px 미만 1열에서는 유지). 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger). +- verification: `Default`, `No Viewport`, `Indicator Alignment`. - Story의 Content 폭은 `style` 인라인으로 지정한다(새 Story 파일의 Tailwind 임의값 클래스가 개발 서버에서 생성되지 않는 일이 있었다). ## 10. 사용자 확정 결정 diff --git a/docs/components/sheet.md b/docs/components/sheet.md index d5581b5..3654e44 100644 --- a/docs/components/sheet.md +++ b/docs/components/sheet.md @@ -11,7 +11,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Sheet (Radix `Dialog`) | -| 대표 Story | `Components/Overlay/Sheet` → `All` (이름 "Sheet") | +| 대표 Story | `Components/Overlay/Sheet` → `All` (이름 "Sheet", 사이드바 `시트·드로어`, Drawer 예제 포함) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/sonner.md b/docs/components/sonner.md index 8142f7b..ab57224 100644 --- a/docs/components/sonner.md +++ b/docs/components/sonner.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-03 | | 기반 | shadcn/ui Sonner (`sonner` 2.0.8) | -| 대표 Story | `Components/Feedback/Sonner` → `All` (이름 "Toast") | +| 대표 Story | `Components/Feedback/Sonner` → `All` (이름 "Sonner", 사이드바 `토스트 알림`) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/docs/components/tabs.md b/docs/components/tabs.md index 63d12b3..bf75873 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -32,7 +32,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui" | 구현 상태 | 기본 Primary Fill + 명시적 Secondary Fill + Legacy non-fill + 넘침 이동 버튼(D-090) 구현 (`hm-tab` class 없음) | | 기준일 | 2026-10-09 (D-01 조합·D-02 상단 White 80% 반영, API는 2026-10-08 기본값 유지) | | 기반 | shadcn/ui Tabs (Radix `Tabs`) | -| 대표 Story | `Components/Navigation/Tabs` → `All` (이름 "Tabs"): Primary(Fill + content) / Secondary(Fill + surface) | +| 대표 Story | `Components/Navigation/Tabs` → `All` (이름 "Tabs"): Variants Primary(Fill + content) / Secondary(Fill + surface), Behavior Overflow, Usage | | 보류 | 아래 §11 | ## 2. 적용 근거 @@ -126,13 +126,13 @@ Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving ta ## 9. Storybook -- 대표 Story `All`("Tabs"): **Variants**는 Primary(fill / panelSurface를 생략한 기본)·Secondary(`panelSurface="surface"`)·Overflow(20개 탭, 이전 States의 Overflow 예시)를 PC 균등 3열로 두고, **Usage**는 Card 내부 · Secondary / 페이지 직접 배치 · Primary 2열이다(2026-11-09 Overview·States 구역을 Variants로 통합, 이 페이지의 위아래 패딩만 줄여 1280×900 한 화면). 같은 Primary가 반복되던 **Default API** 구역(`fill`만, `fill` + `panelSurface="content"` 명시)은 2026-10-22 숨김 verification `DefaultApi`로 옮겼다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다. +- 대표 Story `All`("Tabs"): **Variants**는 Primary(fill / panelSurface를 생략한 기본)·Secondary(`panelSurface="surface"`) 균등 2열, **Behavior**는 Overflow(20개 탭, 이전 States의 Overflow 예시, 이전/다음 이동 버튼·내부 스크롤) 1개를 같은 2열 grid의 왼쪽 열에, **Usage**는 Card 내부 · Secondary / 페이지 직접 배치 · Primary 균등 2열이다. 세 구역은 같은 grid(24px 간격)를 쓰며, 섹션 본문의 실제 콘텐츠 너비(컨테이너 쿼리, 공통 board 들여쓰기 제외)가 720px 이상이면 2열, 미만이면 1열이다. 섹션 제목·예제 라벨·간격은 공통 `StorySection` / `StateBoard`를 따른다(2026-11-13 Overflow를 Variants에서 Behavior로 분리, 예제 내용 불변. 이전: 2026-11-09 Overview·States 구역을 Variants로 통합). 같은 Primary가 반복되던 **Default API** 구역(`fill`만, `fill` + `panelSurface="content"` 명시)은 2026-10-22 숨김 verification `DefaultApi`로 옮겼다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다. - verification(`tags: ["verification"]`): `DefaultApi`, `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다. - `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다. - non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10). - 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다. - Variants / Usage, `DefaultApi` 및 Fill verification 예제의 콘텐츠 panel은 `px-0 py-6`로 통일해 D-01을 실제로 반영한다. Primary는 상위 content 면, Secondary는 상위 surface 면에 연결하며 내부 패널의 별도 border·배경·radius를 제거한다. Compatibility의 Legacy non-fill 두 예제만 이전 `border bg-card rounded-b p-4`를 보존한다. -- **Usage**(2026-10-09 추가)는 공개 Card / Input / Textarea / Table을 조합해 Card 내부 Secondary와 페이지 content 면 위 Primary를 보여준다. 두 예제 모두 `TabsContent`는 `px-0 py-6`이며 별도 배경·border·radius를 추가하지 않는다. 대표 Story ID·기존 구역·verification Story는 유지한다. +- **Usage**(2026-10-09 추가)는 공개 Card / Input / Textarea / Table을 조합해 Card 내부 Secondary와 페이지 content 면 위 Primary를 보여준다. 두 예제 모두 `TabsContent`는 좌우 16px·상단 12px·하단 24px(`px-4 pt-3 pb-6`, 2026-11-13)이며 Card 예제의 Card / CardContent 여백은 그대로 둔다. 패널은 별도 배경·border·radius를 추가하지 않는다. 대표 Story ID·기존 구역·verification Story는 유지한다. ## 10. 사용자 확정 결정 diff --git a/docs/components/tooltip.md b/docs/components/tooltip.md index 0bb4822..cab72cf 100644 --- a/docs/components/tooltip.md +++ b/docs/components/tooltip.md @@ -9,7 +9,7 @@ | 구현 상태 | 구현 완료 (현재 HM 규격) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Tooltip (Radix `Tooltip`) | -| 대표 Story | `Components/Overlay/Tooltip` → `All` (이름 "Tooltip") | +| 대표 Story | `Components/Overlay/Tooltip` → `All` (이름 "Tooltip", 사이드바 `툴팁·호버 카드`, Hover Card 예제 포함) | | 보류 | 아래 §11 | ## 2. 적용 근거 diff --git a/src/components/accordion-story.css b/src/components/accordion-story.css new file mode 100644 index 0000000..3ae7b96 --- /dev/null +++ b/src/components/accordion-story.css @@ -0,0 +1,8 @@ +.sb-page.sb-accordion-story { + --sb-page-section-gap: 24px; +} + +.sb-accordion-story .sb-page-section-body > [data-sb-board] { + /* Keep empty tracks aligned and cap the shared board at three columns. */ + grid-template-columns: repeat(auto-fill, minmax(min(100%, max(320px, (100% - 48px) / 3)), 1fr)); +} diff --git a/src/components/accordion.stories.tsx b/src/components/accordion.stories.tsx index 7e9763e..1428f39 100644 --- a/src/components/accordion.stories.tsx +++ b/src/components/accordion.stories.tsx @@ -1,8 +1,16 @@ import type { ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hm-design/ui" -import { StateBoard, type BoardState } from "@/components/story-board" +import { StateBoard } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { + ClosedSpecimen as CollapsibleClosed, + ControlledSpecimen as CollapsibleControlled, + DisabledSpecimen as CollapsibleDisabled, + LongSpecimen as CollapsibleLong, + OpenSpecimen as CollapsibleOpen, +} from "./collapsible-examples" +import "./accordion-story.css" // 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 @@ -102,45 +110,44 @@ function LongSpecimen() { ) } -const sections = [ - { id: "single", label: "Single", node: }, - { id: "collapsible-single", label: "Collapsible Single", node: }, - { id: "multiple", label: "Multiple", node: }, - { id: "disabled", label: "Disabled Item", node: }, - { id: "long", label: "Long Content", node: }, -] - // No `component` here: Accordion props are a union (single | multiple), so Stories without args would not type-check. const meta = { title: "Components/Disclosure/Accordion", parameters: { layout: "padded" } } satisfies Meta export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), one column -// below that. Each specimen sits in its own single-column StateBoard (min larger than any column), so the title, divider and -// specimen start line up, unused columns stay empty and `items-start` lets an opened item grow without touching its neighbors. -function StateGrid({ states, cols }: { states: BoardState[]; cols: 2 | 3 }) { - return ( -
- {states.map((state) => ( - - ))} -
- ) -} - -const board = (cols: 2 | 3, ...ids: string[]) => ( - ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: s.node }))} /> -) - -// Representative Story: the common Story page; the sections group the same specimens. +// The combined disclosure page uses the same specimens as the separate verification Stories. export const All: Story = { name: "Accordion", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - + - {board(3, "single", "collapsible-single", "multiple")} - {board(3, "disabled", "long")} + + }, + { id: "collapsible-single", label: "Collapsible Single", children: }, + { id: "multiple", label: "Multiple", children: }, + ]} /> + + + }, + ]} /> + + + }, + { id: "collapsible-open", label: "Open", children: }, + { id: "collapsible-disabled", label: "Disabled", children: }, + ]} /> + + + }, + { id: "collapsible-long", label: "Collapsible · Long Content", children: }, + { id: "collapsible-controlled", label: "Button trigger", children: }, + ]} /> + ), } diff --git a/src/components/alert-dialog-examples.tsx b/src/components/alert-dialog-examples.tsx new file mode 100644 index 0000000..eebb87c --- /dev/null +++ b/src/components/alert-dialog-examples.tsx @@ -0,0 +1,64 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, +} from "@hm-design/ui" + +// Shared Alert Dialog specimens (Storybook only). Used by the Alert Dialog Stories (verification) and by the Dialog representative +// page, which shows general and confirmation / warning dialogs under one sidebar item ("대화상자"). +// Alert Dialog is a modal Radix overlay (Portal) for a decision that must be confirmed or cancelled: no Close (X), no outside +// dismiss. Action / Cancel are the HM Button: both confirmation and deletion use a Solid pair +// (deletion pairs Cancel with a destructive Action). + +export function AlertDialogConfirmation({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + + + + + + + 로그아웃하시겠습니까? + 저장하지 않은 변경 사항은 사라집니다. + + + 취소 + 로그아웃 + + + + ) +} + +export function AlertDialogDestructive({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + + + + + + + 프로젝트를 삭제하시겠습니까? + 삭제하면 되돌릴 수 없습니다. 프로젝트의 모든 데이터가 함께 삭제됩니다. + + + 취소 + + 삭제 + + + + + ) +} diff --git a/src/components/alert-dialog.stories.tsx b/src/components/alert-dialog.stories.tsx index 70a70fa..9828434 100644 --- a/src/components/alert-dialog.stories.tsx +++ b/src/components/alert-dialog.stories.tsx @@ -1,76 +1,19 @@ import type { Meta, StoryObj } from "@storybook/react-vite" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - Button, -} from "@hm-design/ui" +import { AlertDialog } from "@hm-design/ui" import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { AlertDialogConfirmation as Confirmation, AlertDialogDestructive as Destructive } from "./alert-dialog-examples" // 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. -// Action / Cancel are the HM Button: ordinary confirmation uses a Solid pair; deletion uses an Outline pair. - -function Confirmation({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - - - - - - - 로그아웃하시겠습니까? - 저장하지 않은 변경 사항은 사라집니다. - - - 취소 - 로그아웃 - - - - ) -} - -function Destructive({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - - - - - - - 프로젝트를 삭제하시겠습니까? - 삭제하면 되돌릴 수 없습니다. 프로젝트의 모든 데이터가 함께 삭제됩니다. - - - 취소 - - 삭제 - - - - - ) -} +// dismiss. The specimens live in ./alert-dialog-examples and are also shown on the Dialog representative page, which is the +// single sidebar item for both ("대화상자"). Every Story here keeps its ID and direct URL but is hidden from the sidebar. const meta = { title: "Components/Overlay/Alert Dialog", component: AlertDialog, parameters: { layout: "centered" } } satisfies Meta export default meta type Story = StoryObj -// Representative Story: the common Story page; each trigger opens the real dialog. // Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider +// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle // has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its // own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { @@ -83,8 +26,10 @@ function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: ) } -// Representative Story: Overview and Variants are merged into one Variants section. +// Former representative Story: kept for its ID and direct URL; hidden from the sidebar (tag `verification`) since the Dialog page +// shows the same specimens. export const All: Story = { + tags: ["verification"], name: "Alert Dialog", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( diff --git a/src/components/breadcrumb.stories.tsx b/src/components/breadcrumb.stories.tsx index f372a3b..9b8a6b7 100644 --- a/src/components/breadcrumb.stories.tsx +++ b/src/components/breadcrumb.stories.tsx @@ -99,7 +99,7 @@ function CustomLink() { - 홈 (custom link) + 홈 / @@ -115,15 +115,15 @@ const sections = [ { id: "basic", label: "Basic", node: }, { id: "ellipsis", label: "Ellipsis", node: }, { id: "long", label: "Long path", node: }, - { id: "custom", label: "Custom link · separator", node: }, + { id: "custom", label: "Custom", node: }, ] const meta = { title: "Components/Navigation/Breadcrumb", component: Breadcrumb, parameters: { layout: "padded" } } satisfies Meta export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): Variants use 3 equal columns on PC (iframe width 1000px and up), -// fewer below that (240px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so +// Representative-page layout (Storybook presentation only): Variants use 3 equal columns on PC (iframe width 1000px and up) +// and 1 column below that (2026-11-13, same 1 → 3 pattern as Accordion / Collapsible, so no 2 + 1 row). Each specimen sits in its own single-column StateBoard (min larger than any column), so // StateBoard's auto-fill grid cannot leave an empty or stretched track. const specimen = (id: string, children?: ReactNode): BoardState => { const s = sections.find((section) => section.id === id)! @@ -131,8 +131,8 @@ const specimen = (id: string, children?: ReactNode): BoardState => { } // Representative Story: the common Story page; the sections group the same specimens. -// Overview is merged into Variants (Basic is its first item). The Long path example gets up to 900px so it fits on one line on -// PC; BreadcrumbList is `flex-wrap`, so it wraps naturally (never clipped or forced nowrap) when the space is narrower. +// Overview is merged into Variants (Basic is its first item). Usage (Long path) takes the full section width (2026-11-13, the +// former 900px cap is removed); BreadcrumbList is `flex-wrap`, so it wraps naturally (never clipped or forced nowrap) when narrower. export const All: Story = { name: "Breadcrumb", parameters: { layout: "fullscreen", controls: { disable: true } }, @@ -140,14 +140,14 @@ export const All: Story = { -
+
{["basic", "ellipsis", "custom"].map((id) => ( ))}
- {sections.find((s) => s.id === "long")!.node}
)]} /> + {sections.find((s) => s.id === "long")!.node})]} />
), diff --git a/src/components/collapsible-examples.tsx b/src/components/collapsible-examples.tsx new file mode 100644 index 0000000..34560f3 --- /dev/null +++ b/src/components/collapsible-examples.tsx @@ -0,0 +1,96 @@ +import { useState, type ReactNode } from "react" +import { ChevronDownIcon } from "lucide-react" +import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@hm-design/ui" +import { StateBoard, type BoardState } from "@/components/story-board" +import { StorySection } from "@/components/story-page" + +// Shared Collapsible specimens (Storybook only). Used by the Collapsible Stories (verification) and by the Accordion +// representative page, which shows Accordion and Collapsible under one sidebar item ("접기·펼치기"). +// One independent disclosure per state with real, clickable triggers. Collapsible is a single disclosure: no divider system +// and no animation. + +const longText = "긴 텍스트는 줄바꿈으로 전체를 표시합니다. 공백 없는 긴 문자열도 너비를 밀어내지 않고 줄바꿈합니다. ".repeat(6) + +const narrow = (children: ReactNode) =>
{children}
+ +function Frame({ label, defaultOpen, disabled, children }: { label: string; defaultOpen?: boolean; disabled?: boolean; children: ReactNode }) { + return narrow( + + + {label} + + + {children} + + ) +} + +export const ClosedSpecimen = () => 추가 옵션이 여기에 표시됩니다. +export const OpenSpecimen = () => ( + + 처음부터 열려 있는 영역입니다. + +) +export const DisabledSpecimen = () => ( + + 열 수 없습니다. + +) +export const LongSpecimen = () => ( + +

{longText}

+

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

+ +) + +// Controlled: the parent owns `open`; an HM Button is the trigger through asChild and keeps its style. +export function ControlledSpecimen() { + const [open, setOpen] = useState(false) + return narrow( + + + + + {/* 2026-11-13: sample content instead of the former `controlled: open = …` debug text; the trigger label shows the state. */} + 펼친 영역의 내용입니다. + + ) +} + +const sections = [ + { id: "closed", label: "Closed", node: }, + { id: "open", label: "Open", node: }, + { id: "disabled", label: "Disabled", node: }, + { id: "long", label: "Long Content", node: }, + { id: "controlled", label: "Button trigger", node: }, +] + +// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), one column +// below that. Each specimen sits in its own single-column StateBoard (min larger than any column), so the title and +// specimen start line up, unused columns stay empty and `items-start` lets an opened item grow without touching its neighbors. +function StateGrid({ states, cols }: { states: BoardState[]; cols: 2 | 3 }) { + return ( +
+ {states.map((state) => ( + + ))} +
+ ) +} + +// The Collapsible sections of a representative page. `titlePrefix` names the component in each section title and `idPrefix` +// keeps the specimen ids (data-overview-state) unique when the sections share a page with other components. +export function CollapsibleSections({ titlePrefix, idPrefix = "" }: { titlePrefix?: string; idPrefix?: string }) { + const title = (name: string) => (titlePrefix ? `${titlePrefix} · ${name}` : name) + const board = (cols: 2 | 3, ...ids: string[]) => ( + ids.includes(s.id)).map((s) => ({ id: `${idPrefix}${s.id}`, label: s.label, children: s.node }))} /> + ) + return ( + <> + {board(3, "closed", "open", "disabled")} + {board(3, "long", "controlled")} + + ) +} diff --git a/src/components/collapsible.stories.tsx b/src/components/collapsible.stories.tsx index 0e47d4d..aa90e69 100644 --- a/src/components/collapsible.stories.tsx +++ b/src/components/collapsible.stories.tsx @@ -1,101 +1,26 @@ -import { useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" -import { ChevronDownIcon } from "lucide-react" -import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from "@hm-design/ui" -import { StateBoard, type BoardState } from "@/components/story-board" -import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { Collapsible } from "@hm-design/ui" +import { StoryPage, StoryPageHeader } from "@/components/story-page" +import { ClosedSpecimen, CollapsibleSections, DisabledSpecimen, LongSpecimen, OpenSpecimen } from "./collapsible-examples" -// 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 -// system and no animation. - -const longText = "긴 텍스트는 줄바꿈으로 전체를 표시합니다. 공백 없는 긴 문자열도 너비를 밀어내지 않고 줄바꿈합니다. ".repeat(6) - -const narrow = (children: ReactNode) =>
{children}
- -function Frame({ label, defaultOpen, disabled, children }: { label: string; defaultOpen?: boolean; disabled?: boolean; children: ReactNode }) { - return narrow( - - - {label} - - - {children} - - ) -} - -const ClosedSpecimen = () => 추가 옵션이 여기에 표시됩니다. -const OpenSpecimen = () => ( - - 처음부터 열려 있는 영역입니다. - -) -const DisabledSpecimen = () => ( - - 열 수 없습니다. - -) -const LongSpecimen = () => ( - -

{longText}

-

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

- -) - -// Controlled: the parent owns `open`; an HM Button is the trigger through asChild and keeps its style. -function ControlledSpecimen() { - const [open, setOpen] = useState(false) - return narrow( - - - - - {`controlled: open = ${String(open)}`} - - ) -} - -const sections = [ - { id: "closed", label: "Closed", node: }, - { id: "open", label: "Open", node: }, - { id: "disabled", label: "Disabled", node: }, - { id: "long", label: "Long Content", node: }, - { id: "controlled", label: "Button trigger", node: }, -] +// Collapsible is a single disclosure. The specimens live in ./collapsible-examples and are also shown on the Accordion +// representative page ("접기·펼치기"), the single sidebar item for both. Every Story here keeps its ID and direct URL but is +// hidden from the sidebar (tag `verification`). const meta = { title: "Components/Disclosure/Collapsible", component: Collapsible, parameters: { layout: "padded" } } satisfies Meta export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), one column -// below that. Each specimen sits in its own single-column StateBoard (min larger than any column), so the title, divider and -// specimen start line up, unused columns stay empty and `items-start` lets an opened item grow without touching its neighbors. -function StateGrid({ states, cols }: { states: BoardState[]; cols: 2 | 3 }) { - return ( -
- {states.map((state) => ( - - ))} -
- ) -} - -const board = (cols: 2 | 3, ...ids: string[]) => ( - ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: s.node }))} /> -) - -// Representative Story: the common Story page; the sections group the same specimens. +// Former representative Story: kept for its ID and direct URL; hidden from the sidebar (tag `verification`) since the Accordion +// page shows the same sections. export const All: Story = { + tags: ["verification"], name: "Collapsible", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - {board(3, "closed", "open", "disabled")} - {board(3, "long", "controlled")} + ), } diff --git a/src/components/command.stories.tsx b/src/components/command.stories.tsx index 77b557b..01b51e3 100644 --- a/src/components/command.stories.tsx +++ b/src/components/command.stories.tsx @@ -59,7 +59,7 @@ function Grouped() { 환경설정 ⌘, - 결제 (사용 불가) + 결제 @@ -80,7 +80,7 @@ function Long() { ) } -// Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles, dividers and +// Same 3-column track as the Form pages (1 column on narrow screens). Each cell is a one-specimen StateBoard so subtitles and // command boxes line up; sections with fewer specimens leave the remaining tracks empty. const Grid = ({ children }: { children: ReactNode }) => (
{children}
diff --git a/src/components/context-menu-examples.tsx b/src/components/context-menu-examples.tsx new file mode 100644 index 0000000..cf3186e --- /dev/null +++ b/src/components/context-menu-examples.tsx @@ -0,0 +1,72 @@ +import { useState } from "react" +import { + ContextMenu, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuGroup, + ContextMenuItem, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from "@hm-design/ui" + +// Shared Context Menu specimen (Storybook only). Used by the Context Menu Stories (verification) and by the Dropdown Menu +// representative page, which shows both menus under one sidebar item ("팝업 메뉴"). +// Context Menu opens from pointer context (right click, the context-menu key / Shift+F10, long press) on its trigger area. +// Radix owns keyboard navigation, focus return and Escape. +export function ContextMenuFull() { + const [showStatus, setShowStatus] = useState(true) + const [showPanel, setShowPanel] = useState(false) + const [position, setPosition] = useState("bottom") + return ( + + {/* tabIndex lets keyboard users focus the area and open the menu with Shift+F10 / the context-menu key; Radix Trigger is a plain span. */} + + 이 영역에서 오른쪽 클릭 + + + 파일 + + + 열기 ⌘O + + + 이름 바꾸기 F2 + + 붙여넣기 + + + 보기 + + 상태 표시줄 + + + 패널 + + + 위치 + + 상단 + 하단 + 오른쪽 + + + + 더 보기 + + 이메일로 공유 + 링크 복사 + + + + 삭제 + + + ) +} diff --git a/src/components/context-menu.stories.tsx b/src/components/context-menu.stories.tsx index 7d73b46..22ab237 100644 --- a/src/components/context-menu.stories.tsx +++ b/src/components/context-menu.stories.tsx @@ -1,83 +1,21 @@ -import { useState } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" -import { - ContextMenu, - ContextMenuCheckboxItem, - ContextMenuContent, - ContextMenuGroup, - ContextMenuItem, - ContextMenuLabel, - ContextMenuRadioGroup, - ContextMenuRadioItem, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuTrigger, -} from "@hm-design/ui" +import { ContextMenu } from "@hm-design/ui" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { ContextMenuFull as Full } from "./context-menu-examples" // 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. - -function Full() { - const [showStatus, setShowStatus] = useState(true) - const [showPanel, setShowPanel] = useState(false) - const [position, setPosition] = useState("bottom") - return ( - - {/* tabIndex lets keyboard users focus the area and open the menu with Shift+F10 / the context-menu key; Radix Trigger is a plain span. */} - - 이 영역에서 오른쪽 클릭 - - - 파일 - - - 열기 ⌘O - - - 이름 바꾸기 F2 - - 붙여넣기 (사용 불가) - - - 보기 - - 상태 표시줄 - - - 패널 - - - 위치 - - 상단 - 하단 - 오른쪽 - - - - 더 보기 - - 이메일로 공유 - 링크 복사 - - - - 삭제 - - - ) -} +// The specimen lives in ./context-menu-examples and is also shown on the Dropdown Menu representative page, which is the single +// sidebar item for both ("팝업 메뉴"). Every Story here keeps its ID and direct URL but is hidden from the sidebar. const meta = { title: "Components/Overlay/Context Menu", component: ContextMenu, parameters: { layout: "centered" } } satisfies Meta export default meta type Story = StoryObj -// Representative Story: right click the area to open the full menu (item, disabled, separator, shortcut, checkbox, radio group, sub menu, destructive). +// Former representative Story: kept for its ID and direct URL; hidden from the sidebar (tag `verification`) since the Dropdown Menu +// page shows the same specimen. Right click the area to open the full menu (item, disabled, separator, shortcut, checkbox, radio +// group, sub menu, destructive). export const All: Story = { + tags: ["verification"], name: "Context Menu", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( diff --git a/src/components/dialog.stories.tsx b/src/components/dialog.stories.tsx index ac1e896..a3c74b8 100644 --- a/src/components/dialog.stories.tsx +++ b/src/components/dialog.stories.tsx @@ -6,8 +6,9 @@ import { Input } from "@hm-design/ui" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@hm-design/ui" import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { AlertDialogConfirmation, AlertDialogDestructive } from "./alert-dialog-examples" -// Dialog is an overlay (Radix Portal), so it does not fit the StateBoard. The representative Story is a row of triggers that open +// Dialog is an overlay (Radix Portal): each trigger sits in its own single-column StateBoard (SpecimenGrid) and opens // the real Dialog for each ui_viz case (dialog.md §9 Reference Viewer examples); the same cases also exist as separate Stories tagged // `verification` that start open. Pseudo-state helpers are not used to fake an open Dialog. // Usage: native label below is a preserved Reference fixture. New HM React fields use public Label @@ -176,16 +177,15 @@ export default meta type Story = StoryObj // Representative Story: the common Story page; every case opens from its trigger. Named after the component, as in ui_viz. -// It is the only Dialog entry in the default sidebar, so Storybook shows Dialog as a single 1-depth item. -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { +// It is the only Dialog entry in the default sidebar, so Storybook shows Dialog as a single 1-depth item. It also shows the Alert +// Dialog (confirmation / warning) specimens from ./alert-dialog-examples, so "대화상자" is the one sidebar item for both; the Alert +// Dialog Stories keep their IDs and direct URLs but are hidden from the sidebar. +// Keep the board's accessible headings and shared indent, with only the triggers visible in the four-column grid. +function SpecimenGrid({ states }: { states: BoardState[] }) { return ( -
+
{states.map((state) => ( - {state.children}
}]} /> + {state.children}
}]} /> ))} ) @@ -216,6 +216,14 @@ export const All: Story = { ]} /> + + }, + { id: "alert-destructive", label: "Destructive", children: }, + ]} + /> +
), } diff --git a/src/components/drawer-examples.tsx b/src/components/drawer-examples.tsx new file mode 100644 index 0000000..cfcc4cb --- /dev/null +++ b/src/components/drawer-examples.tsx @@ -0,0 +1,127 @@ +import type { ReactNode } from "react" +// 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 { StateBoard, type BoardState } from "@/components/story-board" +import { StorySection } from "@/components/story-page" + +// Shared Drawer specimens (Storybook only). Used by the Drawer Stories (verification) and by the Sheet representative page, +// which shows Sheet and Drawer under one sidebar item ("시트·드로어"). +// Drawer is a modal vaul overlay (Portal). The sections open 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. + +const longText = "긴 내용은 Body 영역만 세로로 scroll 하고 Header와 Footer는 그대로 보입니다. " + +function Frame({ + trigger, + defaultOpen, + title, + description, + children, + actions, +}: { + trigger: string + defaultOpen?: boolean + title: string + description?: string + children: ReactNode + actions?: ReactNode +}) { + return ( + + + + + + + {title} + {description ? {description} : null} + + {children} + {actions} + + + ) +} + +const actions = ( + <> + + + + + + + +) + +export function DrawerBasic({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + +

짧은 내용은 내용 높이만큼만 차지합니다.

+ + ) +} + +export function DrawerForm({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + +
+ + +
+ + ) +} + +export function DrawerLong({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + + {Array.from({ length: 30 }, (_, i) => ( +

+ {longText} +

+ ))} + + ) +} + +// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that +// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle +// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its +// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. +function SpecimenGrid({ states, hideSpecimenLabels }: { states: BoardState[]; hideSpecimenLabels: boolean }) { + return ( +
header]:sr-only" : ""}`}> + {states.map((state) => ( + {state.children}
}]} /> + ))} + + ) +} + +// The Drawer section of a representative page: Overview and Usage merged into one Usage section (Basic, Form, Long Content). +// On a shared page `title` names the section after the component (as "Alert Dialog" on the Dialog page) and `idPrefix` +// keeps the specimen ids (data-overview-state) unique. +export function DrawerSections({ title = "Usage", idPrefix = "", hideSpecimenLabels = false }: { title?: string; idPrefix?: string; hideSpecimenLabels?: boolean }) { + return ( + + }, + { id: `${idPrefix}form`, label: "Form", children: }, + { id: `${idPrefix}long-content`, label: "Long Content", children: }, + ]} + /> + + ) +} diff --git a/src/components/drawer.stories.tsx b/src/components/drawer.stories.tsx index e0c32df..c8836cc 100644 --- a/src/components/drawer.stories.tsx +++ b/src/components/drawer.stories.tsx @@ -1,135 +1,30 @@ 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 { StateBoard, type BoardState } from "@/components/story-board" -import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { Drawer } from "@hm-design/ui" +import { StoryPage, StoryPageHeader } from "@/components/story-page" +import { DrawerBasic, DrawerForm, DrawerLong, DrawerSections } from "./drawer-examples" -// 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. - -const longText = "긴 내용은 Body 영역만 세로로 scroll 하고 Header와 Footer는 그대로 보입니다. " - -function Frame({ - trigger, - defaultOpen, - title, - description, - children, - actions, -}: { - trigger: string - defaultOpen?: boolean - title: string - description?: string - children: React.ReactNode - actions?: React.ReactNode -}) { - return ( - - - - - - - {title} - {description ? {description} : null} - - {children} - {actions} - - - ) -} - -const actions = ( - <> - - - - - - - -) - -function Basic({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - -

짧은 내용은 내용 높이만큼만 차지합니다.

- - ) -} - -function FormCase({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - -
- - -
- - ) -} - -function LongCase({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - - {Array.from({ length: 30 }, (_, i) => ( -

- {longText} -

- ))} - - ) -} +// Drawer is a modal vaul overlay (Portal). The specimens live in ./drawer-examples and are also shown on the Sheet +// representative page ("시트·드로어"), the single sidebar item for both. Every Story here keeps its ID and direct URL but is +// hidden from the sidebar (tag `verification`). const meta = { title: "Components/Overlay/Drawer", component: Drawer, parameters: { layout: "centered" } } satisfies Meta export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { - return ( -
- {states.map((state) => ( - {state.children}
}]} /> - ))} - - ) -} - -// Representative Story: Overview and Usage are merged into one Usage section (Basic, Form, Long Content). +// Former representative Story: kept for its ID and direct URL; hidden from the sidebar (tag `verification`) since the Sheet +// page shows the same section. export const All: Story = { + tags: ["verification"], name: "Drawer", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - - }, - { id: "form", label: "Form", children: }, - { id: "long-content", label: "Long Content", children: }, - ]} - /> - + ), } -export const Open: Story = { tags: ["verification"], render: () => } -export const Form: Story = { tags: ["verification"], render: () => } -export const LongContent: Story = { tags: ["verification"], name: "Long Content", render: () => } +export const Open: Story = { tags: ["verification"], render: () => } +export const Form: Story = { tags: ["verification"], render: () => } +export const LongContent: Story = { tags: ["verification"], name: "Long Content", render: () => } diff --git a/src/components/dropdown-menu.stories.tsx b/src/components/dropdown-menu.stories.tsx index 51e37d4..359f285 100644 --- a/src/components/dropdown-menu.stories.tsx +++ b/src/components/dropdown-menu.stories.tsx @@ -18,9 +18,12 @@ import { DropdownMenuTrigger, } from "@hm-design/ui" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { ContextMenuFull } from "./context-menu-examples" // 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. +// The representative page also shows the Context Menu specimen from ./context-menu-examples, so "팝업 메뉴" is the one +// sidebar item for both; the Context Menu Stories keep their IDs and direct URLs but are hidden from the sidebar. function Full({ defaultOpen }: { defaultOpen?: boolean }) { const [showStatus, setShowStatus] = useState(true) @@ -42,7 +45,7 @@ function Full({ defaultOpen }: { defaultOpen?: boolean }) { 설정 ⌘, - 결제 (사용 불가) + 결제 보기 @@ -78,18 +81,22 @@ const meta = { title: "Components/Overlay/Dropdown Menu", component: DropdownMen export default meta type Story = StoryObj -// Representative Story: the trigger opens the full menu (item, disabled, separator, checkbox, radio group, sub menu, destructive). +// Representative Story: the trigger opens the full menu (item, disabled, separator, checkbox, radio group, sub menu, destructive); +// right click the Context Menu area to open the same kinds of items. export const All: Story = { name: "Dropdown Menu", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - + + + + ), } diff --git a/src/components/hover-card-examples.tsx b/src/components/hover-card-examples.tsx new file mode 100644 index 0000000..0b7e4ce --- /dev/null +++ b/src/components/hover-card-examples.tsx @@ -0,0 +1,80 @@ +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui" +import { StateBoard, type BoardState } from "@/components/story-board" +import { StorySection } from "@/components/story-page" + +// Shared Hover Card specimens (Storybook only). Used by the Hover Card Stories (verification) and by the Tooltip representative +// page, which shows Tooltip and Hover Card under one sidebar item ("툴팁·호버 카드"). +// Hover Card is a Radix overlay (Portal) for a richer preview than Tooltip. The section shows real triggers (hover or focus to +// open); the verification Story starts one open. Radix owns the open / close delay and the hover bridge. + +const longText = "긴 콘텐츠는 카드 폭에서 줄바꿈되고 카드 높이만 늘어납니다. 공백 없는 긴 문자열도 폭을 밀어내지 않습니다. ".repeat(4) + +export function HoverCardPreview({ defaultOpen }: { defaultOpen?: boolean }) { + return ( + + + + 한맥 프로젝트 + + + +
+

한맥 프로젝트

+

노선과 시설물 정보를 관리하는 프로젝트입니다.

+

담당 김한맥 · 최근 수정 2026-10-02

+
+
+
+ ) +} + +function LongContent() { + return ( + + + + 긴 콘텐츠 + + + +
+

긴 콘텐츠

+

{longText}

+
+
+
+ ) +} + +// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that +// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle +// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its +// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. +// `columns` lets a shared page keep the column track of its other sections. +function SpecimenGrid({ states, columns = 4, hideSpecimenLabels }: { states: BoardState[]; columns?: 4 | 5; hideSpecimenLabels?: boolean }) { + return ( +
header]:sr-only" : ""}`}> + {states.map((state) => ( + {state.children}
}]} /> + ))} + + ) +} + +// The Hover Card section of a representative page: Overview and Usage merged into one Variants section (Basic, Long Content). +// On a shared page `title` names the section after the component (as "Alert Dialog" on the Dialog page), `columns` follows the +// page's track and `idPrefix` keeps the specimen ids (data-overview-state) unique. +export function HoverCardSections({ title = "Variants", idPrefix = "", columns = 4, hideSpecimenLabels = false }: { title?: string; idPrefix?: string; columns?: 4 | 5; hideSpecimenLabels?: boolean }) { + return ( + + }, + { id: `${idPrefix}long-content`, label: "Long Content", children: }, + ]} + /> + + ) +} diff --git a/src/components/hover-card.stories.tsx b/src/components/hover-card.stories.tsx index 50ae7ca..da1acb8 100644 --- a/src/components/hover-card.stories.tsx +++ b/src/components/hover-card.stories.tsx @@ -1,83 +1,26 @@ import type { Meta, StoryObj } from "@storybook/react-vite" -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hm-design/ui" -import { StateBoard, type BoardState } from "@/components/story-board" -import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { HoverCard } from "@hm-design/ui" +import { StoryPage, StoryPageHeader } from "@/components/story-page" +import { HoverCardPreview, HoverCardSections } from "./hover-card-examples" -// 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. - -const longText = "긴 콘텐츠는 카드 폭에서 줄바꿈되고 카드 높이만 늘어납니다. 공백 없는 긴 문자열도 폭을 밀어내지 않습니다. ".repeat(4) - -function Preview({ defaultOpen }: { defaultOpen?: boolean }) { - return ( - - - - 한맥 프로젝트 - - - -
-

한맥 프로젝트

-

노선과 시설물 정보를 관리하는 프로젝트입니다.

-

담당 김한맥 · 최근 수정 2026-10-02

-
-
-
- ) -} - -function LongContent() { - return ( - - - - 긴 콘텐츠 - - - -
-

긴 콘텐츠

-

{longText}

-
-
-
- ) -} +// Hover Card is a Radix overlay (Portal) for a richer preview than Tooltip. The specimens live in ./hover-card-examples and +// are also shown on the Tooltip representative page ("툴팁·호버 카드"), the single sidebar item for both. Every Story here +// keeps its ID and direct URL but is hidden from the sidebar (tag `verification`). const meta = { title: "Components/Overlay/Hover Card", component: HoverCard, parameters: { layout: "centered" } } satisfies Meta export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { - return ( -
- {states.map((state) => ( - {state.children}
}]} /> - ))} - - ) -} - -// Representative Story: Overview and Usage are merged into one Variants section (Basic, Long Content). +// Former representative Story: kept for its ID and direct URL; hidden from the sidebar (tag `verification`) since the Tooltip +// page shows the same section. export const All: Story = { + tags: ["verification"], name: "Hover Card", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - - }, - { id: "long-content", label: "Long Content", children: }, - ]} - /> - + ), } @@ -87,7 +30,7 @@ export const Open: Story = { parameters: { layout: "fullscreen" }, render: () => (
- +
), } diff --git a/src/components/menubar.stories.tsx b/src/components/menubar.stories.tsx index 0686195..ee9b7cc 100644 --- a/src/components/menubar.stories.tsx +++ b/src/components/menubar.stories.tsx @@ -37,7 +37,7 @@ function Specimen() { 열기 ⌘O - 저장 (사용 불가) + 저장 내보내기 diff --git a/src/components/popover.stories.tsx b/src/components/popover.stories.tsx index 2184eee..afec354 100644 --- a/src/components/popover.stories.tsx +++ b/src/components/popover.stories.tsx @@ -75,15 +75,12 @@ const meta = { title: "Components/Overlay/Popover", component: Popover, paramete export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { +// Keep the board's accessible headings and shared indent, with only the triggers visible in the four-column grid. +function SpecimenGrid({ states }: { states: BoardState[] }) { return ( -
+
{states.map((state) => ( - {state.children}
}]} /> + {state.children}
}]} /> ))} ) diff --git a/src/components/sheet.stories.tsx b/src/components/sheet.stories.tsx index 22a3a3e..0f45349 100644 --- a/src/components/sheet.stories.tsx +++ b/src/components/sheet.stories.tsx @@ -4,6 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite" import { Button, Input, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@hm-design/ui" import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { DrawerSections } from "./drawer-examples" // 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. @@ -94,21 +95,20 @@ const meta = { title: "Components/Overlay/Sheet", component: Sheet, parameters: export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { +// Keep the board's accessible headings and shared indent, with only the triggers visible in the four-column grid. +function SpecimenGrid({ states }: { states: BoardState[] }) { return ( -
+
{states.map((state) => ( - {state.children}
}]} /> + {state.children}
}]} /> ))} ) } // Representative Story: the common Story page. Overview is merged into Variants (Right is its first item). +// 2026-11-13 (user request): it also shows the Drawer section from ./drawer-examples, so "시트·드로어" is the one sidebar item +// for both edge panels; the Drawer Stories keep their IDs and direct URLs but are hidden from the sidebar. export const All: Story = { name: "Sheet", parameters: { layout: "fullscreen", controls: { disable: true } }, @@ -133,6 +133,7 @@ export const All: Story = { ]} /> + ), } diff --git a/src/components/tabs.stories.tsx b/src/components/tabs.stories.tsx index 0ec7b3f..bf248c7 100644 --- a/src/components/tabs.stories.tsx +++ b/src/components/tabs.stories.tsx @@ -9,7 +9,7 @@ import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // Representative "Tabs" Story shows default Primary, explicit API comparisons and Secondary; Legacy stays verification-only. -// D-01 applies to every Fill specimen: flat panels use the actual parent surface and padding 24px 0. +// Fill panels use the actual parent surface without a nested box. The representative page sets its own content padding. // D-02 is shipped by the public Tabs theme mapping (Primary top White alpha .8). Legacy stays compatibility-only. type Variant = "primary" | "secondary" @@ -21,6 +21,8 @@ const variantProps = { secondary: { listProps: { fill: true, panelSurface: "surface" }, surface: "bg-card" }, } as const +const previewPanelSpacing = "px-4 pt-3 pb-6" + function Specimen({ variant = "primary", defaultValue = "menu2", @@ -28,6 +30,7 @@ function Specimen({ count = 3, labels, listProps, + panelSpacing = "px-0 py-6", }: { variant?: Variant defaultValue?: string @@ -35,6 +38,7 @@ function Specimen({ count?: number labels?: string[] listProps?: ListProps + panelSpacing?: string }) { const { listProps: defaults, surface } = variantProps[variant] const effectiveListProps: ListProps = listProps ?? defaults @@ -51,7 +55,7 @@ function Specimen({ ))} {items.map(({ value }, i) => ( - + 메뉴 {i + 1} 콘텐츠 ))} @@ -67,13 +71,16 @@ function SurfaceUsage({ inCard = false }: { inCard?: boolean }) { const id = useId() const [project, setProject] = useState("") const [purpose, setPurpose] = useState("") + // 2026-11-13: both examples use the same panel inset (16 / 12 / 24). In the Card, the Card / CardContent padding stays as is; + // the extra inset applies only to the panel below the tab header, so the header keeps its size and position. + const panelSpacing = previewPanelSpacing const tabs = ( 기본 정보 일정·산출물 - +
@@ -88,7 +95,7 @@ function SurfaceUsage({ inCard = false }: { inCard?: boolean }) { {/* 2026-10-23: this panel can be narrower than the Table default 520px minimum (--hm-table-min-width), which made the table container scroll. The documented token is reset for this panel only: the table follows the panel width and long cell text wraps (Table overflow-wrap). */} - + 산출물마감일화면 설계서10월 14일 @@ -127,17 +134,18 @@ function LongLabelSpecimens() { ) } -// Equal two columns on PC (one column below 900px). Each board holds a single specimen: StateBoard's own auto-fill grid -// would leave an empty third column for two items, so `min` is forced above any column width (same approach as Button). +// 2026-11-13 (user request): Variants, Behavior and Usage share one grid, equal two columns (24px gap) when the section's +// actual content width is 720px or more, one column below. The breakpoint is a container query on the wrapper (its content box, +// after the common board indent), so it follows the Storybook preview's usable width, not the iframe viewport. +// Each board holds a single specimen: StateBoard's own auto-fill grid would leave an empty column for two items, so `min` is +// forced above any column width (same approach as Button). A single child (Behavior) takes the left column; `min-w-0` lets the +// Overflow Tabs keep its own internal scroll (previous / next buttons) inside its column instead of widening the page. const TwoColumns = ({ children }: { children: ReactNode }) => ( -
{children}
+
+
{children}
+
) const SingleBoard = ({ states }: { states: BoardState[] }) => -// Variants: equal three columns on PC (iframe width 1000px and up); fewer columns below that (240px minimum). `min-w-0` lets the -// Overflow Tabs keep its own internal scroll (previous / next buttons) inside its column instead of widening the page. -const ThreeColumns = ({ children }: { children: ReactNode }) => ( -
{children}
-) const meta = { title: "Components/Navigation/Tabs", @@ -147,7 +155,7 @@ const meta = { docs: { description: { component: - "기본 TabsList는 Primary(fill=true, panelSurface=\"content\")입니다. 대표 Story는 기본 Primary, Secondary(fill + panelSurface=\"surface\"), Primary Overflow를 비교하며 명시적 Primary는 숨김 Default API에서 확인합니다. 모든 Fill 예제는 중첩 박스 없는 TabsContent(px-0 py-6)를 사용하며 Usage는 Card 내부 surface와 페이지 content 위의 조합입니다. panelSurface는 List의 선택 면만 바꾸므로 실제 상위 배경에 맞춰 선택합니다. Primary 선택 탭의 gradient는 상단 White alpha 0.8 → content이며 전체 opacity는 1입니다. Secondary는 기존 불투명 White를 유지합니다. Legacy non-fill은 fill={false}로 유지하며 숨김 Compatibility에서 확인합니다. 높이 34, 좌우 16, 상단 radius 4, 16/24/700과 8px 곡선 연결은 기존 값 그대로입니다. Hover는 비선택에만 Black 20%이며 넘칠 때만 이전/다음 이동 버튼이 나타납니다. aria-label과 연결된 입력 Label을 제공하고, 방향키·Home/End·포커스·Disabled·모바일 내부 스크롤을 확인합니다. 곡선 반경 8px과 비선택 합성색의 과거 REVIEW / TEMPORARY 이력은 유지합니다.", + "기본 TabsList는 Primary(fill=true, panelSurface=\"content\")입니다. 대표 Story는 Variants에서 기본 Primary와 Secondary(fill + panelSurface=\"surface\")를 비교하고 Behavior에서 Primary Overflow를 확인하며 명시적 Primary는 숨김 Default API에서 확인합니다. 대표 Story의 평면 패널은 좌우 16px, 상단 12px, 하단 24px 여백을 사용합니다. Usage의 Card 예제도 기존 Card 여백은 그대로 두고 탭 헤더 아래 패널에 같은 좌우 16px, 상단 12px, 하단 24px 여백을 적용합니다. panelSurface는 List의 선택 면만 바꾸므로 실제 상위 배경에 맞춰 선택합니다. Primary 선택 탭의 gradient는 상단 White alpha 0.8 → content이며 전체 opacity는 1입니다. Secondary는 기존 불투명 White를 유지합니다. Legacy non-fill은 fill={false}로 유지하며 숨김 Compatibility에서 확인합니다. 높이 34, 좌우 16, 상단 radius 4, 16/24/700과 8px 곡선 연결은 기존 값 그대로입니다. Hover는 비선택에만 Black 20%이며 넘칠 때만 이전/다음 이동 버튼이 나타납니다. aria-label과 연결된 입력 Label을 제공하고, 방향키·Home/End·포커스·Disabled·모바일 내부 스크롤을 확인합니다. 곡선 반경 8px과 비선택 합성색의 과거 REVIEW / TEMPORARY 이력은 유지합니다.", }, }, }, @@ -159,16 +167,19 @@ export const All: Story = { name: "Tabs", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( - // The common compact Story page (16–24px gutter, 24px section gap) fits this page on a 900px-high PC screen without an override. + // The common compact Story page (16–24px gutter, 28px section gap) fits this page on a 900px-high PC screen without an override. - {/* The former States section (Overflow) is the third Variants item. */} - - }]} /> - }]} /> - }]} /> - + + }]} /> + }]} /> + + + + + }]} /> + diff --git a/src/components/tooltip.stories.tsx b/src/components/tooltip.stories.tsx index e01ed61..81f4c8f 100644 --- a/src/components/tooltip.stories.tsx +++ b/src/components/tooltip.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite" import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hm-design/ui" import { StateBoard, type BoardState } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { HoverCardSections } from "./hover-card-examples" // 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. @@ -67,21 +68,21 @@ const meta = { title: "Components/Overlay/Tooltip", component: Tooltip, paramete export default meta type Story = StoryObj -// Representative-page layout (Storybook presentation only): equal columns on PC (iframe width 1000px and up), fewer below that -// (200px minimum). Each specimen sits in its own single-column StateBoard (min larger than any column), so every subtitle and divider -// has the width of its column; a section with fewer specimens than columns leaves the remaining columns empty. The trigger keeps its -// own size (the wrapper div stretches, the inline-flex trigger does not). Overlays open in a portal, so this layout never affects them. -function SpecimenGrid({ states, columns = 4 }: { states: BoardState[]; columns?: 4 | 5 }) { +// Keep the board's accessible headings and shared indent, with only the triggers visible in the five-column grid. +function SpecimenGrid({ states }: { states: BoardState[] }) { return ( -
+
{states.map((state) => ( - {state.children}
}]} /> + {state.children}
}]} /> ))} ) } // Representative Story: Overview is merged into Variants (Basic is its first item, then the four sides): 5 columns on PC. +// 2026-11-13 (user request): it also shows the Hover Card section from ./hover-card-examples (same 5-column track), so +// "툴팁·호버 카드" is the one sidebar item for both hover previews; the Hover Card Stories keep their IDs and direct URLs but +// are hidden from the sidebar. export const All: Story = { name: "Tooltip", parameters: { layout: "fullscreen", controls: { disable: true } }, @@ -91,7 +92,6 @@ export const All: Story = { }, { id: "top", label: "Top", children: }, @@ -103,13 +103,13 @@ export const All: Story = { }, { id: "disabled", label: "Disabled", children: }, ]} /> +
),